# 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](#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](OUTPUT-INTEGRATION.md). ## Endpoints | Transport | Address | Default | |---|---|---| | Named pipe | `\\.\pipe\riojoy-feedback` | **on** | | UDP (loopback only) | `udp://127.0.0.1:` | 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`: ```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](#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 set one lamp lamp-all set every lamp plasma text [x y] write text to the plasma display plasma clear clear the plasma display plasma row write one full 128-px bitmap row ``` - **``** — RIO lamp address, decimal or `0x` hex. Valid: `0x00–0x47` (the 72 buttons), `0x50–0x5F` (keypad 0), `0x60–0x6F` (keypad 1). See [PROTOCOL.md §5](PROTOCOL.md). - **``** — either words: `[solid|slow|med|fast] off|dim|bright` (flash defaults to `solid`), or a raw state byte `0x00–0x3F` (the `LampRequest` state, [PROTOCOL.md §3](PROTOCOL.md)). `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: `` 0–31 (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](OUTPUT-INTEGRATION.md#bitmap-graphics-esc-p). 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: ```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 `row`s 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` (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): ```lua 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): ```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)) $w.WriteLine('lamp 0x12 fast bright'); $w.WriteLine('plasma text "VIPER 1-1"'); $w.Flush() ``` PowerShell, UDP (with `"UdpPort": 19910` configured): ```powershell $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/`.