docs: plasma/output integration guide for game and sim integrators

Lamp address map by functional group (keypads have no lamps), 112x32 plasma
model with auto-fit font rules and update semantics, rate-budget guidance,
and recipes: rumble config, DCS Export.lua, SimHub, log tailing, vRIO/vPlasma
bench testing. Cross-linked from FEEDBACK.md (now the wire reference) and
README; corrects the lamp count there (72, per CockpitPanel, not 96).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-31 20:13:55 -05:00
co-authored by Claude Fable 5
parent ad7ac19ab2
commit 94d32a1f06
3 changed files with 226 additions and 5 deletions
+1
View File
@@ -23,6 +23,7 @@ Red Planet — talk to the RIO directly and do not use this app.)
| [`docs/PLAN.md`](docs/PLAN.md) | Full modernization plan |
| [`docs/PROTOCOL.md`](docs/PROTOCOL.md) | RIO wire format + `iRIO` input-map reference |
| [`docs/FEEDBACK.md`](docs/FEEDBACK.md) | Game→cockpit feedback endpoint (lamps + plasma over pipe/UDP, rumble) |
| [`docs/OUTPUT-INTEGRATION.md`](docs/OUTPUT-INTEGRATION.md) | Integrator's guide: lamp address map, plasma display model, per-game recipes |
| _RIO board hardware & firmware_ | Moved to the [TeslaRel410 `restoration/`](https://gitea.mysticmachines.com/VWE/TeslaRel410/src/branch/main/restoration) archive — board photos, schematics, GAL decode (`restoration/rio-hardware`) and the RIO 4.3 board firmware (`restoration/rio-firmware`) |
| [`docs/reference/`](docs/reference/) | Cockpit overlay art & the legacy labeling pipeline |
| [`legacy/`](legacy/) | Original C++/vJoy implementation, kept as reference |
+10 -5
View File
@@ -1,11 +1,16 @@
# Game feedback endpoint (game → RIOJoy → cockpit)
Phase 9 lets external programs drive the cockpit's **output** hardware through
the running RIOJoy tray app: the 96 lamps (with board-side flash) and the
plasma/VFD text display. Anything that can write a line of text to a named pipe
or a UDP socket can use it — a DCS `Export.lua`, a SimHub plugin, a game mod, a
PowerShell one-liner. XInput **rumble** is mapped separately (no client needed;
see [Rumble → lamps](#rumble--lamps)).
the running RIOJoy tray app: the 72 button lamps (with board-side flash) and
the plasma/VFD text display. Anything that can write a line of text to a named
pipe or a UDP socket can use it — a DCS `Export.lua`, a SimHub plugin, a game
mod, a PowerShell one-liner. XInput **rumble** is mapped separately (no client
needed; see [Rumble → lamps](#rumble--lamps)).
This page is the wire-protocol reference. For the integrator's view — which
lamp addresses map to which physical buttons, the plasma display's geometry
and fonts, and per-game recipes — see
[OUTPUT-INTEGRATION.md](OUTPUT-INTEGRATION.md).
## Endpoints
+215
View File
@@ -0,0 +1,215 @@
# 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 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/VFD is a **112 × 32 pixel** text display on its own serial port
(so text updates never contend with the input link). Two glyph sizes exist:
| Font | Cell | Fits per line | Auto-selected when |
|---|---|---|---|
| large (font 5) | 10×14 px | ~11 chars | text ≤ 9 chars |
| small (font 2) | 5×7 px | ~22 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). This is the right default for callsigns, scores, and status
words.
- **`plasma text <x> <y> <text>`** — explicit cursor position in **pixels**
(top-left origin). 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.
Text is **Latin-1** (one byte per char) — don't send UTF-8.
Not exposed through the endpoint yet (the underlying `PlasmaCommands` port has
them, so they're a small extension when needed): explicit font/attribute
selection and box draw/fill.
### Update semantics
Plasma writes are **single-flight, latest-pending-wins**: while one text is
being written, only the *newest* pending command survives. Flooding a score
update every frame is safe — the display shows the latest value — but
interleaving *two different fields* at high rate from one client can starve
one of them. 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.