vPLASMA (standalone): vRIO-style picker, real firmware demo, PD5 fix
- Connection: drop the hardwired COM12; replicate vRIO's endpoint picker — a combo of pipe:vplasma (the DOSBox-X namedpipe backend) + the COM ports, with Rescan and Open/Close. Nothing opens automatically; baud straps drive a COM open. - Demo (jumper 6) now runs the REAL firmware demonstration, not the vPLASMA self-test. The 10 demo screens are extracted verbatim from the ROM demo pointer table ($8000) into PlasmaFirmwareDemo.cs and looped through the parser. Added ESC I / ESC i (draw/display page select) as 1-operand commands — consumed but not acted on in the single-page model — so the demo's page commands don't desync the stream. - Orientation (jumper 4 / PD5) polarity fixed: unstrapped = horizontal 128x32 (the normal cockpit setup, now the default), installed = vertical. Verified: 29 unit tests pass (2 new: demo replay, page-select operand); the real demo screens render with the correct text/positioning/fonts, and the picker lists pipe:vplasma + COM ports with no auto-open. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+111
-69
@@ -1,26 +1,27 @@
|
||||
using System.IO.Ports;
|
||||
using VPlasma.Core.Device;
|
||||
|
||||
namespace VPlasma.App;
|
||||
|
||||
/// <summary>
|
||||
/// vPLASMA main window: the plasma glass on the left, a config panel on the
|
||||
/// right. The serial device end is <see cref="PortName"/> (the plasma side of
|
||||
/// the second com0com pair); it is <b>opened manually</b> from the panel — no
|
||||
/// auto-open. The DOSBox-X fork's named-pipe transport
|
||||
/// (<c>\\.\pipe\vplasma</c>) is served permanently regardless.
|
||||
/// right. Connections mirror vRIO: pick an endpoint — the device's named pipe
|
||||
/// (<c>pipe:vplasma</c>, the DOSBox-X fork's namedpipe backend) or a COM port
|
||||
/// (for the game through a com0com null-modem pair) — and Open. Nothing opens
|
||||
/// automatically.
|
||||
///
|
||||
/// <para>The right panel mirrors the real PD01D221's <b>JP1 configuration
|
||||
/// jumpers</b> (recovered in <c>PlasmaNew/FIRMWARE.md</c>): the two baud
|
||||
/// straps drive the open baud, jumper 6 runs the built-in demo (as the
|
||||
/// firmware does when that jumper is installed), and the rest are the
|
||||
/// hardware's config bits. Plus display controls, live counters, and a wire
|
||||
/// log. Double-click the glass to cycle the self-test pages; right-click to
|
||||
/// reset to the power-on state.</para>
|
||||
/// <para>The panel mirrors the real PD01D221's <b>JP1 configuration jumpers</b>
|
||||
/// (recovered in <c>PlasmaNew/FIRMWARE.md</c>): the baud straps (1+2) drive the
|
||||
/// COM baud, orientation (4), display test (5), and the demonstration program
|
||||
/// (6) — which replays the real 10-screen firmware demo. Plus display
|
||||
/// controls, live counters, and a wire log. Double-click the glass to cycle
|
||||
/// the self-test pages; right-click to reset.</para>
|
||||
/// </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";
|
||||
/// <summary>The plasma's pipe endpoint in the picker (matches DOSBox-X's
|
||||
/// <c>serial2=namedpipe pipe:vplasma</c>).</summary>
|
||||
private static readonly string PipeEndpoint = $"pipe:{VPlasmaPipeService.DefaultPipeName}";
|
||||
|
||||
private readonly VPlasmaDevice _device = new();
|
||||
private readonly VPlasmaSerialService _service;
|
||||
@@ -28,21 +29,22 @@ internal sealed class MainForm : Form
|
||||
private readonly PlasmaCanvas _canvas = new();
|
||||
|
||||
private readonly System.Windows.Forms.Timer _uiTimer = new() { Interval = 500 };
|
||||
private readonly System.Windows.Forms.Timer _demoTimer = new() { Interval = 3000 };
|
||||
private readonly System.Windows.Forms.Timer _demoTimer = new() { Interval = 2800 };
|
||||
private int _selfTestPage;
|
||||
private int _demoScreen;
|
||||
|
||||
// ---- config panel controls -------------------------------------------
|
||||
private readonly Button _openClose = new() { Text = "Open COM12", Width = 100, Height = 24 };
|
||||
private readonly ComboBox _portBox = new() { Width = 168, DropDownStyle = ComboBoxStyle.DropDownList };
|
||||
private readonly Button _rescan = new() { Text = "Rescan", Width = 54, Height = 22 };
|
||||
private readonly Button _openClose = new() { Text = "Open", Width = 54, Height = 22 };
|
||||
private readonly Label _linkStatus = new()
|
||||
{
|
||||
Location = new Point(12, 42),
|
||||
AutoSize = true,
|
||||
MaximumSize = new Size(286, 0),
|
||||
ForeColor = Color.Gray,
|
||||
};
|
||||
|
||||
// The seven JP1 jumpers, as toggles. Checked = shunt installed (logic 0 to
|
||||
// the CPU). Text/tooltips carry each strap's real function and HC11 pin.
|
||||
// The seven JP1 jumpers, as toggles. Checked = shunt installed.
|
||||
private readonly CheckBox _jp1 = Jumper("1 — Baud select A (PA0)");
|
||||
private readonly CheckBox _jp2 = Jumper("2 — Baud select B (PA2)");
|
||||
private readonly CheckBox _jp3 = Jumper("3 — HW config line (PA3)");
|
||||
@@ -55,11 +57,7 @@ internal sealed class MainForm : Form
|
||||
private readonly Button _selfTest = new() { Text = "Self test", Width = 130, Height = 26 };
|
||||
private readonly Button _clear = new() { Text = "Clear display", Width = 130, Height = 26 };
|
||||
|
||||
private readonly Label _counters = new()
|
||||
{
|
||||
AutoSize = true,
|
||||
Font = new Font("Consolas", 8f),
|
||||
};
|
||||
private readonly Label _counters = new() { AutoSize = true, Font = new Font("Consolas", 8f) };
|
||||
|
||||
private readonly TextBox _logBox = new()
|
||||
{
|
||||
@@ -77,18 +75,19 @@ internal sealed class MainForm : Form
|
||||
|
||||
private static CheckBox Jumper(string text) => new() { Text = text, AutoSize = true };
|
||||
|
||||
private bool IsOpen => _service.IsOpen || _pipeService.IsListening;
|
||||
|
||||
public MainForm()
|
||||
{
|
||||
Text = $"vPLASMA v{Application.ProductVersion} — Virtual plasma display";
|
||||
Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
|
||||
ClientSize = new Size(_canvas.Width + 316, Math.Max(_canvas.Height + 8, 560));
|
||||
MinimumSize = new Size(_canvas.Width + 332, 480);
|
||||
ClientSize = new Size(_canvas.Width + 316, Math.Max(_canvas.Height + 8, 580));
|
||||
MinimumSize = new Size(_canvas.Width + 332, 500);
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
|
||||
_service = new VPlasmaSerialService(_device);
|
||||
_pipeService = new VPlasmaPipeService(_device);
|
||||
|
||||
// The glass, on a dark backdrop that fills any extra window space.
|
||||
var backdrop = new Panel { Dock = DockStyle.Fill, BackColor = Color.FromArgb(28, 28, 28) };
|
||||
_canvas.Location = new Point(8, 8);
|
||||
backdrop.Controls.Add(_canvas);
|
||||
@@ -100,19 +99,19 @@ internal sealed class MainForm : Form
|
||||
_device.Logged += line => RunOnUi(() => PrependLog(line));
|
||||
_service.Logged += line => RunOnUi(() => PrependLog(line));
|
||||
_service.ConnectionChanged += _ => RunOnUi(UpdateStatus);
|
||||
_pipeService.Logged += line => RunOnUi(() => PrependLog(line));
|
||||
_pipeService.Logged += line => RunOnUi(() => PrependLog($"pipe: {line}"));
|
||||
_pipeService.ClientChanged += _ => RunOnUi(UpdateStatus);
|
||||
|
||||
_openClose.Click += (_, _) => ToggleConnection();
|
||||
_rescan.Click += (_, _) => RefreshPorts();
|
||||
_openClose.Click += (_, _) => ToggleOpen();
|
||||
_canvas.DoubleClick += (_, _) => RunSelfTest();
|
||||
_canvas.MouseClick += (_, e) => { if (e.Button == MouseButtons.Right) ResetDisplay(); };
|
||||
|
||||
_selfTest.Click += (_, _) => RunSelfTest();
|
||||
_clear.Click += (_, _) => ResetDisplay();
|
||||
_clearLog.Click += (_, _) => _logBox.Clear();
|
||||
|
||||
// Functional straps: baud (1+2), orientation (4), display test (5),
|
||||
// demo (6). Jumpers 3 and 7 are tracked toggles (board-level effects).
|
||||
// demo (6). Jumpers 3 and 7 are tracked toggles.
|
||||
_jp1.CheckedChanged += (_, _) => ApplyBaud();
|
||||
_jp2.CheckedChanged += (_, _) => ApplyBaud();
|
||||
_jp4.CheckedChanged += (_, _) => ApplyOrientation();
|
||||
@@ -120,10 +119,9 @@ internal sealed class MainForm : Form
|
||||
_jp6.CheckedChanged += (_, _) => ApplyDemo();
|
||||
_demoTimer.Tick += (_, _) => StepDemo();
|
||||
|
||||
// Default straps: jumper 2 installed = 9600 (the game's rate); jumper 4
|
||||
// installed = horizontal orientation (both the normal cockpit setup).
|
||||
// Default straps: jumper 2 installed = 9600 (the game's rate). Jumper 4
|
||||
// unstrapped = horizontal (the normal cockpit orientation).
|
||||
_jp2.Checked = true;
|
||||
_jp4.Checked = true;
|
||||
|
||||
_uiTimer.Tick += (_, _) => UpdateCounters();
|
||||
_uiTimer.Start();
|
||||
@@ -138,20 +136,27 @@ internal sealed class MainForm : Form
|
||||
|
||||
_canvas.UpdateFrom(_device);
|
||||
_service.Baud = SelectedBaud();
|
||||
RefreshPorts();
|
||||
UpdateStatus();
|
||||
UpdateCounters();
|
||||
_pipeService.Start(); // the pipe transport still serves automatically
|
||||
PrependLog("vPLASMA ready. Serving \\\\.\\pipe\\vplasma. Click “Open COM12” for the serial link; jumper 6 for the demo.");
|
||||
PrependLog("vPLASMA ready. Pick an endpoint (pipe:vplasma or a COM port) and Open. Jumper 6 runs the firmware demo.");
|
||||
}
|
||||
|
||||
private Panel BuildConfigPanel()
|
||||
{
|
||||
var panel = new Panel { Dock = DockStyle.Right, Width = 300, BackColor = SystemColors.Control };
|
||||
_openClose.Location = new Point(12, 12);
|
||||
|
||||
panel.Controls.Add(new Label { Text = "Connection:", Location = new Point(12, 14), AutoSize = true });
|
||||
_portBox.Location = new Point(12, 34);
|
||||
_rescan.Location = new Point(186, 34);
|
||||
_openClose.Location = new Point(244, 34);
|
||||
_linkStatus.Location = new Point(12, 62);
|
||||
panel.Controls.Add(_portBox);
|
||||
panel.Controls.Add(_rescan);
|
||||
panel.Controls.Add(_openClose);
|
||||
panel.Controls.Add(_linkStatus);
|
||||
|
||||
var jumpers = new GroupBox { Text = "JP1 configuration jumpers", Location = new Point(12, 70), Size = new Size(276, 176) };
|
||||
var jumpers = new GroupBox { Text = "JP1 configuration jumpers", Location = new Point(12, 90), Size = new Size(276, 176) };
|
||||
CheckBox[] jp = { _jp1, _jp2, _jp3, _jp4, _jp5, _jp6, _jp7 };
|
||||
for (int i = 0; i < jp.Length; i++)
|
||||
{
|
||||
@@ -162,28 +167,28 @@ internal sealed class MainForm : Form
|
||||
jumpers.Controls.Add(_baudLabel);
|
||||
panel.Controls.Add(jumpers);
|
||||
|
||||
_tips.SetToolTip(_openClose, "Open or close the COM12 serial link. Not opened automatically.");
|
||||
_tips.SetToolTip(_portBox, "pipe:vplasma serves the DOSBox-X namedpipe backend; a COM port pairs with the game via com0com.");
|
||||
_tips.SetToolTip(_jp1, "Baud select, low bit. With jumper 2: 4800/9600/19.2K/38.4K.");
|
||||
_tips.SetToolTip(_jp2, "Baud select, high bit. Installed alone = 9600 (the game's rate).");
|
||||
_tips.SetToolTip(_jp3, "Sets flag B7.2, driving the PA5 hardware line to a fixed level (a board config line; exact effect board-dependent).");
|
||||
_tips.SetToolTip(_jp4, "Display orientation: installed = horizontal 128×32, removed = vertical (rotated 32×128).");
|
||||
_tips.SetToolTip(_jp3, "Sets flag B7.2, driving the PA5 hardware line to a fixed level (board config line; exact effect board-dependent).");
|
||||
_tips.SetToolTip(_jp4, "Display orientation: removed = horizontal 128×32 (normal), installed = vertical 32×128.");
|
||||
_tips.SetToolTip(_jp5, "Installed at power-on runs the panel pixel test pattern (a walking-bit dead-dot check).");
|
||||
_tips.SetToolTip(_jp6, "Install to run the built-in demonstration program (as the firmware does).");
|
||||
_tips.SetToolTip(_jp6, "Install to run the built-in demonstration program (the real 10-screen firmware demo).");
|
||||
_tips.SetToolTip(_jp7, "Selects the parallel interface on the real board; no effect here (serial only).");
|
||||
|
||||
var display = new GroupBox { Text = "Display", Location = new Point(12, 256), Size = new Size(276, 62) };
|
||||
var display = new GroupBox { Text = "Display", Location = new Point(12, 276), Size = new Size(276, 62) };
|
||||
_selfTest.Location = new Point(12, 22);
|
||||
_clear.Location = new Point(146, 22);
|
||||
display.Controls.Add(_selfTest);
|
||||
display.Controls.Add(_clear);
|
||||
panel.Controls.Add(display);
|
||||
|
||||
_counters.Location = new Point(12, 328);
|
||||
_counters.Location = new Point(12, 348);
|
||||
panel.Controls.Add(_counters);
|
||||
|
||||
var logLabel = new Label { Text = "Wire log:", Location = new Point(12, 410), AutoSize = true };
|
||||
var logLabel = new Label { Text = "Wire log:", Location = new Point(12, 434), AutoSize = true };
|
||||
panel.Controls.Add(logLabel);
|
||||
_logBox.SetBounds(12, 428, 276, 0);
|
||||
_logBox.SetBounds(12, 452, 276, 0);
|
||||
panel.Controls.Add(_logBox);
|
||||
panel.Controls.Add(_clearLog);
|
||||
|
||||
@@ -196,22 +201,53 @@ internal sealed class MainForm : Form
|
||||
return panel;
|
||||
}
|
||||
|
||||
// ---- connection --------------------------------------------------------
|
||||
// ---- connection (mirrors vRIO's endpoint picker) -----------------------
|
||||
|
||||
private void ToggleConnection()
|
||||
private void RefreshPorts()
|
||||
{
|
||||
if (_service.IsOpen)
|
||||
if (IsOpen)
|
||||
return; // an open row is disabled and shows the served endpoint
|
||||
string? current = _portBox.SelectedItem?.ToString();
|
||||
_portBox.Items.Clear();
|
||||
_portBox.Items.Add(PipeEndpoint); // always present — a pipe server needs no hardware
|
||||
foreach (string name in SerialPort.GetPortNames().Distinct().OrderBy(n => n, StringComparer.OrdinalIgnoreCase))
|
||||
_portBox.Items.Add(name);
|
||||
int idx = current is null ? -1 : _portBox.Items.IndexOf(current);
|
||||
_portBox.SelectedIndex = idx >= 0 ? idx : 0;
|
||||
}
|
||||
|
||||
private void ToggleOpen()
|
||||
{
|
||||
if (IsOpen)
|
||||
{
|
||||
_service.Close();
|
||||
_pipeService.Stop();
|
||||
UpdateStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_portBox.SelectedItem?.ToString() is not { } endpoint)
|
||||
{
|
||||
PrependLog("No endpoint selected — pick pipe:vplasma or a COM port.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_service.Open(PortName);
|
||||
if (endpoint == PipeEndpoint)
|
||||
{
|
||||
_pipeService.Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
_service.Baud = SelectedBaud();
|
||||
_service.Open(endpoint);
|
||||
}
|
||||
UpdateStatus();
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidOperationException or ArgumentException)
|
||||
{
|
||||
PrependLog($"Open failed: {ex.Message}");
|
||||
PrependLog($"Could not open {endpoint}: {ex.Message}");
|
||||
UpdateStatus();
|
||||
}
|
||||
}
|
||||
@@ -231,25 +267,25 @@ internal sealed class MainForm : Form
|
||||
{
|
||||
int baud = SelectedBaud();
|
||||
_service.Baud = baud;
|
||||
if (_service.IsOpen)
|
||||
if (_service.IsOpen && _portBox.SelectedItem?.ToString() is { } port && port != PipeEndpoint)
|
||||
{
|
||||
PrependLog($"Baud straps → {baud} 8N1 (reopening)");
|
||||
PrependLog($"Baud straps → {baud} 8N1 (reopening {port})");
|
||||
_service.Close();
|
||||
ToggleConnection();
|
||||
try { _service.Open(port); }
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidOperationException) { PrependLog($"Reopen failed: {ex.Message}"); }
|
||||
}
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
private void ApplyOrientation()
|
||||
{
|
||||
// Jumper 4 installed (checked) = horizontal 128×32; removed = vertical.
|
||||
_device.Orientation = _jp4.Checked ? PlasmaOrientation.Horizontal : PlasmaOrientation.Vertical;
|
||||
PrependLog($"Jumper 4 → {(_jp4.Checked ? "horizontal 128×32" : "vertical 32×128")} orientation");
|
||||
// Jumper 4 unstrapped (unchecked) = horizontal 128×32; installed = vertical.
|
||||
_device.Orientation = _jp4.Checked ? PlasmaOrientation.Vertical : PlasmaOrientation.Horizontal;
|
||||
PrependLog($"Jumper 4 → {(_jp4.Checked ? "vertical 32×128" : "horizontal 128×32")} orientation");
|
||||
}
|
||||
|
||||
private void ApplyTestPattern()
|
||||
{
|
||||
// Jumper 5 installed runs the power-on panel test (all dots lit).
|
||||
if (_jp5.Checked)
|
||||
{
|
||||
_device.ShowTestPattern();
|
||||
@@ -266,8 +302,8 @@ internal sealed class MainForm : Form
|
||||
{
|
||||
if (_jp6.Checked)
|
||||
{
|
||||
PrependLog("Jumper 6 installed — demonstration mode (cycling the self-test pages)");
|
||||
_selfTestPage = 0;
|
||||
PrependLog("Jumper 6 installed — running the firmware demonstration program");
|
||||
_demoScreen = 0;
|
||||
StepDemo();
|
||||
_demoTimer.Start();
|
||||
}
|
||||
@@ -278,11 +314,12 @@ internal sealed class MainForm : Form
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Play the next real firmware demo screen (loops 0..9).</summary>
|
||||
private void StepDemo()
|
||||
{
|
||||
byte[] bytes = PlasmaSelfTest.BuildPage(_selfTestPage);
|
||||
_device.OnReceived(bytes, bytes.Length);
|
||||
_selfTestPage = (_selfTestPage + 1) % PlasmaSelfTest.PageCount;
|
||||
byte[] screen = PlasmaFirmwareDemo.Screens[_demoScreen];
|
||||
_device.OnReceived(screen, screen.Length);
|
||||
_demoScreen = (_demoScreen + 1) % PlasmaFirmwareDemo.Count;
|
||||
}
|
||||
|
||||
// ---- display controls --------------------------------------------------
|
||||
@@ -304,15 +341,20 @@ internal sealed class MainForm : Form
|
||||
|
||||
private void UpdateStatus()
|
||||
{
|
||||
_openClose.Text = _service.IsOpen ? "Close COM12" : "Open COM12";
|
||||
_openClose.Text = IsOpen ? "Close" : "Open";
|
||||
_portBox.Enabled = !IsOpen;
|
||||
_rescan.Enabled = !IsOpen;
|
||||
_baudLabel.Text = $"{SelectedBaud()} baud";
|
||||
string line = _service.IsOpen
|
||||
? $"Listening on {PortName} @ {_service.Baud} 8N1."
|
||||
: $"{PortName} closed.";
|
||||
if (_pipeService.IsClientConnected)
|
||||
line += " Pipe host connected.";
|
||||
|
||||
string line;
|
||||
if (_service.IsOpen)
|
||||
line = $"Serving {_service.PortName} @ {_service.Baud} 8N1.";
|
||||
else if (_pipeService.IsListening)
|
||||
line = $"Serving \\\\.\\pipe\\{_pipeService.PipeName}" + (_pipeService.IsClientConnected ? " — host connected." : " — waiting for host.");
|
||||
else
|
||||
line = "Not connected.";
|
||||
_linkStatus.Text = line;
|
||||
_linkStatus.ForeColor = _service.IsOpen ? Color.DarkGreen : Color.Gray;
|
||||
_linkStatus.ForeColor = IsOpen ? Color.ForestGreen : Color.Gray;
|
||||
}
|
||||
|
||||
private void UpdateCounters()
|
||||
|
||||
Reference in New Issue
Block a user