Common pitfalls
The rules that keep a sampling package from losing data, with the CARP packages that broke them and the fixes.
A sampling package loses data in a few known ways. Most rules below come from a defect in a CARP sampling package or in CARP Mobile Sensing (CAMS) 2.5.1 itself.
The rules are ordered by how much data breaking them loses. The first ones stop the app or drop whole studies, and the last ones cost accuracy or privacy. Check your package against them before you publish it.
| Rule | What breaking it loses |
|---|---|
| Initialize CAMS before you register packages | Everything: the app throws at startup. |
| Register every class that CAMS stores | The study at the next start, and every study saved after it. |
| Report the device status that CAMS acts on | Every measure on the device. |
| Create every probe, and check the device at resume | The measure, for good. |
| Keep one device manager per device type | Every measure on the device. |
| Give each probe the configuration it expects | The measure, without an error. |
| Register a user task factory for each app task type | Every app task of that type. |
| Make each data type equal its measure type | The upload of the data, and its triggers and privacy functions. |
| Declare the permissions that each measure needs | The measure on Android, or the fields that need the permission. |
| Report errors and keep the rest of the data | The records after a failure, or the whole measure. |
| Get units and timestamps right | Timestamps and values that analysis can use. |
| Leave the deployment's configurations unchanged | The measure until a restart, or data that is stored again. |
| Make resume safe to repeat, and release what a probe starts | Data after a reconnect, and battery while the probe is paused. |
| Emit data as soon as it exists | A recording when the app is killed, and the first events. |
| Know when transformers run | No data, but data can leave the phone unprotected. |
Initialize CAMS before you register packages
- What breaks: the first call to
SamplingPackageRegistry()registers the built-in CAMS packages, and one of them needs CAMS to be initialized. Registering a package beforeCarpMobileSensing.ensureInitialized()throws "Null check operator used on a null value", and the app shows a blank screen. Registering afterSmartPhoneClientManager().configure()is too late:configure()restores the saved studies and app tasks, which need the package's classes and factories. - Seen in: the README of every CARP sampling package, and the doc comments of the registry and of the eSense package, leave out the initialization.
- Fix: initialize CAMS, register the packages and then configure the client, in your app, README, example and tests. Creating
SmartPhoneClientManager()also initializes CAMS. The example app'smain()keeps this order:
For the reasons behind this order, see Startup order.
Register every class that CAMS stores
- What breaks: CAMS saves protocols, deployments and measurements as JSON and reads them back through
FromJsonFactory. At the next start, the first study with an unknown class throws aSerializationException, and CAMS drops it with every study saved after it. With CARP Web Services (CAWS), one unregistered data class makes every upload attempt of its study fail. - Seen in:
carp_movesense_packagenever registers its device registration class, so a study fails to load at the first start after the sensor has connected.carp_polar_packageregisters its class, but one field holds a plugin enum that the plugin writes in one form and the package reads in another. Both packages' tests missed it: one decodes withoutFromJsonFactory, the other leaves that field empty. - Fix: in
onRegister(), register every class that a protocol, a deployment or a measurement can hold. These are device configurations and registrations, sampling configurations, app tasks and theirCompletedAppTasktypes, and data classes.
Test each class through FromJsonFactory, with every field set. The example package's test/json_test.dart does this for each class:
For the other round trips, see Test the JSON round trips.
Report the device status that CAMS acts on
- What breaks: CAMS resumes a device's tasks when its status becomes
connected, pauses them ondisconnectingand resumes them onreconnected. Any other sequence leaves the device's measures stopped.connect()also sets the status thatonConnect()returns, and does nothing while the status isconnecting. - Seen in:
carp_movisens_packagereturnsconnectingeven on success. When it has to scan for the sensor, nothing setsconnectedlater, so its tasks never start.carp_polar_packagereportsconnectedwhen the first data type arrives, so its other probes start before their data.carp_esense_packageandcarp_movesense_packagereport a lost link asdisconnected, which pauses nothing. - Fix: set
connectedwhen the device is ready,disconnectingwhen the link is lost andreconnectedwhen it is back. ReturnstatusfromonConnect(), not a constant, and put a timeout on the SDK's connect call. The thermometer example maps its link events like this:
For the complete device manager, see Devices and services.
Create every probe, and check the device at resume
- What breaks: CAMS creates and initializes the probes before it connects the devices. A measure whose
create()returnsnullnever collects, and a probe whoseonInitialize()returnsfalseignores every later resume. - Seen in:
carp_movesense_packagecreates its temperature probe only when it knows the sensor model, which it learns after connecting. The activity and heart rate probes ofcarp_movisens_packagereturnfalsefromonInitialize()until the sensor is connected. - Fix: return a probe for every measure type, and do not use
deviceManagerin the probe's constructor: the registry sets it aftercreate()returns. Check the device inonResume()or in thestreamgetter, which CAMS reads when the probe resumes. The thermometer example's probe checks it in the getter:
For when CAMS creates the probes, see Read the device in a probe.
Keep one device manager per device type
- What breaks: CAMS finds a device's manager by the type of the device configuration, and gives each probe the value of the package's
deviceManagergetter. AdeviceTypethat differs from the configuration's type leaves the device unconfigured. A getter that builds a new manager gives the probes one that CAMS never configures or connects. - Seen in: no CARP package; the rule follows from how CAMS matches devices.
- Fix: keep the manager in a
finalfield, and return the configuration'sDEVICE_TYPE, which must equaldk.carp.cams.devices.followed by the configuration's class name:
See Expose the device in the package.
Give each probe the configuration it expects
- What breaks:
IntervalProbeand the periodic and buffering probes of CAMS cast their sampling configuration withas. A measure type without a default configuration gets aNoOptionsSamplingConfiguration, so the cast throws insideonResume(). CAMS does not catch it, and the probe never starts, without an error measurement. - Seen in: the geofence probe of
carp_context_packagethrows when its measure has no configuration. The beacon probe ofcarp_connectivity_packagethrows for any other configuration class. - Fix: give every measure type whose probe needs a configuration a default of that class. Read the configuration with a type test and a fallback, and check its values in
onResume().AirPressureProbein the example package falls back to its default period:
For defaults and value checks, see Sampling configurations.
Register a user task factory for each app task type
- What breaks: CAMS queues an app task only if a user task factory is registered for its type. Without one, it logs a warning and drops the task, also when it restores the task queue at start. Each app task also adds a
dk.cachet.carp.completedapptask.<type>measure, which CAMS can decode only for its built-in task types. - Seen in:
carp_audio_packagedefines audio, video and image measures but no factory for their app task types, so those app tasks never reach the participant. - Fix: register the factory and the JSON classes in
onRegister(), so that they are in place beforeconfigure():
In the user task, call backgroundTaskExecutor.resume() after super.onStart(), which initializes the executor; executors have no start(). See Add app task types.
Make each data type equal its measure type
- What breaks: a data object's type is its
jsonType, by defaultdk.cachet.carp.followed by the class name in lowercase. CAMS stores the data under that type and matches triggers and privacy functions by it. The CAWS data manager uploads only the types that are measure types of the deployment. - Seen in: the activity and heart rate probes of
carp_movisens_packageemit subtypes such asdk.cachet.carp.movisens.activity.steps, which the CAWS data manager never uploads. Two of its classes use the data type of a third, so their data decodes as the wrong class. In CAMS itself, the data ofNON_GRAVITATIONAL_ACCELERATIONis stored asdk.cachet.carp.acceleration. - Fix: return the measure type constant from
jsonType, in a namespace that you control, and give each class its own type. Choose a last segment that does not occur earlier in the type.DataType.fromString()cuts the namespace at the first occurrence of the name, socom.example.exampleparses as the namespacecom.
The example package's AirPressure returns its measure type:
See Return the measure type from jsonType.
Declare the permissions that each measure needs
- What breaks: when CAMS deploys or restores a study, it asks only for the permissions in each measure's
CamsDataTypeMetaData. On Android, the base probes do not start without them, so a missing permission stops the measure or leaves fields empty. An extra permission asks for access that the measure does not need, and stops the measure on Android when the participant denies it. - Seen in: the Wi-Fi measure of
carp_connectivity_packagedeclares no location permission, which Android needs for the network name. The text message measure ofcarp_communication_packagedeclares the phone permission instead of the SMS permission. - Fix: declare exactly the Android permission groups that each probe needs, and leave out the permissions to reach a device. List the manifest and
Info.plistentries in your README. The measures of the example package declare none, because Android needs no permission for the barometer.
For how CAMS asks for permissions, see Declare permissions.
Report errors and keep the rest of the data
- What breaks: an exception while converting one record can lose that record, the rest of a batch or the whole measure. A
MeasurementProbewhosegetMeasurement()throws never pauses itself, so its task ignores the next trigger. Errors that a probe keeps to itself never reach the app or the data manager. - Seen in:
carp_communication_packageforce-unwraps a date that incoming text messages never have, so it loses all of them. The health probe ofcarp_health_packagereads all its data types in one call, so one failing type empties the measure. It also reports its errors on a stream that nothing listens to. - Fix: convert each record in its own
try, and report failures withaddError(), which the data manager stores as an error measurement. IngetMeasurement(), return anErrormeasurement ornullinstead of throwing.PluginRecordstands for a record of the plugin:
For error handling in probes, see Probes.
Get units and timestamps right
- What breaks: CAMS measurement times are microseconds since the epoch, and the durations in its JSON are microseconds. A
DateTimeis written with aZonly when it is in UTC. A value in the wrong unit or time zone dates every record wrong. - Seen in:
carp_communication_packagereads Android's millisecond dates as microseconds, so every call and text message is dated January 1970. The activity probe ofcarp_context_packagestamps the time the event reaches the app, not the time of the activity. The Bluetooth scan ofcarp_connectivity_packagedeclares a time span but sets no end time. - Fix: convert each plugin value with its documented unit, and use the sensor's time when it has a reliable one. Convert
DateTimevalues to UTC, and setsensorEndTimeon data that covers a period.AirPressureSummaryProbein the example package sets both times of its window:
Test each conversion against recorded plugin data, field by field. The test of carp_movesense_package checks only the type of its motion data, which holds accelerometer values where the gyroscope and magnetometer values belong.
For timestamps and units, see Data classes.
Leave the deployment's configurations unchanged
- What breaks: a sampling configuration belongs to the deployment, or is a package default that every probe of the type shares. A probe that changes one changes it for every probe and study that uses it, and fails on a
constlist. A history measure keeps itslastTimeover a restart only when its configuration belongs to the deployment. - Seen in: the health probe of
carp_health_packageremoves unsupported types from the configured list in place. On aconstlist this throws, and the measure stops until the app restarts.APP_USAGE,PHONE_LOG,CALENDARandHEALTHmeasures without their own configuration read their whole default window again after every restart, and store overlapping data. - Fix: copy a configuration's lists before you filter them, and give protocol authors a method that returns a history measure with its own configuration. Say in your README that a task holds one measure per type: CAMS collapses two measures of one type in one task into one. See Read history since the last collection.
Make resume safe to repeat, and release what a probe starts
- What breaks: CAMS calls
onResume()again on a resumed probe, so whateveronResume()starts without stopping the old one runs twice. A probe that keeps its stream gets nothing after a reconnect, and a broadcast wrapper keeps the sensor running while the probe is paused. A stream that completes closes the probe for good. - Seen in: the geofence probe of
carp_context_packageadds a location listener at every resume. Its activity probe keeps activity recognition running while it is paused. The sensor probe ofcarp_esense_packagekeeps its stream, so its data stops after a reconnect, andcarp_movesense_packagenever cancels its battery timer. - Fix: build a fresh stream in the
streamgetter, and keep one timer. Cancel a subscription before you listen again and inonPause(), and stop native work when the probe pauses.AirPressureSummaryProbein the example package ignores a second resume:
For the probe base classes that need this guard, see Resume guard.
Emit data as soon as it exists
- What breaks: CAMS stores only the measurements that a probe has added. Data that a probe holds back until it pauses is lost when the app is killed. Polling and window probes add their first measurement one interval after they resume, so an app that restarts more often than that collects nothing.
- Seen in:
carp_audio_packageadds the measurement of a recording only when its probe pauses, so a killed app leaves a file without a measurement.carp_movesense_packageskips the first 10 notifications of every subscription, which for its tap measure are the first 10 taps. - Fix: add each measurement when its data is complete, and report long recordings in parts. Never skip or replay events, and choose default intervals with the first delay in mind.
For the first delay of interval and window probes, see Set the default configurations.
Know when transformers run
- What breaks: no data is lost, but data can leave the phone unprotected. Privacy functions run only when the protocol sets
privacySchemaName, and they can run twice on the same data. A hash is not anonymization, and registering a built-in schema again drops every transformer that packages added to it. - Seen in: the READMEs of
carp_communication_packageandcarp_connectivity_packagedo not say that their privacy protection needsprivacySchemaName, and their anonymizers hash values without a salt. - Fix: write privacy functions that give the same result when they run again, and add transformers to the existing schemas. Say in your README which fields the functions change, and that they need
privacySchemaName. See Privacy and data formats.
Next steps
Testing and publishing
Test the package, run it in an app and publish it.
Extending CAMS
Add custom triggers, probes, data managers and transformer schemas.
Best practices
Android and iOS setup recommendations, permissions and troubleshooting.