fgh
This commit is contained in:
@@ -15,27 +15,26 @@ public class CanWorker(
|
|||||||
{
|
{
|
||||||
public override Task StartAsync(CancellationToken cancellationToken)
|
public override Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var channel = ResolveChannel();
|
// Seed filters and bitmasks from appsettings regardless of connection state
|
||||||
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[]>() ?? [])
|
foreach (var f in configuration.GetSection("CanOptions:Filters").Get<CanFilter[]>() ?? [])
|
||||||
canService.AddFilter(f);
|
canService.AddFilter(f);
|
||||||
|
|
||||||
foreach (var b in configuration.GetSection("CanOptions:Bitmasks").Get<CanBitmask[]>() ?? [])
|
foreach (var b in configuration.GetSection("CanOptions:Bitmasks").Get<CanBitmask[]>() ?? [])
|
||||||
canService.AddBitmask(b);
|
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);
|
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);
|
return base.StartAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +42,29 @@ public class CanWorker(
|
|||||||
{
|
{
|
||||||
while (!stoppingToken.IsCancellationRequested)
|
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);
|
var result = canService.Read(out PcanMessage msg, out ulong timestamp);
|
||||||
|
|
||||||
if (result == PcanStatus.OK)
|
if (result == PcanStatus.OK)
|
||||||
@@ -97,11 +119,10 @@ public class CanWorker(
|
|||||||
return configured;
|
return configured;
|
||||||
|
|
||||||
logger.LogInformation("No channel configured — scanning for available PCAN USB channels...");
|
logger.LogInformation("No channel configured — scanning for available PCAN USB channels...");
|
||||||
var available = GetAvailableUsbChannels();
|
var available = canService.GetAvailableChannels();
|
||||||
|
|
||||||
if (available.Count == 0)
|
if (available.Count == 0)
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException("No PCAN USB channels detected.");
|
||||||
"No PCAN USB channels detected. Set CanOptions:Channel in appsettings.json.");
|
|
||||||
|
|
||||||
logger.LogInformation("Auto-selected channel: {Channel}", available[0]);
|
logger.LogInformation("Auto-selected channel: {Channel}", available[0]);
|
||||||
return available[0];
|
return available[0];
|
||||||
@@ -118,20 +139,6 @@ public class CanWorker(
|
|||||||
return Bitrate.Pcan500;
|
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)
|
private static string GetErrorText(PcanStatus status)
|
||||||
{
|
{
|
||||||
try { Api.GetErrorText(status, out var text); return text; }
|
try { Api.GetErrorText(status, out var text); return text; }
|
||||||
|
|||||||
@@ -10,18 +10,73 @@
|
|||||||
|
|
||||||
<h1>CAN Monitor</h1>
|
<h1>CAN Monitor</h1>
|
||||||
|
|
||||||
@* ── Status bar ─────────────────────────────────────────────────────────────── *@
|
@* ── Connection ──────────────────────────────────────────────────────────────── *@
|
||||||
<div class="d-flex align-items-center gap-3 mb-3">
|
<div class="card mb-3">
|
||||||
@if (CanService.IsConnected)
|
<div class="card-body py-2">
|
||||||
|
@if (CanService.IsConnected && !showConnectionForm)
|
||||||
{
|
{
|
||||||
<span class="badge bg-success fs-6">● Connected</span>
|
<div class="d-flex align-items-center gap-3">
|
||||||
<span class="text-muted">Channel: <strong>@CanService.ChannelName</strong></span>
|
<span class="badge bg-success">● Connected</span>
|
||||||
|
<span class="font-monospace fw-semibold">@CanService.ChannelName</span>
|
||||||
|
<span class="text-muted">@BitrateLabel(CanService.CurrentBitrate)</span>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary ms-auto"
|
||||||
|
@onclick="() => { showConnectionForm = true; ScanChannels(); }">
|
||||||
|
Change
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="badge bg-danger fs-6">○ Not connected</span>
|
<div class="row g-2 align-items-end">
|
||||||
<span class="text-muted small">Check that a PCAN USB adapter is plugged in and CanOptions are set in appsettings.json.</span>
|
<div class="col-auto">
|
||||||
|
<label class="form-label small mb-0">Channel</label>
|
||||||
|
<select class="form-select form-select-sm font-monospace" @bind="selectedChannel"
|
||||||
|
style="min-width:100px">
|
||||||
|
@if (availableChannels.Count == 0)
|
||||||
|
{
|
||||||
|
<option value="">— none found —</option>
|
||||||
}
|
}
|
||||||
|
@foreach (var ch in availableChannels)
|
||||||
|
{
|
||||||
|
<option value="@ch.ToString()">@ch.ToString()</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<label class="form-label small mb-0">Bitrate</label>
|
||||||
|
<select class="form-select form-select-sm" @bind="selectedBitrate"
|
||||||
|
style="min-width:120px">
|
||||||
|
@foreach (var (val, label) in BitrateOptions)
|
||||||
|
{
|
||||||
|
<option value="@val.ToString()">@label</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto d-flex gap-2">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" @onclick="ScanChannels">Scan</button>
|
||||||
|
<button class="btn btn-sm btn-primary" @onclick="Connect"
|
||||||
|
disabled="@(availableChannels.Count == 0 || isConnecting)">
|
||||||
|
@(isConnecting ? "Connecting…" : CanService.IsConnected ? "Reconnect" : "Connect")
|
||||||
|
</button>
|
||||||
|
@if (CanService.IsConnected)
|
||||||
|
{
|
||||||
|
<button class="btn btn-sm btn-outline-secondary"
|
||||||
|
@onclick="() => showConnectionForm = false">Cancel</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if (availableChannels.Count == 0)
|
||||||
|
{
|
||||||
|
<div class="text-warning small mt-1">
|
||||||
|
No PCAN USB channels detected. Plug in your adapter and click Scan.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (connectError != null)
|
||||||
|
{
|
||||||
|
<div class="text-danger small mt-1">@connectError</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@* ── Live messages ──────────────────────────────────────────────────────────── *@
|
@* ── Live messages ──────────────────────────────────────────────────────────── *@
|
||||||
@@ -294,6 +349,28 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
// ── Connection form ───────────────────────────────────────────────────────
|
||||||
|
private List<PcanChannel> 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 ──────────────────────────────────────────────────────────────────
|
// ── Data ──────────────────────────────────────────────────────────────────
|
||||||
private readonly List<CanMessageDto> messages = [];
|
private readonly List<CanMessageDto> messages = [];
|
||||||
private readonly Dictionary<uint, CanMessageDto> latestMessages = new();
|
private readonly Dictionary<uint, CanMessageDto> latestMessages = new();
|
||||||
@@ -333,6 +410,17 @@
|
|||||||
filters = CanService.Filters.ToList();
|
filters = CanService.Filters.ToList();
|
||||||
bitmasks = CanService.Bitmasks.ToList();
|
bitmasks = CanService.Bitmasks.ToList();
|
||||||
CanService.MessageReceived += OnMessageReceived;
|
CanService.MessageReceived += OnMessageReceived;
|
||||||
|
|
||||||
|
ScanChannels();
|
||||||
|
if (CanService.IsConnected)
|
||||||
|
{
|
||||||
|
selectedChannel = CanService.ChannelName;
|
||||||
|
selectedBitrate = CanService.CurrentBitrate.ToString();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
showConnectionForm = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnMessageReceived(CanMessageDto msg)
|
private void OnMessageReceived(CanMessageDto msg)
|
||||||
@@ -344,15 +432,71 @@
|
|||||||
messages.RemoveAt(messages.Count - 1);
|
messages.RemoveAt(messages.Count - 1);
|
||||||
}
|
}
|
||||||
lock (latestMessages)
|
lock (latestMessages)
|
||||||
|
{
|
||||||
|
if (!latestMessages.TryGetValue(msg.Id, out var existing) ||
|
||||||
|
!msg.Data.SequenceEqual(existing.Data))
|
||||||
{
|
{
|
||||||
latestMessages[msg.Id] = msg;
|
latestMessages[msg.Id] = msg;
|
||||||
updateCounts[msg.Id] = updateCounts.GetValueOrDefault(msg.Id) + 1;
|
updateCounts[msg.Id] = updateCounts.GetValueOrDefault(msg.Id) + 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
InvokeAsync(StateHasChanged);
|
InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() => CanService.MessageReceived -= OnMessageReceived;
|
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<PcanChannel>(selectedChannel, out var channel))
|
||||||
|
{ connectError = "Please select a channel."; return; }
|
||||||
|
if (!Enum.TryParse<Bitrate>(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 ──────────────────────────────────────────────────────────────
|
// ── Messages ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private void ClearMessages()
|
private void ClearMessages()
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ public class CanService : ICanService
|
|||||||
private readonly object _lock = new();
|
private readonly object _lock = new();
|
||||||
|
|
||||||
public bool IsConnected { get; private set; }
|
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<CanMessageDto>? MessageReceived;
|
public event Action<CanMessageDto>? MessageReceived;
|
||||||
|
|
||||||
@@ -103,6 +106,42 @@ public class CanService : ICanService
|
|||||||
|
|
||||||
// ── PCAN passthrough ──────────────────────────────────────────────────────
|
// ── PCAN passthrough ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public IReadOnlyList<PcanChannel> GetAvailableChannels()
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
public void Initialize(PcanChannel channel, Bitrate bitrate)
|
||||||
{
|
{
|
||||||
_channel = channel;
|
_channel = channel;
|
||||||
@@ -110,6 +149,7 @@ public class CanService : ICanService
|
|||||||
if (result != PcanStatus.OK)
|
if (result != PcanStatus.OK)
|
||||||
throw new InvalidOperationException($"CAN init failed: {GetErrorText(result)}");
|
throw new InvalidOperationException($"CAN init failed: {GetErrorText(result)}");
|
||||||
IsConnected = true;
|
IsConnected = true;
|
||||||
|
CurrentBitrate = bitrate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Uninitialize()
|
public void Uninitialize()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ public interface ICanService
|
|||||||
{
|
{
|
||||||
bool IsConnected { get; }
|
bool IsConnected { get; }
|
||||||
string ChannelName { get; }
|
string ChannelName { get; }
|
||||||
|
Bitrate CurrentBitrate { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Raised on the worker thread each time a CAN message passes all filters.
|
/// Raised on the worker thread each time a CAN message passes all filters.
|
||||||
@@ -31,6 +32,20 @@ public interface ICanService
|
|||||||
IReadOnlyDictionary<string, double> ExtractSignals(uint messageId, byte[] data);
|
IReadOnlyDictionary<string, double> ExtractSignals(uint messageId, byte[] data);
|
||||||
void PublishMessage(CanMessageDto dto);
|
void PublishMessage(CanMessageDto dto);
|
||||||
|
|
||||||
|
// ── Channel management ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Returns all PCAN USB channels that are currently available.</summary>
|
||||||
|
IReadOnlyList<PcanChannel> GetAvailableChannels();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asks the worker to reinitialise on the next loop iteration.
|
||||||
|
/// Non-blocking — the actual reconnect happens asynchronously.
|
||||||
|
/// </summary>
|
||||||
|
void RequestReinitialize(PcanChannel channel, Bitrate bitrate);
|
||||||
|
|
||||||
|
/// <summary>Called by the worker to pick up a pending reinit request.</summary>
|
||||||
|
bool TryConsumePendingReinit(out PcanChannel channel, out Bitrate bitrate);
|
||||||
|
|
||||||
// ── PCAN passthrough ──────────────────────────────────────────────────────
|
// ── PCAN passthrough ──────────────────────────────────────────────────────
|
||||||
void Initialize(PcanChannel channel, Bitrate bitrate);
|
void Initialize(PcanChannel channel, Bitrate bitrate);
|
||||||
void Uninitialize();
|
void Uninitialize();
|
||||||
|
|||||||
Reference in New Issue
Block a user