Skip to content

feat(ble): client-token pairing with physical button confirmation - #71

Draft
kb1ibt wants to merge 5 commits into
flip-dots:mainfrom
kb1ibt:pr/pairing
Draft

kb1ibt wants to merge 5 commits into
flip-dots:mainfrom
kb1ibt:pr/pairing

Conversation

@kb1ibt

@kb1ibt kb1ibt commented Sep 20, 2026

Copy link
Copy Markdown

Replaces the pairing half of #49.

Firmware that pairs clients (C2000 G2 v0.3.3.0) answers the client registration with status 09 the first time it sees a token and waits for the button on the device; once pressed it pushes a grant on packet pattern 030101 and remembers the token, so later connections are accepted silently.

  • SolixBLEDevice(ble_device, capability=None, client_token=None): the token registered in 4027, defaulting to the class UUID string so the Prime bytes on the wire are unchanged.
  • 4827 status 00 authorizes the link; 09 sets pairing_required and runs the registered add_pairing_callback / remove_pairing_callback callbacks so the user can be told to press the button (the callback option from the feat(prime): A91B2 (240W) + A2345 (250W) live BLE telemetry + control #51 review). connect() keeps waiting for the grant instead of restarting the handshake; the grant on 030101 authorizes.
  • On the encrypted path negotiated now requires the authorization; the plain-text path is unchanged.
  • Docs: "Pairing with a button press" section in protocols.rst, including a note that an account's owner_user_id passed as the token registers the same bytes the app sends and pairs without a press.

Validated on hardware: C2000 G2 with a fresh token (button press) and with a remembered one.

Suite: 369 passed.

🤖 Generated with Claude Code

kb1ibt and others added 5 commits September 13, 2026 14:30
The Anker manufacturer record (company id 0xffff) carries the device MAC,
model, and a capability byte declaring which negotiation path the device
accepts -- readable at scan time, before any frame is sent. Add a parser
for it as the basis for choosing the cleartext vs encrypted handshake per
device rather than by product class.

Capability is length-relative (last byte when present, absent on the F3800),
so it is derived from the sku length rather than read at a fixed offset.
Decoded against the app's own field values for five bench records.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Move the AES-GCM negotiation out of PrimeDevice into SolixBLEDevice so any
device can take it. The path is selected from the capability byte in the
device's advertisement when the caller passes one, otherwise from the class
default (encrypted for Prime devices, plain text for the Solix power
stations). The device's declared MTU and auth mode from 4803 are echoed in
4005, and the timezone confer carries the local UTC offset.

A fresh P-256 key pair is generated for every negotiation instead of the
hard-coded key pairs; the keys the recorded test vectors were captured with
move to the test constants and are pinned by the fake_time fixture. The
cipher and ECDH primitives live in utilities as plain functions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The client registration on the encrypted path now carries a client token
(the class identifier unless the caller passes one) and the link only counts
as negotiated once the device accepts it. Firmware that pairs clients
answers a new token with status 9 and waits for its button: the device
reports pairing_required, runs the registered pairing callbacks so a user
can be prompted, and connect() keeps waiting instead of restarting the
handshake. The grant the device then pushes on pattern 030101 authorizes
the link. The device remembers the token, so later connections with the
same token are accepted without a press.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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