Skip to content

fix: restore AtomHeart Eclair BLE reliability - #639

Merged
tadelv merged 2 commits into
mainfrom
odev/issue-629-eclair-reliability
Aug 24, 2026
Merged

fix: restore AtomHeart Eclair BLE reliability#639
tadelv merged 2 commits into
mainfrom
odev/issue-629-eclair-reliability

Conversation

@ODevStudio

@ODevStudio ODevStudio commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

What changed, and why?

  • Replace the Decenza-style UUIDs with the current AtomHeart Eclair service,
    data/status, and command characteristics, and update scale discovery.
  • Send the timer reset, start, and stop opcodes used by current Eclair firmware.
  • Keep the scale connecting until a valid checksummed weight frame arrives.
    Reset a silent notification subscription at most twice, then disconnect so
    normal ConnectionManager recovery can retry.
  • Document the Eclair protocol and readiness behavior.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore / infra
  • Plugin (DYE2 or bundled skin)

Scope (select all touched areas)

  • BLE transport / device comms
  • REST API / handlers
  • WebSocket API
  • Machine state / shot logic
  • Scale / weight / flow
  • Profiles / beans / grinders / workflows
  • WebUI skins
  • Plugins / JS runtime
  • UI / Flutter widgets
  • Storage / Drift database
  • CI / build / infra
  • Docs / specs

Linked Issues

Fixes #629

Remaining hardware validation before merge: Reproduce the Android connection path on a current-firmware AtomHeart Eclair and verify service discovery, notifications, recovery, and timer controls.

Root Cause (if bug fix)

  • Root cause: The AtomHeart implementation used Decenza-style UUIDs and timer
    opcodes and marked the link ready before receiving a valid Eclair frame.
  • Missing detection or guardrail: Tests asserted neither current Eclair UUIDs
    nor the first-valid-frame readiness boundary.
  • Contributing context: The issue log may also contain a native GATT timeout
    outside the protocol layer; hardware validation remains mandatory.

Regression Test Plan (if bug fix or refactor)

  • Coverage level that should have caught this:
    • Unit test
    • Integration test (mock transport edge)
    • End-to-end test (simulate=1 + curl/websocat)
    • Existing coverage already sufficient
  • Target test or file: test/unit/models/atomheart_scale_reliability_test.dart
    and test/unit/services/device_matcher_test.dart.
  • Scenario the test should lock in: Current UUID matching, timer opcodes,
    checksummed first-frame readiness, bounded notification reset, and disconnect.
  • If no new test added, why not: N/A; regression tests were added first.

Documentation Obligations (required)

  • API spec updated: assets/api/rest_v1.yml or assets/api/websocket_v1.yml (if REST/WebSocket changed)
  • API docs updated: doc/Api.md (if user-facing endpoint changed)
  • Plugin docs updated: doc/Plugins.md (if events/API changed)
  • Skin docs updated: doc/Skins.md (if skin behavior changed)
  • Profile docs updated: doc/Profiles.md (if profile handling changed)
  • Device docs updated: doc/DeviceManagement.md (if device flows changed)
  • N/A - no docs affected

doc/AI_BLE_NOTES.md is also updated with the protocol constraint.

Security Impact (required)

  • New or changed REST endpoints? No.
  • New or changed WebSocket topics? No.
  • New or changed network calls? No.
  • BLE/USB surface changed? Yes, Eclair UUIDs, timer writes, and readiness
    recovery changed.
  • File system access changed? No.
  • Plugin sandbox boundary changed? No.
  • Risk and mitigation: Writes are limited to documented Eclair timer opcodes;
    notification recovery is checksummed, bounded to two resets, then fails
    closed through normal disconnect recovery.

User-Visible Changes

  • Current Eclair firmware is discovered through its actual service UUID and is
    reported connected only after usable scale data arrives.
  • Timer controls use the current Eclair protocol. A silent subscribed link
    fails closed after three 800 ms windows instead of appearing ready.

Verification

Local gates (run before pushing)

  • dart format lib test - no remaining candidate changes
  • flutter analyze - clean
  • flutter test - 3,186 passed, 1 skipped
  • ./scripts/fetch_dye2_plugin.sh - not rerun for this local draft

Manual verification (if applicable)

  • OS / platform tested: Not applicable to the changed behavior.
  • Simulated devices? (simulate=1): No.
  • Real hardware? (DE1/Bengle/scale): No; no Eclair was available.
  • What you personally verified and how: Protocol, matcher, readiness, and
    recovery behavior through focused tests.
  • Edge cases checked: Invalid/silent frames, bounded resubscription, timer
    reset/start/stop, and disconnect.
  • What you did not verify: Current-firmware Eclair behavior on the
    reporter's Android tablet. This PR has no meaningful changed visual surface,
    so desktop Computer Use cannot validate its BLE protocol behavior.

Evidence

  • Test output (failing before + passing after)
  • Log snippets
  • Screenshot / recording (UI changes)
  • curl / websocat output (API changes)

Detailed evidence:

  • Regression-first proof: the focused tests failed against the old UUIDs,
    timer opcodes, immediate-ready behavior, and missing silent-link recovery.
  • dart format --output=none --set-exit-if-changed lib/src/models/device/impl/atomheart/atomheart_scale.dart test/unit/models/atomheart_scale_reliability_test.dart test/unit/services/device_matcher_test.dart
  • Focused AtomHeart scale and device-matcher suite (49 passed).
  • flutter analyze --no-pub (no issues).
  • flutter test --no-pub with the package-matched QuickJS DLL on PATH
    (3,186 passed, 1 skipped).
  • No Eclair hardware was available locally. A current-firmware Eclair and the
    reporter's Android tablet remain required to validate native connection,
    service discovery, notification recovery, and timer controls end to end.

Compatibility & Migration

  • Backward compatible? Yes for supported current Eclair firmware; devices
    advertising only the obsolete Decenza-style UUID are no longer misidentified.
  • Config / env changes needed? No.
  • Database migration needed? No.
  • Exact steps: None.

Risks & Mitigations

  • Risk: The issue's field failure may occur before this protocol code, or a
    current device may behave differently from the captured protocol.
    • Mitigation: Require hardware validation before merge; validate service discovery,
      notifications, recovery, and timer controls on an Eclair before publishing.

Contributor Responsibility

AI-assisted development is allowed. The submitter remains responsible for the submitted work.

  • I have reviewed and understand all changes in this PR and take responsibility for their correctness, security, behavior, licensing, and provenance, including any AI-assisted or AI-generated work.

@ODevStudio

Copy link
Copy Markdown
Collaborator Author

[P1] The new readiness gate can accept a truncated frame as “valid.” In lib/src/models/device/impl/atomheart/atomheart_scale.dart:195, parseFrame() rejects only data.length < 9. But the parser consumes:

byte 0: 0x57 header
bytes 1–4: weight
bytes 5–8: timer
final byte: checksum

That requires at least 10 bytes. With a 9-byte input, data[8] becomes both the fourth timer byte and data.last, which is treated as the checksum.

A concrete malformed frame that passes today is:

57 00 00 00 00 00 00 00 00

The XOR of bytes 1–7 is 0, and byte 8 is also 0, so parseFrame() returns a zero-weight snapshot. The newly added _parseNotification() then completes _firstValidFrame, causing onConnect() to declare the scale connected. That defeats this PR's core guarantee that readiness requires a checksummed Eclair weight frame.

I'd change the guard to at least:

if (data.length < 10) return null;

If the Eclair notification is specified as fixed-width, data.length != 10 would be stronger. I'd also add a regression case emitting the 9-byte all-zero frame before the good frame and asserting the scale remains connecting.

Everything else I checked looks consistent: the UUID replacement propagates through DeviceMatcher, the reset/start/stop opcodes match the hardware-tested de1app change, production resetSubscription() genuinely disables/re-enables notifications rather than merely reattaching the Dart listener, and the Actions run passed format, analyze, tests, and Linux build.

@tadelv

tadelv commented Aug 20, 2026

Copy link
Copy Markdown
Member

@ODevStudio requesting re-review

@ODevStudio

Copy link
Copy Markdown
Collaborator Author

No further code-review findings. Prior P1 is resolved.
The current head is b4a1e25. The fix now enforces the Eclair frame as exactly 10 bytes, rejecting both the problematic 9-byte frame and overlong frames. It also adds the requested readiness-gate regression test: the malformed 9-byte frame produces no snapshot and leaves the scale connecting until a valid frame arrives.
I re-checked the surrounding notification retry/state transition logic and did not find another actionable correctness issue. The PR remains mergeable and is still intentionally a draft.
The latest PR workflow for this exact head completed successfully; format, analysis/tests, and Linux build all passed.
Review disposition: approve from a code-review standpoint. The remaining Eclair hardware/Android validation called out in the PR is still appropriate before taking it out of draft, but I would not treat that as another code-review finding.

@ODevStudio
ODevStudio marked this pull request as ready for review August 22, 2026 14:07
@ODevStudio
ODevStudio requested a review from tadelv August 22, 2026 15:17
ODevStudio and others added 2 commits August 24, 2026 13:34
Use the current Eclair GATT and timer protocol, and only report readiness after valid scale data. Retry a silent notification subscription twice before disconnecting for normal recovery.
The readiness gate accepted a 9-byte notification. The parser consumes a
0x57 header, four weight bytes, four timer bytes, and a trailing XOR
checksum, so 9 bytes let the fourth timer byte double as the checksum.
57 00 00 00 00 00 00 00 00 therefore XOR-validated as a zero-weight
snapshot and completed the first-valid-frame gate, marking the scale
connected without usable data.

Require the exact 10-byte Eclair frame that de1app's hardware-tested
parser scans, and cover both the truncated and over-long cases plus the
readiness boundary.
@tadelv
tadelv force-pushed the odev/issue-629-eclair-reliability branch from 75c342d to 65ab1ed Compare August 24, 2026 11:34
@tadelv
tadelv merged commit 57bc579 into main Aug 24, 2026
5 checks passed
@tadelv
tadelv deleted the odev/issue-629-eclair-reliability branch August 26, 2026 10:05
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.

[Bug Report] **eclair BLE scale not able to connect 蓝牙称无法连接上**

2 participants