From ad5fda71b09aa83d6c9767d05392c23949908ffb Mon Sep 17 00:00:00 2001 From: Twirpytherobot <32495213+Twirpytherobot@users.noreply.github.com> Date: Wed, 11 Mar 2026 21:15:50 +0000 Subject: [PATCH] fgh --- IOModuleTestBlazor/CanWorker.cs | 67 ++++--- .../Components/Pages/CanMonitor.razor | 172 ++++++++++++++++-- IOModuleTestBlazor/Services/CanService.cs | 42 ++++- IOModuleTestBlazor/Services/ICanService.cs | 15 ++ 4 files changed, 251 insertions(+), 45 deletions(-) diff --git a/IOModuleTestBlazor/CanWorker.cs b/IOModuleTestBlazor/CanWorker.cs index ab95bdf..c060532 100644 --- a/IOModuleTestBlazor/CanWorker.cs +++ b/IOModuleTestBlazor/CanWorker.cs @@ -15,27 +15,26 @@ public class CanWorker( { public override Task StartAsync(CancellationToken cancellationToken) { - var channel = ResolveChannel(); - var bitrate = ResolveBitrate(); + // 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 (InvalidOperationException ex) + catch (Exception ex) { - logger.LogError(ex, "Failed to initialize CAN channel {Channel}", channel); - throw; + logger.LogWarning(ex, "CAN auto-connect failed — configure via the UI."); } - // Seed filters and bitmasks from appsettings - foreach (var f in configuration.GetSection("CanOptions:Filters").Get() ?? []) - canService.AddFilter(f); - - foreach (var b in configuration.GetSection("CanOptions:Bitmasks").Get() ?? []) - canService.AddBitmask(b); - - logger.LogInformation("CAN channel {Channel} ready at {Bitrate}", channel, bitrate); return base.StartAsync(cancellationToken); } @@ -43,6 +42,29 @@ public class CanWorker( { 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) @@ -97,11 +119,10 @@ public class CanWorker( return configured; logger.LogInformation("No channel configured — scanning for available PCAN USB channels..."); - var available = GetAvailableUsbChannels(); + var available = canService.GetAvailableChannels(); if (available.Count == 0) - throw new InvalidOperationException( - "No PCAN USB channels detected. Set CanOptions:Channel in appsettings.json."); + throw new InvalidOperationException("No PCAN USB channels detected."); logger.LogInformation("Auto-selected channel: {Channel}", available[0]); return available[0]; @@ -118,20 +139,6 @@ public class CanWorker( return Bitrate.Pcan500; } - private static List GetAvailableUsbChannels() - { - var available = new List(); - foreach (var ch in Enum.GetValues() - .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; } diff --git a/IOModuleTestBlazor/Components/Pages/CanMonitor.razor b/IOModuleTestBlazor/Components/Pages/CanMonitor.razor index a0bd6ea..77b9ff5 100644 --- a/IOModuleTestBlazor/Components/Pages/CanMonitor.razor +++ b/IOModuleTestBlazor/Components/Pages/CanMonitor.razor @@ -10,18 +10,73 @@

CAN Monitor

-@* ── Status bar ─────────────────────────────────────────────────────────────── *@ -
- @if (CanService.IsConnected) - { - ● Connected - Channel: @CanService.ChannelName - } - else - { - ○ Not connected - Check that a PCAN USB adapter is plugged in and CanOptions are set in appsettings.json. - } +@* ── Connection ──────────────────────────────────────────────────────────────── *@ +
+
+ @if (CanService.IsConnected && !showConnectionForm) + { +
+ ● Connected + @CanService.ChannelName + @BitrateLabel(CanService.CurrentBitrate) + +
+ } + else + { +
+
+ + +
+
+ + +
+
+ + + @if (CanService.IsConnected) + { + + } +
+
+ @if (availableChannels.Count == 0) + { +
+ No PCAN USB channels detected. Plug in your adapter and click Scan. +
+ } + @if (connectError != null) + { +
@connectError
+ } + } +
@* ── Live messages ──────────────────────────────────────────────────────────── *@ @@ -294,6 +349,28 @@
@code { + // ── Connection form ─────────────────────────────────────────────────────── + private List availableChannels = []; + private string selectedChannel = ""; + private string selectedBitrate = Bitrate.Pcan500.ToString(); + private bool showConnectionForm; + private bool isConnecting; + private string? connectError; + + private static readonly (Bitrate Value, string Label)[] BitrateOptions = + [ + (Bitrate.Pcan1000, "1000 kbps"), + (Bitrate.Pcan800, "800 kbps"), + (Bitrate.Pcan500, "500 kbps"), + (Bitrate.Pcan250, "250 kbps"), + (Bitrate.Pcan125, "125 kbps"), + (Bitrate.Pcan100, "100 kbps"), + (Bitrate.Pcan50, "50 kbps"), + (Bitrate.Pcan20, "20 kbps"), + (Bitrate.Pcan10, "10 kbps"), + (Bitrate.Pcan5, "5 kbps"), + ]; + // ── Data ────────────────────────────────────────────────────────────────── private readonly List messages = []; private readonly Dictionary latestMessages = new(); @@ -333,6 +410,17 @@ filters = CanService.Filters.ToList(); bitmasks = CanService.Bitmasks.ToList(); CanService.MessageReceived += OnMessageReceived; + + ScanChannels(); + if (CanService.IsConnected) + { + selectedChannel = CanService.ChannelName; + selectedBitrate = CanService.CurrentBitrate.ToString(); + } + else + { + showConnectionForm = true; + } } private void OnMessageReceived(CanMessageDto msg) @@ -345,14 +433,70 @@ } lock (latestMessages) { - latestMessages[msg.Id] = msg; - updateCounts[msg.Id] = updateCounts.GetValueOrDefault(msg.Id) + 1; + if (!latestMessages.TryGetValue(msg.Id, out var existing) || + !msg.Data.SequenceEqual(existing.Data)) + { + latestMessages[msg.Id] = msg; + updateCounts[msg.Id] = updateCounts.GetValueOrDefault(msg.Id) + 1; + } } InvokeAsync(StateHasChanged); } public void Dispose() => CanService.MessageReceived -= OnMessageReceived; + // ── Connection ──────────────────────────────────────────────────────────── + + private void ScanChannels() + { + availableChannels = CanService.GetAvailableChannels().ToList(); + if (availableChannels.Count > 0 && + !availableChannels.Any(c => c.ToString() == selectedChannel)) + selectedChannel = availableChannels[0].ToString(); + } + + private async Task Connect() + { + connectError = null; + if (!Enum.TryParse(selectedChannel, out var channel)) + { connectError = "Please select a channel."; return; } + if (!Enum.TryParse(selectedBitrate, out var bitrate)) + { connectError = "Please select a bitrate."; return; } + + isConnecting = true; + CanService.RequestReinitialize(channel, bitrate); + + // Poll until the worker completes the reinit (typically < 50 ms) + for (int i = 0; i < 20; i++) + { + await Task.Delay(100); + if (CanService.IsConnected) break; + } + + isConnecting = false; + if (CanService.IsConnected) + showConnectionForm = false; + else + connectError = "Connection failed. Check the adapter is plugged in and try again."; + + StateHasChanged(); + } + + private static string BitrateLabel(Bitrate b) => b switch + { + Bitrate.Pcan1000 => "1000 kbps", + Bitrate.Pcan800 => "800 kbps", + Bitrate.Pcan500 => "500 kbps", + Bitrate.Pcan250 => "250 kbps", + Bitrate.Pcan125 => "125 kbps", + Bitrate.Pcan100 => "100 kbps", + Bitrate.Pcan50 => "50 kbps", + Bitrate.Pcan20 => "20 kbps", + Bitrate.Pcan10 => "10 kbps", + Bitrate.Pcan5 => "5 kbps", + _ => b.ToString() + }; + // ── Messages ────────────────────────────────────────────────────────────── private void ClearMessages() diff --git a/IOModuleTestBlazor/Services/CanService.cs b/IOModuleTestBlazor/Services/CanService.cs index 31f6c91..767931d 100644 --- a/IOModuleTestBlazor/Services/CanService.cs +++ b/IOModuleTestBlazor/Services/CanService.cs @@ -11,7 +11,10 @@ public class CanService : ICanService private readonly object _lock = new(); public bool IsConnected { get; private set; } - public string ChannelName => _channel.ToString(); + public string ChannelName => IsConnected ? _channel.ToString() : ""; + public Bitrate CurrentBitrate { get; private set; } + + private (PcanChannel Channel, Bitrate Bitrate)? _pendingReinit; public event Action? MessageReceived; @@ -103,6 +106,42 @@ public class CanService : ICanService // ── PCAN passthrough ────────────────────────────────────────────────────── + public IReadOnlyList GetAvailableChannels() + { + var available = new List(); + foreach (var ch in Enum.GetValues() + .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; + } + + public void RequestReinitialize(PcanChannel channel, Bitrate bitrate) + { + lock (_lock) _pendingReinit = (channel, bitrate); + } + + public bool TryConsumePendingReinit(out PcanChannel channel, out Bitrate bitrate) + { + lock (_lock) + { + if (_pendingReinit.HasValue) + { + channel = _pendingReinit.Value.Channel; + bitrate = _pendingReinit.Value.Bitrate; + _pendingReinit = null; + return true; + } + channel = default; + bitrate = default; + return false; + } + } + public void Initialize(PcanChannel channel, Bitrate bitrate) { _channel = channel; @@ -110,6 +149,7 @@ public class CanService : ICanService if (result != PcanStatus.OK) throw new InvalidOperationException($"CAN init failed: {GetErrorText(result)}"); IsConnected = true; + CurrentBitrate = bitrate; } public void Uninitialize() diff --git a/IOModuleTestBlazor/Services/ICanService.cs b/IOModuleTestBlazor/Services/ICanService.cs index c7a5725..6a1bf1d 100644 --- a/IOModuleTestBlazor/Services/ICanService.cs +++ b/IOModuleTestBlazor/Services/ICanService.cs @@ -7,6 +7,7 @@ public interface ICanService { bool IsConnected { get; } string ChannelName { get; } + Bitrate CurrentBitrate { get; } /// /// Raised on the worker thread each time a CAN message passes all filters. @@ -31,6 +32,20 @@ public interface ICanService IReadOnlyDictionary ExtractSignals(uint messageId, byte[] data); void PublishMessage(CanMessageDto dto); + // ── Channel management ──────────────────────────────────────────────────── + + /// Returns all PCAN USB channels that are currently available. + IReadOnlyList GetAvailableChannels(); + + /// + /// Asks the worker to reinitialise on the next loop iteration. + /// Non-blocking — the actual reconnect happens asynchronously. + /// + void RequestReinitialize(PcanChannel channel, Bitrate bitrate); + + /// Called by the worker to pick up a pending reinit request. + bool TryConsumePendingReinit(out PcanChannel channel, out Bitrate bitrate); + // ── PCAN passthrough ────────────────────────────────────────────────────── void Initialize(PcanChannel channel, Bitrate bitrate); void Uninitialize();