|
|
|
|
using Peak.Can.Basic;
|
|
|
|
|
using IOModuleTestBlazor.Models;
|
|
|
|
|
using IOModuleTestBlazor.Services;
|
|
|
|
|
|
|
|
|
|
namespace IOModuleTestBlazor;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Background service that initialises the PCAN adapter and streams CAN messages
|
|
|
|
|
/// to the rest of the app via <see cref="ICanService.MessageReceived"/>.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public class CanWorker(
|
|
|
|
|
ILogger<CanWorker> logger,
|
|
|
|
|
IConfiguration configuration,
|
|
|
|
|
ICanService canService) : BackgroundService
|
|
|
|
|
{
|
|
|
|
|
public override Task StartAsync(CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
// Seed filters and bitmasks from appsettings regardless of connection state
|
|
|
|
|
foreach (var f in configuration.GetSection("CanOptions:Filters").Get<CanFilter[]>() ?? [])
|
|
|
|
|
canService.AddFilter(f);
|
|
|
|
|
foreach (var b in configuration.GetSection("CanOptions:Bitmasks").Get<CanBitmask[]>() ?? [])
|
|
|
|
|
canService.AddBitmask(b);
|
|
|
|
|
|
|
|
|
|
// Auto-connect if config is present; failures are non-fatal so the app
|
|
|
|
|
// still starts and the user can configure the connection from the UI.
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var channel = ResolveChannel();
|
|
|
|
|
var bitrate = ResolveBitrate();
|
|
|
|
|
canService.Initialize(channel, bitrate);
|
|
|
|
|
logger.LogInformation("CAN channel {Channel} ready at {Bitrate}", channel, bitrate);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
logger.LogWarning(ex, "CAN auto-connect failed — configure via the UI.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return base.StartAsync(cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
|
|
|
{
|
|
|
|
|
while (!stoppingToken.IsCancellationRequested)
|
|
|
|
|
{
|
|
|
|
|
// Pick up any reinit request from the UI
|
|
|
|
|
if (canService.TryConsumePendingReinit(out var newChannel, out var newBitrate))
|
|
|
|
|
{
|
|
|
|
|
logger.LogInformation("Reinitializing CAN: {Channel} at {Bitrate}", newChannel, newBitrate);
|
|
|
|
|
if (canService.IsConnected) canService.Uninitialize();
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
canService.Initialize(newChannel, newBitrate);
|
|
|
|
|
logger.LogInformation("CAN reinitialized: {Channel} at {Bitrate}", newChannel, newBitrate);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
logger.LogError(ex, "CAN reinitialization failed.");
|
|
|
|
|
}
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!canService.IsConnected)
|
|
|
|
|
{
|
|
|
|
|
await Task.Delay(100, stoppingToken);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result = canService.Read(out PcanMessage msg, out ulong timestamp);
|
|
|
|
|
|
|
|
|
|
if (result == PcanStatus.OK)
|
|
|
|
|
{
|
|
|
|
|
if (!canService.PassesFilter(msg.ID))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
var data = new byte[msg.DLC];
|
|
|
|
|
for (int i = 0; i < msg.DLC; i++)
|
|
|
|
|
data[i] = msg.Data[i];
|
|
|
|
|
|
|
|
|
|
var signals = canService.ExtractSignals(msg.ID, data);
|
|
|
|
|
|
|
|
|
|
canService.PublishMessage(new CanMessageDto(
|
|
|
|
|
Id: msg.ID,
|
|
|
|
|
Data: data,
|
|
|
|
|
Dlc: msg.DLC,
|
|
|
|
|
TimestampUs: timestamp,
|
|
|
|
|
IsExtended: msg.MsgType == MessageType.Extended,
|
|
|
|
|
Signals: signals.Count > 0 ? signals : null
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
logger.LogDebug("[RX] ID=0x{Id:X3} DLC={Dlc} Data={Data}",
|
|
|
|
|
msg.ID, msg.DLC, Convert.ToHexString(data));
|
|
|
|
|
}
|
|
|
|
|
else if (result == PcanStatus.ReceiveQueueEmpty)
|
|
|
|
|
{
|
|
|
|
|
await Task.Delay(1, stoppingToken);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
logger.LogError("CAN read error: {Error}", GetErrorText(result));
|
|
|
|
|
await Task.Delay(100, stoppingToken);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override Task StopAsync(CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
canService.Uninitialize();
|
|
|
|
|
logger.LogInformation("CAN channel uninitialized.");
|
|
|
|
|
return base.StopAsync(cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Channel / bitrate resolution ──────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
private PcanChannel ResolveChannel()
|
|
|
|
|
{
|
|
|
|
|
var name = configuration["CanOptions:Channel"];
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(name) &&
|
|
|
|
|
Enum.TryParse(name, ignoreCase: true, out PcanChannel configured))
|
|
|
|
|
return configured;
|
|
|
|
|
|
|
|
|
|
logger.LogInformation("No channel configured — scanning for available PCAN USB channels...");
|
|
|
|
|
var available = canService.GetAvailableChannels();
|
|
|
|
|
|
|
|
|
|
if (available.Count == 0)
|
|
|
|
|
throw new InvalidOperationException("No PCAN USB channels detected.");
|
|
|
|
|
|
|
|
|
|
logger.LogInformation("Auto-selected channel: {Channel}", available[0]);
|
|
|
|
|
return available[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Bitrate ResolveBitrate()
|
|
|
|
|
{
|
|
|
|
|
var name = configuration["CanOptions:Bitrate"];
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(name) &&
|
|
|
|
|
Enum.TryParse(name, ignoreCase: true, out Bitrate configured))
|
|
|
|
|
return configured;
|
|
|
|
|
|
|
|
|
|
logger.LogInformation("No bitrate configured — defaulting to 500 kbps.");
|
|
|
|
|
return Bitrate.Pcan500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string GetErrorText(PcanStatus status)
|
|
|
|
|
{
|
|
|
|
|
try { Api.GetErrorText(status, out var text); return text; }
|
|
|
|
|
catch (PcanBasicException) { return status.ToString(); }
|
|
|
|
|
}
|
|
|
|
|
}
|