# Recipes

Copy-paste, runnable snippets for the six profiles the WebBLE MCP server also emits. Every recipe assumes the extension is installed and `@ios-web-bluetooth/core` is loaded. Each uses only documented W3C surface except where explicitly marked premium.

---

## Heart rate

Standard Heart Rate profile (`0x180D`). The first byte of `heart_rate_measurement` is a flags byte; if bit 0 is set, the BPM value is uint16 little-endian starting at offset 1; otherwise it's a single uint8.

```js
const device = await navigator.bluetooth.requestDevice({
  filters: [{ services: ['heart_rate'] }]
});
const server = await device.gatt.connect();
const service = await server.getPrimaryService('heart_rate');
const char = await service.getCharacteristic('heart_rate_measurement');
await char.startNotifications();
char.addEventListener('characteristicvaluechanged', (ev) => {
  const v = ev.target.value;
  const bpm = v.getUint8(0) & 0x01 ? v.getUint16(1, true) : v.getUint8(1);
  console.log(`${bpm} bpm`);
});
```

**Spec:** Bluetooth SIG Heart Rate Service 1.0.

---

## Battery level

Standard Battery Service (`0x180F`). Single uint8 characteristic value 0–100.

```js
const device = await navigator.bluetooth.requestDevice({
  filters: [{ services: ['battery_service'] }]
});
const server = await device.gatt.connect();
const service = await server.getPrimaryService('battery_service');
const char = await service.getCharacteristic('battery_level');
const level = (await char.readValue()).getUint8(0);
console.log(`${level}% battery`);

// Optional: subscribe if the peripheral supports notify
if (char.properties.notify) {
  await char.startNotifications();
  char.addEventListener('characteristicvaluechanged', (ev) => {
    console.log('battery now', ev.target.value.getUint8(0));
  });
}
```

---

## CGM (continuous glucose monitor)

CGM Service (`0x181F`). Glucose measurement characteristic reports values in mg/dL as a 16-bit SFLOAT; most vendors also provide a simpler vendor characteristic — check your device's profile.

```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();
function decodeSFLOAT(val) {
  let mantissa = val & 0x0FFF;
  let exp = (val >> 12) & 0xF;
  if (mantissa >= 0x0800) mantissa -= 0x1000;
  if (exp >= 0x8) exp -= 0x10;
  return mantissa * Math.pow(10, exp);
}
char.addEventListener('characteristicvaluechanged', (ev) => {
  const v = ev.target.value;
  // Byte 0: size. Byte 1: flags. Bytes 2-3: glucose concentration (SFLOAT)
  const glucose = decodeSFLOAT(v.getUint16(2, true));
  console.log(`glucose mg/dL=${glucose.toFixed(1)}`);
});
```

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

---

## Lock

Many consumer smart locks expose a vendor-specific service with a write-characteristic that accepts authenticated command packets. The shape below is generic; supply your vendor's UUIDs and command frame.

```js
const LOCK_SERVICE = '0000fee0-0000-1000-8000-00805f9b34fb';
const LOCK_COMMAND = '0000fee1-0000-1000-8000-00805f9b34fb';

const device = await navigator.bluetooth.requestDevice({
  filters: [{ services: [LOCK_SERVICE] }]
});
const server = await device.gatt.connect();
const service = await server.getPrimaryService(LOCK_SERVICE);
const char = await service.getCharacteristic(LOCK_COMMAND);

// Example: 0x02 = unlock, followed by an auth token negotiated at pairing
const authToken = new Uint8Array([0xde, 0xad, 0xbe, 0xef]); // Replace with your negotiated auth token
await char.writeValue(new Uint8Array([0x02, ...authToken]));
```

Real locks require vendor authentication handshakes — consult your lock's integration spec.

---

## Beacon (premium)

Receive an iOS OS notification when a beacon with a specific service UUID is in range, even while Safari is backgrounded. Requires the WebBLE companion app.

```js
if (!('webbleIOS' in window)) {
  throw new Error('Beacon scanning requires the WebBLE companion app');
}

const registration = await window.webbleIOS.backgroundSync.registerBeaconScanning({
  filters: [{ services: ['heart_rate'] }],
  cooldownSeconds: 30,
  template: {
    title: 'Beacon in range',
    body: 'Detected {{deviceName}} at {{timestamp}}',
    url: 'https://example.com/beacon-hit'
  }
});

console.log('registered', registration.id);
// later: registration.unregister();
```

Template placeholders: `{{deviceName}}`, `{{device.id}}`, `{{value.hex}}`, `{{value.utf8}}`, `{{value.int16be}}`, `{{value.int32be}}`, `{{timestamp}}`.

---

## Peripheral chat

Advertise a custom GATT server from the web page. A central (another phone, a laptop, a sensor tag, …) scans, connects, and exchanges bytes. This is premium — no other browser exposes it.

```js
if (!('webbleIOS' in window)) {
  throw new Error('Peripheral mode requires the WebBLE companion app');
}

const CHAT_SERVICE = '12345678-1234-5678-1234-56789abcdef0';
const CHAT_CHAR    = '12345678-1234-5678-1234-56789abcdef1';

const record = await window.webbleIOS.peripheral.addService({
  uuid: CHAT_SERVICE,
  characteristics: [{
    uuid: CHAT_CHAR,
    properties: ['read', 'write', 'notify'],
    value: new TextEncoder().encode('hello')
  }]
});

window.webbleIOS.peripheral.onwriterequest = (ev) => {
  const msg = new TextDecoder().decode(ev.value);
  console.log('central wrote:', msg);
};

await window.webbleIOS.peripheral.startAdvertising({
  localName: 'WebBLE Chat',
  serviceUUIDs: [CHAT_SERVICE]
});
```

When a subscribed central is present, push bytes back with the peripheral manager's `notifyCharacteristic`-style events — see the API reference for the full event list.
