Files
riojoy/docs/PLAN.md
T
CydandClaude Opus 4.8 b3cb764f4d Phase 2: serial + RIO protocol core (RioJoy.Core) with unit tests
Port the RIO wire protocol from legacy/riovjoy2.cpp into testable C#:

- Protocol/: command + length table, 7-bit checksum, packet builder, and a
  streaming receive-side framing state machine (PacketParser) that mirrors the
  legacy ReadCommBlock framing/resync (high-bit-mid-packet abort). Typed RIO->PC
  decodes: AnalogReport (14-bit sign-extend), VersionInfo, CheckStatus; lamp-state
  composition.
- Serial/: RioSerialLink drives an async receive loop with ACK/NAK reply policy
  (legacy force-accept vs. opt-in VerifyInboundChecksum), the analog poll timer,
  and the >5s reset-recovery watchdog. IRioTransport abstracts the COM port; the
  SerialPort-backed transport does 9600 8N1 + DTR reset pulse, and acquire/release
  is just create/dispose (foundation for native-game serial yield).
- tests/RioJoy.Core.Tests: 54 xUnit tests covering checksum, framing/resync,
  builder round-trips, analog sign-extension + sentinel rejection, lamp combos,
  and the read loop driven against an in-memory fake transport.

Hardware verification (version/check/analog against a cabinet) remains; it can't
be done off-device.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 13:04:03 -05:00

9.3 KiB
Raw Blame History

RIOJoy — modernization plan

Modernize the cockpit RIO interface app for Windows 10/11, removing the vJoy dependency and replacing it with a custom virtual HID device, rewritten in C#/.NET as a background tray app with per-game profiles.

The legacy app is preserved under legacy/ as the behavioral reference. The RIO wire format and input map are documented in PROTOCOL.md.


Purpose

The cockpits run two native games (Firestorm, Red Planet) that talk to the RIO hardware directly and never use this app. RIOJoy exists to broaden which other games can run in the cockpits: arbitrary games don't know about the cockpit's extra hardware (5 analog axes, 96 lighted buttons, the plasma/VFD display, the labeled wallpaper), so RIOJoy bridges the RIO to whatever input those games do understand — joystick, keyboard, and mouse — and drives the cockpit's outputs on their behalf.


Target architecture

   ┌─────────────────────── C# / .NET 8 tray app (x64) ───────────────────────┐
   │  Serial (RIO protocol)   →  Input mapper (profile)   →  Output router      │
   │   COM port, 9600 8N1         72 inputs + keypads          ├─ Keyboard/Mouse: SendInput (P/Invoke)
   │   packet parse + ACK/NAK     decode iRIO bitfield         ├─ Joystick: HID report → DeviceIoControl ↓
   │   analog poll + recovery     axis calibration            └─ Lamps: LampRequest back over serial
   │  Plasma/VFD on 2nd COM   ·  Profiles + auto-switch + tray UI + logging      │
   └───────────────────────────────────────────────┬───────────────────────────┘
                                                     │ IOCTL (input report bytes)
   ┌─────────────────────────────────────────────────▼──────────────────────────┐
   │  RioGamepad.sys  — KMDF + VHF (vhf.sys) virtual HID device                   │
   │  Report descriptor: X,Y,Z,Rx,Ry,Rz (16-bit) · 1 hat · 96 buttons            │
   │  Control device + custom IOCTL  →  VhfReadReportSubmit()  →  Windows sees    │
   │                                                              a HID gamepad   │
   └─────────────────────────────────────────────────────────────────────────────┘

Decisions (confirmed)

  • Virtual joystick: custom VHF/UMDF HID driver (full fidelity: 6 axes, 96 buttons, 1 hat — exactly the legacy vJoy layout). Not ViGEm (can't hold 96 buttons).
  • Stack: C# / .NET 8 (LTS), x64. Driver is C (WDK), separate toolchain.
  • Form: background tray app (NotifyIcon), auto-start with Windows.
  • Targets: Windows 10/11 x64 only. The legacy x86 / WinXP targets are dropped.

What ports over vs. what's new

Concern Legacy Modern
Serial + RIO protocol overlapped I/O + watch thread SerialPort + async loop; faithful port of the packet state machine
Input decode iRIO[] bitfield + Press_V2 same semantics, ported to C#
Keyboard/mouse SendInput scancode SendInput via P/Invoke (≈verbatim)
Joystick vJoy SetAxis/SetBtn/SetDiscPov HID report → IOCTL → RioGamepad.sys
Lamps LampRequest over serial same
Axis calibration UpdateJoystick/Throttle/Padal ported math
Plasma display CPlasma (COM2) ported; content per-profile
Config SimpleIni, single file, hard-coded COM1 profile library (JSON), configurable ports; importer for legacy RIO.ini

Profiles (the core abstraction)

A profile fully describes how the cockpit behaves for one non-native game. Everything is per-profile, not global:

  • Button/keypad mapping (the decoded iRIO table for this game)
  • Axis calibration, curves, and invert flags
  • Lamp behavior
  • Plasma/VFD content (or "off")
  • Cockpit overlay labels + the generated wallpaper (Phase 7)
  • Display/resolution targets

Profiles never describe the native games — those are the "hands-off" case.

Serial-port yield & the auto-switch state machine

Because the native games own the RIO's COM port directly, RIOJoy must yield it. A process/window watcher drives three states:

Detected running app RIOJoy behavior
Native game (Firestorm, Red Planet) Release COM port, go fully dormant — no serial, no HID, no overlay
Supported non-native game Acquire port, load that game's profile, drive HID / keyboard / mouse / lamps / plasma / wallpaper
Nothing / desktop Idle — port released or a configurable neutral default

Config therefore holds: the per-game profile library, a list of native games to yield to, and the executable→profile match rules. Manual override from the tray menu is always available.


Phases

Phase 0 — Repo & scaffold (this commit)

  • git init, remote https://gitea.mysticmachines.com/VWE/riovjoy2.git.
  • Legacy C++ moved to legacy/; cockpit art to docs/reference/.
  • Solution RioJoy.sln with src/RioJoy.Core (lib) + src/RioJoy.Tray (tray app); driver/ placeholder for the WDK project.
  • This plan + PROTOCOL.md.

Phase 1 — Virtual HID driver (highest risk; do first)

  • KMDF + VHF virtual HID gamepad: report descriptor = 6×16-bit axes, 1 hat, 96 buttons.
  • Control device + custom IOCTL → VhfReadReportSubmit.
  • Test harness (throwaway C#) wiggles axes/buttons; verify in joy.cpl.
  • Test-signing setup for the cabinets.

Phase 2 — Serial + RIO protocol core (RioJoy.Core) — code-complete

Implemented in src/RioJoy.Core/Protocol + Serial, covered by tests/RioJoy.Core.Tests (xUnit, 54 tests):

  • Packet parser/builder: command/length table, 7-bit checksum, control chars, framing resync on a high-bit byte mid-packet (PacketParser, PacketBuilder).
  • Typed RIO→PC decodes: AnalogReport (14-bit sign-extend), VersionInfo, CheckStatus; lamp-state composition (RioLampState).
  • RioSerialLink: async receive loop with ACK/NAK policy (legacy force-accept vs. opt-in VerifyInboundChecksum), analog poll timer + >5 s reset-recovery.
  • IRioTransport abstraction with a SerialPort-backed implementation (9600 8N1, DTR reset pulse); clean COM-port acquire/release = create/dispose the transport (foundation for serial yield). The read loop is tested against an in-memory fake transport.
  • Remaining: verify against real hardware (version reply, check reply, analog stream) — needs a cabinet; can't be done off-device.

Phase 3 — Input mapping + output routing

  • Port iRIO decode and routing precedence (keyboard/mouse/joy/hat/RIO-command).
  • Keyboard/mouse via SendInput P/Invoke; lamp feedback over serial.
  • HID-report feeder → the Phase 1 driver via DeviceIoControl.

Phase 4 — Axis calibration + plasma display

  • Port UpdateJoystick/Throttle/Padal math (deadzones, ratchet, rudder).
  • Port the CPlasma ESC command set on the secondary COM port.

Phase 5 — Tray app + profiles

  • NotifyIcon + menu mirroring the legacy console menu (reset/recalibrate axes, version/status, toggle raw-axis & poll-rate readouts, quit) + status/log window.
  • Profile library, manual selection, and the three-state auto-switch watcher (incl. native-game yield).
  • Config persistence; auto-start.

Phase 6 — Packaging / signing / deploy

  • Driver install via pnputil; app installer; test-signing script.
  • Cabinet deployment doc. (Production option: attestation signing.)

Phase 7 — Profile/mapping editor + cockpit overlay generator

Replaces the legacy Google-Sheet → .data → GIMP → Script-Fu pipeline (see docs/reference/customBackground/).

  • Mapping editor: per-button UI to set action + label + lamp, no hex bit-twiddling; clone-from-existing profile.
  • Overlay generator: render labels into named regions over a base cockpit image using SkiaSharp, porting the auto-fit/justification logic from sg-goobie-MFD.scm; export the per-profile wallpaper and re-apply via SystemParametersInfo.
  • Region authoring: extract the label rectangles from riojoy.xcf into a regions.json; in-app box editor for future tweaks.
  • The unified profile JSON supersedes both RIO.ini and the Google Sheet, with importers for each.
  • To confirm at Phase 7: target wallpaper resolution(s); static wallpaper vs. live overlay (e.g. lit-button highlighting mirroring lamp state).

Open items / risks

  • Driver signing is the main friction point. Test-signing is fine for owned cabinets; redistribution needs attestation signing (EV cert + Partner Center).
  • Input injection vs. session: SendInput targets the interactive session — fine for a tray app, which is why a Windows service was rejected.
  • Legacy quirks to decide on (see PROTOCOL.md ⚠️ notes): disabled inbound checksum verification; odd mouse-move deltas.