Skip to main content

Step 8: Connect TeleBGM and save glucose readings

What we are building​

A Devices screen that discovers TeleBGM Gen1 BLE and remembers its pairing, plus a measurement path from the SDK’s normalized result to the authenticated Patient’s FHIR-backed observation history.

What you should already have​

  • The single-device BTProvider in the authenticated Patient route group.
  • IntegratedDevices.BGM in acceptedDevices.
  • A physical TeleBGM Gen1 BLE meter and the package configuration from step 1.

The implementation​

Add the discovery and pair screen​

Create app/(patient)/devices.tsx:

import { IntegratedDevices, useNearbyDevices, usePairedDevices } from "@ovok/native/bt-management";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useState } from "react";
import { Button, Text, View } from "react-native";

export default function DevicesScreen() {
const { devices, pair } = useNearbyDevices();
const remembered = usePairedDevices({ storage: AsyncStorage, autoPair: true });
const [message, setMessage] = useState<string>();

const meters = devices.filter(
(device) => device.deviceData.name === IntegratedDevices.BGM,
);

async function connect(deviceId: string) {
setMessage("Connecting to TeleBGM…");
try {
const device = await pair(deviceId);
if (!device) throw new Error("The meter is no longer available.");
await remembered.pair(device);
setMessage("TeleBGM is connected.");
} catch {
setMessage("Could not connect. Check Bluetooth and try again.");
}
}

async function remove(deviceId: string) {
await remembered.forget(deviceId);
setMessage("The remembered meter was removed.");
}

return (
<View>
<Text>Devices</Text>
<Text>TeleBGM Gen1 BLE measures blood glucose.</Text>
{meters.length === 0 ? (
<Text>Turn on the meter and keep it nearby while this screen is open.</Text>
) : (
meters.map((device) => (
<View key={device.id}>
<Text>TeleBGM Gen1 BLE · {device.status ?? "Available"}</Text>
{remembered.paired.includes(device.id) ? (
<Button title="Remove device" onPress={() => void remove(device.id)} />
) : (
<Button title="Connect" onPress={() => void connect(device.id)} />
)}
</View>
))
)}
{message ? <Text accessibilityRole="alert">{message}</Text> : null}
<Text>For other compatible meters, see the supported-device catalogue.</Text>
</View>
);
}

useNearbyDevices() and usePairedDevices() must run under the BTProvider. The pairing hook stores device IDs, not authentication credentials. The runtime owns connection status and emits events; it does not write measurements. See Bluetooth hooks and the Supported Device Catalog.

Save normalized results through Ovok​

Update src/features/bluetooth/patient-bluetooth.tsx from step 6 so it handles BTProvider.onResult. Keep the save in the authenticated patient area, where useCurrentPatientProfile() can provide the active Patient ID. Replace that file’s contents with:

import {
MeasurementTypeKey,
useCurrentPatientProfile,
useSaveMeasurement,
} from "@ovok/core";
import { BTProvider, type ResultCallback, IntegratedDevices } from "@ovok/native/bt-management";
import { BleManager } from "react-native-ble-plx";
import { useCallback, useRef, useState } from "react";
import type { PropsWithChildren } from "react";
import { Button, Text } from "react-native";

import { acceptedDevices } from "./accepted-devices";

const bleManager = new BleManager();

export function PatientBluetoothWorkspace({ children }: PropsWithChildren) {
const saveMeasurement = useSaveMeasurement();
const { data: currentPatient } = useCurrentPatientProfile();
const [message, setMessage] = useState<string>();
const [pendingResult, setPendingResult] = useState<ResultCallback<IntegratedDevices.BGM> | null>(null);
const [saving, setSaving] = useState(false);
const savingRef = useRef(false);
const saveResult = useCallback(async (result: ResultCallback<IntegratedDevices.BGM>) => {
const value = result.data.bloodGlucose;
const patientId = currentPatient?.profile.id;

if (result.data.measurementTypeKey !== MeasurementTypeKey.bloodGlucose) return;
if (typeof value !== "number" || !Number.isFinite(value)) {
setMessage("The meter did not provide a valid glucose value.");
return;
}
if (!patientId) {
setMessage("Your Patient profile is not ready. Reopen the measurement screen.");
return;
}
if (savingRef.current) return;

savingRef.current = true;
setSaving(true);
setPendingResult(result);
setMessage("Saving measurement…");
try {
const saved = await saveMeasurement({
patientId,
device: result.deviceData,
measurement: result.data,
...(result.data.recordedAt
? { effectiveDateTime: result.data.recordedAt }
: {}),
});
setPendingResult(null);
setMessage(
saved.status === "saved"
? "Blood-glucose reading saved."
: "Blood-glucose reading is queued for delivery.",
);
} catch {
setMessage("Could not save the reading. Check the connection, then retry this reading.");
} finally {
savingRef.current = false;
setSaving(false);
}
}, [currentPatient?.profile.id, saveMeasurement]);
const handleResult = useCallback(
(result: ResultCallback<IntegratedDevices.BGM>) => void saveResult(result),
[saveResult],
);

return (
<BTProvider
bleManager={bleManager}
acceptedDevices={acceptedDevices}
onResult={handleResult}
>
{children}
{message ? <Text accessibilityRole="alert">{message}</Text> : null}
{pendingResult ? (
<Button
title={saving ? "Saving…" : "Retry save"}
disabled={saving}
onPress={() => void saveResult(pendingResult)}
/>
) : null}
</BTProvider>
);
}

The callback uses the current SDK shape: result.deviceData plus normalized result.data. useSaveMeasurement() converts the typed measurement to FHIR Observation resource(s). The device may provide recordedAt; when it does not, the SDK uses the save time. See useSaveMeasurement and blood-glucose units and shape.

The data path is:

TeleBGM Gen1 BLE
→ BTProvider normalized bloodGlucose (mg/dL)
→ authenticated Patient ID + device metadata
→ useSaveMeasurement()
→ FHIR Observation
→ useObservations()

Handle units and context explicitly​

The current typed blood-glucose API expects mg/dL and does not convert mmol/L. Do not reinterpret or silently convert the reading. TeleBGM does not provide a fasting/pre-meal/post-meal label in this SDK shape. If a product needs that context, ask the Patient explicitly and store it only through a reviewed supported FHIR model.

Important Ovok decisions​

  • onResult is a device event callback, not a persistence API. The app explicitly calls useSaveMeasurement() and handles rejected writes.
  • The example does not enable the optional durable offline measurement queue. A failed reading is retained only in memory so the user can retry it; it is lost if the app closes before a successful save.
  • Do not log glucose values, Patient identifiers, serial numbers, or raw device data.
  • The BLE event does not establish a diagnosis and does not supply meal context.
  • The device may not expose battery level. Show it only if the SDK actually emits a battery event.

Expected result​

The authenticated Patient can discover and pair the TeleBGM meter, take a measurement, and receive a saved/queued/error state from the Ovok measurement API. A successful save appears as an Observation-backed measurement on the history screen.

Common errors and troubleshooting​

  • No TeleBGM candidate appears: keep the app in foreground, grant Bluetooth permission, wake the meter, and use a physical phone.
  • Pairing succeeds but no reading arrives: check the meter’s measurement operation and current device compatibility; this tutorial cannot simulate the BLE protocol in Expo Go.
  • A value is missing: the callback’s bloodGlucose is optional. Do not save a missing or non-finite value.
  • The UI says saved but no history appears: check useSaveMeasurement errors, the Patient ID, and Observation permissions; then refetch the Observation list.
  • A value is displayed in the wrong units: treat the SDK value as mg/dL. No mmol/L conversion occurs in this path.

Previous / Next​

Previous: build the screening home · Next: build results and history