Repository navigation
feat: node data layer — PDU telemetry, node mapping, new-node detection #61
Description
Summary
Firmware is adding the PDU monitoring node (OpenSkyHawk/OpenSkyhawk#216, Firmware v0.2.0): one per console, measuring the 12 V and 5 V rails (voltage, current) plus board temperature. Its data travels on a new CAN frame family, TELEM_n, which PanelBridge forwards to this client as a new DCS-BIOS message, _NODE_TELEM. The client needs to tap, decode and display it.
_NODE_STATUS is unchanged (stays proto v2). PDU nodes already appear in the Connected Nodes roster through it — up/down, die temp, and the degraded badge when a rail faults — with no client change.
Wire format (as designed in #216 — final layout lands with the firmware PR)
_NODE_TELEM <hex> 18 hex chars: nodeId(2) + the 8 TELEM frame bytes (16), in wire order
- Pushed by PanelBridge as each frame arrives — no request, no cache, no end marker. Bridge mode only (same path as
_NODE_STATUS). - Byte 0 of the frame is a page id (
TelemPage, global and append-only;0x00invalid). The bridge never decodes pages, so the client owns the decode. Multi-byte fields are little-endian (note: unlike_NODE_STATUS, these are raw frame bytes, not MSB-first fields). - Rate: one message per second per PDU (same as HEALTH_n), rotating through the three pages — each page refreshes every 3 s — plus an immediate send when any flag changes (≤ 1 per 100 ms per page). ~3 PDUs → ~3 messages/s steady.
| Page | Byte 1 | Bytes 2–3 | Bytes 4–5 | Bytes 6–7 |
|---|---|---|---|---|
0x01 PDU_RAIL_12V / 0x02 PDU_RAIL_5V |
railFlags |
vAvgMv u16 |
vMinMv u16 (window min) |
iAvgMa int16 |
0x03 PDU_STATUS |
statusFlags |
ntcTempDC int16, 0.1 °C (0x8000 = unavailable) |
i12PeakMa u16 |
i5PeakMa u16 |
railFlags: bit0 UNDER_V · bit1 OVER_V · bit2 OVER_CURRENT · bit3 FUSE_OPEN · bit4 SENSOR_FAULTstatusFlags: bit0 UNCAL_VDDA · bit1 UNCAL_ZERO · bit2 TEMP_WARN · bit3 TEMP_CRIT · bit4 NTC_FAULT- All flags fault-sense: all-zero = healthy.
iAvgMais signed — a small negative value is residual sensor offset; clamp to 0 for display.- Averages / min / peak cover the 3 s since that page's previous periodic send. Peak current is after the board's ~100 µs RC filter.
ntcTempDCis a real board temperature (±0.5 °C class), unlike the uncalibrated die temp in_NODE_STATUS.
Contract source: NodeStatus.h gains NODE_TELEM_MSG_NAME, NODE_TELEM_PROTO_VERSION (1), and the TelemPage / PduRailFlag / PduStatusFlag enums, so sync-a4ec can generate them alongside the node-status contract. It also gains new NodeFaultCode values 0x06 FUSE_OPEN, 0x07 OVER_TEMPERATURE, 0x08 SENSOR_FAULT — they render as Fault N until the sync is re-pointed (the decoder already passes unknown ids through).
Today's behaviour without this change
relayFromDevice (src/main/session.ts:369) taps only _NODE_STATUS / _NODE_STATUS_END. _NODE_TELEM lines fall through to stats.command + the command log, so the log fills with ~3 lines/s of telemetry noise. They are also relayed to DCS:7778 like every device line; DCS ignores the unknown identifier (same as _NODE_STATUS). Nothing breaks.
Changes
PDU telemetry (existing scope, minus UI)
-
scripts/sync-a4ec.ts— re-pointPINto22b6a87e(feat(firmware): PDU monitoring node and TELEM_n telemetry frame (#216) OpenSkyhawk#343, which closes #216); emitNODE_TELEMconstants +TelemPage/PduRailFlag/PduStatusFlag; regen — also bringsNodeFaultCode0x06 FUSE_OPEN / 0x07 OVER_TEMPERATURE / 0x08 SENSOR_FAULT (today they renderFault N) -
reference.test.ts— assertNODE_TELEM.protoVersion== newSUPPORTED_TELEM_PROTO -
src/shared/— pureparseNodeTelem(hex)+ latest-value store keyed(nodeId, page)with last-seen; fixtures for all 3 pages, signed current,0x8000temp, malformed, unknown page -
src/main/session.ts— tap_NODE_TELEM(not logged as a command); throttlednodes:telemetryevent - A node that sends PDU pages is a PDU; stale telemetry (> 10 s) reported as unknown
Node mapping (new)
-
<userData>/node-mapping.json— per NODE_ID: display name, console, power zone (PDU); tolerant read / best-effort write likesettings.ts - Defaults from the bundled NODE_IDS (
name,console; PDU = the PDU on that console); only differences are stored - Reset (all) to registry defaults; count of nodes that differ
- Ignored NODE_IDs list (permanent, with un-ignore)
New-node detection (new)
- A NODE_ID that is in the roster but not in NODE_IDS and not mapped/ignored → "new"; kind = PDU if it sends PDU pages, else panel group; first-seen time
- Registry nodes are placed automatically (never "new")
Status rules (new, pure + unit-tested)
- A registry node absent from the first complete roster after the link comes up → offline (no separate "waiting" state)
- "Hidden until it reports again" flag in
node-mapping.json(replaced / removed boards): such nodes are skipped by the rule above and the flag clears when the node reports - PDU-offline grouping: PDU offline + more than half of its mapped nodes going offline within ±2 s (or found late by the next roster poll, ≤ 7 s after the PDU) → one group event (feeds the Cockpit's combined Attention item and the fuse hint)
- PDU with status but no telemetry (never or > 10 s) → "no readings" (separate from status colour);
everHadReadingsflag for the "PanelBridge firmware may be too old" hint
Decisions (settled during implementation, PR #71)
- PDUs in the registry are recognised by the
PDU_name prefix (NODE_IDS.md has no type column). A panel's default power zone is the PDU on its console when there is exactly one. - Grouping window: 2 s. Nodes that lose power together stop heartbeating together, so the bridge's 3 s timeout fires within one 500 ms heartbeat period and each drop arrives as a live delta. A drop whose delta was lost is caught by the 5 s roster poll, so it can count up to 7 s after the PDU's. A PDU and its nodes all missing from the first roster also group.
- First-seen for new nodes is in memory only, so a node nobody acts on stays new/pending. It is saved to
node-mapping.jsononce adopted. - Reset all clears registry overrides only, including those of an ignored registry node (the "differ" count matches). Adopted nodes, the ignore list and hidden flags stay.
- Adopt requires a name and a console. A power zone must be a registry or adopted PDU that is not ignored; a PDU has none. Checked on set; loading stays tolerant.
- Hide until it reports again applies only to registry and adopted nodes (Ignore is the action for a new id). It clears on an offline→online transition, not while the bridge still lists the board.
node-mapping.jsonis written atomically. A failed save returns{ ok: false }and is not applied. A damaged file is moved aside before starting empty. An unreadable file, or one from a newer client, is loaded read-only.- Readings need a fresh 12 V, 5 V or status page; an unknown page type never counts. A status counts as a roster reply only within 1 s of the request, so a bridge that never sends
_NODE_STATUS_ENDcannot skew grouping; registry nodes then stayunknown(rosterComplete: false). - Stale pages (> 10 s) keep their last values with
stale: true, so a reading can be shown as last known or unknown. - Fault badge tags are unique. The new
OVER_TEMPERATURE(0x07) would have taggedOVER, the same asOVER_VOLTAGE. Tags that share a first token now take the next token's initial (OVER-V/OVER-T), and the sync fails if two still collide.
IPC boundary (for #63)
#61 owns the main↔renderer IPC and preload. #63 is UI only, including the renderer store. Pushes are not gated on relaying (device-link independence comes from #69).
-
push
nodes:telemetry:NodeTelemSnapshot[], at most 1/s, on change or a stale transition. -
push
nodes:model:NodeModelwithnodes(effective name / console / power zone, kind, source registry/adopted/new, availability online/offline/unknown, offline timing, last status, first-seen, readings + everHadReadings),outages,differingCount,ignored,hiddenandrosterComplete. -
invoke
getNodes()returns{ model, telemetry }. -
invoke
setNodeMapping(id, patch),adoptNode(id, spec),forgetNode(id),resetNodeMapping(),ignoreNode(id, on)andhideNode(id, on). Each returns{ ok, model }or{ ok: false, error }. A real change also pushesnodes:model; a no-op writes and pushes nothing. Malformed input resolves to{ ok: false }. -
Connection status lives in one place, owned by feat: decouple the device link from the DCS-BIOS relay #69 (
device:status/isLinkUp()); feat: Cockpit tab — cockpit health & status #63 subscribes to it. feat: node data layer — PDU telemetry, node mapping, new-node detection #61 adds no link field. When feat: decouple the device link from the DCS-BIOS relay #69 lands:- the roster and node reset move to link open, together;
- the
_NODE_TELEMtap and the node flush move to the device link; - node pushes keep flowing while relaying is stopped;
- on link down the pushes stop and the renderer keeps the last model, dimmed. There is no separate "stopped" signal.
Until feat: decouple the device link from the DCS-BIOS relay #69,
Session.stop()stops the pushes and reports no-device, which reads the same. (Review findings 6 and 17 on PR feat(nodes): node data layer — PDU telemetry, node mapping, new-node detection #71, handed to feat: decouple the device link from the DCS-BIOS relay #69.)
Out of scope
UI — the NodesCard PDU rows / alerts originally listed here move to the Cockpit tab (#63). No stop-gap readout.
Related
- feat: Cockpit tab — cockpit health & status #63 Cockpit tab · feat: event log — node + client events, Log Events view #64 node event log · feat: Overview — Cockpit tile and attention banner #65 Overview · feat: Connection — nodes summary card #66 Connection · feat: Settings — sections, Cockpit options, export/import configuration #67 Settings · chore(design): keep the client design artifact current #68 design source
- Design: https://claude.ai/artifact/R8DXWWC4uyAby54tBQLo8v
Depends on: OpenSkyHawk/OpenSkyhawk#216 (merged via #343, 22b6a87e).
Activity
[-]feat: PDU rail telemetry (_NODE_TELEM) — decode and per-rail display[/-][+]feat: node data layer — PDU telemetry, node mapping, new-node detection[/+]feat(nodes): node data layer — PDU telemetry, node mapping, new-node …
feat(cockpit): add the Cockpit tab with health band, plan and Attention
feat(settings): sections, Cockpit options and About (#78)
feat(cockpit): add the Cockpit tab with health band, plan and Attenti…