Files
CydandClaude Opus 4.8 39a3dab1fc Phase 0: scaffold modern RIOJoy solution + plan
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>
2026-06-26 12:43:01 -05:00

9.4 KiB
Raw Permalink Blame History

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.


  • 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 is 0x80 .. 0x80 + N where N is 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 (0x000x47)
0x89 ButtonReleased RIO→PC 1 index (0x000x47)
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:

  1. 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
0x000x47 (071) the 72 digital button inputs (ButtonPressed/Released index)
0x500x5F (8095) keypad pad 0 (KeyPressed with pad=0, app adds 0x50)
0x600x6F (96111) keypad pad 1 (KeyPressed with pad=1, app adds 0x60)

(0x480x4F 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)

  1. 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.
  2. If joy && hat && mouse all set (0x7000) → it's a RIO command, not an output: calls RIOcmd(value) (axis resets, recalibrate, status, etc. — see RIOcmd).
  3. Else if joy → set joystick button value.
  4. Else if hat → set POV to direction value (release → center).
  5. 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/dy oddly (e.g. "up" sets dx=-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.