Devices and services
Write the device configuration, device manager and probes for a sampling package that reads a wearable or an online service.
Some measures run outside the phone: on a Bluetooth Low Energy (BLE) wearable or on an online service, such as a web API. A sampling package for such measures defines the device for the study protocol.
The package also provides a device manager: the object that CARP Mobile Sensing (CAMS) uses to configure, connect and monitor the device. The package's probes read their data through the device manager.
The example package needs none of this: its measures run on the phone, and SmartphoneSamplingPackage supplies the phone's device manager. To continue with a package whose measures run on the phone, see Privacy and data formats.
The examples on this page are two small packages of their own. The pollen web service is built like WeatherService in the context package, and the BLE thermometer like the devices of the Polar package. Both use the namespace com.example; use a namespace that you control.
Choose the base classes
Each kind of device has its own base classes in CAMS:
| Device | Configuration | Registration | Device manager | Example |
|---|---|---|---|---|
| Online service, or a service on the phone | ServiceConfiguration | ServiceRegistration | ServiceManager | WeatherService in the context package, HealthService in the health package |
| BLE device | BLEDevice | BLEDeviceRegistration or a subclass | BLEDeviceManager | PolarDevice in the Polar package, ESenseDevice in the eSense package |
| Other hardware with a battery | CamsDevice | HardwareDeviceRegistration or a subclass | HardwareDeviceManager | None in the CARP packages |
| The phone's own sensors | None | None | None: extend SmartphoneSamplingPackage | The example package |
Class names in CAMS 1.x
CAMS 1.x called these classes OnlineService, OnlineServiceManager and BTLEDeviceManager. CAMS 2.x has none of them: use ServiceConfiguration, ServiceManager and BLEDeviceManager.
Define the device configuration
A device configuration describes a device's role in a study protocol: its type, its role name and its settings. The pollen service's configuration holds the key for the service's API:
- Device type:
CamsDevicebuilds the JSON type from the class name, heredk.carp.cams.devices.PollenService. CAMS finds the device manager for a device in the protocol by this string. ADEVICE_TYPEthat differs from it makes CAMS skip the device, with the warning "A device of type 'dk.carp.cams.devices.PollenService' is not available on this device". - Class name: keep it stable and distinctive. Saved protocols and deployments refer to the type, so renaming the class breaks them. CAMS also keeps only the first device manager registered for a type.
- Role name: names the device in a protocol and must be unique there. CAMS stores the device's registration under it.
- Default values: qualify them with the class name, as in
PollenService.DEFAULT_ROLE_NAME.json_serializablecopies a default into the generated code, where an unqualified name does not compile. - Settings: put them in fields that the device manager or the probes read, such as
apiKey. CAMS ignores thedefaultSamplingConfigurationof a connected device, so set a measure's configuration on the measure itself. See How configuration lookup works.
Settings are stored in plain text
The configuration's JSON holds every field in plain text, API keys included. CAMS saves it with the protocol and the deployment on the phone, and CARP Web Services (CAWS) keeps the protocols that are uploaded to it. Use a key that is restricted to the study and that you can revoke.
Write the device manager
The device manager configures and connects the device on the phone. ServiceManager and HardwareDeviceManager extend DeviceManager, and BLEDeviceManager extends HardwareDeviceManager, which adds the battery level. Extend ServiceManager for a service and BLEDeviceManager for a BLE device, and implement these members:
| Member | ServiceManager | BLEDeviceManager | When CAMS uses it |
|---|---|---|---|
displayName | Implement | Implemented as bleName ?? bleAddress | Apps show it, and createRegistration() can use it. |
canConnect | Implement | Implement | CAMS connects the device when it deploys or restores the study, only if this is true. |
onConfigure() | Implement | Implemented; call super if you override it | Runs once, synchronously, when CAMS configures the manager. Keep it light. |
createRegistration() | Implement | Implement | CAMS calls it about 5 seconds after each change to connected and stores the result in the deployment. |
onHasPermissions() | Optional, default true | Implemented; call super if you override it | connect() checks it first and sets disconnected when it is false. |
onRequestPermissions() | Implement | Implemented; call super if you override it | Runs when the app calls requestPermissions(). CAMS never calls it. |
onConnect() | Implement | Implement | connect() sets the status that it returns. |
onDisconnect() | Implement | Implement | Runs when the app calls disconnect(), after CAMS has paused the device's tasks. |
batteryLevel | None | Implement | Read only by your createRegistration(). |
Do not override configure(), connect(), disconnect(), hasPermissions(), requestPermissions() or pair(). They are marked @nonVirtual, and CAMS relies on what they do around your callbacks.
The pollen service's manager creates the API client from the configuration:
PollenClient stands for the client of your service's API. The manager reports connected as soon as the client exists, and the probe handles network failures per request.
CAMS connects devices when it deploys the study and at each app start. It also connects a device when a task that targets another device, such as the phone, starts one of its measures. A service that reports disconnected stays disconnected until one of these happens, unless the app calls connect().
Read the device in a probe
A probe reaches its device through its deviceManager, which the registry sets to the package's manager. Narrow its type with a getter, and collect the data through it. The pollen package has one data class and one probe:
A MeasurementProbe fits a service that answers one request each time the task runs. The probe returns an Error measurement instead of throwing. After an exception, a MeasurementProbe does not pause itself, and its task ignores the next trigger.
The probe also reports a missing PollenService: every probe gets its package's manager, also when the protocol has no such device.
CAMS creates and initializes the probes before it connects the devices, so a probe sees its device in different states:
| Point | Device | deviceManager of the probe |
|---|---|---|
create() in the package | Configured, not connected | Not set |
| Probe constructor and field initializers | Configured, not connected | Not set: reading it throws a LateInitializationError |
onInitialize() | Configured, not connected | Set |
onResume() and the stream getter | Usually connected, because CAMS resumes a device's tasks when it connects; the link may drop in between | Set |
- Create a probe for every measure type: CAMS skips a measure whose
create()returnsnull, and it callscreate()before the device connects.carp_movesense_packagecreates its temperature probe only when it already knows the sensor model, which it learns after connecting, so the measure collects nothing. - Keep
onInitialize()free of device calls: a probe whoseonInitialize()returnsfalsestaysCreatedand ignores every later resume. The activity and heart rate probes ofcarp_movisens_packagereturnfalsewhile the sensor's Bluetooth service is missing, which it is until the sensor connects. - Check the device when the probe resumes: in
onResume(), or in thestreamgetter of aStreamProbe, which CAMS reads when the probe resumes. The thermometer's probe under Write a BLE device manager does this.
Expose the device in the package
The package tells CAMS which device its measures run on and gives CAMS the device manager. Implement SamplingPackage directly, as the BLE packages do:
deviceTypereturnsDEVICE_TYPE.register()registers the manager under it in theDeviceController, and CAMS matches it with the type of the device in the protocol.deviceManagerreturns the same instance every time, kept in afinalfield.register()gives that instance to the device controller, where CAMS configures and connects it, and the registry gives it to every probe. A getter that builds a new manager gives the probes one that CAMS never configures or connects.onRegister()registers the device configuration and the data class for JSON, because CAMS decodes them when it restores a saved study. For what else it registers, see Register the JSON classes and privacy functions.CamsDataTypeMetaDatalists only the measure's own runtime permissions. The permissions to reach a device, such as Bluetooth, belong to the device manager.
A package supports one device type. For several types, register one package per device type. The context package does this from its onRegister() for its location, weather and air quality services.
Packages that share a device type must share one manager instance, because the device controller keeps the first manager registered for a type.
Add the device to a protocol
App developers add the service to a protocol as a connected device of the phone and target the measure's task at the service. Show this in your package's README:
CAMS resumes a task only while its target device is connected, and resumes it when the device connects. In a task that targets the phone, a device's stream probe can start before the device connects, find no stream and miss the connection. POLLEN_INDEX is a one-time measure, so it needs a trigger that repeats it: its probe collects once each time the task runs.
Report the connection status
CAMS drives sampling on a device by the manager's status, a DeviceStatus. Set it as the connection changes:
| Status | Set by | What CAMS does |
|---|---|---|
unknown | The constructor | Nothing. connect() refuses an unconfigured manager. |
configured | configure() | Nothing. |
paired | pair() of a BLE manager | Nothing. It counts as configured. |
connecting | connect(), before it calls onConnect() | Nothing. connect() returns at once while connecting. |
connected | onConnect() or your listener | Resumes every task that targets the device, and registers the device about 5 seconds later. |
reconnected | Your listener, when a lost link is back | Resumes, 15 seconds later, the tasks that disconnecting paused. |
disconnecting | Your listener, when the link is lost | Pauses the device's tasks, marks the running ones to be resumed and sets disconnected. |
disconnected | connect() on missing permissions or an exception, and disconnect() | Nothing. |
The status setter emits only on a change, so setting connected a second time resumes nothing. A BLE connection runs like this:
sequenceDiagram
participant A as App
participant C as CAMS
participant M as Device manager
participant D as Device
A->>M: bind the device, requestPermissions()
A->>C: tryDeployment()
C->>M: configure()
Note over C: creates the probes
C->>M: connect()
M->>D: connect through the vendor SDK
M-->>C: status connecting
D-->>M: link up
M-->>C: status connected
Note over C: resumes the tasks that target the device
D-->>M: link lost
M-->>C: status disconnecting
Note over C: pauses those tasks until reconnected
- The app binds the device to the manager and asks for the device's permissions.
- When the study is deployed, CAMS configures the manager, creates the probes and calls
connect(). onConnect()starts the connection through the vendor's SDK and returnsconnecting.- When the link is up, the manager sets
connected, and CAMS resumes the tasks that target the device. - When the link is lost, the manager sets
disconnecting, and CAMS pauses those tasks until the manager setsreconnected.
Five rules follow from this:
- Return the real status from
onConnect(): when a listener sets the status later, returnstatus, not a constant.connect()sets the returned value, which overwrites what the listener set.carp_movisens_packagereturnsconnectingeven on success, and when it has to scan for the sensor, as on iOS, nothing setsconnectedlater. CAMS then never starts its tasks. - Map a lost link to
disconnectingand its return toreconnected: onlydisconnectingpauses the tasks so thatreconnectedresumes them.carp_esense_packageandcarp_movesense_packagesetdisconnectedinstead, which pauses nothing. - Set
connectedonly when the probes have what they need:carp_polar_packagesetsconnectedwhen its first data type arrives, usually heart rate. Its other probes then start before their data types are available, and find none. - Time out the SDK's connect call:
connect()does nothing while the status isconnecting. A call that never completes therefore blocks the device until a restart. On iOS,carp_esense_packagestaysconnectingfor about 83 minutes when the earbuds are missing. A timeout makesconnect()setdisconnected, so that a laterconnect()can try again. - Cancel the old listeners in
onConnect(): CAMS and apps callconnect()again after a failed attempt.carp_polar_packageandcarp_esense_packageadd new listeners at every connection, so each event is handled more than once.
Write a BLE device manager
A BLE package follows the same pattern with a BLEDevice, a registration class for the details it stores and a BLEDeviceManager. The thermometer package defines its device class and registration class like this:
ThermoDevice adds no fields, but the package needs the class anyway, because its name is the device type. CAMS does not use the scan settings of a BLEDevice, such as namePrefix: they are hints for the app's own scanner.
ThermoSdk stands for the vendor's SDK: it connects to an address, reports the link on linkEvents and streams temperatures and battery levels. The device manager connects through it:
BLEDeviceManagerimplementsonConfigure(),onHasPermissions()andonRequestPermissions(). If you override them, callsuper:onConfigure()restoresbleAddressandbleNamefrom a stored registration, and the permission methods cover Bluetooth and, on Android, location.canConnectstaysfalseuntil the app binds a thermometer, so CAMS does not try to connect a device it cannot find.onDisconnect()resets_hasConnected. The next connection after an explicitdisconnect()then reportsconnected, which resumes the paused tasks, notreconnected, which resumes only the tasks thatdisconnectingpaused.
The thermometer's probe streams data only while the thermometer is connected. SkinTemperature is the package's data class:
Store device details in a registration
A device registration identifies the physical device that fills a role in a deployment, for example by its Bluetooth address. CAMS calls createRegistration() about 5 seconds after each change to connected.
CAMS sends the registration to the deployment service and stores it in the deployment, under the device's role name. The phone saves the deployment with the study.
At the next app start, what CAMS does with the stored registration depends on the deployment service:
| Deployment service | At the next start |
|---|---|
| The local deployment service, the default | CAMS passes the stored registration to configure(), and BLEDeviceManager takes bleAddress and bleName from it, so the device connects again by itself. |
| CAWS | CAMS sends CAWS only the base fields of the registration, and passes no registration to configure(). The app must bind the device again after every start. |
CAMS registers BLEDeviceRegistration and ServiceRegistration for JSON itself. A package needs a registration class of its own only for extra details, such as the thermometer's firmware version.
Extend one of the CAMS registration classes, so that CAMS can convert the registration for CAWS and read isConnected from it. Register the class in onRegister():
At every start, configure() restores the saved studies from JSON, their registrations included. An unregistered registration class throws a SerializationException, and CAMS drops that study and every study saved after it, with the warning "Failed to load studies".
carp_movesense_package never registers its registration class, so its study fails to load at the first start after the sensor has connected. carp_polar_package registers its class, but stores a plugin enum in it that does not decode, with the same result.
- Test a JSON round trip of the registration with every field set, as
createRegistration()sets them after a connection. See Testing and publishing. - Keep the registration class stable across versions of the package. CAMS passes a stored registration to the manager's
configure(), and a registration of another class throws aTypeErrorthere, so the study does not start.
Bind a physical device
CAMS never scans for devices. The app finds the device, for example with its own Bluetooth scan, and binds it to the device manager before CAMS connects it. Document these steps for app developers:
- The app gets the manager from the
SmartPhoneClientManager's device controller, which exists only afterconfigure(). - It asks for the device's permissions with
requestPermissions(). CAMS asks only for the permissions that measures declare. Without the device's permissions,connect()setsdisconnectedand logs "has not the permissions required to connect". On Android,BLEDeviceManagerneedsPermission.bluetoothScan,Permission.bluetoothConnectandPermission.locationWhenInUseon every Android version; on iOS it asks forPermission.bluetooth. - It sets
bleAddressandbleNamebefore CAMS configures the manager, which happens when the study is deployed. Callingpair()at that point instead sets the status topaired, which counts as configured. CAMS then skipsconfigure(), soonConfigure()never runs and a laterconnectedresumes nothing. - After CAMS has configured the manager, for example after a restart with CAWS, the app binds with
pair()and callsconnect()itself. CAMS does not connect a device when the app binds it or when a permission is granted later.
List the platform setup that the device needs in your README, such as the Bluetooth permissions in AndroidManifest.xml and the Bluetooth usage description in Info.plist. For a complete device page from the app developer's side, see Connecting a device.
Report battery and liveness
HardwareDeviceManager, and so BLEDeviceManager, has batteryLevel (percent, null when unknown) and batteryEvents (empty by default). CAMS reads neither: the level reaches the deployment only through your createRegistration(), as the BatteryChargingState that HardwareDeviceRegistration.parseBatteryLevel() derives from it. A level of 0–10% is critical, 11–20% low, 21–90% normal and 91–100% full.
No device manager sends heartbeats in CAMS 2.x. The HEARTBEAT measure of the built-in device package reports the phone only. To collect a device's battery level or connection state as data, define a measure for it in your package.
Cancel battery timers and listeners in onDisconnect(). carp_movesense_package polls the battery every 10 minutes with a timer that it never cancels.
Next step
Privacy and data formats
Protect sensitive values and convert data to other formats.