KVMote/Transport/SerialTransport.cs

153 lines
4.9 KiB
C#

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<string>? DataReceived;
// ── Detection ──────────────────────────────────────────────────────────
public Task<bool> 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<bool> 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();
}
}
}