using System.Diagnostics; using System.Drawing; using System.Drawing.Imaging; using System.IO.Ports; using VPlasma.Core.Device; using VPlasma.Core.Protocol; namespace VPlasma.Wire; /// /// Differential-test helper for the plasma display work. Generates repeatable /// command streams, replays them to any target (the real panel, the Matrix /// Portal replica, or a virtual port), captures live traffic, and renders the /// vPLASMA "golden image" for comparison. Feed the same stream to all three /// and the glass should match. /// /// Commands: /// /// synth --kind selftest|demo|charset|banner --out stream.bin /// render --in stream.bin --out frame.png [--scale 8] [--orient h|v] /// replay --in stream.bin --port COM3 [--baud 9600] [--paced] /// capture --port COM12 --out stream.bin [--tee COM3] [--seconds N] /// /// internal static class Program { private static int Main(string[] args) { if (args.Length == 0) return Usage(); var a = new Args(args); try { switch (args[0].ToLowerInvariant()) { case "synth": return Synth(a); case "render": return Render(a); case "replay": return Replay(a); case "capture": return Capture(a); case "-h" or "--help" or "help": return Usage(); default: Console.Error.WriteLine($"Unknown command '{args[0]}'."); return Usage(); } } catch (Exception ex) { Console.Error.WriteLine($"Error: {ex.Message}"); return 1; } } // ---- synth: build a repeatable command stream -------------------------- private static int Synth(Args a) { string kind = a.Get("kind", "selftest").ToLowerInvariant(); string outPath = a.Require("out"); var b = new List(); switch (kind) { case "selftest": for (int p = 0; p < PlasmaSelfTest.PageCount; p++) b.AddRange(PlasmaSelfTest.BuildPage(p)); break; case "demo": foreach (byte[] screen in PlasmaFirmwareDemo.Screens) b.AddRange(screen); break; case "banner": b.AddRange(PlasmaSelfTest.BuildPage(0)); break; case "charset": b.Add(PlasmaProtocol.Esc); b.Add(PlasmaProtocol.CmdClearScreen); b.Add(PlasmaProtocol.Esc); b.Add(PlasmaProtocol.CmdCursorMode); b.Add(0x00); for (int c = 0x20; c <= 0x7E; c++) b.Add((byte)c); break; default: Console.Error.WriteLine("--kind must be selftest | demo | banner | charset"); return 1; } File.WriteAllBytes(outPath, b.ToArray()); Console.WriteLine($"Wrote {b.Count} bytes to {outPath} (kind={kind})"); return 0; } // ---- render: the vPLASMA golden image ---------------------------------- private static int Render(Args a) { string inPath = a.Require("in"); string outPath = a.Require("out"); int scale = a.GetInt("scale", 8); bool vertical = a.Get("orient", "h").StartsWith("v", StringComparison.OrdinalIgnoreCase); var device = new VPlasmaDevice(); if (vertical) device.Orientation = PlasmaOrientation.Vertical; byte[] data = File.ReadAllBytes(inPath); device.OnReceived(data, data.Length); var frame = new byte[VPlasmaDevice.Width * VPlasmaDevice.Height]; device.CopyFrame(frame); RenderPng(frame, outPath, scale); Console.WriteLine($"Fed {data.Length} bytes; rendered {VPlasmaDevice.Width}×{VPlasmaDevice.Height} " + $"({device.TextCharsDrawn} chars, {device.GraphicsRows} graphics rows) → {outPath}"); return 0; } private static void RenderPng(byte[] frame, string outPath, int scale) { int pitch = scale; int dot = Math.Max(1, scale - 2); int W = VPlasmaDevice.Width, H = VPlasmaDevice.Height; using var bmp = new Bitmap(W * pitch, H * pitch, PixelFormat.Format24bppRgb); using var g = Graphics.FromImage(bmp); g.Clear(Color.FromArgb(20, 18, 16)); using var glass = new SolidBrush(Color.FromArgb(26, 14, 6)); g.FillRectangle(glass, 0, 0, bmp.Width, bmp.Height); using var unlit = new SolidBrush(Color.FromArgb(46, 24, 10)); using var lit = new SolidBrush(Color.FromArgb(255, 106, 26)); using var half = new SolidBrush(Color.FromArgb(150, 62, 15)); for (int y = 0; y < H; y++) { for (int x = 0; x < W; x++) { byte px = frame[y * W + x]; // Static image: render a flashing dot in its "on" phase. Brush brush = (px & VPlasmaDevice.PixelLit) != 0 ? ((px & VPlasmaDevice.PixelHalf) != 0 ? half : lit) : unlit; g.FillRectangle(brush, x * pitch, y * pitch, dot, dot); } } bmp.Save(outPath, ImageFormat.Png); } // ---- replay: send a stream to a target port ---------------------------- private static int Replay(Args a) { string inPath = a.Require("in"); string portName = a.Require("port"); int baud = a.GetInt("baud", PlasmaProtocol.BaudRate); bool paced = a.Has("paced"); byte[] data = File.ReadAllBytes(inPath); using var port = new SerialPort(portName, baud, Parity.None, 8, StopBits.One) { Handshake = Handshake.None, WriteTimeout = 5000, DtrEnable = true, RtsEnable = true, }; port.Open(); if (paced) { // One byte per 10-bit frame time — for virtual ports with no UART. long period = Stopwatch.Frequency * 10 / baud; long slot = Stopwatch.GetTimestamp(); var one = new byte[1]; foreach (byte t in data) { slot = Math.Max(slot + period, Stopwatch.GetTimestamp()); while (Stopwatch.GetTimestamp() < slot) { /* spin */ } one[0] = t; port.Write(one, 0, 1); } } else { // A real UART at `baud` paces itself; a USB-CDC target takes it fast. port.Write(data, 0, data.Length); } Console.WriteLine($"Replayed {data.Length} bytes to {portName} @ {baud} 8N1{(paced ? " (paced)" : "")}."); Thread.Sleep(200); // let the last bytes drain before closing return 0; } // ---- capture: log live traffic (optionally teeing to the display) ------ private static int Capture(Args a) { string portName = a.Require("port"); string outPath = a.Require("out"); string? teeName = a.Get("tee", ""); int seconds = a.GetInt("seconds", 0); using var port = new SerialPort(portName, a.GetInt("baud", PlasmaProtocol.BaudRate), Parity.None, 8, StopBits.One) { Handshake = Handshake.None, ReadTimeout = 200 }; port.Open(); SerialPort? tee = null; if (!string.IsNullOrWhiteSpace(teeName)) { tee = new SerialPort(teeName, a.GetInt("baud", PlasmaProtocol.BaudRate), Parity.None, 8, StopBits.One) { Handshake = Handshake.None, DtrEnable = true, RtsEnable = true }; tee.Open(); } using var outFile = File.Create(outPath); var stop = new ManualResetEventSlim(false); Console.CancelKeyPress += (_, e) => { e.Cancel = true; stop.Set(); }; var deadline = seconds > 0 ? Stopwatch.StartNew() : null; Console.WriteLine($"Capturing {portName} → {outPath}" + (tee != null ? $" (tee → {teeName})" : "") + $". {(seconds > 0 ? $"{seconds}s or " : "")}Ctrl+C to stop."); long total = 0; var buf = new byte[512]; while (!stop.IsSet && (deadline is null || deadline.Elapsed.TotalSeconds < seconds)) { int n; try { n = port.Read(buf, 0, buf.Length); } catch (TimeoutException) { continue; } if (n <= 0) continue; outFile.Write(buf, 0, n); tee?.Write(buf, 0, n); total += n; Console.Write($"\r{total} bytes captured"); } Console.WriteLine($"\nDone. {total} bytes → {outPath}"); tee?.Dispose(); return 0; } // ---- tiny arg parser --------------------------------------------------- private sealed class Args { private readonly Dictionary _kv = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet _flags = new(StringComparer.OrdinalIgnoreCase); public Args(string[] args) { for (int i = 1; i < args.Length; i++) { if (!args[i].StartsWith("--")) continue; string key = args[i].Substring(2); if (i + 1 < args.Length && !args[i + 1].StartsWith("--")) _kv[key] = args[++i]; else _flags.Add(key); } } public bool Has(string k) => _flags.Contains(k) || _kv.ContainsKey(k); public string Get(string k, string dflt) => _kv.TryGetValue(k, out var v) ? v : dflt; public int GetInt(string k, int dflt) => _kv.TryGetValue(k, out var v) && int.TryParse(v, out var n) ? n : dflt; public string Require(string k) => _kv.TryGetValue(k, out var v) ? v : throw new ArgumentException($"--{k} is required"); } private static int Usage() { Console.WriteLine(""" vPLASMA wire tool — differential-test helper. synth --kind selftest|demo|banner|charset --out stream.bin Build a repeatable command stream. render --in stream.bin --out frame.png [--scale 8] [--orient h|v] Feed the stream through the vPLASMA engine and save the resulting 128×32 frame as a PNG (the golden image). replay --in stream.bin --port COM3 [--baud 9600] [--paced] Send the stream to a target: the real panel, the Matrix Portal replica (USB-CDC), or a virtual port (--paced). capture --port COM12 --out stream.bin [--tee COM3] [--seconds N] Log live traffic to a file; --tee forwards it on to the real display so capture is non-intrusive. Ctrl+C to stop. Differential test: `synth` a stream once, `render` the golden PNG, `replay` the same stream to the replica and to the real panel, and compare the three. """); return 0; } }