Skip to content

3. Using CARP Mobile Sensing

Jakob E. Bardram edited this page Sep 29, 2022 · 83 revisions

Defining and Running a Study

A CAMS study is configured, deployed, executed, and used in the following steps:

  1. Define a StudyProtcol.
  2. Deploy this protocol to a DeploymentService.
  3. Get a study deployment for the phone and start executing this study deployment using a SmartPhoneClientManager.
  4. Use the generated data locally in the app or specify how and where to store or upload it using a DataEndPoint.

Note that as a mobile sensing framework running on a phone, CAMS could be limited to support 3-4. However, to support the 'full cycle', CAMS also supports 1-2. This allows for local creation, deployment, and execution of study protocols (which in many applications have shown to be useful).

Defining a StudyProtcol

In CAMS, a sensing protocol is configured in a StudyProtocol. Below is a simple example of how to set up a protocol that sense step counts (pedometer), ambient light (light), screen activity (screen), and power consumption (battery).

// import package
import 'package:carp_core/carp_core.dart';
import 'package:carp_mobile_sensing/carp_mobile_sensing.dart';

void example() async {
  // Create a study protocol storing data in files.
  SmartphoneStudyProtocol protocol = SmartphoneStudyProtocol(
    ownerId: 'AB',
    name: 'Track patient movement',
    dataEndPoint: FileDataEndPoint(
      bufferSize: 500 * 1000,
      zip: true,
      encrypt: false,
    ),
  );

  // Define which devices are used for data collection.
  // In this case, its only this smartphone.
  Smartphone phone = Smartphone();
  protocol.addMasterDevice(phone);

  // Add a background task that immediately starts collecting step counts,
  // ambient light, screen activity, and battery level.
  protocol.addTriggeredTask(
      ImmediateTrigger(),
      BackgroundTask()
        ..addMeasures([
          Measure(type: SensorSamplingPackage.PEDOMETER),
          Measure(type: SensorSamplingPackage.LIGHT),
          Measure(type: DeviceSamplingPackage.SCREEN),
          Measure(type: DeviceSamplingPackage.BATTERY),
        ]),
      phone);

The above example defines a simple SmartphoneStudyProtocol which will store data in a file locally on the phone using a FileDataEndPoint. Sampling is configured by using the pre-defined SamplingSchema from the SensorSamplingPackage and DeviceSamplingPackage packages. This sampling schema contains a set of default sampling configurations for how to sample the different measures. These measures are triggered immediately when sensing is started (see below), and runs automatically in the background using a BackgroundTask .

Sampling can be configured in a very sophisticated ways, by specifying different types of triggers, tasks, measures, and sampling configurations. See the CAMS domain model for an overview.

You can write your own DataEndPoint definitions and coresponding DataManagers for uploading data to your own data endpoint. See the wiki on how to add a new data manager.

Using a DeploymentService

A device deployment specifies how a study protocol is executed on a specific device - in this case a smartphone. A StudyProtocol can be deployed to a DeploymentService which handles the deployment of protocols for different devices. CAMS comes with a simple deployment service (the SmartphoneDeploymentService) which runs locally on the phone. This can be used to deploy a protocol and get back a MasterDeviceDeployment, which can be executed on the phone.

...

  // Use the on-phone deployment service.
  DeploymentService deploymentService = SmartphoneDeploymentService();

  // Create a study deployment using the protocol.
  StudyDeploymentStatus status =
      await deploymentService.createStudyDeployment(protocol);

...

Get and Execute a Study

A study deployment for a phone (master device) is handled by a SmartPhoneClientManager. This client manager is able to create a StudyDeploymentController, which controls the execution of a study deployment.

...

String studyDeploymentId = ... // any id obtained e.g. from an invitation
String deviceRolename = ... // the rolename of this phone in the protocol;

  // Create and configure a client manager for this phone.
  SmartPhoneClientManager client = SmartPhoneClientManager();
  await client.configure(deploymentService: deploymentService);

  // Create a study object based on the deployment id and the rolename
  Study study = Study(
    status.studyDeploymentId,
    status.masterDeviceStatus!.device.roleName,
  );

  // Add the study to the client manager and get a study runtime to control this deployment
  await client.addStudy(study);
  SmartphoneDeploymentController? controller = client.getStudyRuntime(study);

  // Deploy the study on this phone.
  await controller?.tryDeployment();

  // Configure the controller and start sampling.
  await controller?.configure();
  controller?.start();

...

CAMS will now take care of executing the study, i.e. collect the data as specified and upload it to the specified data backend (as zipped files on the local device in this example).

  • Appendix A lists the supported measure types (i.e., what data the framework can collect).
  • Appendix C lists available data backends and describes how data can be stored both locally on the device and on remote servers (like CARP and Google Firebase).

Lifecycle Methods

Normally, once data sampling has been started (by calling the controller.start() method), data sampling will run automatically in the background, and you don't need to do more.

However, if you want to control the data sampling from your app, there are several lifecycle methods available.

For example, you can pause and resume sampling;

  controller.executor?.pause();

  ...

  controller.executor?.resume();

You can also pause / resume specific probes;

  controller.executor
      ?.lookupProbe(SensorSamplingPackage.ACCELEROMETER)
      .forEach((probe) => probe.pause());

If you want to configure or dynamically adapt your sampling this can be done by changing the configuration of a Measure in a study. For example, in the following we first configure a measure of type LIGHT and then adapt it while sampling is running. This change is propagated to the probe, which then restarts sampling according to the new measure configuration.

  // override configuration details of a light measure
  Measure lightMeasure = Measure(
    type: SensorSamplingPackage.LIGHT,
  )..overrideSamplingConfiguration = PeriodicSamplingConfiguration(
      interval: const Duration(minutes: 10),
      duration: const Duration(seconds: 20),
    );

  // add it to the study to start immediately
  protocol.addTriggeredTask(
    ImmediateTrigger(),
    BackgroundTask(name: 'Light')..addMeasure(lightMeasure),
    phone,
  );

  ...
  // start the sampling as above
  ...

  // adapt the measure
  lightMeasure
    ..overrideSamplingConfiguration = PeriodicSamplingConfiguration(
      interval: const Duration(minutes: 5),
      duration: const Duration(seconds: 10),
    );
  
  // Restart the light probe(s)
  controller.executor
      ?.lookupProbe(SensorSamplingPackage.LIGHT)
      .forEach((probe) => probe.restart());

  // Alternatively mark the deplyment as changed - calling hasChanged()
  // this will force a restart of the entire sampling
  controller.deployment?.hasChanged();

Finally, you can permanently stop a study by calling stop:

  // Once the sampling has to stop, e.g. in a Flutter dispose() methods, call stop.
  // Note that once a sampling has stopped, it cannot be restarted.
  controller.stop();

Using Collected Data

The approaches above assume that you use the framework to collect data as part of your app and just sends it to one of the available data backends, without using the data in your app.

However, if you want to use the collected data in your app while it is being collected, you can access data via the data stream. For example, the following code would listen on all data events from the study created above and print it to the Dart console.

controller.data.forEach(print);

This data stream is a standard Dart Stream and you can hence do all the normal stream (reactive) operations on it. For example, if you only wants to listen on CARP events, write;

controller.data
    .where((dataPoint) => dataPoint.data!.format.namespace == NameSpace.CARP)
    .listen((event) => print(event));

Or if you only want to listen to light events, write;

controller.data
    .where((dataPoint) =>
        dataPoint.data!.format.toString() == SensorSamplingPackage.LIGHT)
    .listen((event) => print(event));

In general you can listen to events using the Dart StreamSubscription;

StreamSubscription<DataPoint> subscription =
    controller.data.listen((DataPoint dataPoint) {
  // do something w. the datum, e.g. print the json
  print(JsonEncoder.withIndent(' ').convert(dataPoint));
});

The data stream originates from each individual Probe and you can listen directly to a probe (actually, the StudyExecutor in the StudyController is merely a super-probe, merging all data stream from the underlying probes).

Data Transformation

CAMS supports data transformation via two concepts:

  • DatumTransformer which can transform one type of Datum to another type of Datum.
  • Transformer Schemas which hold a set of DatumTransformers that can map from the native CARP namespace to another namespace.

Transformation is configured by specifying which data format is to be used in the data endpoint specified in the study protocol. For example, to transform a data into another format like Open mHealth (OMH), the study protocol would be specified like this:

  // Create a study protocol storing data in files using the OMH data format
  SmartphoneStudyProtocol protocol = SmartphoneStudyProtocol(
    ownerId: 'AB',
    name: 'Track patient movement',
    dataEndPoint: FileDataEndPoint(
      bufferSize: 500 * 1000,
      zip: true,
      encrypt: false,
      dataFormat: NameSpace.OMH,
    ),
  );

See also the section on how to add data transformers of your own.

Privacy Transformer Schemas

CARP Mobile Sensing comes with a default PrivacySchema which implements privacy protection and anonymization methods on the data being collected before it is uploaded to a data backend. For example, one-way hashing of phone numbers before they are stored. To use this schema, simply specify this schema when configuring your SmartphoneDeploymentController, as shown below:

  // configure the controller with the default privacy schema and start sampling
  await controller?.configure(
    privacySchemaName: PrivacySchema.DEFAULT,
  );

  controller?.start();

This will create a controller that uses the default privacy schema. This schema can be extended or you can write your own privacy schemas and use these instead.

Clone this wiki locally