Serve firmware images at a path a node can accept #76

Merged
Claude merged 2 commits from fix/short-firmware-image-paths into feat/firmware-updates 2026-09-20 09:20:03 +00:00
Collaborator

The manifest advertised the cache tree, 72 characters, and the firmware caps a
manifest path at 63 (maxImagePathLength). A longer path fails that entry as
the manifest is parsed, so the node finds nothing for its own type and abandons
terminally — no node has ever issued the image GET, and the bus only carries
SYS_WIFI_STATUS state 6.

Images are now offered at /firmware/<version>/<environment>.bin, for example
/firmware/2026.09.20/node_lighting.bin, 38 characters. The file on disk keeps
its name and place; only the advertised path changed, and requests still resolve
against a dictionary of advertised paths rather than being joined onto the cache
root.

Two guards for failures that are silent on the node:

  • An image whose path would exceed 63 characters is left out of the manifest
    with a warning, rather than offered and rejected by the node it is for.
  • A manifest over 2048 bytes (maxManifestLength) is warned about. It is served
    anyway: which node to drop would be an arbitrary choice, and the log is the
    only place either fact can surface.

Both warnings fire where the manifest is rendered, not where the offers are
built, so a 404 probe on the LAN cannot repeat them.

Tested by extending tests/test_node_file_server.py, including a test that pins
the advertised path length for the longest environment the node registry can
name against the firmware's 63, and one that shows the whole node table
overflowing the manifest cap.

CCS-UHA-20

The manifest advertised the cache tree, 72 characters, and the firmware caps a manifest `path` at 63 (`maxImagePathLength`). A longer path fails that entry as the manifest is parsed, so the node finds nothing for its own type and abandons terminally — no node has ever issued the image GET, and the bus only carries `SYS_WIFI_STATUS` state 6. Images are now offered at `/firmware/<version>/<environment>.bin`, for example `/firmware/2026.09.20/node_lighting.bin`, 38 characters. The file on disk keeps its name and place; only the advertised path changed, and requests still resolve against a dictionary of advertised paths rather than being joined onto the cache root. Two guards for failures that are silent on the node: - An image whose path would exceed 63 characters is left out of the manifest with a warning, rather than offered and rejected by the node it is for. - A manifest over 2048 bytes (`maxManifestLength`) is warned about. It is served anyway: which node to drop would be an arbitrary choice, and the log is the only place either fact can surface. Both warnings fire where the manifest is rendered, not where the offers are built, so a 404 probe on the LAN cannot repeat them. Tested by extending `tests/test_node_file_server.py`, including a test that pins the advertised path length for the longest environment the node registry can name against the firmware's 63, and one that shows the whole node table overflowing the manifest cap. CCS-UHA-20
Serve firmware images at a path a node can accept
All checks were successful
Lint, type check and test / hassfest (pull_request) Successful in 11s
Lint, type check and test / quality (pull_request) Successful in 4m46s
Lint, type check and test / release (pull_request) Has been skipped
0189074499
Claude left a comment

Three things, none of them in the decode path. The path scheme, the 63 boundary (> is right: the firmware accepts exactly 63) and the 2048 boundary all match firmware_manifest.h, and both guards fail the suite when mutated out.

Three things, none of them in the decode path. The path scheme, the 63 boundary (`>` is right: the firmware accepts exactly 63) and the 2048 boundary all match `firmware_manifest.h`, and both guards fail the suite when mutated out.
README.md Outdated
@ -266,0 +265,4 @@
2048 bytes, and refuses the whole update over either. An image with too long
a path is left out and logged instead of offered. A node finds its own entry
by node type, which is byte 0 of the `SYS_ANNOUNCE` it already sends, and
checks the bytes against the hash before it switches boot slot. Only what the manifest offers can be fetched. There is
Author
Collaborator

Line left unwrapped at 120 characters; the rest of the paragraph wraps at ~78.

Line left unwrapped at 120 characters; the rest of the paragraph wraps at ~78.
@ -62,0 +83,4 @@
One dictionary behind both answers, so nothing is servable that the
manifest did not offer. An image a node could not name is left out
entirely: offering it would fail the whole manifest for every node.
Author
Collaborator

"offering it would fail the whole manifest for every node" is not what the firmware does. parseFirmwareManifest descends to its own 0x.. key and skips every other entry with skipValue(), so a too-long path only fails the entry it is in, and only for that node type. Say "fails that node's own entry". The same wording is in the PR body.

"offering it would fail the whole manifest for every node" is not what the firmware does. `parseFirmwareManifest` descends to its own `0x..` key and skips every other entry with `skipValue()`, so a too-long path only fails the entry it is in, and only for that node type. Say "fails that node's own entry". The same wording is in the PR body.
@ -62,0 +89,4 @@
for image in offered(cache):
path = image_path(image)
if len(path) > MAX_IMAGE_PATH:
_LOGGER.warning(
Author
Collaborator

This warns on every GET, not once per manifest: advertised() runs again for each image request and for each unknown path, so any 404 probe on the LAN repeats it. Worth warning only where the manifest is built.

This warns on every GET, not once per manifest: `advertised()` runs again for each image request and for each unknown path, so any 404 probe on the LAN repeats it. Worth warning only where the manifest is built.
Take the review: warn once, and say what a long path really breaks
All checks were successful
Lint, type check and test / hassfest (pull_request) Successful in 11s
Lint, type check and test / quality (pull_request) Successful in 1m46s
Lint, type check and test / release (pull_request) Has been skipped
7f884b44d4
Claude left a comment

7f884b4 covers all three. Nothing outstanding.

7f884b4 covers all three. Nothing outstanding.
Claude merged commit a9d70dc490 into feat/firmware-updates 2026-09-20 09:20:03 +00:00
Claude deleted branch fix/short-firmware-image-paths 2026-09-20 09:20:03 +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!76
No description provided.