feedback: game-to-cockpit endpoint (pipe/UDP lamps+plasma), rumble lamp flash

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>
This commit is contained in:
Cyd
2026-07-31 19:31:03 -05:00
co-authored by Claude Fable 5
parent d13d434e88
commit ad7ac19ab2
33 changed files with 2977 additions and 18 deletions
+57 -2
View File
@@ -183,8 +183,11 @@ Implemented in `src/RioJoy.Core/Calibration` + `Plasma` (105 xUnit tests total):
- `PlasmaCommands` ports the `CPlasma` ESC command set (clear/cursor/font/attr/box
draw+fill/text) + `GetFontSize` + the `PlasmaPosText` auto-fit/centering;
`PlasmaDisplay` writes them over the secondary COM transport.
-**Remaining:** hardware verification of axis feel + plasma output; the
game-specific `PlasmaScoreDraw` layout is profile content (Phase 5/7).
-**Remaining:** hardware verification of axis feel + plasma output. Runtime
plasma wiring (secondary port open, greeting, teardown) landed in **Phase 9**;
the legacy game-specific `PlasmaScoreDraw` layout is superseded by the Phase 9
feedback endpoint (external clients draw score/status content —
[`docs/FEEDBACK.md`](FEEDBACK.md)).
### Phase 5 — Tray app + profiles — code-complete ✅
Core logic in `src/RioJoy.Core/Profiles` + `RioRuntime`; UI/OS in `src/RioJoy.Tray`
@@ -431,6 +434,58 @@ XP consumes pre-rendered wallpapers.
computers, adds shortcuts); the single dist zip carries everything
needed for both XP and 10/11, including offline redistributables.
### Phase 9 — Game feedback (game → cockpit) — code-complete ✅
Inbound feedback endpoint + plasma runtime wiring + rumble→lamp mapping, in
`src/RioJoy.Core/Feedback` (425 xUnit tests total across the suite); protocol
spec + client snippets in [`docs/FEEDBACK.md`](FEEDBACK.md). Delivers the
§Profiles promises "Lamp behavior" and "Plasma/VFD content (or 'off')".
- **Endpoint**: `FeedbackPipeServer` serves `\\.\pipe\riojoy-feedback`
(read-only — no replies ever, which sidesteps the 0-buffer pipe write
deadlock class; ≤4 concurrent clients; reconnect forever; vRIO's
`VRioPipeService` server pattern incl. the poke-connect stop) and
`FeedbackUdpListener` binds loopback-only UDP (off by default,
`AppConfig.Feedback.UdpPort` — the transport sim export scripts speak
natively). One shared text line protocol: `FeedbackLineParser` +
`FeedbackLineBuffer` (Latin-1, LF/CRLF, forgiving — malformed lines drop and
log, never the connection). `FeedbackService` façades the lot; it lives in
`RioCoordinator` for the **app lifetime**, so clients keep their connection
across profile switches and dormancy — only command *application* is gated.
- **Rate governor**: `CoalescingLampScheduler` — per-address desired/last-sent
shadow state, at most one *changed* lamp per 25 ms tick, round-robin. All
feedback lamp traffic (pipe/UDP and rumble) posts here; nothing feedback-side
calls `ILampSink` directly, because every lamp command crosses the link's
stop-and-wait command gate (~150 ms worst case) shared with the ~55 ms
analog poll.
- **Routing/precedence**: `FeedbackRouter` — per-profile gating
(`RioProfile.Feedback`, null = feedback off; `AllowLampCommands`/
`AllowPlasmaText`), profile-owned lamps (`HasLamp`) protected from
press/release fights (dropped, logged once per address per attach), plasma
writes single-flight with a latest-pending-wins slot.
- **Plasma wired at last** (closes the Phase 4 ⏳ wiring): `RioCoordinator.
Activate` opens `PlasmaComPort ?? DefaultPlasmaComPort` via the transport
factory (`pipe:` endpoints work for benchless testing; `"off"`/empty skips;
failure becomes a status suffix and never breaks activation), shows
`PlasmaGreeting` auto-centered, blanks + releases the port on teardown (the
native games open this port too). `PlasmaDisplay` gained its missing write
lock — `PosTextAsync` is five transport writes, and concurrent callers used
to interleave ESC fragments (`PlasmaDisplayTests` pins both the sequence and
the no-interleave guarantee).
- **Rumble → lamps** (net48 only): `ViGEmJoystickSink.RumbleChanged` (plain
byte delegate over ViGEm's `FeedbackReceived`; fires on a ViGEm-owned
thread) → `RumbleLampAdapter`: off below `Threshold`, then slow/med/fast
thirds at full brightness per motor, posting only state **changes** so
XInput's identical-value spam costs nothing — the board sustains the blink
from the state byte. Works with unmodified games that set XInput vibration.
- Config: `FeedbackEndpointConfig` (app-wide) + `ProfileFeedbackConfig` /
`RumbleLampConfig` (per-profile); nullable sections = off/defaults, keeping
pre-Phase-9 JSON byte-compatible (round-trip, unset-stays-null, and
shipped-profile cases in `ConfigStoreTests`).
- ⏳ **Remaining:** on-cabinet verification (real lamps + plasma glass, link
feel under game load); timed flash-then-restore effects (the scheduler's
shadow state is the designed hook); editor UI for the per-profile feedback
settings (JSON-only today); shipped client examples (SimHub plugin / DCS
export script) beyond the FEEDBACK.md snippets.
---
## Open items / risks