Sampling package class
Declare the measure types, create the probes, register the JSON classes and use the package in an app.
The sampling package class is what an app registers with CARP Mobile Sensing (CAMS). It declares the package's measure types with their metadata and default sampling configurations. It also creates a probe for each measure and registers the classes that CAMS decodes from JSON.
Write the package class
Replace the contents of lib/barometer_package.dart with the complete BarometerSamplingPackage class:
Both measures run on the phone, so the class extends SmartphoneSamplingPackage and implements only samplingSchemes, create() and onRegister().
To compile the package, add the two privacy functions that onRegister() uses to lib/barometer_privacy.dart. For their code, see Write a privacy function.
Members of a sampling package
Every sampling package implements the six members of SamplingPackage. SmartphoneSamplingPackage implements four of them for packages that collect data on the phone:
| Member | Called by | In SmartphoneSamplingPackage |
|---|---|---|
samplingSchemes | register() and the registry, each time it creates a probe | You implement it |
create() | The registry, for each measure when CAMS initializes a deployment or a user task starts | You implement it |
onRegister() | register(), once | Does nothing; you override it |
dataTypes | Only the phone's device manager, which lists the types of phone packages for information | samplingSchemes.dataTypes |
deviceType | register(), which registers deviceManager under this type | dk.carp.cams.devices.Smartphone |
deviceManager | register() and the registry, each time it creates a probe | One SmartphoneDeviceManager that all phone packages share |
For a wearable or an online service, implement SamplingPackage with all six members. See Devices and services.
Declare the measure types
samplingSchemes returns a DataTypeSamplingSchemeMap with one DataTypeSamplingScheme per measure type: the type's metadata and its default sampling configuration. It is the list of types that the package supports:
- Only declared types get probes: the registry finds the package for a measure by looking for its type in
samplingSchemes. For a type thatcreate()handles butsamplingSchemeslacks, CAMS creates no probe; it skips the measure and logs a warning. - The default configuration applies when the protocol sets none: give each type the default that its probe expects. A scheme without one gets a
NoOptionsSamplingConfiguration, and a probe that casts its configuration, such as anIntervalProbe, never starts. See Set the default configurations.
The metadata of each scheme is a CamsDataTypeMetaData:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
type | String | Yes | — | The measure type. |
displayName | String | No | '' | A name that apps can show, as the CAMS demo app does in its list of probes. |
timeType | DataTimeType | No | DataTimeType.POINT | POINT for data at one moment, TIME_SPAN for data over an interval, whose measurements set sensorEndTime. |
dataEventType | DataEventType | No | DataEventType.EVENT | EVENT for a measure that collects until it is paused, ONE_TIME for one that collects once each time it is triggered. |
permissions | List<Permission> | No | const [] | The runtime permissions that the probe needs. See Declare permissions. |
CAMS 2.5.1 does not read timeType or dataEventType. They tell apps and data analysts what to expect, while the probe alone decides when collection stops and which timestamps a measurement has. Set them to match the probe: the example declares TIME_SPAN for the summary, whose measurements have an end time.
Declare permissions
List the runtime permissions that a measure's probe needs in permissions, as Permission values of the permission_handler plugin. The sensor package of CAMS, for example, declares activity recognition for its step count measure:
CAMS 2.5.1 then handles the permission in two places:
- CAMS asks when the phone gets a study's deployment: after
tryDeployment()and at every app start inconfigure(). It collects the permissions of all measures in the deployment and asks for the missing ones, one dialog at a time. An app that callsconfigure()withaskForPermissions: falseturns this off. - When a probe resumes, the CAMS probe base classes check the permissions. On Android, a missing permission makes
onResume()returnfalse: the probe does not start, reports no error and checks again at its next resume. On iOS, the check always passes, because iOS asks for a permission when a plugin first uses it.
A few rules follow from this:
- Declare the Android permission group: on iOS, CAMS asks for
Permission.sensorsinstead ofPermission.activityRecognitionand forPermission.bluetoothinstead ofPermission.bluetoothScan. - Leave out the permissions to connect to a device: these permissions, such as Bluetooth access, belong to the device manager. CAMS does not ask for them. See Devices and services.
- Add
permission_handlerto the package's dependencies: CAMS does not exportPermission. In the README, list the Android manifest entries and iOS usage descriptions that an app must add. - Do not add a
permissionsgetter to the package class:SamplingPackagehas none in CAMS 2.x. A getter left over from CAMS 1.x only causes the analyzer warningoverride_on_non_overriding_member, and CAMS never asks for its permissions.
The example declares no permissions. Android needs none to read the barometer. On iOS, sensors_plus reads it through Core Motion, which needs an NSMotionUsageDescription entry in the app's Info.plist; the app setup below adds it.
Declaring Permission.sensors would make CAMS ask for body sensor access on Android, which the barometer does not need. If a participant denied it, neither probe would start there.
Create the probes
create() returns a new probe for a measure type or null for a type that the package does not handle. The registry calls it for each measure of each background task when CAMS initializes a deployment, before any device connects:
- Return a new probe from every call: each measure has its own probe with its own state. An app task's measures get new probes each time one of its user tasks starts.
- Return
nullon an unsupported platform: CAMS then skips the measure with a warning and runs the rest of the protocol. The sensor package of CAMS does this for ambient light on iOS. - Keep the probe constructor free of setup: the registry sets the probe's
deviceManageraftercreate()returns, so reading it in the constructor throws aLateInitializationError. See Probe lifecycle. - Do not choose the probe from device state:
create()runs before the device connects. See Read the device in a probe.
Register the JSON classes and privacy functions
When an app calls register(), the SamplingPackageRegistry takes three steps:
- It adds the metadata from
samplingSchemesto the data types that CAMS knows. - It registers
deviceManagerwith the device controller underdeviceType. The first manager registered for a type is kept. - It calls
onRegister().
In onRegister(), register every class of the package that CAMS decodes from JSON. A placeholder instance is enough: FromJsonFactory stores only the class's decoding function under its jsonType.
| Classes | CAMS decodes them from | When one is not registered |
|---|---|---|
Data classes and a CompletedAppTask for each app task type | Stored measurements, for example before an upload to CARP Web Services (CAWS) | Every upload attempt of the study fails. |
Sampling configurations, device configurations, device registrations and AppTask subclasses | The deployment, which CAMS saves on the phone and restores at every app start | At the next app start, the study and every study saved after it fail to load. CAMS logs the warning "Failed to load studies". |
For the classes in the deployment, the first run hides a missing registration, because the local deployment service keeps the protocol's objects and decodes nothing.
carp_movesense_package, for example, does not register its device registration class. A study in which a Movesense sensor has connected therefore fails to load at the next app start. For a test that catches this, see Test the JSON round trips.
Register only the classes of your package: CAMS registers its own, such as IntervalSamplingConfiguration, when it initializes.
Register each package once, too. register() does not check for duplicates: a second registration runs onRegister() again. With a second instance, CAMS also logs a duplicate-type warning for every probe of the package's types.
The example's onRegister() also adds two privacy functions to the default privacy schema. They run only in studies whose protocol sets privacySchemaName: PrivacySchema.DEFAULT. See Privacy and data formats.
Device type and device manager
deviceType and deviceManager connect the package's probes to a device. SmartphoneSamplingPackage returns the phone's type and the SmartphoneDeviceManager that all phone packages share, so the example implements neither. To implement both for a wearable or an online service, see Expose the device in the package.
Use the package in an app
An app uses your package like any other sampling package: it registers the package, adds the measures to a protocol and deploys it. The example app in example/ does this, and setting it up takes three steps:
Create the example app
Delete
example/test/: its widget test uses the template'sMyAppclass, which the newmain.dartreplaces. Thepathdependency makes the app use the package in the folder above, which applies your changes at each run:example/pubspec.yaml Set up Android and iOS for CAMS
To configure the example app for CAMS on Android and iOS, see Installation. The iOS setup there includes the entry that
sensors_plusneeds to read the barometer:example/ios/Runner/Info.plist Without this entry, the app crashes on iOS when it reads the barometer.
Register the package and deploy a protocol
Replace the contents of
example/lib/main.dartwith:example/lib/main.dart
Startup order
| Stage | What it does |
|---|---|
| Initialize | CarpMobileSensing.ensureInitialized() sets up the data types and JSON classes of CAMS. It must run before the first SamplingPackageRegistry() call. |
| 1. Register | Makes the two measure types, their data classes and the summary configuration known to CAMS. It must run before configure(). |
| 2. Protocol | Creates a protocol with the phone as the primary device. SQLiteDataEndPoint() stores the measurements in a SQLite database on the phone. |
| 3. Measures | Adds both measures to one task. They are event-based, so an ImmediateTrigger starts them once and they run until the study stops. The override shortens the summary interval from 5 minutes to 30 seconds. The first window starts one interval after the probe starts, so the first summary appears within a minute. |
| 4. Deploy | Configures the client and, on the first run only, adds and deploys the study. On later runs, configure() restores the saved study. |
| 5. Print | Prints each measurement as JSON. onError prints the errors that probes report, which would otherwise be uncaught errors. |
Initialize CAMS first
In CAMS 2.5.1, the first SamplingPackageRegistry() call registers the built-in packages, which need the data types that CAMS adds when it initializes. Without CarpMobileSensing.ensureInitialized() first, that call throws "Null check operator used on a null value" before runApp(), and the app shows a blank screen. Creating a SmartPhoneClientManager also initializes CAMS, but creating a protocol does not.
Show the same order in your package's README, example app and doc comments. In the package's tests, initialize CAMS in the setup, as the example's tests do.
Register the package before configure(), because configure() restores the studies saved in earlier runs. It decodes their deployments, including the summary measure's AirPressureSummarySamplingConfiguration, so a package registered later comes too late and the saved study fails to load. Register user task factories and other extensions at the same point.
Run the app
Run the app on a phone from the example/ folder:
For each reading, the console shows a line like the first one below. From about 40 seconds after the start, it also shows a line like the second one for each window:
The first measurement of a study is the dk.cachet.carp.triggeredtask data that CAMS adds when the task starts. CAMS also prints [CAMS INFO] and [CAMS WARNING] lines, because its default log level shows both.
One of the warnings says that CAMS ignores the resume() call: in CAMS 2.5.1, the study's executor cannot resume yet at that point. The probes start anyway, because CAMS resumes the tasks on the phone when the phone's device manager connects.
The iOS Simulator has no barometer. There, each probe logs a warning like this one after 5 seconds and collects nothing:
Add app task types
An app task is a task that the participant performs in the app, such as a survey. A package that adds an app task type registers four things in onRegister():
| Register | Why |
|---|---|
The AppTask subclass, if the type has one | CAMS saves app tasks in the deployment and in the task queue. |
A CompletedAppTask with the type | Every app task adds a measure of type dk.cachet.carp.completedapptask.<type>. CAMS registers that JSON type only for its built-in task types. |
| Each data class that a task returns as its result | The completed-task measurement holds the result. |
A UserTaskFactory for the type | Without a factory, CAMS does not queue a task of the type when it is triggered. Register it with AppTaskController().registerUserTaskFactory(). |
For a package class DiarySamplingPackage with a task type constant DIARY_TYPE and a result class DiaryEntry, onRegister() looks like this:
The factory serves the types in its types list. Each time CAMS triggers an app task of one of them, the factory creates a user task, the runtime form of the app task:
CAMS picks the factory by the type string, and a later factory for the same type replaces an earlier one. Use a type string of your own, not a built-in one such as survey, which the factory of the survey package serves.
For the user task itself, see Extending the app task model. In its onStart(), call super.onStart() before you resume backgroundTaskExecutor, which super.onStart() initializes.
Next steps
Devices and services
Add a device manager for a wearable or an online service.
Privacy and data formats
Protect sensitive values and convert data to other formats.
Testing and publishing
Test the package, run it in an app and publish it.