Skip to content

Unknown session frames aren't reassembled — multi-fragment device posts (e.g. c490) decode only their first fragment #42

Description

@kb1ibt

Summary

_process_notification's unknown-cmd branch decrypts a single raw BLE packet, whereas telemetry frames go through _process_telemetry_packet, which reassembles fragments first. Any non-telemetry session frame that spans more than one notification is therefore never reassembled — only fragment 1 reaches _decrypt_payload, so it can't decode. The C2000 G2 (A1783) posts exactly such a frame — cmd=c490, ~372 bytes cleartext — every ~9 minutes, and it's currently lost.

Background — what c490 is

While monitoring an Anker SOLIX C2000 G2 (A1783) — which connects on the shared C1000 G2 family profile (CapturingC1000G2) — the device posts an unsolicited frame on its own timer, roughly every 540 s. It's not a request/response we drive.

  • Pattern 03010f, cmd=c490, not in _TELEMETRY_COMMANDS, no future registered → falls into the unknown-message else branch.
  • Decrypts with the same AES-CBC session key SolixBLE already negotiates (_decrypt_payload).
  • 372 bytes of cleartext once reassembled — readable ASCII inside (A1763, charging_pps_series_c_0005), and consecutive frames differ in only a handful of isolated byte positions (i.e. it's plaintext, not still-ciphertext — a CBC change would avalanche).

It carries a useful device-state rollup (displayed SoC, pack voltage, cumulative energy, a runtime estimate). The payload semantics are out of scope for SolixBLE — this issue is purely about the transport not delivering the frame intact.

Root cause

In SolixBLE/device.py::_process_notification, the session-message branch splits into:

  • telemetry (cmd in _TELEMETRY_COMMANDS) → _process_telemetry_packet(payload, cmd), which reassembles fragments (payload[0] high nibble = index, low nibble = total), strips the header byte, then decrypts;
  • unknown (else) → decrypts payload directly.

The unknown branch:

  1. never reassembles — it operates on one BLE packet, so a multi-fragment frame like c490 only ever presents fragment 1; and
  2. doesn't strip the fragment-header byte before decrypt (telemetry does, via payload = payload[1:]), which is what the len(payload) % 16 == 15 realign trick is compensating for on the single-packet path.

So multi-fragment non-telemetry frames can't decode at all, and the branch quietly DEBUG-logs the failure.

Proposed fix

Route unknown session frames through the same fragment path as telemetry:

  • extract the fragment reassembly from _process_telemetry_packet into a small shared helper (index/total from payload[0], buffer by cmd, join in order, strip header);
  • have the unknown branch reassemble → decrypt → _parse_payload, same as telemetry;
  • optionally surface the result via a callback/hook (e.g. an on_unknown_frame(cmd, parameters) or a debug-frames callback) so downstream consumers can opt into non-telemetry frames instead of them only reaching the logger.

This keeps telemetry/negotiation/future-awaited handling untouched.

We have a working implementation

We're already doing exactly this downstream via a SolixBLEDevice subclass that intercepts unknown session frames and reassembles them with your fragment scheme, in production against a live A1783. Happy to open a PR that folds it into the base class (with a multi-fragment reassembly test) — wanted to check first whether you'd prefer the result surfaced via a callback vs. just reassembled-and-logged, and whether extracting a shared _reassemble helper off _process_telemetry_packet fits how you'd like it structured.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions