# 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.
