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>
231 lines
9.4 KiB
Markdown
231 lines
9.4 KiB
Markdown
# 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.
|