From 6b02eb4d1b388951bd79c8b16695f85debd8dec2 Mon Sep 17 00:00:00 2001 From: Cyd Date: Sun, 19 Jul 2026 15:32:21 -0500 Subject: [PATCH] =?UTF-8?q?RioSerialMonitor:=20--e0test=20mode=20=E2=80=94?= =?UTF-8?q?=20on-hardware=20E0-threshold=20verification?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two-phase test against a DTR-reset board: phase A forces sub-threshold retransmits (NAK the completed reply frame -> exactly one counted retransmit, then ACK the retry), where the display must hold F0000000; phase B withholds ACKs for a full give-up cycle to cross the threshold, with the expected E0 readout predicted from observed traffic. Bench findings while building it (9600, e0t5 chip): the reply-retry machine sends 5 retransmits per cycle (not 4); responses are honored only after the complete reply frame (mid-frame ACK/NAK/RESTART is ignored); once the first retry fires the cycle runs blind to give-up; a host NAK triggers an immediate counted retransmit. Co-Authored-By: Claude Fable 5 --- tools/RioSerialMonitor/E0Test.cs | 273 ++++++++++++++++++++++++++++++ tools/RioSerialMonitor/Program.cs | 5 + 2 files changed, 278 insertions(+) create mode 100644 tools/RioSerialMonitor/E0Test.cs diff --git a/tools/RioSerialMonitor/E0Test.cs b/tools/RioSerialMonitor/E0Test.cs new file mode 100644 index 0000000..a228fe1 --- /dev/null +++ b/tools/RioSerialMonitor/E0Test.cs @@ -0,0 +1,273 @@ +using System.Diagnostics; +using RioJoy.Core.Serial; + +namespace RioSerialMonitor; + +/// +/// E0-threshold firmware verification (rio-firmware `--e0thresh` images). +/// +/// Two phases against a DTR-reset board (counters zeroed, display F0000000): +/// - Phase A (gate hold): analog requests whose reply we ACK only after the +/// first retransmit — each event increments the board's retransmit +/// counter ($3184) by a small sub-threshold amount, calling the display +/// renderer every time. The _e0t5 gate must keep the display F0000000; +/// stock firmware paints E0... at the first retry. +/// - Phase B (flip): analog requests we never ACK — the board runs the full +/// retry cycle (observed: 5 retransmits) and gives up ($3185++, RESTART +/// $FE). The counters cross the threshold and the display must flip to +/// the E0 readout, which the tool predicts from observed traffic. +/// +/// dotnet run --project tools/RioSerialMonitor -- --e0test [port] +/// [--baud rate] [--hold n] [--flip n] +/// +/// No --baud probes 9600, 31250, 62500 (DTR-resetting each try). HANDS OFF +/// buttons/keypads during the run — an unACKed button packet shifts the +/// counters (the tool folds observed traffic into its predictions either +/// way). Exit: 0 = ran, 2 = no board reply on any baud. +/// +internal static class E0Test +{ + private const int Threshold = 5; + + private enum AckMode { Never, Immediate, NakThenAck } + + public static async Task RunAsync(string[] args) + { + string port = "COM1"; + int baud = 0, holdEvents = 4, flipEvents = 1; + for (int i = 0; i < args.Length; i++) + { + switch (args[i]) + { + case "--e0test": break; + case "--baud" when i + 1 < args.Length && int.TryParse(args[i + 1], out int b): baud = b; i++; break; + case "--hold" when i + 1 < args.Length && int.TryParse(args[i + 1], out int h) && h >= 0: holdEvents = h; i++; break; + case "--flip" when i + 1 < args.Length && int.TryParse(args[i + 1], out int f) && f > 0: flipEvents = f; i++; break; + default: + if (!args[i].StartsWith("--")) port = args[i]; + break; + } + } + + var sw = Stopwatch.StartNew(); + object gate = new(); + var buffer = new List(); + int cursor = 0; + + // Reader-thread ACK automation (latency matters: the board's ACK wait + // at 9600 is ~4ms/retry, so ACKs must not wait for a polling loop). + AckMode ackMode = AckMode.Never; + int replyByte = 0x87; // frame command byte the current phase expects + int frameLen = 12; // full frame: cmd + payload + checksum + int repliesSeen = 0; // completed frames of replyByte since phase reset + int frameCountdown = 0; // >0: inside a reply frame, bytes remaining + bool ackSent = false; + + SerialPortTransport? transport = null; + Task? reader = null; + + void PhaseReset(AckMode mode, int expectReply, int expectLen) + { + lock (gate) + { + ackMode = mode; + replyByte = expectReply; + frameLen = expectLen; + repliesSeen = 0; + frameCountdown = 0; + ackSent = false; + cursor = buffer.Count; + } + } + + int[] bauds = baud != 0 ? new[] { baud } : new[] { 9600, 31250, 62500 }; + (int Major, int Minor)? version = null; + + foreach (int tryBaud in bauds) + { + Console.WriteLine($"[{sw.Elapsed.TotalSeconds,6:F2}s] probing {port} @ {tryBaud} (DTR reset, ~2s boot wait)..."); + try { transport = new SerialPortTransport(port, tryBaud); } + catch (Exception ex) + { + Console.WriteLine($" FAILED to open {port}: {ex.GetType().Name}: {ex.Message}"); + return 2; + } + + lock (gate) { buffer.Clear(); cursor = 0; } + SerialPortTransport t = transport; + reader = Task.Run(async () => + { + var tmp = new byte[256]; + try + { + while (true) + { + int n = await t.ReadAsync(tmp, CancellationToken.None); + if (n <= 0) break; + byte? respond = null; + lock (gate) + { + for (int i = 0; i < n; i++) + { + buffer.Add(tmp[i]); + // Frame tracking: respond only once the FULL reply + // frame is on the wire — the board arms its + // reply-await when the frame finishes sending; a + // response mid-frame is ignored. + if (frameCountdown > 0) + { + if (--frameCountdown > 0) continue; + repliesSeen++; + if (ackMode == AckMode.Immediate && !ackSent) + { + ackSent = true; + respond = 0xFC; + } + else if (ackMode == AckMode.NakThenAck && repliesSeen <= 2) + { + // frame 1 -> NAK (board resends, $3184++), + // frame 2 -> ACK (cycle ends cleanly). + respond = repliesSeen == 1 ? (byte)0xFD : (byte)0xFC; + ackSent = repliesSeen == 2; + } + } + else if (tmp[i] == replyByte) + { + frameCountdown = frameLen - 1; + } + } + } + if (respond is byte r) + await t.WriteAsync(new byte[] { r }, CancellationToken.None); + } + } + catch { /* port closed */ } + }); + + await Task.Delay(2000); // boot: counters cleared, display F0000000 + + PhaseReset(AckMode.Immediate, 0x86, 4); // version reply: cmd+2+ck + await t.WriteAsync(new byte[] { 0x81, 0x01 }, CancellationToken.None); + byte[] got = await Collect(1200, b => IndexOf(b, 0x86) >= 0); + int vi = IndexOf(got, 0x86); + if (vi >= 0) + { + if (vi + 2 < got.Length) version = (got[vi + 1], got[vi + 2]); + baud = tryBaud; + break; + } + + Console.WriteLine($" no version reply at {tryBaud}"); + transport.Dispose(); + transport = null; + if (reader is not null) await reader; + } + + if (transport is null) + { + Console.WriteLine(" no board reply on any baud — is the board powered / the right chip in?"); + return 2; + } + + // Handshake bookkeeping: fold any retries/give-up into the prediction. + await Task.Delay(300); + byte[] hs = Drain(); + int retx = Math.Max(0, CountAll(0x86) - 1); // $3184 prediction + int giveup = SeenAll(0xFE) ? 1 : 0; // $3185 prediction + + Console.WriteLine(); + Console.WriteLine($"== E0-threshold test :: {transport.Description}, firmware {version?.Major}.{version?.Minor} =="); + Console.WriteLine($" threshold {Threshold}; phase A: {holdEvents} sub-threshold event(s), phase B: {flipEvents} give-up event(s)"); + if (retx > 0 || giveup > 0) + Console.WriteLine($" note: handshake cost {retx} retransmit(s){(giveup > 0 ? " + a give-up" : "")}; predictions include them."); + Console.WriteLine(" HANDS OFF buttons/keypads for the whole run."); + Console.WriteLine(); + string expected = Expected(retx, giveup); + Console.WriteLine($">>> WATCH THE 8-DIGIT DISPLAY. It should read {expected} right now. <<<"); + await Task.Delay(4000); + + int eventNo = 0; + bool ok = true; + + for (int k = 0; k < holdEvents && ok; k++) + ok = await RunEvent(AckMode.NakThenAck, "hold"); + for (int k = 0; k < flipEvents && ok; k++) + ok = await RunEvent(AckMode.Never, "flip"); + + Console.WriteLine(); + Console.WriteLine("== done =="); + Console.WriteLine($" final predicted counters: retx=${retx:X2} giveup=${giveup:X2} -> display {Expected(retx, giveup)}"); + Console.WriteLine(" PASS = display held F0000000 through every sub-threshold line above and"); + Console.WriteLine(" matched the E0 predictions after the threshold crossing."); + Console.WriteLine(" (Stock firmware flips at the very first retry.) DTR reset restores F0."); + + transport.Dispose(); + if (reader is not null) await reader; + return 0; + + async Task RunEvent(AckMode mode, string phase) + { + eventNo++; + PhaseReset(mode, 0x87, 12); // analog reply: cmd+10+ck + await transport!.WriteAsync(new byte[] { 0x82, 0x02 }, CancellationToken.None); + + byte[] bytes = mode == AckMode.Never + ? await Collect(2500, b => IndexOf(b, 0xFE) >= 0) + : await Collect(800, _ => false); + await Task.Delay(250); // let the cycle finish either way + bytes = Concat(bytes, Drain()); + + int transmissions = Count(bytes, 0x87); + bool sawRestart = IndexOf(bytes, 0xFE) >= 0; + if (transmissions == 0) + { + Console.WriteLine($"[event {eventNo}] NO analog reply ({bytes.Length} bytes) — aborting."); + return false; + } + + retx += Math.Max(0, transmissions - 1); + if (sawRestart) giveup++; + + Console.WriteLine($"[event {eventNo}] {phase}: reply x{transmissions} " + + $"({transmissions - 1} retries), give-up={sawRestart} " + + $"-> retx=${retx:X2} giveup=${giveup:X2}"); + Console.WriteLine($" >>> DISPLAY SHOULD NOW READ: {Expected(retx, giveup)} <<<"); + await Task.Delay(4000); + return true; + } + + string Expected(int r, int g) => + r >= Threshold || g >= Threshold ? $"E000{r:X2}{g:X2}" : "F0000000"; + + byte[] Drain() + { + lock (gate) + { + byte[] r = buffer.Skip(cursor).ToArray(); + cursor = buffer.Count; + return r; + } + } + + int CountAll(byte v) { lock (gate) return buffer.Count(x => x == v); } + bool SeenAll(byte v) { lock (gate) return buffer.Contains(v); } + + async Task Collect(int ms, Func done) + { + var end = sw.Elapsed + TimeSpan.FromMilliseconds(ms); + var all = new List(); + while (sw.Elapsed < end) + { + all.AddRange(Drain()); + if (done(all.ToArray())) break; + await Task.Delay(15); + } + all.AddRange(Drain()); + return all.ToArray(); + } + + static int IndexOf(byte[] a, byte v) => Array.IndexOf(a, v); + static int Count(byte[] a, byte v) => a.Count(x => x == v); + static byte[] Concat(byte[] a, byte[] b) => a.Concat(b).ToArray(); + } +} diff --git a/tools/RioSerialMonitor/Program.cs b/tools/RioSerialMonitor/Program.cs index b72acd7..e2e5ebb 100644 --- a/tools/RioSerialMonitor/Program.cs +++ b/tools/RioSerialMonitor/Program.cs @@ -15,6 +15,11 @@ using RioJoy.Core.Serial; // --baud: 31250 for the retuned firmware variant (rio-firmware --baud31250). // Exit: 0 = ran, 2 = could not open the port (mash: 1 = wedge detected). +// E0-threshold firmware verification (rio-firmware --e0thresh images, see E0Test.cs): +// dotnet run --project tools/RioSerialMonitor -- --e0test [port] [--baud rate] [--events n] +if (args.Contains("--e0test")) + return await RioSerialMonitor.E0Test.RunAsync(args); + if (args.Contains("--mash")) return await RioSerialMonitor.MashTest.RunAsync(args);