File SimGateway.cpp
File List > Firmware > Libraries > SimGateway > SimGateway.cpp
Go to the documentation of this file
#ifdef ARDUINO_ARCH_RP2040
#include "SimGateway.h"
// ── TinyUSB HID backend (production builds only) ──────────────────────────────
//
// SIMGATEWAY_TEST builds substitute no-op stubs below. The #ifndef guard prevents
// Adafruit_TinyUSB.h from being included in test builds, keeping tests free of USB
// enumeration side effects.
#ifndef SIMGATEWAY_TEST
#include <Adafruit_TinyUSB.h>
namespace {
// HID report: 128 buttons (16 bytes) + 4 hat switches (4-bit each = 2 bytes) + 8 axes (16 bytes)
struct __attribute__((packed)) HIDReport {
uint8_t buttons[16]; // 128 × 1-bit buttons (button 0 = bit 0 of byte 0)
uint8_t hats[2]; // 4 × 4-bit hat values; nibble value ≥ 8 = null / centered
int16_t axes[8]; // X, Y, Z, Rx, Ry, Rz, Slider, Dial
};
static const uint8_t desc_hid_report[] = {
// Joystick application collection
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x04, // Usage (Joystick)
0xA1, 0x01, // Collection (Application)
// 128 Buttons (1-bit each, 16 bytes total)
0x05, 0x09, // Usage Page (Button)
0x19, 0x01, // Usage Minimum (1)
0x29, 0x80, // Usage Maximum (128)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x80, // Report Count (128)
0x81, 0x02, // Input (Data, Variable, Absolute)
// 4 Hat switches (4-bit each, 2 bytes total)
// Logical 0-7 = N/NE/E/SE/S/SW/W/NW; value ≥ 8 = null (centered).
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x39, // Usage (Hat Switch) — hat 0
0x09, 0x39, // Usage (Hat Switch) — hat 1
0x09, 0x39, // Usage (Hat Switch) — hat 2
0x09, 0x39, // Usage (Hat Switch) — hat 3
0x15, 0x00, // Logical Minimum (0)
0x25, 0x07, // Logical Maximum (7)
0x35, 0x00, // Physical Minimum (0 degrees)
0x46, 0x3B, 0x01, // Physical Maximum (315 degrees)
0x65, 0x14, // Unit (Degrees)
0x75, 0x04, // Report Size (4)
0x95, 0x04, // Report Count (4)
0x81, 0x42, // Input (Data, Variable, Absolute, Null State)
// 8 Axes: X, Y, Z, Rx, Ry, Rz, Slider, Dial (16-bit signed each, 16 bytes total)
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x30, // Usage (X)
0x09, 0x31, // Usage (Y)
0x09, 0x32, // Usage (Z)
0x09, 0x33, // Usage (Rx)
0x09, 0x34, // Usage (Ry)
0x09, 0x35, // Usage (Rz)
0x09, 0x36, // Usage (Slider)
0x09, 0x37, // Usage (Dial)
0x16, 0x00, 0x80, // Logical Minimum (-32768)
0x26, 0xFF, 0x7F, // Logical Maximum (32767)
0x75, 0x10, // Report Size (16)
0x95, 0x08, // Report Count (8)
0x81, 0x02, // Input (Data, Variable, Absolute)
0xC0 // End Collection
};
static HIDReport _hidReport = {};
static Adafruit_USBD_HID _usbHid(desc_hid_report, sizeof(desc_hid_report),
HID_ITF_PROTOCOL_NONE, 2, false);
static void _hidBegin() {
// All hat nibbles start centered (null state = 0xF per nibble).
_hidReport.hats[0] = 0xFF;
_hidReport.hats[1] = 0xFF;
_usbHid.begin();
// Block until the host enumerates (2 s timeout: handles benchtop use without USB host).
uint32_t t = millis();
while (!TinyUSBDevice.mounted() && (millis() - t) < 2000) delay(1);
}
static void _hidSetAxis(uint8_t axisIndex, int16_t value) {
if (axisIndex < 8) _hidReport.axes[axisIndex] = value;
}
static void _hidSetButton(uint8_t buttonIndex, bool pressed) {
if (buttonIndex >= 128) return;
uint8_t byte_idx = buttonIndex / 8;
uint8_t bit_mask = 1u << (buttonIndex % 8);
if (pressed) _hidReport.buttons[byte_idx] |= bit_mask;
else _hidReport.buttons[byte_idx] &= ~bit_mask;
}
static void _hidSetHat(uint8_t hatIndex, uint8_t direction) {
if (hatIndex >= 4) return;
// direction: 0=center→0xF, 1=N→0, 2=NE→1, …, 8=NW→7; >8→0xF (center)
uint8_t hid_val = (direction == 0 || direction > 8) ? 0xF : (direction - 1);
uint8_t byte_idx = hatIndex / 2;
uint8_t nibble_idx = hatIndex % 2;
if (nibble_idx == 0) _hidReport.hats[byte_idx] = (_hidReport.hats[byte_idx] & 0xF0) | (hid_val & 0x0F);
else _hidReport.hats[byte_idx] = (_hidReport.hats[byte_idx] & 0x0F) | ((hid_val & 0x0F) << 4);
}
static void _hidSend() {
if (_usbHid.ready()) _usbHid.sendReport(0, &_hidReport, sizeof(_hidReport));
}
} // anonymous namespace
#else // SIMGATEWAY_TEST — no-op HID stubs
namespace {
static void _hidBegin() {}
static void _hidSetAxis(uint8_t, int16_t) {}
static void _hidSetButton(uint8_t, bool) {}
static void _hidSetHat(uint8_t, uint8_t) {}
static void _hidSend() {}
} // anonymous namespace
#endif // SIMGATEWAY_TEST
// ── Test capture globals ───────────────────────────────────────────────────────
#ifdef SIMGATEWAY_TEST
uint16_t _sgtest_lastControlId = 0;
uint16_t _sgtest_lastValue = 0;
uint8_t _sgtest_dispatchCount = 0;
#endif
// ── HIDAxis ───────────────────────────────────────────────────────────────────
namespace OpenSkyhawk {
HIDAxis* HIDAxis::_head = nullptr;
HIDAxis::HIDAxis(uint16_t controlId, uint8_t axisIndex)
: _next(nullptr), _controlId(controlId), _axisIndex(axisIndex)
{
_next = _head;
_head = this;
}
HIDAxis* HIDAxis::head() { return _head; }
uint16_t HIDAxis::controlId() const { return _controlId; }
HIDAxis* HIDAxis::next() const { return _next; }
void HIDAxis::dispatch(uint16_t value) {
_hidSetAxis(_axisIndex, (int16_t)(value - 32768));
}
// ── HIDButton ─────────────────────────────────────────────────────────────────
HIDButton* HIDButton::_head = nullptr;
HIDButton::HIDButton(uint16_t controlId, uint8_t buttonIndex)
: _next(nullptr), _controlId(controlId), _buttonIndex(buttonIndex)
{
_next = _head;
_head = this;
}
HIDButton* HIDButton::head() { return _head; }
uint16_t HIDButton::controlId() const { return _controlId; }
HIDButton* HIDButton::next() const { return _next; }
void HIDButton::dispatch(uint16_t value) {
_hidSetButton(_buttonIndex, value != 0);
}
// ── HIDHatSwitch ──────────────────────────────────────────────────────────────
HIDHatSwitch* HIDHatSwitch::_head = nullptr;
HIDHatSwitch::HIDHatSwitch(uint16_t controlId, uint8_t hatIndex)
: _next(nullptr), _controlId(controlId), _hatIndex(hatIndex)
{
_next = _head;
_head = this;
}
HIDHatSwitch* HIDHatSwitch::head() { return _head; }
uint16_t HIDHatSwitch::controlId() const { return _controlId; }
HIDHatSwitch* HIDHatSwitch::next() const { return _next; }
void HIDHatSwitch::dispatch(uint16_t value) {
_hidSetHat(_hatIndex, (uint8_t)(value > 8 ? 0 : value));
}
} // namespace OpenSkyhawk
// ── Internal parser ───────────────────────────────────────────────────────────
namespace {
enum class ParserState : uint8_t { IDLE, GOT_AA, IN_FRAME };
SerialUART* _uart = nullptr;
ParserState _state = ParserState::IDLE;
uint8_t _frameBuf[4];
uint8_t _framePos = 0;
#ifdef SIMGATEWAY_TEST
constexpr size_t SGTEST_CDC_CAPTURE_CAPACITY = 64;
uint8_t _sgtest_cdcBytes[SGTEST_CDC_CAPTURE_CAPACITY];
size_t _sgtest_cdcCount = 0;
bool _sgtest_cdcOverflow = false;
#endif
void _writeCdc(uint8_t b) {
#ifdef SIMGATEWAY_TEST
if (_sgtest_cdcCount < SGTEST_CDC_CAPTURE_CAPACITY) {
_sgtest_cdcBytes[_sgtest_cdcCount++] = b;
} else {
_sgtest_cdcOverflow = true;
}
#endif
Serial.write(b);
}
// Process one UART byte through the state machine.
// Returns true if a HID setter fired this byte.
bool _processByte(uint8_t b) {
switch (_state) {
case ParserState::IDLE:
if (b == 0xAA) {
_state = ParserState::GOT_AA;
} else {
_writeCdc(b); // DCS-BIOS byte → CDC
}
return false;
case ParserState::GOT_AA:
if (b == 0x55) {
_framePos = 0;
_state = ParserState::IN_FRAME;
} else {
// Resync: 0xAA was not magic; forward both bytes and resume.
_writeCdc(0xAA);
_writeCdc(b);
_state = ParserState::IDLE;
}
return false;
case ParserState::IN_FRAME: {
_frameBuf[_framePos++] = b;
if (_framePos < 4) return false;
uint16_t controlId = (uint16_t)_frameBuf[0] | ((uint16_t)_frameBuf[1] << 8);
uint16_t value = (uint16_t)_frameBuf[2] | ((uint16_t)_frameBuf[3] << 8);
bool fired = false;
for (auto* a = OpenSkyhawk::HIDAxis::head(); a; a = a->next()) {
if (a->controlId() == controlId) { a->dispatch(value); fired = true; }
}
for (auto* btn = OpenSkyhawk::HIDButton::head(); btn; btn = btn->next()) {
if (btn->controlId() == controlId) { btn->dispatch(value); fired = true; }
}
for (auto* hat = OpenSkyhawk::HIDHatSwitch::head(); hat; hat = hat->next()) {
if (hat->controlId() == controlId) { hat->dispatch(value); fired = true; }
}
#ifdef SIMGATEWAY_TEST
if (fired) {
_sgtest_lastControlId = controlId;
_sgtest_lastValue = value;
_sgtest_dispatchCount++;
}
#endif
_state = ParserState::IDLE;
return fired;
}
}
return false;
}
} // anonymous namespace
// ── Status LED state machine ──────────────────────────────────────────────────
//
// Drives the two board-mounted SimGateway status LEDs (RED = GP3, GREEN = GP2,
// active-high) with a non-blocking millis() animator, ticked from loop().
//
// The state-selection + animation-phase logic is PURE: it takes `now` as a
// parameter, touches no hardware, and never calls millis() internally — so
// SIMGATEWAY_TEST builds unit-test it with injected inputs (statusInject /
// statusFaultStep). Only _applyLed() touches GPIO, and the TinyUSBDevice.mounted()
// poll + PL011 RSR read live behind #ifndef SIMGATEWAY_TEST.
#ifndef SIMGATEWAY_TEST
#include "hardware/structs/uart.h" // uart0_hw — PL011 registers
#include "hardware/regs/uart.h" // UART_UARTRSR_*_BITS error-flag masks
#endif
namespace {
using SimGateway::Anim;
using SimGateway::LedState;
enum class LedColor : uint8_t { NONE, RED, GREEN };
constexpr uint8_t PIN_LED_GREEN = 2; // GP2
constexpr uint8_t PIN_LED_RED = 3; // GP3
constexpr uint32_t STREAM_WINDOW_MS = 500; // CDC-RX recency → STREAMING
constexpr uint32_t INIT_WINDOW_MS = 2000; // boot grace before NO_HOST if never mounted
constexpr uint32_t FAULT_MIN_HOLD_MS = 2000; // FAULT visibility floor (~8 fast flashes)
constexpr uint32_t SLOW_PERIOD_MS = 1000; // 1 Hz
constexpr uint32_t FAST_PERIOD_MS = 250; // 4 Hz
constexpr uint32_t ALT_PERIOD_MS = 500; // reserved
constexpr bool ENABLE_TRAFFIC_PULSE = false; // STREAMING is plain SOLID per AC
struct StatusInputs {
uint32_t now;
bool mounted;
uint32_t lastCdcRxMs;
bool everMounted;
bool faultActive;
};
struct LedOutput {
LedState state;
LedColor color;
Anim anim;
bool redOn;
bool greenOn;
};
// Sampled signal state (updated by loop() / statusTick()).
uint32_t _lastCdcRxMs = 0;
bool _uartMovedThisTick = false;
bool _everMounted = false;
// FAULT latch state.
bool _faultLatched = false;
bool _faultEverSeen = false;
uint32_t _lastFaultMs = 0;
uint32_t _lastUartRxMs = 0;
// Pure: pick the active state from sampled inputs (priority high → low).
LedState _selectState(const StatusInputs& in) {
if (in.faultActive) return LedState::FAULT;
if (!in.mounted && (in.everMounted || in.now >= INIT_WINDOW_MS)) return LedState::NO_HOST;
if (in.mounted && (uint32_t)(in.now - in.lastCdcRxMs) <= STREAM_WINDOW_MS) return LedState::STREAMING;
if (in.mounted) return LedState::USB_IDLE;
return LedState::INIT;
}
// Pure: map a state to its colour + animation.
void _animFor(LedState s, LedColor& color, Anim& anim) {
switch (s) {
case LedState::FAULT: color = LedColor::RED; anim = Anim::FAST; break;
case LedState::NO_HOST: color = LedColor::RED; anim = Anim::SOLID; break;
case LedState::STREAMING: color = LedColor::GREEN; anim = ENABLE_TRAFFIC_PULSE ? Anim::PULSE : Anim::SOLID; break;
case LedState::USB_IDLE: color = LedColor::GREEN; anim = Anim::SLOW; break;
case LedState::INIT: color = LedColor::RED; anim = Anim::SLOW; break;
}
}
// Pure: on/off for an animation at time `now` (50% duty for blinks).
bool _animOn(Anim anim, uint32_t now) {
switch (anim) {
case Anim::OFF: return false;
case Anim::SOLID: return true;
case Anim::SLOW: return (now % SLOW_PERIOD_MS) < (SLOW_PERIOD_MS / 2);
case Anim::FAST: return (now % FAST_PERIOD_MS) < (FAST_PERIOD_MS / 2);
case Anim::ALT: return (now % ALT_PERIOD_MS) < (ALT_PERIOD_MS / 2);
case Anim::PULSE: return true; // baseline solid; PULSE off-blip disabled by default
}
return false;
}
// Pure: resolve full LED output (state + colour + anim + pin levels) for `now`.
LedOutput _resolveStatus(const StatusInputs& in) {
LedOutput out{};
out.state = _selectState(in);
_animFor(out.state, out.color, out.anim);
bool on = _animOn(out.anim, in.now);
out.redOn = (out.color == LedColor::RED) && on;
out.greenOn = (out.color == LedColor::GREEN) && on;
return out;
}
// Update the FAULT latch from this tick's signals. Shared by production sampling
// (statusTick) and the SIMGATEWAY_TEST statusFaultStep() hook.
// rsrError — a PL011 error bit was set this tick.
// uartRxMoved — ≥1 error-free byte was read from the UART this tick.
// FAULT latches on any error and re-stamps while errors persist; it clears only
// when (a) ≥ FAULT_MIN_HOLD_MS has elapsed since the last error AND (b) an
// error-free byte arrived on the UART *after* that error. A silent bus therefore
// holds FAULT until clean data resumes. Returns the resolved faultActive flag.
bool _updateFaultLatch(uint32_t now, bool rsrError, bool uartRxMoved) {
if (rsrError) {
_faultEverSeen = true;
_faultLatched = true;
_lastFaultMs = now;
} else if (uartRxMoved) {
_lastUartRxMs = now;
}
if (_faultLatched &&
(uint32_t)(now - _lastFaultMs) >= FAULT_MIN_HOLD_MS &&
(int32_t)(_lastUartRxMs - _lastFaultMs) > 0) {
_faultLatched = false;
}
return _faultLatched;
}
#ifdef SIMGATEWAY_TEST
bool _sgtest_redLevel = false;
bool _sgtest_greenLevel = false;
StatusInputs _sgtest_inputs = {};
LedOutput _sgtest_out = {};
#endif
// The only function that touches the LED GPIO (active-high). In test builds it
// captures the resolved levels instead of writing pins.
void _applyLed(const LedOutput& out) {
#ifdef SIMGATEWAY_TEST
_sgtest_redLevel = out.redOn;
_sgtest_greenLevel = out.greenOn;
#else
digitalWrite(PIN_LED_RED, out.redOn ? HIGH : LOW);
digitalWrite(PIN_LED_GREEN, out.greenOn ? HIGH : LOW);
#endif
}
} // anonymous namespace
// ── SimGateway ────────────────────────────────────────────────────────────────
namespace SimGateway {
void setup(SerialUART& uart, uint8_t txPin, uint8_t rxPin) {
_uart = &uart;
#ifndef SIMGATEWAY_TEST
// USB identity must be set before TinyUSB begins enumerating.
TinyUSBDevice.setID(0x2E8A, 0x4134);
TinyUSBDevice.setManufacturerDescriptor("OpenSkyhawk");
TinyUSBDevice.setProductDescriptor("A-4E Skyhawk");
Serial.begin(250000); // start Adafruit_USBD_CDC; required before available()/write() work
// (baud arg ignored by USB CDC; set to nominal 250000 to match docs)
// Name the CDC interface (iInterface) so the serial port is identifiable by name, not just
// VID/PID + CDC class. Must follow Serial.begin(), which otherwise leaves the library
// default "TinyUSB Serial".
Serial.setStringDescriptor("A-4E Skyhawk DCS-BIOS");
#endif
_uart->setTX(txPin);
_uart->setRX(rxPin);
_uart->begin(250000);
_hidBegin(); // no-op in SIMGATEWAY_TEST builds
statusLedBegin(); // configure GP2/GP3 status LEDs (both off)
#ifndef SIMGATEWAY_TEST
Serial.println(F("=============================="));
Serial.println(F(" SimGateway"));
Serial.println(F("=============================="));
#endif
}
void loop() {
// 1. Forward CDC → UART (PC DCS-BIOS stream to PanelBridge).
// Bytes moving here is the "host is talking" signal that drives STREAMING.
bool cdcMoved = false;
while (Serial.available()) {
_uart->write(Serial.read());
cdcMoved = true;
}
if (cdcMoved) _lastCdcRxMs = millis();
// 2. Drain UART; HID frames dispatched, DCS-BIOS bytes forwarded to CDC.
// UART RX moving (error-free) is the proof-of-recovery signal for FAULT.
bool anyFired = false;
while (_uart->available()) {
anyFired |= _processByte(_uart->read());
_uartMovedThisTick = true;
}
// 3. Flush one HID report if any setter fired this iteration
if (anyFired) _hidSend();
// 4. Advance the status-LED state machine (non-blocking). Reads the uart0 RSR
// after the drain so this tick's UART errors are visible.
statusTick();
}
void statusLedBegin() {
#ifndef SIMGATEWAY_TEST
pinMode(PIN_LED_RED, OUTPUT);
pinMode(PIN_LED_GREEN, OUTPUT);
digitalWrite(PIN_LED_RED, LOW);
digitalWrite(PIN_LED_GREEN, LOW);
#endif
}
void statusTick() {
uint32_t now = millis();
#ifndef SIMGATEWAY_TEST
bool mounted = TinyUSBDevice.mounted();
// Read the PL011 sticky error flags. Serial1 == uart0 on this board
// (SerialUART.cpp: `#define __SERIAL1_DEVICE uart0`); a future board wiring the
// status UART to uart1 must change STATUS_UART_HW. Clear on every read
// (write-to-clear via the ECR alias) or a single overrun pins FAULT forever.
auto* const STATUS_UART_HW = uart0_hw;
constexpr uint32_t RSR_ERR = UART_UARTRSR_OE_BITS | UART_UARTRSR_BE_BITS |
UART_UARTRSR_PE_BITS | UART_UARTRSR_FE_BITS;
bool rsrError = (STATUS_UART_HW->rsr & RSR_ERR) != 0;
if (rsrError) STATUS_UART_HW->rsr = 0;
#else
bool mounted = false; // test builds drive the state machine via the hooks below
bool rsrError = false;
#endif
if (mounted) _everMounted = true;
bool faultActive = _updateFaultLatch(now, rsrError, _uartMovedThisTick);
_uartMovedThisTick = false;
StatusInputs in{ now, mounted, _lastCdcRxMs, _everMounted, faultActive };
_applyLed(_resolveStatus(in));
}
#ifdef SIMGATEWAY_TEST
bool feedByte(uint8_t b) { return _processByte(b); }
void resetParser() { _state = ParserState::IDLE; _framePos = 0; }
void resetCdcCapture() { _sgtest_cdcCount = 0; _sgtest_cdcOverflow = false; }
size_t cdcCaptureCount() { return _sgtest_cdcCount; }
uint8_t cdcCaptureByte(size_t index) {
return (index < _sgtest_cdcCount) ? _sgtest_cdcBytes[index] : 0;
}
bool cdcCaptureOverflow() { return _sgtest_cdcOverflow; }
// ── Status-LED test hooks ─────────────────────────────────────────────────────
void statusInject(uint32_t now, bool mounted, uint32_t lastCdcRxMs, bool faultActive) {
if (mounted) _everMounted = true;
_sgtest_inputs = StatusInputs{ now, mounted, lastCdcRxMs, _everMounted, faultActive };
}
void statusResolve() {
_sgtest_out = _resolveStatus(_sgtest_inputs);
_applyLed(_sgtest_out);
}
bool statusFaultStep(uint32_t now, bool rsrError, bool uartRxMoved) {
return _updateFaultLatch(now, rsrError, uartRxMoved);
}
LedState statusState() { return _sgtest_out.state; }
Anim statusAnim() { return _sgtest_out.anim; }
bool statusRedLevel() { return _sgtest_redLevel; }
bool statusGreenLevel() { return _sgtest_greenLevel; }
void statusResetForTest() {
_everMounted = false;
_faultLatched = false;
_faultEverSeen = false;
_lastFaultMs = 0;
_lastUartRxMs = 0;
_lastCdcRxMs = 0;
_uartMovedThisTick = false;
_sgtest_redLevel = false;
_sgtest_greenLevel = false;
_sgtest_inputs = StatusInputs{};
_sgtest_out = LedOutput{};
}
#endif
} // namespace SimGateway
#endif // ARDUINO_ARCH_RP2040