Repository navigation
Nodes announce their board kind in HEALTH_n and _NODE_STATUS v3 #344
Description
Why
The client can't tell what kind of board a NODE_ID is. Today it has two weak signals:
- NODE_IDs. These don't work: PDUs can take additional IDs or entirely different ones, so the ID says nothing about the board.
- PDU telemetry (
_NODE_TELEM). This proves a node is a PDU. But a panel group never sends any, so "no telemetry yet" is not proof of anything.
So the client's Adopt dialog (SkyHawkClient#77, PR SkyHawkClient#87) blocks adopting a new node until its kind is known. Until this ticket lands, only PDUs can be adopted. A new panel group shows up but stays locked, which blocks the rest of the client's v0.2.0 Cockpit work.
What
Every board announces its own board kind in its HEALTH_n frame, and the host sees every board's kind in _NODE_STATUS. That includes the bridge board itself (decided 2026-10-16: all boards report, for consistency).
-
Wire (CAN):
NodeHealthPayloadbyte 5 (todayrsvd[0], sent as 0) becomesnodeKind, a new append-onlyNodeKindenum inNodeStatus.h. Suggested values:0x00unknown: old firmware, which already sends 0, so it is backward-safe;0x01panel group;0x02PDU;0x03bridge: the SimGateway + PanelBridge board. It is one board with two MCUs (RP2040 SimGateway on USB, STM32 PanelBridge on CAN). The PanelBridge MCU reports it. The SimGateway is not on the bus and has no kind of its own;- append more as new node types appear.
The remaining bytes 6–7 stay reserved.
-
Who sets it: the board's own firmware, not the ID. For example,
CANProtocol::serviceNodeStatus()takes the kind from the node's role: a PanelGroup sketch → panel group, the PDU / PowerMonitor sketch → PDU. One default per library/role, so a sketch can't forget it, with an override only where a role is ambiguous. -
The bridge reports itself:
_NODE_STATUStoday covers nodes 1–63 only. Add a row for node 0, the bridge's own heartbeat state and its ownHEALTH_0, with kind03, in every roster reply. The spec's "nodeId 1–63" rule changes to 0–63. -
The bridge stays on node 0. Node 0 is the bus master's address: every node watches
HB_0for master loss. That doesn't change. A second bridge has no use case and is out of scope. -
Duplicate NODE_ID detection (added 2026-10-16): two boards flashed with the same NODE_ID today show as one blended node. Uptime and rxCount jump between the two boards, health and faults come from whichever spoke last, and collisions on identical CAN IDs drive error frames, error-passive and possibly bus-off. Inputs mostly still work, so it hides as a "flaky board". Nothing detects it today.
- A node never receives its own transmissions (no loopback). So each node adds its OWN
canIdHb(NODE_ID)to its RX filter list. If a frame arrives on it, another board has its ID. - On that, it raises a new append-only
NodeFaultCodeDUPLICATE_NODE_IDthrough the existing FaultSource /aggregateFaults()path: DEGRADED + faultId in HEALTH_n, logged on DiagSerial. Choose its priority against other faults, so a duplicate isn't masked by a lesser fault. - Latching vs clearing (e.g. clear after N s without a foreign HB): decide in the plan.
- The PanelBridge (node 0) gets the same check for free, but don't build anything bridge-specific.
- A node never receives its own transmissions (no loopback). So each node adds its OWN
-
Host (
_NODE_STATUS): add the field to the cached HEALTH fields, giving 28 hex chars. BumpNODE_STATUS_PROTO_VERSION2 → 3, so a client on v2 fails loudly instead of misreading. -
Not before HEALTH is seen: until the node's first HEALTH frame arrives, the kind reads
00, the same rule asdieTempC = 80.
Done when
-
NodeKindenum +NodeHealthPayload.nodeKind(byte 5);makeNodeHealthPayloadfills it; every production sketch reports the right kind - PanelBridge caches it and forwards it in
_NODE_STATUSv3 (28 hex), including its own node-0 row (kind03). The_NODE_STATUS_ENDcount rule is updated to match. - Unit tests: payload packing,
_NODE_STATUSv3 encoding (kind present /00before the first HEALTH frame) - Duplicate NODE_ID: the own HB ID is in the RX filter; a received own-ID HB raises
DUPLICATE_NODE_ID(faultId + DEGRADED in HEALTH). Unit-tested; bench = two boards flashed with the same NODE_ID, then the fault shows in_NODE_STATUS. Benched on node 0 (two boards); a duplicate between two nodes in 1–63 needs a third board, so it moved to the Firmware v0.2.0 QA list. - Spec synced in the same PR:
Firmware/ScratchPad/FirmwarePlan/02-can-protocol.md(HEALTH layout),04-dcs-bios-integration.md(v3 field list), TechSpec,docs/firmware/+docs/architecture/;tools/docs-drift/check_doc_refs.pyclean - Bench: PanelBridge + one panel group (+ the PDU once it arrives, week of 2026-10-19) report the right kind on the host. Bridge + panel group PASSED 2026-10-16; the PDU check moved to the Firmware v0.2.0 QA list.
- Client resync, after this merges. It is done in the SkyHawkClient Cockpit work (SkyHawkClient#63 / docs: weekly docs-drift review playbook + PR-time reminder #77), not a separate ticket:
- bump
PIN.commitinscripts/sync-a4ec.tsto the merge commit; - run
npm run sync, which regenerates theNodeStatus.hcontract (NodeKindenum,NODE_STATUS_PROTO_VERSION3) and must pass the CI freshness gate; - update the
_NODE_STATUSparser to v3 (28 hex), so a v2 bridge fails loudly; - use
nodeKindas the top source in the kind resolver, so Adopt unlocks for panel groups.
- bump
- Bench after the resync: PanelBridge on v3 + the client on the new pin. The panel group reads "panel group" and can be adopted. A v2 bridge with the new client gives the proto-mismatch error, not a misread.
Outcome
Merged in PR #345 (d29433ab), together with the review follow-ups 814656e0 (node_telem expectation) and ca1691a3 (TechSpec wording). Decisions are recorded as D19 in FirmwarePlan/00-decisions.md:
serviceNodeStatus(NodeKind kind, …):kindis required and passed by each node library.- A
_NODE_STATUSdelta is pushed when an alive node's kind changes. - A cached kind is kept when a node revives.
DUPLICATE_NODE_ID(0x09) is checked beforeaggregateFaults()and auto-clears 3 s after the last foreign frame.
Bench, 2026-10-16. Rig: a clone PanelBridge (needs CPUTAPID 0) and E2E_DCS_Test/PanelGroup, read through the SimGateway CDC. Results:
- boot seed: node-0 row
80/00,END 1; - bridge row kind
03; - the panel group's first sighting reads kind
00, then a kind01delta; - roster
END 2; - kind kept across a removal and revival;
- duplicate on node 0:
02/09while a second board sentHB_0, cleared about 4.5 s after it stopped.
Full table in the PR description.
Observations, outside this ticket's scope:
- The first line after a bridge reset can carry UART junk (
_\xfe). This predates the ticket, and the client has been told. - An alive delta raised by
READY_ncarries the previous boot's cached HB (from Report connected PanelGroup nodes to the host (CAN discovery → SimGateway → client) #86).
Remaining items (PDU kind, a duplicate on nodes 1–63, the DiagSerial-only tests) are on the Firmware v0.2.0 QA list. The client resync and its bench belong to SkyHawkClient#63.
Notes
- Client rule: a new node is adoptable only if its kind is
01panel group or02PDU. Anything else (00unknown,03bridge, future values) can't be adopted; Ignore stays available. The node-0 row is the client's own link, not a "new" node. - The client gets the
DUPLICATE_NODE_IDcode through the resync (npm run sync) and adds a fault-table label, e.g. "Duplicate NODE_ID: another board uses this ID". - Kind order in the client, as decided 2026-10-16: announced
nodeKind> PDU telemetry > stored/registry kind; otherwise unknown. Never inferred from the NODE_ID or from silence. - Old nodes send
00and stay "unknown" in the client, so they can't be adopted until reflashed. That is intended.
Activity
test(firmware): node_telem expects the node-0 row in the roster reply (…
docs(firmware): finish the duplicate-NODE_ID test descriptions in the…
feat(firmware): nodes announce their board kind in HEALTH_n and _NODE…