- Axes now span the hardware windows from RIOJoy's calibrator instead of the full 14-bit wire range: throttle rests at 0 and travels to -800 (forward runs negative, matching the ratchet math), spring-loaded pedals rest at 0 and press to +500, stick covers +/-80 around center (new RioAxisRange in VRio.Core documents the provenance). - Stick X sign flipped: RIOJoy maps negative samples to the high half of its output axis, so dragging right now lowers raw X. - Removed the Rz mix bar - the real RIO has no such indicator. - Wire log: newest entries on top with the bound trimming the oldest lines, and fixed the anchoring bug that let the box grow past the window edge (the panel got its height only after children snapshotted their anchors), which also hid the Clear log button. - Tighter layout: axis readout sits under the status lines, encoder strip shrunk to fit the gauges, and the window sizes itself to the canvas + control strip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
308 lines
12 KiB
C#
308 lines
12 KiB
C#
using System.Diagnostics;
|
|
using System.IO.Ports;
|
|
using VRio.Core.Device;
|
|
|
|
namespace VRio.App;
|
|
|
|
/// <summary>
|
|
/// vRIO main window: the interactive cockpit panel on the left (the same
|
|
/// functional map RIOJoy's profile editor shows) and a control strip on the
|
|
/// right — COM port, device settings, and a live wire log. Open the COM port,
|
|
/// point RIOJoy at the other end of the null-modem pair, and every click here
|
|
/// arrives at RIOJoy exactly as if the physical cockpit sent it.
|
|
/// </summary>
|
|
internal sealed class MainForm : Form
|
|
{
|
|
private readonly VRioDevice _device = new();
|
|
private readonly VRioSerialService _service;
|
|
private readonly PanelCanvas _canvas = new();
|
|
|
|
private readonly ComboBox _portBox = new()
|
|
{
|
|
Location = new Point(80, 12),
|
|
Width = 128,
|
|
DropDownStyle = ComboBoxStyle.DropDownList,
|
|
};
|
|
private readonly Button _rescan = new() { Text = "Rescan", Location = new Point(214, 11), Width = 52 };
|
|
private readonly Button _openClose = new() { Text = "Open", Location = new Point(272, 11), Width = 46 };
|
|
private readonly Label _linkStatus = new()
|
|
{
|
|
Text = "Port closed.",
|
|
Location = new Point(12, 42),
|
|
AutoSize = true,
|
|
ForeColor = Color.Gray,
|
|
};
|
|
|
|
private readonly NumericUpDown _verMajor = new() { Location = new Point(80, 24), Width = 44, Minimum = 0, Maximum = 127, Value = 4 };
|
|
private readonly NumericUpDown _verMinor = new() { Location = new Point(140, 24), Width = 44, Minimum = 0, Maximum = 127, Value = 2 };
|
|
private readonly CheckBox _spring = new() { Text = "Stick springs back to center", Location = new Point(10, 56), AutoSize = true, Checked = true };
|
|
private readonly Button _centerAxes = new() { Text = "Center all axes", Location = new Point(10, 82), Width = 140, Height = 26 };
|
|
private readonly Button _lampsOff = new() { Text = "All lamps off", Location = new Point(156, 82), Width = 140, Height = 26 };
|
|
private readonly Button _testEnter = new() { Text = "Enter test mode", Location = new Point(10, 114), Width = 140, Height = 26 };
|
|
private readonly Button _testExit = new() { Text = "Exit test mode", Location = new Point(156, 114), Width = 140, Height = 26 };
|
|
private readonly CheckBox _wedgeBug = new()
|
|
{
|
|
Text = "Emulate the v4.2 reply-wedge bug",
|
|
Location = new Point(10, 148),
|
|
AutoSize = true,
|
|
};
|
|
private readonly Button _wedgeNow = new() { Text = "Wedge analog now", Location = new Point(10, 172), Width = 140, Height = 26 };
|
|
|
|
private readonly Label _counters = new()
|
|
{
|
|
Location = new Point(12, 290),
|
|
AutoSize = true,
|
|
Font = new Font("Consolas", 8f),
|
|
};
|
|
|
|
private readonly Label _help = new()
|
|
{
|
|
Location = new Point(12, 348),
|
|
MaximumSize = new Size(306, 0),
|
|
AutoSize = true,
|
|
ForeColor = Color.Gray,
|
|
Text = "Left-click a cell: momentary press. Right-click: latch it down. " +
|
|
"Drag the X/Y box and the Z / L / R gauges to move the axes.",
|
|
};
|
|
|
|
private readonly TextBox _logBox = new()
|
|
{
|
|
Location = new Point(12, 428),
|
|
Multiline = true,
|
|
ReadOnly = true,
|
|
ScrollBars = ScrollBars.Vertical,
|
|
BackColor = Color.FromArgb(24, 24, 24),
|
|
ForeColor = Color.Gainsboro,
|
|
Font = new Font("Consolas", 8f),
|
|
Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left,
|
|
WordWrap = false,
|
|
};
|
|
private readonly Button _clearLog = new()
|
|
{
|
|
Text = "Clear log",
|
|
Width = 80,
|
|
Anchor = AnchorStyles.Bottom | AnchorStyles.Left,
|
|
};
|
|
|
|
private readonly System.Windows.Forms.Timer _uiTimer = new() { Interval = 500 };
|
|
private readonly Stopwatch _clock = Stopwatch.StartNew();
|
|
|
|
public MainForm()
|
|
{
|
|
Text = "vRIO — Virtual RIO cockpit device";
|
|
// Fit the window to its content: the cockpit canvas plus the 330px
|
|
// control strip, with just enough height for the strip's log area.
|
|
ClientSize = new Size(_canvas.Width + 332, Math.Max(_canvas.Height, 640));
|
|
MinimumSize = new Size(1000, 620);
|
|
StartPosition = FormStartPosition.CenterScreen;
|
|
|
|
_service = new VRioSerialService(_device);
|
|
|
|
// Panel canvas, scrolled if the window is smaller than the grid.
|
|
var scroller = new Panel { Dock = DockStyle.Fill, AutoScroll = true, BackColor = Color.FromArgb(28, 28, 28) };
|
|
scroller.Controls.Add(_canvas);
|
|
|
|
Controls.Add(scroller);
|
|
Controls.Add(BuildControlStrip());
|
|
|
|
// Canvas ↔ device wiring.
|
|
_canvas.LampProvider = _device.GetLamp;
|
|
_canvas.AxisProvider = _device.GetAxis;
|
|
_canvas.AddressPressed += _device.PressAddress;
|
|
_canvas.AddressReleased += _device.ReleaseAddress;
|
|
_canvas.AxisMoved += (axis, value) => _device.SetAxis(axis, value);
|
|
|
|
// Device / service events arrive on worker threads; marshal to the UI.
|
|
_device.LampChanged += (_, _) => RunOnUi(_canvas.Invalidate);
|
|
_device.AxesChanged += () => RunOnUi(_canvas.Invalidate);
|
|
_device.Logged += line => RunOnUi(() => PrependLog(line));
|
|
_service.Logged += line => RunOnUi(() => PrependLog(line));
|
|
_service.ConnectionChanged += open => RunOnUi(() => OnConnectionChanged(open));
|
|
_service.HostHandshake += high => RunOnUi(() =>
|
|
PrependLog(high ? "Host raised DTR (board-reset handshake)" : "Host dropped DTR"));
|
|
|
|
_rescan.Click += (_, _) => RefreshPorts();
|
|
_openClose.Click += (_, _) => ToggleOpen();
|
|
_verMajor.ValueChanged += (_, _) => _device.VersionMajor = (byte)_verMajor.Value;
|
|
_verMinor.ValueChanged += (_, _) => _device.VersionMinor = (byte)_verMinor.Value;
|
|
_spring.CheckedChanged += (_, _) => _canvas.StickSpringsBack = _spring.Checked;
|
|
_centerAxes.Click += (_, _) =>
|
|
{
|
|
foreach (RioAxis axis in (RioAxis[])Enum.GetValues(typeof(RioAxis)))
|
|
_device.SetAxis(axis, 0);
|
|
};
|
|
_lampsOff.Click += (_, _) =>
|
|
{
|
|
_device.ClearLamps();
|
|
_canvas.Invalidate();
|
|
};
|
|
_testEnter.Click += (_, _) => _device.SendTestMode(1);
|
|
_testExit.Click += (_, _) => _device.SendTestMode(0);
|
|
_wedgeBug.CheckedChanged += (_, _) => _device.EmulateReplyWedge = _wedgeBug.Checked;
|
|
_wedgeNow.Click += (_, _) =>
|
|
{
|
|
_device.WedgeAnalogNow();
|
|
UpdateStatus();
|
|
};
|
|
_clearLog.Click += (_, _) => _logBox.Clear();
|
|
|
|
_uiTimer.Tick += (_, _) => UpdateStatus();
|
|
_uiTimer.Start();
|
|
|
|
FormClosed += (_, _) =>
|
|
{
|
|
_uiTimer.Dispose();
|
|
_service.Dispose();
|
|
};
|
|
|
|
RefreshPorts();
|
|
UpdateStatus();
|
|
PrependLog("vRIO ready. Open a COM port, then point RIOJoy at the other end of the pair.");
|
|
}
|
|
|
|
private Panel BuildControlStrip()
|
|
{
|
|
var panel = new Panel
|
|
{
|
|
Dock = DockStyle.Right,
|
|
Width = 330,
|
|
// Height must be set before the anchored children are added:
|
|
// Anchor offsets are captured against the parent's current size,
|
|
// and the 100px default would pin the log box's bottom far below
|
|
// the panel, making it grow past the window instead of scrolling.
|
|
Height = ClientSize.Height,
|
|
Padding = new Padding(8),
|
|
BorderStyle = BorderStyle.FixedSingle,
|
|
};
|
|
|
|
panel.Controls.Add(new Label { Text = "COM port:", Location = new Point(12, 15), AutoSize = true });
|
|
panel.Controls.Add(_portBox);
|
|
panel.Controls.Add(_rescan);
|
|
panel.Controls.Add(_openClose);
|
|
panel.Controls.Add(_linkStatus);
|
|
|
|
var device = new GroupBox { Text = "Device", Location = new Point(12, 68), Size = new Size(306, 210) };
|
|
device.Controls.Add(new Label { Text = "Firmware:", Location = new Point(10, 27), AutoSize = true });
|
|
device.Controls.Add(_verMajor);
|
|
device.Controls.Add(new Label { Text = ".", Location = new Point(127, 27), AutoSize = true });
|
|
device.Controls.Add(_verMinor);
|
|
device.Controls.AddRange(new Control[] { _spring, _centerAxes, _lampsOff, _testEnter, _testExit, _wedgeBug, _wedgeNow });
|
|
panel.Controls.Add(device);
|
|
|
|
panel.Controls.Add(_counters);
|
|
panel.Controls.Add(_help);
|
|
panel.Controls.Add(new Label { Text = "Wire log:", Location = new Point(12, 410), AutoSize = true });
|
|
|
|
_logBox.Size = new Size(306, ClientSize.Height - _logBox.Top - 44);
|
|
panel.Controls.Add(_logBox);
|
|
|
|
_clearLog.Location = new Point(12, ClientSize.Height - 36);
|
|
panel.Controls.Add(_clearLog);
|
|
|
|
return panel;
|
|
}
|
|
|
|
// ---- Port handling -----------------------------------------------------
|
|
|
|
private void RefreshPorts()
|
|
{
|
|
string? current = _portBox.SelectedItem?.ToString();
|
|
_portBox.Items.Clear();
|
|
foreach (string name in SerialPort.GetPortNames().Distinct().OrderBy(n => n, StringComparer.OrdinalIgnoreCase))
|
|
_portBox.Items.Add(name);
|
|
|
|
if (_portBox.Items.Count == 0)
|
|
return;
|
|
int idx = current is null ? -1 : _portBox.Items.IndexOf(current);
|
|
_portBox.SelectedIndex = idx >= 0 ? idx : 0;
|
|
}
|
|
|
|
private void ToggleOpen()
|
|
{
|
|
if (_service.IsOpen)
|
|
{
|
|
_service.Close();
|
|
return;
|
|
}
|
|
|
|
if (_portBox.SelectedItem?.ToString() is not { } port)
|
|
{
|
|
MessageBox.Show(this, "No COM port selected. On a single PC, install a com0com virtual " +
|
|
"null-modem pair and open one end here.", "vRIO", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
_service.Open(port);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show(this, $"Could not open {port}:\n{ex.Message}", "vRIO",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
}
|
|
}
|
|
|
|
private void OnConnectionChanged(bool open)
|
|
{
|
|
_openClose.Text = open ? "Close" : "Open";
|
|
_portBox.Enabled = _rescan.Enabled = !open;
|
|
_linkStatus.Text = open ? $"Serving {_service.PortName} @ 9600 8N1." : "Port closed.";
|
|
_linkStatus.ForeColor = open ? Color.ForestGreen : Color.Gray;
|
|
UpdateStatus();
|
|
}
|
|
|
|
// ---- Status / log ------------------------------------------------------
|
|
|
|
private void UpdateStatus()
|
|
{
|
|
long polls = _device.AnalogRequests;
|
|
long dropped = _device.AnalogDropped;
|
|
long bad = _device.BadChecksums;
|
|
bool wedged = _device.AnalogWedged;
|
|
_counters.Text = $"Analog polls served: {polls}\n" +
|
|
$"Analog polls dropped: {dropped}\n" +
|
|
$"Bad checksums: {bad}";
|
|
|
|
_canvas.StatusText = _service.IsOpen
|
|
? $"vRIO {_device.VersionMajor}.{_device.VersionMinor} on {_service.PortName}\n" +
|
|
(wedged
|
|
? "** ANALOG WEDGED (v4.2 bug) — awaiting host reset **"
|
|
: $"{polls} analog polls" + (polls > 0 ? " (host is alive)" : " (no host traffic yet)"))
|
|
: "PORT CLOSED\nOpen a COM port to go live.";
|
|
}
|
|
|
|
private void PrependLog(string line)
|
|
{
|
|
// Newest entry on top; older lines flow downward and eventually fall
|
|
// off the bounded end, so the visible top is always current.
|
|
string text = $"[{_clock.Elapsed.TotalSeconds,8:F2}s] {line}{Environment.NewLine}" + _logBox.Text;
|
|
|
|
// Bound the log so an all-day session doesn't grow without limit;
|
|
// cut at a line boundary so the tail isn't half an entry.
|
|
if (text.Length > 120_000)
|
|
{
|
|
int cut = text.LastIndexOf(Environment.NewLine, 60_000, StringComparison.Ordinal);
|
|
text = text.Substring(0, cut < 0 ? 60_000 : cut + Environment.NewLine.Length);
|
|
}
|
|
|
|
_logBox.Text = text;
|
|
_logBox.SelectionStart = 0;
|
|
_logBox.ScrollToCaret();
|
|
}
|
|
|
|
private void RunOnUi(Action action)
|
|
{
|
|
if (IsDisposed)
|
|
return;
|
|
if (InvokeRequired)
|
|
{
|
|
try { BeginInvoke(action); }
|
|
catch (ObjectDisposedException) { }
|
|
catch (InvalidOperationException) { } // handle not created / closing
|
|
return;
|
|
}
|
|
action();
|
|
}
|
|
}
|