|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|