Modernization of the legacy vJoy-based RIO cockpit interface for Win10/11, removing the vJoy dependency in favor of a custom VHF/UMDF HID driver, rewritten in C#/.NET 8 as a background tray app with per-game profiles. - Reorganize: legacy C++ -> legacy/, cockpit art -> docs/reference/ - RioJoy.sln: src/RioJoy.Core (lib) + src/RioJoy.Tray (tray app), net8.0-windows x64 - driver/ placeholder for the RioGamepad WDK driver - docs/PLAN.md (7-phase plan; profiles + serial-yield model) - docs/PROTOCOL.md (RIO wire format + iRIO input-map reference) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
9.4 KiB
RIO serial protocol & iRIO input map
Reverse-engineered from the legacy implementation in
legacy/riovjoy2.cpp. This is the authoritative
reference for the Phase 2 C# port. Line references point into the legacy file.
⚠️ Where the legacy code looks buggy, this document describes what it actually does and flags the suspect behavior. Port the documented behavior, then decide intentionally whether to fix the flagged items.
1. Physical link
- RS-232, 9600 baud, 8 data bits, no parity, 1 stop bit (8N1).
- The RIO is on one COM port (legacy hard-codes COM1,
OpenConnection). - An optional plasma / VFD text display is on a second COM port (legacy
hard-codes COM2,
CPlasma). - DTR is pulsed on open (SETDTR, 50 ms, CLRDTR) as a board reset/handshake.
- Both ports must be configurable in the modern app (no hard-coded COM numbers).
Serial-port ownership (critical)
The native games (Firestorm, Red Planet) talk to the RIO directly over this same port. Only one process can own it at a time, so the modern app must release the COM port and go dormant whenever a native game is running, and re-acquire it when a supported non-native game is active. See PLAN.md §Profiles.
2. Framing
Every message is a packet:
[ command byte ] [ payload byte ... ] [ checksum byte ]
0x80-0x8C high bit clear 7-bit checksum
- Command byte: high bit set (
0x80+). Value identifies the message; see the command table. The valid range is0x80 .. 0x80 + NwhereNis the command count. - Payload length is fixed per command (the length table below), excluding the command byte and checksum byte.
- Payload bytes always have the high bit clear (7-bit data). Receiving a
byte with the high bit set mid-packet means a framing error → abort the
current packet and resync (
ReadCommBlock). - Checksum byte:
(sum of (b & 0x7F) for each byte in command+payload) & 0x7F(build:SendCommand, verify:ReadCommBlock).
Control characters (single bytes, outside packet framing)
| Name | Byte | Meaning |
|---|---|---|
ACK |
0xFC | Packet accepted |
NAK |
0xFD | Packet rejected (resend) |
RESTART |
0xFE | Restart / also used as an in-payload "invalid" sentinel |
IDLE |
0xFF | Idle |
After a valid received packet, the PC replies ACK (when its output queue is
empty). For button packets with a bad checksum it replies NAK; for other
packet types with a bad checksum it still ACKs.
⚠️ The legacy receive path force-accepts every packet regardless of checksum (
static bool s_bool = true;at riovjoy2.cpp#L887), so checksum verification is effectively disabled inbound. Decide whether to enable real verification in the port.
3. Command table
Enum base 0x80 (RIOCommand); payload lengths
from g_baRIOLengthsA.
| Code | Name | Dir | Payload len | Payload |
|---|---|---|---|---|
| 0x80 | CheckRequest | PC→RIO | 0 | — |
| 0x81 | VersionRequest | PC→RIO | 0 | — |
| 0x82 | AnalogRequest | PC→RIO | 0 | — |
| 0x83 | ResetRequest | PC→RIO | 1 | target (see §Reset) |
| 0x84 | LampRequest | PC→RIO | 2 | lamp#, state |
| 0x85 | CheckReply | RIO→PC | 2 | statusType, number |
| 0x86 | VersionReply | RIO→PC | 2 | major, minor |
| 0x87 | AnalogReply | RIO→PC | 10 | 5 axes × (low, high) |
| 0x88 | ButtonPressed | RIO→PC | 1 | index (0x00–0x47) |
| 0x89 | ButtonReleased | RIO→PC | 1 | index (0x00–0x47) |
| 0x8A | KeyPressed | RIO→PC | 2 | pad, index |
| 0x8B | KeyReleased | RIO→PC | 2 | pad, index |
| 0x8C | TestModeChange | RIO→PC | 1 | mode (0 = exit) |
Reset targets (ResetRequest payload)
0 = general/all, 1 = throttle, 2 = left pedal, 3 = right pedal,
4 = vertical joystick (Y), 5 = horizontal joystick (X)
(ResetThrottle etc.).
Lamp state byte (LampRequest)
Composed of flash + two brightness fields
(LampState):
- Flash:
solid=0, flashSlow=1, flashMed=2, flashFast=3 - Field 1:
Off=0x00, Dim=0x04, Bright=0x0C - Field 2:
Off=0x00, Dim=0x10, Bright=0x30 - Common combos:
SolidOff=0x00,SolidDim=0x14,SolidBright=0x3C
4. Analog values
AnalogReply carries 5 axes, each 2 bytes (low then high), in this order:
- Throttle, 2. LeftPedal, 3. RightPedal, 4. JoystickY, 5. JoystickX
(
AnalogEvent).
Each axis is a 14-bit signed value packed as two 7-bit bytes
(CombinePair):
raw = (low & 0x7F) | (high << 7); // 14 bits
if (raw & 0x2000) raw |= ~0x3FFF; // sign-extend bit 13
If any payload byte equals 0xFE, the reply is treated as invalid and ignored.
Polling & recovery
The legacy watch thread requests an analog update on a ~55 ms timeout
(CommWatchProc). If >5 s elapse with no
AnalogReply, it issues a general reset to recover
(riovjoy2.cpp#L1096).
Axis → virtual-device mapping (range 0..32766, center 16383)
Calibration/deadzone math lives in
UpdateJoystick,
UpdateThrottle,
UpdatePadal:
| RIO axis | Virtual axis | Notes |
|---|---|---|
| JoystickX | X | auto-ranging rate from observed min/max, ±5 deadzone |
| JoystickY | Y | same |
| Throttle | Z | range ±800, deadzone 50, ratchet via g_ThrottleResult |
| LeftPedal | Rx | range ±500, deadzone 10 (only when enableZR off) |
| RightPedal | Ry | same |
| (computed) | Rz | rudder = 16383 - leftPedal/2 + rightPedal/2 (when enableZR on) |
Per-axis invert flags (invertX/Y/Z/XR/YR/ZR) and enableZR come from config.
5. Digital inputs → the iRIO map
The RIO reports button/keypad events by address; the app translates each
address to a Windows action via a 112-entry table iRIO[]
(load, decode Press_V2).
Address space (index into iRIO)
| Range | Source |
|---|---|
0x00–0x47 (0–71) |
the 72 digital button inputs (ButtonPressed/Released index) |
0x50–0x5F (80–95) |
keypad pad 0 (KeyPressed with pad=0, app adds 0x50) |
0x60–0x6F (96–111) |
keypad pad 1 (KeyPressed with pad=1, app adds 0x60) |
(0x48–0x4F unused.) Keypad offset logic:
KeypadEvent.
Per-entry 16-bit encoding
Each iRIO[addr] is a 16-bit word. High byte = routing flags, low byte =
payload:
| Bit | Name | Meaning |
|---|---|---|
| 0x8000 | hasLamp | This input drives a lighted button (lamp feedback on press/release) |
| 0x4000 | mouse | Route payload to mouse |
| 0x2000 | hat | Route payload to POV hat |
| 0x1000 | joy | Route payload to joystick button |
| 0x0800 | extended | Keyboard: set KEYEVENTF_EXTENDEDKEY |
| 0x0400 | alt | Keyboard: hold ALT around the key |
| 0x0200 | ctrl | Keyboard: hold CTRL around the key |
| 0x0100 | shift | Keyboard: hold SHIFT around the key |
| 0x00FF | value | VK code / button# / hat direction / mouse action |
Routing precedence (Press_V2 / Release_V2)
- If none of joy/hat/mouse set → keyboard: press modifiers, then send
the key by scancode (
MapVirtualKey(VK, MAPVK_VK_TO_VSC)+KEYEVENTF_SCANCODE, plus extended flag). VK list:legacy/hbb_vkey.cpp. - If joy && hat && mouse all set (
0x7000) → it's a RIO command, not an output: callsRIOcmd(value)(axis resets, recalibrate, status, etc. — seeRIOcmd). - Else if joy → set joystick button
value. - Else if hat → set POV to direction
value(release → center). - Else if mouse → mouse action
value.
Lamp feedback: on press SolidBright, on release SolidDim; lamps initialized
to SolidDim at startup for entries with hasLamp.
Mouse action codes (low byte when mouse set)
0=move up, 1=move right, 2=move down, 3=move left, 4=left click,
5=right click (Mouse).
⚠️ The legacy move directions mix
dx/dyoddly (e.g. "up" setsdx=-50). Treat the codes as the contract; fix the actual movement deltas in the port.
6. Lighted-button & board name tables
Human-readable lamp and board names (useful for the config UI and diagnostics)
are in GetLampName and
GetBoardName.
7. Plasma / VFD display (secondary COM port)
ESC-based command set (CPlasma): clear (ESC @),
cursor X/Y (ESC R/ESC Q), font (ESC K), attribute (ESC H), box draw/fill
(ESC X/ESC x), plus text. Fonts and sizes:
GetFontSize. Port for Phase 4; content becomes
per-profile.