Extending CAMS
Extend CARP Mobile Sensing with custom triggers, sampling packages, data managers and data or privacy transformers.
CARP Mobile Sensing (CAMS) is designed to be extended with domain-specific studies, triggers, tasks, measures, probes and data types. You extend it by implementing classes that inherit from the abstract classes in the library and registering them with CAMS.
| Extension | What you implement | Where you register it |
|---|---|---|
| Trigger | TriggerConfiguration, TriggerExecutor, TriggerFactory | ExecutorFactory().registerTriggerFactory() |
| Sampling package | SamplingPackage, SamplingSchema, Data, Probe, optionally DeviceManager | SamplingPackageRegistry().register() |
| Data manager | DataEndPoint, DataManager, DataManagerFactory | DataManagerRegistry().register() |
| Data transformer | DataTransformerSchema, DataTransformer functions | DataTransformerSchemaRegistry().register() |
| Privacy transformer | Transformer functions that privacy-protect Data properties | The PrivacySchema.DEFAULT schema in the DataTransformerSchemaRegistry |
| User task | UserTask, UserTaskFactory | AppTaskController().registerUserTaskFactory() |
JSON serialization
Most extensions rely on serialization to and from JSON. This applies to triggers, measures, sampling schemas, data and more. CAMS uses polymorphic serialization as supported by the carp_serializable package, an extension of the standard json_serializable package.
Polymorphic JSON serialization requires four steps when you design your classes:
Define a serializable class
Extend from
Serializableand annotate the class:Implement the JSON methods
Implement the three JSON serialization methods:
Register the class
Register the class's
fromJsonFunctionin theFromJsonFactory, for example in theonRegister()callback of a sampling package:Run the code generator
When all JSON classes are implemented, generate the JSON serialization helper classes using
build_runner. Run this command in the root of your Flutter project:This generates the necessary
.g.dartfiles.
For details, see the carp_serializable and json_serializable packages.
Adding new triggers
Triggers are a central part of a StudyProtocol, and you can create your own triggers and add them to CAMS:
- Define one or more new triggers.
- Define a
TriggerExecutorfor each new trigger. - Define and register a
TriggerFactorythat creates the correctTriggerExecutorfor a specific trigger.
Define a trigger
A trigger extends the TriggerConfiguration class and adds the domain-specific fields that configure it. The RemoteTrigger below listens to a resource on a server identified by a URI and triggers when this resource is available.
Trigger configurations are part of a study protocol, so they must support JSON serialization.
Define a trigger executor
Each trigger needs a corresponding TriggerExecutor that executes the trigger at sampling runtime.
- Every
Executorin CAMS can implement runtime behavior on init, start, stop, restart and dispose. - The abstract
TriggerExecutorclass has default implementations of all these methods, so you only override the lifecycle methods where something needs to happen. - Typically you override
onStart(), which is called when sensing starts, and implement the trigger logic there. Call theonTrigger()callback to fire the trigger.
The RemoteTriggerExecutor starts a periodic timer that checks the resource at the URI in the trigger configuration. If the resource is available, it calls onTrigger().
Warning
This trigger executor is very simplistic and does not cover any edge cases, such as exceptions from network errors.
Define and register a trigger factory
A TriggerFactory maps triggers to their executors at runtime.
- A factory can handle multiple triggers, as listed in the set of trigger
typesit supports. - The
create()method creates the correct trigger executor for the specified trigger. - The
onRegister()callback adds the trigger configurations to theFromJsonFactory(), so trigger configurations in a study protocol can be serialized to and from JSON.
Register the trigger factory with CAMS:
Do this when you initialize CAMS, before any protocol is created or loaded.
Adding new sampling capabilities
To add new sensing capabilities, create a new SamplingPackage. A sampling package implements these classes:
| Class | Role |
|---|---|
SamplingPackage | The overall specification of which measures the package can collect. |
SamplingSchema | Specifies sampling configurations. |
Data | Specifies the data model of the collected measures. |
Probe | Implements the runtime data collection. |
DeviceManager | Specifies how an external device is managed, if any. |
Sampling packages that do not use an external device are simpler to implement, because they can extend the default SmartphoneDeviceManager. The examples below use the DeviceSamplingPackage, which does not use an external device.
Sampling package
Your sampling package implements the SamplingPackage interface:
| Member | Purpose |
|---|---|
| Static measure type strings | List the measures the package supports. For example, FREE_MEMORY defines the type dk.cachet.carp.freememory. |
samplingSchemes | A DataTypeSamplingSchemeMap that configures each measure with its CamsDataTypeMetaData and, if needed, its sampling configuration. |
onRegister() | Called when the package is registered. Here it registers the data classes for JSON serialization. |
create() | Called when a probe is to be created. Returns the right probe for the measure type. |
Permissions
CamsDataTypeMetaData lets you specify which OS-specific permissions a measure needs. The measures above need no permissions. In the SensorSamplingPackage, the activityRecognition permission is required to collect step counts, as shown in the samplingSchemes of that package.
Sampling configurations
You can specify default sampling configurations in samplingSchemes, like the IntervalSamplingConfiguration for the FREE_MEMORY measure type above. CAMS has these built-in sampling configurations:
| Sampling configuration | Description |
|---|---|
PersistentSamplingConfiguration | Persistently saves the state of data sampling for continuous long-running sampling across app restarts. |
HistoricSamplingConfiguration | Configures retrospective or backfilled collection of historical data. |
IntervalSamplingConfiguration | Samples at a fixed interval (delay) between measurements. |
PeriodicSamplingConfiguration | Samples in periodic windows using both interval and duration settings. |
You can also write your own sampling configuration, tailored to how a specific measure should be collected in a probe. For example, this is the IntervalSamplingConfiguration:
- A sampling configuration must extend one of the sampling configuration types, such as
SamplingConfigurationorPersistentSamplingConfiguration. - Sampling configurations can be part of a study protocol, where you can override the sampling configuration of a measure.
Data
Define the package-specific Data classes that the package collects. This is the FreeMemory data item:
Tip
You can add privacy protection of collected data to the package. See Adding privacy transformers.
Probes
Implement a probe for each measure type. A probe collects the data and returns it as a Measurement.
The abstract Probe interface defines a probe. To create your own probes, extend one of the predefined abstract probes in CAMS:
| Probe base class | Description |
|---|---|
MeasurementProbe | Collects one measurement and then stops. |
IntervalProbe | Collects measurements at a regular interval. |
StreamProbe | Listens continuously to a stream of measurements. |
PeriodicStreamProbe | Listens to a stream during periodic windows defined by interval and duration. |
BufferingPeriodicProbe | Buffers data for a period and emits a derived measurement. |
BufferingIntervalStreamProbe | Buffers an underlying stream and emits a measurement on regular intervals. |
BufferingPeriodicStreamProbe | Buffers an underlying stream for a fixed period and then emits a measurement. |
The DeviceProbe is a simple MeasurementProbe. It collects information about the phone with the device_info_plus plugin, maps it to a DeviceInformation data item and returns a Measurement with this data. Because it extends MeasurementProbe, it only implements getMeasurement():
The ScreenProbe is a StreamProbe that collects screen activity. It implements the stream property, which maps screen events from the screen_state plugin to ScreenEvent data objects and wraps each one in a measurement with the Measurement.fromData factory:
External devices
A sampling package that collects data from an external device or service specifies which device type it supports and provides a corresponding DeviceManager and DeviceConfiguration. The examples in this section come from the eSense sampling package.
The ESenseSamplingPackage implements the device-related members:
The ESenseDevice device configuration describes how an eSense device is configured. Because a device configuration can be part of a study protocol, it must support JSON serialization.
Device managers
A device manager implements the DeviceManager interface. CAMS has predefined device managers that you can extend: SmartphoneDeviceManager, ServiceManager, HardwareDeviceManager and BLEDeviceManager. The eSense sensor is a BLE device, so the ESenseDeviceManager extends BLEDeviceManager:
With the device manager in place, the eSense probes for the button and the IMU sensor are implemented as stream probes:
Both the device manager and the probes use the ESenseManager from the esense_flutter Flutter plugin.
Packaging and registering
Bundle all files in a Dart library. For example, the carp_esense_package library looks like this:
To use a sampling package, import it into your app together with carp_mobile_sensing:
Before you create and run a study, register the package in the SamplingPackageRegistry:
You can release a sampling package as a Dart package on pub.dev. See the list of existing sampling packages, which covers onboard phone sensors, external wearable devices and online services.
Adding a new data manager
CAMS comes with a set of built-in and external data managers. See Data managers for an overview.
To save or upload data to a custom data backend, implement three interfaces:
Create a data endpoint
Implement the
DataEndPointinterface, which is used in the study protocol.Create a data manager
Implement the
DataManagerinterface, which saves or uploads measurements as they are sampled.Create a data manager factory
Create a
DataManagerFactorythat creates a data manager based on the data endpoint configuration.
Data endpoint
A data endpoint is part of the study protocol. It specifies which data manager stores or forwards data, and how that data manager is configured. Every new data manager needs a corresponding data endpoint that extends the DataEndPoint class.
For example, the FileDataEndPoint specifies that data is saved to a file with the FileDataManager. It configures the data manager with a buffer size and whether the file is zipped or encrypted.
Warning
A DataEndPoint must support JSON serialization to be part of a study protocol.
Data manager
A data manager stores or forwards the collected measurements. Every new data manager implements the DataManager interface:
All of these methods must be implemented. The AbstractDataManager class is a useful starting point. For example, the ConsoleDataManager is a minimal data manager that prints a JSON-encoded version of the data to the console:
Data manager factory
Each new data manager needs a corresponding DataManagerFactory, which creates a data manager of a specific type when the study protocol is loaded. The interface:
The factory for the ConsoleDataManager:
Register the factory in the DataManagerRegistry singleton:
Do this at app start-up, before the protocol is loaded and sensing is started.
Adding data and privacy transformers
For how to use transformers and privacy schemas in a study protocol, see Data transformation and privacy.
Creating data transformer schemas
To create a DataTransformerSchema, implement the namespace getter and the onRegister() callback. This is the Open mHealth transformer schema:
Register each transformer schema in the DataTransformerSchemaRegistry singleton as part of your app setup:
Once the schema is registered, you can create transformers for each data type. A data transformer transforms one type of data into another, as defined by the DataTransformer typedef:
For each Data type you want to transform, define a new class that also extends Data. For example, the OMHGeopositionDataPoint class represents an OMH Geoposition data point:
- The most important member is
transformer, a function that transforms one type ofDatainto another. The mapping between the two data types happens in thefromLocationDatafactory. - The class must support JSON serialization. Here the superclass
OMHContextDataPointhandles it. ThetoJsonmethod is needed to serialize and store the data. - The namespace of the
DataTypeofOMHGeopositionDataPointisOMH.
Add the transformer to the schema:
In carp_context_package, this happens in the onRegister() method of the ContextSamplingPackage.
Using data transformer schemas
You can transform data manually by looking up a transformer schema and applying its transformer. This code transforms a Location into an OMHGeopositionDatum:
More commonly, you set the data format of the DataEndPoint with its dataFormat property, which specifies the namespace, such as NameSpace.OMH. This data endpoint saves measurements to a file in the Open mHealth (OMH) data format:
Note
Not all data types in CAMS have an OMH data format. If no OMH transformer is found, the data is stored in the original CARP data format.
Adding privacy transformers
A PrivacySchema is a special data transformer schema that takes a piece of data and protects its relevant properties. CAMS has a built-in privacy schema with the transformer namespace PrivacySchema.DEFAULT. To add privacy protection:
- Write a transformer function that transforms a
Dataobject so that its properties are privacy-protected. - Register this function in the
DataTransformerSchemaRegistryas part of thePrivacySchema.DEFAULTschema.
These privacy functions anonymize text messages and phone calls:
You can put these functions anywhere in the sampling package. Typically you put them in a separate file if you have many, or in the package Dart file if you have only a few.
Register the anonymizer functions in the PrivacySchema.DEFAULT schema, typically in the package's onRegister() callback:
This example is implemented in the carp_communication_package.
Tip
Consider whether your sampling package needs to supply privacy-protecting functions, and if so, add them to the default privacy schema.