fwt_software/docs/architecture.md

9.4 KiB
Raw Blame History

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 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

  1. Startupmain() 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 $;<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 fresh motor_info.
  3. Control decision (per 10 ms tick, 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<heading> to drive to the MQTT-supplied target heading before triggering.
  4. Trigger & settleTriggerCamera() 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. SaveSaveImage() 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/<RGB|ACR|NIR>/<unix_ms>.jxl. In demo mode it copies test_smoke.jxl instead.
  7. Announce — after each saved frame, publish a CamEvent JSON message to GGS/FWT/<tower>/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<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 == 1 conditions are documented exactly as implemented; see docs/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.