132 lines
9.4 KiB
Markdown
132 lines
9.4 KiB
Markdown
# 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<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. **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
|
||
`$;<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](../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 & 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/<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](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.
|