# MQTT API The program is both an MQTT **subscriber** (remote control) and **publisher** (status + capture events). 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, auto-sweep mode). 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). ## Subscribed topics (inbound — remote control) Subscribed on (re)connect; handled in `MqttControlChannel::message_arrived()` ([src/mqtt/MqttControlChannel.cpp](../src/mqtt/MqttControlChannel.cpp)). | Topic | Payload | Parsed as | Effect | |-------|---------|-----------|--------| | `GGS/FWT//target_HDG` | integer bearing as string, e.g. `"137"` | validated with `stoi`, stored as **string** | Sets the yaw target used in ControlCode 1 (a **true bearing**, 0 = north, converted to encoder counts via `[Motor]`) | | `GGS/FWT//ControlCode` | integer as string, `"0"` or `"1"` | `stoi` → int | Selects the capture mode (see below) | Invalid (non-integer) payloads are caught and logged; the previous value is kept. The main loop consumes these via `get_sub_data()`, which returns a snapshot and **clears the "available" flags** so each update is acted on once (`MqttControlChannel::poll()`). ### ControlCode semantics | ControlCode | Behaviour ([CaptureScheduler](../src/core/CaptureScheduler.cpp)) | |-------------|---------------------------------------------------| | `0` | **Automatic sweep.** Walk the scan grid front to back, then start over: `MOVE` to the next `(yaw,pitch)` waypoint, wait for standstill, trigger cameras. | | `1` | **Directed.** `MOVE` yaw to `target_HDG` (pitch held), wait for standstill, trigger. | > **`target_HDG` is a true bearing and can be refused.** Yaw travel is under a full turn, so there is > always an unreachable arc. A bearing falling in it is logged once and the gimbal **holds position**; > it is not clamped to the nearest limit, which would image a direction up to 180° away and publish it > under the bearing that was asked for. The reachable arc is logged after every home. When a ControlCode message arrives, the program echoes the current code back on the StatusCode topic. ## Published topics (outbound) | Topic | When | Payload | QoS / Retain | |-------|------|---------|--------------| | `GGS/FWT//StatusCode` | At startup (`"0"`); whenever a ControlCode message is received (echoes the code) | integer as string | 1 / retained | | `GGS/FWT//CamEvent` | After each image is saved | JSON object (below) | 1 / retained | ### 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 | | `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: `/