Skip to content

ADR 0020: battery profiling complete — refit derived, deliberately not adopted - #573

Merged
brickbots merged 3 commits into
mainfrom
battery-soc-measured-knots
Aug 6, 2026
Merged

ADR 0020: battery profiling complete — refit derived, deliberately not adopted#573
brickbots merged 3 commits into
mainfrom
battery-soc-measured-knots

Conversation

@brickbots

@brickbots brickbots commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Closes out the battery runtime campaign ADR 0020 has been waiting on since July.

Net effect: documentation only. No SOC_LUT knot changes — the middle commit shipped the refit and the third takes it back out, with the reasoning preserved in the history because the reasoning is the point.

The stale bit

ADR 0020's status said the measured knots were still to come and the shipped LUT was "generic Li-ion folklore, annotated as such in the code." Neither is true: #541 landed a measured curve in July, and the pinned-load confirmation runs it was waiting on have since happened.

What the campaign produced

Six discharges on two rev4 units, identical pinned profile (400 ms / gain 20, brightness 255, sleep off):

Sitting Unit Runtime Load
2026-07-17 e63d1a2e 9h18m degraded — 0% solving
2026-07-17 777f86f4 8h57m degraded — 0% solving
2026-07-24 e63d1a2e 9h45m degraded — 0% solving
2026-07-24 777f86f4 9h10m degraded — 0% solving
2026-07-25 e63d1a2e 9h55m pinned — 100%
2026-07-26 777f86f4 10h03m pinned — 100%

Four runs were spoiled by faults that left the workload looking healthy from outside — IMU pseudo-motion blanking the substituted frame, and logind RemoveIPC=yes deleting the cedar shmem segment at SSH logout (#548). Both left solve attempts churning at full rate with zero matches, which is why the tool's load verdict now requires matches > 0.

Headline: about 10 hours on a full charge under a continuously solving load at full brightness with sleep disabled.

The interesting part: the refit was derived and rejected

Fitting the two pinned runs alone gives knots ~20 mV higher through the 5–25% band. Better fit to the scatter — worse setting for the only thing these knots drive. The warner fires on the first noise dip past a threshold, and near the flat knee one ADC LSB (20 mV) spans about three SoC points, so lifting the low knots moves the crossing a long way in wall-clock time. Replaying both pinned runs through LowBatteryWarner:

Curve 5% fires before shutdown 10%
shipped (kept) 31 min / 38 min 79 / 105 min
pinned-only refit 50 min / 69 min 86 / 105 min

Intent is ~30 min at 5%. The shipped curve's lower knee compensates for BATV quantisation noise and hits it; the refit overshoots ~2×. The percentage is advisory UI output, not a measurement — a curve is only better if the warning lands better.

The ADR now records the refit values, the replay table, the decision, and the rule that follows: judge a candidate curve by replaying telemetry through the warner, never by knot distance alone. The code comment says the same thing at the point of temptation.

Verification

  • Re-derived with tools/battery_runtime_analysis.py over all six runs and over the two pinned runs alone.
  • Replayed both pinned runs through the real LowBatteryWarner under both curves — that table is measured, not estimated.
  • Confirmed git diff origin/main...HEAD changes no knot values.
  • 1159 unit + smoke tests pass; ruff check + format clean; mypy clean under the project config.

Note

user_guide.rst still quotes a v3/PiSugar figure ("four to five hours") for battery life. There is now a real measured rev4 number to replace it with — tracked in the rev4 docs plan (#572), not fixed here.

🤖 Generated with Claude Code

brickbots and others added 3 commits August 6, 2026 16:36
The status line was stale in a way that mattered: it said the measured
knots were still to come and that the shipped LUT was "generic Li-ion
folklore". PR #541 replaced that folklore with a measured curve back in
July, and the pinned-load confirmation runs the ADR was waiting on have
since happened.

Records what the campaign actually produced: six discharge runs on two
rev4 units over 2026-07-17..07-26, of which only the last two carried
the pinned load this ADR defines. The other four were spoiled by IMU
pseudo-motion blanking the substituted frame and by logind RemoveIPC
deleting the cedar shmem segment at SSH logout -- both of which left
solve *attempts* churning at full rate, so the load looked healthy from
the outside. That is why the load verdict now requires matches > 0.

Headline: about 10 hours on a full charge under a continuously solving
load at full brightness with sleep disabled (9h55m and 10h03m).

Also fixes the Decision section's 0% anchor, which still described the
hardware cutoff voltage while the amendment note above it said the
opposite, and records where the raw telemetry lives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The curve shipped in #541 was explicitly marked PROVISIONAL: it was
fitted from two runs that discharged under a degraded load, because
solving had silently stopped in both. The confirmation runs have since
been done -- 2026-07-25 and 2026-07-26, one per rev4 unit, camera
solving 100% of the discharge, 9h55m and 10h03m -- so the provisional
marker can come off.

Re-fitted from those two runs only. Pooling the four degraded runs in
would contradict ADR 0020's own premise (a curve measured at a
different load mis-maps voltage to runtime), and the analysis tool
warns when asked to do it.

The new curve sits up to 21 mV higher through the 5-25% band, so at a
given voltage it reads a few points lower near empty and the 10%/5%
warnings fire slightly earlier -- the intended direction for a warning.

Test expectations that were pinned to the old knots are recomputed, not
loosened. One interpolation case moved from 3.70 V to 3.72 V because
3.70 is now exactly the 15% knot and would no longer test interpolation.

1159 unit+smoke tests pass; ruff and mypy clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reverts the knot change from the previous commit and records why, in
both the code and ADR 0020.

Deriving the refit was right; adopting it was not. It fits the
voltage/runtime scatter better and sets the low-battery advisories
worse, which is the only thing these knots actually drive. The warner
fires on the first noise dip past a threshold, and near the flat knee
one ADC LSB (20 mV) spans about three SoC points -- so lifting the low
knots ~20 mV moves the crossing a long way in wall-clock time.

Replaying both pinned runs through LowBatteryWarner:

  shipped      5% fires 31 / 38 min before shutdown
  refit        5% fires 50 / 69 min before shutdown

Design intent is ~30 min. The shipped curve's lower knee compensates
for BATV quantisation noise and hits it; the refit overshoots roughly
2x. The percentage is advisory UI output, not a measurement, so a curve
is only better if the warning lands better.

ADR 0020 now records the refit, the replay table and the decision, plus
the rule that follows: judge a candidate curve by replaying telemetry
through the warner, never by knot distance alone.

1159 unit+smoke tests pass; ruff and mypy clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@brickbots brickbots changed the title Battery profiling complete: measured SOC_LUT + ADR 0020 status ADR 0020: battery profiling complete — refit derived, deliberately not adopted Aug 6, 2026
brickbots added a commit that referenced this pull request Aug 6, 2026
The bench campaign closed 2026-07-26 (ADR 0020 updated in PR #573), so
WP1 no longer has to leave a TODO where the battery-life number goes:
about 10 hours, measured on two units under the pinned worst-case load.

Framed as a floor rather than a typical figure, since it was measured
with the camera solving continuously, the screen at full brightness and
sleep disabled -- ordinary observing is lighter and runs longer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
brickbots added a commit that referenced this pull request Aug 6, 2026
* docs: plan for updating the user manual for rev4 hardware

v2.6.1 ships full software support for rev4, but docs/source/ still
describes rev3 exclusively -- and in the power/charging area it is
actively wrong for a rev4 owner (states there is no battery indicator
and no low-battery warning, documents a slide switch and a PiSugar
board that rev4 does not have).

This is a plan document only; no .rst is changed. It records the rev3 ->
rev4 delta with sources, four structural decisions that need confirming
before agents start, six agent-sized work packages with dependencies,
the hardware photos that have to be shot by hand, the screenshots agents
can produce themselves, and the hardware facts that are not in the repo
and must not be guessed.

Follows the bringup-implementation-plan.md precedent of keeping plan
docs at the repo root.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: correct rev4 directional input to a 5-way joystick

The plan described rev4's directional input as a "cluster" of separate
switches moved to the right-hand column, following the language in
keypad.py and the bring-up glossary. Physically it is one 5-way
joystick: four directions plus a centre press that duplicates SQUARE.

This matters in two places. For the manual, the joystick is the fastest
visual tell between rev3 and rev4 and needs a one-time explanation of
how it is operated -- the logical key names are unchanged, so nothing
else in the prose moves. For bring-up, a builder whose grid shows one
dead direction is reflowing a single component, not hunting for one of
five switches that does not exist.

Also flags the underlying terminology gap as a grill-with-docs
candidate: the bring-up glossary defines "switch" as one physical
pushbutton, which a multi-contact joystick is not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: record the four settled decisions, and make 176px capture real

Decisions from Rich, now folded into the plan:

- rev4 becomes the default voice; v3/v2.5 differences go in notes.
- "rev4" is introduced alongside "v3"/"v2.5" with no retro-rename.
- 176px is the default for new screenshots, no mass replacement, and a
  page's existing shots get re-taken when that page is edited anyway.
- The rev4 DIY build path is out of scope and gets no scoping note,
  which deletes WP5 outright.

WP0 is implemented here rather than left for an agent, because the plan
should not document a flag that does not exist. pf_remote.py launch now
takes --display (defaulting to headless_176, rev4's panel) and -fb for
the rev4 battery monitor. Verified end to end: launched headless with
-fb, captured /api/screen at 176x176 with the battery icon in the title
bar, converted to a 352x352 doc-ready PNG.

Both skills are updated to match, and the docs skill's product knowledge
base is rewritten -- it described rev4 as a planned future revision with
no working prototype, which would have poisoned anything written from it.

Docs build stays clean: zero warnings under sphinx-build -n.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: resolve the rev4 runtime open question

The bench campaign closed 2026-07-26 (ADR 0020 updated in PR #573), so
WP1 no longer has to leave a TODO where the battery-life number goes:
about 10 hours, measured on two units under the pinned worst-case load.

Framed as a floor rather than a typical figure, since it was measured
with the camera solving continuously, the screen at full brightness and
sleep disabled -- ordinary observing is lighter and runs longer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: surface the 10h runtime and say where WP agents should branch from

Two things agents need before they start.

The measured runtime figure now appears in the rev3->rev4 delta table in
§1, not only in the resolved open question at the bottom, since §1 is
where an agent looks first.

Branching guidance: WP1-WP4 must not branch from main while #572 is
open. They depend on the pf_remote 176px flags, the docs skill's rev4
rules and the corrected product knowledge base, all of which live only
on this branch -- and a branch cut from main looks healthy while
silently producing 128px screenshots and rev3-default prose. Preferred
fix is to merge #572 first; otherwise reset the worktree onto this
branch and open WP PRs with --base pointing here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@brickbots
brickbots marked this pull request as ready for review August 6, 2026 23:53
@brickbots
brickbots merged commit 27ca962 into main Aug 6, 2026
4 checks passed
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