Skip to content
CARP Docs
Ask AISystemLightDark
On this page

Measure types

The two eSense measure types, their timestamps and fields, and how to convert the raw motion values.

Copy MarkdownOpen inOpen in GitHubView as MarkdownOpen in Scira AIOpen in ChatGPTOpen in ClaudeOpen in Cursor

carp_esense_package has two measure types in the dk.cachet.carp.esense namespace: button events and the inertial measurement unit (IMU) data of the left earbud. Add each one to a protocol as Measure(type: ESenseSamplingPackage.<constant>), in a task that runs on an ESenseDevice:

Measure typeConstantKindAndroidiOS
dk.cachet.carp.esense.buttonESENSE_BUTTONEvent-basedYesYes
dk.cachet.carp.esense.sensorESENSE_SENSOREvent-basedYesYes

Both are event-based; see Event-based and one-time measures. They stream from the moment their task starts until it is paused.

Add them to a BackgroundTask with an ImmediateTrigger, and they start when the earbud connects. For a known issue after a lost link, see Handle disconnections.

Unlike most CARP Mobile Sensing (CAMS) data, the eSense data uses snake case JSON keys, such as device_name for the Dart field deviceName. For the measurement envelope around the data, see Measurements and data.

Sampling rate

The motion data follows the samplingRate of the ESenseDevice, in hertz from 1 to 100 (default 10). Neither measure has a sampling configuration, so overrideSamplingConfiguration has no effect, and button events do not depend on the rate.

The earbud treats the rate as a hint: depending on the Bluetooth Low Energy (BLE) connection, motion events may arrive faster or slower. The eSense BLE specification notes that high rates may need a shorter BLE connection interval. The achievable rate also drops when the earbud plays music or records audio at the same time.

Timestamps

The earbud has no clock, so all times in eSense data are phone times:

FieldButton eventsMotion
sensorStartTimeWhen CAMS created the measurement, in microseconds since the Unix epochWhen the phone received the packet, in microseconds since the Unix epoch
timestampWhen CAMS created the event, in ISO 8601 local time without a UTC offsetWhen the phone received the packet, in ISO 8601 local time with millisecond precision
sensorEndTimeNot setNot set

packet_index numbers the motion packets in the order the earbud sends them. It starts at 0 when the plugin starts listening to the sensor, so compare it only within one connection.

On Android, it runs from 0 to 255 and then starts again at 0. Use it to order packets and to find lost packets within a connection.

Button events

dk.cachet.carp.esense.button records an event each time the participant presses the button of the earbud, and another one when they release it.

Each measurement is an ESenseButton with these fields:

FieldTypeDescription
timestampDateTimeTime when CAMS created the event, in ISO 8601 local time.
deviceNameStringThe name of the earbud. JSON key device_name.
pressedbooltrue when the button is pressed, false when it is released.
Example data
{
  "__type": "dk.cachet.carp.esense.button",
  "timestamp": "2026-10-26T15:03:13.250397",
  "device_name": "eSense-0332",
  "pressed": true
}

Motion

dk.cachet.carp.esense.sensor streams the IMU of the left earbud: three accelerometer axes and three gyroscope axes per packet, at the samplingRate of the device.

Each measurement is an ESenseSensor with these fields:

FieldTypeDescription
timestampDateTimeTime when the phone received the packet, in ISO 8601 local time with millisecond precision.
deviceNameStringThe name of the earbud. JSON key device_name.
packetIndexint?Sequence number of the packet within the current connection. JSON key packet_index.
accelList<int>?Raw accelerometer values on the x, y and z axes.
gyroList<int>?Raw gyroscope values on the x, y and z axes.

Known issue

The eSense libraries that esense_flutter 1.2.0 uses decode two values wrongly, and CAMS stores the values as it receives them:

  • On Android, every accel and gyro value whose low byte is at least 0x80 is 256 too low. At the default ranges, that is 0.03 g or 3.9 degrees per second. Add 256 to such a value and wrap the result to a signed 16-bit integer, for example with (value + 256).toSigned(16) in Dart.
  • On iOS, every packet_index is 256 too high. Subtract 256.
Example data
{
  "__type": "dk.cachet.carp.esense.sensor",
  "timestamp": "2026-10-26T15:03:12.123",
  "device_name": "eSense-0332",
  "packet_index": 42,
  "accel": [-415, 8126, 1024],
  "gyro": [12, -7, 3]
}

Unit conversion

accel and gyro hold raw signed 16-bit values. To convert them, divide each value by the scale factor of the range that the IMU uses:

RangeScale factor
Accelerometer ±2 g16384
Accelerometer ±4 g8192
Accelerometer ±8 g4096
Accelerometer ±16 g2048
Gyroscope ±250 degrees per second131
Gyroscope ±500 degrees per second65.5
Gyroscope ±1000 degrees per second32.8
Gyroscope ±2000 degrees per second16.4

The eSense BLE specification gives ±4 g and ±500 degrees per second as the default ranges, with a 5 Hz low-pass filter on both sensors. CAMS does not change these settings and cannot read them. With an earbud in its default configuration, convert the values like this:

client.measurements.listen((measurement) {
  final data = measurement.data;
  if (data is ESenseSensor && data.accel != null && data.gyro != null) {
    // Scale factors of the default ranges, ±4 g and ±500 degrees per second
    final accel = data.accel!.map((value) => value / 8192).toList(); // g
    final gyro = data.gyro!.map((value) => value / 65.5).toList(); // deg/s
    print('Packet ${data.packetIndex}: $accel g, $gyro deg/s');
  }
});

On Android, correct the raw values first; see the known issue under Motion. For the orientation of the axes, see the eSense user documentation.

Quick links

Page · DocumentationCARP DocumentationPage · DocumentationIntroductionPage · DocumentationQuickstartPage · DocumentationTerminologyPage · DocumentationArchitecture