# 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 `0x00–0x47`) 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 (`0x50–0x5F` internal, `0x60–0x6F` 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 | `0x00–0x07` | 4×2; top row `07 06 05 04`, bottom `03 02 01 00` | | Lower Left MFD | `0x08–0x0F` | 4×2; top `0F 0E 0D 0C`, bottom `0B 0A 09 08` | | Secondary column | `0x10–0x17` | vertical 8 | | Screen column | `0x18–0x1F` | vertical 8 | | Upper Middle MFD | `0x20–0x27` | 4×2; top `27 26 25 24`, bottom `23 22 21 20` | | Upper Left MFD | `0x28–0x2F` | 4×2; top `2F 2E 2D 2C`, bottom `2B 2A 29 28` | | Upper Right MFD | `0x30–0x37` | 4×2; top `37 36 35 34`, bottom `33 32 31 30` | | Throttle column | `0x38–0x3F` | vertical 8 (`3D` Panic, `3F` Throttle) | | Joystick cluster | `0x40–0x47` | `40` Main, `41–44` hat B/U/R/L, `45` Pinky, `46` Middle, `47` Upper | | Internal keypad | `0x50–0x5F` | 4×4, **no lamps** | | External keypad | `0x60–0x6F` | 4×4, **no lamps** | (`0x48–0x4F` 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 10–20 chars (longer is truncated to 20) | What the endpoint exposes (v1): - **`plasma 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 `** — explicit cursor position in **pixels**, top-left origin; the firmware accepts x 0–127, y 0–31. 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 `** — 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 (0–31), `x` = left **byte column** (0–15), `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 <32 hex digits> ``` `y` is 0–31; 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 0–31 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 `0x20–0x22`: 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:`) 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.