This commit is contained in:
2026-06-10 20:52:27 +01:00
parent ad5fda71b0
commit ba40821bd5
14 changed files with 1024 additions and 146 deletions
+35 -3
View File
@@ -8,6 +8,7 @@ public class CanService : ICanService
private PcanChannel _channel;
private readonly List<CanFilter> _filters = [];
private readonly List<CanBitmask> _bitmasks = [];
private readonly Dictionary<uint, CanMessageDto> _latestMessages = [];
private readonly object _lock = new();
public bool IsConnected { get; private set; }
@@ -16,9 +17,15 @@ public class CanService : ICanService
private (PcanChannel Channel, Bitrate Bitrate)? _pendingReinit;
public event Action<CanMessageDto>? MessageReceived;
public IReadOnlyDictionary<uint, CanMessageDto> GetLatestMessages()
{
lock (_lock) return new Dictionary<uint, CanMessageDto>(_latestMessages);
}
public void PublishMessage(CanMessageDto dto) => MessageReceived?.Invoke(dto);
public void UpdateLatestMessage(CanMessageDto dto)
{
lock (_lock) _latestMessages[dto.Id] = dto;
}
// ── Filters ───────────────────────────────────────────────────────────────
@@ -148,6 +155,18 @@ public class CanService : ICanService
var result = Api.Initialize(channel, bitrate);
if (result != PcanStatus.OK)
throw new InvalidOperationException($"CAN init failed: {GetErrorText(result)}");
// Optimize PCAN settings for low latency
try
{
// Try to set receive event to 0 to disable buffering delays
Api.SetValue(channel, PcanParameter.ReceiveEvent, 0u);
}
catch (Exception)
{
// Buffer optimization failed but connection still works
}
IsConnected = true;
CurrentBitrate = bitrate;
}
@@ -162,7 +181,20 @@ public class CanService : ICanService
=> Api.Read(_channel, out msg, out timestamp);
public PcanStatus Write(PcanMessage msg)
=> Api.Write(_channel, msg);
{
var status = Api.Write(_channel, msg);
if (status == PcanStatus.OK)
{
var timestampUs = (ulong)(DateTime.UtcNow.Ticks / 10);
var data = new byte[msg.DLC];
Array.Copy(msg.Data, data, msg.DLC);
UpdateLatestMessage(new CanMessageDto(
msg.ID, data, msg.DLC, timestampUs,
msg.MsgType == MessageType.Extended,
null));
}
return status;
}
private static string GetErrorText(PcanStatus status)
{
+3 -4
View File
@@ -10,10 +10,9 @@ public interface ICanService
Bitrate CurrentBitrate { get; }
/// <summary>
/// Raised on the worker thread each time a CAN message passes all filters.
/// Subscribers must marshal UI updates with InvokeAsync(StateHasChanged).
/// Gets the latest received messages, indexed by CAN ID.
/// </summary>
event Action<CanMessageDto> MessageReceived;
IReadOnlyDictionary<uint, CanMessageDto> GetLatestMessages();
// ── Filters ───────────────────────────────────────────────────────────────
IReadOnlyList<CanFilter> Filters { get; }
@@ -30,7 +29,7 @@ public interface ICanService
// ── Helpers used by the worker ────────────────────────────────────────────
bool PassesFilter(uint messageId);
IReadOnlyDictionary<string, double> ExtractSignals(uint messageId, byte[] data);
void PublishMessage(CanMessageDto dto);
void UpdateLatestMessage(CanMessageDto dto);
// ── Channel management ────────────────────────────────────────────────────
@@ -0,0 +1,26 @@
namespace IOModuleTestBlazor.Services;
public interface ISerialPortService
{
bool IsOpen { get; }
string PortName { get; }
/// <summary>Returns available COM port names.</summary>
IReadOnlyList<string> GetPortNames();
/// <summary>Opens the specified port at the given baud rate.</summary>
void Open(string portName, int baudRate);
/// <summary>Closes the port if open.</summary>
void Close();
/// <summary>Sends a line to the port (appends \r\n).</summary>
void WriteLine(string command);
/// <summary>Returns buffered terminal lines (last 200, oldest first).
/// Each line is prefixed with "&gt; " (sent) or "&lt; " (received).</summary>
IReadOnlyList<string> GetLines();
/// <summary>Fired on the SerialPort receive thread when new lines arrive.</summary>
event Action? DataReceived;
}
@@ -0,0 +1,137 @@
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();
}