Files
riojoy/docs/FEEDBACK.md
T
CydandClaude Fable 5 44b636ddd3 plasma: ESC P bitmap rows through the feedback endpoint (plasma row)
PlasmaCommands.GraphicsWrite/GraphicsRow port the display firmware graphics
command (ESC P s y x w h, MSB-left); PlasmaDisplay.RowAsync writes a locked
whole-row update; the line protocol gains `plasma row <y> <hex32>`. The
router plasma slot becomes a bounded FIFO queue: rows stream in order (a
frame must not tear), texts still coalesce to the newest, clear flushes, cap
128 with counted drops. Docs updated; 442 tests.

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

7.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] <text>  write text to the plasma display
plasma clear              clear the plasma display
plasma row <y> <hex32>    write one full 128-px bitmap row
  • <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. Encoding is Latin-1 (one byte = one char, the plasma's wire encoding) — do not send UTF-8 for accented characters.
  • 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 to the newest — 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.

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/.