using System.IO.Ports; namespace IOModuleTestBlazor.Services; public sealed class SerialPortService : ISerialPortService, IDisposable { private readonly Lock _lock = new(); private SerialPort? _port; private readonly List _lines = new(200); private string _receiveBuffer = string.Empty; private const int MaxLines = 200; public bool IsOpen => _port?.IsOpen == true; public string PortName => _port?.PortName ?? string.Empty; public event Action? DataReceived; public IReadOnlyList GetPortNames() => SerialPort.GetPortNames(); public void Open(string portName, int baudRate) { SerialPort newPort; lock (_lock) { ClosePortUnsafe(); newPort = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One) { NewLine = "\r\n", ReadTimeout = SerialPort.InfiniteTimeout, WriteTimeout = 500, Encoding = System.Text.Encoding.ASCII, }; } // Open outside the lock — USB VCP init can block ~1 s; don't hold _lock during that. try { newPort.Open(); } catch { newPort.Dispose(); throw; } lock (_lock) { _port = newPort; _port.DataReceived += OnDataReceived; } } public void Close() { lock (_lock) ClosePortUnsafe(); } // Must be called with _lock held. System.Threading.Lock is non-reentrant, // so Close() cannot be called from Open() while the lock is already held. private void ClosePortUnsafe() { if (_port is null) return; _port.DataReceived -= OnDataReceived; try { _port.Close(); } catch { /* ignore */ } _port.Dispose(); _port = null; } public void WriteLine(string command) { SerialPort? port; lock (_lock) { port = _port; } if (port?.IsOpen != true) return; try { port.WriteLine(command); } catch (Exception ex) when (ex is IOException or TimeoutException or InvalidOperationException or UnauthorizedAccessException) { // Device stopped responding or VCP disconnected — close so IsOpen reflects reality. Close(); DataReceived?.Invoke(); return; } AppendLine($"> {command}"); } public IReadOnlyList GetLines() { lock (_lock) return _lines.ToList(); } public void ClearLines() { lock (_lock) _lines.Clear(); } private void OnDataReceived(object sender, SerialDataReceivedEventArgs e) { SerialPort? port; lock (_lock) { port = _port; } if (port is null) return; try { string incoming = port.ReadExisting(); _receiveBuffer += incoming; // Split on newlines, keep partial last line in buffer var parts = _receiveBuffer.Split('\n'); for (int i = 0; i < parts.Length - 1; i++) { var line = parts[i].TrimEnd('\r'); if (line.Length > 0) AppendLine($"< {line}"); } _receiveBuffer = parts[^1]; } catch { /* port closed mid-read */ } DataReceived?.Invoke(); } private void AppendLine(string line) { lock (_lock) { if (_lines.Count >= MaxLines) _lines.RemoveAt(0); _lines.Add(line); } } public void Dispose() => Close(); }