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>
106 lines
3.7 KiB
Markdown
106 lines
3.7 KiB
Markdown
# 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
|
||
|
||
`<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)
|
||
|
||
```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 '<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/#`.
|