Files
findr-api/docs/mqtt.md
T
mars3142 cadc280761 Scaffold Fastify + Drizzle API with pick-by-light
Layered: routes -> services -> repositories -> db, over PostgreSQL 18 with
split roles (findr_migrator for DDL, findr_app for DML). Keycloak JWT auth
with per-endpoint scope guards. Drizzle migrations run with the DDL role at
container start.

Pick-by-light: one ESP32 controller drives one WS2812 chain through several
boxes; each box owns a contiguous LED slice. MQTT contract in
src/lib/mqtt-topics.ts / docs/mqtt.md. /v1/pick controls light/idle/off and
/v1/boxes + /v1/controllers manage the strip mapping.

Reference resource /v1/parts wired end to end. Vitest, Biome, multi-stage
Dockerfile, local compose.yml with Postgres.

Signed-off-by: Peter Siegmund <mars3142@noreply.mars3142.dev>
2026-09-03 01:07:35 +02:00

3.7 KiB
Raw Blame History

MQTT contract — pick-by-light

Broker: Mosquitto 2.1 on mars3142-01 (mosquitto:1883 internal, mqtts://mqtt.mars3142.dev:8883 public). allow_anonymous false, password + ACL file.

TypeScript source of truth: src/lib/mqtt-topics.ts.

Model

  • Controller — one ESP32-S3/C6 driving a single WS2812 chain. Identified by espId. Can serve several boxes.
  • Box — a sortiment box. Owns a contiguous slice of its controller's chain: [ledOffset, ledOffset + ledCount). Has a grid (columns × rows, wiring).
  • Cell — a compartment ("D4"). Its LED = ledOffset + withinBoxIndex, where withinBoxIndex is locations.ledIndex if set, else derived from the grid.

findr-api owns all topology. It publishes each box's config (retained) and resolves cells → LED indices, so the firmware only needs its own espId and chain length, then paints chain[ledOffset + ledIndex].

Effects

effect box's slice
idle rainbow animation (resting state)
pick only the listed bins lit (take=orange, put=green)
off dark

After a pick with seconds, the box returns to idle on its own.

Topics

<n> = box number, <esp> = controller espId, prefix defaults to findr.

Topic Dir Retain Payload
findr/box/<n>/config api → fw yes BoxConfig
findr/box/<n>/cmd/light api → fw no LightCommand
findr/box/<n>/state/light fw → api yes LightState
findr/box/<n>/evt/button fw → api no ButtonEvent
findr/controller/<esp>/state/online fw → * yes online/offline (LWT)
findr/controller/<esp>/state/info fw → api yes ControllerInfo
findr/controller/<esp>/cmd/identify api → fw no { seconds }

BoxConfig (retained)

{ "boxNumber": 2, "controllerEspId": "a1b2c3", "ledOffset": 40,
  "ledCount": 40, "columns": 8, "rows": 5, "wiring": "serpentine" }

The firmware keeps configs whose controllerEspId matches its own espId.

LightCommand

{ "effect": "pick",
  "bins": [ { "cell": "D4", "ledIndex": 27, "mode": "take" } ],
  "seconds": 30, "requestId": "uuid" }

{ "effect": "idle" } / { "effect": "off" } carry no bins.

LightState (retained)

{ "effect": "pick", "lit": [ { "cell": "D4", "mode": "take" } ],
  "requestId": "uuid", "ts": "2026-09-02T21:15:00Z" }

Broker users & ACL

Two users. Add to /opt/docker/mosquitto/config/passwd:

docker exec mosquitto mosquitto_passwd -b /mosquitto/config/passwd findr-api        '<api-pw>'
docker exec mosquitto mosquitto_passwd -b /mosquitto/config/passwd findr-controller '<ctrl-pw>'

Append to /opt/docker/mosquitto/config/acl.txt:

user findr-api
topic write findr/box/+/config
topic write findr/box/+/cmd/#
topic write findr/controller/+/cmd/#
topic read  findr/box/+/state/#
topic read  findr/box/+/evt/#
topic read  findr/controller/+/state/#

user findr-controller
topic read  findr/box/+/config
topic read  findr/box/+/cmd/#
topic read  findr/controller/+/cmd/#
topic write findr/box/+/state/#
topic write findr/box/+/evt/#
topic write findr/controller/+/state/#

Reload without dropping connections: docker kill -s HUP mosquitto.

findr-controller is shared by every ESP32 for now. Once box provisioning is in place, switch to per-controller users and scope with %u, e.g. pattern write findr/controller/%u/state/#.