# 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** — `.jxl` image 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](../main.cpp) | Config, CLI, thread orchestration, capture cycle logic | | Serial telemetry & commands | [Serial.h](../Serial.h) | Open `/dev/ttyACM0`, parse `motor_info` telemetry, send motor commands | | MQTT client | [MQTT.h](../MQTT.h), [MQTT.cpp](../MQTT.cpp) | Connect to broker, subscribe to control topics, publish status & cam events | | Camera acquisition | [Camera.h](../Camera.h), [Camera.cpp](../Camera.cpp) | Vimba X cameras, frame queue, JXL encode + save, cam event publish | | JPEG XL encoder | [JPEG_XL.h](../JPEG_XL.h) | Wrap libjxl to encode an 8-bit image buffer to a `.jxl` file | | Console parser | [Parser.h](../Parser.h) | Parse stdin command lines into actions | | Timing helpers | [timing.h](../timing.h) | Stopwatch + Unix-ms timestamps used for filenames and loop pacing | | INI parser | [ini.c](../ini.c), [ini.h](../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](../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](../Serial.h) lines 123-139) | | **stdin reader** | `main()` → `inputThread` | `readInput()` blocks on `std::getline`, feeds the `Parser` ([main.cpp](../main.cpp) lines 42-59) | | **MQTT client** | `MQTTClient::connect_client()` → `mqtt_thread` | Connects, then **busy-waits** `while(running);` ([MQTT.cpp](../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](../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` (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 1. **Startup** — `main()` loads `config.ini`, parses CLI flags, constructs `SerialPort`, `MQTTClient`, and `VimbaHandler`. If `--init` is set, it sends an endstop-finding command sequence to the motor controller (`r`, `e`, `q`, wait 60 s, `y`, `ud56`). 2. **Telemetry ingest** — the motor controller streams lines like `$;;;;;;;;;;;`. `SerialPort::parser()` splits on `;`, validates 12 fields, and stores a fresh `motor_info`. 3. **Control decision** (per 10 ms tick, [main.cpp](../main.cpp) lines 293-334): - Read current `motor_info` and current MQTT `ControlCode`/`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` to drive to the MQTT-supplied target heading before triggering. 4. **Trigger & settle** — `TriggerCamera()` resets each observer's `settle` counter to 3 and fires the `TriggerSoftware` command 4 times (~400 ms apart). The first 3 frames are intentionally dumped (sensor settling); the 4th is the real image. 5. **Enqueue** — for each completed frame, `EnqueueToStoreStruct()` deep-copies the pixel buffer into an `image_store_8bit` with 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 via `cv_proc`. 6. **Save** — `SaveImage()` pops frames, builds an OpenCV `Mat`, rotates 90° CCW, optionally displays it, then (unless in demo mode) encodes with `JPEGXL` and writes to `$HOME/projects/Fire_Gimbal_Control/bin/x64/Release//.jxl`. In demo mode it copies `test_smoke.jxl` instead. 7. **Announce** — after each saved frame, publish a `CamEvent` JSON message to `GGS/FWT//CamEvent` with 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" └──────────┬───────────┘ 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 == 1` conditions are documented exactly as implemented; see > [docs/known-issues.md](known-issues.md) for behaviour that looks surprising (e.g. triggering is gated on > `is_moving == 1` rather 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.