Version 1.0 (2026-09-14). Sibling of rest-api.md (HTTP), osc-spec.md (OSC) and midi-spec.md (network MIDI); the table for choosing between the four is Choosing a transport. MQTT ships in the Performance edition (EDITIONS.md); a build without it serves neither the topics below nor GET /api/mqtt. The implementation is firmware/patternflow/features/mqtt/; this file is what a client is written against.
What MQTT is for: putting the panel on the bus a home or a venue already runs (Home Assistant, Node-RED, a lighting desk), and making two panels follow each other. It reaches the knobs, the pattern, a banner message and sleep — not firmware update, pattern install or Wi-Fi, which stay on HTTP.
| Broker | Any MQTT 3.1.1 broker. Set on the panel's MQTT console page or with POST /api/mqtt (host, port, user, password, prefix, role). Passwords are stored on the panel and never reported back. |
| Prefix | One topic prefix per panel, default patternflow. The prefix is the channel (below). No wildcards are used, so an ACL can be an exact list. |
| Roles | off (socket closed) · publisher (sends knobs, pattern and, on a show channel, a snapshot heartbeat) · subscriber (applies the retained snapshot, then follows the live topics). The two are exclusive: a Publisher ignores incoming knob and pattern writes. GET /api/mqtt reports the current role and POST /api/mqtt?role=subscriber flips it. |
| Channels | Prefix patternflow is Broadcast; patternflow1 … patternflow4 are channels 1–4; patternflow5 is Live; anything else is Custom. Channels 1–4 force the Subscriber role. GET /api/mqtt reports the channel as broadcast, ch1–ch4, live, custom or off. |
All under <prefix>/. Direction is from the panel's point of view.
| Topic | Direction | Payload | Retain | Obeyed in |
|---|---|---|---|---|
knob/1 … knob/4 |
out (publisher), in (subscriber) | absolute accumulated click count, signed integer | no | subscriber |
param/1 … param/4 |
out (publisher), in (subscriber) | absolute 0–1000; empty payload releases the hold |
no | subscriber |
pattern |
out (publisher), in (subscriber) | the pattern's display name or slug | no | subscriber |
message |
in | banner text shown on the panel | broadcast: yes · channels: no | either role |
sleep |
in | 1 / on / true / sleep · 0 / off / false / wake · toggle |
no | either role |
sleep/state |
out | 0 or 1, on every change and once per connection |
no | — |
snapshot |
out (publisher, every 8 s), in (subscriber) | compact JSON of the mid-join state, including param:[a,b,c,d] |
yes | channels 1–4 and Live only |
Director-only, on a local broker and not part of the public ACL: query (panel → its module inventory), select (Director → mark modules on /patterns for ZIP export), select/ack (panel → {matched, presets, missing, rev}). Ignore them unless you are that tool.
sleep and message are the two topics a panel obeys in either role: they are "tell the panels something", not "mirror this panel", and a panel that publishes its knobs is still one you want to switch off from home automation. Publishing 1 to <prefix>/sleep puts the panel to sleep, 0 wakes it, and sleep/state mirrors the result — enough for a Home Assistant switch.
Decided per pattern by absoluteReady in its sidecar (GET /api/patterns/<slug>/sidecar — see rest-api.md).
absoluteReady: true→ publish0–1000toparam/<n>. The pattern pins that parameter to a fraction of its declared range; the value is idempotent and survives a restart of whatever is driving it. Physical encoder motion releases the hold, so a hand on the panel always wins. An empty payload releases it explicitly.absoluteReady: false(every module built before the parameter bus existed, and presets, which cannot be interrogated) → publish a new absolute click count toknob/<n>. The panel diffs it against the last value it received and injects the difference as a detent delta. This is a relative control: the pattern integrates the delta through its own step size, so the number you send is not a value the parameter will arrive at.
knob, param and pattern are obeyed only in Subscriber role; a Publisher ignores them silently.
Channels 1–4 and Live subscribe to the retained <prefix>/snapshot, whose param:[…] the firmware applies straight onto the knobs, exactly as if it had arrived on param/<n>. A Publisher on the channel re-sends one every 8 seconds, and the retained copy is redelivered on every reconnect. So on those five channels an external controller is not the only writer: its value is overwritten a moment later by whatever the snapshot last said, with every write succeeding and nothing reporting an error. The symptom is a control that will not stay where it is put.
Broadcast has no snapshot subscription. For one panel driven by one external controller — home automation, a script, a dashboard — Broadcast + Subscriber is the combination that behaves. The show channels exist for a Director driving several panels, which is a different job.
Two orderings exist. Physical is K1–K4 left to right on the front panel. Logical is the order patterns read parameters in. The MQTT indices are logical, like the OSC indices, knobs[] in a sidecar and /api/status. The mapping between the two is in rest-api.md under Knob ordering.
Home Assistant, a switch that sleeps and wakes the panel:
mqtt:
switch:
- name: Patternflow
command_topic: patternflow/sleep
state_topic: patternflow/sleep/state
payload_on: "0"
payload_off: "1"
state_on: "0"
state_off: "1"Node-RED, driving knob 2 of an absoluteReady pattern from a slider: an mqtt out node on topic patternflow/param/2 with the slider's 0–1000 value; put the panel on Broadcast in Subscriber role first.
Two panels following each other: point both at the same broker, same prefix on Live (patternflow5), one as Publisher and the other as Subscriber. Not channels 1–4: they force every panel on them to Subscriber, so neither would lead.
- 1.0 (2026-09-14) — first written contract. Lifted verbatim, with no behaviour change, from the header comment of
firmware/patternflow/features/mqtt/core_mqtt.hand the "Which topic to write" and "The channel decides" sections ofrest-api.md1.x.