From 79eb53253eb4b4fd555a951f60268e01dc830b07 Mon Sep 17 00:00:00 2001 From: Cyd Date: Thu, 16 Jul 2026 19:42:55 -0500 Subject: [PATCH] vPLASMA (standalone): vRIO-style picker, real firmware demo, PD5 fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- PlasmaNew/FIRMWARE.md | 11 +- src/VPlasma.App/MainForm.cs | 180 +++++++++++------- src/VPlasma.Core/Device/PlasmaFirmwareDemo.cs | 37 ++++ src/VPlasma.Core/Device/VPlasmaDevice.cs | 8 + src/VPlasma.Core/Protocol/PlasmaProtocol.cs | 6 + .../VPlasma.Core.Tests/VPlasmaDeviceTests.cs | 25 +++ 6 files changed, 195 insertions(+), 72 deletions(-) create mode 100644 src/VPlasma.Core/Device/PlasmaFirmwareDemo.cs diff --git a/PlasmaNew/FIRMWARE.md b/PlasmaNew/FIRMWARE.md index 2fc584f..5959af5 100644 --- a/PlasmaNew/FIRMWARE.md +++ b/PlasmaNew/FIRMWARE.md @@ -121,9 +121,14 @@ real glyphs to replace vPLASMA's public-domain 5×7 stand-in. ## Demo program Enabled by **jumper 6** (PD3) — confirms the [JP1 map](README.md). A 10-screen -scripted demo; the script at `$8028+` is literal command usage (pointer table -at `$8000`). It's how the new `ESC R/I/Q/i/K` commands were first spotted, e.g. -`1B 47 00` (ESC G 0) `1B 40` (ESC @) `1B 4C` (ESC L) `1B 52 04` (ESC R 4) … +scripted demo; the pointer table at `$8000` (10 × 4-byte entries) points to +each screen, and every screen is `[2-byte count][command stream]`. The player +at `$BB60`/`$BBA4` loops the screens, feeding each byte through the command +parser. **Extracted verbatim** into `src/VPlasma.Core/Device/PlasmaFirmwareDemo.cs` +(all 10 screens as raw wire bytes); the standalone app replays it on jumper 6. +Commands used: `@ G I K L Q R Z i` + text. `ESC I`/`ESC i` (draw/display page) +are consumed by vPLASMA but not acted on (single-page); `ESC Z` (a rarely-used +animation command, one all-zero use in screen 9) is left unimplemented. ## What this means diff --git a/src/VPlasma.App/MainForm.cs b/src/VPlasma.App/MainForm.cs index 297adf8..63c7334 100644 --- a/src/VPlasma.App/MainForm.cs +++ b/src/VPlasma.App/MainForm.cs @@ -1,26 +1,27 @@ +using System.IO.Ports; using VPlasma.Core.Device; namespace VPlasma.App; /// /// vPLASMA main window: the plasma glass on the left, a config panel on the -/// right. The serial device end is (the plasma side of -/// the second com0com pair); it is opened manually from the panel — no -/// auto-open. The DOSBox-X fork's named-pipe transport -/// (\\.\pipe\vplasma) is served permanently regardless. +/// right. Connections mirror vRIO: pick an endpoint — the device's named pipe +/// (pipe:vplasma, 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. /// -/// 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 (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. +/// The panel mirrors the real PD01D221's JP1 configuration jumpers +/// (recovered in PlasmaNew/FIRMWARE.md): 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. /// internal sealed class MainForm : Form { - /// The plasma's fixed port: the device end of the COM2 pair. - private const string PortName = "COM12"; + /// The plasma's pipe endpoint in the picker (matches DOSBox-X's + /// serial2=namedpipe pipe:vplasma). + 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 } } + /// Play the next real firmware demo screen (loops 0..9). 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() diff --git a/src/VPlasma.Core/Device/PlasmaFirmwareDemo.cs b/src/VPlasma.Core/Device/PlasmaFirmwareDemo.cs new file mode 100644 index 0000000..dcfc632 --- /dev/null +++ b/src/VPlasma.Core/Device/PlasmaFirmwareDemo.cs @@ -0,0 +1,37 @@ +// AUTO-GENERATED from the PD01D221 firmware (tms27pc512.BIN) by PlasmaNew +// tooling: the real 10-screen demonstration program (jumper 6), extracted +// from the demo pointer table at $8000. See PlasmaNew/FIRMWARE.md. +namespace VPlasma.Core.Device; + +/// The display's built-in demonstration program — the 10 command +/// scripts the firmware loops through when JP1 jumper 6 is installed. +public static class PlasmaFirmwareDemo +{ + /// The 10 demo screens, each a raw wire command stream. + public static readonly byte[][] Screens = + { + // screen 0 (67 bytes) + new byte[] { 0x1B,0x47,0x00,0x1B,0x40,0x1B,0x4C,0x1B,0x52,0x04,0x59,0x4F,0x55,0x20,0x48,0x41,0x56,0x45,0x20,0x45,0x4E,0x41,0x42,0x4C,0x45,0x44,0x20,0x54,0x48,0x45,0x1B,0x52,0x25,0x50,0x44,0x30,0x31,0x2D,0x44,0x32,0x32,0x31,0x0A,0x1B,0x52,0x01,0x44,0x45,0x4D,0x4F,0x4E,0x53,0x54,0x52,0x41,0x54,0x49,0x4F,0x4E,0x20,0x50,0x52,0x4F,0x47,0x52,0x41,0x4D }, + // screen 1 (99 bytes) + new byte[] { 0x1B,0x49,0x02,0x1B,0x40,0x1B,0x4C,0x1B,0x52,0x02,0x49,0x46,0x20,0x59,0x4F,0x55,0x20,0x44,0x4F,0x20,0x4E,0x4F,0x54,0x20,0x57,0x49,0x53,0x48,0x20,0x54,0x4F,0x1B,0x52,0x0A,0x56,0x49,0x45,0x57,0x20,0x54,0x48,0x49,0x53,0x20,0x50,0x52,0x4F,0x47,0x52,0x41,0x4D,0x2C,0x0A,0x1B,0x52,0x07,0x52,0x45,0x4D,0x4F,0x56,0x45,0x20,0x4A,0x55,0x4D,0x50,0x45,0x52,0x20,0x36,0x20,0x41,0x4E,0x44,0x0A,0x1B,0x52,0x0D,0x52,0x45,0x53,0x45,0x54,0x20,0x54,0x48,0x45,0x20,0x44,0x49,0x53,0x50,0x4C,0x41,0x59,0x1B,0x69,0x02 }, + // screen 2 (38 bytes) + new byte[] { 0x1B,0x49,0x01,0x1B,0x40,0x1B,0x4B,0x04,0x1B,0x51,0x00,0x1B,0x52,0x0A,0x50,0x4C,0x41,0x53,0x4D,0x41,0x44,0x4F,0x54,0x1B,0x52,0x0A,0x50,0x44,0x30,0x31,0x2D,0x44,0x32,0x32,0x31,0x1B,0x69,0x01 }, + // screen 3 (64 bytes) + new byte[] { 0x1B,0x49,0x00,0x1B,0x40,0x1B,0x4B,0x06,0x1B,0x51,0x00,0x1B,0x52,0x1D,0x41,0x20,0x43,0x4F,0x4D,0x50,0x4C,0x45,0x54,0x45,0x0A,0x1B,0x52,0x0F,0x44,0x49,0x53,0x50,0x4C,0x41,0x59,0x20,0x53,0x59,0x53,0x54,0x45,0x4D,0x0A,0x1B,0x52,0x0C,0x49,0x4E,0x20,0x4F,0x4E,0x45,0x20,0x50,0x41,0x43,0x4B,0x41,0x47,0x45,0x21,0x1B,0x69,0x00 }, + // screen 4 (86 bytes) + new byte[] { 0x1B,0x49,0x01,0x1B,0x40,0x1B,0x4B,0x06,0x1B,0x4C,0x46,0x45,0x41,0x54,0x55,0x52,0x45,0x53,0x20,0x49,0x4E,0x43,0x4C,0x55,0x44,0x45,0x20,0x2D,0x1B,0x4B,0x03,0x1B,0x51,0x0C,0x1B,0x52,0x0A,0x4F,0x1B,0x4B,0x02,0x20,0x53,0x45,0x52,0x49,0x41,0x4C,0x20,0x49,0x4E,0x54,0x45,0x52,0x46,0x41,0x43,0x45,0x0A,0x1B,0x4B,0x03,0x1B,0x52,0x0A,0x4F,0x1B,0x4B,0x02,0x20,0x50,0x41,0x52,0x41,0x4C,0x4C,0x45,0x4C,0x20,0x50,0x4F,0x52,0x54,0x1B,0x69,0x01 }, + // screen 5 (85 bytes) + new byte[] { 0x1B,0x49,0x02,0x1B,0x40,0x1B,0x4B,0x06,0x1B,0x4C,0x46,0x45,0x41,0x54,0x55,0x52,0x45,0x53,0x20,0x49,0x4E,0x43,0x4C,0x55,0x44,0x45,0x20,0x2D,0x1B,0x51,0x0C,0x1B,0x4B,0x03,0x1B,0x52,0x0A,0x4F,0x1B,0x4B,0x02,0x20,0x50,0x4F,0x57,0x45,0x52,0x20,0x53,0x55,0x50,0x50,0x4C,0x59,0x0A,0x1B,0x4B,0x03,0x1B,0x52,0x0A,0x4F,0x1B,0x4B,0x02,0x20,0x4B,0x45,0x59,0x50,0x41,0x44,0x20,0x49,0x4E,0x54,0x45,0x52,0x46,0x41,0x43,0x45,0x1B,0x69,0x02 }, + // screen 6 (76 bytes) + new byte[] { 0x1B,0x49,0x00,0x1B,0x40,0x1B,0x4B,0x02,0x1B,0x51,0x00,0x1B,0x52,0x00,0x4D,0x49,0x58,0x20,0x54,0x45,0x58,0x54,0x20,0x41,0x4E,0x44,0x20,0x47,0x52,0x41,0x50,0x48,0x49,0x43,0x53,0x1B,0x52,0x18,0x55,0x53,0x49,0x4E,0x47,0x20,0x38,0x20,0x53,0x54,0x4F,0x52,0x45,0x44,0x0A,0x1B,0x52,0x0D,0x43,0x48,0x41,0x52,0x41,0x43,0x54,0x45,0x52,0x20,0x46,0x4F,0x4E,0x54,0x53,0x20,0x2D,0x1B,0x69,0x00 }, + // screen 7 (63 bytes) + new byte[] { 0x1B,0x49,0x01,0x1B,0x40,0x1B,0x4B,0x02,0x1B,0x4C,0x49,0x4E,0x54,0x45,0x4C,0x4C,0x49,0x47,0x45,0x4E,0x54,0x20,0x49,0x4E,0x54,0x45,0x52,0x46,0x41,0x43,0x45,0x1B,0x52,0x19,0x48,0x41,0x53,0x20,0x44,0x4F,0x5A,0x45,0x4E,0x53,0x20,0x4F,0x46,0x0A,0x1B,0x52,0x25,0x43,0x4F,0x4D,0x4D,0x41,0x4E,0x44,0x53,0x2E,0x1B,0x69,0x01 }, + // screen 8 (70 bytes) + new byte[] { 0x1B,0x49,0x02,0x1B,0x40,0x1B,0x4B,0x02,0x1B,0x4C,0x1B,0x52,0x19,0x43,0x48,0x4F,0x4F,0x53,0x45,0x20,0x45,0x49,0x54,0x48,0x45,0x52,0x0A,0x1B,0x52,0x19,0x48,0x4F,0x52,0x49,0x5A,0x4F,0x4E,0x54,0x41,0x4C,0x20,0x4F,0x52,0x0A,0x1B,0x52,0x04,0x56,0x45,0x52,0x54,0x49,0x43,0x41,0x4C,0x20,0x4F,0x52,0x49,0x45,0x4E,0x54,0x41,0x54,0x49,0x4F,0x4E,0x1B,0x69,0x02 }, + // screen 9 (14 bytes) + new byte[] { 0x1B,0x49,0x00,0x1B,0x40,0x1B,0x5A,0x00,0x00,0x00,0x00,0x1B,0x69,0x00 }, + }; + + /// Screen count. + public static int Count => Screens.Length; +} diff --git a/src/VPlasma.Core/Device/VPlasmaDevice.cs b/src/VPlasma.Core/Device/VPlasmaDevice.cs index f9ccff6..6a7620d 100644 --- a/src/VPlasma.Core/Device/VPlasmaDevice.cs +++ b/src/VPlasma.Core/Device/VPlasmaDevice.cs @@ -331,6 +331,8 @@ public sealed class VPlasmaDevice case PlasmaProtocol.CmdAttributes: case PlasmaProtocol.CmdSetRow: case PlasmaProtocol.CmdSetColumn: + case PlasmaProtocol.CmdDrawPage: + case PlasmaProtocol.CmdDisplayPage: _pendingCommand = b; _state = State.Operand; break; @@ -405,6 +407,12 @@ public sealed class VPlasmaDevice Log($"Cursor col X={operand} (ESC R {operand:X2})"); } break; + + case PlasmaProtocol.CmdDrawPage: + case PlasmaProtocol.CmdDisplayPage: + // Page select: consumed but not acted on (single-page model). + Log($"Page select (single-page: ignored) (ESC {(char)command} {operand:X2})"); + break; } _graphicsLogArmed = true; } diff --git a/src/VPlasma.Core/Protocol/PlasmaProtocol.cs b/src/VPlasma.Core/Protocol/PlasmaProtocol.cs index 3e9a690..0b6a3f7 100644 --- a/src/VPlasma.Core/Protocol/PlasmaProtocol.cs +++ b/src/VPlasma.Core/Protocol/PlasmaProtocol.cs @@ -63,6 +63,12 @@ public static class PlasmaProtocol // column (X, 0-127) — the exact range checks the firmware enforces. public const byte CmdSetRow = (byte)'Q'; public const byte CmdSetColumn = (byte)'R'; + // Page select (the firmware's 10 double-buffered screens): ESC I picks the + // draw page, ESC i the displayed page. vPLASMA models a single page, so + // these are consumed (1 operand each) but not acted on — enough to replay + // the built-in demo, which uses them heavily. + public const byte CmdDrawPage = (byte)'I'; + public const byte CmdDisplayPage = (byte)'i'; /// Operand meaning "restore the default" for ESC K / ESC H. public const byte OperandDefault = 0xFF; diff --git a/tests/VPlasma.Core.Tests/VPlasmaDeviceTests.cs b/tests/VPlasma.Core.Tests/VPlasmaDeviceTests.cs index f61d8d0..194e948 100644 --- a/tests/VPlasma.Core.Tests/VPlasmaDeviceTests.cs +++ b/tests/VPlasma.Core.Tests/VPlasmaDeviceTests.cs @@ -355,6 +355,31 @@ public class VPlasmaDeviceTests Assert.Equal(0, Pixel(device, 64, 16)); } + [Fact] + public void FirmwareDemo_ReplaysAllScreensAndDrawsText() + { + Assert.Equal(10, PlasmaFirmwareDemo.Count); + var device = new VPlasmaDevice(); + foreach (byte[] screen in PlasmaFirmwareDemo.Screens) + { + Assert.NotEmpty(screen); + device.OnReceived(screen, screen.Length); + } + // The 10-screen demo puts plenty of real text on the glass. + Assert.True(device.TextCharsDrawn > 0); + } + + [Fact] + public void PageSelect_ConsumesItsOperand() + { + var device = new VPlasmaDevice(); + // ESC I 2 (draw page) must swallow the '2'; then 'H' is the first glyph. + Feed(device, Esc, (byte)'I', 2, Esc, (byte)'i', 3, (byte)'H'); + Assert.Equal(1, device.TextCharsDrawn); + Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0)); + Assert.Equal(6, device.CursorX); // advanced exactly one cell (no stray chars) + } + [Fact] public void Reset_RestoresPowerOnState() {