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;
}
}