vPLASMA slims to a bare glass hardwired to COM12

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 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-07 15:06:36 -05:00
co-authored by Claude Fable 5
parent a1b7dae3da
commit 19a1abbff2
4 changed files with 54 additions and 189 deletions
+5 -3
View File
@@ -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.
+11 -7
View File
@@ -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
+35 -176
View File
@@ -1,238 +1,97 @@
using System.IO.Ports;
using VPlasma.Core.Device;
namespace VPlasma.App;
/// <summary>
/// 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 <see cref="PortName"/> (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.
/// </summary>
internal sealed class MainForm : Form
{
/// <summary>The plasma's fixed port: the device end of the COM2 pair.</summary>
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)
+3 -3
View File
@@ -11,9 +11,9 @@ namespace VPlasma.App;
/// </summary>
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);