Skip to content

feat(gen2): add the C2000 G2 (A1783) with the c490 device-summary decode - #72

Draft
kb1ibt wants to merge 10 commits into
flip-dots:mainfrom
kb1ibt:pr/c2000g2
Draft

kb1ibt wants to merge 10 commits into
flip-dots:mainfrom
kb1ibt:pr/c2000g2

Conversation

@kb1ibt

@kb1ibt kb1ibt commented Sep 20, 2026

Copy link
Copy Markdown

Replaces #50 and the C2000 G2 part of #51, rebuilt on Harvey's review with the existing names.

Five commits (the fourth widens the display timeout set to the six values the app offers and adds DisplayTimeout.S10; the fifth turns the command payloads into PARAMETERS_* constants):

  1. Fix the Gen 2 and MagGo subscribe commands – _post_connect on the C1000 G2 and the MagGo 3-in-1 still called _send_command with the pre-parameters signature, so their subscribe was never sent. The C1000 G2 hunk is byte-for-byte the one in Fix C1000 Gen 2 subscribe using the new send_command interface #67; this PR adds the same fix for the MagGo and a command test. If Fix C1000 Gen 2 subscribe using the new send_command interface #67 merges first this commit rebases away.
  2. Decode protobuf device-summary posts against schema field maps – SolixBLE/parsing.py walks a protobuf blob to a .path map (packed integer arrays declared per schema), a per-schema field map names and scales the values, and the base class exposes summary, summary_schema, last_summary_update for commands listed in _PROTOBUF_TELEMETRY_COMMANDS. Unknown schemas are kept by path and logged once.
  3. Add the C2000 G2 (A1783) – on the C1000 G2's framing and AC/DC control; negotiates on the encrypted path; _post_connect subscribes (4100) and arms the realtime latch (4057) once per connection; decodes the 4103 system group, status and time-remaining fields, the version block and the BP2000 expansion block; status is polled with get_status_update() like the other power stations (no keep-alive polling).

Names and interfaces follow the existing devices per the review: ac_timer / ac_timer_remaining / set_ac_timer(seconds) and the DC equivalents (the raw fields are live countdowns, confirmed against the app log), ac_charging_power / set_ac_charging_power(watts), power_saving_mode_enabled, device_timeout, is_display_on, display_mode (LightStatus) / set_display_mode(LightStatus), set_display_timeout(DisplayTimeout), software_version_controller, num_expansion. disable_realtime_telemetry, expansion_present, battery_percentage_a6_9 and usb_total_power_out-style aggregates are gone; the charge-cap setters carry a .. warning::; parameters are module constants; commands come after properties.

The c490 device summary the unit posts about every nine minutes is decoded through field maps for its two current schema revisions (charging_pps_series_c_0005 / _0009); the energy ledgers and DC input voltage it carries are properties that read None until a post arrives, since the post is gated by a schema map the device only holds after reaching the vendor cloud.

Docs: c2000g2.rst carries the device note; the index table has a C2000 G2 column with ticks and crosses only; the command test harness now checks every packet a command sends, in order.

Validated on hardware: C2000 G2 (A1783) with and without a BP2000. The C1000 G2 and MagGo subscribe fix is by inspection of the wire bytes (identical to before the regression).

Suite: 407 passed.

🤖 Generated with Claude Code

kb1ibt and others added 10 commits September 13, 2026 14:30
The Anker manufacturer record (company id 0xffff) carries the device MAC,
model, and a capability byte declaring which negotiation path the device
accepts -- readable at scan time, before any frame is sent. Add a parser
for it as the basis for choosing the cleartext vs encrypted handshake per
device rather than by product class.

Capability is length-relative (last byte when present, absent on the F3800),
so it is derived from the sku length rather than read at a fixed offset.
Decoded against the app's own field values for five bench records.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Move the AES-GCM negotiation out of PrimeDevice into SolixBLEDevice so any
device can take it. The path is selected from the capability byte in the
device's advertisement when the caller passes one, otherwise from the class
default (encrypted for Prime devices, plain text for the Solix power
stations). The device's declared MTU and auth mode from 4803 are echoed in
4005, and the timezone confer carries the local UTC offset.

A fresh P-256 key pair is generated for every negotiation instead of the
hard-coded key pairs; the keys the recorded test vectors were captured with
move to the test constants and are pinned by the fake_time fixture. The
cipher and ECDH primitives live in utilities as plain functions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The client registration on the encrypted path now carries a client token
(the class identifier unless the caller passes one) and the link only counts
as negotiated once the device accepts it. Firmware that pairs clients
answers a new token with status 9 and waits for its button: the device
reports pairing_required, runs the registered pairing callbacks so a user
can be prompted, and connect() keeps waiting instead of restarting the
handshake. The grant the device then pushes on pattern 030101 authorizes
the link. The device remembers the token, so later connections with the
same token are accepted without a press.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
_post_connect on the C1000 G2 and the MagGo 3-in-1 still called
_send_command with the pre-parameters signature, so the subscribe that
starts their telemetry was never sent. The raw payload a10121 is parameter
a1 with value 21, so the bytes on the wire are unchanged. Adds a command
test for the C1000 G2 subscribe.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Some devices post a second live frame beside their telemetry stream: a
protobuf message built against a schema the device names in the frame.
The walker flattens it to a .path map, with packed integer arrays declared
per schema so they are not mistaken for sub-messages, and a per-schema
field map names and scales the values. A frame naming an unknown schema is
kept by path and logged once. The summary, its schema name and the time of
the last post are exposed on the device, and state-change callbacks run
when one arrives.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds the C2000(X) Gen 2 power station on top of the C1000 G2, which shares
its telemetry framing and AC/DC control. It negotiates on the encrypted
path, which its current firmware requires, subscribes and arms the
realtime latch on connect, and decodes the 4103 system group (display,
timeouts, SoC limits), the status and time-remaining fields, the version
block, and the BP2000 expansion block. Status is polled with
get_status_update() like the other power stations.

The c490 device summary the unit posts about every nine minutes is decoded
through field maps for its two current schema revisions, and the energy
ledgers and DC input voltage it carries are exposed as properties that
read None until a post arrives; the summary is gated by a schema map the
device only holds after reaching the vendor cloud, so a unit kept off the
network may never post one.

The command test harness now checks every packet a command sends, so a
setup routine that sends two can be asserted.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The accepted set was the three values that happened to appear in the
captures. The app's picker offers 10 s, 20 s, 30 s, 1 min, 5 min and
30 min, so the device now accepts all six and DisplayTimeout gains S10
for the shortest one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Each group-command setting gets a PARAMETERS_* constant with a lambda for
its value, like the other devices, instead of a helper that built the
dictionary at call time.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

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