Skip to content

feat(prime): re-arm realtime telemetry with a keepalive loop - #54

Closed
kb1ibt wants to merge 6 commits into
flip-dots:mainfrom
kb1ibt:pr/prime-keepalive
Closed

feat(prime): re-arm realtime telemetry with a keepalive loop#54
kb1ibt wants to merge 6 commits into
flip-dots:mainfrom
kb1ibt:pr/prime-keepalive

Conversation

@kb1ibt

@kb1ibt kb1ibt commented Jul 23, 2026

Copy link
Copy Markdown

What

Prime chargers stream telemetry ~1/s after a 420b realtime trigger, but the realtime window lapses after ~10 s — so the stream stalls unless the client keeps re-arming it. This adds a generic keepalive task to SolixBLEDevice:

  • When a subclass sets _KEEPALIVE_CMD, connect() starts a background loop that re-sends it every _KEEPALIVE_INTERVAL (default 8 s, ~1 s under the device window), and disconnect() cancels it.
  • PrimeDevice sets _KEEPALIVE_CMD = "420b" (bare a10121 payload via _send_command, so it carries the live session timestamp).
  • PrimeChargingStation240w (A91B2, CBC lineage) overrides _keepalive_loop to send 420b as a10121fe0503<ts> through _send_packet (response=True), matching its own _post_connect framing.

Without this, a client that periodically pokes the device for a refresh ends up re-running _post_connect (re-sending one-time confer commands) and adding a second writer to the command characteristic — the device answers, then drops the link. The keepalive loop makes the device stream on a single steady writer and keeps telemetry flowing indefinitely.

Stacked on #48#52

This branch is stacked on the still-open Prime / telemetry PRs (#48#52). Until those land on main, the diff here shows their commits too — the only new commit in this PR is the top one, feat(prime): re-arm realtime telemetry with a keepalive loop (5e31f9d). Please review/merge the stack first; this is marked draft until then.

Hardware validation

  • A2345 (Prime 250 W charger, GCM) — base PrimeDevice 420b keepalive loop; stream sustained indefinitely.
  • A91B2 (Prime 240 W charging station, CBC) — the _keepalive_loop override; validated live across multiple sessions (continuous 4303 stream + 4a0b acks at ~8 s, no drops, no confer re-sends).

⚠️ The 160 W charger and power bank inherit PrimeDevice._KEEPALIVE_CMD but were not hardware-tested — they will now run the base 420b keepalive loop untested. Flagging for maintainer awareness.

🤖 Generated with Claude Code

kb1ibt and others added 6 commits July 20, 2026 13:29
…flip-dots#42)

Fragment reassembly lived inside _process_telemetry_packet and only ran for
_TELEMETRY_COMMANDS, so any other multi-fragment session frame (e.g. the C2000 G2
c490 device-info blob) had only its first fragment decrypted and the rest dropped
(flip-dots#42).

Extract it into a shared _reassemble()/_join_fragments() that runs in
_process_notification ahead of the cipher split, so telemetry and unknown session
frames share one reassembler regardless of the AES variant (GCM vs CBC). Single vs
fragment is decided by the live notification length (ATT_MTU - 3, via the ff09
_FRAME_OVERHEAD) rather than the frag byte, so families that put no frag byte on
singles (the A91B2 station) need no per-device override; a short single keeps a
0x11 frag byte only when it is a valid single marker. Runs start only on index 1
and terminate on the <index><total> count (so an exact multiple of the cap, with
no short tail, still completes); a partial/cold fragment that cannot decrypt is
dropped rather than crashing the notification handler.

Adds tests/test_reassembly.py (single-no-frag, 0x11 single, two-fragment,
exact-multiple-no-tail, cold index!=1) and gives the mock client a realistic
256-byte MTU so the length gate exercises as it does on device.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mware

Replace the fixed-replay negotiation frames with live/dynamic frames -- each
carries the current timestamp (and the stage-5 confer the local timezone), which
newer firmware requires and rejects a stale one -- and add the account
owner_user_id binding that hardened Prime devices need before they arm telemetry
(without it they ack 09 and withhold updates).

This is the shared connection layer that lets the C1000 G2 / C2000 G2 and
hardened Prime chargers complete negotiation and stream, rather than being
dropped mid-handshake (addresses flip-dots#22).

Split out of flip-dots#45 per review; the c490 summary decode, Prime device support,
and docs follow as separate PRs. Based on the reassembly PR (flip-dots#48).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add walk_protobuf (parsing.py) + _protobuf_body to decode the protobuf
device-summary frame (the C2000 G2's c490) into a `.path` field map exposed via
`summary`, rather than mis-reading it as 1-byte-tag TLV (which garbles the varint
tags). Add the C2000 G2 (A1783) device on top of the C1000 G2.

Split out of flip-dots#45 per review; stacked on the negotiation PR. Prime device support
and docs follow as separate PRs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the Anker Prime USB-charger base (prime_usb_charger) carrying the shared
4303/ca00 telemetry decode, and build the A2345 (250W charger) and A91B2 (240W
charging station) devices on it: per-port voltage/current/power/status, total
output, per-port switches, and the station's two-frame-layout / AC-switch
specifics.

Split out of flip-dots#45 per review; stacked on the negotiation + c490 PRs. Docs follow
as a separate PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t tables

Split out of flip-dots#45 per review; stacked on the device PRs. Completes the split
(reassembly, negotiation, c490 summary, Prime devices, docs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prime chargers stream ~1/s after a 420b trigger, but the realtime window lapses after ~10s, so the stream stalls unless the client re-arms it. Add a generic keepalive task to SolixBLEDevice: when a subclass sets _KEEPALIVE_CMD, connect() starts a background loop re-sending it every _KEEPALIVE_INTERVAL (8s) and disconnect() cancels it.

PrimeDevice sets _KEEPALIVE_CMD=420b (default a10121 payload via _send_command). PrimeChargingStation240w (A91B2, CBC) overrides _keepalive_loop to send 420b as a10121fe0503<ts> through _send_packet (response=True), matching its _post_connect framing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kb1ibt
kb1ibt marked this pull request as ready for review July 25, 2026 05:49
@kb1ibt

kb1ibt commented Jul 26, 2026

Copy link
Copy Markdown
Author

Folding this into #51 instead — the keepalive is what makes that PR's live telemetry actually keep streaming, so it belongs with the device classes rather than as a separate PR on top. Both commits (the 420b keepalive machinery and a follow-up fixing the station's hardcoded confer timezone) are now in pr/prime, and #52 is rebased on top. Closing; no work is lost.

@kb1ibt kb1ibt closed this Jul 26, 2026
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