Skip to content

feat(transport): Adding Transport, Negotiation, Routing Handling - #78

Open
kb1ibt wants to merge 4 commits into
flip-dots:mainfrom
kb1ibt:pr/transport
Open

kb1ibt wants to merge 4 commits into
flip-dots:mainfrom
kb1ibt:pr/transport

Conversation

@kb1ibt

@kb1ibt kb1ibt commented Oct 5, 2026

Copy link
Copy Markdown

Second of four stacked PRs replacing #70; based on the status-byte PR. The existing negotiation code is unchanged and runs on the new routing.

Frames are now routed by their decoded header instead of whole patterns. Pattern 03 <composer> <channel>: the channel is the device's dispatch key (0x01 negotiation, 0x0f / 0x11 session) and the composer says whether the device sent the frame on its own (01) or echoed the request (00). The command's high nibble carries the frame's own state: 0x80 fragmented, 0x40 encrypted.

  • SolixBLE/constructs.py: PacketPattern, PacketCommand (flags + 12-bit message type; also builds), and the channel / composer constants.
  • SolixBLE/device.py:
    • channel 0x01 goes to negotiation whatever the composer, so the grant a device pushes on 030101 after its button is pressed reaches the negotiation;
    • session frames on either composer reach futures and telemetry, so 03000f replies are no longer dropped;
    • other channels are logged with both bytes and dropped;
    • fragments are reassembled when 0x80 is set, not when a frame happens to be 253 bytes long;
    • a session frame is decrypted once, when 0x40 is set; with two futures on one frame the second no longer gets a double decrypt;
    • _process_session(cmd, payload, encrypted=...) is the one place a session frame is dispatched, for later device-specific frame types to extend.
  • Transports (names from your legacy_f2000 branch, so Add support for older F2000/767 firmware version #64 can sit on top): SolixBLE/transport.py NegotiatingTransport (ff09, 8c850002/8c850003) and LegacyTransport (1780, 7777/8888). SolixBLEDevice._TRANSPORT selects one; UUID_TELEMETRY / UUID_COMMAND follow it, and on the legacy transport connect() sends no negotiation and negotiated is True once connected. const.UUID_IDENTIFIERS lists both services and discover_devices() matches either. (On legacy_f2000, discover_devices calls .intersection on that tuple, which raises AttributeError; this PR uses a membership test.)
  • SolixBLE/advertisement.py: the 0xffff manufacturer record as a construct; capability_from_advertisement().
  • SolixBLE/factory.py: device_class_from_advertisement() picks the model class from the advertised product type, else a Prime-style name <part number>_<MAC4>, else Generic; None on the legacy transport.
  • Docs: docs/source/protocols.rst "Transports" (packet layout, channels, transports, advertisement); the two helpers in helpers.rst.
  • Tests: test_constructs.py, test_routing.py (grant routing, unhandled channel, flag-driven decryption and reassembly, two futures), test_transport.py, test_advertisement.py, test_factory.py. MockDevice records the GATT UUIDs it is subscribed to and written to.

Suite: 400 passed (base: 353). mypy --strict: 207 errors, one fewer than the base; ruff adds nothing to the existing files.

kb1ibt and others added 2 commits October 5, 2026 16:23
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>
@kb1ibt

kb1ibt commented Oct 6, 2026

Copy link
Copy Markdown
Author

I was checking some of the older tickets, and #1 included some plaintext 8405, which further confirms the 0x80 fragment check, instead of needing to verify the mtu sizes.

kb1ibt and others added 2 commits October 6, 2026 03:04
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>
@kb1ibt

kb1ibt commented Oct 6, 2026

Copy link
Copy Markdown
Author

Those same #1 frames also showed the telemetry match was on the whole wire command, so a clear 8405 fell through as unknown on a class that lists c405. Two follow-up commits on this PR (632e4ad, fc54998), from going through the older issues here and in HaSolixBLE:

  • Telemetry by message type: _process_session matches the 12-bit msgtype PacketCommand already decodes, so 8405 / c405 / 0405 are one type; the always-telemetry 0300 stays as type 0x300. Test uses Solarbank 2 E1600 Pro wrong values #1's three real fragments.

  • Factory, from posted adverts:

    product type class source
    b112 F3800 F3800 Plus (Testing this with Solix F3800 Plus #2); anker-solix-api's A1790P map equals A1790's
    b006 Solarbank2 Solarbank 2 E1600 Pro (Solarbank 2 E1600 Pro wrong values #1)
    b103 C800 HaSolixBLE#17; A1753 / A1754 / A1755 share one map
    b119 C1000G2 A1765, the A1763's display-board build

    plus the advertised model names seen in the issues as a fallback after the product type (some firmware advertises the serial as the name: [Feature Request] Add support for Anker SOLIX C2000 power station #66, HaSolixBLE#24).

  • Transport2215: the A1340 power bank in HaSolixBLE#48 advertises 2215 and uses 22150002 / 22150003, but per atc1441's Anker_Prime_BLE_hacking tool it's the same ff09 framing and negotiation, so it's a third transport rather than a new protocol. No class yet, so the factory returns None for it as for 1780.

  • discover_devices: also matches the 0xffff record, since a passive scan (HaSolixBLE#17) carries it without the services, and it now uses the scanner it's given.

The F3800 / F3800 Plus and C800 mappings are untested on BLE; if an owner can try device_class_from_advertisement() and the class it returns, that would confirm them.

@kb1ibt
kb1ibt marked this pull request as ready for review October 6, 2026 18:03
@kb1ibt

kb1ibt commented Oct 6, 2026

Copy link
Copy Markdown
Author

Since #77 is just a +14/-4 in the code and a new file in tests, I'm submitting this for review as well @flip-dots

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