Files
riojoy/tools/RioSerialMonitor/Program.cs
T
CydandClaude Fable 5 a8ec285b8e Firmware: RIOv4_2_patched_31250.bin — wedge fix + SCI retuned to 31250 baud
make_patch.py gains --baud31250: one byte beyond the wedge patch — the SCI
init operand at $D62B ($30 -> $02). BAUD $30 = /13 prescale, /1 divider
(2MHz E / 208 = 9615); $02 = /1, /4 -> 31250 exactly, 3.3x faster. The
init write also confirms the 8MHz crystal, which is why 19200/38400 are
unreachable and why 62500/125k are left alone pending an ISR cycle count.

- 24 bytes changed vs original (sha256 9f866cf3...); re-disassembly diff
  vs the classic patched image shows exactly the one operand line, and
  the classic build still reproduces 3fc8170c... (script regression-safe).
- PC side: SerialPortTransport takes an optional baudRate (default 9600,
  runtime untouched); RioSerialMonitor + --mash accept --baud 31250.
- Caveats documented in README/ANALYSIS: non-standard rate (FTDI-class
  adapters only, no 16550s); native games still speak 9600 so this chip
  is bench/RIOJoy-only; validate the wedge patch at 9600 first.

275 tests green; mash --selftest regression unchanged (FAIL/exit-1).

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

173 lines
6.9 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));
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;