You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
137 lines
3.5 KiB
137 lines
3.5 KiB
using System.IO.Ports; |
|
|
|
namespace IOModuleTestBlazor.Services; |
|
|
|
public sealed class SerialPortService : ISerialPortService, IDisposable |
|
{ |
|
private readonly Lock _lock = new(); |
|
private SerialPort? _port; |
|
private readonly List<string> _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<string> 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<string> GetLines() |
|
{ |
|
lock (_lock) |
|
return _lines.ToList(); |
|
} |
|
|
|
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(); |
|
}
|
|
|