# CLAUDE.md — IO Module Blazor Dashboard ## Project Overview Blazor Server app (.NET 10) for monitoring and controlling the STM32H723ZG Nucleo IO module. Communicates over CAN (via PCAN-USB adapter) and serial (via ST-Link VCP). ## Architecture ``` PCAN Hardware ──► CanWorker (IHostedService) ──► CanService (singleton) ──► Razor pages ST-Link VCP ──► SerialPortService (singleton, event-driven) ──► SerialTerminal.razor ``` - **Blazor Server** — all C# runs server-side; browser connects via WebSocket/SignalR - **No background worker for serial** — `System.IO.Ports.SerialPort` fires `DataReceived` events internally; `SerialPortService` handles buffering directly ## Key Files | File | Purpose | |------|---------| | `Services/ICanService.cs` / `CanService.cs` | PCAN adapter: send, receive, filter, signal extraction | | `Services/ISerialPortService.cs` / `SerialPortService.cs` | Serial port: open/close, send lines, receive buffer | | `CanWorker.cs` | `IHostedService` — continuous CAN RX loop, loads config from `appsettings.json` | | `Models/CanModels.cs` | `CanMessageDto`, `CanFilter`, `CanBitmask` | | `Components/Pages/Home.razor` | Button status + termination toggles + CAN message list | | `Components/Pages/CanMonitor.razor` | Full CAN UI: connect, stream, filters, bitmasks, send | | `Components/Pages/SerialTerminal.razor` | Serial terminal for NVMEM commands | ## CAN Sending Pattern ```csharp // PcanMessage.Data is read-only — pass data via the constructor, NOT object initializer + CopyTo var data = new byte[8]; data[0] = payload; var msg = new PcanMessage(0x240, MessageType.Standard, 8, data, false); var result = CanService.Write(msg); // returns PcanStatus.OK on success ``` ## UI State Update Pattern Pages poll `CanService.GetLatestMessages()` via a `Timer` (50ms on Home, 100ms on CanMonitor): ```csharp _updateTimer = new Timer(_ => InvokeAsync(() => { /* update state */ StateHasChanged(); }), null, 0, 50); ``` Serial terminal subscribes to `ISerialPortService.DataReceived` event instead of polling. ## CAN Message Map | ID | Direction | Handler | |----|-----------|---------| | 0x210 | RX | User button → `button1Active` in Home.razor | | 0x220 | RX | CN9-29 button → `button2Active` | | 0x230 | RX | CN9-30 button → `button3Active` | | 0x240 | TX | Termination control — sent from `Home.razor:SendTermination()` | | 0x250 | RX | Termination pin status — bit0=TERM_ON, bit1=TERM_OFF; syncs `_termOn`/`_termOff` in Home.razor | | 0x241 | TX | NVMEM write — sent from SerialTerminal via CAN (if needed) | | 0x242 | TX | NVMEM read request | | 0x243 | RX | NVMEM read response | ## CAN Config (`appsettings.json`) ```json "CanOptions": { "Channel": "", // empty = auto-scan PCAN USB "Bitrate": "Pcan500", "Filters": [], // CanFilter objects "Bitmasks": [] // CanBitmask objects for signal extraction } ``` ## Serial Port Notes - `SerialPortService` is a singleton — only one port open at a time - Buffers last 200 lines; sent lines prefixed `> `, received prefixed `< ` - NVMEM commands: `r ` and `w ` at 115200 baud - Full command reference: `../STM32Nucleo/docs/NVMEMCommands.md` ## Build / Run ``` dotnet run --project IOModuleTestBlazor ``` Requires PCAN-Basic drivers installed. Serial port access requires the app to run with permission to open COM ports (no special config needed on Windows).