Skip to content
CARP Docs
Ask AISystemLightDark
On this page

Quickstart

Build an app that finds a Movesense sensor, connects to it and prints its heart rate.

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

The finished app connects to the first Movesense sensor it finds and prints the sensor's average heart rate after each heartbeat.

Prerequisites

  • The setup in Installation. The example reuses the function from Request permissions at runtime.
  • A Movesense MD, Movesense Active HR+ or Movesense HR2 sensor on its strap or electrodes. See Movesense Plus for the supported sensors.
  • An Android phone or iPhone with Bluetooth turned on. Emulators and simulators do not work. See Installation.

Complete example

Add the plugins that the example uses to request the Android permissions:

flutter pub add permission_handler device_info_plus

permission_handler requires compileSdk 37 or later. See Request permissions at runtime.

Replace the contents of lib/main.dart with:

lib/main.dart
import 'dart:io';

import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:movesense_plus/movesense_plus.dart';
import 'package:permission_handler/permission_handler.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(
    const MaterialApp(
      home: Scaffold(body: Center(child: Text('Streaming heart rate'))),
    ),
  );

  // 1. Ask for the Bluetooth permissions on Android
  if (await requestBluetoothPermissions()) { 
    await streamHeartRate();
  } else {
    print('Bluetooth permissions denied');
  }
}

Future<void> streamHeartRate() async {
  final movesense = Movesense();

  // 2. Scan, and take the first Movesense device found
  final found = movesense.scanResults
      .handleError((Object error) => print('Scan error: $error'))
      .first;
  await movesense.startScan();
  final device = await found;
  await movesense.stopScan();

  // 3. Connect, and wait until the device is ready
  final connected = device.statusEvents
      .firstWhere((status) => status == MovesenseDeviceStatus.connected);
  await device.connect();
  await connected;

  // 4. Stream heart rate
  device.hr.listen((hr) => print('Heart rate: ${hr.average} bpm'));
}

Future<bool> requestBluetoothPermissions() async {
  if (!Platform.isAndroid) return true; // iOS shows its own prompt

  final android = await DeviceInfoPlugin().androidInfo;
  final permissions = android.version.sdkInt >= 31
      ? [Permission.bluetoothScan, Permission.bluetoothConnect]
      : [Permission.locationWhenInUse];

  final statuses = await permissions.request();
  return statuses.values.every((status) => status.isGranted);
}

How it works

StageWhat it does
1. PermissionsAsks for access to nearby devices on Android 12 or later, and for location on Android 11 and lower. On iOS, the function returns true, because iOS asks for Bluetooth access by itself when the app starts.
2. ScanstartScan() on a Movesense client scans for sensors that advertise the Movesense services. scanResults emits a MovesenseDevice for each advertisement, and drops the ones that arrive while nothing listens. The example listens before it starts the scan, stops the scan at the first sensor and prints scan errors with handleError().
3. Connectconnect() returns as soon as the platform accepts the command. When the sensor is connected, the plugin reads its device information and battery level, and statusEvents emits connected. The example listens to statusEvents before it calls connect(), so that it cannot miss the event.
4. Streamhr subscribes to the /Meas/HR resource through the Movesense mobile library (MDS). It emits a MovesenseHR after each heartbeat. Its average is the average heart rate in beats per minute, or 0.0 when the sensor has no valid heart rate.

On iOS, a scan that starts before Bluetooth is ready reports an error, for example at the first launch, before the user allows Bluetooth access. The plugin then starts the scan by itself once Bluetooth is ready, so the example prints the error and keeps waiting.

The example waits forever when it finds no sensor or the sensor never connects. On Android, a failed connection sets the status to MovesenseDeviceStatus.error, which the example ignores. On iOS, a failed attempt reports no error, and the status stays connecting.

To give up after 30 seconds, replace stages 2 and 3, and import dart:async for TimeoutException:

import 'dart:async';

// ... the other imports, main() and the start of streamHeartRate()

// 2. Scan, and give up after 30 seconds
final found = movesense.scanResults
    .handleError((Object error) => print('Scan error: $error'))
    .first
    .timeout(const Duration(seconds: 30));
await movesense.startScan();
final MovesenseDevice device;
try {
  device = await found;
} on TimeoutException {
  print('No sensor found');
  return;
} finally {
  await movesense.stopScan();
}

// 3. Connect, and give up on an error or after 30 seconds
final outcome = device.statusEvents
    .firstWhere((status) => status != MovesenseDeviceStatus.connecting)
    .timeout(
      const Duration(seconds: 30),
      onTimeout: () => MovesenseDeviceStatus.error,
    );
await device.connect();
if (await outcome != MovesenseDeviceStatus.connected) {
  print('Could not connect to ${device.address}');
  await device.disconnect();
  return;
}

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. For connection timeouts, see Connect to a sensor.

Run the app

  1. Put on the sensor

    Wear the sensor on its strap or electrodes, so that it can measure heart rate.

  2. Start the app on your phone

    flutter run

    On Android 12 or later, the app asks for access to nearby devices; on Android 11 and lower, it asks for location. On iOS, it asks for Bluetooth access. Allow them.

  3. Check the console

    When the sensor is connected, the console prints Heart rate: <average> bpm after each heartbeat. On iOS, the first launch may also print a scan error, because the scan starts before you allow Bluetooth access:

    Scan error: MdsError(status: 0, error: Bluetooth is not powered on.)

    The plugin starts the scan by itself once you allow access.

The plugin's example app has tabs that scan for sensors, connect to one, send MDS requests, subscribe to resources and show a log.

Troubleshooting

Nothing is printed

Keep the sensor close to the phone, check that Bluetooth is on, and run the app again. The example waits without a message until it finds a sensor and the sensor connects. Other causes:

  • On Android 11 and lower, location is turned off in the phone's settings. See Installation.
  • The connection failed or never completed. Add the timeouts from How it works.
  • You used hot restart. It restarts the Dart code but not the plugin's native code, which keeps any connection to the sensor. Stop the app and run it again.
The console shows a scan error

Turn on Bluetooth, and allow Bluetooth access for the app. On Android, run the app again; on iOS, the plugin then starts the scan by itself.

For each scan error and its cause, see Scan for sensors.

A stream fails with a StateError

Read device.hr after the status is connected. The plugin checks the connection when you read a stream, not when you listen to it, so a stream read too early emits:

Bad state: Movesense device <address> is not connected.
The connection to the sensor fails

Move the sensor closer to the phone, and run the app again. To see why a connection failed on Android, print the connection events. See Follow all connections.

On iOS, a failed attempt reports no error, so only a timeout ends it.

Next steps

Connecting a device

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

Sensor data

Stream heart rate, ECG, IMU and temperature data.

Device status

Read device information, battery level and system states.

MDS requests

Send requests and subscriptions to any Movesense resource.

Quick links

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