Files
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

106 lines
3.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/#`.