Files
riojoy/docs/OUTPUT-INTEGRATION.md
T
CydandClaude Fable 5 80222c5ea7 docs: input integration guide (cockpit -> RIOJoy -> game)
Companion to OUTPUT-INTEGRATION.md: the three input surfaces (ViGEm x360
pad, SendInput scancode keyboard/mouse, RioGamepad HID), per-button routing
kinds incl. the fixed 11-button pad order, axis calibration + routing with
the triggers-are-buttons trap and the shipped Descent pattern, per-game
strategy, profile building/triggers workflow, benchless testing, checklist.
Cross-linked from README and the output guide.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 20:49:23 -05:00

270 lines
12 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.
# Plasma & output integration guide
How to make a game, sim, or companion tool drive the cockpit's **outputs**
through RIOJoy: the lighted buttons (lamps) and the plasma/VFD text display.
This is the integrator's view — what the hardware can show, which addresses
mean what, and how to feed them. The exact wire grammar lives in
[FEEDBACK.md](FEEDBACK.md); the RIO serial protocol in
[PROTOCOL.md](PROTOCOL.md); the mirror direction (cockpit inputs → game) in
[INPUT-INTEGRATION.md](INPUT-INTEGRATION.md).
## The four output channels
| Channel | Trigger | Effort | Good for |
|---|---|---|---|
| **Automatic lamp feedback** | button press/release | none — built in | lighting the button the player just pressed |
| **Feedback endpoint** (`lamp` / `plasma` lines) | your code writes a text line to a pipe or UDP | small script | warning lights, status flashes, callsigns, scores |
| **Rumble → lamps** | game sets XInput vibration | config only, no code | damage/fire effects from **unmodified** games |
| **Plasma greeting** | profile activation | config only | a static per-game banner |
All four apply only while a profile is **active**; the profile opts into the
endpoint and rumble channels with its `"Feedback"` JSON section (see
[Gating in FEEDBACK.md](FEEDBACK.md#gating)). When RIOJoy is dormant or a
native game owns the ports, endpoint clients stay connected but commands drop.
## The lamp model
### What a lamp is
Each of the **72 cockpit buttons** (RIO addresses `0x000x47`) has a built-in
lamp. A lamp is set with a single state byte combining a **flash mode**
(solid / slow / med / fast) and a **brightness** (off / dim / bright). The
**board runs the blink itself** — one command starts a sustained flash, another
ends it. There is no per-frame cost to a flashing lamp.
The two 4×4 keypads (`0x500x5F` internal, `0x600x6F` external) are valid
protocol addresses but have **no physical lamps** — writes to them are
accepted and do nothing visible.
### Address map (functional groups)
From the panel model (`RioJoy.Core.Editing.CockpitPanel`, mirrored in the
profile editor):
| Group | Addresses | Layout |
|---|---|---|
| Lower Right MFD | `0x000x07` | 4×2; top row `07 06 05 04`, bottom `03 02 01 00` |
| Lower Left MFD | `0x080x0F` | 4×2; top `0F 0E 0D 0C`, bottom `0B 0A 09 08` |
| Secondary column | `0x100x17` | vertical 8 |
| Screen column | `0x180x1F` | vertical 8 |
| Upper Middle MFD | `0x200x27` | 4×2; top `27 26 25 24`, bottom `23 22 21 20` |
| Upper Left MFD | `0x280x2F` | 4×2; top `2F 2E 2D 2C`, bottom `2B 2A 29 28` |
| Upper Right MFD | `0x300x37` | 4×2; top `37 36 35 34`, bottom `33 32 31 30` |
| Throttle column | `0x380x3F` | vertical 8 (`3D` Panic, `3F` Throttle) |
| Joystick cluster | `0x400x47` | `40` Main, `4144` hat B/U/R/L, `45` Pinky, `46` Middle, `47` Upper |
| Internal keypad | `0x500x5F` | 4×4, **no lamps** |
| External keypad | `0x600x6F` | 4×4, **no lamps** |
(`0x480x4F` is a gap — not valid addresses.)
### Ownership: pick lamps the profile doesn't use
Lamps on buttons the active profile maps as *lighted* (`Lit` in the editor,
`iRIO` bit `0x8000`) belong to the automatic press/release feedback — endpoint
writes to them are **dropped** (logged once per address) so your effect can't
fight the built-in behavior. Design your effects on buttons the profile leaves
unlit — a dedicated "warning" MFD cluster the game doesn't bind, for example —
or deliberately leave the target buttons un-Lit in the profile.
### Rate budget
The RIO link is 9600 baud, shared with the ~55 ms analog poll. RIOJoy
coalesces lamp state per address (latest wins) and sends **at most one changed
lamp per 25 ms** (~40/s). Practical consequences:
- Send *state changes*, not periodic refreshes. Repeating the current state
costs nothing but also does nothing.
- A whole-panel effect (`lamp-all`, or sweeping many addresses) takes ~25 ms ×
changed-lamp-count to fully land — a 104-lamp sweep is ~3 s. Fine for an
attract mode; wrong for a fast strobe. For fast effects, use the board's own
flash modes on a few lamps instead.
## The plasma display model
The plasma is a **128 × 32 dot-matrix panel** on its own serial port at 9600
8N1 (so display updates never contend with the input link). It is fully
dot-addressable: it has a text mode (cursor + fonts + attributes) **and a raw
bitmap mode** (`ESC P` graphics write — see
[Bitmap graphics](#bitmap-graphics-esc-p)). The command set is documented in
vRIO's `VPlasma.Core/Protocol/PlasmaProtocol.cs`, recovered from the Tesla
4.10 sources and the display firmware dump.
Text mode has two glyph sizes:
| Font | Cell | Fits per line | Auto-selected when |
|---|---|---|---|
| large (font 5) | 10×14 px | ~12 chars | text ≤ 9 chars |
| small (font 2) | 5×7 px | ~25 chars | text 1020 chars (longer is truncated to 20) |
What the endpoint exposes (v1):
- **`plasma text <text>`** — auto-fit: short text renders large, longer text
small, centered on the display (the `PlasmaPosText` behavior the original
games used; its pivot is x=56, faithfully a touch left of the true 128-px
center). This is the right default for callsigns, scores, and status words.
- **`plasma text <x> <y> <text>`** — explicit cursor position in **pixels**,
top-left origin; the firmware accepts x 0127, y 031. You choose the
position; the font still auto-fits by length. Use this to keep two fields on
screen at once (e.g. callsign top line, score bottom line:
`plasma text 2 2 "VIPER 1-1"` + `plasma text 2 18 "SCORE 4200"`).
- **`plasma clear`** — blank the display.
- **`plasma row <y> <hex32>`** — one full 128-px bitmap row; see
[Bitmap graphics](#bitmap-graphics-esc-p).
Text is **Latin-1** (one byte per char) — don't send UTF-8.
Not exposed through the endpoint yet (small extensions when needed): explicit
font/attribute selection and box draw/fill.
### Bitmap graphics (`ESC P`)
The display accepts **raw bitmap rows** — this is how the native Red Planet
game draws everything (it renders into a local 1-bpp buffer and streams the
*changed* rows). The wire command:
```
ESC P s y x w h data…
```
`s` = screen (single-screen hardware, ignored), `y` = top row (031),
`x` = left **byte column** (015), `w` = bytes per row, `h` = rows, followed
by `w×h` data bytes, **MSB = leftmost pixel**. The native game always sends
whole rows: `x=0, w=16, h=1` — 16 bytes covering one full 128-px row.
Budget the bandwidth: a full-frame repaint is 32 rows × (7-byte header +
16 data bytes) ≈ 740 bytes ≈ **0.77 s** at 9600 baud. That's why the native
game diffs and streams only changed rows — an animation that touches a few
rows per tick is smooth; full-frame repaints are ~1 fps. Text mode is far
cheaper for text; reserve bitmaps for logos, custom gauges, and icons.
The endpoint exposes whole-row writes as a line command:
```
plasma row <y> <32 hex digits>
```
`y` is 031; the 32 hex digits are the row's 16 bytes left-to-right, MSB =
leftmost pixel. Rows stream strictly in arrival order (up to 128 queued;
overflow drops the incoming row and counts it), so push a frame as rows 031
and it lands intact. Example — a horizontal rule across row 16 and a lit
top-left corner block:
```
plasma row 16 FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
plasma row 0 F0000000000000000000000000000000
```
For animation, keep a 1-bpp frame buffer client-side and send only the rows
that changed since the last tick — exactly what the native game does. The
partial-span form of `ESC P` (arbitrary `x/w/h`) exists in
`PlasmaCommands.GraphicsWrite` for host-side code but is not exposed as a
line command.
### Update semantics
Plasma writes are **single-flight over a bounded queue** whose rules follow
what each command means: flooded `text` updates coalesce to the newest value
(safe to spam a score — the display shows the latest), `clear` discards
everything queued before it, and bitmap `row`s stream strictly in order
(never coalesced — a frame is many rows). Still: update fields on change, not
on a timer.
Lifecycle: on profile activation the display clears and shows the profile's
`PlasmaGreeting` (if set); your first `plasma` command replaces it. On
teardown (profile switch, dormancy, native-game yield) RIOJoy blanks the
display and releases the port.
`plasma text` does **not** clear the rest of the display — it draws at a
position. When a new value is shorter than the old one (`SCORE 900` after
`SCORE 1200`), stale pixels can remain; pad the text to a fixed width or
`plasma clear` first when the layout changes.
## Recipes
### Any XInput game — rumble, zero code
Add to the profile's JSON (`%APPDATA%\RIOJoy\config.json`):
```json
"Feedback": { "Rumble": { "LargeMotorLamps": [32, 33], "SmallMotorLamps": [34], "Threshold": 24 } }
```
Vibration now flashes Upper-Middle-MFD lamps `0x200x22`: off below the
threshold, slow/med/fast flash as intensity rises, per motor. Works with any
game that rumbles the ViGEm pad (net48 flavor only).
### DCS World — Export.lua
A pipe opens as a file on Windows; write lines, flush, done:
```lua
local rio = io.open("\\\\.\\pipe\\riojoy-feedback", "w")
local wasCaution = nil
function LuaExportAfterNextFrame()
if not rio then return end
local caution = LoGetMCPState and LoGetMCPState().MasterWarning
if caution ~= wasCaution then -- send changes, not frames
wasCaution = caution
rio:write(caution and "lamp 0x12 fast bright\n" or "lamp 0x12 off\n")
rio:flush()
end
end
function LuaExportStart()
if rio then
rio:write('plasma text "' .. (LoGetPilotName() or "PILOT") .. '"\n')
rio:flush()
end
end
```
(Or configure `"UdpPort"` and use DCS's `socket` library — same lines over
UDP, one or more per datagram.)
### SimHub / other telemetry hubs
Any plugin that can emit custom TCP/UDP/serial output can target the UDP
endpoint (`127.0.0.1:<UdpPort>`) with protocol lines. Map telemetry properties
to `lamp` lines (e.g. shift light → `lamp 0x3D fast bright`) and text
properties to `plasma text`.
### Games with no API — log tailing
```powershell
$p = New-Object IO.Pipes.NamedPipeClientStream '.', 'riojoy-feedback', ([IO.Pipes.PipeDirection]::Out)
$p.Connect(2000)
$w = New-Object IO.StreamWriter $p, ([Text.Encoding]::GetEncoding(28591))
Get-Content 'C:\games\thegame\events.log' -Wait -Tail 0 | ForEach-Object {
if ($_ -match 'PLAYER_HIT') { $w.WriteLine('lamp 0x24 fast bright'); $w.Flush() }
if ($_ -match 'SCORE=(\d+)') { $w.WriteLine("plasma text SCORE $($Matches[1])"); $w.Flush() }
}
```
## Testing without hardware
Both output ports accept `pipe:` endpoints, so the [vRIO](https://gitea.mysticmachines.com/VWE/VRIO)
emulators stand in for the cabinet:
- Profile `RioComPort: "pipe:vrio"` → vRIO's board emulator; its panel shows
lamp states, including flash.
- Profile `PlasmaComPort: "pipe:vplasma"` → the vPlasma display emulator
renders the 112×32 output. On a machine with neither display nor COM2, set
`PlasmaComPort: "off"` instead (the app default is `COM2`).
Then drive the feedback pipe from PowerShell (snippets in
[FEEDBACK.md](FEEDBACK.md#client-snippets)) and watch the emulators.
## Checklist for a new integration
1. Give the game's profile a `"Feedback"` section (it's off otherwise).
2. Choose effect lamps that the profile does **not** mark Lit; note their
addresses from the map above.
3. Decide plasma layout: one auto-centered field, or fixed pixel positions for
multiple fields (pad to fixed width).
4. Emit on **state change** only; let the board do the blinking.
5. Reconnect logic: on pipe write failure, close, reopen, retry — RIOJoy's
endpoint accepts reconnects forever, and commands sent while dormant are
dropped by design (your client doesn't need to track RIOJoy's state).
6. Bench-test against `pipe:vrio` / `pipe:vplasma` before touching the cabinet.