Backlight policy for night riding (#45) #106
No reviewers
Labels
No labels
area:companion
area:docs
area:shared
area:tooling
area:watchapp
blocker
kind:chore
kind:feature
kind:spike
kind:test
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
robert/PedalPebble!106
Loading…
Reference in a new issue
No description provided.
Delete branch "area/backlight-policy"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements #45 (Backlight policy for night riding).
What this adds
watchapp/src/c/backlight.{c,h}— a new module, not main.c per the issue'sliteral file list, because a persisted setting plus mode logic is exactly
the shape fields.c/page.c/state.c/carousel.c already have, and cramming it
into main.c would be inconsistent with the rest of this codebase's
one-concern-per-file pattern. main.c only gains two calls
(
backlight_init()/backlight_deinit()), ride_link.c gains one(
backlight_tick()in its existing ~1 Hz timer).Three modes, persisted as one packed byte under a new
PERSIST_KEY_BACKLIGHT_MODE(page.h's registry, keys 1-4 now):BACKLIGHT_MODE_OFF(default) — no override, system default behaviourBACKLIGHT_MODE_ALWAYS_ON— held on for the whole ride (the issue's"explicit opt-in mode")
BACKLIGHT_MODE_SCHEDULE— held on while riding, only during a configureddark-hours window (default 19:00-06:00)
Persistence uses page_store_load()'s pack-and-validate shape, not
PERSIST_KEY_ACTIVE_PAGE_INDEX's plainpersist_read_int()— a corruptbyte needs to fall back to a known-safe mode rather than be trusted as
whatever int happens to be stored. See backlight.h's own comment for the
full reasoning.
SDK facts checked, not assumed
Checked against the installed SDK
(
~/.local/share/pebble-sdk/SDKs/4.33.1, bothemery/include/pebble.hand
gabbro/include/pebble.hspecifically, 2026-09-05):light_enable(bool enable)andlight_enable_interaction(void)areexactly the signatures expected — verified in the header, not recalled.
AmbientLightLevelappears exactly once, as a 3-bit field insideHealthMinuteData(returned byhealth_service_get_minute_history()) —a historical, minute-granularity health record, not something an app can
poll in real time. So "ambient-aware" activation is not honestly
buildable here;
BACKLIGHT_MODE_SCHEDULEis the schedule-based fallbackthe issue itself names for exactly this case.
docs/DESIGN.mdsection 7.4 (D48:reopened with a checked fact, not an argument) — it previously said
emery/gabbrohave "no backlight to wash out", which contradicts thisissue's own premise and both platforms' installed headers exposing a real
LightAPI with no platform restriction.What's real vs. what's mechanism-only
backlight_tick()runs off ride_link.c's existing ~1 Hz timer and calls
light_enable()only when the desired state actually changes. Driving the emulator
through a full ride lifecycle (with the shipped default temporarily
forced to ALWAYS_ON for observation, then reverted before this commit —
see the commit message) logged exactly two
light_enable()calls acrossthe whole run:
truethe moment the ride was RUNNING,falsethe momentit reached STOPPED, and nothing in between across a pause/resume —
confirming the "only on change" property on real firmware, not just the
host stub. A clean install with the shipped default (
BACKLIGHT_MODE_OFF)logs zero
light_enable()calls at all.backlight_flash_for_turn_prompt()(the brief flash for a turn prompt,
light_enable_interaction()) isbuilt and host-tested, but nothing calls it — there is no turn-cue
rendering anywhere in this watchapp yet (#33-#37 all still open). Matches
the "build the mechanism, note the missing caller" pattern from tonight's
#65/#19.
backlight_set_mode()to move a rider off the OFF default. There is no on-watch settings screen
(carousel.c's button model is fully specified by DESIGN.md section 5, and
D48 closes replanning it without a checked fact — inventing a new button
gesture here would be exactly that) and no phone-side CONFIG_* wire
message for it yet.
backlight_set_mode()/backlight_get_mode()arefully built and tested for whichever future settings issue adds a real
trigger.
Battery cost
Measured here:
heap_bytes_free()before/after (RAM footprint moved from13020 to 13252 bytes on emery/gabbro — two static bools and an enum, per
the memory_usage_report), and the emulator's logged
light_enable()callsequence (above). Not measured here, same as #12/#18/#69: real
multi-hour battery drain against NFR-B1's 25% budget — that needs Robert's
physical watch running BACKLIGHT_MODE_ALWAYS_ON or SCHEDULE for real hours,
which nothing in this codebase can trigger yet (see above).
Testing
watchapp/tests/test_backlight.c: 27 cases — the pure decision logic(schedule window matching including the overnight wraparound, the
riding/mode/dark decision table, turn-prompt gating), pack/unpack
round-trip and out-of-range rejection, and (via two new stub additions to
tests/stubs/pebble.h/pebble_stub.c— a fakelight_enable()/light_enable_interaction()with call tracking, plus a no-opAPP_LOGmacro) the persistence fallback behaviour and backlight_tick()/
backlight_deinit()'s actual call count and call value.
No
cmakebinary in this sandbox — hand-compiled with the exact flags intests/CMakeLists.txt. All five host suites pass clean (fields/page/state/page_render_geometry/backlight, 119 assertions total, no regressions from
the page.h/stub changes).
pebble buildclean on emery, gabbro and basalt.Claude-Session: https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt
New module watchapp/src/c/backlight.{c,h}, following the codebase's one-concern-per-file pattern (fields.c/page.c/state.c/carousel.c) rather than cramming this into main.c as the issue text suggested — a persisted setting plus mode logic is exactly the shape those files already have. Three modes (BACKLIGHT_MODE_OFF / ALWAYS_ON / SCHEDULE), persisted as one packed byte under a new PERSIST_KEY_BACKLIGHT_MODE (page.h's registry, now at keys 1-4), using page_store_load()'s pack-and-validate shape rather than PERSIST_KEY_ACTIVE_PAGE_INDEX's plain persist_read_int() -- see backlight.h's own comment for why a corrupt byte needs to fall back to a known-safe mode rather than be trusted as whatever int is stored. Checked against the installed SDK (~/.local/share/pebble-sdk/SDKs/4.33.1, emery and gabbro headers): light_enable(bool)/light_enable_interaction() are exactly as expected. AmbientLightLevel exists only inside HealthMinuteData (health_service_get_minute_history(), a historical per-minute record) -- there is no live ambient-light-sensor API on this SDK, so BACKLIGHT_MODE_SCHEDULE is schedule-based (a configurable dark- hours window, default 19:00-06:00) rather than ambient-aware, honestly, per the issue's own fallback wording. backlight_ride_state_counts_as_riding() (RUNNING or PAUSED) is wired into ride_link.c's existing ~1 Hz tick for real -- backlight_tick() calls light_enable() only on an actual change of desired state, verified both on the host test suite (via a stub Light surface with call tracking, mirroring the persist_* stub page.c/state.c already use) and against the real SDK in the emery emulator: exactly two light_enable() calls logged across a full ride lifecycle (true on RUNNING, false on STOPPED), none in between across a pause/resume, and zero calls at all with the shipped default mode (OFF). backlight_deinit() (main.c's prv_deinit()) returns the backlight to automatic control on exit if this file had turned it on -- the same NFR-B3 "must return to 0 on exit" reasoning (REQUIREMENTS.md) already applied to the HRM sample period. backlight_flash_for_turn_prompt() (light_enable_interaction(), the brief flash for a turn prompt) is built and host-tested but has no real caller yet -- there is no turn-cue rendering built in the watchapp at all (issues #33-#37 still open), so there is no real trigger point. Matches the "build the mechanism, note the missing caller" pattern. No settings surface (on-watch menu or phone CONFIG push) exists yet to move BACKLIGHT_MODE off its OFF default -- adding a new carousel button gesture for this would reopen DESIGN.md section 5's button model, which D48 closes without a checked fact. backlight_set_mode() is fully built and tested; a future settings issue is expected to call it. Also corrects a factual error in DESIGN.md 7.4 (D48: reopened only with a checked fact) -- it previously claimed emery/gabbro have "no backlight to wash out", contradicted by this issue's own premise and by both platforms' installed SDK headers declaring a real Light API with no platform restriction. tests/test_backlight.c: 27 cases covering the pure decision logic (schedule window matching including overnight wraparound, the riding/mode/dark decision table, turn-prompt gating), pack/unpack, and (via the stub) persistence fallback and the tick/deinit call-count and call-value behaviour. Hand-compiled with the exact flags in tests/CMakeLists.txt (no cmake binary in this sandbox) -- 119 assertions clean across all five suites (fields/page/state/page_render_geometry/ backlight). pebble build clean on emery/gabbro/basalt, RAM footprint unchanged from before this change on emery/gabbro (13020 -> 13252 bytes, the two static bools/enum this file adds). Battery cost: heap_bytes_free() delta and the emulator's light_enable() call sequence are measured here; real multi-hour battery drain against NFR-B1's 25% budget needs Robert's physical watch, same as #12/#18/#69. Claude-Session: https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt