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>
The x803 reply picks the key-establishment path from a registry (PATHS = EcdhPath, LegacyAesPath); each path's matches(announcement, outer) decides, so legacy AES is offered only after a plain 0001 and never by stepping down from 4001. ECDH matches a3 & 0x44 with an auth method in a5, on either outer. Legacy AES (flip-dots#63) matches a1 & 0x02 on the plain outer: 0005 states AES, CBC keyed on the client id and serial carries 4022, the key in 4822 becomes the session key with the same IV, and 4023 binds the client. No matching path, a rejected x805, or an x821/x822 without a key raises UnsupportedNegotiation (now exported); connect() returns False at once, logging the declared values and the stage. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.
Last of four stacked PRs replacing #70; based on the negotiation PR. The device's capability reply (
x803) now picks the key-establishment path, so a device the library doesn't speak fails fast with an error naming what it declared, and the older AES-only modules (#63) get their own path.SolixBLE/protocols/session.py,PATHS = (EcdhPath, LegacyAesPath)): each path hasmatches(announcement, outer); the first that matches runs the rest of the negotiation. A path can depend on the outer, so legacy AES is offered only after a plain0001and is never reached by stepping down from4001.a3 & 0x44with an auth method ina5(every current build), on either outer.a1 & 0x02on the plain outer, e.g. the oldera1 02·a3 04· noa5reply in F3800 (A1790) fails encryption negotiation — device expects a newer key exchange (a5=02,4022/4822/4023/4823) instead of0021/0821#63.0005states AES (a5 02); after0805 00the client keys CBC on its client id's first 16 bytes with the device serial's first 16 bytes as IV and sends4022(clock);4822returns00 a1 10 <16-byte key>, which becomes the session key with the same IV; then4023binds the client (a2client id,a3serial), as the app sends it.x805status other than00, anx821without the device key, or anx822without a key raiseUnsupportedNegotiation(now exported), andconnect()returnsFalseat once, logging the declared values (base_method,encrypt_method,auth_method,mtu) and the stage. Before, such a device was answered with the ECDH steps regardless and ran into the negotiation timeout.protocols.rst"Paths" (thex803table and how to add a path).Legacy AES: the opening, the
0005, the initial key and IV, and the4822layout match the Anker app's own legacy sessions with an older Prime module, decrypted end to end. The switch to the4822key (IV unchanged) is how the device firmware installs it; I have no device on that firmware, so the path after4822is untested on hardware. #63 F3800 owners: could you tryF3800(ble_device)on this branch and post the debug log? The tests use synthetic values only.Also:
EcdhPathandLegacyAesPathsharerecord_identityandsend_clockfromprotocols/base.py;tests/helpers.pygainsfeed_negotiation.