Files
riojoy/tools/RioSerialMonitor/SelftestTransport.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

109 lines
4.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Concurrent;
using System.Diagnostics;
using RioJoy.Core.Protocol;
using RioJoy.Core.Serial;
namespace RioSerialMonitor;
/// <summary>
/// Scripted in-memory RIO for <c>--mash --selftest</c>: proves the mash-test
/// instrumentation (wedge alarm, revival classification, counter delta) works
/// before it is trusted to judge firmware at the cabinet.
///
/// Timeline (seconds from open):
/// 0.0 4.0 analog replies every 50 ms (healthy)
/// 4.0 6.7 analog SILENT (the wedge; alarm must fire at ~6.0 with the
/// default 2 s threshold) while the port stays open
/// 6.5 / 7.0 ButtonPressed / ButtonReleased 0x05 (masher still mashing)
/// 6.7 end analog resumes 200 ms after the button — the tool must
/// classify the wedge as "button-revived" (unpatched signature)
/// CheckRequest is answered with RestartCount 3 / FullBufferCount 1 on the
/// first ask and 7 / 2 afterwards, so the counter delta must read +4 / +1.
/// Expected run outcome: 1 wedge event, verdict FAIL (button-revived), exit 1.
/// </summary>
internal sealed class SelftestTransport : IRioTransport
{
private readonly ConcurrentQueue<byte[]> _rx = new();
private readonly SemaphoreSlim _rxReady = new(0);
private readonly CancellationTokenSource _cts = new();
private readonly Stopwatch _clock = Stopwatch.StartNew();
private int _checkCalls;
public SelftestTransport() => _ = ProduceAsync(_cts.Token);
public string Description => "selftest (scripted in-memory RIO)";
public async Task<int> ReadAsync(byte[] buffer, CancellationToken cancellationToken)
{
using var linked = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cts.Token);
await _rxReady.WaitAsync(linked.Token).ConfigureAwait(false);
if (!_rx.TryDequeue(out byte[]? chunk))
return 0;
Array.Copy(chunk, buffer, chunk.Length);
return chunk.Length;
}
public Task WriteAsync(byte[] data, CancellationToken cancellationToken)
{
// Answer the requests the test sends; swallow analog polls + lamp writes.
if (data.Length > 0)
{
if (data[0] == (byte)RioCommand.VersionRequest)
{
Enqueue(PacketBuilder.Build(RioCommand.VersionReply, new byte[] { 4, 2 }));
}
else if (data[0] == (byte)RioCommand.CheckRequest)
{
bool first = Interlocked.Increment(ref _checkCalls) == 1;
Enqueue(PacketBuilder.Build(RioCommand.CheckReply,
new[] { (byte)RioStatusType.RestartCount, first ? (byte)3 : (byte)7 }));
Enqueue(PacketBuilder.Build(RioCommand.CheckReply,
new[] { (byte)RioStatusType.AbandonCount, (byte)0 }));
Enqueue(PacketBuilder.Build(RioCommand.CheckReply,
new[] { (byte)RioStatusType.FullBufferCount, first ? (byte)1 : (byte)2 }));
}
}
return Task.CompletedTask;
}
private async Task ProduceAsync(CancellationToken ct)
{
bool pressSent = false, releaseSent = false;
var analogPayload = new byte[10]; // all axes zero — valid sample
while (!ct.IsCancellationRequested)
{
await Task.Delay(50, ct).ConfigureAwait(false);
double t = _clock.Elapsed.TotalSeconds;
bool inGap = t >= 4.0 && t < 6.7;
if (!inGap)
Enqueue(PacketBuilder.Build(RioCommand.AnalogReply, analogPayload));
if (!pressSent && t >= 6.5)
{
pressSent = true;
Enqueue(PacketBuilder.Build(RioCommand.ButtonPressed, new byte[] { 0x05 }));
}
if (!releaseSent && t >= 7.0)
{
releaseSent = true;
Enqueue(PacketBuilder.Build(RioCommand.ButtonReleased, new byte[] { 0x05 }));
}
}
}
private void Enqueue(byte[] packet)
{
_rx.Enqueue(packet);
_rxReady.Release();
}
public void Dispose()
{
_cts.Cancel();
_rxReady.Release(); // wake a pending read so the loop can wind down
_cts.Dispose();
}
}