Files
riojoy/docs/FEEDBACK.md
T
CydandClaude Opus 5 46e108de89 feedback: plasma box, explicit fonts, and per-position text coalescing
Three endpoint gaps, found building the Descent 3 score overlay - the boxed
place|score field the original games drew over the callsign:

plasma box <x> <y> <w> <h> draws an outlined box with a blanked interior, the
overlay chrome, as one ESC P graphics write. The wire addresses whole bytes
horizontally, so the write covers the byte-aligned span containing the box and
clears span pixels outside it; documented, with 8-px alignment the advice.
Boxes queue FIFO with rows.

plasma text gains an explicit font: a third numeric token after the position,
with text still following, so `plasma text 2 2 7` still displays "7". Auto-fit
picks the font by LENGTH - short text always rendered large, and a "1" that
must fit a 12-px box simply could not be sent before. ResolvePosText
generalizes the legacy Score-font special case: 0 = auto, nonzero honored.

Text coalescing is now per position. The global rule - any queued text
superseded every other queued text - meant the documented two-field layout
(callsign top, score bottom) could not survive its own send burst: the second
field silently ate the first whenever both were queued. A newer text now
replaces only a queued text at the same (x,y).

15 new tests; 472 pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 11:45:59 -05:00

8.1 KiB
Raw Blame History

Game feedback endpoint (game → RIOJoy → cockpit)

Phase 9 lets external programs drive the cockpit's output hardware through 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).

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.

Endpoints

Transport Address Default
Named pipe \\.\pipe\riojoy-feedback on
UDP (loopback only) udp://127.0.0.1:<port> off (UdpPort unset)

Both feed the same line protocol; the pipe accepts up to 4 concurrent clients. Configured app-wide in %APPDATA%\RIOJoy\config.json:

{ "Feedback": { "PipeEnabled": true, "PipeName": "riojoy-feedback", "UdpPort": 19910 } }

Omitting the Feedback section entirely = pipe on under the default name, UDP off. Endpoint config is read once at the first profile activation; changes take effect on app restart.

The endpoint is app-lifetime: clients keep their connection across profile switches and dormancy. Whether commands apply is per-profile (see Gating).

Line protocol

One command per line. LF or CRLF line endings; on UDP, one datagram carries one or more complete lines and the end of the datagram terminates the last line (no trailing LF needed, no fragments across datagrams). Keywords are case-insensitive. Lines over 256 bytes and UDP datagrams over 4 KB are dropped.

# comment (also ;)
lamp <addr> <state>              set one lamp
lamp-all <state>                 set every lamp
plasma text [x y [font]] <text>  write text to the plasma display
plasma clear                     clear the plasma display
plasma row <y> <hex32>           write one full 128-px bitmap row
plasma box <x> <y> <w> <h>       outlined box with a blanked interior
  • <addr> — RIO lamp address, decimal or 0x hex. Valid: 0x000x47 (the 72 buttons), 0x500x5F (keypad 0), 0x600x6F (keypad 1). See PROTOCOL.md §5.
  • <state> — either words: [solid|slow|med|fast] off|dim|bright (flash defaults to solid), or a raw state byte 0x000x3F (the LampRequest state, PROTOCOL.md §3). lamp 0x12 fast bright = flash-fast at full brightness. The board sustains the blink — one command starts a flash, another (solid dim, off, …) ends it.
  • plasma text — the rest of the line is the text, or quote it ("VIPER 1-1"; quotes stripped, no escapes). Two leading numeric tokens are a cursor position x y; omitted (or 0 0) auto-fits and centers (PlasmaPosText). To display something that starts with two numbers, quote it. A third numeric token after the position — with text still following — is an explicit font: 0 auto (by length: ≤9 chars large, else small), 2 small 5×7, 5 large 10×14. Short positioned text otherwise always renders large, which cannot fit inside a plasma box. Encoding is Latin-1 (one byte = one char, the plasma's wire encoding) — do not send UTF-8 for accented characters. Text coalesces per position: a newer queued text replaces an older one at the same x y only, so multi-field layouts (callsign + score) can update one field without losing the others.
  • plasma row — one full bitmap row: <y> 031 (decimal or 0x hex), then exactly 32 hex digits = 16 bytes = 128 pixels, MSB = leftmost. Rows are written strictly in arrival order (unlike plasma text, which coalesces — a bitmap frame is many rows and must not tear); plasma clear discards any queued rows/text. Up to 128 commands queue; beyond that incoming rows are dropped and counted — pace full-frame pushes (a 32-row frame is ~0.77 s of wire time at 9600 baud; stream changed rows, as the native games did). See OUTPUT-INTEGRATION.md.
  • plasma box — an outlined 1-px box with its interior blanked, in pixel coordinates (x 0127, y 031, must fit the panel). This is the overlay chrome the original games drew for their rank|score field over the callsign bitmap. The wire's graphics command spans whole bytes horizontally, so the write covers the byte-aligned span containing the box; pixels inside the span but outside the box are cleared — place boxes on 8-px boundaries when that matters. Boxes queue FIFO with rows.

Malformed lines are dropped and counted (first few are logged); they never cost a client its connection. The endpoint sends no replies.

Gating

RIOJoy state Listeners Commands
Profile active, profile has a Feedback section up applied
Profile active, no Feedback section up dropped
Dormant / native game owns the ports up dropped
Editor session up dropped

Per-profile, in the profile's JSON:

{
  "Feedback": {
    "AllowLampCommands": true,
    "AllowPlasmaText": true,
    "Rumble": { "LargeMotorLamps": [18, 19], "SmallMotorLamps": [96], "Threshold": 24 }
  }
}

A profile without "Feedback" never applies inbound commands. (Editor UI for these settings is a Phase 9 remaining item — edit the JSON for now.)

Lamp ownership: a lamp write to an address the profile maps as a lighted button (iRIO bit 0x8000, HasLamp) is dropped — the input router owns those lamps (bright on press / dim on release) and feedback must not fight it. Such drops are logged once per address. lamp-all silently skips owned lamps.

Rate: lamp commands share the 9600-baud RIO link with the ~55 ms analog poll, so RIOJoy coalesces per-lamp state (latest wins) and sends at most one changed lamp per 25 ms. Spam freely — identical states cost nothing — but a lamp-all sweep takes ~3 s to fully land. Plasma writes are single-flight over a bounded queue: flooded text updates coalesce to the newest value, clear flushes everything queued before it, and bitmap rows stream in order.

Rumble → lamps

With a Rumble config (above) and the ViGEm pad active, XInput vibration set by the game flashes the configured lamps — works with unmodified games: below Threshold (0255) the lamps are off; the rest of the range maps to slow / med / fast flash at full brightness, per motor. Constant rumble costs one lamp command (the board blinks on its own). net48 flavor only (the XP flavor has no ViGEm).

Client snippets

DCS-style Export.lua (a pipe opens as a file on Windows):

local rio = io.open("\\\\.\\pipe\\riojoy-feedback", "w")
-- in your export tick:
if masterCaution then rio:write("lamp 0x12 fast bright\n")
else rio:write("lamp 0x12 off\n") end
rio:write('plasma text "' .. callsign .. '"\n')
rio:flush()

PowerShell, pipe (hand-testing on the cabinet):

$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))
$w.WriteLine('lamp 0x12 fast bright'); $w.WriteLine('plasma text "VIPER 1-1"'); $w.Flush()

PowerShell, UDP (with "UdpPort": 19910 configured):

$u = New-Object Net.Sockets.UdpClient
$b = [Text.Encoding]::GetEncoding(28591).GetBytes("lamp 0x12 fast bright`nplasma text 42 kills")
$u.Send($b, $b.Length, '127.0.0.1', 19910) | Out-Null

Implementation map

src/RioJoy.Core/Feedback/: FeedbackLineParser (grammar → FeedbackCommand), FeedbackLineBuffer (bytes → lines), FeedbackPipeServer / FeedbackUdpListener (transports), CoalescingLampScheduler (the rate governor — all feedback lamp traffic goes through it, never straight to ILampSink), FeedbackRouter (gating + ownership + plasma single-flight), RumbleLampAdapter, and FeedbackService (the façade RioCoordinator owns). Tests mirror the layout in tests/RioJoy.Core.Tests/Feedback/.