Bring the protocol library and frame_bench up to the merged spec #52

Merged
Claude merged 7 commits from feat/spec-pull-model-update into feat/firmware-updates 2026-09-19 07:54:31 +00:00
Collaborator

Transcribes the pull-model firmware update change from docs/can-protocol.md at fe5309e, sections 8.1, 8.9 and 11. Both transcriptions move together: protocol/catalogue.py and tools/frame_bench/protocol.py.

SYS_WIFI_CONTROL action 3 now carries the update server address in bytes 2-5 and its port in bytes 6-7. Actions 0-2 have nothing to put there and send two bytes, so a field can now say which values of another field put it on the wire, and a sender stops at the shortest DLC that carries what it means. Decoding was already length-driven and is unchanged. BULK_BEGIN gains content type 2, the session credential set held in RAM, and BULK_END becomes addressed because the receiver sends it back to whoever sent the transfer.

tools/frame_bench/examples/send-the-lighting-node-for-its-firmware.json plays the section 11 session as far as the bench can: session credentials, then action 3. The credentials are an exact multiple of six bytes so the example does not have to guess whether a final short BULK_DATA frame is right — the specification does not say.

CCS-UHA-10.

Tested with the existing suite, plus a check in tests/test_bench_agrees_with_the_protocol.py that both sides agree on which fields a frame carries, which is otherwise invisible in the bytes. No shared vectors cover SYS_WIFI_CONTROL or CLASS_BULK yet, so tests/vectors/frames.json is untouched; the wire names updateServerIpv4 and updateServerPort are this repository's guess until the firmware settles them.

Transcribes the pull-model firmware update change from `docs/can-protocol.md` at `fe5309e`, sections 8.1, 8.9 and 11. Both transcriptions move together: `protocol/catalogue.py` and `tools/frame_bench/protocol.py`. `SYS_WIFI_CONTROL` action 3 now carries the update server address in bytes 2-5 and its port in bytes 6-7. Actions 0-2 have nothing to put there and send two bytes, so a field can now say which values of another field put it on the wire, and a sender stops at the shortest DLC that carries what it means. Decoding was already length-driven and is unchanged. `BULK_BEGIN` gains content type 2, the session credential set held in RAM, and `BULK_END` becomes addressed because the receiver sends it back to whoever sent the transfer. `tools/frame_bench/examples/send-the-lighting-node-for-its-firmware.json` plays the section 11 session as far as the bench can: session credentials, then action 3. The credentials are an exact multiple of six bytes so the example does not have to guess whether a final short `BULK_DATA` frame is right — the specification does not say. CCS-UHA-10. Tested with the existing suite, plus a check in `tests/test_bench_agrees_with_the_protocol.py` that both sides agree on which fields a frame carries, which is otherwise invisible in the bytes. No shared vectors cover `SYS_WIFI_CONTROL` or `CLASS_BULK` yet, so `tests/vectors/frames.json` is untouched; the wire names `updateServerIpv4` and `updateServerPort` are this repository's guess until the firmware settles them.
Carry the update server in SYS_WIFI_CONTROL action 3
Some checks failed
Lint, type check and test / hassfest (pull_request) Has been cancelled
Lint, type check and test / release (pull_request) Has been cancelled
Lint, type check and test / quality (pull_request) Has been cancelled
176c2da841
Add the BULK session credential set, content type 2
Some checks failed
Lint, type check and test / quality (pull_request) Has been cancelled
Lint, type check and test / hassfest (pull_request) Has been cancelled
Lint, type check and test / release (pull_request) Has been cancelled
3e7aa42a6d
Address BULK_END to the sender: the receiver sends it
Some checks failed
Lint, type check and test / quality (pull_request) Failing after 49s
Lint, type check and test / hassfest (pull_request) Successful in 39s
Lint, type check and test / release (pull_request) Has been skipped
d7f3ebbc96
Record the update session as a frame bench example
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 12s
Lint, type check and test / release (pull_request) Has been cancelled
Lint, type check and test / quality (pull_request) Has been cancelled
f17a40768b
rob force-pushed feat/spec-pull-model-update from f17a40768b
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 12s
Lint, type check and test / release (pull_request) Has been cancelled
Lint, type check and test / quality (pull_request) Has been cancelled
to 52771ae842
Some checks failed
Lint, type check and test / quality (pull_request) Failing after 1m6s
Lint, type check and test / hassfest (pull_request) Successful in 1m5s
Lint, type check and test / release (pull_request) Has been skipped
2026-09-19 07:34:49 +00:00
Compare
Claude left a comment

One finding, in the bench's carried.

tools/frame_bench/protocol.py:322 matches when_is against the choice name only, so the same frame written two legal ways gives two different outputs with no error:

{"action": 3,     "update_server": "192.168.4.1", "update_server_port": 8080}  ->  030F
{"action": "ota", "update_server": "192.168.4.1", "update_server_port": 8080}  ->  030FC0A804011F90

A number is a legal value for a ChoiceField everywhere else in the bench — _choice_value accepts one, and the form's "Something else, by number" produces one — so a saved document or a CLI call with "action": 3 silently builds a two-byte frame with no update server in it. static/app.js:414 hides the fields for the same reason, so the page agrees with the wrong answer rather than showing it.

Resolve the decider through its choices before the comparison: for a ChoiceField, map the supplied value to its number and compare against the numbers when_is names, so a name and its value behave identically.

Worth a case in tests/test_frame_bench.py alongside test_a_wifi_control_frame_stops_where_the_action_does — the named form is covered, the numeric one is not.

One finding, in the bench's `carried`. `tools/frame_bench/protocol.py:322` matches `when_is` against the choice *name* only, so the same frame written two legal ways gives two different outputs with no error: ``` {"action": 3, "update_server": "192.168.4.1", "update_server_port": 8080} -> 030F {"action": "ota", "update_server": "192.168.4.1", "update_server_port": 8080} -> 030FC0A804011F90 ``` A number is a legal value for a `ChoiceField` everywhere else in the bench — `_choice_value` accepts one, and the form's "Something else, by number" produces one — so a saved document or a CLI call with `"action": 3` silently builds a two-byte frame with no update server in it. `static/app.js:414` hides the fields for the same reason, so the page agrees with the wrong answer rather than showing it. Resolve the decider through its choices before the comparison: for a `ChoiceField`, map the supplied value to its number and compare against the numbers `when_is` names, so a name and its value behave identically. Worth a case in `tests/test_frame_bench.py` alongside `test_a_wifi_control_frame_stops_where_the_action_does` — the named form is covered, the numeric one is not.
Decide a conditional field on the number, not the name
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 9s
Lint, type check and test / quality (pull_request) Failing after 31s
Lint, type check and test / release (pull_request) Has been skipped
856af1587f
Claude left a comment

856af15 fixes it. Nothing blocking left.

One latent trap, not reachable today: static/app.js, carried() does Number(chosen), and Number("") is 0. An empty "other" box therefore reads as the choice numbered 0. Harmless for SYS_WIFI_CONTROL, where 0 is disable and whenIs is ["ota"], but the first conditional field whose trigger value is 0 will show on the page while the server refuses the frame. if (chosen === "") return false; closes it.

856af15 fixes it. Nothing blocking left. One latent trap, not reachable today: `static/app.js`, `carried()` does `Number(chosen)`, and `Number("")` is `0`. An empty "other" box therefore reads as the choice numbered 0. Harmless for `SYS_WIFI_CONTROL`, where 0 is `disable` and `whenIs` is `["ota"]`, but the first conditional field whose trigger value is 0 will show on the page while the server refuses the frame. `if (chosen === "") return false;` closes it.
Read an empty choice box as no choice, not as nought
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 37s
Lint, type check and test / quality (pull_request) Failing after 51s
Lint, type check and test / release (pull_request) Has been skipped
34d078e503
rob force-pushed feat/spec-pull-model-update from 34d078e503
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 37s
Lint, type check and test / quality (pull_request) Failing after 51s
Lint, type check and test / release (pull_request) Has been skipped
to 3f61406325
All checks were successful
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Successful in 1m28s
Lint, type check and test / release (pull_request) Has been skipped
2026-09-19 07:50:53 +00:00
Compare
Claude merged commit f00711385e into feat/firmware-updates 2026-09-19 07:54:31 +00:00
Claude deleted branch feat/spec-pull-model-update 2026-09-19 07:54:32 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rob/CampervanHomeAssistant!52
No description provided.