104 lines
5.0 KiB
Markdown
104 lines
5.0 KiB
Markdown
# Who's home — a Pebble watchapp
|
||
|
||
The household floorplan on your wrist, with who is in each room. Renders the same
|
||
`GET /floorplan/presence` payload as `render/floorplan-3d/`, so if the two ever
|
||
disagree, one of them is lying.
|
||
|
||
Designed in [`docs/pebble-presence-watchface.md`](../docs/pebble-presence-watchface.md);
|
||
this is the build.
|
||
|
||
## It is an app, not a face
|
||
|
||
Watchfaces get **no button events** (Select opens the app menu, Up/Down are system
|
||
shortcuts) and Pebble restricts touch to apps as well. Cycling through rooms is the
|
||
point, so it is a watchapp. What that costs, stated plainly because it is the thing
|
||
you will notice:
|
||
|
||
- You launch it from the menu rather than seeing it by raising your wrist.
|
||
- Its JS — and therefore its data — only lives while it is open, so **every launch
|
||
begins with a fetch**.
|
||
|
||
So the last plan is persisted and drawn **immediately, with its age on screen**, then
|
||
repainted when the fetch lands. A spinner on a screen that already has something true
|
||
to say is worse than slightly stale truth, clearly labelled.
|
||
|
||
A watchface variant later is a second `main()` and a build target, not a second
|
||
project: the rendering, the parser and all the JS are shared.
|
||
|
||
## Screens
|
||
|
||
**Plan** — the floorplan. Occupied rooms are drawn **light**, empty rooms dark, and
|
||
rooms HA never reports on get a **dotted edge**: "nobody is in the study" and "nothing
|
||
can see the study" are different sentences. Occupancy is carried in *lightness*, never
|
||
hue, because colour already means *who* and the two would compete on a 64-colour panel.
|
||
|
||
- `Select` → the room detail
|
||
- `Up`/`Down` → ask the phone for a fresh fetch
|
||
|
||
**Detail** — one room at a time, `Up`/`Down` cycles and wraps at both ends. Each person
|
||
is a coloured dot plus their name; the dot stays even with room for the name, because
|
||
it is what ties the line back to the marker on the plan and to the same person in the
|
||
admin panel. **"Nobody here" is printed** — a blank list reads as a fault.
|
||
|
||
The last entry in the cycle is **"Somewhere in the house"**: everyone who is home but
|
||
not locatable. They are part of the same cycle deliberately — they are exactly who you
|
||
picked the watch up to find.
|
||
|
||
## Positions, when there are any
|
||
|
||
A person fused to a single radar target (see `identity`'s position fusion) is drawn at
|
||
their coordinate. An unattributed target is a **hollow ring** — no colour, no initial,
|
||
because every visual language here for a *person* is reserved for people the system can
|
||
name. A radar target **lights the room** even with nobody named: somebody is in there.
|
||
|
||
## The wire format is the fragile part
|
||
|
||
[`WIREFORMAT.md`](WIREFORMAT.md). One packed byte array, written by
|
||
`src/pkjs/index.js` and read by `src/c/main.c` — two implementations of one format, in
|
||
two languages, that never run in the same process. Nothing but a test catches them
|
||
drifting, and drift decodes as *plausible-looking garbage* rather than as an error.
|
||
|
||
```sh
|
||
test/run-tests.sh # needs node + cc; no Pebble SDK
|
||
```
|
||
|
||
It builds a payload with the real writer, compiles the real reader against a stub SDK,
|
||
and checks the decode — including that a **truncated** payload is refused wholesale
|
||
(half a plan looks exactly like a whole one) and that a **version mismatch** is refused
|
||
rather than decoded.
|
||
|
||
That test has already earned itself once: it caught a disagreement about whether a
|
||
radar target in a room HA doesn't report should read as *occupied* or *unknown*. The
|
||
code was right (a positive detection outranks no-data); the test's expectation was
|
||
wrong.
|
||
|
||
## Building for a real watch
|
||
|
||
```sh
|
||
pebble build && pebble install --phone <ip>
|
||
```
|
||
|
||
**Unverified, in the order it will bite:**
|
||
|
||
1. **The Pebble Round 2's platform name.** `package.json` targets
|
||
`basalt/chalk/diorite/emery`; `chalk` is the *old* round (180×180) and `emery` the
|
||
old Time 2 (200×228). The new 260×260 Round 2 may need a platform this list does not
|
||
have. Nothing in `main.c` hardcodes a resolution — every position comes from
|
||
`layer_get_bounds()` — so adding the platform should be a one-line change, but the
|
||
name has to come from whatever SDK Core Devices ships.
|
||
2. **Round detection in the JS** keys off `platform === "chalk"`, and will need the new
|
||
identifier too. Get it wrong and the plan is drawn to the full rectangle on a round
|
||
screen, so the corners of the house fall off the edge.
|
||
3. **Nothing has run on a watch, in an emulator, or against a real `identity`.** The
|
||
wire format is round-trip tested and the C type-checks against a stub; that is the
|
||
whole of it.
|
||
4. **The token lives in the phone's app config.** It reads the household's presence
|
||
history — treat it as the credential it is.
|
||
|
||
## And the thing no amount of watch code fixes
|
||
|
||
This is only as right as **room-level presence**, which has never been measured in this
|
||
house. If everyone resolves to "home, room unknown", this is a picture of a floorplan
|
||
with everybody in the *Somewhere in the house* list. Test that first: the admin panel's
|
||
floorplan tab, **Live** ticked, walk between two rooms, see whether the marker moves.
|