# 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`](../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 `` = box number, `` = controller espId, prefix defaults to `findr`. | Topic | Dir | Retain | Payload | | ------------------------------------ | -------- | ------ | --------------- | | `findr/box//config` | api → fw | **yes**| `BoxConfig` | | `findr/box//cmd/light` | api → fw | no | `LightCommand` | | `findr/box//state/light` | fw → api | **yes**| `LightState` | | `findr/box//evt/button` | fw → api | no | `ButtonEvent` | | `findr/controller//state/online`| fw → \* | **yes**| `online`/`offline` (LWT) | | `findr/controller//state/info` | fw → api | **yes**| `ControllerInfo`| | `findr/controller//cmd/identify`| api → fw | no | `{ seconds }` | ### `BoxConfig` (retained) ```jsonc { "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` ```jsonc { "effect": "pick", "bins": [ { "cell": "D4", "ledIndex": 27, "mode": "take" } ], "seconds": 30, "requestId": "uuid" } ``` `{ "effect": "idle" }` / `{ "effect": "off" }` carry no bins. ### `LightState` (retained) ```jsonc { "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`: ```sh docker exec mosquitto mosquitto_passwd -b /mosquitto/config/passwd findr-api '' docker exec mosquitto mosquitto_passwd -b /mosquitto/config/passwd findr-controller '' ``` 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/#`.