Skip to content

Bengle integrated scale calibration wizard - #6

Open
ChampionDesigns wants to merge 5 commits into
ben/integrated-scalefrom
ben/scale-calibration
Open

ChampionDesigns wants to merge 5 commits into
ben/integrated-scalefrom
ben/scale-calibration

Conversation

@ChampionDesigns

@ChampionDesigns ChampionDesigns commented Aug 25, 2026 •

Copy link
Copy Markdown
Owner

Stacked — review the LAST commit only: 7a8f2d4 "Bengle integrated scale calibration wizard".

This lane needs the tare write from the integrated-scale PR and the calibration-flow
navigation from the machine-autonomy PR. A branch cannot root on two unmerged branches,
so it is a merge of both, and GitHub's merge-base falls back to ben/detect-protocol.
The PR therefore lists five commits and ~1,269 lines; only the last commit (882 lines) is
new here. The other four are under review as PRs #1, #2 and #4.

Filed inside the fork so the other PRs' diffs stay clean. It will be re-filed against
decentespresso/de1app once its bases have merged there.

ben/machine-autonomy. Retarget to main once both have merged. 882 insertions, 10 files.

Summary

The Bengle scale uses two load cells and needs a per-cell calibration. This adds a
seven-step wizard driving the firmware's calibration state machine over MMR, and the
calibrate4 page that presents it.

register access address encoding
ScaleCalCmd W 0x00803880 0 abort, 1 zero, 2 latch
ScaleCalState R 0x00803884 packed U32
ScaleCalWeight RW 0x00803888 grams ×10
bits 31:24  Step             0 idle, 1 zeroing, 2 latching, 4 taring, 5 complete, 6 error
bits 23:20  DetectedCell     0 none, 1 cell A, 2 cell B
bits 19:16  SubState         0 settling, 1 averaging, 2 done, 3 error
bits 15:8   SecondsRemaining
bits  7:0   Status           0 ok … 8 not isolated, 255 none

There is no step 3. The set is 0, 1, 2, 4, 5, 6.

Addresses, packing, command values and every enum match decentespresso/decaid, which is
the only other implementation of this word — the two reaprime skins reach the flow through
decaid's REST API rather than decoding it themselves.

The seven steps

Remove platform, zero cells, calibrate first cell, calibrate second cell, replace platform,
tare, verify.

Steps 3 and 4 do not ask which cell is which: the firmware reports DetectedCell from
where the weight was placed, so the user puts it on one side and then the other in either
order.

Changes

scale_calibration.tcl — new file. The ::scale_cal namespace: step state, firmware
state from the last poll, a one-second poll timer that runs only while a measurement is
active, status and progress bound to the page, and validation of the reference mass.

pkgIndex.tcl, gui.tcl, de1_comms.tcl — package registration and requires.

de1_comms.tcl and bluetooth.tcl — decode ScaleCalState and ScaleCalWeight in
both MMR-read paths, since a read can arrive on either.

skins/default/ — the calibrate4 page as page 5 of the calibration flow, and its
registration in standard_includes.tcl.

ScaleTare is not added here. It lives in de1_comms.tcl as set_bengle_scale_tare,
added by the integrated-scale PR, because the ordinary scale tare path needs it whether or
not this wizard ships.

Bounds and status handling

The reference mass is bounded to 1 g – 10 kg, matching decaid and both reaprime skins.
All nine firmware status codes map to distinct messages. The tare step polls for the
firmware's own completion rather than declaring success a fixed delay after the write.
Leaving via Ok aborts a run in progress.

Known issues, not fixed here

Raised by a review of the series, recorded rather than fixed. Not reproduced on hardware.

  • The "Page 5 >" button does not call abort_if_running, and enter_page does not
    stop_polling.
    The 2 Hz update_verify_display timer may survive page exit and can be
    duplicated, each new loop orphaning the previous poll_after_id. Exiting via Ok does
    abort correctly.
  • is_bengle_model is evaluated at skin-load time, so on a first-ever connect the page
    may not be registered. Same root cause as in the LED and autonomy PRs.

Impact on a DE1

The package loads and defines a namespace. calibrate4 is reachable only from the Bengle
calibration flow. The two decode branches match addresses a DE1 never reports.

Test plan

  • scale_calibration.tcl sources and runs end to end against stubs: page entry,
    reference-mass validation at both bounds and past them, state-word decode, every
    error code, and the tare step.
  • Confirm the app starts on a DE1 with the new package loaded and no error.
  • TODO (Ben): run all seven steps on a Bengle with a known weight; confirm both cells
    are detected in either order.
  • TODO (Ben): abort mid-measurement; confirm the firmware returns to idle.
  • TODO (Ben): leave the page mid-run via "Page 5 >" and confirm the poll timer stops —
    this is the known issue above.
  • TODO (Ben): verify accuracy against a reference scale.

Testing

Manual verification and a stub harness. There is no automated test coverage for these Tcl
paths in the repo.

Etzzo and others added 5 commits August 25, 2026 16:24
A Bengle exposes an extra BLE characteristic carrying a superset of
ShotSample at higher precision, plus a milk probe and integrated-scale
fields the stock packet has no room for. 28 bytes, big-endian, matching
the firmware's T_BengleShotSample and decodeBengleShotSample in
decentespresso/decaid.

ShotSample (0xA00D) keeps its stock v1 layout on every machine, a Bengle
included, so a stock build talking to a Bengle still works and this app
talking to a DE1 still works, with no negotiation on the shared packet.

Weight is SIGNED (S16P4). It is net of tare, so an unloaded platform after
a tare reads negative; clamping at zero would hide a real reading.

from_shotvalue splits into from_shotvalue / from_bengleshotvalue and a
shared _apply_shotvalue. The stale-frame guard added by decentespresso#343 moves into
the shared body, so it now protects the 0xA013 path too. The
superset-only fields are picked up with [info exists].

On a Bengle the 0xA00D dispatch is gated off: it still streams, but
charting both would double-sample with broken intersample timing.
Receiving 0xA013 pins the protocol to v2 immediately, closing the window
between connect and the model MMR read.

The enable is gated on the characteristic being present in ::cinstance.
That gate is load-bearing: on a DE1 the enable would throw on the unset
instance, and the vital-retry path re-runs a failed vital command every
500 ms WITHOUT advancing the FIFO, stalling the whole BLE queue.

gui.tcl normalises sample-to-sample deltas to their 5 Hz-equivalent
magnitude using the measured intersample time. The chart math was
implicitly tuned for 5 Hz; without this the delta trace and the
diff_flow_rate_text readout scale with the notify rate. Falls back to the
raw delta when the event carries no intersample time.

The milk probe lands here too: ::de1(milk_temperature), a
steam_milk_temperature chart vector, and milktemp / milktemp_text.
0 means no probe. set_target_milk_temp is gated and clamped to 0-85 C,
matching BengleSteamMmr.targetMilkTemp in decaid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The machine only wakes, sleeps and pre-warms while the tablet app is
running and connected. A Bengle can do all of it itself. This gives the
firmware what it needs.

  CupWarmerMode          0x008038AC  RAM only, re-sent every connect
  MatHeaterDrivePct      0x008038B4  read-only
  MatTempFault           0x008038B8  read-only
  InactivitySleepTimeout 0x008038BC  minutes, 0-240
  SetLocalTimeOfWeek     0x008038C0  seconds since Sunday, 0-604800
  ScheduleEntry          0x008038C4  packed (dow<<22)|(start<<11)|end
  ScheduleControl        0x008038C8  0 clear+disable, 1 enable
  MatPreheatEnable       0x008038D0  persisted
  MatPreheatLeadMin      0x008038D4  persisted, 0-120

Every address, clamp and the schedule packing match decentespresso/decaid.

There is no battery-backed RTC. The firmware keeps a software wall clock,
seeded on connect and re-sent every 30 minutes. A power cut loses it and
the next connection re-seeds it; until then only the inactivity timer
runs, so the machine can still sleep itself but cannot wake on schedule.

Schedule sources are the D_Scheduler plugin's per-weekday wake times and
the built-in scheduler's keep-warm window. They cannot double up: the
plugin clears scheduler_enable.

Pre-warm is the firmware's job. With MatPreheatEnable set it runs the mat
from MatPreheatLeadMin minutes before a scheduled wake with no tablet
connected. Write order follows decaid: enabling sends the lead first so
the firmware never acts on a stale one; disabling clears the enable first.

CupWarmerMode is deliberately sent as 0 on every connect. The warmer turns
on only when the user asks or the pre-warm fires -- a machine does not
start heating by itself after a blackout. utils.tcl resets the flag at app
start for the same reason.

Adds the cup warmer page as page 4 of the calibration flow, with live
status from MatHeaterDrivePct and MatTempFault. Nothing requested those
two reads before, so that status could never update; get_cupwarmer_status
now asks for them on connect.

Corrects four addresses in the page's header comment, each of which named
the register one slot below the real one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A Bengle has a scale in the machine, with no BLE peripheral and no handle.
Its weight and gravimetric flow arrive on 0xA013. Every scale check in the
app is really a check for a paired BLE address, so on a Bengle the weight
readouts return empty, the stop-at-weight slider is hidden, and the timer
falls back to volumetric text while a working scale streams the whole time.

_apply_shotvalue feeds Weight into ::device::scale::process_weight_update,
the same entry point all fourteen BLE scale drivers use. That pipeline owns
the weight history, the filtered weight and flow, the
scale_stop_at_half_shot two-cup scaling, tare-completion detection, the
watchdog, the app-side stop-at-weight check and drink-weight recording.
Setting ::de1(scale_weight) directly instead writes the pipeline's outputs
and skips all of it.

GFlow, the firmware's own gravimetric flow, is NOT used for
scale_weight_rate. The pipeline derives flow from the weight history as it
does for every other scale. GFlow is kept on ::de1(integrated_scale_flow)
for the comparison chart; switching to the firmware estimate wants a bench
comparison first.

::device::scale::tare selects on scale_type, which a Bengle does not have,
so the Bengle case is handled before the switch and writes ScaleTare
(0x0080388C). This matters more than a tare button: tare is called
automatically before every espresso and hot-water pour when a cup is on the
platform, from the HotWater state handler, and from any profile step whose
message contains "tare".

set_end_of_shot_weight writes the per-profile target to EndOfShotWeight
(0x00803864, grams x100, clamped 0-1000000) on every frame upload, so the
machine-side target follows the loaded profile. Writing 0 clears one left
on disk by a previous profile.

Three chart vectors record the integrated series beside the existing ones
so the two sources can be compared. On a DE1 they stay empty.

The Visualizer payload maps by protocol. On v1 nothing changes. On v2 the
integrated series fill by_weight and weight, and a paired BLE scale is
emitted as by_weight_external / weight_external rather than dropped.
by_weight_raw is emitted on both, as today.

sensor_lag selects on scale_type, so a Bengle falls to the 0.38 s BLE
default. An integrated scale should be lower. Left at the default rather
than guessed; it needs measuring.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Seven-step wizard driving the firmware's calibration state machine over
MMR, plus the calibrate4 page that presents it.

  ScaleCalCmd    W  0x00803880  0 abort, 1 zero, 2 latch
  ScaleCalState  R  0x00803884  packed U32
  ScaleCalWeight RW 0x00803888  grams x10

ScaleCalState packs Step(31:24) DetectedCell(23:20) SubState(19:16)
SecondsRemaining(15:8) Status(7:0). Step values are 0,1,2,4,5,6 -- there
is no step 3. Addresses, packing, command values and every enum match
decentespresso/decaid, the only other implementation of this word.

Steps 3 and 4 do not ask which cell is which: the firmware reports
DetectedCell from where the weight was placed.

ScaleTare is not added here. It lives in de1_comms.tcl as
set_bengle_scale_tare, added by the integrated-scale PR, because the
ordinary scale tare path needs it whether or not this wizard ships.

The reference mass is bounded to 1 g - 10 kg, matching decaid and both
reaprime skins. All nine firmware status codes map to distinct messages.
The tare step polls for the firmware's own completion rather than
declaring success a fixed delay after the write. Leaving via Ok aborts a
run in progress rather than leaving the poll timer armed.

This branch sits on both the integrated-scale PR (for the tare write) and
the machine-autonomy PR (for the calibration-flow navigation).

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

3 participants