Skip to content

feat(scale): add Timemore Dot BLE scale support - #739

Open
Sofronio wants to merge 3 commits into
decentespresso:mainfrom
Sofronio:feature/timemore-dot
Open

Sofronio wants to merge 3 commits into
decentespresso:mainfrom
Sofronio:feature/timemore-dot

Conversation

@Sofronio

@Sofronio Sofronio commented Aug 30, 2026

Copy link
Copy Markdown

Summary

Add support for the Timemore Dot scale, a compact BLE-only scale from Timemore with no screen and no buttons — all interaction happens over Bluetooth. Pairing is initiated by a long press on the button on the bottom of the scale until the white LED flashes rapidly; a slow flash means it is waiting for a connection, and a solid LED means connected.

What this PR adds:

  • New TimemoreDotScale implementation (service FFF0, weight notifications on FFF1, commands on FFF2) with its A5 5A framed protocol, ported from a reference driver verified against a physical Dot
  • Protocol layer with CRC16/MODBUS command building; all seven verified reference command frames are byte-asserted in test/timemore_dot_protocol_test.dart
  • Framed notification parsing with a resync buffer, unit/mode init sequence (the Dot only reports weight after being told the unit and mode), weight at 0.1 g resolution, battery, and timer support
  • Detection via advertised names containing dot/tes017, plus nameless advertisements carrying the FFF0 service (the Dot can advertise without a name outside pairing mode)
  • Registration in DeviceImplementation, DeviceMatcher, DeviceFactory, and the discovery service

One hardware finding worth noting: notification frames from the Dot are not CRC-validated — captures from a physical unit show the two-byte frame tail stays 0x0000 across every weight/timer value, i.e. the firmware never emits a real CRC. Sending commands does require CRC16/MODBUS, and the seven verified command frames are locked in by tests.

Linked Issue

Fixes #737

Verification

  • 132 unit tests across the new protocol tests, scale tests, matcher tests, factory tests, and remembered-device migration tests — all passing
  • flutter analyze: no issues
  • Full test suite: 3598 tests passing
  • Hardware-verified against a physical Timemore Dot on macOS: device discovered, connects, reports live weight (e.g. 351.3 g), and responds to tare

Impact

  • New device type only — no existing behavior, API, or database changes
  • Docs updated: doc/AI_BLE_NOTES.md (protocol, CRC finding, connect sequence) and doc/DeviceManagement.md (nameless-advertisement matching)

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.

@tadelv tadelv left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two required fixes before this is in line with the repo guidance:

  1. The PR template hard gate is currently broken. The checked Contributor Responsibility line dropped the <!-- contributor-responsibility --> marker from .github/pull_request_template.md, so the Contribution policy workflow is failing and the analyze/test work is being skipped. Please restore the template marker and get the required CI checks green.
  2. There is a connection-lifecycle race in TimemoreDotScale.onConnect(); see the inline comment. Please add a regression test for disconnect-during-init.

Additional guideline findings from the audit:

  • CONTRIBUTING.md requires issue-first development for external contributors. The feature commit (abf6e0f, 2026-08-30 16:18:30Z) predates issue #737 (created 16:21:58Z), and the issue itself says the implementation was already ready. That process requirement was not followed; this is not retroactively fixable, but should be observed for future contributions.
  • The implementation commit subject (Add Timemore Dot BLE scale support) is not a Conventional Commit as required by CONTRIBUTING.md. Please reword it to the repo convention (for example feat(scale): add Timemore Dot BLE scale support).

Otherwise, the PR is well aligned with the device/BLE rules: it keeps third-party BLE details behind the transport abstraction, uses long UUIDs for BLE operations, catches DeviceNotConnectedException in the lowest-level scale write helper, adds focused tests, and updates the relevant device/BLE documentation.

}
await _registerNotifications();
await _initScale();
_connectionStateController.add(ConnectionState.connected);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can publish a stale connected state after the transport has already disconnected during the mandatory init delays. The disconnect listener sets the controller to disconnected, but _write() swallows DeviceNotConnectedException, _initScale() therefore completes, and this line overwrites the state back to connected. ScaleController.connectToScale() trusts the scale's state after onConnect(), so it can install a dead scale. Please verify the connection is still in the connecting generation/state before publishing connected (the Atomheart scale has this guard), and add a regression test that disconnects during init.

@Sofronio
Sofronio force-pushed the feature/timemore-dot branch from fe8fa56 to 9d3aaf3 Compare September 2, 2026 12:53
@Sofronio

Sofronio commented Sep 2, 2026

Copy link
Copy Markdown
Author

All three points addressed in 379a625b:

1. Connect raceTimemoreDotScale.onConnect() now mirrors the Atomheart guard: after _initScale() completes, it checks _connectionStateController.value != ConnectionState.connecting and throws DeviceNotConnectedException.scale() instead of republishing a stale connected over the disconnect already emitted by the listener. (The controller field is now typed BehaviorSubject so the guard can read .value.)

2. Regression test — added disconnect during init does not publish stale connected: the mock transport drops the link on the first init write and keeps throwing DeviceNotConnectedException for the rest, exactly the path where _write() swallows the exception and _initScale() completes anyway. Verified it fails without the guard.

3. Commit message — reworded to feat(scale): add Timemore Dot BLE scale support; the fix is folded into the feature commit.

4. PR body — restored the <!-- contributor-responsibility --> marker that the Contribution policy workflow greps for.

Also hardware-verified on macOS: connect, disconnect cleanup, and weight feed recovery after a scale power-cycle all behave correctly.

The PR checks workflow doesn't appear to have re-run after the push — likely needs approval for the fork run. Could you re-trigger when convenient?

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.

feat(scale): add Timemore Dot BLE scale support

2 participants