WIP
This commit is contained in:
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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 "> " (sent) or "< " (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();
|
||||
}
|
||||
Reference in New Issue
Block a user