using System.Collections.Concurrent; using System.Diagnostics; using RioJoy.Core.Protocol; using RioJoy.Core.Serial; namespace RioSerialMonitor; /// /// Scripted in-memory RIO for --mash --selftest: 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. /// internal sealed class SelftestTransport : IRioTransport { private readonly ConcurrentQueue _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 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(); } }