# vRIO — virtual RIO cockpit device

A software replica of the cockpit **RIO** (Remote Input/Output) board: it opens
a COM port and speaks the **device side** of the RIO serial protocol, so any
host that expects the real hardware — most importantly
[RIOJoy](../riojoy/) — can talk to it without a cockpit attached. (The
cockpit's other serial device gets the same treatment: see
[vPLASMA](#vplasma--the-companion-plasma-display) below.)

The window is an interactive version of the cockpit control panel that
RIOJoy's profile editor draws (the same functional layout from the original
Win32 RIO design: five MFD clusters, four board columns, two keypads, encoder
gauges). But where the editor *edits bindings*, vRIO's cells are the physical
controls:

- **Left-click** a cell — momentary button press (`ButtonPressed`/`Released`
  or `KeyPressed`/`Released` on the wire). **Right-click** — latch it down.
- **Drag** the X/Y box and the Z / L / R gauges — the five analog axes,
  returned by the next `AnalogReply` (14-bit signed, 7-bit-pair packed).
  Gauges span each axis' realistic hardware travel, not the full 14-bit
  range: Z/L/R rest at raw 0 at the gauge bottom and travel to −800
  (throttle — forward travel runs negative, matching RIOJoy's ratchet) or
  +500 (spring-loaded pedals), and the stick covers ±80 around center —
  the windows RIOJoy's calibrator expects.
- Cells shade to the **lamp state the host commands** (`LampRequest`:
  off / dim / bright, with slow/med/fast flash), so RIOJoy's press-feedback
  lights the on-screen panel just like the real buttons.
- **Lamps can mirror onto an RGB keyboard** (*Mirror lamps on RGB keyboard*,
  via Windows 11 Dynamic Lighting): keys bound to lamp-capable buttons glow
  with the panel's palette and blink in step with the on-screen flash modes;
  per-key keyboards get the full button field, zone-lit keyboards show the
  strongest current lamp board-wide, and a picker narrows the mirror to one
  keyboard when several are attached. For the LEDs to stay lit while the
  *game* has focus, vRIO needs package identity: run `pkg\Register-vRIO.ps1`
  once (Developer Mode required; pass the exe folder for a deployed copy),
  launch vRIO from its **Start menu entry** (a direct exe launch runs without
  identity), then drag vRIO to the top of *Settings → Personalization →
  Dynamic Lighting → Background light control*.
- **Keyboard and Xbox (XInput) controller input** drive the same controls
  through a bindings file (`%APPDATA%\vRIO\bindings.txt`, created with
  commented defaults on first run — *Edit bindings…* opens it, *Reload
  bindings* applies edits live). Keys and pad buttons press any RIO address;
  pad sticks/triggers and keys drive the axes in each axis' realistic travel
  window, with deflect (spring-back), rate (throttle-style, position holds),
  deadzone, and invert options. The default profile makes the controller
  mandatory: all five axes live on the pad (left stick / triggers / right
  stick = stick / pedals / throttle) and the keyboard covers the button
  field — number row + QWERTY row = the upper MFD bank, home + bottom
  rows = the lower MFD bank (4-key blocks split by an unbound gap key),
  F1–F6 / F7–F12 = the Secondary / Screen columns, numpad = internal
  keypad (hex keys on the operators), arrows + Space = hat + main, with
  ABXY / dpad / shoulders on the pad's named buttons.
- **Capture one keyboard in the background** (*Capture keyboard* picker in the
  Input panel): by default keyboard input follows the focused window, so vRIO
  goes deaf the moment the sim takes focus. Selecting a specific keyboard
  instead taps it through the Windows **Raw Input** API (`RIDEV_INPUTSINK`), so
  its keys drive the panel *while the game has focus* — the input-side twin of
  the lamp mirror writing that keyboard's LEDs under the same condition. Raw
  Input observes without intercepting: the keystroke still reaches the focused
  app, so this is meant for a keyboard *dedicated* to the panel, not the one you
  also type on. Leaving the picker on *All keyboards* keeps the plain
  focus-only behavior. (No package identity needed — unlike the lamp side, this
  is a plain Win32 tap.)

## Wire behavior

Protocol per `riojoy/docs/PROTOCOL.md` (9600 8N1, `[cmd][payload…][7-bit
checksum]`, ACK `0xFC` / NAK `0xFD` / RESTART `0xFE` / IDLE `0xFF`), with
device behavior grounded in the **real v4.2 firmware dump**
(`riojoy/rio-firmware/RIOv4_2-ANALYSIS.md`):

- ACKs every well-formed packet; NAKs bad-checksum packets.
- **TX is paced at the wire rate** — one byte per 10-bit frame (~1.04 ms at
  9600 8N1), never closer. A virtual null-modem has no UART, so unpaced
  writes would land at the host in microsecond bursts no real board could
  produce; vRIO's writer thread schedules each byte against a monotonic
  slot deadline instead, so e.g. the 51-byte CheckRequest response takes
  the same ~53 ms it takes real hardware.
- `CheckRequest` → the real board's init handshake: `TestModeChange` **enter**,
  one `BoardOk` CheckReply per board (the 11 boards from the legacy firmware's
  table), then `TestModeChange` **exit**. Hosts wait (≤5 s per step) on both
  test-mode packets and send nothing while test mode is active, so the exit is
  mandatory. `VersionRequest` → configurable version, default **4.2**.
- `ResetRequest` re-zeroes the targeted axis (or all).
- A NAK re-sends the last event up to **4 times**, then gives up with a
  RESTART byte — the real board's retry budget.
- Optional **v4.2 reply-wedge emulation** (in `VRio.Core`; the UI toggles
  were removed): after retry exhaustion, analog requests are silently
  dropped — still ACK'd, RX path alive — until a host `ResetRequest`,
  reproducing the latch-leak fault the firmware analysis documents.

## Using it with RIOJoy on one PC

The two apps need a crossed serial link. Install a
[com0com](https://com0com.sourceforge.net/) virtual null-modem pair
(e.g. `COM1 ⇄ COM11`), then:

1. Run `VRio.App`, pick `COM11`, **Open**.
2. RIOJoy will always point to `COM1`.

RIOJoy's DTR open-pulse shows up in the wire log (DSR handshake), its ~55 ms
analog polling drives the "analog polls served" counter, and every click on
the vRIO panel arrives at RIOJoy as real cockpit input. Two physical PCs with
a null-modem cable work the same way.

## Named pipes for DOSBox-X (no com0com)

When the game runs in the patched DOSBox-X (the fork's `namedpipe` serial
backend), the virtual ports aren't needed at all: both devices also serve a
named pipe for the whole app lifetime — `\\.\pipe\vrio` and
`\\.\pipe\vplasma` — and DOSBox-X connects as the client, retrying in the
background until the pipe exists:

```ini
[serial]
serial1 = namedpipe pipe:vrio    rxpollus:100 rxburst:16
serial2 = namedpipe pipe:vplasma
```

A pipe is a plain byte stream, so serial data and modem lines travel as
typed frames (`0x00 len bytes` data, `0x01 bits` DTR/RTS) — the game's DTR
reset pulse keeps its exact position in the byte stream, and a disconnect
reads as all-lines-low (cable unplugged). The contract lives in
`src/VRio.Core/Device/PipeFraming.cs` on this side and `serialnamedpipe.h`
on the fork's. TX is paced exactly like the COM path — with the 9600-baud
wire gone, the pacer is the only thing between the host and an impossible
burst. The COM rows keep working unchanged (RIOJoy, real pods); if a pipe
client connects while a COM host is active the wire log warns, since the
RIO is a single-host device. vRIO's built-in glass and the standalone
vPLASMA share the `vplasma` pipe name — whoever starts second retries until
the name frees up.

## vPLASMA — the companion plasma display

The cockpit's second serial device is the **plasma display**: a 128×32
dot-matrix panel on COM2 (9600 8N1, no flow control) that the game draws
mission text and status graphics on. The software replica comes in two
forms. **Built into vRIO**: the panel's encoder strip hosts the glass at
top left, with its own port row in the control strip (label colour = port
status; auto-opens **COM12** at startup when present). And standalone,
`VPlasma.App`: a bare-glass window that opens **COM12** (the device end of
the plasma's null-modem pair) on startup — retrying while the port is
missing or busy, port status in the title bar. Both decode the display's
command stream and render the dot matrix in plasma orange, text mode
included; only one can hold COM12 at a time.

The command set was recovered from two Tesla 4.10 artifacts:
`CODE\RP\MUNGA_L4\L4PLASMA.CPP` (the game's driver — it renders everything
into a local 1bpp buffer and streams changed rows as `ESC P` graphics
writes, hiding the cursor with `ESC G 0` at boot) and the factory test tool
`VWETEST\VGLTEST\PLASMA.EXE` (the text mode: BS/HT/LF/VT/CR cursor motion,
`ESC @` clear, `ESC L` home, `ESC G` cursor visibility, `ESC K` font
select, `ESC H` attributes — intensity/underline/reverse/flash). The full
recovered grammar lives in `src/VPlasma.Core/Protocol/PlasmaProtocol.cs`.

- **Graphics** — `ESC P screen y xbyte width rows` + packed 1bpp row data,
  MSB = leftmost pixel. This is everything the game sends, so it is the
  wire path a pod's plasma actually sees.
- **Text** — printable ASCII renders through a 5×7 font at a cursor:
  fonts 0–3 give a 21×4 cell grid, fonts 4–7 the same glyphs doubled to
  10×2. Half-intensity draws dimmer, reverse/underline render in the
  cell, flashing text (and the flashing cursor) blink on the glass. The
  panel's own ROM glyphs are lost with the hardware, so the classic
  public-domain 5×7 set stands in.
- **Double-click** the glass to cycle three self-test pages (banner,
  charset, graphics pattern) through the same parser the wire feeds —
  useful without a host. **Right-click** resets the display to its
  power-on state.
- Pair it with the game like vRIO ↔ RIOJoy: a second com0com null-modem
  pair `COM2 ⇄ COM12` — the game's plasma output writes COM2, vPLASMA
  listens on COM12. The plasma never talks back, so there is no transmit
  path to pace.

## Repository layout

| Path | Contents |
|------|----------|
| `src/VRio.Core` | Protocol framing/builder/parser, the `VRioDevice` state machine, serial pump, panel layout data (class library) |
| `src/VRio.App` | WinForms panel UI |
| `src/VPlasma.Core` | Plasma command-stream parser, the `VPlasmaDevice` display state machine, 5×7 font, serial listener (class library) |
| `src/VPlasma.App` | WinForms dot-matrix display UI |
| `pkg` | Sparse-package manifest + registration script: grants VRio.App.exe the package identity Dynamic Lighting's background-control list requires |
| `tests/VRio.Core.Tests` | xUnit tests for the protocol + device engine |
| `tests/VPlasma.Core.Tests` | xUnit tests for the plasma parser + display engine |

## Building

Same toolchain as RIOJoy: **.NET SDK** (8.0+) with the **.NET Framework 4.8**
targeting pack; apps target net48 so deployed builds run in-box on
Windows 10/11.

```sh
dotnet build VRio.sln -c Release
dotnet test VRio.sln
```

Interop is additionally verified against RIOJoy's real `RioSerialLink`
(version/check/analog/lamp/button/keypad/reset round-trips over an in-memory
transport) — see the RIOJoy repo for the host side.
