The main purpose of the CARP Mobile Sensing framework is to allow for extension with domain-specific studies, triggers, tasks, measures, probes, and data types. This is done by implementing classes that inherit from the (abstract) classes in the library.

Add Triggers

Define custom triggers to be used as part of a study protocol.

Add Sampling Packages

Introduce new measures, probes, data classes, and optional device managers.

Add Data Managers

Support custom storage or upload of measurements via new endpoints and data managers.

Add Data Transformers

Creating data and privacy transformer schemas for data re-formatting and privacy control.

JSON serialization

Extension to CAMS often rely on serialization to/from JSON - this applies for triggers, measures, sampling schemas, data, etc. CAMS relies on polymorphic serialization as supported by the carp_serializable package, which is an extension to the standard json_serializable provided in Dart. Support for polymorphic JSON serialization implies four steps when designing your classes:
1

Define a serializable class

Extend from Serializable and annotate the class:
2

JSON serializable methods

Implement the three JSON serializable methods:
3

Registration

Register the class’s fromJsonFunction in the FromJsonFactory, e.g. in the onRegister() callback function of a sampling package:
4

Running the code generator

When all JSON classes have been implemented, the final step is to generate the JSON serialization helper classes. These can be generated using build_runner, by running the following command in the root of your Flutter project:
This will generate the necessary .g.dart files.
See the carp_serializable and json_serializable packages for details on JSON serialization.

Adding new triggers

Triggers are a central part of a StudyProtocol and CAMS allows creating your own triggers and add them to the framework. This is done by the following steps:
  1. Define one or more new Triggers.
  2. Define a TriggerExecutor for each new trigger.
  3. Define and register a TriggerFactory that knows how to create the correct TriggerExecutor based on a specific trigger.

Define a new trigger

Any trigger should extend the TriggerConfiguration class and implement domain-specific fields that configure this trigger. In the example below, we have defined a RemoteTrigger that listens to resources on a server identified by a URI, and triggers when this resource is available.
Trigger configurations are part of a study protocol and hence needs to be serializable to/from JSON.

Define a trigger executor

Each trigger needs a corresponding TriggerExecutor to execute the trigger on sampling runtime. An example of a RemoteTriggerExecutor executor is shown below. Every Executor in CAMS can implement runtime behavior on init, start, stop, restart, and dispose. The abstract class TriggerExecutor is a convenient class to use for the implementation of a trigger executor since it has default implementations of all methods. Hence, you only need to override the lifecycle methods you need something to happen. Typically — and as shown below — the most relevant method to override is the onStart() method which is called when sensing is started. In this method, you will implement the trigger logic. In the RemoteTriggerExecutor the trigger starts a periodic timer that regularly checks the resources specified by the URI in the trigger configuration. If there is a resource available, it triggers by calling the onTrigger() callback method.
Note that this trigger executor is very simplistic and does not cover any edge cases, like exceptions from network errors.

Define and register a trigger factory

The last step is to define a TriggerFactory that knows how to map triggers to their executors on runtime. An example of the RemoteTriggerFactory is shown below. Note that a factory can handle multiple triggers, as defined in the set of trigger types it supports. The main method of the factory is the create method that can create the correct trigger executor for the specified trigger.
Note that in the onRegister() callback function, the trigger configurations are added to the FromJsonFactory(). This allows trigger configurations defined in a study protocol to be serialized to/from JSON.
The last step is to register this trigger factory with CAMS. This is done by calling:
This is typically done when initializing CAMS before any protocol is made or loaded.

Adding new sampling capabilities

If you want to add new sensing capabilities you would basically create a new SamplingPackage. A sampling package implements the following classes:
  • SamplingPackage - the overall specification of what measures this package can collect.
  • SamplingSchema - specifies sampling configurations.
  • Data - specifies the data model of the measures collected.
  • Probe - implements the runtime of data collection.
  • DeviceManager - specifies how an external device is managed (if any).
Not all sampling packages use an external device and are hence simpler to implement since they can extend the default SmartphoneDeviceManager. In the following, we will use the DeviceSamplingPackage as an example of a sampling package that does not use any external device.

Sampling package

The first step is that your new sampling package should implement the SamplingPackage interface:
The measures supported by this package are listed as static strings, such as FREE_MEMORY defining the type dk.cachet.carp.freememory. The DataTypeSamplingSchemeMap defines the configuration of each measure, by specifying the CamsDataTypeMetaData for each measure and its sampling configuration (if needed). The onRegister() method is called when the package is registered and in this case registers the data classes for JSON serialization. Finally, the create() method is called when a probe is to be created and returns the right probe based on the measure type.
A note on Permissions - the CamsDataTypeMetaData allows the specification of what OS-specific permissions are needed to collect a measure.In the measures used as an example above, no permissions are required. However, in the SensorSamplingPackage, the activityRecognition permission is required in order to collect step counts, as shown in the samplingSchemes of that package.

Sampling configurations

Default sampling configurations can be specified as part of samplingSchemes, like the IntervalSamplingConfiguration specified for the FREE_MEMORY measure type above. There are several built-in sampling configurations available: But you can write your own sampling configuration tailored to a specific measure and hence how data should be collected in a probe. As an example of how to write a sampling configuration, the IntervalSamplingConfiguration is shown below:
Note that a sampling configuration must extend from one of the sampling configuration types (e.g., SamplingConfiguration or PersistentSamplingConfiguration). Sampling configurations can be part of a study protocol where you can override the sampling configuration of a measure.

Data

Next, you should define the package-specific Data that the package collects. Here is the example of the collected FreeMemory data item;
A small note on Privacy; it is possible to add privacy protection of collected data to the package. See Creating Data Privacy for how to do this.

Probes

The next step is to implement probes for each measure type. A probe collects the data and returns it as Measurement. The abstract Probe interface defines a probe and how to implement probes. But in order to create your own probes, CAMS has a set of predefined, abstract probes to extend from: The DeviceProbe is an example of a simple MeasurementProbe that collects device information about the phone using the device_info_plus plugin and map this to a DeviceInformation data and returns a Measurement with this data. As shown below, since the DeviceProbe extends MeasurementProbe it just need to implement the getMeasurement() method, which returns the device info mapped to a DeviceInformation data item.
The ScreenProbe is an example of a StreamProbe that collects screen activity data. This probe implements the stream property, which maps screen events from the screen_state plugin to ScreenEvent data objects, which again is wrapped in a measurement using the Measurement.fromData factory.

Device manager

If a sampling package handles (i.e., collects data from) an external device or service, then it should be able to specify what type of device it supports and provide corresponding DeviceManager and DeviceConfiguration classes. For example, in the eSense sampling package, the ESenseSamplingPackage implements the following two methods:
The ESenseDevice device configuration describes how an eSense device is to be configured:
Note that since a device configuration can be part of a study protocol, it needs to support JSON serialization. The device manager should implement the DeviceManager interface. CAMS has a set of predefined device managers which can be extended, including the SmartphoneDeviceManager, ServiceManager, HardwareDeviceManager, and BLEDeviceManager. Since the eSense sensor is a BLE device, the ESenseDeviceManager extends the BLEDeviceManager class.
Once the eSense device manager is in place, the eSense probes (button and IMU sensor) can be implemented as stream probes.
Both the device manager and the probes make use of the ESenseManager from the esense_flutter Flutter Plugin.

Putting the package together

Finally, all files should be bundled together in a Dart library. For example, the carp_esense_package package looks like this;
To use a sampling package, import it into your app together with the carp_mobile_sensing package:
Before creating a study and running it, register this package in the SamplingPackageRegistry.
A package can be released as a Dart package on Pub. We already provide a list of different sampling packages - both using the onboard phone sensors as well as 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. It is possible to extend CAMS with support for new data managers that can save or upload data to custom data backends. Support for this is done by implementing 3 interfaces:
1

Create a data endpoint

Implement the DataEndPoint interface which is used in the study protocol.
2

Create a data manager

Implement the DataManager interface which uploads or saves the measurement as they are sampled.
3

Create a data manager factory

Create a DataManagerFactory that can create a data manager based on the data endpoint configuration,

Data endpoint

A data endpoint is included in the study protocol and specifies what data manager to use for storing or forwarding data, and the configuration of this data manager. Any new data manager should have a corresponding data endpoint that extends from the DataEndPoint class. For example, the FileDataEndPoint specifies that data should be saved to a file using the FileDataManager data manager. This FileDataEndPoint allows for configuring the data manager by specifying buffer size and whether the file should be zipped or encrypted.
A DataEndPoint must support JSON serialization in order to be part of a study protocol

Data manager

A data manager implements the functionality for actually storing or forwarding the collected measurements. Any new data manager should implement the DataManager interface:
All of these methods have to be implemented. However, the AbstractDataManager class provides a useful class to start from. For example, the ConsoleDataManager provides a very simple example of a data manager that prints a json encoded version of the data to the console:

Data manager factory

When creating a new data manager, a corresponding DataManagerFactory must be provided. This factory knows how to create a data manager of a specific type when the study protocol is loaded. The interface looks like this:
The factory for the ConsoleDataManager looks like this:
For a data manager to be used in CAMS, its factory must be registered in the DataManagerRegistry singleton like this:
This should happen at app start-up and before the protocol is loaded and sensing is started.

Adding data and privacy transformers

Creating data transformer schemas

Data transformation is supported by the DataTransformerSchema class and can be implemented by implementing the namespace getter and the onRegister() callback function. As an example, the implementation of the Open mHealth transformer schema is shown below:
Each transformer schema must be registered in the DataTransformerSchemaRegistry (which is a singleton). Hence, add the following line to your setup up part of the app:
Once the schema is registered, transformers for each data type can be created. Data transformation is a transformation of one type of data to another type of data. I.e. data transformation is defined by the DataTransformer typedef.
For each Data you want to transform, you need to define a new class that also extends Data. For example the following OMHGeopositionDataPoint class represents an OMH Geoposition data point.
The most important function to implement is the transformer, and to make sure JSON serialization is supported (in the example above, this is handled by the superclass OMHContextDataPoint). The transformer is a function that can transform one type of Data to another. The mapping between the two data types happens in the fromLocationData factory. The toJson method is needed in order to serialize and store the data. Also, note that the namespace of the DataFormat of this OMHGeopositionDataPoint is OMH. Once the mapper Data class is created, it must be added to the schema. This is done by calling;
In the carp_context_package, this is done in the onRegister() method in the ContextSamplingPackage.

Using data transformer schemas

Data transformation can be done “manually” by looking up a specific transformer schema and applying its transformer. For example, the following code transforms a Location into an OMHGeopositionDatum.
However, a more general and common use of transformers is to specify what data format the DataEndPoint should use. This is done via the dataFormat property, which specifies the namespace, like NameSpace.OMH. The following data endpoint saves measurements to a file using the open mHealth (OMH) data format:
Note that there is no OMH data format for all data types in CAMS. In the case that no OMH transformer is found, the data is stored in the original CARP data format.

Creating data privacy

A special instance of a data transformer schema is the PrivacySchema, which basically takes a piece of data and protects relevant properties. CAMS comes with a built-in schema with a transformer namespace specified in PrivacySchema.DEFAULT. Hence, to add privacy protection, two things have to be implemented:
  1. A transformer function that knows how to transform a Data object so that its properties are privacy-protected.
  2. Register this function in the DataTransformerSchemaRegistry as part of the default PrivacySchema.DEFAULT schema.
Below are examples of privacy functions that anonymize text messages and phone calls:
This function can be put anywhere in the sampling package, but a typical place to put it is in a separate file (if you have many functions) or in the package dart file (if you only have a few). Registering the anonymizer functions in the PrivacySchema.DEFAULT schema:
This is typically done as part of the onRegister() callback in the package. The example above is implemented as part of the carp_communication_package package.
It is good practice to consider if your data sampling package needs to supply privacy-protecting functions, and if so, add these to the default privacy schema.