Repository navigation
Conversation
Parameters only recognised a 00 prefix, so a reply such as 4827 09 a1021e00 parsed to an empty dict. Any first byte below the first TLV tag (0xa1) is now the reply's status, exposed as ParameterDict.status; pushes have none. Building also wrote no prefix: the If condition combined two construct expressions with Python's `or` at import time, which left only `this._parsing`. A parsed reply now builds back to the same bytes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Frames are routed by the decoded pattern (composer, channel) and command (0x80 fragmented, 0x40 encrypted, 12-bit message type) instead of whole patterns: - channel 0x01 reaches the negotiation on either composer, so the 030101 grant does too - session frames on either composer reach futures and telemetry; 03000f replies are no longer dropped - other channels are logged and dropped - fragments are reassembled on the 0x80 flag instead of a 253-byte length - a session frame is decrypted once, on the 0x40 flag; a second future no longer gets a double decrypt - _process_session is the one dispatch point for session frames SolixBLE.transport adds NegotiatingTransport (ff09) and LegacyTransport (1780, the flip-dots#64 transport); _TRANSPORT selects the GATT characteristics, a legacy-transport device skips negotiation, and discover_devices matches either service. SolixBLE.advertisement decodes the 0xffff record with a construct, and SolixBLE.factory picks the model class from it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From the advertisements in SolixBLE and HaSolixBLE issues: product types b112 (F3800 Plus, the A1790's command and telemetry map), b006 (Solarbank 2 E1600 Pro), b103 (C800; A1753/4/5 share one map) and b119 (C1000 Plus / X Gen 2, the A1763's display-board build), and the advertised model names. The A1763 part-number entry goes: that model advertises "SOLIX C1000 Gen 2". The A1340 Prime power bank advertises service 2215 and uses 22150002/22150003 characteristics with the ff09 framing and negotiation; it gets Transport2215, and the factory returns None for it as for the legacy transport until a class exists. discover_devices also matches the 0xffff record, which a passive scan carries without the services, and uses the scanner it is given. Telemetry is matched on the 12-bit message type, so a clear fragmented 8405 (SolixBLE #1) is read as the c405 a model lists; the always-telemetry type stays 0x300. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A device holds a negotiated session per connection (SolixBLE.protocols): an outer protocol (PlainOuter: 0001 in clear, CBC session; EncryptedOuter: 4001 under the static GCM key, GCM session) and the ECDH path after the shared opening. The path sends a fresh P-256 key every negotiation, states its x005 method as the app does per outer, and sends x022 with the UTC offset as int32 seconds west and the POSIX time zone. The outer comes from the advertisement's capability byte when given, else the outer that last authorized on this instance, else the model's default. A device that drops the link before answering a plain 0001 refuses it: connect() reopens once with the encrypted outer and never steps down. Encrypted sessions authorize at 4827 00 or the first decryptable session push; 4827 on 030101 reaches the same handler, and a 09 status and its window are recorded on device.announcement. The opening re-send and deadline sit behind _negotiation_should_restart() / _negotiation_deadline(). PrimeDevice keeps its UUID, the encrypted default and _post_authorize. Prime commands use the base's typed fe 05 03 trailer, and 420a carries the region (set_region(), else the host locale's, else GB) typed 02 and this client's identifier as the owner, as the app sends it. The changed test frames are derived from the recorded sessions. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A device that refuses the plain 0001 can drop the link before the write returns, and the write then raises BleakError out of connect(). If the link is down by then, the error is the refusal: the negotiation sees a dropped link and connect() reopens with 4001, as when the drop lands after the write. Seen on an A1783. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
|
One follow-up commit on this PR (
#80 and #81 are rebased on it. Suite: 469 passed (468 before this commit). mypy |
This branch has not been deployed
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.
Third of four stacked PRs replacing #70; based on #78. It answers the protocol half of your review: each variant is its own class, chosen without a bool, and a device holds the one its firmware speaks.
A device now holds a negotiated session for each connection (
SolixBLE/protocols/,typing.Protocolinterfaces with@override):PlainOuter/EncryptedOuter): how the negotiation travels and which cipher the session uses. Plain opens with0001in the clear and runs CBC; encrypted opens with4001under the static GCM key and runs GCM. The session cipher follows the opening frame.EcdhPath): the ECDH key exchange that follows the shared opening, on either outer. It sends a fresh P-256 key every negotiation (as Add AS220 (SOLIX S2000) device support #65 and anker-solix-api#345 do), states its method inx005as the app does (a5 40on plain,a5 44with the device's MTU and auth mode on encrypted), and sendsx022with the UTC offset as int32 seconds west and the POSIX time zone.DeviceClass(ble_device, advertisement=...)), else the outer that last authorized on this instance, else the model's default. A device that drops the link before answering a plain0001refuses it, andconnect()reopens once with the encrypted outer in the same call; the encrypted outer is never stepped down to plain.DeviceClass(ble_device)with no advertisement keeps working for HaSolixBLE, andtype(device)stays the model class.0821; encrypted at4827status00, or at the first session push the client can decrypt. A4827on the device-initiated pattern030101(the grant after a button press) reaches the same handler. A09status and its confirmation window are recorded ondevice.announcement; waiting for the button is left to the pairing PR, which overrides_negotiation_should_restart()/_negotiation_deadline().PrimeDevicekeeps its UUID, the encrypted default and_post_authorize(4200,420a); its copy of the negotiation and its own_send_commandgo.Prime frames: every Prime command now ends with the typed timestamp trailer
fe 05 03 <ts>(the base_send_command), and420ais built as the app builds it:a2= region typed02(set_region(), else the host locale's, elseGB),a3= this client's identifier as the owner, noa5. These match what the Anker app sends to the Prime Charger 250W (A2345) and the Prime Charging Station (A91B2) and what their firmware reads. I have no 160W or 20K power bank to test on, so their new bytes are untested; could you, or a 160W/20K owner, try it?Derived, not recorded, test vectors: the C1000 plain
4022(new layout), the Prime4200/420a, and the 32 Prime command frames with the typed trailer. Each was derived from the recorded session (the pinned test key and the device's recorded public point) and the old frames decrypt to the old plaintexts first. The pinned keys reproduce the public points in the recorded0021/4021, so the recorded negotiations are unchanged up to those frames.Also:
typing_extensionsfor Python < 3.12 (override);PRIVATE_KEYandNEGOTIATION_COMMAND_*move fromconst.pytotests/const.py;tests/helpers.pygainsinstall_session_keys,RecordingLinkandMockDevice.refuse_after; docs:protocols.rst"Outer protocols".