Watchface now syncs with the Nine Hazes idle task system via its own
pkjs client — the three-artifact architecture is operational.
Wire side:
- pkjs fetches GET /v1/queue on launch, applies clock offset, converts
epoch-ms to seconds for AppMessage, resolves task name from the
cached dictionary (refetch on version drift)
- GameSnapshot persisted at key 0x30 (protocol-specified); task name
at 0x32 (face-local nicety)
- AppMessage inbox widened to 256 bytes to carry the full snapshot
dictionary in a single logical message
Display side:
- Task strip renders two lines: countdown-to-completion + used/36 +
task count, then task name with "(+)" pending-rewards cue
- Status states render distinctly: FROZEN / ABORTED(code) / LOCKED /
No hero yet (EMPTY)
- serverDown sentinel key: oxblood "!" icon on failed check; absence
on successful sync clears it; volatile — never persisted
- Countdown math is server-clock corrected (clock_off), computed
locally from bound_sec — zero radio to stay fresh
Also:
- ES5-normalized pkjs for SDK webpack 1.15/acorn parser
- Replaced face_packet.{h,c} with snapshot.{h,c} (PACKET_KEY 101
retired; old blob orphaned harmlessly)
- Mock Task API server seeded with 3-task queue (in-memory)
Verified: seeded queue renders "0:xx 16/36 seg 3 tasks / Gather (+)";
server kill → cached strip + offline icon, no error surface.
5.2 KiB
Nine Hazes — Watch Client for OSRIC MUD Idle System
Consolidated record v3.0 (post-pivot, 2026-09-12)
1. Concept
Pebble Time 2 (emery) watchface + companion watchapp acting as remote monitor and manager for the Nine Hazes OSRIC MUD idle task system. NOT a standalone game — all game logic lives on the MUD server. The watch displays server snapshots and submits intents (add/cancel/claim); the server validates and executes. Watch behaves like a normal watchface except during active sync.
2. Critical architecture decision — "face gets its own sync"
Pebble persist storage is per-app sandboxed with NO app↔app communication (verified against platform docs). Therefore the watchface and watchapp each own an independent pkjs sync to the same Task API. No watch-side data sharing exists or is attempted. Shared truth = server.
3. Protocol
Wire contract: "Nine Hazes — Idle Task Protocol v1.0" (separate document, authored server-side; authoritative). Summary:
- HTTPS + Bearer token (scoped queue:read queue:write claim; token on phone localStorage only, NEVER on watch)
- GET /v1/dictionary (versioned, 304-style caching via ?v=)
- GET /v1/queue — full sync in one call (tasks[], used/budget, pend, bound, status, tok.exp)
- POST /v1/queue/tasks (reqId idempotency), DELETE /v1/queue/tasks/:tid, POST /v1/claim, POST /v1/token/refresh
- Device protocol: GameSnapshot packed struct (~28 bytes) at persist key 0x30; dictionary blob at 0x31 (watchapp use)
- Clock correction: all countdowns from bound + clock_off, never raw watch clock
- Failure ladder: unreachable/malformed → silent, render cached snapshot
4. Artifacts & ownership
| Artifact | Owner | Status |
|---|---|---|
| Watchface (C, canvas renderer, widget table) | watch team | RENDERING BASELINE COMPLETE: custom-canvas data-driven renderer, 10 widget types + 2 spare slots, per-widget layout persist (key 100), parchment/ink palette, task strip rendering, server-down icon, dev.sh tooling. Committed to Gitea. |
| Watchapp (queue manager UI) | watch team | deferred (build order item) |
| pkjs ×2 (face sync, watchapp sync) | watch team | face pkjs fetches /v1/queue → AppMessage snapshot |
| Bridge/state server | OBSOLETE — deleted. Superseded by MUD gateway + placeholder mock (below) | |
| Placeholder mock server (idle protocol §10) | watch team | zero-dependency node:http implementation (NOT Express — locked project law: TypeScript, node stdlib only) |
5. Widget set (unchanged)
Time, date, watch battery (+days estimator, key 102), phone battery, steps, distance, weather composite (NWS default / Open-Meteo fallback — geographic auto-switch), advisory omens, sleep, Current Task Strip (2-line: line 1 = countdown to task completion + used/36 + task count, line 2 = current task name (+n) pending indicator; offline "!" icon on failed check). All toggleable/movable/font+color stylable. Spare slots reserved (incl. game preview / companion preview, deferred).
6. Design laws (north stars)
- Casual glance — no game-triggered interrupts, ever.
- Game logic dormant until invited; watch computes only countdown strings.
- Fully free face — no protected widgets; "restore default" escape hatch.
- Face shows minimal: countdown + queue line only.
- Battery-first: zero network in ambient use; one sync per interaction.
- Fail-dead-silent: server down → cached data or hidden strip; no errors.
- Zero-dependency TypeScript server code; no workers/background radio ever; wakeups = semantic events only, rolling single wakeup (Muninn pattern).
7. Platform/environment
emery only ("targetPlatforms": ["emery"]) · Core Devices PebbleOS SDK · system fonts first (carved from Apache release), OFL TTFs bundled later · Apache-2.0 · Gitea master (auth-gated), GitHub public mirror at v0.1.0 · Liberapay/Ko-fi FUNDING.yml.
8. Reference projects (ideas only, clean-room; unlicensed repos = read-for-protocol
never copy code; Remix-on-CloudPebble ≠ redistribution grant) IdleFantasy (queue model), Doors of Doom (AppMessage), Pixel8 (chunked transfer), Muninn (wakeup/battery sampling patterns), Tamagotchi-API/TamaTime (companion-preview idea, DEFERRED — feature not focus).
9. Build order (current)
- ✔ Watchface rendering baseline (committed)
- ✔ pkjs fetch + AppMessage + cached-packet strip + offline icon (v1 protocol)
- → NOW: idle-protocol adaptation — snapshot struct, mock server, clock-offset countdown (this revision)
- Widget wiring: health (steps/distance/sleep), weather (NWS/Open-Meteo)
- Watchapp: queue UI (add via presets/segment picker, cancel, claim), wakeup at segment boundary, launch_args deep links, App Glance/timeline pins
- Settings phase 2: hosted config page (drag-drop layout, presets, server URL/ token entry)
- Polish: PDC sprites/icons (pickaxe, fist, coin, moon), theme, vibration
10. Open items to relay to server team
- bound/t/sync timestamps in epoch-ms exceed AppMessage uint32 → watchface uses epoch-SECONDS for wire transfer (recommend protocol clarify units)
- Face receives resolved task NAME from pkjs (v1); dictionary blob 0x31 is watchapp-scoped, chunked transfer deferred
- Player identity: X-Pebble-Id informational; real auth = MUD token