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