93 lines
3.8 KiB
Markdown
93 lines
3.8 KiB
Markdown
# The wire format
|
||
|
||
One byte-array tuple over AppMessage, defined here and implemented twice — in
|
||
`src/pkjs/index.js` (writer) and `src/c/main.c` (reader). **If you change one, change
|
||
the other and the version byte.** A silent mismatch between them decodes into garbage
|
||
rooms rather than failing, which is why the version byte exists at all.
|
||
|
||
## Why a byte array and not a dictionary
|
||
|
||
Pebble's guaranteed AppMessage buffers are small (124 in / 636 out documented minimum,
|
||
~2 KB in practice for a JS-backed app), and a dictionary costs ~7 bytes of overhead per
|
||
tuple. A ten-room plan as one tuple per room would spend more on keys than on rooms. As
|
||
one packed array it is a few hundred bytes with room to spare.
|
||
|
||
## Coordinates are quantised to a byte
|
||
|
||
Room polygons are already stored normalised 0.0–1.0 by `identity`. Multiply by 255 and
|
||
round: a vertex costs 2 bytes and the error is under half a percent of the plan's
|
||
width, which on a 170 px inscribed box is sub-pixel. The phone does the projection into
|
||
the watch's inscribed box; the watch does no floating point at all.
|
||
|
||
## Layout
|
||
|
||
```
|
||
header
|
||
u8 version 1
|
||
u8 room_count
|
||
u8 unplaced_count
|
||
u8 flags bit0: positions are available on this level
|
||
|
||
per room (room_count times)
|
||
u8 name_len
|
||
u8[] name UTF-8, truncated to 24 bytes by the writer
|
||
u8 state 0 = empty, 1 = occupied, 2 = unknown (drawn, never reported)
|
||
u8 vertex_count
|
||
u16[] vertices x,y each u8, so 2 bytes per vertex
|
||
u8 target_count anonymous radar targets — somebody is there, nobody knows who
|
||
u16[] targets x,y each u8
|
||
u8 occupant_count
|
||
per occupant
|
||
u8 colour_index 0..7 into identity's PERSON_COLORS, 255 = unknown
|
||
u8 initial ASCII
|
||
u8 occ_flags bit0: has a fused position
|
||
u16 position x,y each u8 — PRESENT ONLY when bit0 is set
|
||
u8 name_len
|
||
u8[] name UTF-8, truncated to 16 bytes
|
||
|
||
per unplaced person (unplaced_count times)
|
||
u8 colour_index
|
||
u8 initial
|
||
u8 name_len
|
||
u8[] name
|
||
```
|
||
|
||
## Truncation, and why the writer does it
|
||
|
||
The writer builds the payload and, if it exceeds the inbox size, sheds in this order:
|
||
|
||
1. **Occupant names** — the detail screen loses full names and falls back to initials.
|
||
Worst outcome of the three and still legible.
|
||
2. **Rooms with no occupants** — an empty room is the least informative thing on the
|
||
plan.
|
||
3. **Whole levels** — refuse, and say so on the watch.
|
||
|
||
It never silently sends a partial structure. A payload that decodes half-way is worse
|
||
than one that does not arrive, because the watch cannot tell the difference between
|
||
"three rooms" and "three rooms and then the buffer ran out".
|
||
|
||
## What is NOT in this format: the toggles line
|
||
|
||
The toggles screen (README.md) sends its own message, as plain text:
|
||
|
||
```
|
||
name|1|Loggia\nname|0|Desk
|
||
```
|
||
|
||
One record per line, three fields: name, `1`/`0` for on, and the detail string the watch
|
||
displays. The watch replies with the row INDEX it was looking at, never an id, so no
|
||
identifier string has to be stored on the watch or copied back.
|
||
|
||
It is deliberately not squeezed into the binary format above. That format exists because
|
||
a floorplan does not otherwise fit in a ~2 KB AppMessage; a handful of short strings with
|
||
no geometry gains nothing from it and would lose the readability that makes the format
|
||
worth auditing. What the two share is the discipline: the packer (`src/pkjs/index.js`)
|
||
and the parser (`src/c/main.c`) are round-tripped against each other in
|
||
`test/run-tests.sh`, including the two cases that would otherwise decode as plausible
|
||
garbage — a value containing the `|` separator, and a record truncated mid-way.
|
||
|
||
Field lengths are capped by the writer to the C buffers' sizes (`TOGGLE_NAME_LEN`,
|
||
`TOGGLE_DETAIL_LEN`), so truncation happens once, on the phone, rather than differently
|
||
on each side.
|
||
|