Skip to content

Low battery: blind-floor shutdown, 10%/5% warnings, re-anchored SOC_LUT (ADR 0021)#541

Open
brickbots wants to merge 1 commit into
mainfrom
low-battery-shutdown
Open

Low battery: blind-floor shutdown, 10%/5% warnings, re-anchored SOC_LUT (ADR 0021)#541
brickbots wants to merge 1 commit into
mainfrom
low-battery-shutdown

Conversation

@brickbots

Copy link
Copy Markdown
Owner

Implements ADR 0021: when the BQ25895's ADC goes blind (~3.5 V) while on battery, PiFinder performs a clean, warned software shutdown instead of running blind into an SD-corrupting hard power cut. Advisory warnings at 10% and 5% state of charge precede it.

What's in here

Honest blindness. A raw-0 BATV read no longer decodes to a fake 2.304 V measurement: BatteryState publishes None for every ADC-derived field (with an adc_blind property), so nothing can feed the artifact into the discharge curve. Status bits (charge phase, power source) are plain register reads and stay valid below the floor — they gate the trigger. The title-bar icon shows empty while blind.

Debounced shutdown trigger. LowBatteryShutdownTrigger (pure, unit-tested like plan_charging_writes): 4 consecutive ADC-blind polls (~20 s) while on_external_power is false → the battery monitor puts low_battery_shutdown on ui_queue. Single blind reads never trigger (conversions fail intermittently in the 3.50–3.55 V twilight); external power always inhibits and resets. The main loop shows the final warning and routes through the existing user-shutdown chokepoint (SHUTDOWN earcon delivery wait → GPIO14 latch, per ADR 0007/0008). Never SoC-triggered.

Warnings at 10% / 5%. LowBatteryWarner (pure): once per crossing, on battery only, most-severe-only on a fast drop through both, ±2-point hysteresis so 20 mV ADC quantisation can't re-fire it, and a None estimate (blind twilight) suppresses without re-arming. Warning = popup + console line + the LOW_BATTERY earcon (previously defined-but-unwired). The watcher runs on a monotonic clock so a backwards GPS time step can't silence it.

Re-anchored SOC_LUT (hard dependency). The shipped curve read ~21% at the blind floor — 10%/5% could never fire on it. Replaced with the provisional fit from the 2026-07-17 bench campaign, 0% re-anchored to the blind-floor shutdown: all knots measured, none extrapolated. Marked provisional pending pinned-load confirmation runs.

Analysis tool promoted (python/tools/battery_runtime_analysis.py, per ADR 0020 tool-and-knots-merge-together): now with a blind-floor anchor mode as default — T_cutoff = the row where the shutdown debounce would fire (same 4-poll count), sane rows only for voltage pairing; --anchor power-death reproduces the old fit. The knot-refresh follow-up PR after the confirmation runs is a re-run of this tool plus a paste.

Dev/test. battery_fake now ends each sweep lap in an ADC-blind tail that trips the debounce, so -fh -fb exercises warnings → blind icon → shutdown warning end to end. On the Fake hardware platform the main loop skips the actual OS power-off (the request can only come from the scripted fake there), so a lap can never power off a real Pi running -fh for docs screenshots, or a dev box.

Verification

  • 866 unit+smoke tests green; ruff + mypy clean. New pure-function tests for trigger, warner, blind decode, blind icon, and the new LUT knots.
  • Headless run (-fh -fb, pifinder-remote skill): observed the 10% popup, the 5% popup, the blind-tail empty icon, and the "Low battery / Shutting down" final warning fire at the expected times, once per discharge lap.
  • Analysis tool smoke-tested on synthetic runs: blind-floor anchor finds T_cutoff past an intermittent twilight, discards the blind tail, emits a fully measured LUT; all-blind runs are excluded gracefully.
  • Multi-agent code review run on the diff; surviving findings (twilight warning re-fire, fake-battery real-shutdown hazard, wall-clock watcher, tool crash on all-blind runs) all fixed in this commit.

Notes for review

  • The four .po catalogs gained the two new msgids with AI translations flagged needs human review (hand-added; no babel extract run per the repo's i18n practice). The English warning string is worded so % sits at end-of-string — babel 2.16 parses "% le"-style sequences as python-format placeholders and rejects the catalog otherwise.
  • The user-facing docs (Power & Charging page) don't yet mention the auto-shutdown — happy to add that here or in a follow-up, your call.
  • Follow-ups per the campaign plan: merge main into battery-runtime-test, run the pinned-load confirmation runs (they double as live acceptance tests and now end cleanly at the shutdown), then the knot-refresh PR.

🤖 Generated with Claude Code

…e-anchored SOC_LUT

Implements docs/adr/0021-blind-floor-shutdown.md end to end:

- BatteryState represents ADC blindness honestly: a raw-0 BATV read
  publishes None for every ADC-derived field (never the 2.304 V decode
  artifact), with an adc_blind property; status-derived fields stay
  valid below the floor. Title-bar icon shows empty when blind.
- Debounced low-battery shutdown: LowBatteryShutdownTrigger (pure, next
  to plan_charging_writes) fires after 4 consecutive ADC-blind polls
  (~20 s) on battery only — never on external power, never from SoC.
  battery_monitor puts "low_battery_shutdown" on ui_queue; the main
  loop shows the final warning and routes through the existing
  user-shutdown chokepoint (SHUTDOWN earcon wait, GPIO14 latch, ADR
  0007/0008). On the Fake hardware platform the OS power-off is
  skipped, so battery_fake's scripted blind tail can never power off a
  host machine.
- Advisory warnings at 10% and 5% SoC: LowBatteryWarner (pure) fires
  once per crossing, on battery only, with quantisation-jitter
  hysteresis; a None estimate (blind twilight) suppresses without
  re-arming so intermittent blind/sane alternation cannot re-fire the
  warning. Popup + console + LOW_BATTERY earcon (now wired); watcher
  uses a monotonic clock so GPS time steps can't silence it.
- SOC_LUT replaced with the re-anchored provisional fit from the
  2026-07-17 bench campaign (0% = blind-floor shutdown; all knots
  measured, none extrapolated; pending pinned-load confirmation runs).
  The old curve read ~21% at the blind floor, so the warnings could
  never have fired on it.
- tools/battery_runtime_analysis.py promoted from battery-runtime-test
  (tool and derived knots merge together, ADR 0020), with a blind-floor
  anchor mode as default: T_cutoff = the row where the shutdown
  debounce would fire; --anchor power-death reproduces the old fit.
- battery_fake: each sweep lap ends in an ADC-blind tail that trips the
  debounce, so -fh -fb exercises warnings and the shutdown warning end
  to end with no hardware.
- i18n: new UI strings wrapped and hand-added to the four tracked .po
  catalogs (AI-translated, flagged for review), .mo recompiled.
- Pure-function tests for the trigger, warner, blind decode, blind
  icon, and the new LUT; ADR 0021 status flipped to implemented;
  Battery CONTEXT.md glossary updated.

Verified headless (-fh -fb, pifinder-remote): 10% and 5% popups fire
once each per discharge, blind tail shows the empty icon, and the
"Low battery / Shutting down" warning appears at the debounce point.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@brickbots
brickbots marked this pull request as ready for review July 19, 2026 19:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant