@page "/" @rendermode InteractiveServer @implements IDisposable @using IOModuleTestBlazor.Models @using IOModuleTestBlazor.Services @using Peak.Can.Basic @inject ICanService CanService IO Module Dashboard

IO Module Dashboard

@* ── Connection Status ──────────────────────────────────────────────────── *@
@if (CanService.IsConnected) {
● Connected @CanService.ChannelName @GetBitrateLabel(CanService.CurrentBitrate)
} else { ● Disconnected No CAN connection }
@* ── Push Button Status ─────────────────────────────────────────────────── *@
Push Button Status
@* ── Termination Control ────────────────────────────────────────────────── *@
CAN Hat Termination Control
@if (!CanService.IsConnected) {

Connect to CAN to control termination

}
@* ── CAN Messages Table ──────────────────────────────────────────────────── *@
CAN Messages
@latestMessages.Count message types
@if (latestMessages.Count == 0) {
@if (CanService.IsConnected) {

Waiting for CAN messages...

} else {

Connect to CAN to see messages

}
} else {
@foreach (var msg in latestMessages.Values.OrderBy(m => m.Id)) { }
Timestamp ID DLC Data Signals
@FormatTimestamp(msg.TimestampUs) 0x@(msg.Id.ToString("X3")) @msg.Dlc @FormatData(msg.Data) @if (msg.Signals?.Count > 0) { @foreach (var signal in msg.Signals) { @signal.Key: @signal.Value.ToString("F2") } }
}
@code { private Dictionary latestMessages = new(); private bool button1Active = false; private bool button2Active = false; private bool button3Active = false; private bool _termOn = false; private bool _termOff = false; private Timer? _updateTimer; protected override void OnInitialized() { // Poll for updates every 50ms for responsive UI (20 FPS) _updateTimer = new Timer(UpdateUI, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(50)); } private void UpdateUI(object? state) { InvokeAsync(() => { // Get latest messages from CAN service var newMessages = CanService.GetLatestMessages(); // Check if any messages have changed bool hasChanges = false; foreach (var (id, message) in newMessages) { if (!latestMessages.TryGetValue(id, out var existing) || !existing.Data.SequenceEqual(message.Data) || existing.TimestampUs != message.TimestampUs) { latestMessages[id] = message; UpdateButtonStates(message); hasChanges = true; } } // Only trigger UI update if something actually changed if (hasChanges) { StateHasChanged(); } }); } private void UpdateButtonStates(CanMessageDto message) { // STM32H7 Nucleo CAN Protocol Implementation // Based on CAN message documentation v1.2 switch (message.Id) { case 0x210: // User Button State (onboard blue button) if (message.Data.Length > 0) { // Use onboard button for Button 1 indicator button1Active = message.Data[0] > 0; } break; case 0x220: // CN9-29 Button State (Button 1 - PG0) if (message.Data.Length >= 2) { // Byte 0: Toggle state (persistent) // Byte 1: GPIO state (real-time press state) // Use GPIO state for immediate visual feedback button2Active = message.Data[1] > 0; } break; case 0x230: // CN9-30 Button State (Button 2 - PG1) if (message.Data.Length >= 2) { // Byte 0: Toggle state (persistent) // Byte 1: GPIO state (real-time press state) // Use GPIO state for immediate visual feedback button3Active = message.Data[1] > 0; } break; case 0x250: // Termination pin status — bit0=TERM_ON (PB10), bit1=TERM_OFF (PB11) if (message.Data.Length > 0) { _termOn = (message.Data[0] & 0x01) != 0; _termOff = (message.Data[0] & 0x02) != 0; } break; } // Alternative: Use toggle states instead of GPIO states for persistent indication // Uncomment these lines if you want the lights to stay on after button release: // case 0x220: // Button 1 Toggle State // if (message.Data.Length > 0) // button2Active = message.Data[0] > 0; // break; // // case 0x230: // Button 2 Toggle State // if (message.Data.Length > 0) // button3Active = message.Data[0] > 0; // break; } // ── Termination control ─────────────────────────────────────────────────── private void ToggleTermOn() { _termOn = !_termOn; SendTermination(); } private void ToggleTermOff() { _termOff = !_termOff; SendTermination(); } private void SendTermination() { var data = new byte[8]; data[0] = (byte)((_termOn ? 0x01 : 0x00) | (_termOff ? 0x02 : 0x00)); // bit0=TERM_ON, bit1=TERM_OFF var msg = new PcanMessage(0x240, MessageType.Standard, 8, data, false); CanService.Write(msg); } private void ClearMessages() { latestMessages.Clear(); StateHasChanged(); } private string FormatTimestamp(ulong timestampUs) { var dt = DateTime.Now; var ms = (timestampUs / 1000) % 1000; return $"{dt:HH:mm:ss}.{ms:D3}"; } private string FormatData(byte[] data) { return string.Join(" ", data.Select(b => b.ToString("X2"))); } private string GetBitrateLabel(Peak.Can.Basic.Bitrate bitrate) { return bitrate switch { Peak.Can.Basic.Bitrate.Pcan1000 => "1000 kbps", Peak.Can.Basic.Bitrate.Pcan800 => "800 kbps", Peak.Can.Basic.Bitrate.Pcan500 => "500 kbps", Peak.Can.Basic.Bitrate.Pcan250 => "250 kbps", Peak.Can.Basic.Bitrate.Pcan125 => "125 kbps", Peak.Can.Basic.Bitrate.Pcan100 => "100 kbps", Peak.Can.Basic.Bitrate.Pcan50 => "50 kbps", Peak.Can.Basic.Bitrate.Pcan20 => "20 kbps", Peak.Can.Basic.Bitrate.Pcan10 => "10 kbps", Peak.Can.Basic.Bitrate.Pcan5 => "5 kbps", _ => bitrate.ToString() }; } public void Dispose() { _updateTimer?.Dispose(); } }