diff --git a/src/RioJoy.Core/RioRuntime.cs b/src/RioJoy.Core/RioRuntime.cs
index ae8e388..1759a8f 100644
--- a/src/RioJoy.Core/RioRuntime.cs
+++ b/src/RioJoy.Core/RioRuntime.cs
@@ -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);
}
+ ///
+ /// 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.
+ ///
+ public bool EchoAllLamps { get; set; }
+
/// Raised when a diagnostic toggle RIO command fires (raw-axes / poll-rate).
public event Action? DiagnosticToggle;
@@ -47,6 +55,12 @@ public sealed class RioRuntime : IRioCommandSink, IDisposable
///
public event Action? ButtonActivity;
+ /// Raised with the firmware version when a arrives.
+ public event Action? VersionReceived;
+
+ /// Raised for each board/lamp status item in a .
+ public event Action? CheckReceived;
+
/// Subscribe to the link and set all lamps to their idle state.
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();
/// Invoke a RIO command directly (e.g. from the tray menu).
@@ -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);
}
diff --git a/src/RioJoy.Tray/Editor/PanelCanvas.cs b/src/RioJoy.Tray/Editor/PanelCanvas.cs
index 4ea6c45..65021b5 100644
--- a/src/RioJoy.Tray/Editor/PanelCanvas.cs
+++ b/src/RioJoy.Tray/Editor/PanelCanvas.cs
@@ -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)
diff --git a/src/RioJoy.Tray/Editor/PanelView.cs b/src/RioJoy.Tray/Editor/PanelView.cs
index 9bd6ff8..7f8df8a 100644
--- a/src/RioJoy.Tray/Editor/PanelView.cs
+++ b/src/RioJoy.Tray/Editor/PanelView.cs
@@ -75,6 +75,7 @@ public static class PanelView
Func label,
Func function,
Func lit,
+ Func 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
- : Color.FromArgb(120, 102, 30)) // Secondary/Screen — dim yellow
- : lit(b.Address)
- ? Color.FromArgb(215, 60, 60) // lamp ON (IsLit checked)
+ ? (bright ? Color.FromArgb(240, 205, 50) // Secondary/Screen — yellow lamp
+ : Color.FromArgb(120, 102, 30)) // Secondary/Screen — dim yellow
+ : 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
}
}
- ///
- /// Outline the buttons currently held down on the live RIO (the editor's
- /// check-button-function feedback), drawn over the normal cells.
- ///
- public static void PaintLivePressed(Graphics g, IReadOnlyList buttons, ISet 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);
- }
- }
-
/// Hit-test the [JoyStick] toggle cells; null if the point misses.
public static CalibrationCell? HitTestCalibration(Point p)
{
diff --git a/src/RioJoy.Tray/Editor/ProfileEditorForm.cs b/src/RioJoy.Tray/Editor/ProfileEditorForm.cs
index 0e78907..c716d51 100644
--- a/src/RioJoy.Tray/Editor/ProfileEditorForm.cs
+++ b/src/RioJoy.Tray/Editor/ProfileEditorForm.cs
@@ -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 _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),
};
/// A pick-list entry: what the user sees and the byte it maps to.
@@ -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.
///
- public void ShowLiveActivity(int address, bool pressed)
+ public void ShowLiveActivity(int address, bool pressed) =>
+ RunOnUi(() => _canvas.SetLivePressed(address, pressed));
+
+ /// Show the firmware version from a RIO version reply (serial thread).
+ public void ShowVersion(VersionInfo version) =>
+ RunOnUi(() => { _version = version; RenderStatus(); });
+
+ /// Append a board/lamp status item from a RIO check reply (serial thread).
+ 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 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(),
+ };
}
diff --git a/src/RioJoy.Tray/RioCoordinator.cs b/src/RioJoy.Tray/RioCoordinator.cs
index 3420040..bf55eff 100644
--- a/src/RioJoy.Tray/RioCoordinator.cs
+++ b/src/RioJoy.Tray/RioCoordinator.cs
@@ -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();
diff --git a/src/RioJoy.Tray/TrayApplicationContext.cs b/src/RioJoy.Tray/TrayApplicationContext.cs
index 204feb7..6e2e93c 100644
--- a/src/RioJoy.Tray/TrayApplicationContext.cs
+++ b/src/RioJoy.Tray/TrayApplicationContext.cs
@@ -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();