# Bluetooth on the next client (preview)

Canonical page: https://docs.ovok.com/native-sdk/guide/modules/next-bluetooth

`@ovok/native/next/bluetooth` finds supported Bluetooth devices, connects to them, collects their readings and saves each one through the [next `Ovok` client](/native-sdk/guide/modules/next). It replaces `BTProvider` and its hooks from [`@ovok/native/bt-management`](/native-sdk/guide/modules/bt-management) for apps on the next client. The Flutter SDK follows this contract, so its names match.

It is built in layers. Each layer works on its own, and each depends only on the layers below it:

| Layer | What it is |
|---|---|
| Core | `BluetoothRuntime`: scanning, connections, readings and saving, in plain TypeScript with no React and no BLE library. |
| Ports | `BluetoothScanner` (with `Peripheral` and `Link`), `BluetoothPermissions`, `ReadingSink` and an `OvokStorage`: the BLE adapter, OS permissions, where readings go and where pairings persist. |
| Drivers | `DeviceSupport`, `defineDevice` and `SIG_PROTOCOLS`: which peripherals are supported and how to talk to them. |
| Adapters | `bleManagerScanner`, `builtInDevices`, `osBluetoothPermissions`, `ovokReadingSink` and `createBluetoothRuntime`: the ports and drivers on `react-native-ble-plx`, React Native and the `Ovok` client. In browsers, [`@ovok/native/next/bluetooth/web`](/native-sdk/guide/modules/next-bluetooth-web) has the same runtime over Web Bluetooth. |
| Hooks | `useBluetooth` for actions; `useNearbyDevices`, `useMeasurements` and the others for state. |
| UI | `BluetoothProvider` and `BluetoothDevicePicker`: optional, built only from the public hooks. |

## Scan, pick, connect, save [#scan-pick-connect-save]

```tsx
import * as React from 'react';
import { Text } from 'react-native';
import { BleManager } from 'react-native-ble-plx';
import { BluetoothDevicePicker, BluetoothProvider, builtInDevices, createBluetoothRuntime, IntegratedDevices, useBluetooth, useMeasurements } from '@ovok/native/next/bluetooth';
import { ovok } from './ovok'; // the next Ovok client, signed in

const bluetooth = createBluetoothRuntime({ client: ovok, bleManager: new BleManager(), devices: [builtInDevices([IntegratedDevices.BP2])] });

export const App = () => <BluetoothProvider runtime={bluetooth}><Measure /></BluetoothProvider>;

const Measure = () => {
  const runtime = useBluetooth();
  const readings = useMeasurements();
  React.useEffect(() => { runtime.startScan().catch(() => undefined); }, [runtime]);
  return <><BluetoothDevicePicker visible={readings.length === 0} />{readings.map((r) => <Text key={r.id}>{r.save.status}</Text>)}</>;
};
```

`startScan` asks for the Bluetooth permission when it is not granted yet. Picking a device in the picker connects to it. Each reading appears in `useMeasurements` and is saved for the signed-in account with `ovok.measurements.save`. The reading's `save.status` moves from `saving` to `saved`, `queued` or `failed`.

The app owns the runtime: create it once and `await bluetooth.dispose()` when Bluetooth is no longer needed. `dispose()` stops scanning and resolves once every device has disconnected; after it, the snapshot no longer changes.

## Actions and state [#actions-and-state]

Every action is a method on the runtime, which `useBluetooth()` returns. Every hook only reads state, and re-renders when its slice changes.

| Runtime method | What it does |
|---|---|
| `startScan()` / `stopScan()` | Scans whenever the adapter is on. `startScan` asks for permission first. |
| `connect(id)` / `disconnect(id)` | Connects for this session only. |
| `pair(id)` / `forget(id)` | `pair` remembers the device in the client's secure storage and connects now, and again whenever the scan finds it. A device that keeps failing is retried with a growing delay. |
| `requestPermission()` / `checkPermission()` / `openPermissionSettings()` | The Bluetooth permission. |
| `retrySaves()` | Saves again the readings whose save failed, and flushes the readings the client queued offline. |
| `dispose()` | Stops everything. |

| Hook | Returns |
|---|---|
| `useBluetoothState()` | The adapter state: `unknown`, `on`, `off`, `unauthorized` or `unsupported`. |
| `useBluetoothPermission()` | `granted`, `denied`, `blocked` or `unknown`. Re-checked whenever the app returns to the foreground, so a permission granted in the system settings shows up. |
| `useScanning()` | Whether a scan is requested. |
| `useNearbyDevices()` / `useNearbyDevice(id)` | The supported devices found, each with its `connection` state and last `error`. |
| `usePairedDevices()` | The paired device IDs. |
| `useMeasurements()` | The recent readings, oldest first. Readings waiting offline have `save.status === 'queued'`. |

A device's `connection` is one of `idle`, `connecting`, `connected`, `measuring`, `disconnected` or `failed`. Every failure is a `BluetoothError` whose `code` says why, such as `permission-denied`, `device-not-found`, `connection-failed` or `save-failed`; the underlying error is its `cause`.

## Build your own UI [#build-your-own-ui]

`BluetoothProvider` and `BluetoothDevicePicker` are optional. The picker uses only `useNearbyDevices` and the runtime's `connect`, so any list can replace it:

```tsx
import { Button } from 'react-native';
import { useBluetooth, useNearbyDevices } from '@ovok/native/next/bluetooth';

const DeviceList = () => {
  const runtime = useBluetooth();
  const devices = useNearbyDevices();

  return devices.map((device) => (
    <Button key={device.id} title={`${device.name}: ${device.connection}`} onPress={() => runtime.pair(device.id).catch(() => undefined)} />
  ));
};
```

Without React, use the runtime directly. `subscribe` calls back after every change and `getSnapshot` returns the current state:

```ts
const stop = bluetooth.subscribe(() => render(bluetooth.getSnapshot()));
await bluetooth.startScan();
await bluetooth.connect(deviceId);
```

## Add a custom device [#add-a-custom-device]

`defineDevice` adds a device without forking the SDK. Give it a key, how to recognise its advertisement and a protocol. The protocol talks to the device over a `Link` and yields readings:

```ts
import { MeasurementTypeKey } from '@ovok/core/next';
import { builtInDevices, createBluetoothRuntime, defineDevice, IntegratedDevices } from '@ovok/native/next/bluetooth';

const acmeThermometer = defineDevice({
  key: 'acme-thermometer',
  matches: ({ name }) => name.startsWith('ACME'),
  protocol: async function* (link) {
    for await (const bytes of link.subscribe('0000feed-0000-1000-8000-00805f9b34fb', '0000beef-0000-1000-8000-00805f9b34fb')) {
      yield { measurementTypeKey: MeasurementTypeKey.bodyTemperature, bodyTemperature: bytes[0] / 10 };
    }
  },
});

const bluetooth = createBluetoothRuntime({ client: ovok, bleManager, devices: [acmeThermometer, builtInDevices([IntegratedDevices.BP2])] });
```

Every reading this thermometer sends is kept, even two equal temperatures in a row. A device that stores readings and sends them again on the next connection should set each reading's `recordedAt`: a reading with a time is recognised when it comes again, and kept once.

The first entry of `devices` whose `match` recognises a peripheral handles it. The link connects with retries, ends its subscriptions when the device disconnects, and closes when the connection is cancelled. For full control, implement `DeviceSupport` yourself: `match` returns the device key and the `Driver` that runs the peripheral. A driver can name each reading with its own `id`.

A device declared with `defineCustomDevice` from `@ovok/native/bt-management` works too: pass it to `builtInDevices`.

## What the link does for a device [#what-the-link-does-for-a-device]

After connecting and discovering services, the link reads Device Information (manufacturer name, model number and serial number). Each `NearbyDevice` then carries them as `information`, every `DeviceReading.device` repeats them, and `ovokReadingSink` saves them with the reading, together with the time the device took it (`recordedAt`) when it reported one. A custom driver sees the same values as `link.deviceInformation` and as a `DeviceEvent` of kind `information`.

A `link.write` waits until the notifications of the characteristics the link subscribed to are on, so a command is not sent before a device that requires indications is ready for it. When a read or a write fails because the device needs pairing, the link keeps the connection and tries the step again while the user finishes the system pairing dialog, for up to 30 seconds.

## Which readings are kept [#which-readings-are-kept]

`resultPolicy` decides which readings of a connection are kept and saved. The default keeps every reading. `firstReadingPerKind` keeps the first of each measurement type, `dailyTotalsOnly` keeps activity totals once their day has closed, and `policyByKind` picks a policy per measurement type. `acceptedMemorySlots([1])` keeps only the readings of those device users of a multi-user monitor or scale (the slot is the device's user index plus one, in `memorySlot`); a reading that names no user is kept. Combine it with `policyByKind` to apply it to some measurement types only.

## Replace a port [#replace-a-port]

`createBluetoothRuntime` wires the React Native defaults. To change one, build the runtime from its ports:

```ts
const bluetooth = new BluetoothRuntime({
  scanner: bleManagerScanner(bleManager),
  permissions: osBluetoothPermissions(),
  sink: ovokReadingSink(ovok), // or any ReadingSink, such as a local store
  devices: [builtInDevices([IntegratedDevices.BP2])],
  pairedStorage: ovok.storage,
});
```

A `BluetoothScanner` reports each `Peripheral` with an `openLink` that connects to it, so another BLE library, or a fake in tests, only has to implement that port.

## Android foreground service [#android-foreground-service]

To keep connections alive while the app is in the background on Android, pass `androidForegroundService` to the provider. The service runs while the provider is mounted:

```tsx
<BluetoothProvider runtime={bluetooth} androidForegroundService={{ notificationTitle: 'Measuring' }} onForegroundServiceError={report}>
```

## Platform notes [#platform-notes]

- **Browsers:** Chrome and Edge on desktop and Android, and Bluefy on iOS, run the same devices over Web Bluetooth. See [Bluetooth in browsers](/native-sdk/guide/modules/next-bluetooth-web).
- **Permissions:** Android 12 and later asks for Bluetooth scan and connect; earlier Android asks for fine location, and also needs Location turned on to find devices. iOS asks for Bluetooth.
- **Bluetooth off:** the scan waits and starts as soon as the adapter turns on. iOS cannot turn Bluetooth on for the user; show `useBluetoothState()` instead.
- **Reconnecting:** a scan reports each peripheral once, so the runtime restarts the scan when a paired device disconnects, to find it again.

## API reference [#api-reference]

Every export, with signatures and types, is in the [`next/bluetooth` API reference](/native-sdk/reference/next-bluetooth).
