vPLASMA (standalone): restore config panel with the 7 JP1 jumpers

The standalone app gets its right-side config panel back, now built around
the real PD01D221's JP1 jumper block (from PlasmaNew/FIRMWARE.md): seven
toggles labelled with each strap's function and HC11 pin. The two baud
straps drive the open baud (4800/9600/19.2K/38.4K; jumper 2 = 9600 default,
the game's rate) via a new VPlasmaSerialService.Baud property; jumper 6
runs a demonstration loop, mirroring the firmware's jumper-6 demo. Plus the
Display controls (self test / clear), live counters, and the wire log.

The glass, auto-open COM12 + named-pipe transport, double-click self-test,
and right-click reset all stay. Only VPlasma.App is touched (plus the
additive Baud property) — the vPLASMA glass embedded in vRIO, which shares
PlasmaCanvas, is left exactly as it was.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-16 18:04:55 -05:00
co-authored by Claude Opus 4.8
parent 49e88fbe20
commit 90a57ba2cf
2 changed files with 251 additions and 45 deletions
+241 -43
View File
@@ -3,16 +3,20 @@ using VPlasma.Core.Device;
namespace VPlasma.App;
/// <summary>
/// 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
/// DOSBox-X fork's named-pipe transport (<c>\\.\pipe\vplasma</c>) is served
/// permanently alongside it (if vRIO's built-in glass already holds the
/// pipe name, the server just retries until it frees up). 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.
/// vPLASMA main window: the plasma glass on the left, a config panel on the
/// right. 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 DOSBox-X fork's named-pipe transport (<c>\\.\pipe\vplasma</c>) is
/// served permanently alongside it.
///
/// <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 (exactly what
/// 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>
/// </summary>
internal sealed class MainForm : Form
{
@@ -25,76 +29,216 @@ internal sealed class MainForm : Form
private readonly PlasmaCanvas _canvas = new();
private readonly System.Windows.Forms.Timer _reconnectTimer = new() { Interval = 3000 };
private readonly System.Windows.Forms.Timer _uiTimer = new() { Interval = 500 };
private readonly System.Windows.Forms.Timer _demoTimer = new() { Interval = 3000 };
private int _selfTestPage;
// ---- config panel controls -------------------------------------------
private readonly Label _linkStatus = new()
{
Location = new Point(12, 12),
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.
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 — Config bit (PA3)");
private readonly CheckBox _jp4 = Jumper("4 — Config bit (PD5)");
private readonly CheckBox _jp5 = Jumper("5 — Config bit (PD4)");
private readonly CheckBox _jp6 = Jumper("6 — Demo program (PD3)");
private readonly CheckBox _jp7 = Jumper("7 — Parallel select (J2 SEL)");
private readonly Label _baudLabel = new() { AutoSize = true, ForeColor = Color.DimGray };
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 TextBox _logBox = new()
{
Multiline = true,
ReadOnly = true,
ScrollBars = ScrollBars.Both,
BackColor = Color.FromArgb(24, 24, 24),
ForeColor = Color.Gainsboro,
Font = new Font("Consolas", 8f),
WordWrap = false,
Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right,
};
private readonly Button _clearLog = new() { Text = "Clear log", Width = 80, Height = 24, Anchor = AnchorStyles.Bottom | AnchorStyles.Left };
private readonly ToolTip _tips = new();
private static CheckBox Jumper(string text) => new() { Text = text, AutoSize = true };
public MainForm()
{
Text = $"vPLASMA v{Application.ProductVersion} — Virtual plasma display";
Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
FormBorderStyle = FormBorderStyle.FixedSingle; // the glass is a fixed-size device
MaximizeBox = false;
ClientSize = _canvas.Size;
ClientSize = new Size(_canvas.Width + 316, Math.Max(_canvas.Height + 8, 520));
MinimumSize = new Size(_canvas.Width + 332, 420);
StartPosition = FormStartPosition.CenterScreen;
_service = new VPlasmaSerialService(_device);
_pipeService = new VPlasmaPipeService(_device);
_canvas.Location = new Point(0, 0);
Controls.Add(_canvas);
// 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);
Controls.Add(backdrop);
Controls.Add(BuildConfigPanel());
// Device / service events arrive on the serial reader thread; marshal to the UI.
// Device / service events arrive on worker threads; marshal to the UI.
_device.Updated += () => RunOnUi(() => _canvas.UpdateFrom(_device));
_service.ConnectionChanged += _ => RunOnUi(UpdateTitle);
_pipeService.ClientChanged += _ => RunOnUi(UpdateTitle);
_device.Logged += line => RunOnUi(() => PrependLog(line));
_service.Logged += line => RunOnUi(() => PrependLog(line));
_service.ConnectionChanged += _ => RunOnUi(UpdateStatus);
_pipeService.Logged += line => RunOnUi(() => PrependLog(line));
_pipeService.ClientChanged += _ => RunOnUi(UpdateStatus);
_canvas.DoubleClick += (_, _) => RunSelfTest();
_canvas.MouseClick += (_, e) =>
{
if (e.Button == MouseButtons.Right)
_device.Reset();
};
_canvas.MouseClick += (_, e) => { if (e.Button == MouseButtons.Right) ResetDisplay(); };
_selfTest.Click += (_, _) => RunSelfTest();
_clear.Click += (_, _) => ResetDisplay();
_clearLog.Click += (_, _) => _logBox.Clear();
// Baud straps → reopen at the selected rate. Jumper 6 → demo loop.
_jp1.CheckedChanged += (_, _) => ApplyBaud();
_jp2.CheckedChanged += (_, _) => ApplyBaud();
_jp6.CheckedChanged += (_, _) => ApplyDemo();
_demoTimer.Tick += (_, _) => StepDemo();
// Default straps: jumper 2 installed only = 9600 (the game's setting).
_jp2.Checked = true;
// Open at startup, retry while closed (port missing/busy, host restarts).
_reconnectTimer.Tick += (_, _) => EnsureOpen();
_reconnectTimer.Start();
_uiTimer.Tick += (_, _) => UpdateCounters();
_uiTimer.Start();
FormClosed += (_, _) =>
{
_reconnectTimer.Dispose();
_uiTimer.Dispose();
_demoTimer.Dispose();
_service.Dispose();
_pipeService.Dispose();
};
_canvas.UpdateFrom(_device);
_service.Baud = SelectedBaud();
UpdateStatus();
UpdateCounters();
EnsureOpen();
_pipeService.Start();
PrependLog("vPLASMA ready. Serving COM12 + \\\\.\\pipe\\vplasma. Toggle jumper 6 for the built-in demo.");
}
private void EnsureOpen()
private Panel BuildConfigPanel()
{
var panel = new Panel { Dock = DockStyle.Right, Width = 300, BackColor = SystemColors.Control };
panel.Controls.Add(_linkStatus);
var jumpers = new GroupBox { Text = "JP1 configuration jumpers", Location = new Point(12, 44), Size = new Size(276, 176) };
CheckBox[] jp = { _jp1, _jp2, _jp3, _jp4, _jp5, _jp6, _jp7 };
for (int i = 0; i < jp.Length; i++)
{
jp[i].Location = new Point(12, 20 + i * 20);
jumpers.Controls.Add(jp[i]);
}
_baudLabel.Location = new Point(150, 20);
jumpers.Controls.Add(_baudLabel);
panel.Controls.Add(jumpers);
_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, "Firmware config bit on PA3 (function not yet decoded).");
_tips.SetToolTip(_jp4, "Firmware config bit on PD5 (function not yet decoded).");
_tips.SetToolTip(_jp5, "Firmware config bit on PD4 (function not yet decoded).");
_tips.SetToolTip(_jp6, "Install to run the built-in demonstration program (as the firmware does).");
_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, 230), 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, 302);
panel.Controls.Add(_counters);
var logLabel = new Label { Text = "Wire log:", Location = new Point(12, 384), AutoSize = true };
panel.Controls.Add(logLabel);
_logBox.SetBounds(12, 402, 276, 0);
panel.Controls.Add(_logBox);
panel.Controls.Add(_clearLog);
panel.Resize += (_, _) =>
{
_logBox.Size = new Size(276, Math.Max(40, panel.ClientSize.Height - _logBox.Top - 40));
_clearLog.Location = new Point(12, panel.ClientSize.Height - 32);
};
return panel;
}
// ---- jumper behaviour --------------------------------------------------
/// <summary>Baud from the two straps (installed = logic 0), per the datasheet.</summary>
private int SelectedBaud() => (_jp1.Checked, _jp2.Checked) switch
{
(true, true) => 4800,
(false, true) => 9600,
(true, false) => 19200,
(false, false) => 38400,
};
private void ApplyBaud()
{
int baud = SelectedBaud();
_service.Baud = baud;
PrependLog($"Baud straps → {baud} 8N1 (reopening)");
if (_service.IsOpen)
return;
try
{
_service.Open(PortName);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidOperationException or ArgumentException)
{
UpdateTitle(); // stays closed; the timer tries again
}
_service.Close(); // the reconnect timer reopens at the new baud
EnsureOpen();
UpdateStatus();
}
private void UpdateTitle()
private void ApplyDemo()
{
// ProductVersion carries the git stamp (see StampGitVersion in the csproj).
string status = _service.IsOpen
? $"{PortName} @ 9600 8N1"
: $"{PortName} unavailable — retrying";
if (_pipeService.IsClientConnected)
status += " • pipe host connected";
Text = $"vPLASMA v{Application.ProductVersion} — {status}";
if (_jp6.Checked)
{
PrependLog("Jumper 6 installed — demonstration mode (cycling the self-test pages)");
_selfTestPage = 0;
StepDemo();
_demoTimer.Start();
}
else
{
_demoTimer.Stop();
PrependLog("Jumper 6 removed — demo stopped");
}
}
private void StepDemo()
{
byte[] bytes = PlasmaSelfTest.BuildPage(_selfTestPage);
_device.OnReceived(bytes, bytes.Length);
_selfTestPage = (_selfTestPage + 1) % PlasmaSelfTest.PageCount;
}
// ---- display controls --------------------------------------------------
private void RunSelfTest()
{
byte[] bytes = PlasmaSelfTest.BuildPage(_selfTestPage);
@@ -102,6 +246,60 @@ internal sealed class MainForm : Form
_selfTestPage = (_selfTestPage + 1) % PlasmaSelfTest.PageCount;
}
private void ResetDisplay()
{
_device.Reset();
PrependLog("Display reset to power-on state");
}
private void EnsureOpen()
{
if (_service.IsOpen)
return;
try
{
_service.Open(PortName);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidOperationException or ArgumentException)
{
UpdateStatus(); // stays closed; the timer tries again
}
}
// ---- status / counters / log ------------------------------------------
private void UpdateStatus()
{
_baudLabel.Text = $"{SelectedBaud()} baud";
string line = _service.IsOpen
? $"Listening on {PortName} @ {_service.Baud} 8N1."
: $"{PortName} unavailable — retrying.";
if (_pipeService.IsClientConnected)
line += " Pipe host connected.";
_linkStatus.Text = line;
_linkStatus.ForeColor = _service.IsOpen ? Color.DarkGreen : Color.Gray;
}
private void UpdateCounters()
{
_counters.Text =
$"Bytes received: {_device.BytesReceived}\n" +
$"Graphics rows: {_device.GraphicsRows}\n" +
$"Chars drawn: {_device.TextCharsDrawn}\n" +
$"Font: {_device.Font} ({_device.FontWidth}×{_device.FontHeight})\n" +
$"Cursor: X {_device.CursorX}, Y {_device.CursorY} ({_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)
@@ -27,6 +27,14 @@ public sealed class VPlasmaSerialService : IDisposable
_device = device ?? throw new ArgumentNullException(nameof(device));
}
/// <summary>
/// Bit rate to open the port at (default 9600). On the real display this is
/// the baud the two JP1 jumpers select (4800/9600/19.2K/38.4K); over a
/// virtual null-modem it's cosmetic, but the standalone app exposes the
/// jumpers so it's honored on the next open. Set before <see cref="Open"/>.
/// </summary>
public int Baud { get; set; } = PlasmaProtocol.BaudRate;
/// <summary>True while a COM port is open.</summary>
public bool IsOpen => _port?.IsOpen == true;
@@ -47,7 +55,7 @@ public sealed class VPlasmaSerialService : IDisposable
Close();
var port = new SerialPort(portName, PlasmaProtocol.BaudRate, Parity.None, 8, StopBits.One)
var port = new SerialPort(portName, Baud, Parity.None, 8, StopBits.One)
{
Handshake = Handshake.None,
// Finite read timeout so the reader thread can notice shutdown.
@@ -65,7 +73,7 @@ public sealed class VPlasmaSerialService : IDisposable
_reader = new Thread(ReadLoop) { IsBackground = true, Name = "vPLASMA serial reader" };
_reader.Start();
Logged?.Invoke($"Opened {portName} @ {PlasmaProtocol.BaudRate} 8N1 — listening for the host");
Logged?.Invoke($"Opened {portName} @ {Baud} 8N1 — listening for the host");
ConnectionChanged?.Invoke(true);
}