9.4 KiB
Architecture
Purpose
The program automates a pan gimbal carrying up to four cameras. It periodically rotates the gimbal to a series of headings, stops, triggers the cameras, compresses each captured frame to JPEG XL, writes it to disk, and announces the capture over MQTT so a ground station can ingest the images for wildfire detection. Remote operators can override the behaviour over MQTT (e.g. point at a specific heading).
There is no persistent state beyond image files. Everything is one of:
- Configuration — read once at startup from
config.ini. - Live state — in-memory C++ structs, guarded by mutexes, overwritten each cycle.
- Artifacts —
.jxlimage files on disk. - Messages — MQTT publish/subscribe (no local persistence).
Component overview
┌──────────────────────────────────────────────────────┐
│ main.cpp │
│ - load config.ini (ini.c) │
│ - parse CLI args (Boost.Program_options) │
│ - own the 10 ms control loop │
└───┬───────────────┬───────────────┬──────────────┬─────┘
│ │ │ │
reads/writes │ reads │ reads │ drives │
▼ ▼ ▼ ▼
┌────────────────┐ ┌───────────┐ ┌──────────────┐ ┌──────────┐
│ SerialPort │ │ MQTTClient│ │ Parser / │ │VimbaHandler│
│ (Serial.h) │ │ (MQTT.*) │ │ CMD_eval │ │ (Camera.*) │
│ Boost.Asio │ │ Paho C++ │ │ Boost.Spirit │ │ Vimba X │
└──────┬─────────┘ └─────┬─────┘ └──────────────┘ └─────┬──────┘
│ │ │
serial │ /dev/ttyACM0 │ TCP GigE/USB │
▼ ▼ ▼
motor controller MQTT broker cameras
(MCU + sensors) (ground station) (RGB/ACR/NIR)
| Component | File(s) | Responsibility |
|---|---|---|
| Entry / control loop | main.cpp | Config, CLI, thread orchestration, capture cycle logic |
| Serial telemetry & commands | Serial.h | Open /dev/ttyACM0, parse motor_info telemetry, send motor commands |
| MQTT client | MQTT.h, MQTT.cpp | Connect to broker, subscribe to control topics, publish status & cam events |
| Camera acquisition | Camera.h, Camera.cpp | Vimba X cameras, frame queue, JXL encode + save, cam event publish |
| JPEG XL encoder | JPEG_XL.h | Wrap libjxl to encode an 8-bit image buffer to a .jxl file |
| Console parser | Parser.h | Parse stdin command lines into actions |
| Timing helpers | timing.h | Stopwatch + Unix-ms timestamps used for filenames and loop pacing |
| INI parser | ini.c, ini.h | Third-party inih used to read config.ini |
Threading model
The process runs five concurrent contexts:
| Thread | Created in | Loop / blocking behaviour |
|---|---|---|
| Main / control loop | main() |
while(running); 10 ms sleep per tick (main.cpp lines 243-336) |
| Serial I/O | main() → io_thread |
Runs boost::asio::io_service::run(); async read-until \n re-arms itself (Serial.h lines 123-139) |
| stdin reader | main() → inputThread |
readInput() blocks on std::getline, feeds the Parser (main.cpp lines 42-59) |
| MQTT client | MQTTClient::connect_client() → mqtt_thread |
Connects, then busy-waits while(running); (MQTT.cpp lines 82-99). Paho's own threads deliver callbacks |
| Image saver | VimbaHandler::Start() → image_saver_thread |
Waits on a condition variable, drains the per-camera queues, encodes + writes JXL (Camera.cpp lines 303-394) |
Vimba X additionally delivers frames on its own internal acquisition threads via the FrameObserver callback.
Shared state and synchronization
| Shared data | Type | Guard | Producer → Consumer |
|---|---|---|---|
motor_info telemetry |
struct | SerialPort::mut |
Serial I/O thread → main loop (get_controller_info()) |
mqtt_sub_data (target heading, control code) |
struct | MQTTCallback::mqtt_mut |
Paho callback thread → main loop (get_sub_data(), which clears the "available" flags) |
| Per-camera frame queues | std::queue<ImageStore8Ptr> (max 100) |
VimbaHandler::queue_mut |
FrameObserver/enqueue → saver thread; saver woken by cv_proc |
parser_data (console command) |
struct | Parser::mut |
stdin thread → main loop |
End-to-end data flow
- Startup —
main()loadsconfig.ini, parses CLI flags, constructsSerialPort,MQTTClient, andVimbaHandler. If--initis set, it sends an endstop-finding command sequence to the motor controller (r,e,q, wait 60 s,y,ud56). - Telemetry ingest — the motor controller streams lines like
$;<Xenc>;<Xerr>;<sgt_val>;<sgt_stat>;<is_moving>;<control_status>;<hdg>;<deviation_warn>;<humid>;<temp>;<fan_pwm>.SerialPort::parser()splits on;, validates 12 fields, and stores a freshmotor_info. - Control decision (per 10 ms tick, main.cpp lines 293-334):
- Read current
motor_infoand current MQTTControlCode/target_HDG. - ControlCode 0 (automatic sweep): when the image interval has elapsed and the camera is started, send
p(stop/advance), then once stopped, trigger the cameras. - ControlCode 1 (directed): same timing, but send
kd<heading>to drive to the MQTT-supplied target heading before triggering.
- Read current
- Trigger & settle —
TriggerCamera()resets each observer'ssettlecounter to 3 and fires theTriggerSoftwarecommand 4 times (~400 ms apart). The first 3 frames are intentionally dumped (sensor settling); the 4th is the real image. - Enqueue — for each completed frame,
EnqueueToStoreStruct()deep-copies the pixel buffer into animage_store_8bitwith a Unix-ms timestamp and pushes it onto that camera's bounded queue (oldest reused if the queue is at 100). Enqueuing camera 0 notifies the saver viacv_proc. - Save —
SaveImage()pops frames, builds an OpenCVMat, rotates 90° CCW, optionally displays it, then (unless in demo mode) encodes withJPEGXLand writes to$HOME/projects/Fire_Gimbal_Control/bin/x64/Release/<RGB|ACR|NIR>/<unix_ms>.jxl. In demo mode it copiestest_smoke.jxlinstead. - Announce — after each saved frame, publish a
CamEventJSON message toGGS/FWT/<tower>/CamEventwith the tower, camera name, heading (×10, integer), and timestamp.
Capture state machine (per camera trigger cycle)
interval elapsed AND cam_started
│
▼
┌──────────────────────┐ ControlCode 0 → send "p"
│ request gimbal stop │ ControlCode 1 → send "kd<target_HDG>"
└──────────┬───────────┘ set trigger_after_stopping = true, reset loop_timer
│
▼ (after >100 ms, when is_moving == 1)
┌──────────────────────┐
│ TriggerCamera() │ reset settle=3; fire TriggerSoftware ×4 (400 ms apart)
└──────────┬───────────┘
│ frames 1-3 dumped (settling), frame 4 enqueued
▼
┌──────────────────────┐
│ enqueue → save → MQTT │ trigger_after_stopping = false
└──────────────────────┘
The settle/trigger timing and the
is_moving == 1conditions are documented exactly as implemented; see docs/known-issues.md for behaviour that looks surprising (e.g. triggering is gated onis_moving == 1rather than== 0).
Why this shape
The design favours low-latency real-time control over durability: a tight polling loop reacts to motor feedback in milliseconds, the camera pipeline is decoupled by an in-memory queue so encoding never blocks acquisition, and all coordination with the outside world is asynchronous (serial async I/O + MQTT). Persistence is deliberately limited to image files plus fire-and-forget MQTT messages.