140 lines
6.9 KiB
Markdown
140 lines
6.9 KiB
Markdown
# 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/<tower_name>/...
|
||
```
|
||
|
||
`<tower_name>` comes from `config.ini` (`General.tower_name`). All messages use **QoS 1**. Published messages
|
||
are **retained**.
|
||
|
||
## Connection
|
||
|
||
- Broker URI is assembled from `Network.zkms_server_ip` + `Network.mqtt_port` as `tcp://<host>:<port>`
|
||
(`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/<tower>/target_HDG` | integer heading as string, e.g. `"137"` | validated with `stoi`, stored as **string** | Sets the yaw target used in ControlCode 1 (converted to encoder counts via `[Motor]`) |
|
||
| `GGS/FWT/<tower>/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. |
|
||
|
||
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/<tower>/StatusCode` | At startup (`"0"`); whenever a ControlCode message is received (echoes the code) | integer as string | 1 / retained |
|
||
| `GGS/FWT/<tower>/CamEvent` | After each image is saved | JSON object (below) | 1 / retained |
|
||
| `GGS/FWT/<tower>/Env` | Once per fresh ambient sensor sample (see `[Env] period_ms`) | 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 yaw heading **× 10** (one decimal place encoded as integer) 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: `<RGB|ACR|NIR>/<time>.jxl`.
|
||
|
||
**`pit` is measured against gravity, `hdg` against the encoder.** The two axes come from
|
||
different sources deliberately. The pitch encoder only measures the axis relative to the
|
||
gimbal's own base, so a leaning mast is invisible to it; the IMU measures against gravity
|
||
and therefore reports the elevation the imagery is actually pointing at. Yaw stays on the
|
||
encoder because IMU yaw drifts and depends on the heading reset and XKF profile, whereas
|
||
encoder yaw is calibrated against the IMU by `gimbal calib` and then holds.
|
||
|
||
Two consequences for consumers:
|
||
|
||
- **`pit` no longer agrees with the commanded waypoint by construction.** Commanding `+10.0`
|
||
and receiving `pit: 104` is correct — the residual is real tilt, not error. Before this
|
||
change both came off the same encoder scale and agreed trivially.
|
||
- **Archives spanning the change contain two different measurements** in this field: an axis
|
||
angle before it, a true elevation after. If the mast is level the two agree; if it leans,
|
||
they differ by the lean.
|
||
|
||
If the IMU is disabled (`[Features] enable_imu = false`), `pit` falls back to the encoder as
|
||
before. If the IMU is enabled but has no fresh sample, it also falls back — and logs a
|
||
warning saying so, once per outage, since that mixes the two measurements in one field.
|
||
|
||
Both `hdg` and `pit` are sampled **when the frame arrives**, not when its JPEG XL encode
|
||
finishes, so a slow encode or a backed-up queue cannot attribute a later pose to earlier
|
||
pixels.
|
||
|
||
### Env payload
|
||
|
||
Built by `MqttControlChannel::publishEnv` from an `EnvEvent`, sourced from whichever `IEnvSensor` backend is
|
||
active (SHT41 on the LattePanda's own I2C bus by default; see [configuration.md](configuration.md)):
|
||
|
||
```json
|
||
{ "fwt":"ExampleTower", "temp_c":22.4, "humid_pct":46.1, "time":1719312345678 }
|
||
```
|
||
|
||
| Field | Type | Meaning |
|
||
|-------|------|---------|
|
||
| `fwt` | string | Tower name (`config.ini` `tower_name`) |
|
||
| `temp_c` | float | Ambient temperature, °C |
|
||
| `humid_pct` | float | Ambient relative humidity, % |
|
||
| `time` | int | Sample timestamp, Unix epoch **milliseconds** |
|
||
|
||
## Topic summary
|
||
|
||
```
|
||
subscribe: GGS/FWT/<tower>/target_HDG (int heading)
|
||
GGS/FWT/<tower>/ControlCode (0 = auto sweep, 1 = directed)
|
||
|
||
publish: GGS/FWT/<tower>/StatusCode (echoed control code)
|
||
GGS/FWT/<tower>/CamEvent (JSON: fwt, cam, hdg×10, pit×10, time-ms)
|
||
GGS/FWT/<tower>/Env (JSON: fwt, temp_c, humid_pct, time-ms)
|
||
```
|
||
|
||
## Local testing
|
||
|
||
Point `zkms_server_ip` at a local broker and exercise the topics with Mosquitto:
|
||
|
||
```bash
|
||
mosquitto_sub -t 'GGS/FWT/ExampleTower/#' -v # watch everything for the tower
|
||
mosquitto_pub -t 'GGS/FWT/ExampleTower/ControlCode' -m '0'
|
||
mosquitto_pub -t 'GGS/FWT/ExampleTower/target_HDG' -m '180'
|
||
```
|