File PanelBridge.cpp
File List > Firmware > Libraries > PanelBridge > PanelBridge.cpp
Go to the documentation of this file
// DcsBios.h defines tryToSendDcsBiosMessage only under DCSBIOS_DEFAULT_SERIAL.
// Pre-declare it so the inline sendDcsBiosMessage() in DcsBios.h compiles without the flag.
// The definition is provided by the sketch TU (compiled with -DDCSBIOS_DEFAULT_SERIAL).
namespace DcsBios {
bool tryToSendDcsBiosMessage(const char* msg, const char* arg);
}
// Include DcsBios.h WITHOUT DCSBIOS_DEFAULT_SERIAL to avoid ODR violations.
// The sketch TU owns ProtocolParser, DcsBios::setup(), DcsBios::loop(), and the definition
// of tryToSendDcsBiosMessage(). This TU only needs the types and constants.
#ifdef DCSBIOS_DEFAULT_SERIAL
# undef DCSBIOS_DEFAULT_SERIAL
# define _PBRIDGE_RESTORE_DCSBIOS
#endif
#include <DcsBios.h>
#ifdef _PBRIDGE_RESTORE_DCSBIOS
# define DCSBIOS_DEFAULT_SERIAL
# undef _PBRIDGE_RESTORE_DCSBIOS
#endif
#ifdef ARDUINO_ARCH_STM32
#include "PanelBridge.h"
#include <CANProtocol.h>
#include <STM32Board.h>
#include <A4EC_InputMap.h>
#include <HIDControls.h> // NODE_STATUS_REQ_ADDR / NODE_STATUS_MSG_NAME (node-status reporting, #86)
#include <string.h>
namespace {
// ── Node tracking ─────────────────────────────────────────────────────────────
static constexpr uint8_t MAX_NODE_ID = 63;
static constexpr uint32_t HB_TIMEOUT_MS = 3000;
static constexpr uint32_t MASTER_HB_PERIOD_MS = 500; // HB_0 master heartbeat cadence
struct NodeState {
bool alive;
bool everSeen;
uint32_t lastSeenMs;
#ifdef PANELBRIDGE_NODE_STATUS
HeartbeatPayload last; // last HB payload, surfaced to the host (#86)
#endif
};
static NodeState _nodes[MAX_NODE_ID] = {}; // index = nodeId - 1
static void (*_cbAlive)(uint8_t) = nullptr;
static void (*_cbDead)(uint8_t) = nullptr;
static uint8_t _deadCount = 0; // tracked nodes currently timed-out (drives WARNING)
static uint32_t _lastMasterHbMs = 0; // millis() of last HB_0 transmit
#ifdef PANELBRIDGE_NODE_STATUS
// ── Node-status reporting (#86) ──────────────────────────────────────────────
// Surface node presence + health to the host (OpenSkyhawk Client) as DCS-BIOS
// command messages on a reserved control name. Owned entirely by PanelBridge;
// SimGateway relays the ASCII verbatim. See HIDControls.h for the wire format.
// Emit one node's status: _NODE_STATUS <nodeId present flags uptime rxCount esr> (18 hex chars).
static void emitNode(uint8_t nodeId, bool present) {
const HeartbeatPayload& hb = _nodes[nodeId - 1].last;
char hex[19];
snprintf(hex, sizeof(hex), "%02X%02X%02X%04X%04X%04X",
(unsigned)nodeId, present ? 1u : 0u, (unsigned)hb.flags,
(unsigned)hb.uptime, (unsigned)hb.rxCount, (unsigned)hb.esr);
DcsBios::sendDcsBiosMessage(NODE_STATUS_MSG_NAME, hex);
}
// Emit the full roster (request response / boot seed): one _NODE_STATUS per alive
// node, then _NODE_STATUS_END <count> so the host knows the burst is complete and
// can reconcile (prune nodes absent from it). count=0 = no panels connected.
static void emitAllNodes() {
uint8_t count = 0;
for (uint8_t i = 0; i < MAX_NODE_ID; i++)
if (_nodes[i].alive) { emitNode(i + 1, true); count++; }
char arg[4];
snprintf(arg, sizeof(arg), "%u", (unsigned)count);
DcsBios::sendDcsBiosMessage(NODE_STATUS_END_MSG_NAME, arg);
}
#endif // PANELBRIDGE_NODE_STATUS
// ── TEST_SEQ state ─────────────────────────────────────────────────────────────
static uint16_t _testSeqNum = 0;
static uint32_t _testSeqSentMs = 0;
// ── DCS session change ─────────────────────────────────────────────────────────
static int32_t _lastModelTimeSec = -1; // -1 = unseeded
// ── Helpers ────────────────────────────────────────────────────────────────────
static void broadcastSyncReq() {
static const uint8_t empty[1] = {};
CANProtocol::send(CAN_ID_SYNC_REQ, empty, 0);
STM32Board::log("[BRIDGE] SYNC_REQ broadcast");
}
static void sendHidFrame(uint16_t controlId, uint16_t value) {
uint8_t frame[6] = {
0xAA, 0x55,
(uint8_t)(controlId & 0xFF), (uint8_t)(controlId >> 8),
(uint8_t)(value & 0xFF), (uint8_t)(value >> 8)
};
Serial.write(frame, 6);
}
// Internal helper called by BridgeExportListener — separated for testability.
void handleDcsBiosExport(uint16_t address, uint16_t value) {
#ifdef PANELBRIDGE_NODE_STATUS
// Reserved: the host's node-status request — handled by NodeStatusReqListener,
// never broadcast onto CAN.
if (address == NODE_STATUS_REQ_ADDR) return;
#endif
// A real DCS-BIOS export → data is flowing → CONNECTED (green solid). The reserved
// node-status poll above is excluded so it does not masquerade as live DCS data.
STM32Board::setLinkActive(true);
ControlPacket pkt;
pkt.controlId = address;
pkt.value = value;
CANProtocol::sendBatched(CAN_ID_CTRL_BCAST, pkt);
}
// Binary search A4EC_INPUT_MAP (sorted ascending by cmdId) -> entry, or nullptr.
static const DcsBiosInputEntry* lookupDcsEntry(uint16_t controlId) {
int lo = 0, hi = (int)A4EC_INPUT_MAP_SIZE - 1;
while (lo <= hi) {
int mid = (lo + hi) / 2;
if (A4EC_INPUT_MAP[mid].cmdId == controlId) return &A4EC_INPUT_MAP[mid];
else if (A4EC_INPUT_MAP[mid].cmdId < controlId) lo = mid + 1;
else hi = mid - 1;
}
return nullptr;
}
// Resolve controlId -> DCS-BIOS name and send it with the given arg string. The dispatch FORM
// (the arg) is decided by the caller from the CAN frame the value arrived on (#147); the map
// carries only the name.
static void sendDcs(uint16_t controlId, const char* arg) {
const DcsBiosInputEntry* entry = lookupDcsEntry(controlId);
if (!entry) {
if (STM32Board::isDebug()) {
auto& d = STM32Board::diagSerial();
d.print(F("[BRIDGE] unknown DCS ctrl=0x")); d.println(controlId, HEX);
}
return;
}
DcsBios::sendDcsBiosMessage(entry->name, arg);
if (STM32Board::isDebug()) {
auto& d = STM32Board::diagSerial();
d.print(F("[BRIDGE] DCS -> \"")); d.print(entry->name);
d.print(F("\" \"")); d.print(arg); d.println('"');
}
}
// ABS frame (canIdEvt): value is an absolute uint16 -> set_state. Switches, selectors, pots.
static void dispatchDcsInput(uint16_t controlId, uint16_t value) {
char buf[7]; // "65535\0" + guard
snprintf(buf, sizeof(buf), "%u", (unsigned)value);
sendDcs(controlId, buf);
}
// REL frame (canIdEvtRel): value is a signed +-step -> variable_step (e.g. "+3200").
static void dispatchDcsRel(uint16_t controlId, uint16_t value) {
char buf[8]; // "-32768\0" + guard
snprintf(buf, sizeof(buf), "%+d", (int)(int16_t)value);
sendDcs(controlId, buf);
}
// DIR frame (canIdEvtDir): value is strictly +-1 -> fixed_step (INC / DEC). Anything else is a
// malformed slot -> drop + log, so a corrupt value never turns into a real cockpit step.
static void dispatchDcsDir(uint16_t controlId, uint16_t value) {
const int16_t v = (int16_t)value;
if (v == 1) sendDcs(controlId, "INC");
else if (v == -1) sendDcs(controlId, "DEC");
else if (STM32Board::isDebug()) {
auto& d = STM32Board::diagSerial();
d.print(F("[BRIDGE] bad dir ctrl=0x")); d.print(controlId, HEX);
d.print(F(" val=")); d.println(v);
}
}
static void dispatchEvtSlot(uint16_t controlId, uint16_t value) {
if (controlId == 0x0000) return; // null sentinel
if (controlId >= CTRL_ID_DCS_MIN && controlId <= CTRL_ID_DCS_MAX)
dispatchDcsInput(controlId, value);
else if (controlId < CTRL_ID_DCS_MIN)
sendHidFrame(controlId, value);
else {
if (STM32Board::isDebug()) {
auto& d = STM32Board::diagSerial();
d.print(F("[BRIDGE] drop ctrl=0x")); d.println(controlId, HEX);
}
}
}
// Relative (canIdEvtRel) slot — DCS-routed only; value reinterpreted signed -> %+d (variable_step).
static void dispatchRelSlot(uint16_t controlId, uint16_t value) {
if (controlId == 0x0000) return; // null sentinel
if (controlId >= CTRL_ID_DCS_MIN && controlId <= CTRL_ID_DCS_MAX)
dispatchDcsRel(controlId, value);
else if (STM32Board::isDebug()) {
auto& d = STM32Board::diagSerial();
d.print(F("[BRIDGE] drop rel ctrl=0x")); d.println(controlId, HEX);
}
}
// Directional (canIdEvtDir) slot — DCS-routed only; value +-1 -> INC/DEC (fixed_step).
static void dispatchDirSlot(uint16_t controlId, uint16_t value) {
if (controlId == 0x0000) return; // null sentinel
if (controlId >= CTRL_ID_DCS_MIN && controlId <= CTRL_ID_DCS_MAX)
dispatchDcsDir(controlId, value);
else if (STM32Board::isDebug()) {
auto& d = STM32Board::diagSerial();
d.print(F("[BRIDGE] drop dir ctrl=0x")); d.println(controlId, HEX);
}
}
// Update liveness state and fire alive callback on dead/unseen → alive transition.
// Returns true if this call caused a transition (caller responsible for SYNC_REQ).
static bool markNodeAlive(uint8_t nodeId, uint32_t now) {
uint8_t idx = nodeId - 1;
bool wasAlive = _nodes[idx].alive;
bool wasDead = _nodes[idx].everSeen && !_nodes[idx].alive; // timed-out, not just unseen
_nodes[idx].alive = true;
_nodes[idx].everSeen = true;
_nodes[idx].lastSeenMs = now;
if (!wasAlive) {
if (STM32Board::isDebug()) {
auto& d = STM32Board::diagSerial();
d.print(F("[BRIDGE] node=")); d.print(nodeId); d.println(F(" alive"));
}
// A previously-dead node recovered — clear WARNING once none remain dead.
if (wasDead && _deadCount > 0 && --_deadCount == 0)
STM32Board::setWarning(false);
if (_cbAlive) _cbAlive(nodeId);
#ifdef PANELBRIDGE_NODE_STATUS
emitNode(nodeId, true); // push presence on dead/unseen → alive (#86)
#endif
return true;
}
return false;
}
static void onCanRx(uint32_t canId, const uint8_t* data, uint8_t len) {
uint32_t now = millis();
// HB_1 – HB_63: update liveness; broadcast SYNC_REQ only on dead→alive transition
if (canId >= canIdHb(1) && canId <= canIdHb(MAX_NODE_ID)) {
uint8_t nodeId = (uint8_t)(canId - canIdHb(0));
#ifdef PANELBRIDGE_NODE_STATUS
if (len >= 8) memcpy(&_nodes[nodeId - 1].last, data, 8); // cache health for #86 reporting
#endif
if (markNodeAlive(nodeId, now)) broadcastSyncReq();
return;
}
// EVT_1 – EVT_63: 8-byte ControlPacketPair, dispatch each non-null slot
if (canId >= canIdEvt(1) && canId <= canIdEvt(MAX_NODE_ID)) {
if (len < 8) return;
ControlPacketPair pair;
memcpy(&pair, data, 8);
dispatchEvtSlot(pair.a.controlId, pair.a.value);
if (pair.b.controlId != 0x0000)
dispatchEvtSlot(pair.b.controlId, pair.b.value);
return;
}
// EVT_REL_1 – EVT_REL_63: 8-byte ControlPacketPair; value reinterpreted signed -> %+d (variable_step)
if (canId >= canIdEvtRel(1) && canId <= canIdEvtRel(MAX_NODE_ID)) {
if (len < 8) return;
ControlPacketPair pair;
memcpy(&pair, data, 8);
dispatchRelSlot(pair.a.controlId, pair.a.value);
if (pair.b.controlId != 0x0000)
dispatchRelSlot(pair.b.controlId, pair.b.value);
return;
}
// EVT_DIR_1 – EVT_DIR_63: 8-byte ControlPacketPair; value +-1 -> INC/DEC (fixed_step)
if (canId >= canIdEvtDir(1) && canId <= canIdEvtDir(MAX_NODE_ID)) {
if (len < 8) return;
ControlPacketPair pair;
memcpy(&pair, data, 8);
dispatchDirSlot(pair.a.controlId, pair.a.value);
if (pair.b.controlId != 0x0000)
dispatchDirSlot(pair.b.controlId, pair.b.value);
return;
}
// ECHO_1 – ECHO_63: consume TEST_SEQ RTT response, log locally
if (canId >= canIdEcho(1) && canId <= canIdEcho(MAX_NODE_ID)) {
if (!STM32Board::isDebug() || len < 8) return;
uint16_t seq; memcpy(&seq, data, 2);
uint32_t sentMs; memcpy(&sentMs, data + 2, 4);
uint32_t rtt = now - sentMs;
auto& d = STM32Board::diagSerial();
d.print(F("[BRIDGE] ECHO node=")); d.print((uint8_t)(canId - canIdEcho(0)));
d.print(F(" seq=")); d.print(seq);
d.print(F(" rtt=")); d.print(rtt); d.println(F("ms"));
return;
}
// READY_1 – READY_63: node boot-complete — mark alive, always broadcast SYNC_REQ.
// SYNC_REQ is unconditional: READY signals a fresh boot regardless of prior state.
if (canId >= canIdReady(1) && canId <= canIdReady(MAX_NODE_ID)) {
uint8_t nodeId = (uint8_t)(canId - canIdReady(0));
if (STM32Board::isDebug()) {
auto& d = STM32Board::diagSerial();
d.print(F("[BRIDGE] READY node=")); d.println(nodeId);
}
markNodeAlive(nodeId, now);
broadcastSyncReq();
return;
}
// All other IDs discarded silently
}
static void checkNodeTimeouts(uint32_t now) {
for (uint8_t i = 0; i < MAX_NODE_ID; i++) {
if (!_nodes[i].alive) continue;
if (now - _nodes[i].lastSeenMs > HB_TIMEOUT_MS) {
_nodes[i].alive = false;
uint8_t nodeId = i + 1;
if (STM32Board::isDebug()) {
auto& d = STM32Board::diagSerial();
d.print(F("[BRIDGE] node=")); d.print(nodeId); d.println(F(" dead"));
}
_deadCount++; // a tracked node died → WARNING (amber)
STM32Board::setWarning(true);
if (_cbDead) _cbDead(nodeId);
#ifdef PANELBRIDGE_NODE_STATUS
emitNode(nodeId, false); // push removal on alive → dead (#86)
#endif
}
}
}
static void onModelTimeChange(char* value) {
int32_t sec = 0;
for (uint8_t i = 0; i < 5 && value[i] != '\0'; i++) {
if (value[i] == '.') break;
if (value[i] >= '0' && value[i] <= '9')
sec = sec * 10 + (value[i] - '0');
}
if (_lastModelTimeSec < 0) {
_lastModelTimeSec = sec; // seed — no SYNC_REQ on first value
return;
}
if (sec < _lastModelTimeSec) {
STM32Board::log("[BRIDGE] DCS session change — SYNC_REQ");
broadcastSyncReq();
}
_lastModelTimeSec = sec;
}
// DCS-BIOS export listener — intercepts 0x8000–0x86FF and batches to CTRL_BCAST.
// ExportStreamListener::firstExportStreamListener (Protocol.cpp) is zero-initialized
// before any constructors run, so file-scope registration is safe.
class BridgeExportListener : public DcsBios::ExportStreamListener {
public:
BridgeExportListener() : DcsBios::ExportStreamListener(0x8000, 0x86FF) {}
void onDcsBiosWrite(unsigned int address, unsigned int data) override {
handleDcsBiosExport((uint16_t)address, (uint16_t)data);
}
};
static BridgeExportListener _exportListener;
static DcsBios::StringBuffer<5> _modelTimeBuf(CommonData_MOD_TIME_A, onModelTimeChange);
#ifdef PANELBRIDGE_NODE_STATUS
// Host roster request (#86): the client writes NODE_STATUS_REQ_ADDR on the DCS-BIOS
// export stream; PanelBridge replies with the full roster. handleDcsBiosExport()
// drops this address so it is never broadcast onto CAN.
class NodeStatusReqListener : public DcsBios::ExportStreamListener {
public:
NodeStatusReqListener()
: DcsBios::ExportStreamListener(NODE_STATUS_REQ_ADDR, NODE_STATUS_REQ_ADDR) {}
void onDcsBiosWrite(unsigned int, unsigned int) override { emitAllNodes(); }
};
static NodeStatusReqListener _nodeStatusReqListener;
#endif
} // anonymous namespace
namespace PanelBridge {
void onNodeAlive(NodeCallback cb) { _cbAlive = cb; }
void onNodeDead(NodeCallback cb) { _cbDead = cb; }
void setup() {
STM32Board::begin();
if (STM32Board::isDebug()) {
auto& d = STM32Board::diagSerial();
d.println(F("=============================="));
d.println(F(" PanelBridge NODE_ID=0"));
d.println(F("=============================="));
}
Serial.begin(250000);
CANProtocol::onStatusChange(STM32Board::onCanStatus);
CANProtocol::filterAcceptAll();
CANProtocol::onReceive(onCanRx);
CANProtocol::start();
broadcastSyncReq();
#ifdef PANELBRIDGE_NODE_STATUS
emitAllNodes(); // seed the host with the current roster (empty at cold boot) (#86)
#endif
}
void loop() {
uint32_t now = millis();
STM32Board::tick();
CANProtocol::drain();
checkNodeTimeouts(now);
// Master heartbeat (HB_0) every 500 ms. Unlike CTRL_BCAST (which only moves when a
// DCS export changes), this is an unconditional liveness beacon so PanelGroup nodes
// can detect master loss even when the sim is idle/paused.
if (now - _lastMasterHbMs >= MASTER_HB_PERIOD_MS) {
_lastMasterHbMs = now;
HeartbeatPayload hb = CANProtocol::makeHeartbeatPayload(0, 0);
CANProtocol::send(canIdHb(0), reinterpret_cast<const uint8_t*>(&hb), sizeof(hb));
}
auto& diag = STM32Board::diagSerial();
while (diag.available()) {
char c = (char)diag.read();
if (c != 'T') continue;
_testSeqNum++;
_testSeqSentMs = millis();
uint8_t payload[8] = {};
payload[0] = (uint8_t)(_testSeqNum & 0xFF);
payload[1] = (uint8_t)(_testSeqNum >> 8);
payload[2] = (uint8_t)(_testSeqSentMs & 0xFF);
payload[3] = (uint8_t)((_testSeqSentMs >> 8) & 0xFF);
payload[4] = (uint8_t)((_testSeqSentMs >> 16) & 0xFF);
payload[5] = (uint8_t)((_testSeqSentMs >> 24) & 0xFF);
// payload[6..7] = reserved = 0
CANProtocol::send(CAN_ID_TEST_SEQ, payload, 8);
if (STM32Board::isDebug()) {
diag.print(F("[BRIDGE] TEST_SEQ seq=")); diag.println(_testSeqNum);
}
}
}
} // namespace PanelBridge
#ifdef PANELBRIDGE_TEST
namespace PanelBridge {
void testDispatchEvt(uint16_t controlId, uint16_t value) {
dispatchEvtSlot(controlId, value);
}
void testDispatchRel(uint16_t controlId, uint16_t value) {
dispatchRelSlot(controlId, value);
}
void testDispatchDir(uint16_t controlId, uint16_t value) {
dispatchDirSlot(controlId, value);
}
void testHandleExport(uint16_t address, uint16_t value) {
handleDcsBiosExport(address, value);
}
#ifdef PANELBRIDGE_NODE_STATUS
void testFeedHeartbeat(uint8_t nodeId, uint8_t flags, uint16_t uptime,
uint16_t rxCount, uint16_t esr) {
HeartbeatPayload hb{ nodeId, flags, uptime, rxCount, esr };
memcpy(&_nodes[nodeId - 1].last, &hb, sizeof(hb));
markNodeAlive(nodeId, millis());
}
void testRequestNodeStatus() { emitAllNodes(); }
void testCheckTimeouts(uint32_t now) { checkNodeTimeouts(now); }
#endif // PANELBRIDGE_NODE_STATUS
} // namespace PanelBridge
#endif // PANELBRIDGE_TEST
#endif // ARDUINO_ARCH_STM32