diff --git a/src/VPlasma.App/MainForm.cs b/src/VPlasma.App/MainForm.cs
index 71d84b3..ea63b60 100644
--- a/src/VPlasma.App/MainForm.cs
+++ b/src/VPlasma.App/MainForm.cs
@@ -3,16 +3,20 @@ using VPlasma.Core.Device;
namespace VPlasma.App;
///
-/// 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
-/// DOSBox-X fork's named-pipe transport (\\.\pipe\vplasma) 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 (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 (\\.\pipe\vplasma) is
+/// served permanently alongside it.
+///
+/// The right panel mirrors the real PD01D221's JP1 configuration
+/// jumpers (recovered in PlasmaNew/FIRMWARE.md): 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.
///
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 --------------------------------------------------
+
+ /// Baud from the two straps (installed = logic 0), per the datasheet.
+ 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)
diff --git a/src/VPlasma.Core/Device/VPlasmaSerialService.cs b/src/VPlasma.Core/Device/VPlasmaSerialService.cs
index 60de14c..3040523 100644
--- a/src/VPlasma.Core/Device/VPlasmaSerialService.cs
+++ b/src/VPlasma.Core/Device/VPlasmaSerialService.cs
@@ -27,6 +27,14 @@ public sealed class VPlasmaSerialService : IDisposable
_device = device ?? throw new ArgumentNullException(nameof(device));
}
+ ///
+ /// 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 .
+ ///
+ public int Baud { get; set; } = PlasmaProtocol.BaudRate;
+
/// True while a COM port is open.
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);
}