Asset Tracking

Using the SDKs

This topic explains how to get started using the Ably Asset Tracking SDKs.

Supported platforms

There are two Asset Tracking SDKs, one for publishing and one for subscribing. The following platforms are supported:

  • Android (Java and Kotlin) – publisher and subscriber SDK
  • iOS (Objective-C and Swift) – publisher and subscriber SDK
  • Web (JavaScript, with first class TypeScript support) – subscriber SDK

SDK repositories

The SDKs can be found in the following GitHub repositories:

Prerequisites

You need to have a suitable development environment installed, for example:

  • Android – Android Studio or Gradle (requires Android SDK to be installed)
  • iOS – Xcode
  • JavaScript – any suitable environment of your choice

You also need to have suitable credentials for the various SDK components:

  • ABLY_API_KEY – Your Ably API key
  • MAPBOX_ACCESS_TOKEN – Mapbox public key
  • MAPBOX_DOWNLOADS_TOKEN – Mapbox private key

On Android development systems you can set these values in your ~/.gradle/gradle.properties file.

Installing the SDK

You can find information on installing the Ably Asset Tracking SDKs in the following resources:

Authentication

The client requires authentication in order to establish a connection with Ably. There are three methods that can be used:

1. Basic authentication
2. Token authentication
3. JWT authentication

Usually a client will use either token or JWT authentication, as basic authentication would require exposing the API keys on the client.

Examples of establishing a connection using the three methods are given in the following sections. While the examples shown are for either the Publishing or Subscribing SDK, you can use the same approach for both SDKs.

Basic Authentication

The following example demonstrates establishing a connection using basic authentication:

val publisher = Publisher.publishers() // get the Publisher builder in default state
  .connection(ConnectionConfiguration(Authentication.basic(CLIENT_ID, ABLY_API_KEY)))
const subscriber = new Subscriber({ key: 'ABLY_API_KEY' })

This method should not be used on a client however, as it exposes the API key.

You can read more about basic authentication in our documentation.

Token Authentication

The following example demonstrates establishing a connection using token authentication:

val publisher = Publisher.publishers() // get the Publisher builder in default state
    .connection(ConnectionConfiguration(Authentication.tokenRequest(CLIENT_ID) { requestParameters ->
        // get TokenRequest from your server
        getTokenRequestFromAuthServer(requestParameters); // customer implements this function
        }))
/* authURL is the endpoint for your authentication server. It returns either
  a `TokenRequest` or a `Token` */
const subscriber = new Subscriber({
  authUrl: 'http://my.website/auth',
  clientId: 'CLIENT_ID'
})

You can read more about token authentication in our documentation.

JWT Authentication

The following example demonstrates establishing a connection using JWT authentication:

val publisher = Publisher.publishers() // get the Publisher builder in default state
  .connection(ConnectionConfiguration(Authentication.jwt(CLIENT_ID) { tokenParameters ->
        // get JWT from your server
        getJWTFromAuthServer(tokenParameters); // customer implements this function
        }))
// authURL is the endpoint for your authentication server. It returns a JWT
const subscriber = new Subscriber({
  authUrl: 'http://my.website/auth',
  clientId: 'CLIENT_ID'
})

You can read more about JWT authentication in our documentation.

The client requires authentication in order to establish a connection with Ably. Currently, the Swift SDK only supports basic authentication: you authenticate with your Ably API key (available in your account dashboard) and can optionally identify the client with a client ID. The following example demonstrates how to achieve this:

let publisher = try PublisherFactory.publishers() // get a Publisher builder
.connection(ConnectionConfiguration(apiKey: ABLY_API_KEY,
                                    clientId: CLIENT_ID))
/* Any additional configuration */
.start()

Using the Publishing SDK

Common operations you need to carry out on the publisher include:

  • Initialize the publisher.
  • Start tracking an asset.
  • Stop tracking an asset.
  • Set the resolution constraints on an asset.

Initializing the Publisher

During initialization of the publisher various methods can be called to configure the Builder interface of the Publisher.

The required methods are:

connection
Called to provide Ably connection information, such as API keys, and any other configuration parameters as needed.
map
Called to provide Mapbox configuration, such as API keys, any other configuration parameters as needed.
androidContext
Called to provide the Android runtime context (on Android only).
resolutionPolicy
Sets the policy factory to be used to define the target resolution for publishers created from this builder.
backgroundTrackingNotificationProvider
Sets the notification that will be displayed for the background tracking service. Please note that this notification will be removed when you call the stop method (on Android only).
start
Creates a Publisher and starts publishing. The returned publisher instance does not start in a state whereby it is actively tracking anything. If tracking is required from the outset then the Publisher.track or Publisher.add method must be subsequently called. In order to detect the device’s location ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION permission must be granted.

The optional methods are:

profile
Called to set the means of transport being used for the initial state of publishers created from this builder. If not set then the default value is RoutingProfile.DRIVING.
locationSource
Sets the location source to be used instead of the GPS. The location source provides location updates for the Publisher.
logHandler
Sets the log handler (experimental API).
rawLocations
Enables sending of raw location updates. This should only be enabled for diagnostics. In the production environment this should be always disabled. By default this is disabled (experimental API).
sendResolution
Enables sending of calculated resolutions. By default this is enabled.
rawHistoryDataCallback
Specifies a callback that will be called with the filepath of raw history data from the Navigation SDK component. This will be probably removed in the future. Do not use this in the production environment (experimental API).
constantLocationEngineResolution
Enables using a constant location engine resolution. If the resolution is not null then instead of using ResolutionPolicy to calculate a dynamic resolution for the location engine the resolution will be used as the location engine resolution. If the resolution is null then the constant resolution is disabled and the location engine resolution will be calculated by the ResolutionPolicy. By default this is disabled.
vehicleProfile
Set the type of vehicle being used by the publisher user. If not set then the default value is VehicleProfile.CAR.

Other publisher methods of note are:

track
Adds a Trackable object and makes it the actively tracked object, meaning that the state of the active field will be updated to this object, if that wasn’t already the case. If this object was already in this publisher’s tracked set then this method only serves to change the actively tracked object. This method returns a StateFlow that represents the TrackableState of the added Trackable.
add
Adds a Trackable object, but does not make it the actively tracked object, meaning that the state of the active field will not change. If this object was already in this publisher’s tracked set then this method does nothing. This method returns a StateFlow that represents the TrackableState of the added Trackable.
remove
Removes a Trackable object if it is known to this publisher, otherwise does nothing and returns false. If the removed object is the current actively active object then that state will be cleared, meaning that for another object to become the actively tracked delivery then the track method must be subsequently called.
getTrackableState
Returns a trackable state flow representing the TrackableState for an already added Trackable.
stop
Stops this publisher from publishing locations. Once a publisher has been stopped, it cannot be restarted. Please note that calling this method will remove the notification provided by Builder.backgroundTrackingNotificationProvider.

Publisher properties of note:

active
The actively tracked object, being the Trackable object whose destination will be used for location enhancement, if available. This state can be changed by calling the track method.
routingProfile
The active means of transport for this publisher.
locations
The shared flow emitting location values when they become available.
trackables
The shared flow emitting all trackables tracked by the publisher.
locationHistory
The shared flow emitting trip location history when it becomes available.

In the following sections you will learn how to set up some resolution constraints and then start publishing.

The following code example creates some example resolution constraints:

// Prepare Resolution Constraints for an asset that will be used in the Resolution Policy
val exampleConstraints = DefaultResolutionConstraints(
    DefaultResolutionSet( // this constructor provides one Resolution for all states
        Resolution(
            accuracy = Accuracy.BALANCED,
            desiredInterval = 1000L,
            minimumDisplacement = 1.0
        )
    ),
    proximityThreshold = DefaultProximity(spatial = 1.0),
    batteryLevelThreshold = 10.0f,
    lowBatteryMultiplier = 2.0f
)
let resolution = Resolution(accuracy: .balanced,
                            desiredInterval: 1000,
                            minimumDisplacement: 1.0)
let resolutions = DefaultResolutionSet(resolution: resolution)
let proximityThreshold = DefaultProximity(spatial: 1)
let exampleConstraints = DefaultResolutionConstraints(resolutions: resolutions,
                                              proximityThreshold: proximity,
                                              batteryLevelThreshold: 10,
                                              lowBatteryMultiplier: 2)

The next step is create a default resolution to be used:

// Prepare the default resolution for the Resolution Policy
val defaultResolution = Resolution(Accuracy.BALANCED,
                                   desiredInterval = 1000L,
                                   minimumDisplacement = 1.0)
// Prepare the default resolution for the Resolution Policy
let defaultResolution = Resolution(accuracy: .balanced,
                            desiredInterval: 1000,
                            minimumDisplacement: 1.0)

Once these are created you can then initialize the publisher with the constraints and default resolution, and start the publisher:

// Initialize and Start the Publisher
val publisher = Publisher.publishers() // get the Publisher builder in default state
  // Required configuration
  .connection(ConnectionConfiguration(Authentication.basic(CLIENT_ID, ABLY_API_KEY))) // provide Ably configuration with credentials
  .map(MapConfiguration(MAPBOX_ACCESS_TOKEN)) // provide Mapbox configuration with credentials
  .androidContext(this) // provide Android runtime context
  .resolutionPolicy(DefaultResolutionPolicyFactory(defaultResolution, this)) // provide either the default resolution policy factory or your custom implementation
  .backgroundTrackingNotificationProvider(
    object : PublisherNotificationProvider {
      override fun getNotification(): Notification {
          // TODO: create the notification for location updates background service
      }
    },
    NOTIFICATION_ID
  )
  // Optional configuration
  .profile(RoutingProfile.DRIVING) // provide mode of transportation for better location enhancements
  .logHandler(object : LogHandler {
      override fun logMessage(level: LogLevel, message: String, throwable: Throwable?) {
        // TODO: log the message to internal or external loggers
      }
    })
  .rawLocations(false) // send raw location updates to subscribers
  .sendResolution(true) // send calculated trackable network resolution to subscribers
  .constantLocationEngineResolution(constantLocationEngineResolution) // provide a constant resolution for the GPS engine
  .vehicleProfile(VehicleProfile.CAR) // provide vehicle type for better location enhancements
  .locationSource(LocationSourceRaw.create(historyData)) // use an alternative location source for GPS locations
  // Create and start the publisher
  .start()
// Initialise and start the Publisher
let publisher = try PublisherFactory.publishers() // get a Publisher Builder
  .connection(ConnectionConfiguration(apiKey: ABLY_API_KEY,
                                      clientId: CLIENT_ID)) // provide Ably configuration with credentials
  .log(LogConfiguration()) // provide logging configuration
  .transportationMode(TransportationMode()) // provide mode of transportation for better location enhancements
  .delegate(self) // provide delegate to handle location updates locally if needed
  .start()

Start tracking

You can start tracking an asset (a Trackable), by calling the track or add method of the publisher. A Trackable is composed of the following:

trackingID
The tracking identifier for the asset.
destination
A Destination object, which is a latitude and longitude.
constraints
A set of resolution constraints.

The track method adds a Trackable object, and makes it the actively tracked object, meaning that the state of the active field will be updated to this object, if that wasn’t already the case. If this object was already in this publisher’s tracked set, then this method only serves to change the actively tracked object. Takes a trackable as a parameter, which is the object to be added to this publisher’s tracked set, if it’s not already there, and which will be made the actively tracked object.

The add method adds a Trackable object, but does not make it the actively tracked object, meaning that the state of the active field will not change. If this object was already in this publisher’s tracked set then this method does nothing. Takes a trackable as a parameter, which is the object to be added to this publisher’s tracked set, if it’s not already there.

Both of these methods return a StateFlow that represents the TrackableState of the added Trackable.

You can start tracking an asset by calling the track method of the publisher. You need to supply the tracking identifier of the asset to be tracked and the completion handler.

The following code example demonstrates how to start tracking an asset:

// Start tracking an asset
try {
    publisher.track(
        Trackable(
            trackingId, // provide a tracking identifier for the asset
            destination, // provide a destination as a latitude and longitude
            constraints = exampleConstraints // provide a set of Resolution Constraints
        )
    )
    // TODO handle asset tracking started successfully
} catch (exception: Exception) {
    // TODO handle asset tracking could not be started
}
// Start tracking an asset with its tracking ID
publisher.track(trackable: trackable) { [weak self] result in
    switch result {
    case .success:
        self?.trackables = [trackable]
        logger.info("Initial trackable tracked successfully.")
    case .failure(let error):
        logger.info("Unable to track trackable.")
    }
}

Stop tracking

You can stop tracking a trackable (asset) that is registered with the publisher using the remove method, as shown in the following code:

publisher.remove(trackable)
publisher.remove(trackable)

Using the Subscribing SDK

Common operations you will need to carry out on the subscriber include:

  • Initialize the subscriber.
  • Listen for location updates sent from from the publisher.
  • Listen for asset status updates sent from the publisher.
  • Request a different resolution to be sent from the publisher.

Initializing the Subscriber

During initialization of the subscriber various methods can be called to configure the Subscriber.

The required methods are:

connection
Called to provide Ably connection information, such as API keys, and any other configuration parameters as needed.
trackingId
Sets the asset to be tracked, using the unique tracking identifier of the asset.

The optional methods are:

resolution
Request a specific resolution of updates to be requested from the remote publisher.

The following code example demonstrates initializing and starting the subscriber:

Initialize the Subscriber with a ClientOptions object. You can also optionally configure event handlers for location updates and asset state changes during subscriber initialization.

The following code example demonstrates initializing and starting the subscriber:

// Initialize and Start the Subscriber
val subscriber = Subscriber.subscribers() // Get an AssetSubscriber
    // Required configuration
    .connection(ConnectionConfiguration(Authentication.basic(CLIENT_ID, ABLY_API_KEY))) // provide Ably configuration with credentials
    .trackingId(trackingId) // provide the tracking identifier for the asset that needs to be tracked
    // Optional configuration
    .resolution( // request a specific resolution to be considered by the publisher
      Resolution(Accuracy.MAXIMUM, desiredInterval = 1000L, minimumDisplacement = 1.0)
    )
    .logHandler(object : LogHandler {
      override fun logMessage(level: LogLevel, message: String, throwable: Throwable?) {
        // TODO: log the message to internal or external loggers
      }
    })
    // Create and start the subscriber
    .start() // start listening for updates
// Initialize and start the subscriber
let subscriber = SubscriberFactory.subscribers()  // get a Subscriber builder
    .connection(ConnectionConfiguration(apiKey: ABLY_API_KEY,
                                        clientId: CLIENT_ID))  // connect to Ably
    .trackingId(trackingId)   // provide a Tracking ID for the asset to be tracked
    .routingProfile(.driving) // provide a routing profile for better location enhancements
    .log(LogConfiguration())  // provide logging configuration
    .resolution(Resolution(accuracy: .maximum,
                           desiredInterval: 10000,
                           minimumDisplacement: 500)) // request a specific resolution from the publisher
    .delegate(self) // provide a delegate to handle received location updates
    .start() // start listening to updates
// Initialize the Subscriber
import { Subscriber, Accuracy } from '@ably/asset-tracking';

const ablyOptions = {
  key: 'ABLY_API_KEY',
  clientId: 'CLIENT_ID',
}

const subscriber = new Subscriber({
  ablyOptions,
  onLocationUpdate,   // optional
  onStatusUpdate,     // optional
})

// Start the subscriber, specifying the tracking identifier of the asset
await subscriber.start(trackingId)

Subscribe to updates

You can subscribe to updates from the publisher, specifying a function that is called when each update is received. This is shown in the following example:

// Listen for location updates
subscriber.locations
    .onEach { } // provide a function to be called when enhanced location updates are received
    .launchIn(scope) // coroutines scope on which the locations are received
// Override subscriber method of SubscriberDelegate to be notified of location updates
class MySubscriberDelegate: SubscriberDelegate {
  ...
  override func subscriber(sender: Subscriber, didUpdateEnhancedLocation location: CLLocation) {
    print("Location update received. Coordinates: \(location.coordinate)");
  }
  ...
}
// Listen for location updates
subscriber.onLocationUpdate((locationUpdate) => {
  console.log(
    `Location update received. Coordinates: ${locationUpdate.location.geometry.coordinates}`
  )
})

Note that you can also configure the update event handler during subscriber initialization.

Subscribe to asset state changes

You can subscribe to asset state changes from the publisher, specifying a function that is called when each state change is received. This is shown in the following example:

// Listen for asset state changes
subscriber.trackableStates
    .onEach { } // provide a function to be called when the asset changes its state
    .launchIn(scope) // coroutines scope on which the statuses are received
// Listen for asset state changes
subscriber.onLocationUpdate((isOnline) => {
  console.log(
    `Status update: Publisher is now ${isOnline ? 'online' : 'offline'}`
  )
})

To listen to asset state change events from the publisher, you must provide a class that implements some or all of the methods in SubscriberDelegate:

class MySubscriberDelegate: SubscriberDelegate {
  // Implement some or all of the delegate methods
  override func subscriber(sender: Subscriber, didChangeAssetConnectionStatus status: ConnectionState) {
      /* Handle the change */
  }

  override func subscriber(sender: Subscriber, didFailWithError error: ErrorInformation) {
      /* Handle the error */
  }

  override func subscriber(sender: Subscriber, didUpdateEnhancedLocation location: CLLocation) {
      /* Handle the location update */
  }
}

You can achieve this by using one of the following approaches:

  • Create a separate class that implements the required SubscriberDelegate, as shown in the example above. Reference that class by either:
    • Setting subscriber.delegate = MySubscriberDelegate() somewhere in your code.
    • Passing this class to the SubscribeFactory.delegate() method.
  • Implement the SubscriberDelegate methods in your current class and specify subscriber.delegate=self.
  • Implement the SubscriberDelegate methods as an extension.

Note that you can also configure the asset state change event handler during subscriber initialization.

Request a different resolution

The subscriber can always request a different resolution preference by calling the resolutionPreference method, passing in the required Resolution. This is shown in the following example:

The subscriber can always request a different resolution preference by calling the resolutionPreference method, passing in the required Resolution and the completion handler. This is shown in the following example:

The subscriber can request a different resolution by calling the sendChangeRequest method, passing in the required accuracy, desiredInterval, and minimumDisplacement property values. This is shown in the following example:

// Request a different resolution when needed.
try {
    subscriber.resolutionPreference(Resolution(Accuracy.MAXIMUM, desiredInterval = 100L, minimumDisplacement = 2.0))
    // TODO change request submitted successfully
} catch (exception: Exception) {
    // TODO change request could not be submitted
}
subscriber?.resolutionPreference(resolution: resolution) { [weak self] result in
    switch result {
    case .success:
        self?.currentResolution = resolution
        self?.updateResolutionLabel()
        logger.info("Updated resolution to: \(resolution)")
    case .failure(let error):
        let errorDescription = DescriptionsHelper.ResolutionStateHelper.getDescription(for: .changeError(error))
        self?.showErrorDialog(withMessage: errorDescription)
    }
}
await subscriber.sendChangeRequest({
  accuracy: Accuracy.Low,
  desiredInterval: 3000,
  minimumDisplacement: 5,
})

Stop receiving updates

Stop receiving updates from the publisher by calling the stop() method:

await subscriber.stop()

See also


Need help?

If you need any help with your implementation or if you have encountered any problems, do get in touch. You can also quickly find answers from our knowledge base, and blog.