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.

140 lines
4.9 KiB

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)
{
var channel = ResolveChannel();
var bitrate = ResolveBitrate();
try
{
canService.Initialize(channel, bitrate);
}
catch (InvalidOperationException ex)
{
logger.LogError(ex, "Failed to initialize CAN channel {Channel}", channel);
throw;
}
// Seed filters and bitmasks from appsettings
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);
logger.LogInformation("CAN channel {Channel} ready at {Bitrate}", channel, bitrate);
return base.StartAsync(cancellationToken);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
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 = GetAvailableUsbChannels();
if (available.Count == 0)
throw new InvalidOperationException(
"No PCAN USB channels detected. Set CanOptions:Channel in appsettings.json.");
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 List<PcanChannel> GetAvailableUsbChannels()
{
var available = new List<PcanChannel>();
foreach (var ch in Enum.GetValues<PcanChannel>()
.Where(c => c.ToString().StartsWith("Usb", StringComparison.OrdinalIgnoreCase))
.OrderBy(c => c.ToString()))
{
var result = Api.GetValue(ch, PcanParameter.ChannelCondition, out uint condition);
if (result == PcanStatus.OK && condition != 0)
available.Add(ch);
}
return available;
}
private static string GetErrorText(PcanStatus status)
{
try { Api.GetErrorText(status, out var text); return text; }
catch (PcanBasicException) { return status.ToString(); }
}
}