# BLE developer protocol v1

Custom service: `b7130000-9981-4d45-8c1a-72ed765f9f11`. All integer fields are little endian. One central at a time. The development name is `RohanRing-DEV`; do not identify a device solely by its name in a shipped app.

| Characteristic | UUID first group | Access |
| --- | --- | --- |
| Command | b7130001 | Write with response, encrypted |
| Status | b7130002 | Read, encrypted |
| Audio | b7130003 | Notify, encrypted CCC configuration |

Commands: `[01, duration_ms_lo, duration_ms_hi]` starts a 100–5000 ms clip only with fresh local consent; `[02]` stops; `[03]` requests a health window if worn-state and power allow it. Unsupported lengths, offsets and opcodes are rejected. The development adapter has no worn-state sensor, so `[03]` is denied there.

Status is exactly 12 bytes:

| Byte(s) | Meaning |
| --- | --- |
| 0 | Protocol version = 1 |
| 1 | Mode: 0 idle, 1 measuring, 2 recording, 3 charging, 4 low/unknown battery, 5 fault |
| 2 | Flags: bit0 battery input valid, bit1 worn, bit2 encrypted, bit3 local consent pending |
| 3 | Adapter flags: bit0 bench power assumption; production must clear only after real measurement integration |
| 4–5 | Battery millivolts, zero if unavailable; DEV can report assumed 3800 |
| 6–7 | Most recent requested clip duration in milliseconds; not a received-sample count |
| 8–11 | Clip ID, increments on accepted start |

Audio notification: uint32 clip_id, uint32 first_sample_offset, then an even number of PCM bytes. PCM is mono signed 16-bit LE. Start at offset zero; contiguous offsets required. Offsets count samples, not bytes. No timestamp or calibrated clock correction exists in v1. Audio is not retained in flash. Do not save a transfer with missing packets as a complete clip.

For production add an explicit final clip result with total sample count, transport error, quality flags and checksum, plus an owned-device identity. Never publish health values using zero as a substitute for a missing measurement.

## A1 extension

A1 advertises `RohanRing-A1`; bench-power assumptions are disabled. Command `03` accepts fresh local physical-button consent for one 20-second raw measurement even though automatic worn-state detection is absent. Command `02` stops both health acquisition and recording. Audio requires ATT MTU >=185; health requires MTU >=35. Encryption is checked before notifications.

Health UUID `b7130004-9981-4d45-8c1a-72ed765f9f11` supports encrypted read and notify. Its 32-byte little-endian packet is:

| Offset | Format | Meaning |
| --- | --- | --- |
| 0 | u8 | Version 1 |
| 1 | u8 | Valid mask: bit 0 red/IR, bit 1 acceleration, bit 2 temperature |
| 2 | u8 | Charger status register when sampled; zero otherwise |
| 3 | u8 | Hardware fault flag |
| 4 | u32 | Sequence number, incremented per sample-bearing packet |
| 8, 12 | u32 each | Raw 18-bit red and IR readings |
| 16, 18, 20 | i16 each | Raw LIS2DW12 X/Y/Z registers; not pre-scaled g |
| 22 | i16 | TMP117 temperature /128°C |
| 24 | u32 | Uptime milliseconds, wraps at 2^32 |
| 28 | u8 | MAX30102 status when optical data sampled |
| 29–31 | bytes | Reserved, zero |

Only interpret fields whose valid bits are set. Environmental reads occur every 250 ms; optical samples appear while an explicitly armed window is active. The client must poll Status for faults even when health notifications stop. Read returns the most recent sample/fault snapshot, not a fresh conversion. No clinical metric is derived by this protocol.
