# 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".
