diff --git a/src/RioJoy.Core/Serial/SerialPortTransport.cs b/src/RioJoy.Core/Serial/SerialPortTransport.cs
index bb2107a..20f8dc2 100644
--- a/src/RioJoy.Core/Serial/SerialPortTransport.cs
+++ b/src/RioJoy.Core/Serial/SerialPortTransport.cs
@@ -17,6 +17,9 @@ public sealed class SerialPortTransport : IRioTransport
/// DTR reset-pulse hold time on open.
public static readonly TimeSpan DtrPulse = TimeSpan.FromMilliseconds(50);
+ /// Longest will wait for the port to close.
+ public static readonly TimeSpan CloseGracePeriod = TimeSpan.FromSeconds(2);
+
private readonly SerialPort _port;
private readonly Stream _stream;
@@ -63,6 +66,33 @@ public sealed class SerialPortTransport : IRioTransport
public void Dispose()
{
// Disposing closes and releases the COM port for other owners.
- _port.Dispose();
+ //
+ // net48's SerialStream.Dispose calls FlushFileBuffers, which blocks until
+ // the driver drains any buffered TX bytes — indefinitely when the peer has
+ // stopped reading (a wedged board, or a virtual-port pair with no reader).
+ // Seen in the field as the tray UI freezing on editor close. Abort + clear
+ // both queues first so the flush has nothing to wait on, and bound the
+ // close so no driver can hang the calling (usually UI) thread.
+ try
+ {
+ _port.DiscardOutBuffer();
+ _port.DiscardInBuffer();
+ }
+ catch
+ {
+ // Port already gone (unplugged adapter / driver error) — closing below
+ // still releases the handle.
+ }
+
+ Task close = Task.Run(() =>
+ {
+ try { _port.Dispose(); }
+ catch { /* best-effort release */ }
+ });
+
+ // If the close is still stuck in the driver despite the purge, give up after
+ // the grace period and let it finish on the pool thread; reopening the port
+ // may fail until it does, which callers already surface as a status error.
+ close.Wait(CloseGracePeriod);
}
}