# Module Reference Per-file reference for the refactored tree, plus the shared data structures. ## Core (`fgc_core` — SDK-independent, unit-tested) | File | Contents | |------|----------| | [include/fgc/Config.h](../include/fgc/Config.h), [src/core/Config.cpp](../src/core/Config.cpp) | Typed `AppConfig` (General/Network/Serial/Camera/Paths/Features/Logging/UI/Motor/Scan/**IMU**) + `ConfigLoader` (INI parse, env overrides, validation) | | [include/fgc/Paths.h](../include/fgc/Paths.h), [src/core/Paths.cpp](../src/core/Paths.cpp) | `~`/`$ENV` expansion, executable dir, config search order, default output dir | | [include/fgc/Logger.h](../include/fgc/Logger.h), [src/core/Logger.cpp](../src/core/Logger.cpp) | Leveled, thread-safe logger + per-category wire trace; `LOG_TRACE..LOG_ERROR`, `LOG_TRACE_CAT` | | [include/fgc/Geometry.h](../include/fgc/Geometry.h), [src/core/Geometry.cpp](../src/core/Geometry.cpp) | Per-axis degrees↔encoder-counts affine map (`[Motor]` calibration) | | [include/fgc/ScanGrid.h](../include/fgc/ScanGrid.h), [src/core/ScanGrid.cpp](../src/core/ScanGrid.cpp) | Capture waypoints (CSV or generated) + ping-pong cursor (`[Scan]`) | | [include/fgc/TelemetryParser.h](../include/fgc/TelemetryParser.h), [src/core/TelemetryParser.cpp](../src/core/TelemetryParser.cpp) | `parseTelemetryLine` (firmware `ST` line) → `std::optional` | | [include/fgc/CommandParser.h](../include/fgc/CommandParser.h), [src/core/CommandParser.cpp](../src/core/CommandParser.cpp) | `parseCommand` whitespace tokenizer → `Command` | | [include/fgc/HelpText.h](../include/fgc/HelpText.h), [src/core/HelpText.cpp](../src/core/HelpText.cpp) | Operator command catalog (`helpCatalog`) + `renderHelp` (console & TUI help) | | [include/fgc/DumpParser.h](../include/fgc/DumpParser.h), [src/core/DumpParser.cpp](../src/core/DumpParser.cpp) | `parseDump` firmware `DUMP` block → `DumpData` (per-axis state + decoded TMC registers/flags); `formatDump` | | [include/fgc/DiagParser.h](../include/fgc/DiagParser.h), [src/core/DiagParser.cpp](../src/core/DiagParser.cpp) | `parseDiag` firmware `DG` self-test stream → `DiagResult` (per-axis tests, PASS/FAIL); `formatDiag` | | [include/fgc/Calibration.h](../include/fgc/Calibration.h), [src/core/Calibration.cpp](../src/core/Calibration.cpp) | `linearFit` (least-squares, R²) + `circularMeanDeg` for the IMU-referenced calibration | | [include/fgc/CalibrationRoutine.h](../include/fgc/CalibrationRoutine.h), [src/core/CalibrationRoutine.cpp](../src/core/CalibrationRoutine.cpp) | `gimbal calib` worker thread: home-if-needed → pitch sweep at the first yaw position → pitch 0° → switch IMU to a no-mag XKF profile → IMU no-rotation + heading reset → yaw sweep, fitting degrees↔counts; tunable via `CalibParams`; exposes `progress()`/`report()`/`takeResult()` | | [include/fgc/MtiProtocol.h](../include/fgc/MtiProtocol.h), [src/core/MtiProtocol.cpp](../src/core/MtiProtocol.cpp) | Xsens MTi binary protocol: `MtiFramer` (checksum framing), config-message builders, `parseMTData` → `ImuSample` (temp/acc/gyr/mag/euler), config-readback query builders + `applyImuConfigAck`/`finalizeImuConfig` → `ImuDeviceConfig` (product/firmware/device-id/output mode+settings/sample rate/**XKF scenario**); orientation-control builders `msgSetNoRotation` (gyro-bias update) + `msgResetOrientation` (heading reset / store) + `msgSetFilterProfile` (select XKF profile) + `pickNoMagProfile` (choose a magnetometer-free profile from the available list) | | [include/fgc/ImageQuality.h](../include/fgc/ImageQuality.h), [src/core/ImageQuality.cpp](../src/core/ImageQuality.cpp) | `analyzeFrame` → `ImageMetrics` (mean luma, clipped/dark fraction, variance-of-Laplacian sharpness) over a raw frame buffer. No OpenCV, so it lives in `fgc_core` and is testable on synthetic buffers | | [include/fgc/ExposurePolicy.h](../include/fgc/ExposurePolicy.h), [src/core/ExposurePolicy.cpp](../src/core/ExposurePolicy.cpp) | The exposure/gain control law: `evaluate` → `Verdict` (accept + reason + corrected settings), `score` for ranking attempts. Clipping outranks the mean; exposure before gain when brightening, gain before exposure when darkening; damped to prevent oscillation | | [include/fgc/ExposureStore.h](../include/fgc/ExposureStore.h), [src/core/ExposureStore.cpp](../src/core/ExposureStore.cpp) | Per-angle exposure memory (CSV), keyed by quantised yaw/pitch. Three-tier `seed()` (fresh angle entry → last accepted anywhere → defaults) + `referenceSharpness()` for the relative blur check | | [include/fgc/GatedCameraSource.h](../include/fgc/GatedCameraSource.h), [src/core/GatedCameraSource.cpp](../src/core/GatedCameraSource.cpp) | `ICameraSource` **decorator** turning one `trigger()` into shoot → judge → correct → reshoot, seeded from the store; keeps the best attempt and flags it `degraded` when the budget runs out. Depends only on the interface + pure modules, so the whole loop is testable against a fake camera | | [include/fgc/sensors/Sht41Protocol.h](../include/fgc/sensors/Sht41Protocol.h), [src/sensors/Sht41Protocol.cpp](../src/sensors/Sht41Protocol.cpp) | Sensirion SHT4x wire protocol, pure: command bytes, `sht41Crc8` (poly 0x31/init 0xFF), `decodeSht41Measurement` (6-byte response → `Sht41Reading`, CRC-checked, raw→°C/%RH with RH clamped 0..100) | | [include/fgc/CaptureScheduler.h](../include/fgc/CaptureScheduler.h), [src/core/CaptureScheduler.cpp](../src/core/CaptureScheduler.cpp) | Capture state machine over the interfaces; injectable clock; `setGeometry` adopts a recalibration | | [include/fgc/Application.h](../include/fgc/Application.h), [src/core/Application.cpp](../src/core/Application.cpp) | Factory (real vs mock, headless vs TUI), wiring, control loop, `gimbal …` commands, background-result polling, `buildSnapshot()` | | [include/fgc/ui/UiSnapshot.h](../include/fgc/ui/UiSnapshot.h), [src/ui/UiSnapshot.cpp](../src/ui/UiSnapshot.cpp) | Plain-data view model (incl. `ImuView`, `EnvView`, `ActivityView`, `CalibResultView`, `DumpView`) + pure formatting helpers (state label/colour, degrees, time-ago, pending-sensors) | | [include/fgc/ui/HeadlessUi.h](../include/fgc/ui/HeadlessUi.h), [src/ui/HeadlessUi.cpp](../src/ui/HeadlessUi.cpp) | Default line console: stdin → command sink; logs via the default stdout/stderr writer | | [ini.c](../ini.c), [ini.h](../ini.h) | Bundled third-party inih INI parser | ## Interfaces | File | Interface | Shared structs | |------|-----------|----------------| | [include/fgc/IMotorController.h](../include/fgc/IMotorController.h) | `IMotorController` | `MotorTelemetry` | | [include/fgc/IControlChannel.h](../include/fgc/IControlChannel.h) | `IControlChannel` | `ControlCommand`, `CamEvent`, `EnvEvent` | | [include/fgc/ICameraSource.h](../include/fgc/ICameraSource.h) | `ICameraSource` | `Frame` | | [include/fgc/IImuSource.h](../include/fgc/IImuSource.h) | `IImuSource` (incl. `config()`, `refreshConfig()`, `noRotation()`, `headingReset()`, `setFilterProfile()`) | `ImuSample` (from `MtiProtocol.h`) | | [include/fgc/IEnvSensor.h](../include/fgc/IEnvSensor.h) | `IEnvSensor` (`connected()`, `sample()`, `name()`) | `EnvSample` | | [include/fgc/ui/IUserInterface.h](../include/fgc/ui/IUserInterface.h) | `IUserInterface` | `UiSnapshot` | ## Real implementations (SDK-gated) | File | Implements | Built when | |------|-----------|-----------| | [src/serial/SerialMotorController.cpp](../src/serial/SerialMotorController.cpp) | `IMotorController` over Boost.Asio serial (pImpl) | always | | [src/serial/MtiImuSource.cpp](../src/serial/MtiImuSource.cpp) | `IImuSource` over Boost.Asio serial: configures the MTi to Euler+calibrated, reads back its config (`config()` → `ImuDeviceConfig`), then frames the MTData stream | always | | [src/sensors/I2cBus.cpp](../src/sensors/I2cBus.cpp) | Thin `/dev/i2c-N` wrapper (open/close, `write`, `writeRead` via `I2C_SLAVE` ioctl) | always | | [src/sensors/Sht41EnvSensor.cpp](../src/sensors/Sht41EnvSensor.cpp) | `IEnvSensor` over I2C (pImpl): soft-reset, poll thread at `[Env] period_ms`, decode via `Sht41Protocol`, staleness = no valid sample within 2× the period | always | | [src/mqtt/MqttControlChannel.cpp](../src/mqtt/MqttControlChannel.cpp) | `IControlChannel` over Eclipse Paho | `WITH_MQTT` | | [src/ui/TuiUi.cpp](../src/ui/TuiUi.cpp) | `IUserInterface` over FTXUI: panels, key bar, log pane | `WITH_TUI` | | [src/camera/VimbaCameraSource.cpp](../src/camera/VimbaCameraSource.cpp) | `ICameraSource` over Vimba X (pImpl) | `WITH_VIMBA` | | [src/camera/JpegXlEncoder.cpp](../src/camera/JpegXlEncoder.cpp) | libjxl encode-to-file | always | | [src/camera/ImagePipeline.cpp](../src/camera/ImagePipeline.cpp) | frame → rotate → encode → write → CamEvent (worker thread) | always | ## Mock / null implementations | File | Implements | |------|-----------| | [include/fgc/mock/MockMotorController.h](../include/fgc/mock/MockMotorController.h) | Simulated sweeping gimbal (incl. canned `DUMP`) | | [include/fgc/mock/NullControlChannel.h](../include/fgc/mock/NullControlChannel.h) | No-op channel; auto-sweep | | [include/fgc/mock/MockCameraSource.h](../include/fgc/mock/MockCameraSource.h) | Synthetic gradient frames | | [include/fgc/mock/MockImuSource.h](../include/fgc/mock/MockImuSource.h) | Synthetic IMU (sinusoidal orientation, gravity on accZ) | | [include/fgc/mock/MockEnvSensor.h](../include/fgc/mock/MockEnvSensor.h) | Synthetic ambient sensor (slow sinusoidal temp/humidity, never stale); `--mock-env` | ## Entry point & scripts | File | Role | |------|------| | [main.cpp](../main.cpp) | CLI parsing → `AppConfig` + `RuntimeOptions` → `Application::run()` | | [scripts/run.sh](../scripts/run.sh) | Path-independent launcher | | [scripts/fire-gimbal-control.service](../scripts/fire-gimbal-control.service) | systemd unit template | ## Data structures ### `MotorTelemetry` / `AxisTelemetry` ([IMotorController.h](../include/fgc/IMotorController.h)) Per-axis `yaw` and `pitch` segments (`pitch_present` flag), each an `AxisTelemetry`: `state` (`AxisState` B/R/H/A/E), `xactual`, `xenc` (encoder counts), `drv_status`, `sg`/`cs`/`pwm`, and flags `standstill`/`stall`/`overtemp`/`endstop_l`/`endstop_r`, with `moving()`/`ready()` helpers. Parsed from the firmware `ST Y:...[ P:...]` line; degrees are derived via `Geometry`. ### `ControlCommand` ([IControlChannel.h](../include/fgc/IControlChannel.h)) `control_code` (0 = scan-grid sweep, 1 = directed to `target_HDG`) + `target_heading`, each with an `*_available` flag. ### `CamEvent` ([IControlChannel.h](../include/fgc/IControlChannel.h)) `tower`, `camera` (RGB/ACR/NIR), `heading_decideg` (yaw×10), `pitch_decideg` (pitch×10), `timestamp_ms`, and `degraded` (the quality gate exhausted its attempts and kept its best effort). Serialized to the CamEvent JSON payload (see [mqtt-api.md](mqtt-api.md)). ### `EnvEvent` ([IControlChannel.h](../include/fgc/IControlChannel.h)) `tower`, `temp_c`, `humidity_pct`, `timestamp_ms`. Published once per **fresh** sample (deduplicated on `timestamp_ms`, not per control-loop tick) to the `Env` topic — see [mqtt-api.md](mqtt-api.md). ### `EnvSample` ([IEnvSensor.h](../include/fgc/IEnvSensor.h)) One ambient reading: `valid`, `temp_c` (°C), `humidity_pct` (0..100), `timestamp_ms` (epoch ms). `IEnvSensor::sample()` returns `nullopt` rather than a stale sample once nothing valid has arrived within 2× the configured poll period, so a silently dead sensor reads as absent, not as a frozen value. Surfaced as `EnvView` in the TUI (`i` view) and as `SensorsView::env` in the compact panel. ### `Frame` ([ICameraSource.h](../include/fgc/ICameraSource.h)) Owned pixel buffer + `width`, `height`, `channels` (1 or 3), `timestamp_ms`, `cam_id`, and `degraded` (carried through to the `CamEvent`). ### `ImuSample` ([MtiProtocol.h](../include/fgc/MtiProtocol.h)) One decoded Xsens MTi reading: `temp_c` (°C), `acc[3]` (m/s², incl. gravity), `gyr[3]` (rad/s), `mag[3]` (a.u., earth-normalized), `roll_deg` (−180..180), `pitch_deg` (−90..90), `yaw_deg` (**0..360 heading** — the MTi's native −180..180 is shifted by `parseMTData`), `sample_counter`. `gimbal calib` phase-unwraps the swept yaw (`unwrapNear`) so a sweep crossing 0/360 still fits a clean line. ### `ImuDeviceConfig` ([MtiProtocol.h](../include/fgc/MtiProtocol.h)) The device configuration read back during the Config-state handshake (each `has_*` flag marks whether the device actually answered): `product_code`, `device_id`, `firmware`, output mode flags (temp/calibrated/orientation/aux/status), output settings (`orientation_mode`, `timestamp_mode`, per-channel `acc/gyr/mag_enabled`, `data_format`), `period`/`sample_rate_hz`, and the **XKF scenario** (`scenario_type`/`version`, resolved to `scenario_label` against `available_profiles`). Built by `applyImuConfigAck` (one ack frame at a time) + `finalizeImuConfig` (derive rate, resolve label). Surfaced read-only in the TUI's `i` view (IMU CONFIG section); the legacy "scenario" is the modern "filter profile" — same MIDs (`0x62/0x64/0x65`). ## On-disk artifacts | Artifact | Path | Format | |----------|------|--------| | Captured images | `//.jxl` | JPEG XL, rotated 90° CCW | | Demo placeholder | `bin/x64/Release/test_smoke.jxl` | copied verbatim in demo mode | | Diagnostics log | `/fire_gimbal_control/logs/diag_.log` | raw `DG` stream + parsed summary | | Calibration log | `/fire_gimbal_control/logs/calib_.log` | per-axis samples + the least-squares fit | Diagnostics/calibration results are also surfaced live in the TUI (activity strip + gimbal `g` view) and applied to the running session's geometry; other state lives in memory.