# Bluetooth devices

Canonical page: https://docs.ovok.com/flutter-sdk/bluetooth

`BluetoothRuntime` is the framework-free core for scanning, connecting to supported
peripherals, receiving readings, and saving them. It uses small platform ports so
device protocols and application code do not depend on a BLE plugin.

## Define device support [#define-device-support]

Each definition matches an advertisement and turns the peripheral's GATT stream into
Ovok measurements:

```dart
defineDevice(
  BluetoothDeviceDefinition(
    key: 'portable-scale',
    matches: (advertisement) =>
        advertisement.serviceUuids.contains('scale-service-uuid'),
    protocol: (link) async* {
      await for (final bytes in link.subscribe(
        'scale-service-uuid',
        'weight-characteristic-uuid',
      )) {
        yield decodeScaleReading(bytes);
      }
    },
  ),
)
```

The `sigProtocols` map provides decoders for the standard Bluetooth SIG blood pressure,
temperature, weight, pulse oximetry, heart rate, and glucose services. The glucose protocol
requests stored records through the Record Access Control Point and omits control-solution
samples. Pass a profile protocol to a `BluetoothDeviceDefinition` when your app wants to
support a SIG-compatible peripheral.

Boso Medicus system monitors use the standard blood-pressure profile. Register them as a
custom device so the app opts into their advertised name:

```dart
final bosoName = RegExp(r'^boso medicus [0-9a-f]{6}$', caseSensitive: false);
final bosoSupport = defineDevice(
  BluetoothDeviceDefinition(
    key: 'boso-medicus-system',
    matches: (advertisement) => bosoName.hasMatch(advertisement.name),
    protocol: sigProtocols['standard-blood-pressure']!,
  ),
);
```

For A&D UA-651/656/1200BLE and UB-1100BLE monitors, the UC-352BLE scale, Viatom/Lepu ZH-SG-A606
blood-glucose meters, Viatom BPW1 blood-pressure watches, Joytech and Controly
blood-pressure monitors, Veroval BPU26/BPW26 and UEBE Visomat Comfort Soft BT monitors, Joytech DET and Genial T31 thermometers,
Yonker YK-BPA1–7 and YK-IBPA1 blood-pressure monitors and YK-IRT4/BPTI 24 thermometers,
Lepu F4/F8 and S5 scales, Lescale F3/F4P/P3 scales, Lepu F5 and MY_SCALE body-weight scales,
TaiDoc TD4216 glucose meters and TD1241-family thermometers, Viatom TMB BP3-B, BP3-C1, BP3-C2,
BP2A Nexus, BP4 Flex, TMB-2288, and TMB_2088 blood-pressure monitors, GlucoCheck-compatible meters,
TeleBGM meters, Famidoc blood-pressure monitors, Famidoc contact and infrared thermometers,
Contec BC401/BC01 urine analyzers, Cofoe KF-66EP blood-pressure monitors, AOJ-70A/70B
pulse oximeters, Jumper JPD-500D family oximeters, and Viatom/Lepu OxySmart PC-60FW
pulse oximeters, add the
accepted integrated devices to `BluetoothRuntimeOptions.devices` with `builtInDevices`:

```dart
devices: [
  builtInDevices([
    IntegratedDevices.aoj20a,
    IntegratedDevices.aoj70b,
    IntegratedDevices.pc60wf,
    IntegratedDevices.andBpm,
    IntegratedDevices.andBpmCts,
    IntegratedDevices.andScale,
    IntegratedDevices.bc01,
    IntegratedDevices.cofoeBpm,
    IntegratedDevices.zhSgA606,
    IntegratedDevices.viatomBpw1Bpm,
    IntegratedDevices.viatomTmbBpm,
    IntegratedDevices.famidocBpm,
    IntegratedDevices.famidocContactThermometer,
    IntegratedDevices.famidocIrThermometer,
    IntegratedDevices.genialT31,
    IntegratedDevices.joytechBpm,
    IntegratedDevices.joytechThermometer,
    IntegratedDevices.jumperBpm,
    IntegratedDevices.jumperOximeter,
    IntegratedDevices.jumperThermometer,
    IntegratedDevices.f4,
    IntegratedDevices.lepuF4Scale,
    IntegratedDevices.s5,
    IntegratedDevices.lepuF5Scale,
    IntegratedDevices.taidocGlucose,
    IntegratedDevices.taidocThermometer,
    IntegratedDevices.yonkerBpm,
    IntegratedDevices.yonkerThermometer,
    IntegratedDevices.verovalBpm,
    IntegratedDevices.uebeVisomatComfortSoft,
    IntegratedDevices.glucoCheck,
    IntegratedDevices.bgm,
  ]),
],
```

The Jumper HA120 blood-pressure and FR302 thermometer variants are also available through
`IntegratedDevices.jumperOximeter`, `IntegratedDevices.jumperBpm`, and
`IntegratedDevices.jumperThermometer`.

Viatom TMB monitors are available through `IntegratedDevices.viatomTmbBpm`. The driver
downloads stored blood-pressure readings and leaves the device clock unchanged.

The Viatom BPW1 watch is available through `IntegratedDevices.viatomBpw1Bpm`. It reads
stored blood-pressure records without changing the device clock.

The Jumper JPD-500D oximeter reads live pulse, oxygen saturation, and perfusion index values.
It reports the last values after three seconds without a new reading and selects the GATT
service advertised by the device.

Veroval BPU26 and BPW26 monitors are available through `IntegratedDevices.verovalBpm`.
Both report triple-measurement status; BPU26 also reports atrial-fibrillation and resting-mode
status.

`IntegratedDevices.uebeVisomatComfortSoft` reads the Visomat Comfort Soft BT standard
blood-pressure profile without sending pairing or setup commands.

The Genial T31 thermometer is available through `IntegratedDevices.genialT31`.

AOJ-70A and AOJ-70B pulse oximeters are available through `IntegratedDevices.aoj70b`.
Their live pulse, oxygen saturation, and perfusion index values are reported together after
three seconds without a new value; the driver does not write setup commands.

Viatom/Lepu OxySmart PC-60FW pulse oximeters are available through
`IntegratedDevices.pc60wf`. The driver emits the last valid pulse and oxygen sample when the
device reports that the probe has been removed.

TaiDoc TD1241-family thermometers are available through
`IntegratedDevices.taidocThermometer`; the driver sets the clock before reading stored results.

`IntegratedDevices.bc01` retrieves stored urine-strip readings without deleting device
history. Flutter does not expose a clock-sync option, so keep the device clock current.

`IntegratedDevices.cofoeBpm` supports KF-66EP live blood-pressure monitors and sets the
device clock before reading measurements.

Yonker YK-BPA1–7 and YK-IBPA1 blood-pressure monitors sync their clock when connected.
YK-IRT4 and Balans Plus BPTI 24 thermometers report live readings and suppress repeats for
30 seconds.

`IntegratedDevices.aoj20a` reads AOJ-20A realtime and stored thermometer results after setting
the device clock. Stored readings retain their device timestamps.

`decodeScaleReading` is application code that validates a device frame and returns
the measurement shape accepted by `ovok.measurements`. Include `recordedAt` when the
device supplies a timestamp; the runtime uses it to recognize a resent reading.

## Create a runtime [#create-a-runtime]

Pass a scanner, a permission adapter, and a reading sink. The first device definition
that matches an advertisement handles that peripheral.

```dart
import 'package:ovok_flutter/ovok_flutter.dart';

final scanner = FlutterReactiveBleScanner();

final bluetooth = BluetoothRuntime(
  BluetoothRuntimeOptions(
    scanner: scanner,
    permissions: permissions,
    sink: readingSink,
    devices: [scaleSupport],
    pairedStorage: secureStore,
    resultPolicy: everyReading,
  ),
);
```

`BluetoothScanner` reports adapter state and discovered peripherals. A
`BluetoothPeripheral` opens a `BluetoothLink`; the link subscribes to or writes GATT
characteristics and closes the connection. `FlutterReactiveBleScanner` supplies these
scanner and GATT ports through `flutter_reactive_ble`. `BluetoothPermissions` checks,
requests, and opens system settings for the platform permissions; provide an adapter
for your app's permission library. `BluetoothReadingSink` saves readings or queues
them for offline retry.

Configure Bluetooth permissions in the host app. Add these Android manifest entries:

```xml
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
    android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
    android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"
    android:maxSdkVersion="30" />
```

Request `BLUETOOTH_SCAN` and `BLUETOOTH_CONNECT` at runtime on Android 12 and later,
and location permission on earlier versions. On iOS 13 and later add
`NSBluetoothAlwaysUsageDescription` to `Info.plist`; iOS 12 and earlier use
`NSBluetoothPeripheralUsageDescription`.

When using `osBluetoothPermissions()`, enable `PERMISSION_BLUETOOTH=1` in the
iOS Podfile's `GCC_PREPROCESSOR_DEFINITIONS`; `permission_handler` disables
permissions that the app does not enable at build time.

## Manage devices [#manage-devices]

`createBluetoothRuntime` connects Flutter's BLE scanner and OS permission adapter
to the Ovok measurement API and secure paired-device storage. The app owns the
runtime and disposes it when the signed-in session ends.

```dart
final bluetooth = createBluetoothRuntime(
  client: ovok,
  devices: [
    builtInDevices([
      IntegratedDevices.aoj20a,
      IntegratedDevices.pc60wf,
      IntegratedDevices.taidocGlucose,
    ]),
  ],
);
```

Place `BluetoothDeviceManager` inside `BluetoothProvider` and an app-owned
`Scaffold`. It scans for supported devices and lets users pair, reconnect,
disconnect, forget devices, and open the available manufacturer manuals. Paired
IDs are stored with `client.storage` when using the runtime factory.

`BluetoothDevicePicker.show(context)` must be called below `BluetoothProvider`.
The runtime must already be scanning; selecting a device starts its connection
and returns that device from the bottom sheet. It does not add a pairing.

`BluetoothProvider` refreshes Bluetooth permission when the app returns from
system settings. Call `bluetooth.dispose()` when removing the provider's runtime.

## Scan and connect [#scan-and-connect]

`startScan()` requests permission when needed and keeps scanning while Bluetooth is
on. It throws `BluetoothError` with code `permission-denied` when access is not
granted. Stop discovery with `stopScan()`; connected peripherals stay connected.

```dart
await bluetooth.startScan();
final snapshot = bluetooth.getSnapshot();
final device = snapshot.device(peripheralId);

if (device != null) {
  await bluetooth.connect(device.id);
}
```

Pair a device to reconnect when a later scan finds it. Pass `pairedStorage` to persist
paired IDs between runtime instances. `disconnect()` pauses automatic reconnect for
the current session; `forget()` removes the pairing and disconnects the device.

Listen to `subscribe()` for adapter state, permission, scan errors, nearby devices,
pairings, recent measurements, and each measurement's save state. Call `dispose()`
when the runtime is no longer needed; it stops scanning and disconnects every device.

## Select readings [#select-readings]

`everyReading` is the default. `firstReadingPerKind` keeps only the first reading of
each measurement type per connection. `dailyTotalsOnly` keeps one closed-day activity
total per type and connection. Use `policyByKind` to combine policies by
`measurementTypeKey`. `keepReadings(events, policy)` filters reading events while
preserving driver status and recoverable error events.

## Saving and retrying [#saving-and-retrying]

Each new reading is sent to `BluetoothReadingSink.save()`. The snapshot reports
`saving`, `saved`, `queued`, or `failed`; failed readings and offline queue entries
can be retried with `retrySaves()`. The runtime retains the 100 most recent readings
in its snapshot and recognizes the last 1,000 timestamped reading IDs per runtime.
