Files
riojoy/docs/OUTPUT-INTEGRATION.md
T
CydandClaude Fable 5 66c3cbdb57 docs: plasma bitmap mode (ESC P) + correct panel geometry to 128x32
The display is fully dot-addressable: document the ESC P graphics write
(row format, MSB-left, changed-row streaming as Red Planet does it, and the
~0.77s full-frame budget at 9600 baud) per vRIO PlasmaProtocol.cs, recovered
from the Tesla 4.10 sources + firmware dump. Fixes the guide''s 112x32 guess
(the legacy auto-center pivot x=56 sits left of the true 128px center) and
notes the firmware cursor ranges (x 0-127, y 0-31).

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

11 KiB
Raw Blame History

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; the RIO serial protocol in 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). 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). 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.

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, box draw/fill, and the bitmap mode below.

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.

Status: the hardware and the vPlasma emulator fully support ESC P, but RIOJoy's PlasmaCommands port and the feedback endpoint don't expose it yet. The natural extension is a line command (e.g. plasma row <y> <32-hex-digit row> or a base64 block form) — if an integration needs it, that plus a PlasmaCommands.GraphicsWrite builder is a small, self-contained addition.

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):

"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:

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

$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 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) 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.