Each device row now serves one endpoint at a time, picked from a list of pipe:vrio / pipe:vplasma (first, default) plus the machine's COM ports — nothing opens automatically at startup. The pipe services gained an IsListening property and are started/stopped by the row's Open/Close button instead of running for the whole app lifetime, which also retires the dual-transport warning. Status line shows the active endpoint; OFFLINE when none. Standalone vPLASMA is unchanged (hardwired COM12 + permanent pipe). Also fixes the xUnit1031 warnings in the plasma pipe tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
201 lines
11 KiB
Markdown
201 lines
11 KiB
Markdown
# 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: each device's connection
|
||
picker lists its named-pipe endpoint — `pipe:vrio`, `pipe:vplasma` —
|
||
alongside the machine's COM ports. Select it and **Open** to serve
|
||
`\\.\pipe\vrio` / `\\.\pipe\vplasma`; 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. Each row serves one endpoint at a time — the RIO is a single-host
|
||
device — and nothing opens automatically at startup; the COM path is
|
||
unchanged for RIOJoy and real pods. 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 connection row in the control strip (label colour =
|
||
connection status; picker offers `pipe:vplasma` and the COM ports). 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.
|