Blazor app with can interface
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.

144 lines
3.6 KiB

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