Wire Format
What the bytes on a characteristic are: a small frame header, then a CBOR message whose keys are numbers from a shared dictionary.
Overview
Every characteristic value, in both directions, is one frame. A frame is a 4-byte header followed by up to 146 bytes of payload. The payload of all the frames of one message, joined in order, is one CBOR (RFC 8949) map: the message envelope. Names that appear in the envelope and inside values travel as small integers looked up in the dictionary, so a typical state change is about eleven bytes on the air.
Nothing here needs a special library. The reference implementation, a dependency-free JavaScript package with the encoder, the framing and the generated dictionary, is what the BattleCore app uses; the same rules are a few hundred lines in any language.
1. Frame
| Byte | Meaning |
|---|---|
0 | Format marker. 0xC1 is version 1 of this format; a later version uses a different marker so the two can be told apart. |
1 | Frame id. A per-sender counter that keeps concurrent multi-frame messages apart. |
2 | Part index, from 0. |
3 | Part count. A message that fits one frame has count 1. |
4… | Payload: up to 146 bytes of the CBOR message, so a frame fits an iPhone's default 185-byte MTU. |
Requests are framed too, so a request may span several writes to CMD_IN. A receiver collects frames by id until all parts are in; a partial message older than five seconds is dropped. Frames of one message arrive in order on one characteristic, but CMD_OUT and EVENTS_OUT are separate streams.
Base64 is your library's business
2. Message envelope
A CBOR map with integer keys:
| Key | Field | Value |
|---|---|---|
0 | kind | 1 get · 2 set · 3 ok · 4 error · 5 event |
1 | ns | Dictionary id, or text for a name the device does not know |
2 | prop | Dictionary id, or text |
3 | value | Any CBOR value |
4 | id | Your correlation id (unsigned integer or text). Absent on events. |
5 | key | Error key (dictionary id or text). Errors only. |
6 | msg | Error message text. Errors only. |
7 | data | Extra error detail, usually the field at fault. Errors only. |
The rest of the documentation shows messages in their decoded form, with the field names above, because that is what you work with after decoding.
3. The CBOR subset
- Integers use the shortest integer encoding. A number that is not an integer is a float32 when that holds it exactly, else a float64. Decoders accept float16, float32 and float64.
- Strings are UTF-8 text strings. A byte string decodes to an array of numbers.
- Arrays and maps are definite-length. Indefinite-length items and tags are never sent and are rejected.
- A map key is text or an unsigned integer. An integer key is always a dictionary id; a text key is literal.
nullandundefinedare the CBOR simple values of the same name.
4. Names and the dictionary
Every name the firmware knows, namespaces, properties, state keys, module and event names, record fields and error keys, has a permanent id in the dictionary. Ids are never renumbered or reused. When you send a name the device knows, send its id; when you send one it does not know, send the text. The device does the same in reply. Both are valid everywhere a name can appear: the ns and prop fields, the error key, and the keys of any object inside value.
That rule is what makes custom events cheap and possible at once: an app can raise a module and event name of its own through fsm / emit and it simply travels as text.
5. Worked example
The event for a weapon ammo change, decoded and as bytes. With deviceState at id 11 and WEAPON_AMMO at id 168 in the dictionary:
// decoded
{ "kind": 5, "ns": "deviceState", "prop": "WEAPON_AMMO", "value": 19 }
// the message, 11 bytes
A4 map of 4 pairs
00 05 0: kind = 5 (event)
01 0B 1: ns = 11
02 18 A8 2: prop = 168
03 13 3: value = 19
// the frame that carries it (one part)
C1 07 00 01 A4 00 05 01 0B 02 18 A8 03 13The same event as the JSON this API used to speak was 50 bytes.
6. Versioning
The format marker in byte 0 is the version. This page describes marker 0xC1. Adding names to the dictionary is not a version change: an id you do not know is one you can show as a number, and a text name is always valid. Changing the frame layout or the envelope would take a new marker.