Skip to content

SimGateway

The SimGateway is the one device the PC plugs into. It's an off-the-shelf RP2040 module that presents a single composite USB device to DCS and relays everything to the rest of the cockpit over a UART. This page covers how it works. For why a separate gateway exists at all — the STM32 USB failure that forced it — see Design Decisions D1.

Hardware

  • MCU: an off-the-shelf RP2040 module (e.g. Raspberry Pi Pico). No custom PCB.
  • USB: the single connection to the PC.
  • UART: RP2040 Serial1 / UART0 — GP0 (TX → PanelBridge RX/PA3) and GP1 (RX ← PanelBridge TX/PA2), at 250000 baud. Both sides are 3.3 V; no level shifter.

SimGateway::setup(Serial1) configures the pins, starts the UART at 250000, sets the USB identity, and enumerates the TinyUSB HID device. The baud rate matches the DCS-BIOS protocol rate exactly, so there's no buffering mismatch on the relay.

USB composite device

SimGateway enumerates as a composite CDC + HID device on one USB port:

  • CDC (serial) carries the DCS-BIOS stream, both directions.
  • HID (joystick) presents flight-control axes and buttons to the PC.

USB identity is set at runtime before the HID device starts (TinyUSB replaces the default Pico USB stack):

TinyUSBDevice.setID(0x2E8A, 0x4134);   // VID = Raspberry Pi, PID = A-4E Skyhawk
TinyUSBDevice.setManufacturerDescriptor("OpenSkyhawk");
TinyUSBDevice.setProductDescriptor("A-4E Skyhawk");
Serial.setStringDescriptor("A-4E Skyhawk DCS-BIOS"); // CDC interface name (iInterface)

The product string names the HID joystick, but the CDC serial interface advertises its own name (iInterface) — A-4E Skyhawk DCS-BIOS — so host tooling such as OpenSkyhawk Client can match the port by name, not just VID/PID + CDC class.

HID device profile

The HID descriptor is built on the Adafruit TinyUSB library and frozen:

Capability Value
Axes 8 — X, Y, Z, Rx, Ry, Rz, Slider, Dial (16-bit signed, ±32767)
Buttons 128 (1 bit each)
Hat switches 4 (4-bit nibble each; 0xF = centered)
Report size 34 bytes

This covers all 7 planned OpenSkyhawk axes (Roll, Pitch, Throttle, Rudder, Brake L, Brake R, Zoom) with one spare, plus headroom on buttons and hats. Validated against DCS World (DirectInput) as a full DIJOYSTATE2 match — 8 axes, 128 buttons, 4 POV hats, no limits exceeded.

Never grow the HID report past 64 bytes

TinyUSB on the RP2040 hardcodes CFG_TUD_HID_EP_BUFSIZE = 64; build-flag overrides are silently ignored. A report over 64 bytes makes sendReport() return false and drop silently — no error, no crash. The production report is 34 bytes; keep it well under 64.

The relay contract

This is the heart of SimGateway, and it's deliberately dumb:

  • Every byte from USB CDC is forwarded to the UART immediately.
  • Every UART byte that is not part of a HID frame is forwarded to USB CDC immediately.
  • SimGateway never buffers, parses, or delays DCS-BIOS bytes, and never runs the DCS-BIOS library. PanelBridge therefore sees the stream with USB-CDC latency only.

The two streams share one UART because they can't collide: DCS-BIOS text is pure printable ASCII + LF (every byte ≤ 0x7F), while HID frames start with the magic bytes 0xAA 0x55 (both have bit 7 set).

This "dumb relay" is also why node-status reporting (#86) needs no SimGateway change: PanelBridge reports connected PanelGroup nodes to the host over DCS-BIOS itself — _NODE_STATUS ASCII command messages downstream, and a roster request as a DCS-BIOS export write to reserved address 0x86FE upstream. Both ride the existing streams and transit SimGateway transparently; PanelBridge owns the feature.

controlId routing logic

Incoming HID frames (PanelBridge → SimGateway) are fixed 6-byte little-endian:

Bytes Field
0–1 Magic 0xAA 0x55
2–3 controlId (uint16 LE)
4–5 value (uint16 LE)

SimGateway demultiplexes the UART byte stream:

  • Byte ≤ 0x7F → DCS-BIOS byte → forward to USB CDC.
  • Byte 0xAA followed by 0x55 → HID frame: read 4 more bytes, dispatch by controlId.
  • Byte 0xAA not followed by 0x55 → not a frame: forward both bytes and resume scanning (self-healing resync at the next byte boundary).

HID controls are owned entirely here — PanelBridge wraps them but never interprets them. The controlId ranges SimGateway dispatches:

Range Type Handler
0x00100x001F Axes HIDAxis
0x00200x002F Hat switches HIDHatSwitch
0x00300x00AF Buttons HIDButton

HIDAxis maps the incoming 0–65535 value to ±32767 internally; HIDButton treats value != 0 as pressed; HIDHatSwitch reads a direction nibble (0 = centered, 1–8 = N…NW).

Dispatch and startup behaviour

Each SimGateway::loop() iteration drains all pending HID frames, applies them to the axis / button / hat objects, and then calls OsJoystick.send() once to flush the HID report. Sending once per drain cycle — not once per frame — keeps the HID report rate predictable and avoids redundant USB packets.

At startup, SimGateway::setup() owns all of it: USB identity, GP0/GP1 pin configuration, Serial1.begin(250000), and TinyUSB HID enumeration. The sketch only declares its HIDAxis / HIDButton / HIDHatSwitch objects and calls setup() / loop() — DCS-BIOS relay is automatic and needs no declarations.

Status LEDs

The gateway carries two board-mounted status LEDs on the Gateway_Bridge board — GREEN on GP2, RED on GP3 — so you can read its USB / data / fault state at a glance without a host. They are active-high (HIGH = on) and driven by a non-blocking millis() animator ticked from SimGateway::loop(); the RP2040 module's onboard WS2812 is not used. State priority runs highest to lowest — the topmost active row wins:

Priority State What it means LED
1 (highest) FAULT A UART hardware error on the PanelBridge link (overrun / framing / parity) Red, fast blink (4 Hz)
2 NO_HOST USB not enumerated — no PC, or the cable was unplugged Red, solid
3 STREAMING DCS-BIOS data flowing from the PC (seen within the last ~500 ms) Green, solid
4 USB_IDLE USB connected, but no recent DCS-BIOS data Green, slow blink (1 Hz)
5 (lowest) INIT Just booted, USB not yet mounted (brief) Red, slow blink

When the fault light clears

FAULT is latched, not momentary: it stays on for at least ~2 s (so a single glitch is visible) and then clears only once clean PanelBridge data resumes on the UART — an error-free byte received after the fault. A fault on an otherwise-silent link therefore stays lit until traffic returns error-free, rather than timing out on its own. The error is read straight from the RP2040's uart0 PL011 error register (RSR), because the arduino-pico SerialUART driver doesn't surface overrun/framing flags any other way.

Active-high, separate power domain

Both LEDs are active-high (HIGH = lit) and sit on the RP2040 module's own 3V3 LDO, isolated from the STM32 rail — so they reflect the gateway's state even if the STM32 side is unpowered or held in reset. The two BRIDGE LEDs next to them (PB14/PB15) are driven separately by the STM32 firmware.