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:
- never reassembles — it operates on one BLE packet, so a multi-fragment frame like
c490 only ever presents fragment 1; and
- 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.
Summary
_process_notification's unknown-cmdbranch 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
c490isWhile 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.03010f,cmd=c490, not in_TELEMETRY_COMMANDS, no future registered → falls into the unknown-messageelsebranch._decrypt_payload).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: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;else) → decryptspayloaddirectly.The unknown branch:
c490only ever presents fragment 1; andpayload = payload[1:]), which is what thelen(payload) % 16 == 15realign 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:
_process_telemetry_packetinto a small shared helper (index/total frompayload[0], buffer bycmd, join in order, strip header);_parse_payload, same as telemetry;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
SolixBLEDevicesubclass 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_reassemblehelper off_process_telemetry_packetfits how you'd like it structured.