From dc9a29410190185a8759ca56cc9609ae46aeea5b Mon Sep 17 00:00:00 2001 From: Cyd Date: Mon, 6 Jul 2026 13:41:16 -0500 Subject: [PATCH] Writer thread survives write faults instead of dying mid-run A single failed port write killed the paced writer thread permanently while the port still reported open, so every later byte (ACKs, the CheckRequest handshake) queued forever: wire tap showed 2 of 3 bytes of a stray ButtonPressed at t=54ms, then 1167 unanswered CheckRequests. The trigger was com0com flow control with the game side not yet open -- the third byte blocked past WriteTimeout, threw, and the catch returned. A real UART cannot wedge; it shifts bits into the line whether or not anyone listens. On a write fault, drop the stalled byte plus the stale backlog, log the stall/recovery transition once, and keep the writer alive -- TX resumes as soon as the host drains its end. Co-Authored-By: Claude Fable 5 --- src/VRio.Core/Device/VRioSerialService.cs | 34 +++++++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/VRio.Core/Device/VRioSerialService.cs b/src/VRio.Core/Device/VRioSerialService.cs index 4d33cfe..16fb36b 100644 --- a/src/VRio.Core/Device/VRioSerialService.cs +++ b/src/VRio.Core/Device/VRioSerialService.cs @@ -18,6 +18,12 @@ namespace VRio.Core.Device; /// monotonic slot deadline (slot = max(prevSlot + period, now)), so /// the stream averages the true baud rate without bursting after idle. /// +/// A write fault never kills the writer — a real UART streams into an +/// unterminated line rather than blocking. If the virtual wire's far side +/// stops draining (peer end closed, write timeout), the stalled byte and the +/// queued backlog are dropped and transmission resumes with the next fresh +/// packet once the host reads again. +/// /// RIOJoy pulses DTR for 50 ms when it opens its end (the board-reset /// handshake); through a null modem that arrives here as a DSR blip, which is /// surfaced via so the UI can show that a host @@ -201,6 +207,7 @@ public sealed class VRioSerialService : IDisposable { var one = new byte[1]; long slot = Stopwatch.GetTimestamp(); + bool txHealthy = true; // log stall/recovery transitions, not every byte while (_running) { @@ -226,12 +233,33 @@ public sealed class VRioSerialService : IDisposable try { port.Write(one, 0, 1); + if (!txHealthy) + { + txHealthy = true; + Logged?.Invoke("TX recovered — host is draining the wire again"); + } } catch (Exception ex) when (ex is IOException or InvalidOperationException or TimeoutException) { - if (_running) - Logged?.Invoke($"Write failed: {ex.Message}"); - return; + if (!_running) + return; + // A real UART cannot wedge: it shifts bits onto the line whether + // or not anyone is listening. A failed write means the virtual + // wire's far side stopped draining (peer end closed), so the + // queued backlog is already stale — drop it and keep serving + // fresh traffic; writes land again once the host reads its end. + int dropped; + lock (_txGate) + { + dropped = _txQueue.Count; + _txQueue.Clear(); + } + if (txHealthy) + { + txHealthy = false; + Logged?.Invoke($"TX stalled ({ex.Message.TrimEnd('.')}) — dropped {dropped + 1} stale byte(s), writer stays alive"); + } + continue; } // If the wait overshot its slot, pace the next byte from the