Omi Device BLE Protocol

Shared contract for device SDKs (sdks/python, sdks/swift, sdks/react-native, sdks/device/*).

GATT

Role UUID
Omi service 19b10000-e8f2-537e-4f6c-d104768a1214
Audio data stream (notify) 19b10001-e8f2-537e-4f6c-d104768a1214
Audio codec (read) 19b10002-e8f2-537e-4f6c-d104768a1214
Battery service 0000180f-0000-1000-8000-00805f9b34fb
Battery level 00002a19-0000-1000-8000-00805f9b34fb

Codec IDs (first byte of codec characteristic)

ID Codec Firmware
0 PCM 16-bit
1 PCM 8-bit
20 / 0x14 Opus — 160-sample PDM frames (10 ms) @ 100 fps DevKit (omi/firmware/devkit/src/config.h)
21 / 0x15 Opus FS320 (opus_fs320) — 320-sample PDM frames (20 ms) @ 50 fps Omi CV1 (omi/firmware/omi/src/lib/core/config.h)

Both Opus IDs decode to the same PCM contract; they differ only in frame duration, so a decoder that assumes 10 ms frames mis-times a CV1 stream. Codec/frame parity source of truth is the app's BleAudioCodec (app/lib/backend/schema/bt_device/bt_device.dart).

Default stream assumption in thin SDKs: Opus @ 16 kHz mono. The OPUS_FRAME_SAMPLES = 960 constant each SDK exports is a decode buffer bound for opus_decode, not a wire frame size — the wire frame is 160 or 320 samples per the table above.

Audio packet framing

Notify payload on audio data characteristic:

[3-byte header][codec payload...]

Device SDKs strip the first 3 bytes before Opus/PCM decode (matches sdks/python/omi/decoder.py).

Sample rate

PCM output: 16-bit little-endian mono @ 16_000 Hz.

Header size and codec map are firmware-coupled — change here and in every device SDK together.