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()
{