Editor cockpit rework + dormant-by-default serial port use

Profile editor:
- Cockpit-style encoder gauges centered over the Upper Middle MFD: Z left,
  X/Y right, and L/R/Rz arranged as a big "U".
- [JoyStick] section surfaced as a vertical "Axis" toggle column (6 inverts
  + ZR mix), written back to the profile's calibration.
- Tighten the panel layout (close gaps, trim empty border columns); colour the
  Secondary/Screen columns yellow; rename the "Joystick / Hat" board to
  "Joystick"; add an "Import .ini..." tray entry.
- "RIO commands (live)" button group (all RioCommandCodes) fired at the live RIO.

Serial-port handling:
- Start dormant; only take the COM port for a profiled game (auto-switch) or
  while editing, so the native games can open the port without clashing.
- Editor sessions hold the port but route keyboard/mouse/joystick to no-op
  sinks (NullInputSink), so button function can be checked without injecting
  input; RioRuntime.ButtonActivity drives a live cyan press indicator and
  lamp feedback still applies.
- AutoSwitchWatcher.Reset() re-syncs the watcher after an editor session.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-06-27 22:55:53 -05:00
co-authored by Claude Opus 4.8
parent a4a3b58f7e
commit 4994ab699f
12 changed files with 481 additions and 45 deletions
+8 -8
View File
@@ -70,23 +70,23 @@ public static class CockpitPanel
{
// Upper MFD row.
new PanelGroup("Upper Left MFD", PanelGroupKind.Mfd, 4, 2, true, 0, 1, Mfd(0x2F)),
new PanelGroup("Upper Middle MFD", PanelGroupKind.Mfd, 4, 2, true, 5, 1, Mfd(0x27)),
new PanelGroup("Upper Right MFD", PanelGroupKind.Mfd, 4, 2, true, 10, 1, Mfd(0x37)),
new PanelGroup("Upper Middle MFD", PanelGroupKind.Mfd, 4, 2, true, 4, 1, Mfd(0x27)),
new PanelGroup("Upper Right MFD", PanelGroupKind.Mfd, 4, 2, true, 8, 1, Mfd(0x37)),
// Lower MFD row.
new PanelGroup("Lower Left MFD", PanelGroupKind.Mfd, 4, 2, true, 0, 5, Mfd(0x0F)),
new PanelGroup("Lower Right MFD", PanelGroupKind.Mfd, 4, 2, true, 10, 5, Mfd(0x07)),
new PanelGroup("Lower Right MFD", PanelGroupKind.Mfd, 4, 2, true, 8, 5, Mfd(0x07)),
// Board columns.
new PanelGroup("Throttle", PanelGroupKind.Column, 1, 8, true, 0, 9, Col8(0x38)),
new PanelGroup("Secondary", PanelGroupKind.Column, 1, 8, true, 5, 9, Col8(0x10)),
new PanelGroup("Screen", PanelGroupKind.Column, 1, 8, true, 7, 9, Col8(0x18)),
new PanelGroup("Joystick / Hat", PanelGroupKind.Column, 1, 8, true, 13, 9, Col8(0x40)),
new PanelGroup("Secondary", PanelGroupKind.Column, 1, 8, true, 4, 9, Col8(0x10)),
new PanelGroup("Screen", PanelGroupKind.Column, 1, 8, true, 6, 9, Col8(0x18)),
new PanelGroup("Joystick", PanelGroupKind.Column, 1, 8, true, 11, 9, Col8(0x40)),
// Keypads (no lamps): internal in the middle (below Upper Middle MFD, above
// the Secondary/Screen columns); external between those columns and Joystick.
new PanelGroup("Internal Keypad", PanelGroupKind.Keypad, 4, 4, false, 5, 4, Keypad(0x50)),
new PanelGroup("External Keypad", PanelGroupKind.Keypad, 4, 4, false, 9, 9, Keypad(0x60)),
new PanelGroup("Internal Keypad", PanelGroupKind.Keypad, 4, 4, false, 4, 4, Keypad(0x50)),
new PanelGroup("External Keypad", PanelGroupKind.Keypad, 4, 4, false, 7, 9, Keypad(0x60)),
};
/// <summary>Flatten the groups to positioned <see cref="PanelButton"/>s (absolute cells).</summary>
+17
View File
@@ -0,0 +1,17 @@
namespace RioJoy.Core.Mapping;
/// <summary>
/// A no-op <see cref="IInputSink"/>: swallows all keyboard/mouse output. Used while
/// a profile is being edited so the RIO can be exercised to check button function
/// without injecting keystrokes/clicks into the OS (lamp feedback still applies).
/// </summary>
public sealed class NullInputSink : IInputSink
{
public void KeyDown(byte virtualKey, bool extended) { }
public void KeyUp(byte virtualKey, bool extended) { }
public void MouseMove(int dx, int dy) { }
public void MouseButton(MouseButton button, bool down) { }
}
@@ -31,6 +31,13 @@ public sealed class AutoSwitchWatcher
/// <summary>The most recently resolved decision (null until the first <see cref="Poll"/>).</summary>
public SwitchDecision? Current => _last;
/// <summary>
/// Forget the last decision so the next <see cref="Poll"/> re-raises
/// <see cref="DecisionChanged"/> even if the resolved decision is unchanged. Used
/// after a manual/editor override to re-sync the coordinator with the foreground app.
/// </summary>
public void Reset() => _last = null;
/// <summary>Resolve once; raise <see cref="DecisionChanged"/> if it changed. Returns the decision.</summary>
public SwitchDecision Poll()
{
+22 -4
View File
@@ -40,6 +40,13 @@ public sealed class RioRuntime : IRioCommandSink, IDisposable
/// <summary>Raised when a diagnostic toggle RIO command fires (raw-axes / poll-rate).</summary>
public event Action<RioCommandCode>? DiagnosticToggle;
/// <summary>
/// Raised for every decoded button/keypad event (RIO address, <c>true</c> on press
/// and <c>false</c> on release) — independent of how the entry is routed, so the
/// editor can show live button activity even with no-op output sinks.
/// </summary>
public event Action<int, bool>? ButtonActivity;
/// <summary>Subscribe to the link and set all lamps to their idle state.</summary>
public void Start()
{
@@ -85,20 +92,31 @@ public sealed class RioRuntime : IRioCommandSink, IDisposable
switch (packet.Command)
{
case RioCommand.ButtonPressed when p[0] < RioAddress.ButtonCount:
_router.Press(RioAddress.FromButton(p[0]));
Activity(RioAddress.FromButton(p[0]), pressed: true);
break;
case RioCommand.ButtonReleased when p[0] < RioAddress.ButtonCount:
_router.Release(RioAddress.FromButton(p[0]));
Activity(RioAddress.FromButton(p[0]), pressed: false);
break;
case RioCommand.KeyPressed when IsKeypad(p):
_router.Press(RioAddress.FromKeypad(p[0], p[1]));
Activity(RioAddress.FromKeypad(p[0], p[1]), pressed: true);
break;
case RioCommand.KeyReleased when IsKeypad(p):
_router.Release(RioAddress.FromKeypad(p[0], p[1]));
Activity(RioAddress.FromKeypad(p[0], p[1]), pressed: false);
break;
}
}
// Route the event through the input pipeline and notify any live-activity listener.
private void Activity(int address, bool pressed)
{
if (pressed)
_router.Press(address);
else
_router.Release(address);
ButtonActivity?.Invoke(address, pressed);
}
private static bool IsKeypad(ReadOnlySpan<byte> p) => p[0] is 0 or 1 && p[1] <= 0x0F;
// IRioCommandSink: RIO commands routed from a button (RIOcmd port).