|
|
|
@ -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"> |
|
|
|
|
|
|
|
<div class="card-body py-2"> |
|
|
|
|
|
|
|
@if (CanService.IsConnected && !showConnectionForm) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
<div class="d-flex align-items-center gap-3"> |
|
|
|
|
|
|
|
<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 |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
<div class="row g-2 align-items-end"> |
|
|
|
|
|
|
|
<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) |
|
|
|
@if (CanService.IsConnected) |
|
|
|
{ |
|
|
|
{ |
|
|
|
<span class="badge bg-success fs-6">● Connected</span> |
|
|
|
<button class="btn btn-sm btn-outline-secondary" |
|
|
|
<span class="text-muted">Channel: <strong>@CanService.ChannelName</strong></span> |
|
|
|
@onclick="() => showConnectionForm = false">Cancel</button> |
|
|
|
} |
|
|
|
} |
|
|
|
else |
|
|
|
</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) |
|
|
|
{ |
|
|
|
{ |
|
|
|
<span class="badge bg-danger fs-6">○ Not connected</span> |
|
|
|
<div class="text-danger small mt-1">@connectError</div> |
|
|
|
<span class="text-muted small">Check that a PCAN USB adapter is plugged in and CanOptions are set in appsettings.json.</span> |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
</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() |
|
|
|
|