Skip to content

feat(firmware): PDU monitoring node — telemetry firmware + new CAN telemetry packet class #216

Description

@mottihoresh

2026-10-15: firmware implemented in PR #343 — everything here is done or moved (PDU bench → #202, client → SkyHawkClient#61); closes on merge. Originally: — PowerMonitor tests 106/106 PASS on hardware, real-bus smoke PASS (non-PDU board + CANable2), all 37 projects compile. Remaining: bench on the PDU boards (below), test_node_telem run (needs the UART2 adapter), client SkyHawkClient#61. Change from the design: SYNC_REQ sends all three pages, not just the next.

Split from #202 (hardware). Firmware / software for the PDU monitoring node. Design signed off 2026-10-15 (below). Depends on #340 (node heartbeat / HEALTH / master watchdog moved into CANProtocol) — that PR lands first, this one builds on it, not stacked. Implementation is otherwise compile-only until the Rev 0.2.0 boards arrive (JLC W2026093021050394 + LCSC WM2610010130, ~Oct 16–22), then bench.

Node role

Per-console Power Distribution + Monitoring CAN node (STM32F103C8), one per console (Left · Right · Center). Reads rail V/I + board temp, publishes telemetry + fault state over CAN → PanelBridge → SkyHawkClient. Built on STM32Board + CANProtocol only (no PanelGroup dependency — the shared node plumbing comes from CANProtocol after #340); adds its own NODE_ID and a new telemetry frame family. Rev 0.2.0 is the first PDU revision to be assembled (0.1.0 never will be); its first job is measuring the real cockpit load so the final fuse and shunt sizing is data-driven.

Hardware inputs — verified against the Rev 0.2.0 schematic netlist (2026-10-15)

Signal Pin Front end
12 V rail voltage PA0 33k / 10k divider (0.1 %) → 1k → 100 nF
5 V rail voltage PA1 10k / 13k divider (0.1 %) → 1k → 100 nF
12 V current PA2 10 mΩ shunt → INA180A2 (gain 50, FS 6.6 A) → 1k → 100 nF
5 V current PA3 10 mΩ shunt → INA180A2 → 1k → 100 nF
Board temp PA6 10k from +3V3, NTC B3450 ±1 % to GND → 1k → 100 nF
Internal IN16 / IN17 die temp (already in HEALTH_n) / Vrefint
  • Fuses F1 5 A (12 V) / F2 2 A (5 V). AMS1117 is fed from the fused +5 V, so a blown 5 V fuse takes the node off the bus — the client sees it as "node offline".
  • The INA180 output RC is 1k / 100 nF (τ = 100 µs, ~1.6 kHz) — the anti-alias filter for 1 kHz sampling; peak-current fields mean "peak after this filter".
  • The NTC bias runs from the same +3V3 that feeds VDDA through FB1, so temperature is ratiometric — no VDDA correction needed.

What exists already (checked against main, 2026-10-08)

  • HEALTH_n (0x140+n, NodeHealthPayload) is on main — die temp, OVERHEAT/DEGRADED flags, faultId, sent every 1 s by every node, forwarded in _NODE_STATUS proto v2. This is the generic all-node health frame, so the PDU layers rail data on top of it instead of duplicating it.
  • The fault path exists (D14): a FaultSource reports a NodeFaultCode; aggregateFaults() fills HEALTH_n.faultId + DEGRADED. OVER_VOLTAGE / UNDER_VOLTAGE / SHORT_CIRCUIT are already defined.
  • 0x700+n (proposed in the original body) is taken by EVT_ACTION_n (feat(firmware): ActionButton — momentary push-button input #116). Free per-node blocks: 0x180, 0x1C0, and the +0x40 halves of 0x2xx–0x7xx.
  • SkyHawkClient polls the roster every 5 s (NODES_REFRESH_MS); alive/dead is pushed immediately. Node rows show up/down, CAN TEC/REC, uptime, die temp, and an amber dot + fault label when DEGRADED. A PDU shows up there with no client change; a rail fault reaches the roster within ≤ 5 s. Unknown fault ids render as Fault N.

Design (signed off)

1. Frame family — TELEM_n = canIdTelem(n) = 0x180 + n

  • Sits with HB (0x100) and HEALTH (0x140) in the node-status block; 0x1C0+n stays free.
  • Outranks EVT_n in arbitration, same as HEALTH. At ≤ 3 frames/s per PDU the worst added latency to an input event is one frame (~0.27 ms).
  • One ID with a page byte, not several fixed IDs: fixed IDs would cost a 64-slot block each, and with a page byte PanelBridge forwards frames opaquely, so future page types need no bridge change.

2. Payload — byte 0 = TelemPage (global, append-only, 0x00 invalid), little-endian

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 (INT16_MIN = unavailable) i12PeakMa u16 i5PeakMa u16
  • railFlags: bit0 UNDER_V · bit1 OVER_V · bit2 OVER_CURRENT · bit3 FUSE_OPEN · bit4 SENSOR_FAULT.
  • statusFlags (fault-sense, all-zero = healthy, like HEALTH): bit0 UNCAL_VDDA · bit1 UNCAL_ZERO · bit2 TEMP_WARN · bit3 TEMP_CRIT · bit4 NTC_FAULT.
  • iAvgMa is signed so residual zero-offset stays visible; the client clamps for display.
  • Die temp is not repeated (it lives in HEALTH_n). The original "CAN-fail" bit is dropped: HB_n already carries BOFF/EPVF + TEC/REC, and a node cannot report its own CAN failure over CAN.

3. Units — engineering units on the node

mV / mA / 0.1 °C. The node owns its per-board cal; PanelBridge forwards bytes untouched; the client needs no per-board knowledge. Raw ADC counts on DiagSerial only (bring-up / cal).

4. Faults into HEALTH_n

RailMonitor ×2 + BoardThermal each implement FaultSource. OV → OVER_VOLTAGE, UV → UNDER_VOLTAGE, OC → SHORT_CIRCUIT; new NodeFaultCode 0x06 FUSE_OPEN, 0x07 OVER_TEMPERATURE (TEMP_CRIT only), 0x08 SENSOR_FAULT. UNCAL_* and TEMP_WARN are telemetry flags only, not faults.

5. Thresholds — compile-time PDU_* build flags, defaults tuned on the bench

Condition 12 V 5 V
Under-voltage 11.40 V 4.75 V
Over-voltage 13.2 V 5.5 V (below the TVS standoff)
Over-current (sustained 100 ms) 4.5 A 1.8 A (90 % of fuse)
Fuse open < 1.0 V — (node loses power)

NTC warn 60 °C / critical 75 °C; NTC fault when outside 2–98 % of full scale. Voltage faults: 50 ms persistence to set, 500 ms good to clear, 100 mV hysteresis.

6. Rates

  • ADC scan (5 external + Vrefint) in loop(), target 1 kHz — measure on the bench, fall back to 500 Hz. Coexists with STM32Board::readDieTempC() on the same ADC/thread.
  • Periodic: one TELEM frame per second per PDU — the same rate as HEALTH_n — rotating through the three pages, so each page refreshes every 3 s. Each page's avg / min V / peak I cover the 3 s since that page was last sent periodically. (Reduced from 3 frames/s 2026-10-15 to keep _NODE_TELEM traffic to DCS low.)
  • On change: any flag edge (set or clear) sends that page immediately, ≤ 1 per 100 ms per page; an immediate send does not reset the window. HEALTH_n stays at 1 Hz, unchanged.

7. Bus load @ 500 kbit/s (8-byte frame ≈ 130 bits worst-case stuffing ≈ 0.27 ms)

Traffic Frames/s Load
1 PDU, steady 1 0.03 %
3 PDUs, steady 3 0.08 %
3 PDUs, fault storm (rate-limit ceiling) 90 2.4 %
Existing HB + HEALTH, 20 nodes (reference) 60 1.6 %

8. PanelBridge routing

New branch for canIdTelem(1..63) under the existing PANELBRIDGE_NODE_STATUS flag → emit _NODE_TELEM <nodeId(2 hex)><8 frame bytes as 16 hex> immediately: no cache, no decode. New NODE_TELEM_MSG_NAME + a separate NODE_TELEM_PROTO_VERSION 1 in NodeStatus.h; _NODE_STATUS stays v2 (new message, not a change to the old one). SimGateway passes it through unchanged. Client side → OpenSkyHawk/SkyHawkClient#61.

9. Calibration

  • VDDA (decided): uncalibrated = assume 3300 mV + UNCAL_VDDA. F103 Vrefint is untrimmed (1.16–1.24 V, ±3 %) — worse than the AMS1117's ±1–2 %, so Vrefint only helps once anchored. CAL VDDA <mV> (one DMM reading of the 3V3 rail per board) derives and stores this chip's Vrefint; afterwards VDDA = Vrefint-tracked. (Corrects DESIGN-NOTES §B1b "no probes".)
  • INA180 zero: CAL ZERO — refuses if the raw current reads above the load-present guard: 75 mA on 12 V, ~120 mA on 5 V. The PDU's own 3V3 regulator, CAN transceiver and LED sit after the 5 V shunt (~30–50 mA; ~3 mA on 12 V), so the 5 V rail never reads true zero: zero-cal subtracts the PDU's own draw and the reading becomes downstream load only (decided 2026-10-15).
  • Power-up blanking: all rail / thermal faults are ignored for the first 500 ms after boot (rail ramp + inrush).
  • Trigger (decided): DiagSerial only for v0.2.0. A blank cal page → defaults + UNCAL flags; nothing is written automatically.
  • Storage: last 1 KB flash page, 0x0800FC00 (STM32F103C8). board_upload.maximum_size = 64512 so the build fails if code grows into it. Append-only log of 32-byte records, newest valid wins, erase only when full (32 writes).
Off Size Field
0x00 u32 magic 'PCAL'
0x04 u8 version (1)
0x05 u8 calFlags (VDDA / ZERO / GAIN / NTC)
0x06 u16 vrefintMv (default 1200)
0x08 i16 12 V zero offset, 0.1 mA
0x0A i16 5 V zero offset, 0.1 mA
0x0C u16 ×2 current gain trims (10000 = 1.0)
0x10 u16 ×2 voltage gain trims (10000 = 1.0)
0x14 i16 NTC offset, 0.1 °C
0x16 u16 reserved
0x18 u32 seq
0x1C u32 CRC32 over 0x00–0x1B

10. Status LED (decided)

Same as every other node: the PB14/PB15 LED shows CAN/link state only, plus the master-loss WARNING that every non-bridge node gets from CANProtocol after #340. Rail faults surface via HEALTH DEGRADED, the TELEM flags and the client — not the LED (D14). Rail presence stays visible on the hardware D3/D4 LEDs even if the MCU is dead.

11. Code layout

  • New Firmware/Libraries/PowerMonitor/ — RailMonitor, BoardThermal, PduCalStore, PduTelemetry (+ a node wrapper so sketches stay thin).
  • Sketches Firmware/Panels/PDU_Left/ (NODE_ID 3), PDU_Right/ (4), PDU_Center/ (5) — three identical thin sketches that only start the PDU library; one dir per board so tools/check_node_ids.py validates each NODE_ID against NODE_IDS.md unchanged. Plain numbers + comments (named NODE_ID constants = possible future improvement, not now).
  • Node plumbing (HB / HEALTH / master watchdog) via CANProtocol::serviceNodeStatus() from refactor(firmware): move node heartbeat, HEALTH and master watchdog into CANProtocol #340; the PDU sends its own one-line READY_n at boot and answers SYNC_REQ by sending its next telemetry page immediately.

12. Installation assumptions (decided 2026-10-15)

  • Power never chains past a PDU: the downstream harness does not feed another PDU, so a blown fuse leaves that console's rail dark. Fuse-open detection and the per-rail current readings both rely on this.
  • PanelBridge is powered from one of the PDUs (likely Center). If its PDU's 5 V fuse blows, the bridge goes down with it and the client sees every node offline rather than a fuse alert — the troubleshooting step is "check the PDU fuses". Document in the power / troubleshooting docs.
  • CANProtocol.h: canIdTelem() + 8-byte TelemPayload (static_assert). NodeStatus.h: TelemPage, PduRailFlag, PduStatusFlag, new fault codes, NODE_TELEM_* (so the client's sync-a4ec generates them).

Implementation checklist (one PR, branch off fresh main) — PR #343 (2026-10-15)

Code

  • CANProtocol.h — canIdTelem, TelemPayload; fix stale INA226 comment (:73)
  • NodeStatus.h — NODE_TELEM_*, TelemPage, PDU flag enums, NodeFaultCode 0x06–0x08
  • PanelBridge.cpp — TELEM branch → _NODE_TELEM
  • Libraries/PowerMonitor/ + Panels/PDU_Left|Right|Center/ — flash use fits the 63 KB budget (last 1 KB page reserved for cal)
  • NODE_IDS.md — register 3 / 4 / 5; fix stale "next available is 2"
  • Docs: installation assumptions (§12) in architecture/power.md
  • Tests/PowerMonitor — page packing, cal-store CRC + append + full-page erase, threshold hysteresis/debounce on injected readings
  • Tests/PanelBridge — TELEM scenario via testFeedCanFrame; Tests/CANProtocol — canIdTelem range

Spec + docs (same PR)

  • FirmwarePlan 00-decisions (D17 TELEM_n; fix INA226 in D13), 02-can-protocol (ID fn, frame table, TELEM payload, bridge accept ranges, INA226), 04-dcs-bios-integration (_NODE_TELEM), 06-panelbridge-api, 08-hardware-firmware-contracts (PDU ADC map + cal page), 10-implementation-plan Phase 5b
  • TechSpec CANProtocol, NodeStatus, PanelBridge, new PowerMonitor.md + README index
  • docs architecture/can-bus.md, architecture/power.md, firmware/index.md, firmware/build-flags.md, firmware/node-id.md
  • docs/_source/base-boards.md INA226 → INA180A2; DESIGN-NOTES §B1b VDDA note

Bench (after boards — #202 bring-up)

Deferred

  • CAN-triggered calibration → Notion Tasks "PDU calibration over CAN — host-triggered CAL" (directional; needs a host→node command path).
  • Named NODE_ID constants — dropped (possible future improvement; the CI-validated Firmware/NODE_IDS.md stays the registry).

Spotted while verifying (not in scope)

Related: #202 (hardware), #201, #213 / #163 / #221 (HEALTH_n), SkyHawkClient#61 (client).

Metadata

Metadata

Assignees

No one assigned

    Labels

    firmwareFirmware implementation workpriority: mediumWanted and scoped; not blocking today

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions