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