fwt_software/docs/architecture.md

132 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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