Files
frame-processor/CLAUDE.md
2026-06-07 13:32:14 +02:00

3.4 KiB

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.