# RIO serial protocol & `iRIO` input map Reverse-engineered from the legacy implementation in [`legacy/riovjoy2.cpp`](../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`](../legacy/riovjoy2.cpp#L747)). - An optional **plasma / VFD text display** is on a second COM port (legacy hard-codes **COM2**, [`CPlasma`](../legacy/riovjoy2.cpp#L41)). - 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`](../legacy/riovjoy2.cpp#L871)). - **Checksum byte:** `(sum of (b & 0x7F) for each byte in command+payload) & 0x7F` ([build: `SendCommand`](../legacy/riovjoy2.cpp#L1217), [verify: `ReadCommBlock`](../legacy/riovjoy2.cpp#L884)). ### 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 `ACK`s. > ⚠️ The legacy receive path force-accepts every packet regardless of checksum > (`static bool s_bool = true;` at [riovjoy2.cpp#L887](../legacy/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`](../legacy/riovjoy2.cpp#L189)); payload lengths from [`g_baRIOLengthsA`](../legacy/riovjoy2.cpp#L171). | 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`](../legacy/riovjoy2.cpp#L1273) etc.). ### Lamp state byte (LampRequest) Composed of flash + two brightness fields ([`LampState`](../legacy/riovjoy2.cpp#L213)): - 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`](../legacy/riovjoy2.cpp#L1127)). Each axis is a **14-bit signed** value packed as two 7-bit bytes ([`CombinePair`](../legacy/riovjoy2.cpp#L1116)): ``` 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`](../legacy/riovjoy2.cpp#L1069)). If **>5 s** elapse with no `AnalogReply`, it issues a general reset to recover ([riovjoy2.cpp#L1096](../legacy/riovjoy2.cpp#L1096)). ### Axis → virtual-device mapping (range 0..32766, center 16383) Calibration/deadzone math lives in [`UpdateJoystick`](../legacy/riovjoy2.cpp#L1722), [`UpdateThrottle`](../legacy/riovjoy2.cpp#L1637), [`UpdatePadal`](../legacy/riovjoy2.cpp#L1504): | 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](../legacy/riovjoy2.cpp#L352), [decode `Press_V2`](../legacy/riovjoy2.cpp#L1944)). ### 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`](../legacy/riovjoy2.cpp#L1185). ### 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`](../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`](../legacy/riovjoy2.cpp#L1852)). 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`](../legacy/riovjoy2.cpp#L2075)). > ⚠️ 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`](../legacy/riovjoy2.cpp#L1332) and [`GetBoardName`](../legacy/riovjoy2.cpp#L1413). --- ## 7. Plasma / VFD display (secondary COM port) ESC-based command set ([`CPlasma`](../legacy/riovjoy2.cpp#L2146)): 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`](../legacy/riovjoy2.cpp#L2198). Port for Phase 4; content becomes per-profile.