using Peak.Can.Basic; using IOModuleTestBlazor.Models; using IOModuleTestBlazor.Services; namespace IOModuleTestBlazor; /// /// Background service that initialises the PCAN adapter and stores CAN messages /// in the CanService for polling by UI components. /// public class CanWorker( ILogger 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() ?? []) canService.AddFilter(f); foreach (var b in configuration.GetSection("CanOptions:Bitmasks").Get() ?? []) 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; } // Process multiple messages in batch to drain buffer faster and reduce latency int messagesProcessed = 0; const int maxBatchSize = 10; // Process up to 10 messages per loop iteration while (messagesProcessed < maxBatchSize) { var result = canService.Read(out PcanMessage msg, out ulong timestamp); if (result == PcanStatus.OK) { if (!canService.PassesFilter(msg.ID)) { logger.LogDebug("[FILTERED] ID=0x{Id:X3} blocked by filter", msg.ID); messagesProcessed++; 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); var messageDto = new CanMessageDto( Id: msg.ID, Data: data, Dlc: msg.DLC, TimestampUs: timestamp, IsExtended: msg.MsgType == MessageType.Extended, Signals: signals.Count > 0 ? signals : null ); canService.UpdateLatestMessage(messageDto); logger.LogDebug("[RX] ID=0x{Id:X3} DLC={Dlc} Data={Data} Published to UI", msg.ID, msg.DLC, Convert.ToHexString(data)); messagesProcessed++; } else if (result == PcanStatus.ReceiveQueueEmpty) { // No more messages in buffer, exit batch processing break; } else { logger.LogError("CAN read error: {Error}", GetErrorText(result)); await Task.Delay(100, stoppingToken); break; } } // Small yield to prevent 100% CPU usage when no messages if (messagesProcessed == 0) { await Task.Yield(); } } } 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(); } } }