Skip to content
CARP Docs
Ask AISystemLightDark
On this page

Connecting a device

Scan for Movesense sensors, connect to one and follow its connection status.

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

Your app finds Movesense sensors with a Bluetooth Low Energy (BLE) scan. It then connects to a sensor through the Movesense mobile library (MDS), which the plugin bundles. The sensor is ready for requests and data streams once its status is connected:

sequenceDiagram
    participant A as Your app
    participant D as MovesenseDevice
    participant P as Plugin and MDS
    participant S as Sensor
    A->>P: startScan()
    Note over S: advertises
    P-->>A: MovesenseDevice on scanResults
    A->>D: connect()
    D->>P: connect(address)
    P-->>D: command accepted
    D-->>A: connect() completes
    P->>S: BLE and MDS connection
    P-->>D: bleConnected event
    P-->>D: connected event
    D->>S: GET /Info, GET /System/States/1
    S-->>D: device info, battery state
    D-->>A: statusEvents: connected
  1. Your app calls startScan() on a Movesense client. When a sensor advertises a Movesense service, scanResults emits a MovesenseDevice for it.
  2. Your app calls connect() on the MovesenseDevice. The status changes to connecting, and connect() completes as soon as the platform accepts the command.
  3. The plugin connects to the sensor. When the BLE link is up, it reports a bleConnected event, and the status stays connecting.
  4. When MDS completes the connection, the plugin reports a connected event with the serial number of the sensor. MovesenseDevice stores it in serial.
  5. MovesenseDevice reads the /Info resource into deviceInfo and the battery state into batteryLevel. The connection completes even when these reads fail.
  6. The status changes to connected, and statusEvents emits it.

For deviceInfo and batteryLevel, see Device status.

Scan for sensors

startScan() on a Movesense client starts a scan that runs until you call stopScan(). On iOS, connect() also stops it. The scan reports only sensors that advertise one of the two Movesense service UUIDs:

  • 61353090-8231-49cc-b57a-886370740041
  • 0000FDF3-0000-1000-8000-00805F9B34FB

scanResults emits a new MovesenseDevice for every advertisement it receives, so each sensor in range is reported many times. On Android, name may be null. On iOS, the plugin skips sensors without a name.

scanResults is a broadcast stream, so it drops the reports that arrive while nothing listens. Listen before you call startScan(), and deduplicate the reports by address:

final movesense = Movesense();
final sensors = <String, MovesenseDevice>{}; // address -> device

final scan = movesense.scanResults.listen(
  (device) => sensors.putIfAbsent(device.address, () => device),
  onError: (Object error) => print('Scan error: $error'),
);
await movesense.startScan();

// Collect the sensors in range for 10 seconds
await Future<void>.delayed(const Duration(seconds: 10));
await movesense.stopScan();
await scan.cancel();

The map keeps the first MovesenseDevice of each sensor. Each object tracks its own status, so connect that object and keep using it for the sensor. A sensor's name, such as Movesense 233830000816, ends with its serial number, which helps the user pick a sensor.

startScan() completes when the platform accepts the command. Scan errors arrive later on scanResults, as an MdsError with a status and an error message:

PlatformStatusMessageCause
Android0Bluetooth LE scanner is not available.Bluetooth is off, or the phone has no Bluetooth.
Android0The message of the SecurityException from Android, or Missing Bluetooth scan permission.The app does not have the Bluetooth scan permission.
AndroidThe Android error codeBLE scan failed with code <code>Android failed to start the scan. The code is one of the SCAN_FAILED_* constants of ScanCallback.
iOS0Bluetooth is not powered on.Bluetooth is off, not ready yet or not available to the app, for example after the user denied Bluetooth access. The plugin starts the scan once Bluetooth is on and the app has access.

An error does not close scanResults. On Android 11 and lower, a scan without the location permission reports no error and finds no sensors. For the permissions, see Request permissions at runtime.

Connect to a sensor

connect() starts the connection and completes as soon as the platform accepts the command, not when the sensor is connected. Follow the outcome on statusEvents, which emits each change of the MovesenseDeviceStatus:

StatusWhen it is set
disconnectedWhen you create the MovesenseDevice, when you call disconnect(), and when the plugin reports that the sensor disconnected.
connectingWhen you call connect(). It lasts while MDS connects and MovesenseDevice reads /Info and the battery state.
connectedWhen the sensor is ready for requests and data streams.
errorWhen connect() throws, or when the plugin reports a connection error for the sensor.

status holds the current status, and isConnected is true while it is connected. statusEvents is a broadcast stream that emits changes only, so listen to it before you call connect(). serial holds the serial number that MDS reported, from the connected event until the sensor disconnects or a connection error occurs.

connect() does nothing while the status is connecting or connected. When the platform rejects the command, connect() sets the status to error and throws the platform's exception. For example, it throws a PlatformException on macOS, which the plugin does not support.

This function connects to a sensor and waits up to 30 seconds for the outcome:

lib/connect_with_timeout.dart
import 'package:flutter/services.dart';
import 'package:movesense_plus/movesense_plus.dart';

Future<bool> connectWithTimeout(
  MovesenseDevice device, {
  Duration timeout = const Duration(seconds: 30),
}) async {
  if (device.isConnected) return true;

  // Listen before connecting, so that no status change is missed
  final outcome = device.statusEvents.firstWhere(
    (status) => status != MovesenseDeviceStatus.connecting,
  );

  try {
    await device.connect();
  } on PlatformException {
    return false; // the platform rejected the command
  }

  final status = await outcome.timeout(
    timeout,
    onTimeout: () => MovesenseDeviceStatus.error,
  );
  if (status == MovesenseDeviceStatus.connected) return true;

  // End the failed attempt
  await device.disconnect();
  return false;
}

The status leaves connecting when the sensor is ready, when the plugin reports an error, or when the connection drops. After an error or a timeout, disconnect() ends the attempt and sets the status to disconnected, ready for a new connect(). The timeout matters most on iOS, where the plugin reports no error when a connection attempt fails.

Connect by address

When you know the address of a sensor, for example from an earlier scan, create a MovesenseDevice for it:

final device = MovesenseDevice(address: '0C:8C:DC:1B:23:BF');

device.statusEvents.listen((status) {
  print('Connection status: ${status.name}');
});

await device.connect();

The constructor takes these parameters:

ParameterTypeRequiredDefaultDescription
addressStringYes—The address of the sensor on this platform.
nameString?NonullThe name of the sensor, such as Movesense 233830000816. The plugin does not use it to connect.
movesenseMovesense?NoA new Movesense()The client that sends the commands of this MovesenseDevice.

The address depends on the platform:

PlatformAddressExample
AndroidThe Bluetooth MAC address of the sensor, in uppercase0C:8C:DC:1B:23:BF
iOSA UUID that iOS assigns to the sensor on this phone89BD8BF8-C9E7-0395-3B1D-119082516DDE

iOS assigns the UUID the first time the phone encounters the sensor, so a UUID stored on one iPhone may not work on another phone. Store the address from a scan on the same phone; if the sensor does not connect, scan again. On iOS, the plugin reports an error event with the message Invalid device UUID. for an address that is not a UUID.

All Movesense clients created without a platform share one platform instance, so they see the same scan results and connection events. A MovesenseDevice that creates its own client therefore works like one from a scan.

To use a client with another platform, such as a fake platform in tests, pass it as movesense. See Testing without a sensor.

Disconnect

disconnect() ends the connection, or stops an attempt that is still running. It does nothing when the status is already disconnected.

Otherwise it sends the command to the platform and does not wait for the sensor. It clears serial, resets batteryLevel to unknown and sets the status to disconnected; deviceInfo keeps its last value.

The plugin does not close open data streams when a sensor disconnects, so cancel them first. To stop a data stream, see Stop a stream.

When you no longer need the MovesenseDevice, call dispose(). It stops following connection events and closes statusEvents, but it does not disconnect the sensor:

final subscription = device.hr.listen((hr) => print('${hr.average} bpm'));

// ... when you no longer need the sensor
await subscription.cancel();
await device.disconnect();
await device.dispose();

Do not use a MovesenseDevice after dispose(): its statusEvents is closed, so you cannot follow its status. To connect to the sensor again, create a new MovesenseDevice.

Handle a lost connection

When the connection to the sensor drops, the plugin reports a disconnected event, and statusEvents emits disconnected. The plugin adds no reconnection logic of its own.

On Android, call connect() again, for example with connectWithTimeout(). Your app's own disconnect() calls, such as the one in connectWithTimeout(), also set the status to disconnected, so reconnect only after a drop.

On iOS, MDS reconnects to the sensor by itself, also in the background. Unless you have disposed it, the MovesenseDevice then reads the device information again and changes to connected without a new connect() call.

The plugin does not restart data streams after a reconnection. To stream across reconnections, see Stop a stream.

Follow all connections

connectionEvents on a Movesense client emits a MovesenseConnectionEvent for every sensor, whichever client or MovesenseDevice started the connection. Use it to follow several sensors in one place. It also lets you connect without a MovesenseDevice: the client has its own connect() and disconnect() methods, which take the address.

The type of an event is a MovesenseConnectionEventType, and each type sets some of the fields address, serial and error:

TypeMeaningFields set
bleConnectedThe BLE link is up, and MDS is still setting up its connection.address
connectedMDS has completed the connection.address, serial
disconnectedThe connection has ended.On Android, address. On iOS, serial, and address when the plugin reported the connection.
errorA connection attempt failed, or the plugin rejected it.address, error

The error message comes from MDS or from the plugin, such as Already connected to <address> on Android or Invalid device UUID. on iOS. A MovesenseDevice picks the events of its sensor by address, ignoring case, or by serial. That is how a disconnected event without an address still reaches it.

This listener logs the events of every sensor:

movesense.connectionEvents.listen((event) {
  switch (event.type) {
    case MovesenseConnectionEventType.connected:
      print('Connected to ${event.serial} at ${event.address}');
    case MovesenseConnectionEventType.disconnected:
      print('Disconnected from ${event.serial ?? event.address}');
    case MovesenseConnectionEventType.error:
      print('Connection to ${event.address} failed: ${event.error}');
    case MovesenseConnectionEventType.bleConnected:
      break;
  }
});

Platform differences

AndroidiOS
  • When Bluetooth is off, startScan() reports Bluetooth LE scanner is not available. and does not scan when Bluetooth turns on later. Once Bluetooth is on, call startScan() again.
  • A scan failure reports the Android error code as status, for example 1 when startScan() is called during a scan. The running scan continues.
  • Connecting to a sensor does not stop a running scan.
  • Connecting to an address that is already connected or connecting reports an error event with Already connected to <address>. Every MovesenseDevice with that address that has called connect() then changes to error, including a connected one. The connected one loses its serial while the sensor stays connected, so its request() calls and new streams fail with a StateError. Keep one object per sensor.
  • connect() stops a running scan. To find more sensors, call startScan() again.
  • startScan() reports Bluetooth is not powered on. when Bluetooth is not ready for the app: it is off, it is still starting, or the app has no access. The plugin then starts the scan by itself once Bluetooth is on and the app has access. A startScan() call during a scan is ignored.
  • The plugin reports no error event when a connection attempt fails, so the status stays connecting. End such an attempt with a timeout, as connectWithTimeout() does.
  • A disconnected event carries the serial, and the address only when the plugin reported the connected event of the sensor.
  • After a connection drops, MDS reconnects to the sensor by itself, also in the background. See Handle a lost connection.

Quick links

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