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 thecarp_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 This will generate the necessary
build_runner, by running the following command in the root of your Flutter project:.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 aStudyProtocol and CAMS allows creating your own triggers and add them to the framework. This is done by the following steps:
- Define one or more new
Triggers. - Define a
TriggerExecutorfor each new trigger. - Define and register a
TriggerFactorythat knows how to create the correctTriggerExecutorbased on a specific trigger.
Define a new trigger
Any trigger should extend theTriggerConfiguration 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.
Define a trigger executor
Each trigger needs a correspondingTriggerExecutor 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.
Define and register a trigger factory
The last step is to define aTriggerFactory 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 newSamplingPackage. 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).
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 theSamplingPackage interface:
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 ofsamplingSchemes, 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:
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-specificData that the package collects.
Here is the example of the collected FreeMemory data item;
Probes
The next step is to implement probes for each measure type. A probe collects the data and returns it asMeasurement.
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.
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 correspondingDeviceManager and DeviceConfiguration classes.
For example, in the eSense sampling package, the ESenseSamplingPackage implements the following two methods:
ESenseDevice device configuration describes how an eSense device is to be configured:
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.
ESenseManager from the esense_flutter Flutter Plugin.
Putting the package together
Finally, all files should be bundled together in a Dart library. For example, thecarp_esense_package package looks like this;
carp_mobile_sensing package:
SamplingPackageRegistry.
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 theDataEndPoint 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.
Data manager
A data manager implements the functionality for actually storing or forwarding the collected measurements. Any new data manager should implement theDataManager interface:
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 correspondingDataManagerFactory 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:
ConsoleDataManager looks like this:
DataManagerRegistry singleton like this:
Adding data and privacy transformers
Creating data transformer schemas
Data transformation is supported by theDataTransformerSchema
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:
DataTransformerSchemaRegistry (which is a singleton).
Hence, add the following line to your setup up part of the app:
DataTransformer typedef.
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.
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;
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 aLocation into an OMHGeopositionDatum.
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 thePrivacySchema, 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:
- A transformer function that knows how to transform a
Dataobject so that its properties are privacy-protected. - Register this function in the
DataTransformerSchemaRegistryas part of the defaultPrivacySchema.DEFAULTschema.
PrivacySchema.DEFAULT schema:
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.