The physical RIO board docs (photos, schematics, GAL decode) and the
board firmware (dumps, disassembly, make_patch.py, RIO 4.3 + FastRIO
images, analysis, testlogs) describe hardware shared across all Tesla
cockpits with a lifecycle independent of this Windows app — a
native-game-only cabinet runs the firmware and never touches RIOjoy.
They now live in TeslaRel410/restoration/{rio-hardware,rio-firmware}
with full history preserved (git subtree).
Kept here: docs/PROTOCOL.md (this app's interface contract) and the
RioSerialMonitor bench harness (--mash/--e0test, C# on RioJoy.Core).
README + code comments now point at the TeslaRel410 archive.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
304 lines
14 KiB
C#
304 lines
14 KiB
C#
using System.Diagnostics;
|
|
using RioJoy.Core.Serial;
|
|
|
|
namespace RioSerialMonitor;
|
|
|
|
/// <summary>
|
|
/// E0-threshold firmware verification (TeslaRel410 restoration/rio-firmware `--e0thresh` images).
|
|
///
|
|
/// Two phases against a DTR-reset board (counters zeroed, display F0000000):
|
|
/// - Phase A (gate hold): analog requests answered NAK-then-ACK — NAKing
|
|
/// the COMPLETED reply frame forces exactly one immediate retransmit,
|
|
/// ACKing that retransmit ends the cycle. Each event increments the
|
|
/// teardown counter $3185, invoking the display renderer sub-threshold.
|
|
/// The _e0t5 gate must keep the display F0000000; stock firmware paints
|
|
/// E0... at the first event. (Responses must follow the complete frame:
|
|
/// the board arms its reply-await only when the frame finishes sending,
|
|
/// and once the first retry timeout lapses the cycle runs blind.)
|
|
/// - Phase B (flip): analog requests we never ACK — the board runs the
|
|
/// full retry cycle (5 retransmits), gives up ($3184++, RESTART $FE)
|
|
/// and tears down ($3185++). $3185 crosses the threshold; the display
|
|
/// must flip to the E0 readout the tool predicts from observed traffic.
|
|
///
|
|
/// Bench 2026-07-19 (9600 e0t5 chip): PASS — display held F0000000 through
|
|
/// the handshake and all four phase-A teardowns, flipped exactly at the
|
|
/// 5th teardown to E0000105 ($3187=00, $3184=01, $3185=05).
|
|
///
|
|
/// 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.
|
|
/// </summary>
|
|
internal static class E0Test
|
|
{
|
|
private const int Threshold = 5;
|
|
|
|
private enum AckMode { Never, Immediate, NakThenAck }
|
|
|
|
public static async Task<int> 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<byte>();
|
|
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)
|
|
{
|
|
// ACK every completed frame (check replies
|
|
// arrive as several 0x85 frames in a row).
|
|
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.
|
|
// Bench-calibrated counter semantics (2026-07-19, confirmed on the
|
|
// display): $3184 = timeout-retry cycles (give-ups), $3185 = reply
|
|
// teardowns, i.e. any cycle that needed at least one retransmission.
|
|
await Task.Delay(300);
|
|
byte[] hs = Drain();
|
|
int cycles = SeenAll(0xFE) ? 1 : 0; // $3184 prediction
|
|
int teardowns = CountAll(0x86) > 1 ? 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 (cycles > 0 || teardowns > 0)
|
|
Console.WriteLine($" note: handshake was not clean (teardowns={teardowns}, give-ups={cycles}); predictions include it.");
|
|
Console.WriteLine(" HANDS OFF buttons/keypads for the whole run.");
|
|
Console.WriteLine();
|
|
string expected = Expected(cycles, teardowns);
|
|
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");
|
|
|
|
if (ok)
|
|
{
|
|
// Edit-6 acceptance (rc1/RIO4.3 chips): a CheckRequest self-test
|
|
// must repaint the display afterwards — F0000000 when healthy,
|
|
// or the E0 readout when counters are over threshold (they are
|
|
// now, after the flip). Chips without edit 6 show 04000000 here.
|
|
PhaseReset(AckMode.Immediate, 0x85, 4); // check replies: cmd+2+ck
|
|
await transport.WriteAsync(new byte[] { 0x80, 0x00 }, CancellationToken.None);
|
|
byte[] chk = await Collect(4000, _ => false); // self-test ~1-2s, lamps flash
|
|
int checkReplies = Count(chk, 0x85);
|
|
Console.WriteLine($"[check ] CheckRequest sent: self-test ran (lamps flash), {checkReplies} status frame(s).");
|
|
Console.WriteLine($" Edit-6 chips repaint, then re-render the over-threshold E0 readout.");
|
|
Console.WriteLine($" >>> DISPLAY SHOULD NOW READ: {Expected(cycles, teardowns)} — small drift in either pair is normal");
|
|
Console.WriteLine($" (late ACKs among the status frames start timeout retries: $3184 +1 each;");
|
|
Console.WriteLine($" 04000000 here = no edit 6 on this chip) <<<");
|
|
await Task.Delay(3000);
|
|
}
|
|
|
|
Console.WriteLine();
|
|
Console.WriteLine("== done ==");
|
|
Console.WriteLine($" final predicted counters: $3184=${cycles:X2} $3185=${teardowns:X2} -> display {Expected(cycles, teardowns)}");
|
|
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<bool> 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;
|
|
}
|
|
|
|
if (transmissions > 1) teardowns++; // $3185: imperfect cycle
|
|
if (sawRestart && mode == AckMode.Never) cycles++; // $3184: give-up
|
|
|
|
Console.WriteLine($"[event {eventNo}] {phase}: reply x{transmissions} " +
|
|
$"({transmissions - 1} retransmit(s)), give-up cycle={mode == AckMode.Never} " +
|
|
$"-> $3184=${cycles:X2} $3185=${teardowns:X2}");
|
|
Console.WriteLine($" >>> DISPLAY SHOULD NOW READ: {Expected(cycles, teardowns)} <<<");
|
|
await Task.Delay(4000);
|
|
return true;
|
|
}
|
|
|
|
string Expected(int cy, int td) =>
|
|
cy >= Threshold || td >= Threshold ? $"E000{cy:X2}{td: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<byte[]> Collect(int ms, Func<byte[], bool> done)
|
|
{
|
|
var end = sw.Elapsed + TimeSpan.FromMilliseconds(ms);
|
|
var all = new List<byte>();
|
|
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();
|
|
}
|
|
}
|