Add the field value store and page descriptor model (#58) #81

Merged
robert merged 1 commit from area/field-store-page-descriptors into main 2026-09-04 00:49:39 +02:00
Owner

Implements #58 (Field value store and page descriptor model).

What this adds

  • watchapp/src/c/fields.h / fields.c — the FieldId enum (every metric in the issue, one
    entry per DESIGN.md section 2 row), a value store keyed by it, typed setters
    (field_store_set_u8/u16/u32) that recognise each wire width's PROTOCOL.md rule-3 sentinel and mark
    the field unavailable automatically — a call site can't forget to check for it — group-heartbeat
    staleness aging (D44), and one formatter table (decimal places, unit string, unavailable text).
    Zero Pebble SDK calls anywhere in this file; verified by compiling it standalone with plain host
    gcc against a small smoke-test harness (sentinel handling, zero-is-not-absent, the D44
    constant-value-stays-fresh scenario, staleness-vs-unavailable). That test isn't checked in — #68 owns
    the actual harness — but the file is proven host-buildable as written.
  • watchapp/src/c/page.h / page.c — PageTemplate, PageDescriptor{template, fields[6]},
    page_has_live_field() (the "does this descriptor have at least one live field" primitive #60's
    carousel needs), wire/persisted packing that matches PROTOCOL.md 2.6's CONFIG_PAGES byte layout
    exactly (1 byte template + 6 bytes fields), and the three default pages (Ride/Effort/Progress) from
    DESIGN.md section 4. Persistence uses persist_read_data/persist_write_data, confirmed against the
    installed SDK 4.33.1 headers (256-byte-per-key limit, uint32_t keys). Only page_store_load/save
    touch the SDK — the rest of the file is pure.

pebble build is clean for emery, gabbro and basalt.

Design calls made without an explicit spec (flagging for review)

  • Staleness thresholds (D44 asks for "reasonable intervals per field type", not exact numbers):
    ride and nav groups grey at 6 s (2x PROTOCOL's stated 3 s heartbeat — the same "two missed
    heartbeats" point PROTOCOL.md §4 itself uses to call the whole link down). Laps never age by time —
    PROTOCOL.md §4 gives them no heartbeat at all ("on lap events only"), so there's nothing periodic to
    compare against; only availability matters. The clock never ages (the app is its own source). HR is
    watch-local with no PROTOCOL heartbeat to anchor to, so it provisionally reuses the ride threshold
    pending whatever sample period a future HR-producer issue actually requests.
  • ETA formats as a duration (M:SS/H:MM:SS, matching ELAPSED/LAP_TIME) rather than a
    clock-of-day. NAV_ETA_S is "seconds at rolling average speed", which reads as a duration, and a
    clock-of-day render would need timezone/localtime handling that would compromise this file's
    host-testability.
  • Persistent storage packs all three default descriptors as one 21-byte blob under a single
    persist_* key (PERSIST_KEY_PAGE_DESCRIPTORS = 1), reusing PROTOCOL.md's CONFIG_PAGES wire
    format byte-for-byte rather than inventing a separate on-flash shape. This is sized for exactly the
    three shipped pages; #61 (phone-driven CONFIG_PAGES) will need to generalise it to a phone-supplied
    count.

Deliberately not in scope here

  • The AppMessage receive handler that will call field_store_set_* — it needs proto.h's generated
    keys, which is #7 and hasn't landed yet. This PR only builds the setters it will call.
  • #68's host-test CMake harness. fields.c/fields.h are proven host-buildable (see above) but
    page.c mixes pure logic with the two persist_* calls, so a clean host build of it needs either a
    stub or a file split — that's a harness-design decision I left for #68 rather than pre-empting it
    here, per the issue's own guidance not to force it if it doesn't fit naturally.

Claude-Session: https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt

Implements #58 (Field value store and page descriptor model). ## What this adds - **`watchapp/src/c/fields.h` / `fields.c`** — the `FieldId` enum (every metric in the issue, one entry per DESIGN.md section 2 row), a value store keyed by it, typed setters (`field_store_set_u8/u16/u32`) that recognise each wire width's PROTOCOL.md rule-3 sentinel and mark the field unavailable automatically — a call site can't forget to check for it — group-heartbeat staleness aging (D44), and one formatter table (decimal places, unit string, unavailable text). **Zero Pebble SDK calls anywhere in this file**; verified by compiling it standalone with plain host `gcc` against a small smoke-test harness (sentinel handling, zero-is-not-absent, the D44 constant-value-stays-fresh scenario, staleness-vs-unavailable). That test isn't checked in — #68 owns the actual harness — but the file is proven host-buildable as written. - **`watchapp/src/c/page.h` / `page.c`** — `PageTemplate`, `PageDescriptor{template, fields[6]}`, `page_has_live_field()` (the "does this descriptor have at least one live field" primitive #60's carousel needs), wire/persisted packing that matches PROTOCOL.md 2.6's `CONFIG_PAGES` byte layout exactly (1 byte template + 6 bytes fields), and the three default pages (Ride/Effort/Progress) from DESIGN.md section 4. Persistence uses `persist_read_data`/`persist_write_data`, confirmed against the installed SDK 4.33.1 headers (256-byte-per-key limit, `uint32_t` keys). Only `page_store_load`/`save` touch the SDK — the rest of the file is pure. `pebble build` is clean for `emery`, `gabbro` and `basalt`. ## Design calls made without an explicit spec (flagging for review) - **Staleness thresholds** (D44 asks for "reasonable intervals per field type", not exact numbers): ride and nav groups grey at 6 s (2x PROTOCOL's stated 3 s heartbeat — the same "two missed heartbeats" point PROTOCOL.md §4 itself uses to call the whole link down). Laps never age by time — PROTOCOL.md §4 gives them no heartbeat at all ("on lap events only"), so there's nothing periodic to compare against; only availability matters. The clock never ages (the app is its own source). HR is watch-local with no PROTOCOL heartbeat to anchor to, so it provisionally reuses the ride threshold pending whatever sample period a future HR-producer issue actually requests. - **ETA formats as a duration** (`M:SS`/`H:MM:SS`, matching `ELAPSED`/`LAP_TIME`) rather than a clock-of-day. `NAV_ETA_S` is "seconds at rolling average speed", which reads as a duration, and a clock-of-day render would need timezone/localtime handling that would compromise this file's host-testability. - **Persistent storage** packs all three default descriptors as one 21-byte blob under a single `persist_*` key (`PERSIST_KEY_PAGE_DESCRIPTORS = 1`), reusing PROTOCOL.md's `CONFIG_PAGES` wire format byte-for-byte rather than inventing a separate on-flash shape. This is sized for exactly the three shipped pages; #61 (phone-driven `CONFIG_PAGES`) will need to generalise it to a phone-supplied count. ## Deliberately not in scope here - The AppMessage receive handler that will call `field_store_set_*` — it needs `proto.h`'s generated keys, which is #7 and hasn't landed yet. This PR only builds the setters it will call. - #68's host-test CMake harness. `fields.c`/`fields.h` are proven host-buildable (see above) but `page.c` mixes pure logic with the two `persist_*` calls, so a clean host build of it needs either a stub or a file split — that's a harness-design decision I left for #68 rather than pre-empting it here, per the issue's own guidance not to force it if it doesn't fit naturally. Claude-Session: https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt
Add the field value store and page descriptor model (#58)
Some checks failed
dev-artifact / build-pbw (push) Has been cancelled
dev-artifact / build-apk (push) Has been cancelled
dev-artifact / publish (push) Has been cancelled
fast-lane / host-c-tests (push) Has been cancelled
fast-lane / jvm-tests (push) Has been cancelled
fast-lane / pebble-build (push) Has been cancelled
fast-lane / lint-and-secrets (push) Has been cancelled
fast-lane / meta-declares-required-jobs (push) Has been cancelled
fast-lane / host-c-tests (pull_request) Has been cancelled
fast-lane / jvm-tests (pull_request) Has been cancelled
fast-lane / pebble-build (pull_request) Has been cancelled
fast-lane / lint-and-secrets (pull_request) Has been cancelled
fast-lane / meta-declares-required-jobs (pull_request) Has been cancelled
6ad781f10c
FieldId enum, a value store keyed by it, one formatter table per field,
and PageDescriptor{template, fields[6]} persisted on the watch —
docs/DESIGN.md sections 1-2, docs/PROTOCOL.md section 2.

- fields.h/fields.c: FieldId, the value store (available flag distinct
  from a real zero, per-entry last-update timestamp), typed setters that
  recognise each wire width's sentinel automatically, group-heartbeat
  staleness (D44: aged against the group, never a key's own arrival),
  and one formatter table (decimal places, unit, unavailable text).
  Free of Pebble SDK calls throughout, verified with a standalone host
  gcc build against a small smoke-test harness covering the sentinel,
  zero-is-not-absent, staleness-vs-unavailable and D44's
  constant-value-stays-fresh scenarios.
- page.h/page.c: PageTemplate, PageDescriptor, page_has_live_field() (the
  carousel primitive #60 needs), wire/persisted packing matching
  PROTOCOL.md 2.6's byte layout exactly, and the three default pages
  (Ride/Effort/Progress) from DESIGN.md section 4. Persistence uses
  persist_read_data/persist_write_data (confirmed against the installed
  4.33.1 SDK headers); only page_store_load/save touch the SDK, so the
  pure logic in this file stays separable for #68.

`pebble build` verified clean for emery, gabbro and basalt.

Design calls made without an explicit spec (flagged for review):
- Staleness thresholds: 6s (2x the 3s PROTOCOL heartbeat) for the ride
  and nav groups; laps never age by time (event-only, no heartbeat
  exists to compare against); the clock never ages (it's the app's own
  RTC); HR provisionally reuses the ride threshold pending a real
  sample-period decision elsewhere.
- ETA formats as a duration (matching ELAPSED/LAP_TIME) rather than a
  clock-of-day, to avoid pulling timezone/localtime handling into a file
  that must stay host-testable.
- Persistent storage packs all three default descriptors as one 21-byte
  blob under a single persist key, reusing PROTOCOL.md's CONFIG_PAGES
  wire format byte-for-byte; #61 will need to generalise this when the
  phone can configure more than three pages.

Not built here: the AppMessage receive handler that will call these
setters (blocked on #7's key generation) and #68's host-test harness
(left clean for that issue — page.c's SDK-touching functions are
isolated but not split out).

Claude-Session: https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt
robert merged commit b5ebddc300 into main 2026-09-04 00:49:39 +02:00
Sign in to join this conversation.
No description provided.