# Battery Level Recipe

Canonical write-up for `battery.html` / `battery.ts`. Read the standard Battery Service from any BLE peripheral that exposes it.

## 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"**.
- `requestDevice` called from a user gesture.

## Spec citation

- **Bluetooth SIG Battery Service 1.0** — Service UUID `0x180F`, Battery Level characteristic UUID `0x2A19`.
- Value is a single UINT8 from `0x00` to `0x64` (0–100%). Values outside that range are reserved.
- The `notify` property is **optional** per the spec; always check `characteristic.properties.notify` before calling `startNotifications()`.
- W3C Web Bluetooth CG spec, §3.4.4 *`readValue()`* and §3.4.6 *`startNotifications()`*.

## Exact `requestDevice` filter

```js
{ filters: [{ services: ['battery_service'] }] }
```

`'battery_service'` resolves to UUID `0000180f-0000-1000-8000-00805f9b34fb`.

## GATT sequence

1. `navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })`.
2. `device.gatt.connect()`.
3. `server.getPrimaryService('battery_service')`.
4. `service.getCharacteristic('battery_level')`.
5. `char.readValue()` → `DataView` of length 1. `getUint8(0)` is the percent.
6. **Optional:** if `char.properties.notify` is `true`, call `char.startNotifications()` and listen for `characteristicvaluechanged`.

## Expected output

```
78% battery
battery now 77
battery now 77
```

Many peripherals only emit a notification when the level crosses a whole-percent boundary. Some hardware only supports `read` — plan accordingly.

## Common errors

| Error (`name`) | Cause | Remediation |
|---|---|---|
| `NotFoundError` | User cancelled the picker, or no device exposes `0x180F`. | Confirm the device advertises the Battery Service; some manufacturers put battery status on a proprietary service. |
| `SecurityError` | Not a user gesture, or missing "Always Allow on Every Website". | Invoke from a click handler; re-grant site permission. |
| `NotSupportedError` (on `startNotifications`) | Peripheral advertises the char but without the `notify` property. | Fall back to periodic `readValue()` — do not attempt to subscribe. |
| `NetworkError` | GATT link dropped. | Reconnect via `gatt.connect()`; expect the new server/service/characteristic handles. |

## 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>Battery Level — 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 Battery Service 1.0 (Service UUID <code>0x180F</code>, Battery Level characteristic <code>0x2A19</code>). Single UINT8 (0–100%). Notify is optional per spec.
    <p>Canonical write-up: <a href="./battery.md">battery.md</a>.</p>
  </aside>

  <h1>Battery Level (0x180F / 0x2A19)</h1>
  <button id="start">Read battery</button>
  <pre id="log" aria-live="polite"></pre>

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

    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: ['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);
        log(`${level}% battery`);

        // Optional: subscribe if the peripheral supports notify
        if (char.properties.notify) {
          await char.startNotifications();
          char.addEventListener('characteristicvaluechanged', (ev) => {
            log('battery now ' + ev.target.value.getUint8(0));
          });
        }
      } catch (err) {
        log(`error: ${err.name}: ${err.message}`);
      }
    });
  </script>
</body>
</html>
```

## Canonical JS snippet (parity with `docs-src/recipes.md`)

```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));
  });
}
```
