54 lines
3.4 KiB
Markdown
54 lines
3.4 KiB
Markdown
# 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.
|