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:
+241
-43
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user