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

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. }
@* ── 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 { // ── 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; } private void OnMessageReceived(CanMessageDto msg) { lock (messages) { messages.Insert(0, msg); if (messages.Count > MaxMessages) messages.RemoveAt(messages.Count - 1); } lock (latestMessages) { latestMessages[msg.Id] = msg; updateCounts[msg.Id] = updateCounts.GetValueOrDefault(msg.Id) + 1; } InvokeAsync(StateHasChanged); } public void Dispose() => CanService.MessageReceived -= OnMessageReceived; // ── 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); } }