Files
riojoy/tools/RioSerialMonitor/Program.cs
T
CydandClaude Fable 5 fcf26cfd15 Firmware validation harness: instrumented RIO_TAP mash test in RioSerialMonitor
New --mash mode (tools/RioSerialMonitor/MashTest.cs) mechanizes the
wedge-patch validation plan from RIOv4_2-ANALYSIS.md:
- Runs the live link with the app's >5s reset-recovery DISABLED so a
  board wedge stays observable, and echoes lamps on every press
  (lamp/reply collisions are the wedge trigger).
- Gap timing uses ANY AnalogReply packet (0xFE sentinels included -
  a sentinel still proves the reply path is alive); logs a gap
  histogram + top-10 longest gaps with timestamps.
- WEDGE detector: analog silent past the threshold (default 2s) ->
  beep + banner; on resume, classifies self-recovered (patched
  expectation) vs button-revived (button event within 300ms of resume,
  the unpatched signature) vs unresolved at run end.
- Board self-reported RestartCount/AbandonCount/FullBufferCount
  snapshotted before/after via CheckRequest, delta printed
  (7-bit wrap-aware).
- Fixed-layout summary teed to riomash-<label>-<stamp>.log so
  baseline-vs-patched runs diff directly. Exit 0 = no wedge, 1 = wedge.

--mash --selftest drives the whole instrument against a scripted
in-memory board (SelftestTransport) that goes silent at t=4.0s and
revives 200ms after a button at t=6.5s: verified end-to-end - alarm at
6.0s, wedge classified button-revived (2.75s), counter delta +4/+0/+1,
verdict FAIL, exit 1. Use it to sanity-check the alarm at the cabinet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 14:35:27 -05:00

165 lines
6.5 KiB
C#

using System.Diagnostics;
using RioJoy.Core.Editing;
using RioJoy.Core.Mapping;
using RioJoy.Core.Protocol;
using RioJoy.Core.Serial;
// Hardware bringup monitor: open the RIO COM port, run the real serial link, and
// log every event (buttons, keypad, axis, version/check, control bytes, framing).
// It flashes all lamps once to prove the PC -> RIO output path, then echoes a lamp
// on each button press so a physical press lights up.
// dotnet run --project tools/RioSerialMonitor -- [port] [seconds]
// Firmware wedge-patch validation (RIO_TAP mash test, see MashTest.cs):
// dotnet run --project tools/RioSerialMonitor -- --mash [port] [seconds]
// [--label baseline|patched] [--no-lamps] [--wedge seconds]
// Exit: 0 = ran, 2 = could not open the port (mash: 1 = wedge detected).
if (args.Contains("--mash"))
return await RioSerialMonitor.MashTest.RunAsync(args);
string port = args.Length > 0 ? args[0] : "COM1";
int seconds = args.Length > 1 && int.TryParse(args[1], out int s) ? s : 30;
Dictionary<int, string> groupOf = CockpitPanel.Buttons().ToDictionary(b => b.Address, b => b.Group.Title);
string Name(int addr) => groupOf.TryGetValue(addr, out string? g) ? $"0x{addr:X2} ({g})" : $"0x{addr:X2}";
var gate = new object();
var sw = Stopwatch.StartNew();
void Log(string msg)
{
lock (gate) Console.WriteLine($"[{sw.Elapsed.TotalSeconds,6:F2}s] {msg}");
}
Console.WriteLine($"== RIO serial monitor :: {port} @ 9600 8N1 for {seconds}s ==");
SerialPortTransport transport;
try
{
transport = new SerialPortTransport(port);
}
catch (Exception ex)
{
Console.WriteLine($" FAILED to open {port}: {ex.GetType().Name}: {ex.Message}");
Console.WriteLine(" (Is the tray app or another program holding the port? Is the cable on the right COM?)");
return 2;
}
Console.WriteLine($" opened {transport.Description}");
var link = new RioSerialLink(transport);
int packets = 0, presses = 0, releases = 0, analog = 0, controls = 0, framing = 0;
var seen = new SortedSet<int>();
VersionInfo? version = null;
var checks = new List<CheckStatus>();
// Analog: log on meaningful change only (it polls ~18x/sec). Track per-field range.
const int Deadband = 30;
AnalogReport lastLogged = default;
bool haveAnalog = false;
TimeSpan lastAnalogLog = TimeSpan.Zero;
var min = new short[5]; var max = new short[5];
for (int i = 0; i < min.Length; i++) { min[i] = short.MaxValue; max[i] = short.MinValue; }
void Track(AnalogReport r)
{
Span<short> v = stackalloc short[] { r.Throttle, r.LeftPedal, r.RightPedal, r.JoystickY, r.JoystickX };
for (int i = 0; i < 5; i++) { if (v[i] < min[i]) min[i] = v[i]; if (v[i] > max[i]) max[i] = v[i]; }
}
bool Changed(AnalogReport a, AnalogReport b) =>
Math.Abs(a.Throttle - b.Throttle) > Deadband || Math.Abs(a.LeftPedal - b.LeftPedal) > Deadband ||
Math.Abs(a.RightPedal - b.RightPedal) > Deadband || Math.Abs(a.JoystickY - b.JoystickY) > Deadband ||
Math.Abs(a.JoystickX - b.JoystickX) > Deadband;
link.PacketReceived += p =>
{
packets++;
byte[] pl = p.Payload;
switch (p.Command)
{
case RioCommand.ButtonPressed when pl[0] < RioAddress.ButtonCount:
{
int a = RioAddress.FromButton(pl[0]); presses++; seen.Add(a);
Log($"BUTTON DOWN {Name(a)}");
_ = link.SetLampAsync((byte)a, RioLampState.SolidBright); // light it
break;
}
case RioCommand.ButtonReleased when pl[0] < RioAddress.ButtonCount:
{
int a = RioAddress.FromButton(pl[0]); releases++; seen.Add(a);
Log($"button up {Name(a)}");
_ = link.SetLampAsync((byte)a, RioLampState.SolidDim);
break;
}
case RioCommand.KeyPressed when pl[0] is 0 or 1 && pl[1] <= 0x0F:
{
int a = RioAddress.FromKeypad(pl[0], pl[1]); presses++; seen.Add(a);
Log($"KEYPAD DOWN pad{pl[0]} key 0x{pl[1]:X} -> {Name(a)}");
break;
}
case RioCommand.KeyReleased when pl[0] is 0 or 1 && pl[1] <= 0x0F:
{
int a = RioAddress.FromKeypad(pl[0], pl[1]); releases++; seen.Add(a);
Log($"keypad up pad{pl[0]} key 0x{pl[1]:X} -> {Name(a)}");
break;
}
}
};
link.AnalogReceived += r =>
{
analog++;
Track(r);
if (!haveAnalog || (Changed(r, lastLogged) && sw.Elapsed - lastAnalogLog > TimeSpan.FromMilliseconds(200)))
{
Log($"analog {r}");
lastLogged = r; haveAnalog = true; lastAnalogLog = sw.Elapsed;
}
};
link.VersionReceived += v => { version = v; Log($"VERSION reply: firmware {v}"); };
link.CheckReceived += c => { checks.Add(c); Log($"CHECK reply: {c}"); };
link.ControlReceived += b => { controls++; Log($"control byte 0x{b:X2} ({(Enum.IsDefined(typeof(RioControl), (RioControl)b) ? (RioControl)b : (object)"?")})"); };
link.FramingError += () => { framing++; Log("FRAMING ERROR (resync)"); };
using var cts = new CancellationTokenSource();
Task run = link.RunAsync(cts.Token);
await Task.Delay(400);
Log("requesting firmware version + board/lamp check...");
await link.RequestVersionAsync();
await link.RequestCheckAsync();
await Task.Delay(900);
Log("lamp test: all lamps BRIGHT for 1.5s (watch the MFD)...");
for (int a = 0; a <= RioAddress.MaxAddress; a++) _ = link.SetLampAsync((byte)a, RioLampState.SolidBright);
await Task.Delay(1500);
for (int a = 0; a <= RioAddress.MaxAddress; a++) _ = link.SetLampAsync((byte)a, RioLampState.SolidOff);
Log(">>> NOW: press the MFD buttons, press keypad keys, and move the axis <<<");
Log(">>> a pressed button should light up <<<");
await Task.Delay(TimeSpan.FromSeconds(seconds));
cts.Cancel();
try { await run; } catch { /* shutdown */ }
transport.Dispose();
Console.WriteLine();
Console.WriteLine("== summary ==");
Console.WriteLine($" packets={packets} presses={presses} releases={releases} analog={analog} control={controls} framing={framing}");
Console.WriteLine($" firmware version : {(version is null ? "(no reply)" : version.ToString())}");
Console.WriteLine($" check items : {(checks.Count == 0 ? "(none)" : string.Join(", ", checks))}");
Console.WriteLine($" addresses seen : {(seen.Count == 0 ? "(none)" : string.Join(", ", seen.Select(Name)))}");
if (haveAnalog)
{
string[] axes = { "Throttle", "LeftPedal", "RightPedal", "JoyY", "JoyX" };
Console.WriteLine(" analog raw range (min..max), the live axis is the one that moved:");
for (int i = 0; i < 5; i++)
Console.WriteLine($" {axes[i],-10}: {min[i],6} .. {max[i],-6} (span {max[i] - min[i]})");
}
else
{
Console.WriteLine(" analog: (no analog replies received)");
}
return 0;