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