On a wedged (fully silent) board the receive loop sits in a pending net48 serial read that ignores cancellation; awaiting the link before closing the port hangs the tool at run end and loses the summary - exactly what happened on the first real baseline run (the board wedged at 0.62s and stayed dead, so no byte ever completed the read). Close the port first (same order RioCoordinator.Teardown uses); applies to both monitor and --mash modes. Selftest unaffected (its fake transport honors cancellation, which is why it never caught this). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
175 lines
7.0 KiB
C#
175 lines
7.0 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] [--baud rate]
|
|
// 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] [--baud rate]
|
|
// --baud: 31250 for the retuned firmware variant (rio-firmware --baud31250).
|
|
// Exit: 0 = ran, 2 = could not open the port (mash: 1 = wedge detected).
|
|
|
|
if (args.Contains("--mash"))
|
|
return await RioSerialMonitor.MashTest.RunAsync(args);
|
|
|
|
int baud = 9600;
|
|
var positional = new List<string>();
|
|
for (int i = 0; i < args.Length; i++)
|
|
{
|
|
if (args[i] == "--baud" && i + 1 < args.Length && int.TryParse(args[i + 1], out int b)) { baud = b; i++; }
|
|
else positional.Add(args[i]);
|
|
}
|
|
string port = positional.Count > 0 ? positional[0] : "COM1";
|
|
int seconds = positional.Count > 1 && int.TryParse(positional[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} @ {baud} 8N1 for {seconds}s ==");
|
|
|
|
SerialPortTransport transport;
|
|
try
|
|
{
|
|
transport = new SerialPortTransport(port, baud);
|
|
}
|
|
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));
|
|
// Transport first: a silent board leaves the receive loop in a pending read
|
|
// that only the port close unblocks (net48 ReadAsync ignores cancellation).
|
|
cts.Cancel();
|
|
transport.Dispose();
|
|
try { await run; } catch { /* shutdown */ }
|
|
|
|
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;
|