# Bluetooth

Canonical page: https://docs.ovok.com/native-sdk/guide/modules/bt-management

Use the Bluetooth module to discover supported BLE devices, connect to them, and receive normalized measurements. Your app owns the Bluetooth manager lifetime, the accepted device list, the user experience around measurement, and what happens to each result.

## Before you start [#before-you-start]

- Follow [Installation and native setup](/native-sdk/guide/how-to/installation) to install native peers and configure platform permissions.
- Test on a physical iOS or Android device. Simulators do not reproduce the full BLE scan, permission, and pairing path.
- Decide which device models the app will support. Review [all supported Bluetooth devices](/native-sdk/guide/supported-devices) and the [supported device catalog](/native-sdk/guide/components/bt-management/supported-devices/); use [custom device declarations](/native-sdk/guide/components/bt-management/custom-devices/) only when the SDK does not already implement the protocol.

## 1. Create a long-lived BLE manager [#1-create-a-long-lived-ble-manager]

Create one `BleManager` for the app or Bluetooth feature. Keep it outside a component render, or in a stable app-level ref, so a render does not replace the native manager.

~~~tsx
import { BleManager } from "react-native-ble-plx";
import {
  BTProvider,
  IntegratedDevices,
} from "@ovok/native/bt-management";

const bleManager = new BleManager();
const acceptedDevices = [
  IntegratedDevices.BP2,
  IntegratedDevices.F4,
] as const;
~~~

`acceptedDevices` is a typed allowlist. The SDK only creates handlers for entries in that list. Keep the array identity stable for the lifetime of the provider.

## 2. Mount `BTProvider` around the measurement flow [#2-mount-btprovider-around-the-measurement-flow]

`BTProvider` requests Bluetooth access, creates the managed device runtime, starts discovery, and tears it down when it unmounts. Keep it mounted for as long as the flow should own the scan and connections.

~~~tsx
export function MeasurementFlow() {
  return (
    <BTProvider
      bleManager={bleManager}
      acceptedDevices={acceptedDevices}
      onDeviceFound={(device) => device.connect()}
      onDeviceStatusChanged={({ deviceData, status }) => {
        console.log(deviceData.name, status);
      }}
      onResult={(result) => {
        const idempotencyKey = result.id;
        const measurement = result.data;

        void saveMeasurement({ idempotencyKey, measurement });
      }}
      onError={({ deviceData, error, code }) => {
        reportBluetoothError(deviceData?.name, error, code);
      }}
    >
      <MeasurementScreen />
    </BTProvider>
  );
}
~~~

By default, the permission request runs when the provider mounts. If access is denied, the provider renders its default settings prompt; supply `permissionFallback` to render your own. To wait until a patient chooses to pair, set `requestPermissionOnMount={false}` and call `useBluetoothState().requestPermission()` from a child action. The provider stays mounted, but discovery starts only after that request succeeds. See [BTProvider permission behavior](/native-sdk/guide/components/bt-management/bt-provider/) for an example.

## 3. Handle readings and connection state [#3-handle-readings-and-connection-state]

A result contains `deviceData` and normalized measurement `data`. It also has a stable `id` for deduplication. The SDK defines `id` as an enumerable property that is non-writable at runtime, so object spreads and JSON serialization include it. Pass it as an idempotency key when the app may retry uploads.

A stored record keeps its `id` whenever the device sends it again, in any time zone. A reading without a time of its own is a new measurement each time, so its `id` includes when it arrived. `legacyId` is the id SDKs before 1.16.1 gave the same reading: look it up to recognise readings an earlier version already saved.

Use `onDeviceStatusChanged` for the compact status values: `Connected`, `Disconnected`, `Measuring`, and `LowBattery`. Use `onConnectionEvent` for pairing prompts, bonding, battery values, and history transfer progress. A transfer's `progress.total` can be missing; show a count rather than a percentage unless a total is present.

The provider callbacks are app-owned boundaries. Persist, associate with a patient, validate, and upload measurements in your app. The SDK does not choose a patient or submit results to your backend.

## 4. Add a device selection and pairing experience [#4-add-a-device-selection-and-pairing-experience]

For apps that connect to whichever accepted device is nearby, enable `autoConnect`. One discovered device connects automatically after the settle delay; when multiple devices are found, handle `onDeviceSelectionRequired` and present a chooser such as [BluetoothDevicePicker](/native-sdk/guide/components/bt-management/bluetooth-device-picker/).

For a discover-and-pair screen, use [PairingFlow](/native-sdk/guide/components/ui/pairing-flow). It reads nearby devices from the provider runtime and stores remembered device IDs through the storage adapter you supply. See [Bluetooth state notice](/native-sdk/guide/components/ui/bluetooth-state-notice) to give users a clear route to enable Bluetooth or permissions.

## Keep device pairing secrets [#keep-device-pairing-secrets]

Some devices hand the phone a secret once, at pairing, and expect it back on every later connection. Transtek's white-label blood-pressure monitors, scales and activity tracker do: the device sends a 32-bit password while it is in pairing mode, and every later session proves the phone holds it. The SDK keeps these secrets in memory for the app session. Give it durable, private storage so a paired device keeps working after the app restarts:

```ts
import * as SecureStore from "expo-secure-store";
import { setDeviceSecretStorage } from "@ovok/native";

setDeviceSecretStorage({
  getItem: (key) => SecureStore.getItemAsync(key.replace(/[^A-Za-z0-9._-]/g, "_")),
  setItem: (key, value) =>
    SecureStore.setItemAsync(key.replace(/[^A-Za-z0-9._-]/g, "_"), value),
});
```

Call it once at startup, before connecting. A device without a stored secret reports a `bluetooth.pairing-required` error: put it in pairing mode (on most Transtek devices, hold the Bluetooth or memory button until the display flashes) and connect again.

## 5. Add device UI where it helps [#5-add-device-ui-where-it-helps]

The package has two device catalog approaches:

- [SupportedDevicesCatalogList](/native-sdk/guide/components/ui/supported-devices-catalog-list) shows the richer catalog with measurement types, manual links, artwork, and an optional selection callback.
- [BTDeviceList](/native-sdk/guide/components/bt-device/bt-device-list/) renders the SDK's device-status list. Pair it with [DeviceListProvider](/native-sdk/guide/components/bt-device/device-list-provider/) and optionally use [BTDeviceInstruction](/native-sdk/guide/components/bt-device/bt-device-instruction/) for an available manual.

Catalog entries are metadata. Check each entry's `acceptedDevices` before offering a connect action; entries with no accepted device declaration cannot be passed to `BTProvider`.

For ECG readings, prepare the waveform samples and sampling rate from the result data in your app, then render them with [EcgStripViewer](/native-sdk/guide/components/ui/ecg-strip-viewer). The viewer presents a trace; it does not parse a device result or interpret a rhythm.

## Lifecycle and operational choices [#lifecycle-and-operational-choices]

- Mount one provider for one active scanning scope. Unmounting it stops scanning, detaches subscriptions, and destroys its managed `BTManager`.
- Use `useScanControl` to pause and resume scanning without unmounting the provider.
- Configure [background delivery](/native-sdk/guide/modules/background-sync) separately. It requires durable storage and native background configuration; it is opt-in.
- Tune result filtering only when the product requires it. `resultPolicies` can select `all`, `daily-totals-only`, `first-reading-per-kind-per-connection`, or `series` at the device or measurement level. A device's entry can also list `acceptedMemorySlots`, so that only the readings of those device users are reported.

## Streams as series [#streams-as-series]

A device that streams, such as a heart-rate strap, a live oximeter or the Lepod, sends a value every second or so. With a `series` result policy (`LIVE_RESULT_POLICY` sets it for live devices), the manager keeps every live value in a series of one-second slots, one per device and kind of measurement, with `null` for a second nothing arrived in. A series closes when its device disconnects, after an hour, or after 15 minutes without a value, and is reported as one result: `result.series` holds every value (`{ measurementTypeKey, start, periodMs, values }`), and `result.data` each property's mean. Save it with @ovok/core's `seriesObservations`, which writes the values as `valueSampledData`.

- A disconnect ends the device's series, so it's saved straight away; reconnecting starts a new one.
- Pass `seriesStorage` (the store your results queue uses; `BTProvider` passes its background-sync storage) so a series survives the app closing. Without it, series live in memory.
- A reading dated more than two minutes from now is a stored record, not a live value, and is reported on its own. So are the single measurements `LIVE_RESULT_POLICY` keeps as `all`: blood pressure, glucose and ECG recordings.
- To show values before they are stored, read `manager.getOpenSeries()` (each with its `latest` values) and `subscribeOpenSeries`, or `useOpenSeries(manager)`. Readings still waiting in a `BackgroundSyncCoordinator` come from `getQueued()` and `subscribe`, or `useQueuedResults(coordinator)`.
- A Viatom O2Ring, Oxyfit, KidsO2 or BabyO2 on the legacy O2 protocol, kept as a series, also streams its pulse wave when the ring serves one: raw 125 Hz samples (unitless, inverted, so systolic peaks are minima), handed over in results of up to five minutes and when the device disconnects. `result.series` is `{ measurementTypeKey: bloodOxygenPulseRate, start, periodMs: 8, values: { pulseWave } }`, with `null` where the sensor lost contact or samples never arrived; the id starts `wave:`, and `result.data` holds only `measurementTypeKey` and `recordedAt`. Its saturation and pulse rate still go into the one-second series. A ring that answers without a wave is polled for saturation and pulse rate as before. The five minutes being recorded are kept in memory only, so closing the app loses them.
- Keep native permission and Bluetooth-on state distinct. Permission being granted does not guarantee that the radio is powered on.

## Glucose meters [#glucose-meters]

A Bluetooth SIG glucose meter (`standardGlucoseDevice`) keeps its readings until the app asks for them. After connecting, the SDK writes a request to the Record Access Control Point: the records after the newest one it has read, or every record when it has no store or nothing stored yet.

- Pass `recordCursorStorage` to `BTManager` (the store your results queue uses; `BTProvider` passes its background-sync storage) so the next connection asks only for newer records. A meter that refuses the sequence-number filter is asked for every record instead. The SDK stores each meter's newest sequence number under `@ovok/native/glucose-sequence/<device id>`, counting a record it withholds too.
- A meter that was reset starts numbering again, so its new records are skipped until it passes the stored number. Overwrite that entry with an empty string to download every record again.
- `result.data.bloodGlucose` is in mg/dL with one decimal, so a 5.5 mmol/L record reads 99.1 and converts back to 5.5. `reportedUnit` says which unit the meter showed.
- Each reading also carries `sequenceNumber`, `specimen` and `sampleLocation` (see `GlucoseRecordDetails`).
- When the meter flags a result above or below what it can measure, the reading has `outOfRange: "high"` or `"low"`: its `bloodGlucose` is the edge of the meter's range, so do not store it as a measurement. A low battery, a hot or cold sensor or a time fault leaves the value usable and is listed in `sensorStatus`. A record flagged as a failed test (sensor fault, too little blood, strip error, wrong strip, interrupted read, device fault) is withheld.
- The Glucose Measurement Context characteristic (meal, tester, health, exercise and medication) is not read yet.

## Viatom ER1 and BP2 history [#viatom-er1-and-bp2-history]

An ER1 or BP2 keeps its recordings until the app asks: connecting only lists them, and `device.syncHistory()` (web: `BluetoothRuntime.syncHistory(id)`) reads them over the open connection.

- Pass `recordCursorStorage` to `BTManager` (web: `builtInDevices(accepted, { recordCursorStorage })`) and return a promise from `onResult` that resolves once the reading is queued or saved, such as `return coordinator.enqueue(result)`. A later sync then opens only the files whose readings were not taken. The web runtime takes a reading once its sink saved or queued it.
- A reading whose promise rejects, or whose callback returns nothing, leaves its file to be read again on the next sync.
- The SDK stores each device's file names under `@ovok/native/lepu-delivered-files/<device id>`, only those the device still lists. Overwrite that entry with an empty string to read every file again.

## Related references [#related-references]

- [BTProvider and its callbacks](/native-sdk/guide/components/bt-management/bt-provider/)
- [Supported devices](/native-sdk/guide/components/bt-management/supported-devices/)
- [Custom devices](/native-sdk/guide/components/bt-management/custom-devices/)
- [Bluetooth components](/native-sdk/guide/components/bt-device/)
- [PairingFlow](/native-sdk/guide/components/ui/pairing-flow)
- [Background sync](/native-sdk/guide/modules/background-sync)

## API reference [#api-reference]

Every export of `@ovok/native/bt-management`, with signatures, props and types, is in the [`bt-management` API reference](/native-sdk/reference/bt-management).
