Files
riojoy/docs/PROTOCOL.md
T
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

231 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` (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`](../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` (071) | the 72 digital button inputs (`ButtonPressed/Released` index) |
| `0x50``0x5F` (8095) | keypad **pad 0** (`KeyPressed` with `pad=0`, app adds 0x50) |
| `0x60``0x6F` (96111) | 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.