@page "/can-monitor" @rendermode InteractiveServer @implements IDisposable @using Peak.Can.Basic @inject ICanService CanService CAN Monitor

CAN Monitor

@* ── 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 ──────────────────────────────────────────────────────────── *@
@if (msgTab == "stream") {
@lock (messages) { foreach (var m in messages) { } }
Timestamp (µs) ID DLC Data Signals
@m.TimestampUs @($"0x{m.Id:X3}") @m.Dlc @(m.Data.Length > 0 ? BitConverter.ToString(m.Data).Replace("-", " ") : "–") @SignalsText(m)
} else {
@foreach (var m in GetLatestRows()) { }
ID DLC Data Signals Timestamp (µs) Count
@($"0x{m.Id:X3}") @m.Dlc @(m.Data.Length > 0 ? BitConverter.ToString(m.Data).Replace("-", " ") : "–") @SignalsText(m) @m.TimestampUs @updateCounts.GetValueOrDefault(m.Id)
}
@* ── Filters + Send (two columns) ──────────────────────────────────────────── *@
@* Filters *@
Filters @(filters.Count == 0 ? "(no filters — all messages pass)" : $"({filters.Count} active)")
@if (filters.Count > 0) { @foreach (var f in filters) { }
IDMaskDescription
@($"0x{f.MessageId:X3}") @($"0x{f.Mask:X3}") @f.Description
}
@if (filterError != null) {
@filterError
} @if (filters.Count > 0) { }
@* Send *@
Send Message
@if (sendFeedback != null) { @sendFeedback }
@* ── Bitmasks ───────────────────────────────────────────────────────────────── *@
Signal Bitmasks
@if (bitmasks.Count == 0) {

No bitmasks configured. Add one to extract named signal values from a message's data bytes.
Formula: physicalValue = ((rawUint64 & DataMask) >> RightShift) × Scale + Offset

} else { @foreach (var b in bitmasks) { }
Msg IDSignalDataMask (64-bit) ShiftScaleOffsetUnit
@($"0x{b.MessageId:X3}") @b.SignalName @($"0x{b.DataMask:X16}") @b.RightShift @b.Scale @b.Offset @b.Unit
}
@if (bitmaskError != null) {
@bitmaskError
} @if (bitmasks.Count > 0) { }
@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(); private readonly Dictionary updateCounts = new(); private string msgTab = "stream"; private const int MaxMessages = 200; private List filters = []; private List bitmasks = []; // ── Filter form ─────────────────────────────────────────────────────────── private string newFilterId = ""; private string newFilterMask = "7FF"; private string newFilterDesc = ""; private string? filterError; // ── Bitmask form ────────────────────────────────────────────────────────── private string newBmMsgId = ""; private string newBmName = ""; private string newBmMask = ""; private int newBmShift = 0; private double newBmScale = 1.0; private double newBmOffset = 0.0; private string newBmUnit = ""; private string? bitmaskError; // ── Send form ───────────────────────────────────────────────────────────── private string sendIdHex = ""; private string sendDataHex = ""; private bool sendExtended; private string? sendFeedback; private bool sendOk; // ── Lifecycle ───────────────────────────────────────────────────────────── protected override void OnInitialized() { 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) { lock (messages) { messages.Insert(0, msg); if (messages.Count > MaxMessages) messages.RemoveAt(messages.Count - 1); } lock (latestMessages) { 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() { lock (messages) messages.Clear(); lock (latestMessages) { latestMessages.Clear(); updateCounts.Clear(); } } // ── Filters ─────────────────────────────────────────────────────────────── private void AddFilter() { filterError = null; if (!TryParseHex32(newFilterId, out uint msgId)) { filterError = "Invalid ID — enter a hex value e.g. 100"; return; } if (!TryParseHex32(newFilterMask, out uint mask)) { filterError = "Invalid mask — enter a hex value e.g. 7FF"; return; } var filter = new CanFilter { MessageId = msgId, Mask = mask, Description = newFilterDesc }; CanService.AddFilter(filter); filters = CanService.Filters.ToList(); newFilterId = ""; newFilterMask = "7FF"; newFilterDesc = ""; } private void RemoveFilter(Guid id) { CanService.RemoveFilter(id); filters = CanService.Filters.ToList(); } private void ClearFilters() { CanService.ClearFilters(); filters = []; } // ── Bitmasks ────────────────────────────────────────────────────────────── private void AddBitmask() { bitmaskError = null; if (!TryParseHex32(newBmMsgId, out uint msgId)) { bitmaskError = "Invalid Msg ID"; return; } if (string.IsNullOrWhiteSpace(newBmName)) { bitmaskError = "Signal name is required"; return; } if (!TryParseHex64(newBmMask, out ulong mask)) { bitmaskError = "Invalid 64-bit mask — enter hex e.g. 00FF000000000000"; return; } var bitmask = new CanBitmask { MessageId = msgId, SignalName = newBmName.Trim(), DataMask = mask, RightShift = newBmShift, Scale = newBmScale, Offset = newBmOffset, Unit = string.IsNullOrWhiteSpace(newBmUnit) ? null : newBmUnit }; CanService.AddBitmask(bitmask); bitmasks = CanService.Bitmasks.ToList(); newBmMsgId = ""; newBmName = ""; newBmMask = ""; newBmShift = 0; newBmScale = 1.0; newBmOffset = 0.0; newBmUnit = ""; } private void RemoveBitmask(Guid id) { CanService.RemoveBitmask(id); bitmasks = CanService.Bitmasks.ToList(); } private void ClearBitmasks() { CanService.ClearBitmasks(); bitmasks = []; } // ── Send ────────────────────────────────────────────────────────────────── private void SendMessage() { sendFeedback = null; if (!TryParseHex32(sendIdHex, out uint id)) { sendFeedback = "Invalid ID"; sendOk = false; return; } byte[] data; try { var hex = (sendDataHex ?? "").Replace(" ", ""); data = hex.Length > 0 ? Convert.FromHexString(hex) : []; } catch { sendFeedback = "Invalid hex data"; sendOk = false; return; } if (data.Length > 8) { sendFeedback = "Max 8 bytes"; sendOk = false; return; } var msg = new PcanMessage { ID = id, MsgType = sendExtended ? MessageType.Extended : MessageType.Standard, DLC = (byte)data.Length, Data = new byte[8] }; data.CopyTo(msg.Data, 0); var result = CanService.Write(msg); sendOk = result == PcanStatus.OK; sendFeedback = sendOk ? "Sent!" : $"Error: {result}"; } // ── Helpers ─────────────────────────────────────────────────────────────── private List GetLatestRows() { lock (latestMessages) return latestMessages.Values.OrderBy(m => m.Id).ToList(); } private static string SignalsText(CanMessageDto m) { if (m.Signals is null || m.Signals.Count == 0) return "–"; return string.Join(", ", m.Signals.Select(kv => $"{kv.Key}={kv.Value:G4}")); } private static bool TryParseHex32(string? s, out uint value) { value = 0; if (string.IsNullOrWhiteSpace(s)) return false; var hex = s.Trim(); if (hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) hex = hex[2..]; return uint.TryParse(hex, System.Globalization.NumberStyles.HexNumber, null, out value); } private static bool TryParseHex64(string? s, out ulong value) { value = 0; if (string.IsNullOrWhiteSpace(s)) return false; var hex = s.Trim(); if (hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) hex = hex[2..]; return ulong.TryParse(hex, System.Globalization.NumberStyles.HexNumber, null, out value); } }