# 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 <addr_hex> <len>` and `w <addr_hex> <bytes...>` 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).
