# Frame Processor

Self-hosted ASP.NET Core (.NET 10) service that dithers and color-remaps images for Spectra 6 e-ink frames, persists the result to disk, and notifies the frame via MQTT.

See `SPEC.md` for the authoritative behavior contract and `PLAN.md` for the implementation design. Update both if requirements change.

## Status

Greenfield — only `SPEC.md` and `PLAN.md` exist so far. No source, no tests, no Dockerfile yet. The layout in `PLAN.md` §"Project layout" is the target, not the current state.

## Stack

- .NET 10, ASP.NET Core (minimal hosting + controllers)
- SixLabors.ImageSharp — decode, resize, dither, indexed-PNG encode
- MQTTnet v4 — persistent client in an `IHostedService`
- Serilog — console + rolling file
- xUnit — golden-image fixture tests
- Docker + docker-compose for deployment

## Commands

Once scaffolded:

- `dotnet build` / `dotnet test` — from repo root
- `docker compose up --build` — full stack including a `mosquitto` broker
- `dotnet run --project src/FrameProcessor` — local dev (set `Mqtt__Host=localhost` if broker is on host)

Manual smoke:

- `curl -H "X-Api-Key: ..." -F "image=@photo.jpg" http://localhost:8080/api/frames/living-room/image`
- `mosquitto_sub -t 'frames/#' -v` to watch publishes

## Load-bearing conventions

These are easy to get subtly wrong — re-read `SPEC.md` §3.2 if in doubt.

- **Dither, then remap.** Dither against the *display* palette (the `color` field — what the eye sees on the panel), then map each pixel to its `deviceColor` (what firmware expects in the input PNG). Reversing this order produces wrong output.
- **MAC normalization at every boundary.** Store, match, and publish using lowercase hex with no separators (`aabbccddeeff`). Normalize on config load, on URL path entry, and before building MQTT topics.
- **Atomic writes only.** Write to `{mac}.png.tmp`, then `File.Move(..., overwrite: true)`. The GET endpoint must never observe a partial file.
- **MQTT failure is non-fatal to the request.** A publish failure does not fail the upload — the PNG is still saved, the response is still `200`, and the response body's `mqttPublished: false` tells the caller. Retry happens in a background loop.
- **Per-frame serialization.** A single `SemaphoreSlim(1,1)` per frame name wraps the entire pipeline (fetch/decode → process → write → publish-attempt) so the latest PNG on disk always corresponds to the latest publish attempt. Different frames are independent.
- **`frames.json` reload asymmetry.** Invalid frame at *startup* → fail fast with a clear error. Invalid frame on *hot-reload* → log a warning and skip that one frame; keep serving the others. A typo must not take the service down.
- **Indexed PNG output.** `PngColorType.Palette` with the smallest bit depth that fits the palette (4-bit for ≤16 colors). Pass `PaletteQuantizer(deviceColorPalette)` to `PngEncoder`.

## Testing approach

Pipeline correctness is the load-bearing part — verify it with golden-image fixtures (`tests/FrameProcessor.Tests/ImagePipelineTests.cs`). ImageSharp dither output is deterministic for a fixed input/palette/algorithm, so byte-equality assertions are reliable.

No controller/integration tests in v1 — HTTP plumbing is thin enough that `curl` smoke-tests are sufficient.

## Out of scope (don't add without discussion)

See `SPEC.md` §12. Notably: no auth beyond the shared API key, no SSRF protection on URL fetch, no per-frame history, no web UI, no HTTPS termination in-process.
