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

165 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:<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`:
```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 <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](PROTOCOL.md).
- **`<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](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: `<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](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` (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):
```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/`.