Repository navigation
feat(firmware): nodes announce their board kind in HEALTH_n and _NODE_STATUS v3 (#344) - #345
Merged
Merged
Conversation
…_STATUS v3 (#344) HEALTH_n byte 5 carries a NodeKind (unknown / panel group / PDU / bridge) set by each node library's role via a required serviceNodeStatus() argument, never from NODE_ID. PanelBridge forwards it in _NODE_STATUS v3 (28 hex), adds its own node-0 row to every roster reply, and pushes a delta when an alive node's kind changes. Every node also filters its own HB_n; a frame on it means another board has the same NODE_ID and raises DUPLICATE_NODE_ID ahead of every FaultSource, clearing 3 s after the last foreign frame. Spec synced: FirmwarePlan 02/04/06/10 + D19, TechSpec, docs.
This was referenced Oct 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #344.
Every board now says what it is, and the host sees every board's kind, the bridge's included. Today a NODE_ID is no signal (PDUs take any ID) and silence proves nothing, so the client could never let a panel group be adopted. This also adds runtime detection of two boards flashed with the same NODE_ID.
What changed
Wire (CAN,
HEALTH_n)NodeHealthPayloadbyte 5 (wasrsvd[0], always 0) is nownodeKind, a new append-onlyNodeKindinNodeStatus.h:00unknown,01panel group,02PDU,03bridge. Bytes 6–7 stay reserved. Astatic_assertpins the struct at 8 bytes.Who sets the kind
CANProtocol::serviceNodeStatus(NodeKind kind, uint16_t rxCount = 0):kindis required, so a node library that forgets it won't compile. Sketches never call the service, so the library is the role:PanelGrouppassesPANEL_GROUP;PduNode(PowerMonitor) passesPDU;PanelBridgepassesBRIDGE.makeNodeHealthPayload(nodeId, kind, dieTempC, fault)packs it.Host (
_NODE_STATUSv3)NODE_STATUS_PROTO_VERSION2 → 3. Each row is 28 hex, withnodeKindappended last, so a v2 client fails loudly.HEALTH_0it last sent (newCANProtocol::lastNodeHealth()), kind03.nodeIdrange is now 0–63._NODE_STATUS_END <count>now counts the node-0 row, so1means the bridge alone.00; the kind delta follows ~1 s later with its first HEALTH. Without it, the kind would wait for the client's next roster poll.00until a node's first HEALTH (same rule asdieTempC80).Duplicate NODE_ID detection
start()adds the node's ownHB_nto its filter, on every node. bxCAN never receives its own frames, so a frame there means another board has this ID.NodeFaultCode::DUPLICATE_NODE_ID(0x09) inHEALTH_n: DEGRADED plusfaultId, and the DiagSerial edge log.aggregateFaults(), so no FaultSource can mask it. Registry order is construction order, which can't guarantee a priority across translation units.DUP_NODE_TIMEOUT_MS) after the last foreign frame. A live duplicate re-raises within one 500 ms heartbeat.startLoopback()the node's own heartbeats come back. The service counts the heartbeats it sent anddrain()discounts that many, so the existing loopback tests don't false-trip.Decisions (signed off 2026-10-16)
0x03= bridge, reported by PanelBridge inHEALTH_0. SimGateway is not on the bus and has no kind._NODE_STATUSdelta on a kind change.DUPLICATE_NODE_IDtakes top priority and auto-clears after 3 s.Recorded as D19 in
FirmwarePlan/00-decisions.md.Spec + docs (same PR)
02-can-protocol.md: HEALTH layout,nodeKind, the duplicate check, the bridge RX note;04-dcs-bios-integration.md: v3 field list, node-0 row, END count, kind delta;06-panelbridge-api.md;10-implementation-plan.md;00-decisions.md.NodeStatus,CANProtocol,PanelBridge(new node-status section),PanelGroup/PanelGroup,PowerMonitor.docs/architecture/can-bus.md,docs/architecture/firmware-tiers.md,docs/firmware/node-id.md(duplicate NODE_ID warning; an ID is not a type), anddocs/firmware/build-flags.md(text, plus line cites re-pointed).tools/docs-drift/check_doc_refs.py: clean. The PlatformIO impact map is up to date.Tests
Tests/CANProtocol:health_payload: kind in byte 5, bytes 6–7 zero,UNKNOWNpacks 0;health_flags: updated to the new signature;test_duplicate_node_id(loopback): own heartbeats never trip the check; extra own-ID HBs raiseDUPLICATE_NODE_IDplus DEGRADED, hold while they flow, and clear about 3 s after they stop;node_status_bus: the summary now printsfault/kind, and it doubles as the two-board duplicate bench.Tests/PanelBridge/node_status, v3 expectations:01in the alive delta);00, then its first HEALTH pushes a02delta;03), and_NODE_STATUS_END 3.Compiled locally (all pass, 73 envs):
Tests/CANProtocol(19),Tests/PanelBridge(14),Tests/PanelGroup(8),Tests/PowerMonitor(5),Tests/DrumDisplay(18),Tests/NodeStatus,Templates/PanelBridge,Templates/PanelGroup,Panels/PDU_Left|Center|Right,Examples/E2E_DCS_Test/{PanelBridge,PanelGroup,PanelGroupInputs}. Not yet run on hardware.Bench (hardware, by the maintainer)
Rig: three dev boards.
_NODE_STATUSon the SimGateway USB port. A duplicate fault drives no LED, by design.Clone chips (CPUTAPID).
Templates/PanelBridgeandExamples/E2E_DCS_Test/PanelGroupkeep theset CPUTAPID 0override commented out. If an upload fails with an unexpected tap ID / "tap not found", uncomment theirupload_flagsblock locally and don't commit it.Tests/CANProtocolalready sets it.Watcher.
_NODE_STATUS/_NODE_STATUS_ENDline for N seconds, with a timestamp.nodeId present flags uptime rxCount esr dieTempC hFlags faultMask faultId nodeKind. The last pair is the kind; the pair before it isfaultId. The expected rows below add spaces between fields for readability; real lines have none.A. Programmer on the PanelBridge board
~/.platformio/penv/bin/pio run -d Firmware/Templates/PanelBridge -t upload00before its first HEALTH.30, and press reset on the bridge while it runs._NODE_STATUS 00 01 …ending…80 00 00 00 00, then_NODE_STATUS_END 1. That is the node-0 row with dieTempC unseen and kind00.03. Run the watcher with5. Expect the node-0 row ending…00 00 00 03with a real dieTempC, then_NODE_STATUS_END 1.B. Move the programmer to the panel-group board (the one swap)
HB_0(node 0) every 50 ms:~/.platformio/penv/bin/pio run -d Firmware/Tests/CANProtocol -e test_dual_bidir_node0 -t upload5. Expect the node-0 row withhFlags02(DEGRADED) andfaultId09(DUPLICATE_NODE_ID), ending…02 00 09 03. The kind stays03.5. The node-0 row is back to…00 00 00 03.~/.platformio/penv/bin/pio run -d Firmware/Examples/E2E_DCS_Test/PanelGroup -t upload20, then reset the panel-group board. Expect two live deltas about 1 s apart:_NODE_STATUS 01 01 …ending…00(alive, kind not seen yet);…01(panel group).5. Expect the node-0 row…03, node 1…01, and_NODE_STATUS_END 2.20, power the panel group off, and power it back on after about 8 s. Expect:_NODE_STATUS 01 00 …ending…01(removed, kind kept);01 01 …already ending…01, and no second kind delta.C. Later
Panels/PDU_Left(NODE_ID 3) and put it on the bus. Watcher with20, then reset the PDU: expect the alive delta03 01 …00, then…02about 1 s later._NODE_STATUS) on node 0.test_duplicate_node_id,test_health_payload) report only on DiagSerial. They are compile-checked here and can run whenever a USB-UART is on a board.Bench results (2026-10-16, firmware dfe06ee)
Rig: the bridge is a clone (
DPIDR 0x2ba01477, needsCPUTAPID 0; flashed through OpenOCD with the override,platformio.iniuntouched). The panel group runsE2E_DCS_Test/PanelGroup. Read through the SimGateway CDC.0001000000000000008000000000+_NODE_STATUS_END 10001000006000000692D000000030001000003000000002E02000903whiletest_dual_bidir_node0sentHB_0; back to…00000003~4.5 s after it stopped (3 s timeout + 1 s HEALTH cadence)00→0101010000280000001E8000000000, then01010000280000001E2402000101…03, node 1…01,_NODE_STATUS_END 2…0100…01on timeout,…0101…01on return, no second kind deltaNot part of this PR's checks:
01(I2C_PERIPHERAL) is expected, since this rig has no OLEDs or mux._\xfe). It predates this PR; since v3 it lands on the node-0 seed row. The client side has been told.READY_ncarries the previous boot's cached HB (from Report connected PanelGroup nodes to the host (CAN discovery → SimGateway → client) #86).Still open: the PDU (when the board arrives), and a duplicate on a node 1–63, which needs a third board.
After merge
The client resync (PIN bump,
npm run sync, v3 parser, adopt whitelist01/02,DUPLICATE_NODE_IDlabel) happens in the SkyHawkClient Cockpit work (SkyHawkClient#63 / #77), per #344.