From 19a1abbff23f9068884baa35e2c83ef3e7caa720 Mon Sep 17 00:00:00 2001 From: Cyd Date: Tue, 7 Jul 2026 15:06:36 -0500 Subject: [PATCH] vPLASMA slims to a bare glass hardwired to COM12 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The window is now just the display: 640x160 dot field (5 px pitch, 4 px bezel) plus the title bar, which carries the port status. COM12 — the device end of the plasma's null-modem pair — is hardwired and opened at startup, with a retry timer that keeps trying while the port is missing or busy and reopens it if it dies. The control strip, port picker, counters, and wire log are gone; the glass keeps two gestures: double-click cycles the self-test pages, right-click resets the display to its power-on state. Co-Authored-By: Claude Fable 5 --- .claude/skills/verify/SKILL.md | 8 +- README.md | 18 +-- src/VPlasma.App/MainForm.cs | 211 ++++++-------------------------- src/VPlasma.App/PlasmaCanvas.cs | 6 +- 4 files changed, 54 insertions(+), 189 deletions(-) diff --git a/.claude/skills/verify/SKILL.md b/.claude/skills/verify/SKILL.md index 321560a..b0358b6 100644 --- a/.claude/skills/verify/SKILL.md +++ b/.claude/skills/verify/SKILL.md @@ -13,9 +13,11 @@ dotnet build vrio.sln -v q -nologo # Debug # exe: src\VPlasma.App\bin\Debug\net48\VPlasma.App.exe (companion display) ``` -Everything below applies to vPLASMA too. Its serial path can be tested -end-to-end on this machine: vPLASMA opens **COM12**, the script plays the -game writing **COM2** (second com0com pair; vRIO/RIOJoy use COM1⇄COM11). +Everything below applies to vPLASMA too. It auto-opens **COM12** on +launch (hardwired; status in the title bar), so its serial path tests +end-to-end with no UI driving: the script just plays the game writing +**COM2** (second com0com pair; vRIO/RIOJoy use COM1⇄COM11). Double-click +the glass = self-test pages, right-click = reset. Killing a writer mid-stream can leave stale bytes queued in the com0com buffer — the next session's byte counter will run high; re-run clean before trusting counts. diff --git a/README.md b/README.md index 89be816..afb21fc 100644 --- a/README.md +++ b/README.md @@ -108,8 +108,10 @@ a null-modem cable work the same way. The cockpit's second serial device is the **plasma display**: a 128×32 dot-matrix panel on COM2 (9600 8N1, no flow control) that the game draws mission text and status graphics on. `VPlasma.App` is its software replica: -it opens the device end of a COM port, decodes the display's command -stream, and renders the dot matrix in plasma orange — text mode included. +a bare-glass window that opens **COM12** (the device end of the plasma's +null-modem pair) on startup — retrying while the port is missing or busy, +port status in the title bar — decodes the display's command stream, and +renders the dot matrix in plasma orange, text mode included. The command set was recovered from two Tesla 4.10 artifacts: `CODE\RP\MUNGA_L4\L4PLASMA.CPP` (the game's driver — it renders everything @@ -129,12 +131,14 @@ recovered grammar lives in `src/VPlasma.Core/Protocol/PlasmaProtocol.cs`. cell, flashing text (and the flashing cursor) blink on the glass. The panel's own ROM glyphs are lost with the hardware, so the classic public-domain 5×7 set stands in. -- **Self test** cycles three pages (banner, charset, graphics pattern) - through the same parser the wire feeds — useful without a host. +- **Double-click** the glass to cycle three self-test pages (banner, + charset, graphics pattern) through the same parser the wire feeds — + useful without a host. **Right-click** resets the display to its + power-on state. - Pair it with the game like vRIO ↔ RIOJoy: a second com0com null-modem - pair (e.g. `COM2 ⇄ COM12`) — the game's plasma output writes COM2, - vPLASMA listens on COM12. The plasma never talks back, so there is no - transmit path to pace. + pair `COM2 ⇄ COM12` — the game's plasma output writes COM2, vPLASMA + listens on COM12. The plasma never talks back, so there is no transmit + path to pace. ## Repository layout diff --git a/src/VPlasma.App/MainForm.cs b/src/VPlasma.App/MainForm.cs index 5d0ece1..ad5e6b8 100644 --- a/src/VPlasma.App/MainForm.cs +++ b/src/VPlasma.App/MainForm.cs @@ -1,238 +1,97 @@ -using System.IO.Ports; using VPlasma.Core.Device; namespace VPlasma.App; /// -/// vPLASMA main window: the plasma glass on the left, a control strip on the -/// right — COM port, self-test/clear, live counters, and a decoded-command -/// log. Open the COM port, point the game's COM2 (plasma) passthrough at the -/// other end of the null-modem pair, and whatever the sim draws on the -/// cockpit's plasma display appears here. +/// vPLASMA main window: just the plasma glass, sized to the display. The +/// device end is hardwired to (the plasma side of the +/// second com0com pair) and opened automatically — a retry timer keeps +/// trying while the port is missing or busy, and reopens it if it dies. The +/// title bar carries the port status; double-clicking the glass cycles the +/// self-test pages (banner, charset, graphics) through the wire parser, and +/// a right-click resets the display to its power-on state. /// internal sealed class MainForm : Form { + /// The plasma's fixed port: the device end of the COM2 pair. + private const string PortName = "COM12"; + private readonly VPlasmaDevice _device = new(); private readonly VPlasmaSerialService _service; private readonly PlasmaCanvas _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 Button _selfTest = new() { Text = "Self test", Location = new Point(10, 22), Width = 140, Height = 26 }; - private readonly Button _clear = new() { Text = "Clear display", Location = new Point(156, 22), Width = 140, Height = 26 }; - private readonly Label _displayHint = new() - { - Text = "Self test cycles three pages (banner, charset, graphics) through the same parser the wire feeds.", - Location = new Point(10, 54), - MaximumSize = new Size(288, 0), - AutoSize = true, - ForeColor = Color.Gray, - }; - - private readonly Label _counters = new() - { - Location = new Point(12, 178), - AutoSize = true, - Font = new Font("Consolas", 8f), - }; - - private readonly TextBox _logBox = new() - { - Location = new Point(12, 296), - Multiline = true, - ReadOnly = true, - ScrollBars = ScrollBars.Both, // long wire lines don't wrap — scroll to read - 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", - Anchor = AnchorStyles.Bottom | AnchorStyles.Left, - }; - - private readonly System.Windows.Forms.Timer _uiTimer = new() { Interval = 500 }; + private readonly System.Windows.Forms.Timer _reconnectTimer = new() { Interval = 3000 }; private int _selfTestPage; public MainForm() { - // ProductVersion carries the git stamp (see StampGitVersion in the csproj). - Text = $"vPLASMA v{Application.ProductVersion} — Virtual plasma display"; - // Title-bar/taskbar icon from the exe's embedded ApplicationIcon (vwe.ico). Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath); - ClientSize = new Size(_canvas.Width + 24 + 332, 560); - MinimumSize = new Size(_canvas.Width + 24 + 332, 420); + FormBorderStyle = FormBorderStyle.FixedSingle; // the glass is a fixed-size device + MaximizeBox = false; + ClientSize = _canvas.Size; StartPosition = FormStartPosition.CenterScreen; _service = new VPlasmaSerialService(_device); - // The glass, on a dark backdrop that absorbs any extra window space. - var backdrop = new Panel { Dock = DockStyle.Fill, AutoScroll = true, BackColor = Color.FromArgb(28, 28, 28) }; - _canvas.Location = new Point(12, 12); - backdrop.Controls.Add(_canvas); - - Controls.Add(backdrop); - Controls.Add(BuildControlStrip()); + _canvas.Location = new Point(0, 0); + Controls.Add(_canvas); // Device / service events arrive on the serial reader thread; marshal to the UI. _device.Updated += () => RunOnUi(() => _canvas.UpdateFrom(_device)); - _device.Logged += line => RunOnUi(() => PrependLog(line)); - _service.Logged += line => RunOnUi(() => PrependLog(line)); - _service.ConnectionChanged += open => RunOnUi(() => OnConnectionChanged(open)); + _service.ConnectionChanged += _ => RunOnUi(UpdateTitle); - _rescan.Click += (_, _) => RefreshPorts(); - _openClose.Click += (_, _) => ToggleOpen(); - _selfTest.Click += (_, _) => RunSelfTest(); - _clear.Click += (_, _) => + _canvas.DoubleClick += (_, _) => RunSelfTest(); + _canvas.MouseClick += (_, e) => { - _device.Reset(); - PrependLog("Display reset to power-on state"); + if (e.Button == MouseButtons.Right) + _device.Reset(); }; - _clearLog.Click += (_, _) => _logBox.Clear(); - _uiTimer.Tick += (_, _) => UpdateStatus(); - _uiTimer.Start(); + // Open at startup, retry while closed (port missing/busy, host restarts). + _reconnectTimer.Tick += (_, _) => EnsureOpen(); + _reconnectTimer.Start(); FormClosed += (_, _) => { - _uiTimer.Dispose(); + _reconnectTimer.Dispose(); _service.Dispose(); }; - RefreshPorts(); - UpdateStatus(); _canvas.UpdateFrom(_device); - PrependLog("vPLASMA ready. Open a COM port, then point the game's plasma output (COM2) at the other end of the pair."); + EnsureOpen(); } - private Panel BuildControlStrip() - { - var panel = new Panel - { - Dock = DockStyle.Right, - Width = 330, - BackColor = SystemColors.Control, - }; - - 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 display = new GroupBox { Text = "Display", Location = new Point(12, 68), Size = new Size(306, 100) }; - display.Controls.Add(_selfTest); - display.Controls.Add(_clear); - display.Controls.Add(_displayHint); - panel.Controls.Add(display); - - panel.Controls.Add(_counters); - - var logLabel = new Label { Text = "Wire log:", Location = new Point(12, 278), AutoSize = true }; - panel.Controls.Add(logLabel); - panel.Controls.Add(_logBox); - panel.Controls.Add(_clearLog); - - panel.Resize += (_, _) => - { - _logBox.Size = new Size(306, panel.ClientSize.Height - _logBox.Top - 42); - _clearLog.SetBounds(12, panel.ClientSize.Height - 34, 80, 26); - }; - - return panel; - } - - private void RefreshPorts() - { - string? selected = _portBox.SelectedItem as string; - _portBox.Items.Clear(); - foreach (string name in SerialPort.GetPortNames().Distinct().OrderBy(n => n, StringComparer.OrdinalIgnoreCase)) - _portBox.Items.Add(name); - if (selected is not null && _portBox.Items.Contains(selected)) - _portBox.SelectedItem = selected; - else if (_portBox.Items.Count > 0) - _portBox.SelectedIndex = 0; - } - - private void ToggleOpen() + private void EnsureOpen() { if (_service.IsOpen) - { - _service.Close(); return; - } - - if (_portBox.SelectedItem is not string port) - { - PrependLog("No COM port selected — install a com0com pair or attach an adapter, then Rescan."); - return; - } try { - _service.Open(port); + _service.Open(PortName); } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidOperationException or ArgumentException) { - PrependLog($"Open failed: {ex.Message}"); + UpdateTitle(); // stays closed; the timer tries again } } - private void OnConnectionChanged(bool open) + private void UpdateTitle() { - _openClose.Text = open ? "Close" : "Open"; - _portBox.Enabled = !open; - _rescan.Enabled = !open; - _linkStatus.Text = open ? $"Listening on {_service.PortName} @ 9600 8N1." : "Port closed."; - _linkStatus.ForeColor = open ? Color.DarkGreen : Color.Gray; + // ProductVersion carries the git stamp (see StampGitVersion in the csproj). + string status = _service.IsOpen + ? $"{PortName} @ 9600 8N1" + : $"{PortName} unavailable — retrying"; + Text = $"vPLASMA v{Application.ProductVersion} — {status}"; } private void RunSelfTest() { byte[] bytes = PlasmaSelfTest.BuildPage(_selfTestPage); - PrependLog($"Self test page {_selfTestPage + 1}/{PlasmaSelfTest.PageCount}: {bytes.Length} bytes fed through the parser"); _device.OnReceived(bytes, bytes.Length); _selfTestPage = (_selfTestPage + 1) % PlasmaSelfTest.PageCount; } - private void UpdateStatus() - { - VPlasmaDevice.Point cursor = _device.CursorCell; - _counters.Text = - $"Bytes received: {_device.BytesReceived}\n" + - $"Graphics rows: {_device.GraphicsRows}\n" + - $"Chars drawn: {_device.TextCharsDrawn}\n" + - $"Font / grid: {_device.Font} ({VPlasmaDevice.Width / _device.CellWidth}×{VPlasmaDevice.Height / _device.CellHeight} cells)\n" + - $"Cursor: col {cursor.Col}, row {cursor.Row} ({_device.CursorMode})\n" + - $"Attributes: {(_device.Attributes == PlasmaAttributes.None ? "default" : _device.Attributes.ToString())}"; - } - - private void PrependLog(string line) - { - string stamped = $"{DateTime.Now:HH:mm:ss.fff} {line}"; - string text = _logBox.TextLength == 0 ? stamped : stamped + Environment.NewLine + _logBox.Text; - if (text.Length > 20000) - text = text.Substring(0, 20000); - _logBox.Text = text; - } - private void RunOnUi(Action action) { if (IsDisposed || Disposing) diff --git a/src/VPlasma.App/PlasmaCanvas.cs b/src/VPlasma.App/PlasmaCanvas.cs index 835e9a6..613a30f 100644 --- a/src/VPlasma.App/PlasmaCanvas.cs +++ b/src/VPlasma.App/PlasmaCanvas.cs @@ -11,9 +11,9 @@ namespace VPlasma.App; /// internal sealed class PlasmaCanvas : Control { - private const int DotPitch = 6; // 5 px dot + 1 px gap - private const int DotSize = 5; - private const int Bezel = 16; + private const int DotPitch = 5; // 4 px dot + 1 px gap → a 640×160 dot field + private const int DotSize = 4; + private const int Bezel = 4; private static readonly Color BezelColor = Color.FromArgb(20, 18, 16); private static readonly Color GlassColor = Color.FromArgb(26, 14, 6);