Add RioSerialMonitor hardware-bringup tool

A console diagnostic that opens the RIO COM port, runs the real serial link,
and logs every event (buttons, keypad, axis, version/check, control bytes,
framing). Flashes all lamps once to prove the PC->RIO path and echoes a lamp
on each button press. Verified end-to-end against a partial RIO on COM1
(firmware 4.2; MFD + keypad inputs and the throttle axis all decode).

    dotnet run --project tools/RioSerialMonitor -- COM1 30

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-06-28 21:12:22 -05:00
co-authored by Claude Opus 4.8
parent 4994ab699f
commit 36bf1c0190
2 changed files with 173 additions and 0 deletions
+158
View File
@@ -0,0 +1,158 @@
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]
// Exit: 0 = ran, 2 = could not open the port.
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];
Array.Fill(min, short.MaxValue); Array.Fill(max, 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++;
ReadOnlySpan<byte> pl = p.Payload.Span;
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((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;
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>RioSerialMonitor</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\RioJoy.Core\RioJoy.Core.csproj" />
</ItemGroup>
</Project>