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