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);