diff --git a/Firmware/Libraries/CANProtocol/CANProtocol.cpp b/Firmware/Libraries/CANProtocol/CANProtocol.cpp index 62f3d628..d8d68c7b 100644 --- a/Firmware/Libraries/CANProtocol/CANProtocol.cpp +++ b/Firmware/Libraries/CANProtocol/CANProtocol.cpp @@ -77,8 +77,8 @@ static bool _warnAsserted = false; // what this service last w static uint32_t _lastDupMs = 0; // millis() of the last foreign own-ID HB static bool _dupSeen = false; // a foreign own-ID HB within DUP_NODE_TIMEOUT_MS static uint8_t _ownHbLoopPending = 0; // loopback only: own HBs sent, not yet looped back -// Last HEALTH_n built (lastNodeHealth()) — not-yet-seen until the first one: dieTempC INT8_MIN. -static NodeHealthPayload _lastHealth = { 0, INT8_MIN, 0, 0, 0, 0, { 0, 0 } }; +// Last HEALTH_n built (lastNodeHealth()) — not-yet-seen until the first one. +static NodeHealthPayload _lastHealth = { 0, DIE_TEMP_UNAVAILABLE, 0, 0, 0, 0, { 0, 0 } }; // ── Private helpers ─────────────────────────────────────────────────────────── @@ -132,7 +132,7 @@ static void _applyFilters() { ids[count++] = CAN_ID_CTRL_BCAST; ids[count++] = CAN_ID_TEST_SEQ; ids[count++] = CAN_ID_SYNC_REQ; - if (NODE_ID != 0) ids[count++] = canIdHb(0); // master heartbeat → serviceNodeStatus watchdog + if (NODE_ID != GATEWAY_NODE_ID) ids[count++] = canIdHb(GATEWAY_NODE_ID); // master heartbeat → serviceNodeStatus watchdog ids[count++] = canIdHb(NODE_ID); // own heartbeat ID → duplicate NODE_ID check (#344) for (uint8_t i = 0; i < _filterCount; i++) { ids[count++] = _filterIds[i]; @@ -301,7 +301,7 @@ void drain() { _lastDupMs = millis(); _dupSeen = true; } - } else if (NODE_ID != 0 && frame.canId == canIdHb(0)) { + } else if (NODE_ID != GATEWAY_NODE_ID && frame.canId == canIdHb(GATEWAY_NODE_ID)) { // Master heartbeat: stamp it for serviceNodeStatus()'s watchdog, then still hand it // to the node's own handler like any other frame. _lastMasterMs = millis(); @@ -433,10 +433,12 @@ HeartbeatPayload makeHeartbeatPayload(uint8_t nodeId, uint16_t rxCount) { HeartbeatPayload p; uint32_t esr = CAN1->ESR; p.nodeId = nodeId; - p.flags = (uint8_t)(((esr >> 2) & 0x01) | (esr & 0x02)); // bit0=BOFF, bit1=EPVF + p.flags = (uint8_t)(((esr & CAN_ESR_BOFF) ? (uint8_t)HeartbeatFlag::BOFF : 0u) + | ((esr & CAN_ESR_EPVF) ? (uint8_t)HeartbeatFlag::EPVF : 0u)); p.uptime = (uint16_t)(millis() / 1000); p.rxCount = rxCount; - p.esr = (uint16_t)(esr >> 16); // low byte=TEC, high byte=REC + p.esr = (uint16_t)((((esr >> 16) & 0xFFu) << HB_ESR_TEC_SHIFT) // ESR[23:16] = TEC + | (((esr >> 24) & 0xFFu) << HB_ESR_REC_SHIFT)); // ESR[31:24] = REC return p; } @@ -449,7 +451,7 @@ NodeHealthPayload makeNodeHealthPayload(uint8_t nodeId, NodeKind kind, int8_t di #ifdef NODE_OVERHEAT_C // Overheat trip is opt-in: the internal sensor is uncalibrated, so a sane threshold // needs field data first. Only compute the flag when a build defines NODE_OVERHEAT_C. - if (dieTempC != INT8_MIN && dieTempC >= (int)(NODE_OVERHEAT_C)) + if (dieTempC != DIE_TEMP_UNAVAILABLE && dieTempC >= (int)(NODE_OVERHEAT_C)) p.flags |= static_cast(NodeHealthFlag::OVERHEAT); // bit0 — OR in, never overwrite #endif // Degraded fault (#163): faultId carries the code, DEGRADED bit is derived. faultMask stays 0 @@ -508,17 +510,17 @@ void serviceNodeStatus(NodeKind kind, uint16_t rxCount) { NodeHealthPayload h = makeNodeHealthPayload(NODE_ID, kind, STM32Board::readDieTempC(), fault); _lastHealth = h; send(canIdHealth(NODE_ID), reinterpret_cast(&h), sizeof(h)); - STM32Board::logNodeFaultEdge(NODE_ID == 0 ? "BRIDGE" : "NODE", fault, faultDetail); + STM32Board::logNodeFaultEdge(NODE_ID == GATEWAY_NODE_ID ? "BRIDGE" : "NODE", fault, faultDetail); #ifdef NODE_OVERHEAT_C _overheat = (h.flags & static_cast(NodeHealthFlag::OVERHEAT)) != 0; // The bridge's latch is also PanelBridge's dead-node WARNING, which this service can't see — // so on the bridge overheat stays raise-only rather than risk clearing that. - if (NODE_ID == 0 && _overheat) STM32Board::setWarning(true); + if (NODE_ID == GATEWAY_NODE_ID && _overheat) STM32Board::setWarning(true); #endif } #endif - if (NODE_ID == 0) return; // PanelBridge is the master: no master-loss watchdog + if (NODE_ID == GATEWAY_NODE_ID) return; // PanelBridge is the master: no master-loss watchdog // Master-loss watchdog. Signed elapsed so a stamp a hair ahead of `now` reads as fresh, never as // overdue (#225). diff --git a/Firmware/Libraries/CANProtocol/CANProtocol.h b/Firmware/Libraries/CANProtocol/CANProtocol.h index 4b00b84c..1c52e190 100644 --- a/Firmware/Libraries/CANProtocol/CANProtocol.h +++ b/Firmware/Libraries/CANProtocol/CANProtocol.h @@ -49,10 +49,10 @@ struct __attribute__((packed)) ControlPacketPair { */ struct __attribute__((packed)) HeartbeatPayload { uint8_t nodeId; ///< Node ID — redundant with CAN ID, aids logging - uint8_t flags; ///< bit0=BOFF, bit1=EPVF (Error Passive; TEC >= 128) + uint8_t flags; ///< HeartbeatFlag bits (NodeStatus.h): BOFF, EPVF uint16_t uptime; ///< Seconds since boot, little-endian (wraps at ~18 h) uint16_t rxCount; ///< Caller-owned accepted RX count, little-endian - uint16_t esr; ///< (CAN1->ESR >> 16): low byte=TEC, high byte=REC + uint16_t esr; ///< TEC at HB_ESR_TEC_SHIFT, REC at HB_ESR_REC_SHIFT (NodeStatus.h) }; /** @@ -79,7 +79,7 @@ struct __attribute__((packed)) HeartbeatPayload { */ struct __attribute__((packed)) NodeHealthPayload { uint8_t nodeId; ///< 0: node ID — redundant with CAN ID, aids logging - int8_t dieTempC; ///< 1: internal die temp, whole °C (INT8_MIN = unavailable) — #213 + int8_t dieTempC; ///< 1: internal die temp, whole °C (DIE_TEMP_UNAVAILABLE) — #213 uint8_t flags; ///< 2: NodeHealthFlag bits (NodeStatus.h): OVERHEAT (#213), DEGRADED (#163) uint8_t faultMask; ///< 3: fault source/domain bitmap — reserved for future domain bits (0) uint8_t faultId; ///< 4: NodeFaultCode (NodeStatus.h): 0 = no fault, else primary fault — #163 @@ -121,7 +121,7 @@ static constexpr uint32_t CAN_ID_SYNC_REQ = 0x012; ///< Request all nodes to // ── CAN ID functions (per-node IDs computed from NODE_ID) ───────────────────── -/** @brief Heartbeat frame ID for node n. Range 0x100-0x13F; n=0 is PanelBridge. */ +/** @brief Heartbeat frame ID for node n. Range 0x100-0x13F; n=GATEWAY_NODE_ID (0) is PanelBridge. */ constexpr uint32_t canIdHb(uint8_t n) { return 0x100 + n; } /** @brief Node-health/thermal telemetry frame ID for node n. Range 0x140-0x17F; n=0 is PanelBridge. @@ -354,7 +354,7 @@ namespace CANProtocol { * * Fills uptime, CAN health flags, and ESR-derived TEC/REC from CANProtocol state. * - * @param nodeId Node ID (1-63 PanelGroup; 0 reserved for PanelBridge). + * @param nodeId Node ID (1-63 PanelGroup; GATEWAY_NODE_ID for PanelBridge). * @param rxCount Caller-owned receive counter to embed in the payload. * @return Fully populated HeartbeatPayload ready to send as HB_n. */ @@ -370,9 +370,9 @@ namespace CANProtocol { * NONE (the enum→byte cast happens here), and nodeKind from `kind`. faultMask + reserved bytes * stay 0. * - * @param nodeId Node ID (1-63 PanelGroup; 0 PanelBridge). + * @param nodeId Node ID (1-63 PanelGroup; GATEWAY_NODE_ID PanelBridge). * @param kind The node's board kind (#344) — from its firmware role, never from nodeId. - * @param dieTempC Internal die temp in whole °C (INT8_MIN = unavailable). + * @param dieTempC Internal die temp in whole °C (DIE_TEMP_UNAVAILABLE = unavailable). * @param fault Aggregated node fault (NodeFaultCode::NONE = healthy). Default NONE. * @return Fully populated NodeHealthPayload ready to send as HEALTH_n. */ @@ -429,8 +429,8 @@ namespace CANProtocol { * @brief The HEALTH_n payload serviceNodeStatus() last built for this node (#344). * * PanelBridge reports it as its own node-0 `_NODE_STATUS` row. Before the first HEALTH_n - * (or with `-DNODE_HEALTH_TELEM=0`) it reads not-yet-seen: dieTempC INT8_MIN, everything else 0 - * (nodeKind UNKNOWN) — the same rule the bridge applies to a node it has no HEALTH_n from. + * (or with `-DNODE_HEALTH_TELEM=0`) it reads not-yet-seen: dieTempC DIE_TEMP_UNAVAILABLE, + * everything else 0 (nodeKind UNKNOWN) — the same rule the bridge applies to a node it has no HEALTH_n from. */ const NodeHealthPayload& lastNodeHealth(); diff --git a/Firmware/Libraries/NodeStatus/NodeStatus.h b/Firmware/Libraries/NodeStatus/NodeStatus.h index 2712e6ac..550ef938 100644 --- a/Firmware/Libraries/NodeStatus/NodeStatus.h +++ b/Firmware/Libraries/NodeStatus/NodeStatus.h @@ -4,12 +4,17 @@ * * Owns the cross-node **status API** — not just health: * - the `_NODE_STATUS` DCS-BIOS host-reporting contract (proto version, request address, message - * names) — **the canonical source the client's `sync-a4ec.ts` parses**; - * - `NodeHealthFlag` (HEALTH_n flag bits) and `NodeFaultCode` (the compact wire faultId dictionary - * the client maps to labels); + * names, `GATEWAY_NODE_ID`, row layout `NodeStatusField`) — **the canonical source the client's + * `sync-a4ec.ts` parses**; + * - `HeartbeatFlag` + the `HB_ESR_*` split (HB_n flags / esr fields); + * - `NodeHealthFlag` (HEALTH_n flag bits), `DIE_TEMP_UNAVAILABLE`, and `NodeFaultCode` (the compact + * wire faultId dictionary the client maps to labels); * - `NodeKind` (HEALTH_n board-kind byte — what board a NODE_ID is, announced by its firmware); * - the `_NODE_TELEM` host contract and the TELEM_n page vocabulary (`TelemPage`, `PduRailFlag`, - * `PduStatusFlag`) — #216; + * `PduStatusFlag`, the per-page field offsets, `NTC_TEMP_UNAVAILABLE`) — #216; + * + * Every wire value is a named `enum class X : T { NAME = literal }` or `constexpr T NAME = literal` + * so the client sync can read it (#346) — never only a comment, a bare literal or a format string. * - `FaultSource` — the interface a fault-producing object implements so a node-level aggregator * can roll it up. A `FaultSource` is *one piece* of node status, not the whole story. * @@ -34,6 +39,40 @@ #define NODE_STATUS_MSG_NAME "_NODE_STATUS" #define NODE_STATUS_END_MSG_NAME "_NODE_STATUS_END" +/** + * @brief Node ID of the Gateway board's PanelBridge MCU — the CAN master. + * + * It sends HB_0 / HEALTH_0, and its own row leads every `_NODE_STATUS` roster reply (#344). CAN + * nodes take 1–63 (Firmware/NODE_IDS.md). + */ +constexpr uint8_t GATEWAY_NODE_ID = 0; + +/// `_NODE_STATUS` row length in bytes, sent as twice as many hex chars (proto v3: 14 → 28 hex). +constexpr uint8_t NODE_STATUS_ROW_BYTES = 14; + +/** + * @brief `_NODE_STATUS` row field offsets, in bytes of the decoded row (hex index = 2 × offset). + * + * Each field is its numeric value as fixed-width uppercase hex, most-significant byte first, so + * the u16 fields read big-endian here although they are little-endian on CAN. A field runs to the + * next offset; the last runs to NODE_STATUS_ROW_BYTES. The first six come from the node's HB_n + * (plus `present`), the last five from its HEALTH_n. (Not `NODE_ID`: every node build defines + * that as a macro.) + */ +enum class NodeStatusField : uint8_t { + ID = 0, // u8: node ID; GATEWAY_NODE_ID = the bridge's own row, 1-63 = CAN nodes + PRESENT = 1, // u8: 01 alive, 00 removed + HB_FLAGS = 2, // u8: HeartbeatFlag bits + UPTIME = 3, // u16: seconds since boot, wraps at 65535 + RX_COUNT = 5, // u16: node-owned accepted RX count, wraps at 65535 + ESR = 7, // u16: TEC at HB_ESR_TEC_SHIFT, REC at HB_ESR_REC_SHIFT + DIE_TEMP_C = 9, // i8: whole degC, DIE_TEMP_UNAVAILABLE = not seen + HEALTH_FLAGS = 10, // u8: NodeHealthFlag bits + FAULT_MASK = 11, // u8: reserved, 00 + FAULT_ID = 12, // u8: NodeFaultCode + NODE_KIND = 13, // u8: NodeKind +}; + // ── _NODE_TELEM DCS-BIOS host-reporting contract (#216) ─────────────────────── // PanelBridge forwards every TELEM_n CAN frame to the host unread, as one DCS-BIOS message per // frame: `_NODE_TELEM <8 frame bytes as 16 hex, wire order>` (18 hex chars). Pushed @@ -43,12 +82,41 @@ #define NODE_TELEM_PROTO_VERSION 1 #define NODE_TELEM_MSG_NAME "_NODE_TELEM" +/// `_NODE_TELEM` row length in bytes, sent as twice as many hex chars (9 → 18 hex). +constexpr uint8_t NODE_TELEM_ROW_BYTES = 9; + +/** @brief `_NODE_TELEM` row field offsets, in bytes of the decoded row (hex index = 2 × offset). */ +enum class NodeTelemField : uint8_t { + ID = 0, // u8: sending node ID (from the CAN ID) + FRAME = 1, // 8 bytes: the TELEM_n frame unread, wire order (TelemPage at FRAME + 0) +}; + +/** @brief HB_n `flags` bits — CAN controller error state (maps to HeartbeatPayload.flags). */ +enum class HeartbeatFlag : uint8_t { + BOFF = 0x01, // CAN controller is bus-off + EPVF = 0x02, // error passive: TEC or REC >= 128 +}; + +/// HB_n `esr` field: bit position of the transmit error counter (TEC, low byte). +constexpr uint8_t HB_ESR_TEC_SHIFT = 0; +/// HB_n `esr` field: bit position of the receive error counter (REC, high byte). +constexpr uint8_t HB_ESR_REC_SHIFT = 8; + /** @brief HEALTH_n `flags` bits — node-level health conditions (maps to NodeHealthPayload.flags). */ enum class NodeHealthFlag : uint8_t { OVERHEAT = 0x01, // die temp >= NODE_OVERHEAT_C (opt-in, #213) DEGRADED = 0x02, // a FaultSource reports faultCode() != NodeFaultCode::NONE (#163) }; +/** + * @brief HEALTH_n `dieTempC` meaning "unavailable / not yet seen" (`80` on the wire). + * + * Sent when the die sensor can't be read, and cached by PanelBridge for a node whose first + * HEALTH_n hasn't arrived. Written as a literal so the client sync can read it; equal to INT8_MIN. + */ +constexpr int8_t DIE_TEMP_UNAVAILABLE = -128; +static_assert(DIE_TEMP_UNAVAILABLE == INT8_MIN, "DIE_TEMP_UNAVAILABLE is the int8 minimum"); + /** * @brief HEALTH_n `faultId` dictionary (#163) — cross-node fault codes. * @@ -90,11 +158,8 @@ enum class NodeKind : uint8_t { * @brief TELEM_n page ids (#216) — byte 0 of TelemPayload, naming the layout of bytes 1-7. * * Global and append-only: a page id means the same layout whichever node sends it, so the client - * decodes by page, not by node type. 0x00 is invalid. Layouts (little-endian): - * PDU_RAIL_12V / PDU_RAIL_5V: [1] PduRailFlag bits · [2-3] vAvgMv u16 · [4-5] vMinMv u16 · - * [6-7] iAvgMa int16 - * PDU_STATUS: [1] PduStatusFlag bits · [2-3] ntcTempDC int16 (0.1 °C, - * INT16_MIN = unavailable) · [4-5] i12PeakMa u16 · [6-7] i5PeakMa u16 + * decodes by page, not by node type. 0x00 is invalid. Each page's layout is its field-offset enum + * below (PDU_RAIL_* → PduRailField, PDU_STATUS → PduStatusField); multi-byte fields little-endian. */ enum class TelemPage : uint8_t { INVALID = 0x00, // never sent @@ -121,6 +186,38 @@ enum class PduStatusFlag : uint8_t { NTC_FAULT = 0x10, // NTC channel open or shorted }; +/** + * @brief PDU_RAIL_12V / PDU_RAIL_5V field offsets, in bytes of the 8-byte TELEM_n frame. + * + * Byte 0 is the TelemPage; a field runs to the next offset, the last to byte 7. Little-endian. + */ +enum class PduRailField : uint8_t { + FLAGS = 1, // u8: PduRailFlag bits + V_AVG_MV = 2, // u16: window-average rail voltage, mV + V_MIN_MV = 4, // u16: window-minimum rail voltage, mV + I_AVG_MA = 6, // int16: window-average rail current, mA (signed: zero offset stays visible) +}; + +/** + * @brief PDU_STATUS field offsets, in bytes of the 8-byte TELEM_n frame. + * + * Byte 0 is the TelemPage; a field runs to the next offset, the last to byte 7. Little-endian. + */ +enum class PduStatusField : uint8_t { + FLAGS = 1, // u8: PduStatusFlag bits + NTC_TEMP_DC = 2, // int16: board temperature, 0.1 degC (NTC_TEMP_UNAVAILABLE = no reading) + I12_PEAK_MA = 4, // u16: 12 V rail peak current since the last page, mA + I5_PEAK_MA = 6, // u16: 5 V rail peak current since the last page, mA +}; + +/** + * @brief PDU_STATUS `ntcTempDC` meaning "unavailable" — no reading yet, or the NTC is open/shorted. + * + * `0x8000` on the wire. Written as a literal so the client sync can read it; equal to INT16_MIN. + */ +constexpr int16_t NTC_TEMP_UNAVAILABLE = -32768; +static_assert(NTC_TEMP_UNAVAILABLE == INT16_MIN, "NTC_TEMP_UNAVAILABLE is the int16 minimum"); + namespace OpenSkyhawk { /** diff --git a/Firmware/Libraries/PanelBridge/PanelBridge.cpp b/Firmware/Libraries/PanelBridge/PanelBridge.cpp index 632930f9..186ffea7 100644 --- a/Firmware/Libraries/PanelBridge/PanelBridge.cpp +++ b/Firmware/Libraries/PanelBridge/PanelBridge.cpp @@ -41,7 +41,7 @@ struct NodeState { #ifdef PANELBRIDGE_NODE_STATUS HeartbeatPayload last; // last HB payload, surfaced to the host (#86) // Cached NodeHealthPayload fields from the node's last HEALTH_n frame (#221 contract): - int8_t dieTempC = INT8_MIN; // internal die temp °C (INT8_MIN = unseen) — #213 + int8_t dieTempC = DIE_TEMP_UNAVAILABLE; // internal die temp °C, unseen until HEALTH_n — #213 uint8_t healthFlags = 0; // overheat (#213) / degraded (#163) bits uint8_t faultMask = 0; // fault source/domain bitmap — reserved #163 uint8_t faultId = 0; // NodeFaultCode (NodeStatus.h) — #163 @@ -60,15 +60,23 @@ static uint8_t _deadCount = 0; // tracked nodes currently tim // command messages on a reserved control name. Owned entirely by PanelBridge; // SimGateway relays the ASCII verbatim. See NodeStatus.h for the wire format. -// Emit one status row (proto v3, 28 hex chars): +// Emit one status row (proto v3, NODE_STATUS_ROW_BYTES = 14 bytes as 28 hex chars): // _NODE_STATUS -// The last five come from the node's HEALTH_n frame (#221 contract). dieTempC 80 + nodeKind 00 = -// not-yet-seen; hFlags (DEGRADED) + faultId are populated per node by aggregateFaults (#163); +// The last five come from the node's HEALTH_n frame (#221 contract). DIE_TEMP_UNAVAILABLE + nodeKind +// 00 = not-yet-seen; hFlags (DEGRADED) + faultId are populated per node by aggregateFaults (#163); // faultMask stays 0 (reserved for future fault-domain bits); nodeKind is the board kind (#344). +// The format string is the row layout: each field's width must match NodeStatusField (NodeStatus.h). +using NSF = NodeStatusField; +static_assert((uint8_t)NSF::ID == 0 && (uint8_t)NSF::PRESENT == 1 && (uint8_t)NSF::HB_FLAGS == 2 + && (uint8_t)NSF::UPTIME == 3 && (uint8_t)NSF::RX_COUNT == 5 && (uint8_t)NSF::ESR == 7 + && (uint8_t)NSF::DIE_TEMP_C == 9 && (uint8_t)NSF::HEALTH_FLAGS == 10 + && (uint8_t)NSF::FAULT_MASK == 11 && (uint8_t)NSF::FAULT_ID == 12 + && (uint8_t)NSF::NODE_KIND == 13 && NODE_STATUS_ROW_BYTES == 14, + "NodeStatusField moved: update emitRow's snprintf format to match"); static void emitRow(uint8_t nodeId, bool present, const HeartbeatPayload& hb, int8_t dieTempC, uint8_t hFlags, uint8_t faultMask, uint8_t faultId, uint8_t nodeKind) { - char hex[29]; + char hex[2 * NODE_STATUS_ROW_BYTES + 1]; snprintf(hex, sizeof(hex), "%02X%02X%02X%04X%04X%04X%02X%02X%02X%02X%02X", (unsigned)nodeId, present ? 1u : 0u, (unsigned)hb.flags, (unsigned)hb.uptime, (unsigned)hb.rxCount, (unsigned)hb.esr, @@ -84,12 +92,12 @@ static void emitNode(uint8_t nodeId, bool present) { ns.nodeKind); } -// The bridge's own row, node 0 (#344): its heartbeat state now, and the HEALTH_0 it last sent -// (kind BRIDGE once the first HEALTH_0 has gone out). Always present — it is the one reporting. +// The bridge's own row, GATEWAY_NODE_ID (#344): its heartbeat state now, and the HEALTH_0 it last +// sent (kind BRIDGE once the first HEALTH_0 has gone out). Always present — it is the one reporting. static void emitSelf() { - const HeartbeatPayload hb = CANProtocol::makeHeartbeatPayload(0, 0); + const HeartbeatPayload hb = CANProtocol::makeHeartbeatPayload(GATEWAY_NODE_ID, 0); const NodeHealthPayload& h = CANProtocol::lastNodeHealth(); - emitRow(0, true, hb, h.dieTempC, h.flags, h.faultMask, h.faultId, h.nodeKind); + emitRow(GATEWAY_NODE_ID, true, hb, h.dieTempC, h.flags, h.faultMask, h.faultId, h.nodeKind); } // Emit the full roster (request response / boot seed): the bridge's own node-0 row, one @@ -109,9 +117,12 @@ static void emitAllNodes() { // Forward one TELEM_n frame to the host, unread (#216): // _NODE_TELEM <8 frame bytes, wire order, 16 hex> // No cache and no decode — the page byte tells the client the layout, so a new TelemPage needs no -// bridge change. Contract: NODE_TELEM_* in NodeStatus.h. +// bridge change. Contract: NODE_TELEM_* + NodeTelemField in NodeStatus.h. +static_assert((uint8_t)NodeTelemField::ID == 0 && (uint8_t)NodeTelemField::FRAME == 1 + && NODE_TELEM_ROW_BYTES == 1 + sizeof(TelemPayload), + "NodeTelemField moved: update emitTelem's snprintf format to match"); static void emitTelem(uint8_t nodeId, const uint8_t* data) { - char hex[19]; + char hex[2 * NODE_TELEM_ROW_BYTES + 1]; snprintf(hex, sizeof(hex), "%02X%02X%02X%02X%02X%02X%02X%02X%02X", (unsigned)nodeId, data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7]); diff --git a/Firmware/Libraries/PowerMonitor/BoardThermal.cpp b/Firmware/Libraries/PowerMonitor/BoardThermal.cpp index 2f63b433..9a7f2c6e 100644 --- a/Firmware/Libraries/PowerMonitor/BoardThermal.cpp +++ b/Firmware/Libraries/PowerMonitor/BoardThermal.cpp @@ -4,7 +4,7 @@ namespace OpenSkyhawk { void BoardThermal::sample(int16_t tempDC, bool ntcFault, bool armed) { - _tempDC = ntcFault ? INT16_MIN : tempDC; + _tempDC = ntcFault ? NTC_TEMP_UNAVAILABLE : tempDC; if (!armed) { _warn = _crit = _ntcFault = false; return; } _ntcFault = ntcFault; diff --git a/Firmware/Libraries/PowerMonitor/BoardThermal.h b/Firmware/Libraries/PowerMonitor/BoardThermal.h index ddbf3820..602046e3 100644 --- a/Firmware/Libraries/PowerMonitor/BoardThermal.h +++ b/Firmware/Libraries/PowerMonitor/BoardThermal.h @@ -35,14 +35,14 @@ class BoardThermal : public FaultSource { /** @brief PduStatusFlag bits this monitor owns: TEMP_WARN, TEMP_CRIT, NTC_FAULT. */ uint8_t flags() const; - /** @brief Latest temperature, 0.1 °C; INT16_MIN when unavailable (no reading yet / NTC fault). */ + /** @brief Latest temperature, 0.1 °C; NTC_TEMP_UNAVAILABLE when no reading yet / NTC fault. */ int16_t tempDC() const { return _tempDC; } NodeFaultCode faultCode() const override; const char* faultDetail() const override; private: - int16_t _tempDC = INT16_MIN; + int16_t _tempDC = NTC_TEMP_UNAVAILABLE; bool _warn = false; bool _crit = false; bool _ntcFault = false; diff --git a/Firmware/Libraries/PowerMonitor/PduMath.cpp b/Firmware/Libraries/PowerMonitor/PduMath.cpp index 301bfe09..616ce312 100644 --- a/Firmware/Libraries/PowerMonitor/PduMath.cpp +++ b/Firmware/Libraries/PowerMonitor/PduMath.cpp @@ -1,5 +1,6 @@ #include "PduMath.h" #include "PduConfig.h" +#include // NTC_TEMP_UNAVAILABLE #include namespace OpenSkyhawk { @@ -55,7 +56,7 @@ bool railed(uint16_t counts) { int16_t ntcTempDC(uint16_t counts, int16_t offsetDC, bool& fault) { // Open NTC → node pulled to +3V3 (top); shorted → 0. Either way the reading means nothing. fault = counts < ADC_FULL * 2 / 100 || counts > ADC_FULL * 98 / 100; - if (fault) return INT16_MIN; + if (fault) return NTC_TEMP_UNAVAILABLE; const float x = (float)counts / (float)ADC_FULL; // node / VDDA const float rNtc = NTC_BIAS_OHM * x / (1.0f - x); // NTC is the low side const float invT = 1.0f / NTC_T0_K + logf(rNtc / NTC_R0_OHM) / NTC_BETA_K; diff --git a/Firmware/Libraries/PowerMonitor/PduMath.h b/Firmware/Libraries/PowerMonitor/PduMath.h index ee7b42c1..f44d9fb7 100644 --- a/Firmware/Libraries/PowerMonitor/PduMath.h +++ b/Firmware/Libraries/PowerMonitor/PduMath.h @@ -61,7 +61,7 @@ int32_t rawCurrentDmA(uint32_t adcMv); * @param counts 16-bit ADC count of the NTC node. * @param offsetDC One-point offset, 0.1 °C. * @param fault Set true when the reading is outside 2–98 % of full scale (open / shorted NTC). - * @return Temperature in 0.1 °C, or INT16_MIN when `fault`. + * @return Temperature in 0.1 °C, or NTC_TEMP_UNAVAILABLE (NodeStatus.h) when `fault`. */ int16_t ntcTempDC(uint16_t counts, int16_t offsetDC, bool& fault); @@ -76,5 +76,8 @@ int16_t clampI16(int32_t v); /** @brief Clamp an int32 into uint16 range. */ uint16_t clampU16(int32_t v); +/** @brief Store `v` little-endian at `p[0..1]` — a multi-byte TELEM page field. */ +inline void putLe16(uint8_t* p, uint16_t v) { p[0] = (uint8_t)(v & 0xFF); p[1] = (uint8_t)(v >> 8); } + } // namespace Pdu } // namespace OpenSkyhawk diff --git a/Firmware/Libraries/PowerMonitor/PduNode.cpp b/Firmware/Libraries/PowerMonitor/PduNode.cpp index bd8948f1..a613c7a8 100644 --- a/Firmware/Libraries/PowerMonitor/PduNode.cpp +++ b/Firmware/Libraries/PowerMonitor/PduNode.cpp @@ -55,21 +55,26 @@ TelemPayload statusPage(bool periodic) { const int16_t t = gTherm.tempDC(); const uint16_t p12 = g12.peakMa(), p5 = g5.peakMa(); TelemPayload p; - p.page = (uint8_t)TelemPage::PDU_STATUS; - p.body[0] = statusFlags(); - p.body[1] = (uint8_t)((uint16_t)t & 0xFF); p.body[2] = (uint8_t)((uint16_t)t >> 8); - p.body[3] = (uint8_t)(p12 & 0xFF); p.body[4] = (uint8_t)(p12 >> 8); - p.body[5] = (uint8_t)(p5 & 0xFF); p.body[6] = (uint8_t)(p5 >> 8); + uint8_t* f = reinterpret_cast(&p); // whole frame: PduStatusField offsets index it + p.page = (uint8_t)TelemPage::PDU_STATUS; + f[(uint8_t)PduStatusField::FLAGS] = statusFlags(); + Pdu::putLe16(f + (uint8_t)PduStatusField::NTC_TEMP_DC, (uint16_t)t); + Pdu::putLe16(f + (uint8_t)PduStatusField::I12_PEAK_MA, p12); + Pdu::putLe16(f + (uint8_t)PduStatusField::I5_PEAK_MA, p5); if (periodic) { g12.resetPeak(); g5.resetPeak(); } return p; } +static_assert((uint8_t)PduRailField::FLAGS == (uint8_t)PduStatusField::FLAGS, + "sendPage reads every PDU page's flags from one offset"); + void sendPage(uint8_t slot, bool periodic, uint32_t now) { TelemPayload p = slot == 0 ? g12.buildPage(periodic) : slot == 1 ? g5.buildPage(periodic) : statusPage(periodic); - CANProtocol::send(canIdTelem(NODE_ID), reinterpret_cast(&p), sizeof(p)); - gSched.sent(slot, p.body[0], now); + const uint8_t* f = reinterpret_cast(&p); + CANProtocol::send(canIdTelem(NODE_ID), f, sizeof(p)); + gSched.sent(slot, f[(uint8_t)PduStatusField::FLAGS], now); } void onSyncReq() { gSyncReq = true; } diff --git a/Firmware/Libraries/PowerMonitor/RailMonitor.cpp b/Firmware/Libraries/PowerMonitor/RailMonitor.cpp index b9930b1c..7a86edd8 100644 --- a/Firmware/Libraries/PowerMonitor/RailMonitor.cpp +++ b/Firmware/Libraries/PowerMonitor/RailMonitor.cpp @@ -59,12 +59,12 @@ TelemPayload RailMonitor::buildPage(bool resetWindow) { const int16_t iAvg = Pdu::clampI16(_n ? _sumMa / (int32_t)_n : _lastMa); TelemPayload p; - p.page = (uint8_t)_cfg.page; - p.body[0] = flags(); - p.body[1] = (uint8_t)(vAvg & 0xFF); p.body[2] = (uint8_t)(vAvg >> 8); - p.body[3] = (uint8_t)(vMin & 0xFF); p.body[4] = (uint8_t)(vMin >> 8); - p.body[5] = (uint8_t)((uint16_t)iAvg & 0xFF); - p.body[6] = (uint8_t)((uint16_t)iAvg >> 8); + uint8_t* f = reinterpret_cast(&p); // whole frame: PduRailField offsets index it + p.page = (uint8_t)_cfg.page; + f[(uint8_t)PduRailField::FLAGS] = flags(); + Pdu::putLe16(f + (uint8_t)PduRailField::V_AVG_MV, vAvg); + Pdu::putLe16(f + (uint8_t)PduRailField::V_MIN_MV, vMin); + Pdu::putLe16(f + (uint8_t)PduRailField::I_AVG_MA, (uint16_t)iAvg); if (resetWindow) { _sumMv = 0; _sumMa = 0; _n = 0; _minMv = UINT16_MAX; } return p; diff --git a/Firmware/Libraries/STM32Board/STM32Board.cpp b/Firmware/Libraries/STM32Board/STM32Board.cpp index 829ecdb5..087704f8 100644 --- a/Firmware/Libraries/STM32Board/STM32Board.cpp +++ b/Firmware/Libraries/STM32Board/STM32Board.cpp @@ -353,15 +353,15 @@ uint16_t readVddMv() { int8_t readDieTempC() { #if defined(ATEMP) && defined(AVREF) uint16_t vdd = readVddMv(); - if (vdd == 0) return INT8_MIN; // internal reference unreadable → unavailable + if (vdd == 0) return DIE_TEMP_UNAVAILABLE; // internal reference unreadable uint32_t traw = analogRead(ATEMP); int32_t vsense_mv = (int32_t)((uint64_t)traw * vdd / ADC_FULL); int32_t tempC = ((V25_MV - vsense_mv) * 10) / AVG_SLOPE_10 + 25; - if (tempC <= INT8_MIN) tempC = INT8_MIN + 1; // reserve INT8_MIN as the "unavailable" sentinel + if (tempC <= DIE_TEMP_UNAVAILABLE) tempC = DIE_TEMP_UNAVAILABLE + 1; // keep the sentinel unambiguous if (tempC > INT8_MAX) tempC = INT8_MAX; return (int8_t)tempC; #else - return INT8_MIN; + return DIE_TEMP_UNAVAILABLE; #endif } diff --git a/Firmware/Libraries/STM32Board/STM32Board.h b/Firmware/Libraries/STM32Board/STM32Board.h index 6fad99bb..5ea6bfcc 100644 --- a/Firmware/Libraries/STM32Board/STM32Board.h +++ b/Firmware/Libraries/STM32Board/STM32Board.h @@ -20,7 +20,7 @@ #include #include -#include // NodeFaultCode for logNodeFaultEdge (#163) +#include // NodeFaultCode for logNodeFaultEdge (#163), DIE_TEMP_UNAVAILABLE // CanStatus is owned by CANProtocol; forward-declared here so onCanStatus() // can accept it without pulling the full CANProtocol header into every user. @@ -163,8 +163,8 @@ namespace STM32Board { * @note UNCALIBRATED: no factory trim → ~±few °C absolute; measures DIE temperature * (not ambient) with a self-heat offset. Use for relative trend / overheat * flagging, not precise ambient measurement. - * @return Die temperature in whole °C, or INT8_MIN if the internal channels are - * unavailable on this variant. + * @return Die temperature in whole °C, or DIE_TEMP_UNAVAILABLE (NodeStatus.h) if the internal + * channels are unavailable on this variant. */ int8_t readDieTempC(); diff --git a/Firmware/Libraries/SimGateway/AxisCal.h b/Firmware/Libraries/SimGateway/AxisCal.h index d553df6f..03dfe4e2 100644 --- a/Firmware/Libraries/SimGateway/AxisCal.h +++ b/Firmware/Libraries/SimGateway/AxisCal.h @@ -221,6 +221,94 @@ enum CalNackReason : uint8_t { /// `RESET` and the axis-selection fields use this to mean "all" / "none". constexpr uint8_t CAL_AXIS_NONE = 0xFF; +// ── Payload field offsets ───────────────────────────────────────────────────── +// +// Byte offsets into each type's payload (not the frame — the payload starts after the 8-byte +// header). Multi-byte fields are little-endian, matching the envelope's LEN. A field runs to the +// next offset; the last runs to the type's length in calLenValidForType(). Types with no payload +// have no enum. Named so the client sync generates them rather than hand-typing them (#346). + +/// `HELLO_ACK` payload (6 bytes, device→client). +enum CalHelloAckField : uint8_t { + CAL_HELLO_ACK_PROTO = 0, // u8: CAL_PROTO_VERSION + CAL_HELLO_ACK_BLOB_VERSION = 1, // u8: CAL_VERSION + CAL_HELLO_ACK_AXIS_SLOTS = 2, // u8: AXIS_CAL_SLOTS + CAL_HELLO_ACK_FW_MAJOR = 3, // u8: SimGateway firmware version + CAL_HELLO_ACK_FW_MINOR = 4, // u8 + CAL_HELLO_ACK_FW_PATCH = 5, // u8 +}; + +/// `CAL_DATA` payload (82 bytes, device→client): two bitmasks, then AXIS_CAL_SLOTS records. +enum CalDataField : uint8_t { + CAL_DATA_PRESENT_MASK = 0, // u8: bit i = an HIDAxis uses slot i + CAL_DATA_CALIBRATED_MASK = 1, // u8: bit i = slot i passes axisCalValid() + CAL_DATA_RECORDS = 2, // AXIS_CAL_SLOTS records of CAL_DATA_REC_BYTES, slot order +}; + +/// One `CAL_DATA` record's length in bytes. +constexpr uint8_t CAL_DATA_REC_BYTES = 10; + +/// Field offsets within one `CAL_DATA` record (record i starts at CAL_DATA_RECORDS + i × 10). +enum CalDataRecField : uint8_t { + CAL_DATA_REC_CONTROL_ID = 0, // u16: HID controlId of the axis in this slot, 0 = absent + CAL_DATA_REC_MIN = 2, // u16: AxisCal::min + CAL_DATA_REC_CENTRE = 4, // u16: AxisCal::centre + CAL_DATA_REC_MAX = 6, // u16: AxisCal::max + CAL_DATA_REC_DEADZONE = 8, // u16: AxisCal::deadzone, always 0 +}; + +/// `SESSION_OPEN` payload (1 byte, client→device). +enum CalSessionOpenField : uint8_t { + CAL_SESSION_OPEN_AXIS_IDX = 0, // u8: axis to stream, CAL_AXIS_NONE = none +}; + +/// `SESSION_ACK` payload (5 bytes, device→client). +enum CalSessionAckField : uint8_t { + CAL_SESSION_ACK_TIMEOUT_MS = 0, // u32: session inactivity timeout, ms + CAL_SESSION_ACK_AXIS_IDX = 4, // u8: echoed streamed-axis selection +}; + +/// `STREAM_SELECT` payload (1 byte, client→device). +enum CalStreamSelectField : uint8_t { + CAL_STREAM_SELECT_AXIS_IDX = 0, // u8: axis to stream, CAL_AXIS_NONE = none +}; + +/// `COMMIT` payload (9 bytes, client→device) — exactly one axis. +enum CalCommitField : uint8_t { + CAL_COMMIT_IDX = 0, // u8: axis slot + CAL_COMMIT_MIN = 1, // u16 + CAL_COMMIT_CENTRE = 3, // u16 + CAL_COMMIT_MAX = 5, // u16 + CAL_COMMIT_DEADZONE = 7, // u16: must be 0, else NACK BAD_DEADZONE +}; + +/// `RESET` payload (1 byte, client→device). +enum CalResetField : uint8_t { + CAL_RESET_IDX = 0, // u8: axis slot, CAL_AXIS_NONE = all +}; + +/// `ACK` payload (1 byte, device→client). +enum CalAckField : uint8_t { + CAL_ACK_TYPE = 0, // u8: CalType being acknowledged +}; + +/// `NACK` payload (3 bytes, device→client). +enum CalNackField : uint8_t { + CAL_NACK_TYPE = 0, // u8: CalType being refused + CAL_NACK_REASON = 1, // u8: CalNackReason + CAL_NACK_DETAIL = 2, // u8: offending axis slot, else CAL_AXIS_NONE +}; + +/// `RAW` payload (5 bytes, device→client). +enum CalRawField : uint8_t { + CAL_RAW_IDX = 0, // u8: streamed axis slot + CAL_RAW_RAW = 1, // u16: reading before calibration + CAL_RAW_CAL = 3, // u16: the same reading through axisCalApply() +}; + +static_assert(CAL_DATA_RECORDS + AXIS_CAL_SLOTS * CAL_DATA_REC_BYTES == CAL_MAX_PAYLOAD, + "CAL_DATA layout no longer fills CAL_MAX_PAYLOAD"); + /** * @brief Is `len` the only length this `type` may legally carry? * @param type Message type byte. diff --git a/Firmware/Libraries/SimGateway/SimGateway.cpp b/Firmware/Libraries/SimGateway/SimGateway.cpp index 14a5f411..2561761a 100644 --- a/Firmware/Libraries/SimGateway/SimGateway.cpp +++ b/Firmware/Libraries/SimGateway/SimGateway.cpp @@ -344,14 +344,24 @@ void _calSend(uint8_t type, uint8_t seq, const uint8_t* payload, uint16_t len) { } void _calNack(uint8_t type, uint8_t seq, uint8_t reason, uint8_t detail) { - const uint8_t p[3] = { type, reason, detail }; + uint8_t p[3]; + p[OpenSkyhawk::CAL_NACK_TYPE] = type; + p[OpenSkyhawk::CAL_NACK_REASON] = reason; + p[OpenSkyhawk::CAL_NACK_DETAIL] = detail; _calSend(OpenSkyhawk::CAL_T_NACK, seq, p, sizeof(p)); } +// The whole ACK payload is the acknowledged type, so it is sent straight from `type`. +static_assert(OpenSkyhawk::CAL_ACK_TYPE == 0, "ACK payload is the type byte alone"); void _calAck(uint8_t type, uint8_t seq) { _calSend(OpenSkyhawk::CAL_T_ACK, seq, &type, 1); } +// Initialised in CalHelloAckField order; the assert holds the list to the offsets. +static_assert(OpenSkyhawk::CAL_HELLO_ACK_PROTO == 0 && OpenSkyhawk::CAL_HELLO_ACK_BLOB_VERSION == 1 + && OpenSkyhawk::CAL_HELLO_ACK_AXIS_SLOTS == 2 && OpenSkyhawk::CAL_HELLO_ACK_FW_MAJOR == 3 + && OpenSkyhawk::CAL_HELLO_ACK_FW_MINOR == 4 && OpenSkyhawk::CAL_HELLO_ACK_FW_PATCH == 5, + "CalHelloAckField moved: reorder _calSendHelloAck's initialiser to match"); void _calSendHelloAck(uint8_t seq) { const uint8_t p[6] = { OpenSkyhawk::CAL_PROTO_VERSION, @@ -385,15 +395,15 @@ void _calSendCalData(uint8_t seq) { if (axisCalValid(_calBlob.axes[i])) calibratedMask = (uint8_t)(calibratedMask | (1u << i)); } - p[0] = presentMask; - p[1] = calibratedMask; - uint16_t o = 2; + p[OpenSkyhawk::CAL_DATA_PRESENT_MASK] = presentMask; + p[OpenSkyhawk::CAL_DATA_CALIBRATED_MASK] = calibratedMask; for (uint8_t i = 0; i < OpenSkyhawk::AXIS_CAL_SLOTS; ++i) { - _put16(p + o, ctrl[i]); o += 2; - _put16(p + o, _calBlob.axes[i].min); o += 2; - _put16(p + o, _calBlob.axes[i].centre); o += 2; - _put16(p + o, _calBlob.axes[i].max); o += 2; - _put16(p + o, _calBlob.axes[i].deadzone); o += 2; + uint8_t* r = p + OpenSkyhawk::CAL_DATA_RECORDS + i * OpenSkyhawk::CAL_DATA_REC_BYTES; + _put16(r + OpenSkyhawk::CAL_DATA_REC_CONTROL_ID, ctrl[i]); + _put16(r + OpenSkyhawk::CAL_DATA_REC_MIN, _calBlob.axes[i].min); + _put16(r + OpenSkyhawk::CAL_DATA_REC_CENTRE, _calBlob.axes[i].centre); + _put16(r + OpenSkyhawk::CAL_DATA_REC_MAX, _calBlob.axes[i].max); + _put16(r + OpenSkyhawk::CAL_DATA_REC_DEADZONE, _calBlob.axes[i].deadzone); } _calSend(OpenSkyhawk::CAL_T_CAL_DATA, seq, p, sizeof(p)); } @@ -483,9 +493,9 @@ void _calEmitRaw(uint8_t axisIndex, uint16_t raw) { : raw; uint8_t p[5]; - p[0] = axisIndex; - _put16(p + 1, raw); - _put16(p + 3, cal); + p[OpenSkyhawk::CAL_RAW_IDX] = axisIndex; + _put16(p + OpenSkyhawk::CAL_RAW_RAW, raw); + _put16(p + OpenSkyhawk::CAL_RAW_CAL, cal); _calSend(OpenSkyhawk::CAL_T_RAW, seq, p, sizeof(p)); } @@ -505,18 +515,19 @@ void _calEndSession() { // and keeps a bad axis from discarding good ones alongside it. void _calHandleCommit(uint8_t seq, const uint8_t* pay, uint16_t len) { (void)len; - const uint8_t idx = pay[0]; + const uint8_t idx = pay[OpenSkyhawk::CAL_COMMIT_IDX]; if (idx >= OpenSkyhawk::AXIS_CAL_SLOTS) { _calNack(OpenSkyhawk::CAL_T_COMMIT, seq, OpenSkyhawk::CAL_NACK_BAD_INDEX, idx); return; } - if (_get16(pay + 7) != 0) { + if (_get16(pay + OpenSkyhawk::CAL_COMMIT_DEADZONE) != 0) { _calNack(OpenSkyhawk::CAL_T_COMMIT, seq, OpenSkyhawk::CAL_NACK_BAD_DEADZONE, idx); return; } const OpenSkyhawk::AxisCal axis = { - _get16(pay + 1), _get16(pay + 3), _get16(pay + 5), _get16(pay + 7) + _get16(pay + OpenSkyhawk::CAL_COMMIT_MIN), _get16(pay + OpenSkyhawk::CAL_COMMIT_CENTRE), + _get16(pay + OpenSkyhawk::CAL_COMMIT_MAX), _get16(pay + OpenSkyhawk::CAL_COMMIT_DEADZONE) }; if (!axisCalValid(axis)) { _calNack(OpenSkyhawk::CAL_T_COMMIT, seq, OpenSkyhawk::CAL_NACK_BAD_ORDER, idx); @@ -564,7 +575,7 @@ void _calHandleFrame(const uint8_t* f, uint16_t n) { break; case OpenSkyhawk::CAL_T_SESSION_OPEN: { - const uint8_t axis = pay[0]; + const uint8_t axis = pay[OpenSkyhawk::CAL_SESSION_OPEN_AXIS_IDX]; if (axis != CAL_AXIS_NONE && axis >= OpenSkyhawk::AXIS_CAL_SLOTS) { _calNack(type, seq, OpenSkyhawk::CAL_NACK_BAD_INDEX, axis); break; @@ -573,15 +584,16 @@ void _calHandleFrame(const uint8_t* f, uint16_t n) { _calStreamAxis = axis; _calRawSeq = 0; uint8_t p[5]; - _put16(p + 0, (uint16_t)(CAL_SESSION_TIMEOUT_MS & 0xFFFF)); - _put16(p + 2, (uint16_t)(CAL_SESSION_TIMEOUT_MS >> 16)); - p[4] = axis; + uint8_t* t = p + OpenSkyhawk::CAL_SESSION_ACK_TIMEOUT_MS; // u32 LE as two u16 + _put16(t + 0, (uint16_t)(CAL_SESSION_TIMEOUT_MS & 0xFFFF)); + _put16(t + 2, (uint16_t)(CAL_SESSION_TIMEOUT_MS >> 16)); + p[OpenSkyhawk::CAL_SESSION_ACK_AXIS_IDX] = axis; _calSend(OpenSkyhawk::CAL_T_SESSION_ACK, seq, p, sizeof(p)); break; } case OpenSkyhawk::CAL_T_STREAM_SELECT: { - const uint8_t axis = pay[0]; + const uint8_t axis = pay[OpenSkyhawk::CAL_STREAM_SELECT_AXIS_IDX]; if (axis != CAL_AXIS_NONE && axis >= OpenSkyhawk::AXIS_CAL_SLOTS) { _calNack(type, seq, OpenSkyhawk::CAL_NACK_BAD_INDEX, axis); break; @@ -605,7 +617,7 @@ void _calHandleFrame(const uint8_t* f, uint16_t n) { break; case OpenSkyhawk::CAL_T_RESET: { - const uint8_t idx = pay[0]; + const uint8_t idx = pay[OpenSkyhawk::CAL_RESET_IDX]; if (idx != CAL_AXIS_NONE && idx >= OpenSkyhawk::AXIS_CAL_SLOTS) { _calNack(type, seq, OpenSkyhawk::CAL_NACK_BAD_INDEX, idx); break; diff --git a/Firmware/ScratchPad/FirmwarePlan/02-can-protocol.md b/Firmware/ScratchPad/FirmwarePlan/02-can-protocol.md index e4ba057b..9758ce08 100644 --- a/Firmware/ScratchPad/FirmwarePlan/02-can-protocol.md +++ b/Firmware/ScratchPad/FirmwarePlan/02-can-protocol.md @@ -139,12 +139,13 @@ struct __attribute__((packed)) HeartbeatPayload { }; ``` -**flags bitmask:** +**flags bitmask** (`HeartbeatFlag`, `NodeStatus.h`): - bit 0 (`0x01`): BOFF — CAN controller is in bus-off state - bit 1 (`0x02`): EPVF — Error Passive (TEC ≥ 128 or REC ≥ 128) **esr field:** `(uint16_t)(CAN1->ESR >> 16)`. CAN_ESR register layout: TEC in bits [23:16], -REC in bits [31:24]. After the shift: low byte = TEC, high byte = REC. +REC in bits [31:24]. After the shift: low byte = TEC, high byte = REC — `HB_ESR_TEC_SHIFT` (0) and +`HB_ESR_REC_SHIFT` (8) in `NodeStatus.h`. **rxCount field:** node-owned receive counter for diagnostics. PanelGroup uses accepted `CTRL_BCAST` frames received since boot. @@ -170,7 +171,7 @@ Vrefint internally to reference Vsense to Vdd — Vdd itself is not transmitted) ```cpp struct __attribute__((packed)) NodeHealthPayload { uint8_t nodeId; // 0: node ID (redundant with CAN ID, aids logging) - int8_t dieTempC; // 1: internal die temp, whole °C (INT8_MIN = unavailable) — #213 + int8_t dieTempC; // 1: internal die temp, whole °C (DIE_TEMP_UNAVAILABLE) — #213 uint8_t flags; // 2: NodeHealthFlag bits (NodeStatus.h): OVERHEAT, DEGRADED — #213/#163 uint8_t faultMask; // 3: fault source/domain bitmap — reserved for future domain bits (0) uint8_t faultId; // 4: NodeFaultCode (NodeStatus.h): 0 = no fault, else primary fault — #163 @@ -250,7 +251,10 @@ exactly as `HEALTH_n` does, which at ≤ 1 frame/s per node costs an input event | Page | Byte 1 | Bytes 2–3 | Bytes 4–5 | Bytes 6–7 | |---|---|---|---|---| | `0x01 PDU_RAIL_12V` / `0x02 PDU_RAIL_5V` | `PduRailFlag` bits | `vAvgMv` u16 | `vMinMv` u16 (window min) | `iAvgMa` **int16** | -| `0x03 PDU_STATUS` | `PduStatusFlag` bits | `ntcTempDC` int16, 0.1 °C (`INT16_MIN` = unavailable) | `i12PeakMa` u16 | `i5PeakMa` u16 | +| `0x03 PDU_STATUS` | `PduStatusFlag` bits | `ntcTempDC` int16, 0.1 °C (`NTC_TEMP_UNAVAILABLE` = `INT16_MIN`) | `i12PeakMa` u16 | `i5PeakMa` u16 | + +Each page's byte offsets are named in `NodeStatus.h` — `PduRailField` for both rail pages, +`PduStatusField` for `PDU_STATUS` — and the PDU packs its frames through them. - `PduRailFlag`: bit0 UNDER_V · bit1 OVER_V · bit2 OVER_CURRENT · bit3 FUSE_OPEN · bit4 SENSOR_FAULT. - `PduStatusFlag`: bit0 UNCAL_VDDA · bit1 UNCAL_ZERO · bit2 TEMP_WARN · bit3 TEMP_CRIT · bit4 NTC_FAULT. diff --git a/Firmware/ScratchPad/FirmwarePlan/03-uart-usb-hid-protocol.md b/Firmware/ScratchPad/FirmwarePlan/03-uart-usb-hid-protocol.md index d5026af0..f0eac657 100644 --- a/Firmware/ScratchPad/FirmwarePlan/03-uart-usb-hid-protocol.md +++ b/Firmware/ScratchPad/FirmwarePlan/03-uart-usb-hid-protocol.md @@ -270,6 +270,11 @@ the mapping — but that is data in the message, not the message's identity. | `NACK` | 3 | `type`:u8, `reason`:u8, `detail`:u8 (axis index, or `0xFF`) | | `RAW` | 5 | `idx`:u8, `raw`:u16, `cal`:u16 | +Field offsets are named in `AxisCal.h` — one enum per type with a payload (`CalHelloAckField`, +`CalDataField` + `CalDataRecField` and `CAL_DATA_REC_BYTES`, `CalSessionOpenField`, +`CalSessionAckField`, `CalStreamSelectField`, `CalCommitField`, `CalResetField`, `CalAckField`, +`CalNackField`, `CalRawField`) — and SimGateway reads and writes payloads through them. + **Values on the wire are unsigned 0–65535**, matching storage and the node — `RAW`'s `raw` and `cal`, the `CAL_DATA` endpoints, and what `COMMIT` writes. They are positions in the node's ADC space, where zero carries no meaning, so a sign would say nothing extra. diff --git a/Firmware/ScratchPad/FirmwarePlan/04-dcs-bios-integration.md b/Firmware/ScratchPad/FirmwarePlan/04-dcs-bios-integration.md index 916d8312..0b06119b 100644 --- a/Firmware/ScratchPad/FirmwarePlan/04-dcs-bios-integration.md +++ b/Firmware/ScratchPad/FirmwarePlan/04-dcs-bios-integration.md @@ -38,12 +38,14 @@ forwards host→device; the response is ASCII it forwards device→host). PanelB `NodeStatusReqListener` (an `ExportStreamListener`) fires → emits the full roster. **Response** — `_NODE_STATUS ` where `` is **28 chars** (proto **v3**, #344; v2 was 26 chars -without `nodeKind`), **each field its numeric value as fixed-width uppercase hex -(most-significant nibble first)**: +without `nodeKind`) — a `NODE_STATUS_ROW_BYTES` (14) byte row, **each field its numeric value as +fixed-width uppercase hex (most-significant nibble first)**: `nodeId(2) present(2) flags(2) uptime(4) rxCount(4) esr(4) dieTempC(2) hFlags(2) faultMask(2) faultId(2) nodeKind(2)` -(the 8-byte `HeartbeatPayload` plus `present`, then the node's cached `HEALTH_n` fields). `present`: -`01` alive, `00` removed. `flags`: bit0 BOFF, bit1 EPVF. `esr`: low byte TEC, high byte REC. -`nodeId` 0–63: 0 is the bridge's own row, 1–63 the CAN nodes. `uptime`/`rxCount` are uint16 — +(the 8-byte `HeartbeatPayload` plus `present`, then the node's cached `HEALTH_n` fields). Each field's +byte offset in the row is named by `NodeStatusField` in `NodeStatus.h` (hex index = 2 × offset). +`present`: `01` alive, `00` removed. `flags`: `HeartbeatFlag` — bit0 BOFF, bit1 EPVF. `esr`: low byte +TEC, high byte REC (`HB_ESR_TEC_SHIFT` / `HB_ESR_REC_SHIFT`). `nodeId` 0–63: `GATEWAY_NODE_ID` (0) is +the bridge's own row, 1–63 the CAN nodes. `uptime`/`rxCount` are uint16 — wrap at 65535 (~18 h / 65 k frames); treat as health indicators, not monotonic counters. **Node-0 row (v3).** Every roster reply starts with the bridge's own row: `present` `01`, its @@ -55,7 +57,7 @@ delta: the bridge is the one reporting, so it is present whenever anything arriv The last five fields come from the node's `HEALTH_n` frame (see `02-can-protocol.md`): - `dieTempC`: int8 two's-complement whole °C (internal MCU sensor — UNCALIBRATED, die not ambient). - `80` = `INT8_MIN` = not-yet-seen → render unknown. + `80` = `DIE_TEMP_UNAVAILABLE` (`INT8_MIN`) = not-yet-seen → render unknown. - `hFlags`: node health bits — bit0 overheat, bit1 degraded. - `faultId`: `NodeFaultCode` — `00` = no fault, else the node's primary fault, populated per node by `aggregateFaults()` (#163); the client maps it to a label (no strings on the wire). @@ -97,6 +99,9 @@ frame: _NODE_TELEM 18 hex chars: nodeId(2) + the 8 TELEM frame bytes (16), in wire order ``` +The row is `NODE_TELEM_ROW_BYTES` (9) bytes; `NodeTelemField` names the two offsets (`ID` 0, +`FRAME` 1). + - **Pushed as frames arrive** — no request, no cache, no terminator. The host keeps the latest value per `(nodeId, page)` and treats a page as stale after a few periods (PDU: one frame per second, rotating three pages, so each page every 3 s). diff --git a/Firmware/ScratchPad/TechSpec/CANProtocol.md b/Firmware/ScratchPad/TechSpec/CANProtocol.md index 890c08f4..75343277 100644 --- a/Firmware/ScratchPad/TechSpec/CANProtocol.md +++ b/Firmware/ScratchPad/TechSpec/CANProtocol.md @@ -260,10 +260,10 @@ struct __attribute__((packed)) ControlPacketPair { */ struct __attribute__((packed)) HeartbeatPayload { uint8_t nodeId; ///< Node ID — redundant with CAN ID, aids logging - uint8_t flags; ///< bit0=BOFF, bit1=EPVF (Error Passive; TEC ≥ 128) + uint8_t flags; ///< HeartbeatFlag bits (NodeStatus.h): BOFF, EPVF uint16_t uptime; ///< Seconds since boot, little-endian (wraps at ~18 h) uint16_t rxCount; ///< Node-owned accepted RX count, little-endian - uint16_t esr; ///< (CAN1->ESR >> 16): low byte=TEC, high byte=REC + uint16_t esr; ///< TEC at HB_ESR_TEC_SHIFT, REC at HB_ESR_REC_SHIFT (NodeStatus.h) }; /** @@ -571,9 +571,9 @@ namespace CANProtocol { * fault != NONE (never clobbering the overheat bit), and writes nodeKind (byte 5, #344). * faultMask + reserved bytes 6-7 stay 0 (#163). * - * @param nodeId Node ID to place in the payload; 1-63 PanelGroup, 0 PanelBridge. + * @param nodeId Node ID to place in the payload; 1-63 PanelGroup, GATEWAY_NODE_ID PanelBridge. * @param kind Board kind (NodeKind) from the node's firmware role — never from nodeId. - * @param dieTempC Internal die temp in whole °C (INT8_MIN = unavailable). + * @param dieTempC Internal die temp in whole °C (DIE_TEMP_UNAVAILABLE = unavailable). * @param fault Primary node fault; NONE (default) = healthy node. * @return Fully populated NodeHealthPayload ready to send as HEALTH_n. */ @@ -614,7 +614,7 @@ namespace CANProtocol { void serviceNodeStatus(NodeKind kind, uint16_t rxCount = 0); /** @brief The HEALTH_n payload serviceNodeStatus() last built (PanelBridge's node-0 row, #344). - * Not-yet-seen (dieTempC INT8_MIN, the rest 0) before the first one. */ + * Not-yet-seen (dieTempC DIE_TEMP_UNAVAILABLE, the rest 0) before the first one. */ const NodeHealthPayload& lastNodeHealth(); /** @brief Master-loss timeout — 3× the 500 ms HB_0 cadence. */ diff --git a/Firmware/ScratchPad/TechSpec/NodeStatus.md b/Firmware/ScratchPad/TechSpec/NodeStatus.md index ec093aeb..a0ba4833 100644 --- a/Firmware/ScratchPad/TechSpec/NodeStatus.md +++ b/Firmware/ScratchPad/TechSpec/NodeStatus.md @@ -18,7 +18,12 @@ Owns: version, request address, message names). **The canonical contract source the client's `sync-a4ec.ts` parses**; `reference.test.ts` asserts the proto version so a firmware bump fails the client loudly. -- **`NodeHealthFlag`** — HEALTH_n flag bits (OVERHEAT, DEGRADED) mirroring `NodeHealthPayload.flags`. +- **`GATEWAY_NODE_ID`** (0) and the `_NODE_STATUS` row layout — `NODE_STATUS_ROW_BYTES` (14) and + `NodeStatusField` (byte offset of each row field; hex index = 2 × offset). +- **`HeartbeatFlag`** (BOFF, EPVF) + **`HB_ESR_TEC_SHIFT` / `HB_ESR_REC_SHIFT`** — the HB_n `flags` + bits and `esr` byte split. +- **`NodeHealthFlag`** — HEALTH_n flag bits (OVERHEAT, DEGRADED) mirroring `NodeHealthPayload.flags`; + **`DIE_TEMP_UNAVAILABLE`** (−128 = `INT8_MIN`) — the `dieTempC` "unavailable / not seen" value. - **`NodeFaultCode`** — the compact `faultId` dictionary (cross-node: I2C_PERIPHERAL, OVER_VOLTAGE, UNDER_VOLTAGE, SHORT_CIRCUIT, HOST_LINK_LOST, DUPLICATE_NODE_ID, …). Client maps id → label (SkyHawkClient#40). @@ -29,9 +34,16 @@ Owns: - **`NODE_TELEM_*` + the TELEM_n page vocabulary (#216)** — `NODE_TELEM_MSG_NAME` (`_NODE_TELEM`) and `NODE_TELEM_PROTO_VERSION` (1, independent of `_NODE_STATUS`); `TelemPage` (global, append-only page ids — PDU_RAIL_12V / PDU_RAIL_5V / PDU_STATUS); `PduRailFlag` and `PduStatusFlag` (the page - flag bytes). Here, not in a PDU header, so the client's `sync-a4ec.ts` generates them from the same + flag bytes); `PduRailField` / `PduStatusField` (each page's byte offsets); `NTC_TEMP_UNAVAILABLE` + (−32768 = `INT16_MIN`); `NODE_TELEM_ROW_BYTES` (9) + `NodeTelemField`. Here, not in a PDU header, so the client's `sync-a4ec.ts` generates them from the same contract file (SkyHawkClient#61). +**Every wire value is a named declaration (#346)** — `enum class X : T { NAME = literal }` or +`constexpr T NAME = literal` — never only a comment, a bare literal or a format string, because the +client's `sync-a4ec.ts` can read only those two forms. Sentinels are written as literals +(`-128`, `-32768`) with a `static_assert` against `INT8_MIN` / `INT16_MIN`. Row-field enums say `ID`, +not `NODE_ID`: every node build defines `NODE_ID` as a macro. + `NodeHealthPayload` (the CAN HEALTH_n frame struct) stays in `CANProtocol.h`. Fault **detail strings** stay local (DiagSerial) and never go on the wire or into any shared/HID header. @@ -41,7 +53,7 @@ strings** stay local (DiagSerial) and never go on the wire or into any shared/HI ``` Firmware/Libraries/NodeStatus/ -├── NodeStatus.h ← NODE_STATUS_* + NodeHealthFlag + NodeFaultCode + NodeKind + FaultSource +├── NodeStatus.h ← NODE_STATUS_* + row/page layouts + HeartbeatFlag + NodeHealthFlag + NodeFaultCode + NodeKind + FaultSource ├── NodeStatus.cpp ← FaultSource intrusive-list statics └── library.json ← platforms: * ``` @@ -58,7 +70,21 @@ Firmware/Libraries/NodeStatus/ #define NODE_TELEM_PROTO_VERSION 1 // bump on a _NODE_TELEM framing / existing-page layout change #define NODE_TELEM_MSG_NAME "_NODE_TELEM" +constexpr uint8_t GATEWAY_NODE_ID = 0; // the bridge: HB_0 / HEALTH_0, first roster row +constexpr uint8_t NODE_STATUS_ROW_BYTES = 14; // 28 hex +enum class NodeStatusField : uint8_t { // row byte offsets; multi-byte fields MSB-first + ID = 0, PRESENT = 1, HB_FLAGS = 2, UPTIME = 3, RX_COUNT = 5, ESR = 7, + DIE_TEMP_C = 9, HEALTH_FLAGS = 10, FAULT_MASK = 11, FAULT_ID = 12, NODE_KIND = 13, +}; +constexpr uint8_t NODE_TELEM_ROW_BYTES = 9; // 18 hex +enum class NodeTelemField : uint8_t { ID = 0, FRAME = 1 }; + +enum class HeartbeatFlag : uint8_t { BOFF = 0x01, EPVF = 0x02 }; +constexpr uint8_t HB_ESR_TEC_SHIFT = 0; // HB_n esr: TEC low byte +constexpr uint8_t HB_ESR_REC_SHIFT = 8; // REC high byte + enum class NodeHealthFlag : uint8_t { OVERHEAT = 0x01, DEGRADED = 0x02 }; +constexpr int8_t DIE_TEMP_UNAVAILABLE = -128; // == INT8_MIN, static_assert'd enum class NodeFaultCode : uint8_t { NONE = 0, I2C_PERIPHERAL = 1, OVER_VOLTAGE = 2, UNDER_VOLTAGE = 3, @@ -71,6 +97,11 @@ enum class NodeKind : uint8_t { // HEALTH_n byte 5 (#344), append-only UNKNOWN = 0, PANEL_GROUP = 1, PDU = 2, BRIDGE = 3, // 0x04+ reserved }; +// TELEM_n page layouts (#216): byte offsets into the 8-byte frame, little-endian (byte 0 = TelemPage) +enum class PduRailField : uint8_t { FLAGS = 1, V_AVG_MV = 2, V_MIN_MV = 4, I_AVG_MA = 6 }; +enum class PduStatusField : uint8_t { FLAGS = 1, NTC_TEMP_DC = 2, I12_PEAK_MA = 4, I5_PEAK_MA = 6 }; +constexpr int16_t NTC_TEMP_UNAVAILABLE = -32768; // == INT16_MIN, static_assert'd + namespace OpenSkyhawk { class FaultSource { // a fault-producing object implements this public: diff --git a/Firmware/ScratchPad/TechSpec/PanelBridge.md b/Firmware/ScratchPad/TechSpec/PanelBridge.md index 2434db04..6ca39263 100644 --- a/Firmware/ScratchPad/TechSpec/PanelBridge.md +++ b/Firmware/ScratchPad/TechSpec/PanelBridge.md @@ -375,8 +375,9 @@ a child-node heartbeat — one that does arrive is another board on node 0, whic Behind `-DPANELBRIDGE_NODE_STATUS`. `NodeState` caches each node's last `HeartbeatPayload` and its `HEALTH_n` fields (`dieTempC`, `healthFlags`, `faultMask`, `faultId`, `nodeKind` — not-yet-seen: -`INT8_MIN` / `0`). One `emitRow()` formats a 28-hex `_NODE_STATUS` row; `emitNode(id, present)` -feeds it from the cache and `emitSelf()` from the bridge's own state — `makeHeartbeatPayload(0, 0)` +`DIE_TEMP_UNAVAILABLE` / `0`). One `emitRow()` formats a 28-hex `_NODE_STATUS` row — its `snprintf` +format is the `NodeStatusField` layout, held to it by a `static_assert`; `emitNode(id, present)` +feeds it from the cache and `emitSelf()` from the bridge's own state — `makeHeartbeatPayload(GATEWAY_NODE_ID, 0)` plus `CANProtocol::lastNodeHealth()` (its last `HEALTH_0`, kind `BRIDGE`). `emitAllNodes()` (boot seed + host request) sends the node-0 row, every alive node, then `_NODE_STATUS_END ` with node 0 counted. Live deltas: alive/dead transitions, and a `nodeKind` change on an alive node. diff --git a/Firmware/ScratchPad/TechSpec/PowerMonitor.md b/Firmware/ScratchPad/TechSpec/PowerMonitor.md index 5f5e84be..c88dc38e 100644 --- a/Firmware/ScratchPad/TechSpec/PowerMonitor.md +++ b/Firmware/ScratchPad/TechSpec/PowerMonitor.md @@ -71,7 +71,7 @@ class OpenSkyhawk::RailMonitor : public FaultSource { class OpenSkyhawk::BoardThermal : public FaultSource { void sample(int16_t tempDC, bool ntcFault, bool armed); uint8_t flags() const; // TEMP_WARN | TEMP_CRIT | NTC_FAULT - int16_t tempDC() const; // INT16_MIN = unavailable + int16_t tempDC() const; // NTC_TEMP_UNAVAILABLE = unavailable NodeFaultCode faultCode() const override; // OVER_TEMPERATURE > SENSOR_FAULT }; diff --git a/Firmware/ScratchPad/TechSpec/STM32Board.md b/Firmware/ScratchPad/TechSpec/STM32Board.md index 366dd4cf..5d589d87 100644 --- a/Firmware/ScratchPad/TechSpec/STM32Board.md +++ b/Firmware/ScratchPad/TechSpec/STM32Board.md @@ -264,7 +264,7 @@ namespace STM32Board { * (V25 = 1.43 V, Avg_Slope = 4.3 mV/°C), referencing Vsense to the measured Vdd. * UNCALIBRATED — ~±few °C absolute, die (not ambient), self-heat offset. * - * @returns Die temperature in whole °C, or INT8_MIN if internal channels are unavailable. + * @returns Die temperature in whole °C, or DIE_TEMP_UNAVAILABLE if internal channels are unavailable. */ int8_t readDieTempC(); diff --git a/Firmware/ScratchPad/TechSpec/SimGateway.md b/Firmware/ScratchPad/TechSpec/SimGateway.md index 0e077ea9..6773b2ec 100644 --- a/Firmware/ScratchPad/TechSpec/SimGateway.md +++ b/Firmware/ScratchPad/TechSpec/SimGateway.md @@ -343,6 +343,14 @@ void calBlobSeal(CalBlob& blob); ///< Stamp magic, version, } // namespace OpenSkyhawk ``` +The calibration wire protocol (FirmwarePlan 03) is declared below the blob API: `CalType`, +`CalNackReason`, the envelope constants, and — so the client sync generates them (#346) — one +payload-offset enum per type that carries a payload: `CalHelloAckField`, `CalDataField` + +`CalDataRecField` (`CAL_DATA_REC_BYTES` = 10), `CalSessionOpenField`, `CalSessionAckField`, +`CalStreamSelectField`, `CalCommitField`, `CalResetField`, `CalAckField`, `CalNackField`, +`CalRawField`. Offsets are into the payload, little-endian; `SimGateway.cpp` reads and writes +every payload through them, and a `static_assert` ties `CAL_DATA`'s layout to `CAL_MAX_PAYLOAD`. + --- ## Key Data Structures diff --git a/Firmware/Tests/CANProtocol/tests/health_flags/health_flags.cpp b/Firmware/Tests/CANProtocol/tests/health_flags/health_flags.cpp index 71588afb..a8f3c73c 100644 --- a/Firmware/Tests/CANProtocol/tests/health_flags/health_flags.cpp +++ b/Firmware/Tests/CANProtocol/tests/health_flags/health_flags.cpp @@ -38,7 +38,7 @@ void setup() { // Sentinel temp (unavailable) never trips overheat; fault alone -> DEGRADED only. NodeHealthPayload deg = CANProtocol::makeNodeHealthPayload( - NODE_ID, NodeKind::PDU, INT8_MIN, NodeFaultCode::OVER_VOLTAGE); + NODE_ID, NodeKind::PDU, DIE_TEMP_UNAVAILABLE, NodeFaultCode::OVER_VOLTAGE); check("no-temp + fault -> DEGRADED only", deg.flags == DEGRADED); d.println(pass ? F("=== ALL PASS ===") : F("=== FAIL ===")); diff --git a/Firmware/Tests/CANProtocol/tests/health_payload/health_payload.cpp b/Firmware/Tests/CANProtocol/tests/health_payload/health_payload.cpp index 18df15bc..6b05f352 100644 --- a/Firmware/Tests/CANProtocol/tests/health_payload/health_payload.cpp +++ b/Firmware/Tests/CANProtocol/tests/health_payload/health_payload.cpp @@ -40,7 +40,7 @@ void setup() { int8_t liveTemp = STM32Board::readDieTempC(); uint16_t liveVdd = STM32Board::readVddMv(); - bool tempOk = (liveTemp == INT8_MIN) || (liveTemp >= -40 && liveTemp <= 125); + bool tempOk = (liveTemp == DIE_TEMP_UNAVAILABLE) || (liveTemp >= -40 && liveTemp <= 125); bool vddOk = (liveVdd == 0) || (liveVdd >= 2700 && liveVdd <= 3600); auto& d = STM32Board::diagSerial(); diff --git a/Firmware/Tests/PanelBridge/tests/node_status/node_status.cpp b/Firmware/Tests/PanelBridge/tests/node_status/node_status.cpp index 28ce467d..ef4f1127 100644 --- a/Firmware/Tests/PanelBridge/tests/node_status/node_status.cpp +++ b/Firmware/Tests/PanelBridge/tests/node_status/node_status.cpp @@ -79,7 +79,7 @@ void loop() { case 0: // Phase A — inject heartbeats; both nodes transition to alive d.println(F("[TEST] A: feed HB node 1 & 2 + DEGRADED HEALTH node 1 -> expect 2x _NODE_STATUS ...01...")); // Node 2 back to not-yet-seen (it is dead here, so no delta), so every cycle repeats. - PanelBridge::testFeedHealth(2, INT8_MIN); + PanelBridge::testFeedHealth(2, DIE_TEMP_UNAVAILABLE); // dieTempC 2A + DEGRADED/I2C_PERIPHERAL + PANEL_GROUP — cache before the alive edge emits PanelBridge::testFeedHealth(1, 42, (uint8_t)NodeHealthFlag::DEGRADED, 0, diff --git a/Firmware/Tests/PowerMonitor/tests/board_thermal/board_thermal.cpp b/Firmware/Tests/PowerMonitor/tests/board_thermal/board_thermal.cpp index 6cba94e5..da87f879 100644 --- a/Firmware/Tests/PowerMonitor/tests/board_thermal/board_thermal.cpp +++ b/Firmware/Tests/PowerMonitor/tests/board_thermal/board_thermal.cpp @@ -40,7 +40,7 @@ void setup() { th.sample(0, true, true); check("NTC fault flagged", has(PduStatusFlag::NTC_FAULT)); check("NTC fault -> SENSOR_FAULT", th.faultCode() == NodeFaultCode::SENSOR_FAULT); - checkEq("NTC fault -> temperature unavailable", th.tempDC(), INT16_MIN); + checkEq("NTC fault -> temperature unavailable", th.tempDC(), NTC_TEMP_UNAVAILABLE); check("NTC fault keeps last warn state", has(PduStatusFlag::TEMP_WARN)); th.sample(400, false, true); checkEq("NTC back, 40 C: healthy", th.flags(), 0); diff --git a/Firmware/Tests/PowerMonitor/tests/pdu_math/pdu_math.cpp b/Firmware/Tests/PowerMonitor/tests/pdu_math/pdu_math.cpp index 712bd202..a958ebdc 100644 --- a/Firmware/Tests/PowerMonitor/tests/pdu_math/pdu_math.cpp +++ b/Firmware/Tests/PowerMonitor/tests/pdu_math/pdu_math.cpp @@ -7,6 +7,7 @@ #include "../common/check.h" #include #include +#include // NTC_TEMP_UNAVAILABLE #include using namespace OpenSkyhawk::Pdu; @@ -37,7 +38,7 @@ void setup() { checkNear("NTC 4085 ohm -> 50.0 C", ntcTempDC(19007, 0, fault), 500, 2); checkNear("NTC 28835 ohm -> 0.0 C", ntcTempDC(48661, 0, fault), 0, 2); checkEq("NTC offset applied", ntcTempDC(32768, -15, fault), 235); - checkEq("NTC shorted -> unavailable", ntcTempDC(500, 0, fault), INT16_MIN); + checkEq("NTC shorted -> unavailable", ntcTempDC(500, 0, fault), NTC_TEMP_UNAVAILABLE); check("NTC shorted flagged", fault); ntcTempDC(65000, 0, fault); check("NTC open flagged", fault); diff --git a/Firmware/Tests/STM32Board/tests/adc_clock/adc_clock.cpp b/Firmware/Tests/STM32Board/tests/adc_clock/adc_clock.cpp index c7ea4d16..6c451dab 100644 --- a/Firmware/Tests/STM32Board/tests/adc_clock/adc_clock.cpp +++ b/Firmware/Tests/STM32Board/tests/adc_clock/adc_clock.cpp @@ -89,7 +89,7 @@ void setup() { d.print(F("liveTemp=")); d.print(temp); d.print(F("C")); d.print(F(" liveVdd=")); d.print(vdd); d.println(F("mV")); - check("die temp plausible ", temp == INT8_MIN || (temp >= -40 && temp <= 125)); + check("die temp plausible ", temp == DIE_TEMP_UNAVAILABLE || (temp >= -40 && temp <= 125)); check("Vdd plausible ", vdd == 0 || (vdd >= 2700 && vdd <= 3600)); summary(); diff --git a/docs/firmware/build-flags.md b/docs/firmware/build-flags.md index 5504e5b5..aa625bc1 100644 --- a/docs/firmware/build-flags.md +++ b/docs/firmware/build-flags.md @@ -62,8 +62,8 @@ actually does. |---|---|---|---| | `NODE_ID` | none — omitting it fails to compile | every STM32 env that links our libraries; `WiringCheck` omits it deliberately | `CANProtocol.cpp:225`, `PanelGroup.cpp:300`, `STM32Board.h:29` | | `PANELBRIDGE_NODE_STATUS` | off | on in `Templates/PanelBridge` | `PanelBridge.cpp:41` + ~10 more | -| `NODE_HEALTH_TELEM` | **on** | never set | `CANProtocol.cpp:495` (`serviceNodeStatus`, every node) | -| `NODE_OVERHEAT_C` | **unset — by design** | bench envs only | `CANProtocol.cpp:449`, `CANProtocol.cpp:512` | +| `NODE_HEALTH_TELEM` | **on** | never set | `CANProtocol.cpp:497` (`serviceNodeStatus`, every node) | +| `NODE_OVERHEAT_C` | **unset — by design** | bench envs only | `CANProtocol.cpp:451`, `CANProtocol.cpp:514` | | `FORCE_CLOCK_FALLBACK` | off | `Tests/STM32Board` env `test_adc_clock_fallback` | `STM32Board.cpp:179` | | `PINREF_DEBUG` | off | never set | `PinRef.cpp:8` + ~9 more | | `SHIFTBUS_SCK` | `PB3` | never set | `ShiftBus.cpp:13` |