Conversation
A barista weighs the dose on one scale and the shot on another, and only one of them can be connected. `ScaleController` has a single slot, and the brewing path reads its weight, its generation and its snapshot stream, so turning that slot into a role map would put tare-before-pour, stop-at-weight and the shot record on the diff for a reading that no part of brewing consumes. So this adds a second controller beside it instead. `DosingScaleController` has its own slot, its own connection and its own snapshot stream, and knows nothing about shots. `ScaleController` is untouched. The brewing path learns exactly one thing: a `dosingScaleId` setting beside the existing `preferredScaleId`, and a single exclusion in scale selection — every scale except the one reserved for dosing. With no dosing scale set the selection behaves as it did, which is the regression that matters and is covered by a test. Skeleton for hardware testing: the controller, the setting and the selection rule. The REST and WebSocket surface (`/api/v1/scale/dosing/tare`, `ws/v1/scale/dosing/snapshot`) follows, as does device-management UI for choosing which scale is which. Design: doc/plans/2026-09-12-dosing-scale.md Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Completes the skeleton into something usable. The selection split moved to where selection actually happens. The exclusion had gone into `ConnectionSelectionSession.acceptsScale`, which nothing in lib/ calls — the brewing scale is picked by `resolveScalePolicy` from the list `_runScalePhase` hands it. That list is now split: the reserved scale is removed from what brewing is offered, and goes to `DosingScaleController` instead. Without a `dosingScaleId` the list is passed through untouched and the old path runs unchanged. Its weight is served on `ws/v1/scale/dosing/snapshot` and tared with `PUT /api/v1/scale/dosing/tare`, both beside the brewing scale's endpoints rather than sharing them, so no skin has to learn anything to keep working. The shot-lockout on tare does not apply to a scale no shot reads. Device management gains a Dosing Scale section. Each list hides the scale the other has taken, so the two cannot be set to the same device. A Bengle still takes the brewing slot with its integrated scale and skips external discovery, so no dosing scale is connected in that state. Design: doc/plans/2026-09-12-dosing-scale.md Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The dosing scale is chosen by hand and never remembered on connect, so the page offering that choice has to be able to find a scale that has not connected yet. It lists whatever discovery has found, which before a scan is nothing at all. So it scans: on open when nothing has been discovered, and on demand from a button that reports what it finds. Discovery only, never connecting — connecting here would hand a scale to brewing before the user has said which one weighs the dose. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two differences from `ScaleController.connectToScale` that only show on hardware: the scale was claimed before `onConnect()` had returned and the resulting connection state was never checked, so a scale that failed to come up was still recorded as the dosing scale. And the connect ran outside `_trackConnectionWork` with no timeout, so a scan could start on top of it — connecting during an active scan is exactly what fails on Android. Both now follow the brewing path. The attach also says what it decided, so a dosing scale that is configured but never reached can be told apart from one that was never looked for: `not among the N scales this scan found`, `already connected`, `Connecting dosing scale <id>`, and the existing success and failure lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The design doc moves to doc/plans/archive/ before the PR, per the pre-PR checklist. A stray dart format run on the test file comes with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Author
|
Design discussion for the shape of this — additive now vs waiting for role-keyed multi-scale — is at #835, since "should this exist in this form at all" is a maintainer call rather than a review comment. Happy to rework or withdraw this PR on the back of that conversation. |
This was referenced Sep 13, 2026
1 task
Author
|
Superseded by #869, which reworks this around the generic model asked for in #833: one semantically special scale, auxiliary connections that are explicitly requested, runtime-only, never taken by primary auto-selection, and never read by the brewing path. No core dosing role, no persisted Closing in favour of that PR. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
What changed, and why?
DosingScaleControlleris a new class with its own slot, connection subscription and snapshot stream.ScaleControlleris not modified, so tare-before-pour, stop-at-weight and shot recording are not on this diff.dosingScaleIdsetting sits beside the existingpreferredScaleId. The brewing path's exposure is a single exclusion in_runScalePhase: the reserved scale is filtered out of the list handed to_applyScalePolicy, and every other scale is accepted exactly as before. With nodosingScaleIdset,_runScalePhasereturns down the original path untouched.ws/v1/scale/dosing/snapshotandPUT /api/v1/scale/dosing/tareare new;ws/v1/scale/snapshotandPUT /api/v1/scale/tareare unchanged.targetDoseWeightthe way a typed dose does.The design doc, including the alternatives rejected and why, is at
doc/plans/archive/dosing-scale/2026-09-12-dosing-scale.md.I understand role-keyed multi-scale is the better end state and is presumably where this lands eventually. If it lands, this should be removed rather than kept alongside it.
Linked Issue
Fixes #833
Verification
How did you verify the change? Include relevant tests and any manual or hardware testing.
flutter analyze— no new issues; the only output is four pre-existingasset_directory_does_not_existwarnings for asset dirs a fresh checkout does not have.flutter testin full, against a baseline. My machine has a large number of environmental failures (core.autocrlf=truebreaking byte-comparison tests, pluswebui_supporttests that bind port 3000, which was occupied), so rather than assert they were pre-existing I ran the same suite onorigin/mainin a second worktree and diffed the failure sets:origin/main(39c2156)The two sets of 384 are identical — no test fails on this branch that does not also fail on
main, and none of the 384 is in a file this branch touches. The 21 extra passes are the tests added here.test/controllers/dosing_scale_controller_test.dart— 16 tests in three groups: the brewing selection's handling of a reserved id (4), which scales brewing is actually offered (5), and the controller's own connect / tare / disconnect / generation behaviour (7). Two of them pin the no-dosing-scale case: "accepts every scale when no dosing scale is set" and "no dosing scale set leaves the list untouched".test/device_management_page_test.dart— 5 tests, including "a scale taken for dosing is not offered for brewing" and "with nothing reserved both scales are offered to brewing", which is the regression that matters.test/unit/controllers/settings_controller_test.dart—dosingScaleIdround-trips through the settings service.On hardware, by the reporter, with two BLE scales connected at once: both snapshots arrive on their own sockets, taring one does not tare the other, and the brewing scale behaves as it did. Hot water and steam were exercised as well as the dosing readout. Two scales connected simultaneously is not an open question here.
The first hardware attempt failed to connect the dosing scale while the same scale connected fine as a brew scale. Two differences from
ScaleControllerwere the cause: the dosing path claimed the scale beforeonConnect()returned and never verified the resulting state, and it ran outside_trackConnectionWorkwith no timeout, so a scan running concurrently on Android could stall it indefinitely. Both are fixed inde348cb5and the connect now matches the brewing one.Impact
Note any user-visible behavior, compatibility, migration, API/spec, documentation, or security impact. Write
Noneif there is none.ws/v1/scale/dosing/snapshotandPUT /api/v1/scale/dosing/tare.assets/api/rest_v1.ymlandassets/api/websocket_v1.ymlare updated in the same commits. No existing endpoint changes shape or behaviour.dosingScaleIdunset — which is every existing installation — the scale phase takes the same path it does today.dosingScaleId. Nullable, no migration.doc/Api.mdanddoc/DeviceManagement.mdupdated; the design doc is archived underdoc/plans/archive/dosing-scale/.Contributor Responsibility
AI-assisted development is allowed. The submitter remains responsible for the submitted work.
🤖 Generated with Claude Code