Phase 9: FeedbackPipeServer (\\.\pipe\riojoy-feedback) + loopback UDP share a forgiving text line protocol into FeedbackRouter; CoalescingLampScheduler rate- governs the 9600-baud link; plasma finally wired into activation (greeting, teardown blank, PlasmaDisplay write lock); ViGEm FeedbackReceived drives RumbleLampAdapter. Per-profile Feedback config, docs/FEEDBACK.md, 425 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6.1 KiB
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).
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
<addr>— RIO lamp address, decimal or0xhex. Valid:0x00–0x47(the 72 buttons),0x50–0x5F(keypad 0),0x60–0x6F(keypad 1). See PROTOCOL.md §5.<state>— either words:[solid|slow|med|fast] off|dim|bright(flash defaults tosolid), or a raw state byte0x00–0x3F(theLampRequeststate, 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 positionx y; omitted (or0 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.
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,
latest-pending-wins: flood-updating a score means only the newest pending text
is written.
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 (0–255) 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/.