Editor: live lamp echo, bright-cell feedback, version/status readout
While a profile is being edited, light every pressed button on the RIO (RioRuntime.EchoAllLamps, enabled for editor sessions) regardless of mapping, so a physical press always responds. On the panel, a held button now brightens the cell in its own colour (red MFD / blue keypad / yellow board) instead of a cyan outline. Surface the RIO version/check replies (RioRuntime.VersionReceived/ CheckReceived) and show them grouped in a new "RIO reply" box below the command buttons when "Request version & status" is clicked. Drop the two diagnostic readout-toggle buttons (comms bring-up leftovers). Verified against the partial RIO on COM1 (firmware 4.2; MFD + keypad light and report; missing-board status shown). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ public sealed class RioRuntime : IRioCommandSink, IDisposable
|
||||
private readonly IJoystickSink _joystick;
|
||||
private readonly AxisCalibrator _calibrator;
|
||||
private readonly InputRouter _router;
|
||||
private readonly ILampSink _lamp;
|
||||
private bool _started;
|
||||
|
||||
public RioRuntime(
|
||||
@@ -33,10 +34,17 @@ public sealed class RioRuntime : IRioCommandSink, IDisposable
|
||||
_joystick = joystick ?? throw new ArgumentNullException(nameof(joystick));
|
||||
_calibrator = calibrator ?? new AxisCalibrator();
|
||||
|
||||
var lamp = new SerialLampSink(link);
|
||||
_router = new InputRouter(map, input, joystick, lamp, this);
|
||||
_lamp = new SerialLampSink(link);
|
||||
_router = new InputRouter(map, input, joystick, _lamp, this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When set, light every pressed button on the RIO (bright on press, dim on
|
||||
/// release) regardless of how — or whether — it is mapped. Used by the editor so
|
||||
/// a physical press lights the button even with an empty/partial profile.
|
||||
/// </summary>
|
||||
public bool EchoAllLamps { get; set; }
|
||||
|
||||
/// <summary>Raised when a diagnostic toggle RIO command fires (raw-axes / poll-rate).</summary>
|
||||
public event Action<RioCommandCode>? DiagnosticToggle;
|
||||
|
||||
@@ -47,6 +55,12 @@ public sealed class RioRuntime : IRioCommandSink, IDisposable
|
||||
/// </summary>
|
||||
public event Action<int, bool>? ButtonActivity;
|
||||
|
||||
/// <summary>Raised with the firmware version when a <see cref="RioCommand.VersionReply"/> arrives.</summary>
|
||||
public event Action<VersionInfo>? VersionReceived;
|
||||
|
||||
/// <summary>Raised for each board/lamp status item in a <see cref="RioCommand.CheckReply"/>.</summary>
|
||||
public event Action<CheckStatus>? CheckReceived;
|
||||
|
||||
/// <summary>Subscribe to the link and set all lamps to their idle state.</summary>
|
||||
public void Start()
|
||||
{
|
||||
@@ -56,6 +70,8 @@ public sealed class RioRuntime : IRioCommandSink, IDisposable
|
||||
|
||||
_link.PacketReceived += OnPacket;
|
||||
_link.AnalogReceived += OnAnalog;
|
||||
_link.VersionReceived += OnVersion;
|
||||
_link.CheckReceived += OnCheck;
|
||||
_router.InitializeLamps();
|
||||
}
|
||||
|
||||
@@ -68,8 +84,14 @@ public sealed class RioRuntime : IRioCommandSink, IDisposable
|
||||
|
||||
_link.PacketReceived -= OnPacket;
|
||||
_link.AnalogReceived -= OnAnalog;
|
||||
_link.VersionReceived -= OnVersion;
|
||||
_link.CheckReceived -= OnCheck;
|
||||
}
|
||||
|
||||
private void OnVersion(VersionInfo version) => VersionReceived?.Invoke(version);
|
||||
|
||||
private void OnCheck(CheckStatus status) => CheckReceived?.Invoke(status);
|
||||
|
||||
public void Dispose() => Stop();
|
||||
|
||||
/// <summary>Invoke a RIO command directly (e.g. from the tray menu).</summary>
|
||||
@@ -114,6 +136,11 @@ public sealed class RioRuntime : IRioCommandSink, IDisposable
|
||||
else
|
||||
_router.Release(address);
|
||||
|
||||
// Light the button on the RIO even when it is unmapped (the router only lamps
|
||||
// mapped lamp-buttons), so the editor's check-function workflow always responds.
|
||||
if (EchoAllLamps)
|
||||
_lamp.SetLamp(address, pressed ? RioLampState.SolidBright : RioLampState.SolidDim);
|
||||
|
||||
ButtonActivity?.Invoke(address, pressed);
|
||||
}
|
||||
|
||||
|
||||
@@ -67,9 +67,9 @@ internal sealed class PanelCanvas : Control
|
||||
a => LabelProvider?.Invoke(a),
|
||||
a => FunctionProvider?.Invoke(a),
|
||||
a => LitProvider?.Invoke(a) ?? false,
|
||||
_livePressed.Contains,
|
||||
SelectedAddress);
|
||||
PanelView.PaintCalibration(e.Graphics, s => CalibrationProvider?.Invoke(s) ?? false);
|
||||
PanelView.PaintLivePressed(e.Graphics, AllButtons, _livePressed);
|
||||
}
|
||||
|
||||
protected override void OnMouseDown(MouseEventArgs e)
|
||||
|
||||
@@ -75,6 +75,7 @@ public static class PanelView
|
||||
Func<int, string?> label,
|
||||
Func<int, string?> function,
|
||||
Func<int, bool> lit,
|
||||
Func<int, bool> live,
|
||||
int? selected)
|
||||
{
|
||||
g.Clear(Color.FromArgb(28, 28, 28));
|
||||
@@ -109,14 +110,18 @@ public static class PanelView
|
||||
string? fn = function(b.Address);
|
||||
bool assigned = !string.IsNullOrEmpty(fn);
|
||||
|
||||
// A live (physically held) button lights bright in its own colour, as does
|
||||
// a lamp configured ON; otherwise the cell shows its idle/dim shade.
|
||||
bool yellow = b.Group.Title is "Secondary" or "Screen";
|
||||
bool bright = lit(b.Address) || live(b.Address);
|
||||
Color fill = !b.LampCapable
|
||||
? Color.FromArgb(150, 182, 226) // keypad — neutral blue
|
||||
? (live(b.Address) ? Color.FromArgb(205, 228, 255) // keypad — bright blue when pressed
|
||||
: Color.FromArgb(150, 182, 226)) // keypad — neutral blue
|
||||
: yellow
|
||||
? (lit(b.Address) ? Color.FromArgb(240, 205, 50) // Secondary/Screen — yellow lamp
|
||||
? (bright ? Color.FromArgb(240, 205, 50) // Secondary/Screen — yellow lamp
|
||||
: Color.FromArgb(120, 102, 30)) // Secondary/Screen — dim yellow
|
||||
: lit(b.Address)
|
||||
? Color.FromArgb(215, 60, 60) // lamp ON (IsLit checked)
|
||||
: bright
|
||||
? Color.FromArgb(215, 60, 60) // lamp ON (lit or held)
|
||||
: Color.FromArgb(86, 48, 48); // lamp OFF
|
||||
using (var fb = new SolidBrush(fill)) g.FillRectangle(fb, r);
|
||||
|
||||
@@ -215,23 +220,6 @@ public static class PanelView
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Outline the buttons currently held down on the live RIO (the editor's
|
||||
/// check-button-function feedback), drawn over the normal cells.
|
||||
/// </summary>
|
||||
public static void PaintLivePressed(Graphics g, IReadOnlyList<PanelButton> buttons, ISet<int> pressed)
|
||||
{
|
||||
if (pressed.Count == 0)
|
||||
return;
|
||||
using var pen = new Pen(Color.FromArgb(90, 220, 255), 3f); // cyan = live press
|
||||
foreach (PanelButton b in buttons)
|
||||
if (pressed.Contains(b.Address))
|
||||
{
|
||||
Rectangle r = Cell(b.Col, b.Row);
|
||||
g.DrawRectangle(pen, r.X, r.Y, r.Width - 1, r.Height - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Hit-test the <c>[JoyStick]</c> toggle cells; null if the point misses.</summary>
|
||||
public static CalibrationCell? HitTestCalibration(Point p)
|
||||
{
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
using RioJoy.Core.Editing;
|
||||
using RioJoy.Core.Hid;
|
||||
using RioJoy.Core.Mapping;
|
||||
using RioJoy.Core.Profiles;
|
||||
using RioJoy.Core.Protocol;
|
||||
|
||||
namespace RioJoy.Tray.Editor;
|
||||
|
||||
@@ -42,9 +44,26 @@ public sealed class ProfileEditorForm : Form
|
||||
private readonly Button _save = new() { Text = "Save profile", Location = new Point(70, 240), Width = 110 };
|
||||
private readonly Button _close = new() { Text = "Close", Location = new Point(190, 240), Width = 80 };
|
||||
|
||||
private readonly TextBox _statusBox = new()
|
||||
{
|
||||
Location = new Point(12, 566),
|
||||
Size = new Size(306, 190),
|
||||
Multiline = true,
|
||||
ReadOnly = true,
|
||||
ScrollBars = ScrollBars.Vertical,
|
||||
BackColor = Color.FromArgb(24, 24, 24),
|
||||
ForeColor = Color.Gainsboro,
|
||||
Font = new Font("Consolas", 8f),
|
||||
Text = "Press \"Request version & status\" to query the RIO.",
|
||||
};
|
||||
|
||||
private int? _address;
|
||||
private bool _loading;
|
||||
|
||||
// The most recent version/status reply, accumulated as CheckReply items stream in.
|
||||
private VersionInfo? _version;
|
||||
private readonly List<CheckStatus> _checks = new();
|
||||
|
||||
// The RIO device commands, mirroring the tray menu, surfaced as live buttons so
|
||||
// they can be fired against the RIO while editing (the editor holds the port).
|
||||
private static readonly (string Label, RioCommandCode Code)[] RioCommands =
|
||||
@@ -57,8 +76,6 @@ public sealed class ProfileEditorForm : Form
|
||||
("Reset horizontal joystick", RioCommandCode.ResetHorizontalJoystick),
|
||||
("Reset digital (general)", RioCommandCode.GeneralReset),
|
||||
("Request version & status", RioCommandCode.RequestVersionAndCheck),
|
||||
("Toggle raw-axes readout", RioCommandCode.ToggleRawAxesReadout),
|
||||
("Toggle poll-rate readout", RioCommandCode.TogglePollRateReadout),
|
||||
};
|
||||
|
||||
/// <summary>A pick-list entry: what the user sees and the byte it maps to.</summary>
|
||||
@@ -125,6 +142,8 @@ public sealed class ProfileEditorForm : Form
|
||||
panel.Controls.Add(_valueCombo);
|
||||
panel.Controls.AddRange(new Control[] { _shift, _ctrl, _alt, _ext, _lit, _apply, _unassign, _save, _close });
|
||||
panel.Controls.Add(BuildCommandGroup());
|
||||
panel.Controls.Add(new Label { Text = "RIO reply:", Location = new Point(12, 548), AutoSize = true });
|
||||
panel.Controls.Add(_statusBox);
|
||||
|
||||
return panel;
|
||||
}
|
||||
@@ -132,14 +151,24 @@ public sealed class ProfileEditorForm : Form
|
||||
// A button per RIO device command, fired against the live RIO via CommandRequested.
|
||||
private GroupBox BuildCommandGroup()
|
||||
{
|
||||
var group = new GroupBox { Text = "RIO commands (live)", Location = new Point(12, 278), Size = new Size(306, 330) };
|
||||
var group = new GroupBox { Text = "RIO commands (live)", Location = new Point(12, 278), Size = new Size(306, 262) };
|
||||
|
||||
int y = 24;
|
||||
foreach ((string label, RioCommandCode code) in RioCommands)
|
||||
{
|
||||
RioCommandCode captured = code;
|
||||
var button = new Button { Text = label, Location = new Point(10, y), Width = 286, Height = 26, UseMnemonic = false };
|
||||
button.Click += (_, _) => CommandRequested?.Invoke(captured);
|
||||
button.Click += (_, _) =>
|
||||
{
|
||||
// The version/status reply streams back into the box below; clear it first.
|
||||
if (captured == RioCommandCode.RequestVersionAndCheck)
|
||||
{
|
||||
_version = null;
|
||||
_checks.Clear();
|
||||
_statusBox.Text = "Requesting version & status…";
|
||||
}
|
||||
CommandRequested?.Invoke(captured);
|
||||
};
|
||||
group.Controls.Add(button);
|
||||
y += 29;
|
||||
}
|
||||
@@ -347,17 +376,60 @@ public sealed class ProfileEditorForm : Form
|
||||
/// Lets the user press a physical button and see which cell it is without any
|
||||
/// keystrokes being injected.
|
||||
/// </summary>
|
||||
public void ShowLiveActivity(int address, bool pressed)
|
||||
public void ShowLiveActivity(int address, bool pressed) =>
|
||||
RunOnUi(() => _canvas.SetLivePressed(address, pressed));
|
||||
|
||||
/// <summary>Show the firmware version from a RIO version reply (serial thread).</summary>
|
||||
public void ShowVersion(VersionInfo version) =>
|
||||
RunOnUi(() => { _version = version; RenderStatus(); });
|
||||
|
||||
/// <summary>Append a board/lamp status item from a RIO check reply (serial thread).</summary>
|
||||
public void AddCheckStatus(CheckStatus status) =>
|
||||
RunOnUi(() => { _checks.Add(status); RenderStatus(); });
|
||||
|
||||
// Marshal an action onto the UI thread, tolerating a closing/disposed form.
|
||||
private void RunOnUi(Action action)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return;
|
||||
if (InvokeRequired)
|
||||
{
|
||||
try { BeginInvoke(() => ShowLiveActivity(address, pressed)); }
|
||||
try { BeginInvoke(action); }
|
||||
catch (ObjectDisposedException) { }
|
||||
catch (InvalidOperationException) { } // handle not created / form closing
|
||||
return;
|
||||
}
|
||||
_canvas.SetLivePressed(address, pressed);
|
||||
action();
|
||||
}
|
||||
|
||||
// Render the accumulated version + check reply into the status box.
|
||||
private void RenderStatus()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine(_version is { } v ? $"Firmware: {v}" : "Firmware: (awaiting reply…)");
|
||||
|
||||
if (_checks.Count == 0)
|
||||
{
|
||||
sb.Append("Status: (awaiting reply…)");
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (IGrouping<RioStatusType, CheckStatus> group in _checks.GroupBy(c => c.Type))
|
||||
sb.AppendLine($"{Friendly(group.Key)}: {string.Join(", ", group.Select(c => c.Number))}");
|
||||
}
|
||||
|
||||
_statusBox.Text = sb.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
private static string Friendly(RioStatusType type) => type switch
|
||||
{
|
||||
RioStatusType.BoardOk => "Boards OK",
|
||||
RioStatusType.BoardMissing => "Boards missing",
|
||||
RioStatusType.BoardBad => "Boards bad",
|
||||
RioStatusType.LampBad => "Lamps bad",
|
||||
RioStatusType.RestartCount => "Restart count",
|
||||
RioStatusType.AbandonCount => "Abandon count",
|
||||
RioStatusType.FullBufferCount => "Full-buffer count",
|
||||
_ => type.ToString(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -176,6 +176,8 @@ public sealed class RioCoordinator : IDisposable
|
||||
joystick,
|
||||
new AxisCalibrator(profile.Calibration));
|
||||
|
||||
_runtime.EchoAllLamps = !routeInput; // editor: light any pressed button on the RIO
|
||||
|
||||
_cts = new CancellationTokenSource();
|
||||
_ = _link.RunAsync(_cts.Token);
|
||||
_runtime.Start();
|
||||
|
||||
@@ -155,12 +155,18 @@ internal sealed class TrayApplicationContext : ApplicationContext
|
||||
{
|
||||
activity = editor.ShowLiveActivity;
|
||||
runtime.ButtonActivity += activity;
|
||||
runtime.VersionReceived += editor.ShowVersion;
|
||||
runtime.CheckReceived += editor.AddCheckStatus;
|
||||
}
|
||||
|
||||
editor.FormClosed += (_, _) =>
|
||||
{
|
||||
if (runtime is not null && activity is not null)
|
||||
{
|
||||
runtime.ButtonActivity -= activity;
|
||||
runtime.VersionReceived -= editor.ShowVersion;
|
||||
runtime.CheckReceived -= editor.AddCheckStatus;
|
||||
}
|
||||
_coordinator.EndEditorSession();
|
||||
_watcher.Reset(); // re-sync with the foreground app (may re-activate a game)
|
||||
_watcher.Poll();
|
||||
|
||||
Reference in New Issue
Block a user