# Architecture ## Overview The program rotates a pan gimbal carrying up to four cameras, periodically stops/points it, triggers the cameras, compresses frames to JPEG XL, writes them to disk, and announces each capture over MQTT. Remote operators can override behaviour over MQTT. The design separates **policy** (the control logic) from **mechanism** (the I/O to hardware/broker): - **`fgc_core`** — an SDK-independent static library: typed configuration, path resolution, logging, the telemetry/command parsers, and the `CaptureScheduler` (control state machine). Depends on nothing proprietary, so it builds and unit-tests anywhere. - **Three interfaces** abstract the outside world, each with a real and a mock/null implementation: | Interface | Real | Mock / Null | |-----------|------|-------------| | `IMotorController` | `SerialMotorController` (Boost.Asio) | `MockMotorController` (simulated sweep) | | `IControlChannel` | `MqttControlChannel` (Paho) | `NullControlChannel` (no broker) | | `ICameraSource` | `VimbaCameraSource` (Vimba X) | `MockCameraSource` (synthetic frames) | `Application` picks real vs mock from config + CLI, wires everything to the `ImagePipeline` and `CaptureScheduler`, and runs the loop. Selecting mocks lets the whole system run with **no hardware or broker**. ``` ┌──────────────── fgc_core (no SDKs) ───────────────┐ main.cpp ──► Application ──► CaptureScheduler ──► (interfaces below) │ │ Config · Logger · Paths · TelemetryParser · CommandParser│ └───────────────────────────────────────────────────────────┘ │ builds + owns ┌────────────────┼───────────────────────────┬──────────────────────┐ IMotorController IControlChannel ICameraSource ImagePipeline Serial/Mock Mqtt/Null Vimba/Mock ◄── frames ──┘ encode→.jxl→CamEvent │ │ │ serial MQTT broker cameras ``` ## Threading model | Thread | Where | Role | |--------|-------|------| | Main / control loop | `Application::run` | 10 ms tick: drain console commands, `scheduler.tick()` | | stdin reader | `Application` | reads command lines into a queue | | Serial I/O | `SerialMotorController` | Boost.Asio `io_context`; async read-until parses telemetry | | Image worker | `ImagePipeline` | drains the frame queue: rotate → encode → write → publish | | MQTT client | Paho (internal) | delivers callbacks; auto-reconnects (no busy-wait loop) | | Camera acquisition | Vimba X (internal) | delivers frames via the observer (real source) | Shared state is mutex-guarded: latest `MotorTelemetry` (serial), `ControlCommand` (channel), the frame queue (pipeline), and the console command queue. The `CaptureScheduler` runs only on the main thread. ## Data flow 1. **Startup** — `main()` parses CLI, resolves + loads config, constructs `Application`, which builds the motor/channel/camera (real or mock), the `ImagePipeline`, and the `CaptureScheduler`. 2. **Telemetry** — the real motor controller streams `$;...;` lines; `parseTelemetryLine` turns each into a `MotorTelemetry` snapshot. The mock synthesizes one. 3. **Control input** — `IControlChannel::poll()` returns the latest `ControlCommand` (control code + target heading), clearing its "available" flags so each update is acted on once. 4. **Capture cycle** (`CaptureScheduler::tick`, per 10 ms): - ControlCode 0: when the interval elapses, send `p` to advance/stop, then trigger the cameras. - ControlCode 1: send `kd` to drive to the requested heading, then trigger. 5. **Frame handling** — a triggered camera delivers a `Frame` to the callback, which `submit()`s it to the `ImagePipeline`. The worker rotates it 90° CCW, encodes JPEG XL (or copies the demo image), writes `/