Repository navigation
feat(firmware): PDU monitoring node — telemetry firmware + new CAN telemetry packet class #216
Description
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_telemrun (needs the UART2 adapter), client SkyHawkClient#61. Change from the design:SYNC_REQsends 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_STATUSproto 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
FaultSourcereports aNodeFaultCode;aggregateFaults()fillsHEALTH_n.faultId+ DEGRADED.OVER_VOLTAGE/UNDER_VOLTAGE/SHORT_CIRCUITare already defined. 0x700+n(proposed in the original body) is taken byEVT_ACTION_n(feat(firmware): ActionButton — momentary push-button input #116). Free per-node blocks:0x180,0x1C0, and the+0x40halves of0x2xx–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 asFault 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+nstays free. - Outranks
EVT_nin 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.iAvgMais 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 withSTM32Board::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_TELEMtraffic 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 = 64512so 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 sotools/check_node_ids.pyvalidates each NODE_ID againstNODE_IDS.mdunchanged. 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-lineREADY_nat boot and answersSYNC_REQby 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-byteTelemPayload(static_assert).NodeStatus.h:TelemPage,PduRailFlag,PduStatusFlag, new fault codes,NODE_TELEM_*(so the client'ssync-a4ecgenerates 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,NodeFaultCode0x06–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 viatestFeedCanFrame;Tests/CANProtocol—canIdTelemrange
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-planPhase 5b - TechSpec
CANProtocol,NodeStatus,PanelBridge, newPowerMonitor.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.mdINA226 → INA180A2; DESIGN-NOTES §B1b VDDA note
Bench (after boards — #202 bring-up)
- ADC scan rate achieved (1 kHz target) — 1000 scans/s measured on an STM32F103 (same MCU as the PDU)
- OpenOCD upload erases only written sectors — cal page survives a reflash (verified 2026-10-15: CAL VDDA + CAL ZERO,
pio … -t upload, CAL SHOW unchanged) -
→ moved to feat(pcb): per-console power distribution + monitoring CAN node (PDU) — INA180 telemetry #202 (needs the PDU boards; bring-up checklist)CAL VDDA/CAL ZEROon each board; thresholds tuned against real load -
TELEM +→ moved to feat: node data layer — PDU telemetry, node mapping, new-node detection SkyHawkClient#61 (its acceptance; fix(docs): update header and footer links to new site structure #61 is in that repo's mirrored "Firmware v0.2.0" milestone)_NODE_TELEMend-to-end into the client
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.mdstays the registry).
Spotted while verifying (not in scope)
HB_0 "never transmitted" in D12 / 02 / can-bus.md— resolved in refactor(firmware): move node heartbeat, HEALTH and master watchdog into CANProtocol #340 (PR refactor(firmware): move node heartbeat, HEALTH and master watchdog into CANProtocol (#340) #342; D12 marked superseded).— resolved in PR feat(firmware): PDU monitoring node and TELEM_n telemetry frame (#216) #343: spec now states the as-built bounded retry for HB / HEALTH / TELEM (only02-can-protocolTX policy said HB frames drop-staleCTRL_BCASTcoalesces).
Related: #202 (hardware), #201, #213 / #163 / #221 (HEALTH_n), SkyHawkClient#61 (client).
Activity
feat(pcb): PDU power-distribution + monitoring board (#202) (#217)
ci(firmware): regenerate the PlatformIO impact map for PowerMonitor a…
docs(hardware): Power Distribution Unit page — what it measures, fuse…
fix(firmware): CAL CLEAR reports a failed flash erase instead of clai…
docs(firmware): PDU power LEDs show rail presence only, not faults (#216
docs(firmware): TX queue spec — periodic status frames use bounded re…
feat(firmware): PDU monitoring node and TELEM_n telemetry frame (#216) (
feat(nodes): node data layer — PDU telemetry, node mapping, new-node …