Skip to content
CARP Docs
Ask AISystemLightDark
On this page

Device status

Read the device information, model, battery level and system states of a connected sensor.

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

Once a sensor is connected, your app can check which model it is and which firmware it runs. It can also check whether the battery is low, and follow states such as taps and the attachment to a strap. MovesenseDevice reads all of this from the sensor through the Movesense mobile library (MDS):

MemberTypeResource
deviceInfo, getDeviceInfo()MovesenseDeviceInfo?/Info
deviceTypeMovesenseDeviceType/Info
batteryLevel, getBatteryStatus()MovesenseBatteryLevel/System/States/1
getState()MovesenseState?/System/States/<state_id>
getStateEvents()Stream<MovesenseState>/System/States/<state_id>

The samples on this page assume a connected MovesenseDevice named device. To connect one, see Connecting a device.

Read the device information

deviceInfo holds the /Info resource of the sensor as a MovesenseDeviceInfo. MovesenseDevice reads it when the sensor connects.

If that read fails, the connection still completes, and deviceInfo may be null. deviceInfo keeps its value after the sensor disconnects.

getDeviceInfo() reads /Info again and caches the result in deviceInfo. It returns null when the sensor is not connected, and throws an MdsError when MDS reports an error:

try {
  final info = await device.getDeviceInfo();
  print('${info?.productName} ${info?.serial} runs firmware ${info?.sw}');
} on MdsError catch (error) {
  print('Reading /Info failed with status ${error.status}: ${error.error}');
}

MovesenseDeviceInfo has these fields:

FieldTypeDescription
manufacturerNameStringThe manufacturer, such as Movesense Ltd.
brandNameString?The brand name of the sensor.
productNameStringThe product name, such as Movesense.
variantStringThe internal name of the firmware variant, such as Avocado.
designStringThe product design name, such as black.
hwCompatibilityIdStringIdentifies which firmware can be installed on the sensor.
serialStringThe unique 12-digit serial number of the sensor. MDS addresses the sensor by it.
pcbaSerialStringThe unique identifier of the printed circuit board assembly (PCBA).
swStringThe firmware version, such as 2.1.5.
hwStringThe hardware type, such as H4. deviceType derives the model from it.
additionalVersionInfoString?More version information, converted to a string.
addressInfoList<MovesenseAddressInfo>The addresses of the sensor. Each has a name, which is the address type such as BLE or DFU-BLE, and an address.
apiLevelStringThe Movesense API level, such as 1.

A missing or null value in the response becomes an empty string, or an empty list for addressInfo. Only brandName and additionalVersionInfo can be null. Other fields of the response, such as hwConfig, are dropped.

The plugin's tests use this /Info response of a Movesense HR2:

{
  "Content": {
    "manufacturerName": "Movesense Ltd",
    "brandName": null,
    "productName": "Movesense",
    "variant": "Avocado",
    "design": "black",
    "hwCompatibilityId": "C",
    "serial": "233830000816",
    "pcbaSerial": "80413336500828200000100022666H4233",
    "sw": "2.1.5",
    "hw": "H4",
    "additionalVersionInfo": null,
    "addressInfo": [
      { "name": "BLE", "address": "0C-8C-DC-1B-23-BF" },
      { "name": "DFU-BLE", "address": "E9-AC-4C-25-9A-65" }
    ],
    "apiLevel": "1"
  }
}

For a MovesenseDeviceInfo that you build yourself, for example from a raw request or from stored JSON, use these members:

MemberDescription
MovesenseDeviceInfo.fromMovesenseData()Builds the information from a decoded /Info response, with or without its Content wrapper.
MovesenseDeviceInfo.fromJson()Builds the information from the bare content object, the form that toJson() writes.
toJson()Returns the fields in the table above as a map, with addressInfo as a list of maps with name and address.
deviceTypeDerives the model from hw with the same mapping as MovesenseDevice, so it also works for stored information without a connected sensor.

To keep the device information between app starts, encode it as JSON:

lib/device_info_json.dart
import 'dart:convert';

import 'package:movesense_plus/movesense_plus.dart';

String encodeDeviceInfo(MovesenseDeviceInfo info) => jsonEncode(info.toJson());

MovesenseDeviceInfo decodeDeviceInfo(String json) =>
    MovesenseDeviceInfo.fromJson(jsonDecode(json) as Map<String, dynamic>);

Identify the model

deviceType derives the model of the sensor, a MovesenseDeviceType, from the hw field of deviceInfo:

hwdeviceTypeSensor
A1mdMovesense MD
H3hrPlusMovesense Active HR+
H4hr2Movesense HR2
Any other valueunknownUnrecognized hardware

The comparison ignores case, and deviceType is unknown while deviceInfo is null. MovesenseDeviceType.fromHardwareType() derives the model from a raw hw string in the same way. The enum also has flash for the Movesense FLASH sensor, but the plugin never derives it.

This switch turns the model into a name to show the user:

final model = switch (device.deviceType) {
  MovesenseDeviceType.md => 'Movesense MD',
  MovesenseDeviceType.hrPlus => 'Movesense Active HR+',
  MovesenseDeviceType.hr2 => 'Movesense HR2',
  MovesenseDeviceType.flash || MovesenseDeviceType.unknown => 'Unknown model',
};
print('Connected to $model ${device.serial}');

The model also tells you which data the sensor offers: the Movesense HR2 has no temperature sensor. See Stream temperature.

Read the battery level

batteryLevel holds the battery state as a MovesenseBatteryLevel: ok, low or unknown. /System/States/1 reports only whether the battery is OK or low, not a percentage.

MovesenseDevice reads the level when the sensor connects and does not read it again by itself. disconnect() and a disconnected event reset it to unknown. To refresh the level, call getBatteryStatus(), which reads /System/States/1 and caches the result in batteryLevel:

final level = await device.getBatteryStatus();
if (level == MovesenseBatteryLevel.low) {
  print('The sensor battery is low');
}

getBatteryStatus() does not throw for MDS errors. It returns unknown when the sensor is not connected or answers with an MDS error. It also returns unknown for any value other than 0 and 1.

The Movesense API also defines /System/Energy/Level, which gives an estimated charge in percent. To read it, see Send a request.

Read a system state

The system states of a sensor describe its movement, battery, connector, taps and free fall. getState() reads the current state of one MovesenseSystemState component from /System/States/<state_id>.

It returns a MovesenseState, or null when the sensor is not connected or answers with an MDS error. The error may mean that the sensor does not support the component.

Each component has a StateId in the Movesense API. The plugin decodes the raw value into a MovesenseDeviceState:

ComponentStateIdRaw valuesDecoded state
movement00 not moving, 1 movingnotMoving for 0, otherwise moving
battery10 OK, 1 lowAlways unknown
connectors20 disconnected, 1 connected to gear, 2 unknowndisconnected for 0, otherwise connected
doubleTap30 normal, 1 double-tapped, 2 unknowndoubleTap for 1, otherwise unknown
tap40 no tap, 1 tapped, 2 unknowntap for 1, otherwise unknown
freeFall50 under acceleration, 1 free fall, 2 unknownacceleration for 0, otherwise freeFall

stateId returns the StateId of a component, for example to build the resource '/System/States/${MovesenseSystemState.connectors.stateId}' for an MDS request.

The decoded state hides some raw values, and some states are hard to read with getState():

  • Connectors: the unknown value 2 decodes as connected. To tell an unknown state from a connected one, check value.
  • Free fall: the unknown value 2 decodes as freeFall. To tell an unknown state from a free fall, check value.
  • Battery: state is always unknown. Read value and map it with MovesenseBatteryLevel.fromStateValue(), or use getBatteryStatus().
  • Movement: a read may return null unless the accelerometer is running, for example while you stream inertial measurement unit (IMU) data. See Stream IMU data.
  • Tap and double tap: the value is 1 when the sensor detects a tap and returns to 0 right after, which decodes as unknown. Follow taps with state events.

MovesenseState has these fields:

FieldTypeDescription
componentMovesenseSystemStateThe component that the state belongs to.
stateMovesenseDeviceStateThe decoded state.
valueintThe raw value that the sensor reported.
timestampint?The sensor's timestamp in milliseconds for state events, and null for getState() reads.

This read checks the connector through value:

final connectors = await device.getState(MovesenseSystemState.connectors);
if (connectors == null) {
  print('The sensor did not report its connector state');
} else if (connectors.value == 1) {
  print('The sensor is connected to gear, such as a strap');
}

Subscribe to state events

getStateEvents() subscribes to one component and emits a MovesenseState each time its state changes:

final taps = device
    .getStateEvents(MovesenseSystemState.tap)
    .where((event) => event.state == MovesenseDeviceState.tap)
    .listen(
      (event) => print('Tapped at ${event.timestamp} ms'),
      onError: (Object error) => print('Tap events failed: $error'),
    );

// ... when you no longer need the events
await taps.cancel();

The stream does not emit the state at the time you subscribe; if you need it, read it with getState() first. Like the data streams, it is single-subscription: listening subscribes on the sensor, and canceling unsubscribes. It emits a StateError when the sensor is not connected, and an MdsError when the sensor rejects the subscription.

The sensor allows only one subscription at a time to the motion states: movement, tap, doubleTap and freeFall. A further one fails with an MdsError, as issue #15 of mdsflutter reports, so subscribe to one motion state at a time.

Not every sensor supports every component either: the plugin's authors have confirmed only connectors and tap on the Movesense MD and Movesense HR2.

timestamp comes from the sensor's own clock, in milliseconds since the sensor started, not from the phone. See Timestamps.

Quick links

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