# CGM Recipe

Canonical write-up for `cgm.html` / `cgm.ts`. Continuous Glucose Monitor — protocol-level demo. **Not for medical or dosing use.**

## Preconditions

- **iOS 15.0+** Safari with the WebBLE Safari Web Extension installed.
- Extension enabled under **Settings → Safari → Extensions → WebBLE → Allow**.
- Per-site permission set to **"Always Allow on Every Website"**.
- CGM devices usually bond before allowing service access; pair once through iOS Settings → Bluetooth if required by the manufacturer.

## Spec citation

- **Bluetooth SIG Continuous Glucose Monitoring Service 1.0.1** (CGMS 1.0.1).
  - Service UUID: `0x181F`.
  - CGM Measurement characteristic: `0x2AA7` (notify).
  - CGM Feature characteristic: `0x2AA8` (read).
  - CGM Specific Ops Control Point: `0x2AAC` (write/indicate) — **not used** in this recipe.
- Measurement frame (CGMS 1.0.1 §3.1):
  - Byte 0 — Size (total length including CRC when present).
  - Byte 1 — Flags. Bit 0 = Trend Info present; bit 1 = Quality present; bits 2–4 = Sensor Status Annunciation fields; bit 7 = E2E-CRC present.
  - Bytes 2–3 — Glucose concentration, SFLOAT-16 **little-endian**, mg/dL.
  - Bytes 4–5 — Time offset (UINT16 LE, minutes since session start).
  - Remaining bytes — optional fields gated by flags, then (if flagged) 2-byte CRC-CCITT.
- **SFLOAT-16** is defined in IEEE 11073-20601 §FLOAT-Type: high nibble = signed exponent (4-bit), low 12 bits = signed mantissa.

## Exact `requestDevice` filter

```js
{ filters: [{ services: [0x181f] }] }
```

Numeric form is used here (and in `docs-src/recipes.md`) because CGMS has no assigned name in the Bluetooth SIG "allowed services" name table that ships with Chrome/Edge. Safari's WebBLE extension accepts both numeric and string forms but numeric is the portable choice.

## GATT sequence

1. `navigator.bluetooth.requestDevice({ filters: [{ services: [0x181f] }] })`.
2. `device.gatt.connect()`.
3. `server.getPrimaryService(0x181f)`.
4. `service.getCharacteristic(0x2aa8)` → `readValue()` once to discover vendor features (bitfield; see CGMS 1.0.1 §3.2).
5. `service.getCharacteristic(0x2aa7)` → `startNotifications()`.
6. Handle `characteristicvaluechanged` events. Every frame is self-describing via byte 0 (size) and byte 1 (flags).
7. **Production requirement:** verify CRC when bit 7 of flags is set; discard the frame if it fails. The snippet below reads the raw UINT16 only and does not perform CRC validation.

## Expected output

```
feature bytes=4
glucose raw=1572
glucose raw=1584
```

The raw field is the 16-bit SFLOAT. To convert to mg/dL, decode per IEEE 11073 — the simplified integer reading is fine for demo UI but **must** be converted properly in any deployed product.

## Common errors

| Error (`name`) | Cause | Remediation |
|---|---|---|
| `SecurityError` on service discovery | CGM requires iOS-level pairing first. | Pair the device in iOS Settings → Bluetooth, then retry. |
| `NotAllowedError` | Site did not include the service UUID in its `requestDevice` call filters. | Include the exact service UUID in `filters` — CGMS is not on Chrome's default allowlist either. |
| `NetworkError` mid-session | CGM radios are designed for low duty cycles; they sleep between readings. | Reconnect on `gattserverdisconnected`; do not assume a constant GATT link. |
| CRC mismatch | Interference, partial frame. | Drop the frame; do not propagate the reading upstream. |

## Flag-byte bit-order note

CGMS 1.0.1 uses **LSB-first** bit numbering for the flags byte (bit 0 is the least-significant bit). This matches the standard Bluetooth SIG convention and Web Bluetooth `DataView` semantics when you mask with `& 0x01`, `& 0x02`, etc. Byte-ordering within multi-byte fields is **little-endian** (second argument to `getUint16` is `true`).

## Verbatim HTML snippet

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>CGM — Web Bluetooth on iPhone</title>
</head>
<body>
  <aside data-agent-notes>
    <strong>Preconditions</strong>
    <ul>
      <li>iOS 15.0+ Safari with the WebBLE extension installed.</li>
      <li>Extension enabled: Settings → Safari → Extensions → WebBLE → Allow.</li>
      <li>Per-site permission: "Always Allow on Every Website".</li>
    </ul>
    <strong>Spec</strong>: Bluetooth SIG Continuous Glucose Monitoring Service 1.0.1 (Service UUID <code>0x181F</code>, Measurement <code>0x2AA7</code> notify, Feature <code>0x2AA8</code> read). Measurement bytes 2–3 are the glucose concentration as a 16-bit little-endian SFLOAT (mg/dL).
    <p>Canonical write-up: <a href="./cgm.md">cgm.md</a>. <strong>Not for medical use.</strong></p>
  </aside>

  <h1>CGM (0x181F / 0x2AA7 + 0x2AA8)</h1>
  <button id="start">Pair CGM</button>
  <pre id="log" aria-live="polite"></pre>

  <script type="module">
    import '@ios-web-bluetooth/core/auto';

    const CGM_SERVICE = 0x181f;
    const CGM_MEASUREMENT = 0x2aa7;
    const CGM_FEATURE = 0x2aa8;

    const log = (line) => {
      const pre = document.getElementById('log');
      pre.textContent += line + '\n';
    };

    document.getElementById('start').addEventListener('click', async () => {
      try {
        const device = await navigator.bluetooth.requestDevice({
          filters: [{ services: [CGM_SERVICE] }]
        });
        const server = await device.gatt.connect();
        const service = await server.getPrimaryService(CGM_SERVICE);

        const feature = await service.getCharacteristic(CGM_FEATURE);
        const featureValue = await feature.readValue();
        log(`feature bytes=${featureValue.byteLength}`);

        const char = await service.getCharacteristic(CGM_MEASUREMENT);
        await char.startNotifications();
        char.addEventListener('characteristicvaluechanged', (ev) => {
          const v = ev.target.value;
          // Byte 0: size. Byte 1: flags. Bytes 2-3: glucose concentration (SFLOAT)
          const raw = v.getUint16(2, true);
          log(`glucose raw=${raw}`);
        });
      } catch (err) {
        log(`error: ${err.name}: ${err.message}`);
      }
    });
  </script>
</body>
</html>
```

## Canonical JS snippet (byte-identical to `docs-src/recipes.md`)

```js
const CGM_SERVICE = 0x181f;
const CGM_MEASUREMENT = 0x2aa7;

const device = await navigator.bluetooth.requestDevice({
  filters: [{ services: [CGM_SERVICE] }]
});
const server = await device.gatt.connect();
const service = await server.getPrimaryService(CGM_SERVICE);
const char = await service.getCharacteristic(CGM_MEASUREMENT);
await char.startNotifications();
char.addEventListener('characteristicvaluechanged', (ev) => {
  const v = ev.target.value;
  // Byte 0: size. Byte 1: flags. Bytes 2-3: glucose concentration (SFLOAT)
  const raw = v.getUint16(2, true);
  console.log(`glucose raw=${raw}`);
});
```

CGM frames carry a CRC and sequence number per the spec — discard frames that fail CRC when deploying to production.
