Sampling configurations
Give each measure type a default configuration, write your own configuration class and override it in a protocol.
A sampling configuration holds the settings of one measure, such as how often its probe reads the sensor. Each measure type has a default configuration, which a protocol can override for each measure. CARP Mobile Sensing (CAMS) stores the configurations of a study as JSON, so each configuration class of the package must also be registered.
The example package uses the IntervalSamplingConfiguration of CAMS for AIR_PRESSURE. For AIR_PRESSURE_SUMMARY, it has its own AirPressureSummarySamplingConfiguration, which adds the rate of the barometer to a sampling window.
Choose a configuration class
CAMS and carp_core define these configuration classes:
| Class | Fields | Defaults | Use it for |
|---|---|---|---|
NoOptionsSamplingConfiguration | None | — | A measure without settings. It is the default of a measure type whose scheme has no configuration. |
IntervalSamplingConfiguration | interval | Required | A time between samples: the polling interval of an IntervalProbe, or the sampling period of a sensor, as for AIR_PRESSURE. |
PeriodicSamplingConfiguration | interval, duration | Required | Sampling windows: every interval, the probe collects for duration. Extends IntervalSamplingConfiguration. |
PersistentSamplingConfiguration | lastTime | null | A probe that continues from its last measurement. |
HistoricSamplingConfiguration | past, future, lastTime | 1 day, 1 day, null | Reading stored data back from now, such as app usage or call logs. Extends PersistentSamplingConfiguration. |
BatteryAwareSamplingConfiguration | normal, low, critical | critical is null, the others are required | Nothing in CAMS. No probe in CAMS or the CARP packages reads it; it exists for compatibility with CARP Core. |
GranularitySamplingConfiguration | granularity | Required | Nothing in CAMS. No probe in CAMS or the CARP packages reads it; it exists for compatibility with CARP Core. |
The base class of the probe decides which class works:
IntervalProbeandBufferingIntervalStreamProbecast the configuration toIntervalSamplingConfiguration.PeriodicStreamProbe,BufferingPeriodicProbeandBufferingPeriodicStreamProbecast it toPeriodicSamplingConfiguration.StreamProbe,MeasurementProbeandProbeaccept any class and leave the reading to your code.
A subclass passes these casts too. When a probe needs settings that no built-in class has, extend the class that its base class expects. AirPressureSummarySamplingConfiguration extends PeriodicSamplingConfiguration and adds samplingPeriod.
The CARP packages do the same. BluetoothScanPeriodicSamplingConfiguration in carp_connectivity_package adds scan filters to a PeriodicSamplingConfiguration. HealthSamplingConfiguration in carp_health_package adds the health data types to a HistoricSamplingConfiguration.
For the probe base classes, see Probes.
Set the default configurations
Each measure type gets its default configuration in the samplingSchemes of the package class, next to its metadata. The example package sets both defaults in lib/barometer_package.dart:
A DataTypeSamplingScheme pairs the metadata of a measure type with its default configuration. Without a configuration, the default is a NoOptionsSamplingConfiguration, which fails the cast of a probe that expects another class. For the rest of the package class, see Sampling package class.
A default applies to every measure that a protocol adds without a configuration of its own, so choose values that suit a typical study:
AIR_PRESSUREreads once per second: air pressure changes slowly, and the motion sensors of CAMS read five times as often.AIR_PRESSURE_SUMMARYsummarizes 10 seconds every 5 minutes: the same windows asAMBIENT_LIGHTin CAMS andNOISEincarp_audio_package.
Shorter intervals and longer windows give more data and use more battery. Interval and window probes also start one interval after they resume. A measure therefore collects little data when the app restarts more often than its interval, as it may on iOS.
State the defaults in the README of your package.
CAMS combines the schemes of all registered packages into one map, with one default configuration object per measure type. Every probe and every study on the phone shares that object.
Do not change the default objects: CAMS itself writes only lastTime to them. See Read history since the last collection.
Shared sensor rate
The two barometer measures share one sensor. sensors_plus has one barometer stream and one sampling period per app, and each call to barometerEventStream() sets the period for every listener. The summary probe calls it at the start of each window.
With different rates, the summary would therefore also set the rate of AIR_PRESSURE, from its first window on. Both defaults are 1 second for this reason, and the doc comment of samplingPeriod tells protocol authors to keep the rates equal. If several of your measures read one plugin stream, give them the same default rate.
The rate is a request. On Android, the sensor treats it as a hint. On iOS, sensors_plus ignores it, because the barometer delivers readings at its own rate.
How configuration lookup works
Each time a probe reads samplingConfiguration, CAMS returns the first configuration that it finds, in this order:
- the
overrideSamplingConfigurationof the measure in the protocol; - the entry for the measure type in the
defaultSamplingConfigurationmap of the primary device; - the default in the sampling schemes of the primary device, which for the CAMS
Smartphonecovers only the measure types of CAMS itself; - the default in the sampling schemes of the registered packages.
If none applies, for example for a measure type that no registered package declares, the result is null. Step 3 never applies to the measure types of your package. A measure therefore gets the package default unless the protocol sets step 1 or 2.
CAMS reads steps 2 and 3 only from the primary device. It never reads the defaultSamplingConfiguration of a connected device, so a default set on a wearable or a service has no effect.
Configure the measures of a connected device on the measure itself, or in fields of the device configuration that your device manager reads. See Devices and services.
CAMS does not cache the result, so read samplingConfiguration where you use it, for example once in onResume().
Write a configuration class
Replace the contents of lib/barometer_configuration.dart with the configuration class of the summary measure:
- The base class:
PeriodicSamplingConfigurationgives the classintervalandduration, and lets it pass the cast ofBufferingPeriodicStreamProbe. - A default for the new field: the generated
fromJsonfunction uses the default of the constructor when the JSON has nosamplingPeriod. A protocol that was stored before you added a field therefore still loads. - The JSON members: the annotation,
fromJsonFunction, thefromJsonfactory andtoJson()work as in a data class. See Members of a data class. Rundart run build_runner buildagain after each change to the fields.
The __type comes from carp_core: every configuration class gets dk.cachet.carp.common.application.sampling. followed by its class name. Two classes with the same name in different packages therefore share a JSON type, and the one registered last decodes both. Give your class a name that no other package uses, for example one that starts with the name of its measure.
The summary measure of the example app, with its override, as it appears in a protocol. Each Duration is a whole number of microseconds:
Override the configuration in a protocol
A protocol overrides the default of one measure with the samplingConfiguration parameter of Measure, which sets its overrideSamplingConfiguration. The example app shortens the interval of the summary measure in example/lib/main.dart, so that a summary arrives within a minute:
The override leaves samplingPeriod at 1 second, the rate of AIR_PRESSURE. CAMS stores the override with the protocol and the deployment.
To change the default of a measure type for every measure in a protocol, set it in the defaultSamplingConfiguration map of the phone instead:
A task holds one measure of each type. Measures with the same type are equal, so the task constructor drops a second measure of that type. CAMS does the same when it restores a protocol from JSON.
To run one measure type with two configurations, put each measure in a task of its own.
Read history since the last collection
A history measure reads the data that a plugin or a service has stored since its last collection, such as app usage or health records. PersistentSamplingConfiguration holds lastTime, and HistoricSamplingConfiguration adds past and future. Each time the probe adds a measurement, CAMS sets lastTime to the current time in UTC and saves the deployment, at most once per second.
A history probe is a MeasurementProbe that reads the window from lastTime, or from past before now the first time, up to now. In this example, readRecords() stands for the plugin call that returns the stored data:
- The window: it starts at
lastTime, the time of the last measurement, and ends now, in UTC. A window that does not start before its end returnsnull, which happens when the clock of the phone was set back.HealthProbehas the same check; without it,APP_USAGEincarp_apps_packagefails at every collection until the clock catches up. - Failures: the probe reports a failure with
addError()and returnsnull. AnErrormeasurement would also setlastTime, so the next window would skip the records of the failed one. future: no CARP probe reads it. Leave it at its default unless your probe reads data that lies ahead, such as planned events.
lastTime is the time of addMeasurement(), not the end of the window. Records that are stored between the read and that call therefore fall between two windows.
Give history measures their own configuration
lastTime survives an app restart only in a configuration that is part of the deployment: a measure's override or an entry in the phone's defaultSamplingConfiguration. On the package default, which is one object in memory, lastTime is lost at every restart, and the probe stores the data of past again.
The APP_USAGE, PHONE_LOG, CALENDAR and HEALTH measures of the CARP packages read their data again this way when a protocol adds them without a configuration. The package default is also shared by every probe of its type, so two studies on one phone share one lastTime. Give protocol authors a method that returns the measure with its own configuration, as getHealthMeasure in carp_health_package does:
Validate configuration values
A configuration comes from a protocol that someone else writes, and that CAMS may load from a server. Check its values before the probe uses them.
For a sampling window, duration must be positive and shorter than interval. With a longer or equal duration, BufferingPeriodicStreamProbe opens a new window before the last one closes. In a test, the summaries came out wrong, and listeners stayed open after the probe paused, which keeps the sensor running.
Check the values in onResume(), report a bad configuration with addError() and return false. This addition to onResume() of AirPressureSummaryProbe does that:
Do not check the values in the constructor of the configuration class. CAMS also calls the constructor when it loads a stored deployment, and an exception there drops the study and every study stored after it. Do not check them in onInitialize() either, because a probe whose onInitialize() returns false stays Created for good, without an error measurement.
The check covers only the values. A configuration of another class makes the cast of the base class throw as soon as the probe reads it, and the probe never starts. For the base classes that cast the configuration, see Choose a configuration class.
Register the configuration class
CAMS writes the configurations of a study to JSON with the protocol and the deployment. It reads them back when it restores a study at app start and when it gets a deployment from CARP Web Services (CAWS). Apps also read them when they decode a protocol from JSON.
Register each configuration class of the package in onRegister() of lib/barometer_package.dart, next to the data classes:
FromJsonFactory uses the instance only for its fromJson function and its JSON type, so the values do not matter. CAMS registers its own configuration classes, so register only yours.
A missing registration shows up only at the next app start. CAMS then cannot decode the stored deployment, and it drops the study and every study stored after it. For why the first run works, see Register the JSON classes and privacy functions.
The JSON round trip tests catch a missing registration.
Next step
Sampling package class
Declare the measure types, create the probes and register the package.