using System; using System.IO.Ports; using System.Text; using System.Threading; using System.Threading.Tasks; namespace KVMote.Transport { internal sealed class SerialTransport : IKvmTransport { private const int BaudRate = 9600; private const int MaxTimeouts = 3; private SerialPort? _port; private string _portName = ""; private readonly object _lock = new(); private int _timeoutCount; private bool _disposed; public string DeviceLabel => _portName.Length > 0 ? _portName : "—"; public bool IsConnected => _port?.IsOpen == true; public int ClipboardMaxChars => 500; public int ClipboardDelayMs => 8; public event Action? DataReceived; // ── Detection ────────────────────────────────────────────────────────── public Task DetectAsync(CancellationToken ct) => Task.Run(() => Probe(ct), ct); private bool Probe(CancellationToken ct) { foreach (string port in SerialPort.GetPortNames()) { if (ct.IsCancellationRequested) return false; try { using var sp = new SerialPort(port, BaudRate) { WriteTimeout = 200, ReadTimeout = 600, Encoding = Encoding.ASCII }; sp.Open(); Thread.Sleep(150); sp.Write("~"); Thread.Sleep(500); string resp = sp.ReadExisting(); sp.Close(); if (resp.Contains("[PONG]")) { _portName = port; return true; } } catch { } } return false; } // ── Lifecycle ────────────────────────────────────────────────────────── public Task ConnectAsync() => Task.FromResult(ConnectInternal()); private bool ConnectInternal() { try { _port = new SerialPort(_portName, BaudRate) { WriteTimeout = 200, Encoding = Encoding.ASCII }; _port.DataReceived += OnDataReceived; _port.Open(); _timeoutCount = 0; return true; } catch { _port?.Dispose(); _port = null; return false; } } public void Disconnect() { if (_port is null) return; try { _port.DataReceived -= OnDataReceived; if (_port.IsOpen) _port.Close(); } catch { } finally { _port.Dispose(); _port = null; } } // ── Send ─────────────────────────────────────────────────────────────── public void Send(byte[] data) { if (_port is null || !_port.IsOpen) return; lock (_lock) { try { _port.Write(data, 0, data.Length); _timeoutCount = 0; } catch (TimeoutException) { if (++_timeoutCount >= MaxTimeouts) MarkDisconnected(); } catch { MarkDisconnected(); } } } public void SendLossy(byte[] data) { if (_port is null || !_port.IsOpen) return; if (!Monitor.TryEnter(_lock)) return; try { _port.Write(data, 0, data.Length); _timeoutCount = 0; } catch { } finally { Monitor.Exit(_lock); } } // ── Private ──────────────────────────────────────────────────────────── private void OnDataReceived(object sender, SerialDataReceivedEventArgs e) { try { string data = _port?.ReadExisting() ?? ""; if (!string.IsNullOrEmpty(data)) DataReceived?.Invoke(data); } catch { } } private void MarkDisconnected() { try { _port?.Close(); } catch { } } public void Dispose() { if (_disposed) return; _disposed = true; Disconnect(); } } }