# MQTT API The program is both an MQTT **subscriber** (commands) and **publisher** (its mode, capture events, and command replies). The MQTT channel is the `MqttControlChannel` implementation of `IControlChannel` ([src/mqtt/MqttControlChannel.cpp](../src/mqtt/MqttControlChannel.cpp), Eclipse Paho C++). It is used only when MQTT is enabled; otherwise a `NullControlChannel` runs (publishes dropped and recorded). All topics are namespaced by tower name: ``` GGS/FWT//... ``` `` comes from `config.ini` (`General.tower_name`). All messages use **QoS 1**. Published messages are **retained**, except command replies. ## Connection - Broker URI is assembled from `Network.zkms_server_ip` + `Network.mqtt_port` as `tcp://:` (`NetworkConfig::brokerUri`), and logged on every connect and connect failure. Two escape hatches, so existing configs keep working: a `zkms_server_ip` containing `://` is passed through verbatim, and one already of the form `host:port` keeps that port. In both cases `mqtt_port` is ignored. Client ID = the tower name. - Auth: `mqtt_user` / `mqtt_pw` (preferring `$FGC_MQTT_USER`/`$FGC_MQTT_PW`); `clean_session = true`, keep-alive 20 s, `set_automatic_reconnect(true)`. - Connect timeout 5 s. On connection loss Paho auto-reconnects; the channel re-subscribes on reconnect. - **The program no longer exits if MQTT is unavailable** — it logs a warning and continues in degraded mode. Use `--no-mqtt` to disable MQTT entirely (null channel). ## Modes: auto, pan, manual The tower is always in one of three modes. It changes mode **only by command** - `mode ...`, sent on the `Command` topic below, typed at the console, or the dashboard's `a` / `p` / `m` keys - and reports it on `StatusCode`. | Mode | StatusCode | What the tower does | Command | |---|---|---|---| | auto | `0` | Sweeps the scan grid front to back, a picture at every waypoint, then starts over. The normal state: a tower in service starts here (`config.ini` `[Startup] mode = auto`). | `mode auto` | | pan | `1` | Watches one direction: walks left to right across a span centred on a bearing, a picture at each position, then back to the left edge. | `mode pan []` | | manual | `2` | Nothing by itself; a person drives it. The start mode unless `[Startup] mode = auto`; also where `auto` falls back if the gimbal is not homed. | `mode manual` | **Pan positions.** `mode pan 90 30` covers 75°..105°. One picture covers `pan_step_deg` of bearing (hardware.ini `[Hardware]`, 6 until the lens is measured), so the span is cut into `ceil(span / step)` equal intervals, both edges included: 75, 81, 87, 93, 99, 105 - six pictures. A span that does not divide evenly gets one more picture, never a gap (32° → seven, 5.33° apart). A span under one picture is the one heading, photographed once per loop. Pitch is optional and defaults to 0 (level); a yaw-only tower ignores it, with a warning. Pictures are taken at the same pace as auto (one per `image_interval`, counted from the last picture), and each publishes a CamEvent exactly as a sweep picture does. **Headings are true bearings and can be out of reach.** Yaw travel is under a full turn, so there is always an arc the gimbal cannot reach. Pan positions in it are skipped - not clamped to the nearest limit, which would image a direction nobody asked for and label it with the one they did. A pan with no reachable position, or a pitch outside the homed pitch travel, is refused. The reachable arc is logged after every home. **What each mode accepts.** Manual accepts every command. Auto and pan accept only what cannot move the gimbal or change a setting: - reading: `help`, `gimbal status`, `gimbal dump`, `imu status`, `imu config` - logging: `debug`, `trace` - `mode ...`, `cancel`, `exit` Anything else is refused - `ignored '': the tower is in pan mode ('mode manual' first)` - whether it came over MQTT or from the console. So a web page switches to manual before driving the gimbal, and back to auto (or pan) when done. **A refused mode change changes nothing**, StatusCode included. `mode auto` and `mode pan` are refused while the gimbal is not homed, during heading setup, or while a test or calibration runs. ## Published topics (outbound) | Topic | When | Payload | QoS / Retain | |-------|------|---------|--------------| | `GGS/FWT//StatusCode` | At startup, on every mode change, and again on every reconnect | the mode: `"0"` auto, `"1"` pan, `"2"` manual | 1 / retained | | `GGS/FWT//CamEvent` | After each image is saved | JSON object (below) | 1 / retained | | `GGS/FWT//Health` | When the CPU guard changes state or throttling starts/ends, once a minute, after a `thermal` command, and on every reconnect | JSON object (below) | 1 / retained | StatusCode says what the tower **is doing**, not what it was last asked to do: it is published by the mode change itself, whatever caused it. Being retained, a page that subscribes late still learns the mode at once. ### CamEvent payload Built by `MqttControlChannel::publishCamEvent` from a `CamEvent`: ```json { "fwt":"ExampleTower", "cam":"RGB", "hdg":1373, "pit":300, "time":1719312345678 } ``` | Field | Type | Meaning | |-------|------|---------| | `fwt` | string | Tower name (`config.ini` `tower_name`) | | `cam` | string | Camera label: `RGB`, `ACR`, or `NIR` (by camera index) | | `hdg` | int | Gimbal **true bearing** × 10 (0 = north) at capture time, from the **yaw encoder** | | `pit` | int | Gimbal pitch elevation **× 10** at capture time, from the **IMU** — elevation relative to gravity, i.e. true pointing angle (fallbacks below) | | `time` | int | Capture timestamp, Unix epoch **milliseconds** (matches the `.jxl` filename) | > The `time` value is the same Unix-ms timestamp used as the image filename, so a consumer can locate the file > for a given event: `/