diff --git a/src/RioJoy.Tray/Editor/ProfileEditorForm.cs b/src/RioJoy.Tray/Editor/ProfileEditorForm.cs
index 4d2c379..e353ea1 100644
--- a/src/RioJoy.Tray/Editor/ProfileEditorForm.cs
+++ b/src/RioJoy.Tray/Editor/ProfileEditorForm.cs
@@ -5,6 +5,7 @@ using RioJoy.Core.Output;
using RioJoy.Core.Mapping;
using RioJoy.Core.Profiles;
using RioJoy.Core.Protocol;
+using RioJoy.Core.Serial;
namespace RioJoy.Tray.Editor;
@@ -30,26 +31,32 @@ public sealed class ProfileEditorForm : Form
private readonly TextBox _nameBox = new() { Location = new Point(66, 12), Width = 234 };
private readonly TextBox _matchBox = new() { Location = new Point(66, 40), Width = 234 };
- private readonly Label _info = new() { AutoSize = true, Location = new Point(12, 68), MaximumSize = new Size(310, 0) };
- private readonly TextBox _labelBox = new() { Location = new Point(70, 100), Width = 230 };
- private readonly ComboBox _kindBox = new() { Location = new Point(70, 134), Width = 150, DropDownStyle = ComboBoxStyle.DropDownList };
- private readonly Label _valueLabel = new() { Text = "Key:", Location = new Point(12, 171), AutoSize = true };
- private readonly ComboBox _valueCombo = new() { Location = new Point(70, 168), Width = 230, DropDownStyle = ComboBoxStyle.DropDownList };
- private readonly CheckBox _shift = new() { Text = "Shift", Location = new Point(70, 200), AutoSize = true };
- private readonly CheckBox _ctrl = new() { Text = "Ctrl", Location = new Point(140, 200), AutoSize = true };
- private readonly CheckBox _alt = new() { Text = "Alt", Location = new Point(200, 200), AutoSize = true };
- private readonly CheckBox _ext = new() { Text = "Ext", Location = new Point(250, 200), AutoSize = true };
- private readonly CheckBox _lit = new() { Text = "Lit", Location = new Point(70, 228), AutoSize = true };
- private readonly Button _apply = new() { Text = "Apply to cell", Location = new Point(70, 262), Width = 110 };
- private readonly Button _unassign = new() { Text = "Unassign", Location = new Point(190, 262), Width = 110 };
- private readonly Button _save = new() { Text = "Save profile", Location = new Point(70, 296), Width = 110 };
- private readonly Button _close = new() { Text = "Close", Location = new Point(190, 296), Width = 80 };
- private readonly CheckBox _outputToggle = new() { Text = "Send button output to the PC", Location = new Point(12, 328), AutoSize = true };
+
+ // RIO endpoint: editable so any COM name or pipe:name goes; the drop-down
+ // offers the app default, the machine's COM ports, and the vRIO pipe.
+ private readonly ComboBox _portBox = new() { Location = new Point(66, 68), Width = 234, DropDownStyle = ComboBoxStyle.DropDown };
+ private readonly string _defaultPortItem;
+
+ private readonly Label _info = new() { AutoSize = true, Location = new Point(12, 96), MaximumSize = new Size(310, 0) };
+ private readonly TextBox _labelBox = new() { Location = new Point(70, 128), Width = 230 };
+ private readonly ComboBox _kindBox = new() { Location = new Point(70, 162), Width = 150, DropDownStyle = ComboBoxStyle.DropDownList };
+ private readonly Label _valueLabel = new() { Text = "Key:", Location = new Point(12, 199), AutoSize = true };
+ private readonly ComboBox _valueCombo = new() { Location = new Point(70, 196), Width = 230, DropDownStyle = ComboBoxStyle.DropDownList };
+ private readonly CheckBox _shift = new() { Text = "Shift", Location = new Point(70, 228), AutoSize = true };
+ private readonly CheckBox _ctrl = new() { Text = "Ctrl", Location = new Point(140, 228), AutoSize = true };
+ private readonly CheckBox _alt = new() { Text = "Alt", Location = new Point(200, 228), AutoSize = true };
+ private readonly CheckBox _ext = new() { Text = "Ext", Location = new Point(250, 228), AutoSize = true };
+ private readonly CheckBox _lit = new() { Text = "Lit", Location = new Point(70, 256), AutoSize = true };
+ private readonly Button _apply = new() { Text = "Apply to cell", Location = new Point(70, 290), Width = 110 };
+ private readonly Button _unassign = new() { Text = "Unassign", Location = new Point(190, 290), Width = 110 };
+ private readonly Button _save = new() { Text = "Save profile", Location = new Point(70, 324), Width = 110 };
+ private readonly Button _close = new() { Text = "Close", Location = new Point(190, 324), Width = 80 };
+ private readonly CheckBox _outputToggle = new() { Text = "Send button output to the PC", Location = new Point(12, 356), AutoSize = true };
private readonly TextBox _statusBox = new()
{
- Location = new Point(12, 646),
- Size = new Size(306, 145),
+ Location = new Point(12, 674),
+ Size = new Size(306, 140),
Multiline = true,
ReadOnly = true,
ScrollBars = ScrollBars.Vertical,
@@ -104,13 +111,38 @@ public sealed class ProfileEditorForm : Form
/// Raised when the "send output to the PC" toggle changes (true = send).
public event Action? OutputsEnabledChanged;
- public ProfileEditorForm(RioProfile profile)
+ /// The profile to edit (mutated in place; Save persists).
+ ///
+ /// The app-wide RIO endpoint (), shown
+ /// on the port picker's "(app default)" entry. Null just hides the value.
+ ///
+ public ProfileEditorForm(RioProfile profile, string? defaultEndpoint = null)
{
_profile = profile ?? throw new ArgumentNullException(nameof(profile));
Text = $"RIOJoy — Edit profile: {profile.Name}";
_nameBox.Text = profile.Name;
_matchBox.Text = string.Join(", ", profile.MatchExecutables);
+
+ // Endpoint suggestions: app default, the machine's COM ports, the vRIO
+ // pipe. Free text stays allowed — any COM name or pipe:name works.
+ _defaultPortItem = defaultEndpoint is null ? "(app default)" : $"(app default: {defaultEndpoint})";
+ _portBox.Items.Add(_defaultPortItem);
+ try
+ {
+ foreach (string port in System.IO.Ports.SerialPort.GetPortNames()
+ .Distinct().OrderBy(p => p, StringComparer.OrdinalIgnoreCase))
+ _portBox.Items.Add(port);
+ }
+ catch (Exception)
+ {
+ // Enumerating ports is best-effort (registry read) — typing still works.
+ }
+ _portBox.Items.Add(RioTransportFactory.PipeScheme + "vrio");
+ if (string.IsNullOrWhiteSpace(profile.RioComPort))
+ _portBox.SelectedIndex = 0;
+ else
+ _portBox.Text = profile.RioComPort;
ClientSize = new Size(1320, 820);
StartPosition = FormStartPosition.CenterScreen;
MinimumSize = new Size(900, 500);
@@ -162,16 +194,18 @@ public sealed class ProfileEditorForm : Form
panel.Controls.Add(_nameBox);
panel.Controls.Add(new Label { Text = "Triggers:", Location = new Point(12, 43), AutoSize = true });
panel.Controls.Add(_matchBox);
+ panel.Controls.Add(new Label { Text = "RIO port:", Location = new Point(12, 71), AutoSize = true });
+ panel.Controls.Add(_portBox);
panel.Controls.Add(_info);
- panel.Controls.Add(new Label { Text = "Label:", Location = new Point(12, 103), AutoSize = true });
+ panel.Controls.Add(new Label { Text = "Label:", Location = new Point(12, 131), AutoSize = true });
panel.Controls.Add(_labelBox);
- panel.Controls.Add(new Label { Text = "Action:", Location = new Point(12, 137), AutoSize = true });
+ panel.Controls.Add(new Label { Text = "Action:", Location = new Point(12, 165), AutoSize = true });
panel.Controls.Add(_kindBox);
panel.Controls.Add(_valueLabel);
panel.Controls.Add(_valueCombo);
panel.Controls.AddRange(new Control[] { _shift, _ctrl, _alt, _ext, _lit, _apply, _unassign, _save, _close, _outputToggle });
panel.Controls.Add(BuildCommandGroup());
- panel.Controls.Add(new Label { Text = "RIO reply:", Location = new Point(12, 628), AutoSize = true });
+ panel.Controls.Add(new Label { Text = "RIO reply:", Location = new Point(12, 656), AutoSize = true });
panel.Controls.Add(_statusBox);
return panel;
@@ -180,7 +214,7 @@ 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, 358), Size = new Size(306, 262) };
+ var group = new GroupBox { Text = "RIO commands (live)", Location = new Point(12, 386), Size = new Size(306, 262) };
int y = 24;
foreach ((string label, RioCommandCode code) in RioCommands)
@@ -359,6 +393,11 @@ public sealed class ProfileEditorForm : Form
.Where(s => s.Length > 0)
.ToList();
+ // RIO endpoint: a COM name or pipe:name (e.g. pipe:vrio); blank or the
+ // "(app default)" entry stores null = follow DefaultRioComPort.
+ string port = _portBox.Text.Trim();
+ _profile.RioComPort = port.Length == 0 || port == _defaultPortItem ? null : port;
+
ApplyToCell();
try
{
diff --git a/src/RioJoy.Tray/TrayApplicationContext.cs b/src/RioJoy.Tray/TrayApplicationContext.cs
index cd04b55..2478062 100644
--- a/src/RioJoy.Tray/TrayApplicationContext.cs
+++ b/src/RioJoy.Tray/TrayApplicationContext.cs
@@ -231,33 +231,65 @@ internal sealed class TrayApplicationContext : ApplicationContext
// is suppressed (no keystrokes); the editor only shows which button is pressed.
_coordinator.BeginEditorSession(profile);
- var editor = new ProfileEditorForm(profile);
+ var editor = new ProfileEditorForm(profile, _config.DefaultRioComPort);
editor.IsNameAvailable = name => !_config.Profiles.Any(
p => !ReferenceEquals(p, profile) && string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase));
- editor.Saved += _ => ConfigStore.Save(_config, ConfigPath);
- editor.CommandRequested += cmd => _coordinator.Runtime?.Trigger(cmd);
- editor.OutputsEnabledChanged += enabled => _coordinator.SetEditorOutputs(enabled);
- RioRuntime? runtime = _coordinator.Runtime;
- Action? activity = null;
- if (runtime is not null)
+ // The editor's live wiring targets the CURRENT runtime, which is replaced
+ // when the session re-arms (endpoint change below) — so hook/unhook by pair.
+ Action? unhook = null;
+ void HookRuntime()
{
- activity = editor.ShowLiveActivity;
- runtime.ButtonActivity += activity;
+ RioRuntime? runtime = _coordinator.Runtime;
+ if (runtime is null)
+ {
+ unhook = null;
+ return;
+ }
+ runtime.ButtonActivity += editor.ShowLiveActivity;
runtime.AxesUpdated += editor.ShowAxes;
runtime.VersionReceived += editor.ShowVersion;
runtime.CheckReceived += editor.AddCheckStatus;
- }
-
- editor.FormClosed += (_, _) =>
- {
- if (runtime is not null && activity is not null)
+ unhook = () =>
{
- runtime.ButtonActivity -= activity;
+ runtime.ButtonActivity -= editor.ShowLiveActivity;
runtime.AxesUpdated -= editor.ShowAxes;
runtime.VersionReceived -= editor.ShowVersion;
runtime.CheckReceived -= editor.AddCheckStatus;
+ };
+ }
+
+ bool outputsOn = false;
+ string armedEndpoint = profile.RioComPort ?? _config.DefaultRioComPort;
+
+ editor.Saved += _ =>
+ {
+ ConfigStore.Save(_config, ConfigPath);
+
+ // A saved port/pipe change takes effect immediately: re-arm the held
+ // session on the new endpoint so the live RIO buttons follow it.
+ string endpoint = profile.RioComPort ?? _config.DefaultRioComPort;
+ if (!string.Equals(endpoint, armedEndpoint, StringComparison.OrdinalIgnoreCase))
+ {
+ unhook?.Invoke();
+ _coordinator.BeginEditorSession(profile);
+ HookRuntime();
+ _coordinator.SetEditorOutputs(outputsOn); // new gates start closed
+ armedEndpoint = endpoint;
}
+ };
+ editor.CommandRequested += cmd => _coordinator.Runtime?.Trigger(cmd);
+ editor.OutputsEnabledChanged += enabled =>
+ {
+ outputsOn = enabled;
+ _coordinator.SetEditorOutputs(enabled);
+ };
+
+ HookRuntime();
+
+ editor.FormClosed += (_, _) =>
+ {
+ unhook?.Invoke();
_coordinator.EndEditorSession();
_watcher.Reset(); // re-sync with the foreground app (may re-activate a game)
_watcher.Poll();