18 KiB
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, 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, src/core/Paths.cpp | ~/$ENV expansion, executable dir, config search order, default output dir |
| include/fgc/Logger.h, src/core/Logger.cpp | Leveled, thread-safe logger + per-category wire trace; LOG_TRACE..LOG_ERROR, LOG_TRACE_CAT |
| include/fgc/FileLogSink.h, src/core/FileLogSink.cpp | The persistent log file: append, size rotation, count-based pruning, a self-describing header per file. Runs inside Logger's non-recursive lock, so it never logs - failures latch into failed()/lastError() for the caller to report |
| include/fgc/Geometry.h, src/core/Geometry.cpp | Per-axis degrees↔encoder-counts affine map ([Motor] calibration) |
| include/fgc/ScanGrid.h, src/core/ScanGrid.cpp | Capture waypoints (CSV or generated) + one-way wrapping cursor ([Scan]) |
| include/fgc/TelemetryParser.h, src/core/TelemetryParser.cpp | parseTelemetryLine (firmware ST line) → std::optional<MotorTelemetry> |
| include/fgc/CommandParser.h, src/core/CommandParser.cpp | parseCommand whitespace tokenizer → Command |
| include/fgc/HelpText.h, src/core/HelpText.cpp | Operator command catalog (helpCatalog) + renderHelp (console & TUI help) |
| include/fgc/TmcCurrent.h, src/core/TmcCurrent.cpp | Milliamps → TMC5160 GLOBALSCALER/IRUN register fields, using the sense resistor from [Motor.<axis>]. Pure arithmetic, so it is unit-tested; the host owns the conversion because only it knows the resistor |
| include/fgc/HardwarePresence.h, src/core/HardwarePresence.cpp | reconcilePresence (what hardware.ini declares vs what actually came up) + presenceVerdict (proceed / warn / refuse, per strict_presence and --degraded). Pure comparison, so the policy is unit-tested without a rig |
| include/fgc/HardwareChecks.h, src/core/HardwareChecks.cpp | The verdicts behind test hardware: checkUsbfsMemory, checkUsb3Lpm, checkI2cFunctionality, checkDrivetrainAdopted. Pure, so the thresholds that encode known-issues #2 and #19 cannot drift unnoticed |
| include/fgc/AxisSpeed.h, src/core/AxisSpeed.cpp | planAxisSpeed (deg/s -> TMC VMAX through counts_per_deg) + vmaxToDegPerSec. Pure arithmetic; refuses an uncalibrated scale rather than commanding a near-zero speed |
| include/fgc/FirmwareConfig.h, src/core/FirmwareConfig.cpp | parseNeedLine (firmware NEED line -> axis + keys) + planFirmwareConfig (-> the SET lines that answer it). Reports a key it cannot answer separately from one it does not recognise: incomplete hardware.ini vs. a firmware newer than the host |
| include/fgc/DumpParser.h, src/core/DumpParser.cpp | parseDump firmware DUMP block → DumpData (per-axis state + decoded TMC registers/flags); formatDump. Prefers the firmware's own reset= name over any host-side decode - mcusr is an AVR bitmask on one board and a plain enum on the other, so a single table cannot be right for both |
| include/fgc/DiagParser.h, src/core/DiagParser.cpp | parseDiag firmware DG self-test stream → DiagResult (per-axis tests, PASS/FAIL); formatDiag |
| include/fgc/Calibration.h, src/core/Calibration.cpp | linearFit (least-squares, R²) + circularMeanDeg for the IMU-referenced calibration |
| include/fgc/PitchSetup.h, src/core/PitchSetup.cpp | gimbal pitch setup worker thread: home-if-needed → pitch sweep dwelling on the gravity-referenced IMU pitch → least-squares fit → park level; tunable via CalibParams; exposes progress()/report()/takeResult(). Yaw is not touched |
| include/fgc/HeadingFit.h, src/core/HeadingFit.cpp | gimbal heading fit: least-squares fit of the yaw map from operator sightings of landmarks with known true bearings. Sorts by encoder position, unwraps bearings so a set spanning north fits one line, and refuses a baseline under 10° (two points always fit exactly, so a short baseline looks perfect and is wrong). No IMU involved |
| include/fgc/MtiProtocol.h, 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); named mtmode/mtsettings bitfields with mtDataLen deriving the payload length from them (so kMTDataLen cannot drift from what the parser decodes), plus checkImuDeviceConfig → ImuConfigMismatch (does the device report what the host asked for?) and mtiErrorText for MID 0x42 rejections |
| include/fgc/ImuMount.h, src/core/ImuMount.cpp | ImuMount (the [IMU] x/y/z mounting rotation) and the pure math around it: mountRotationZXY (intrinsic Z-X-Y), the rotationFromEulerZYX/eulerZYXFromRotation pair for the MTi's own yaw-pitch-roll output, applyFrames, which moves a whole ImuSample into the gimbal's NED frame by re-referencing the earth frame (ImuReference/nedFromReference, left-multiplied) and then applying the mount (right-multiplied) — the two are not interchangeable, which is why a mount alone cannot correct a Z-up device; applyMount is the NED-already shorthand. Body-frame vectors are rotated by the mount only. Returns the sample untouched when there is nothing to do |
| include/fgc/MountedImuSource.h | Decorator over any IImuSource applying that rotation in sample(). Wrapped around the factory's output in Application::makeImu(), so every consumer reads gimbal-frame values without knowing a mount exists; skipped entirely when the mount is the identity |
| include/fgc/sensors/Sht41Protocol.h, 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/CameraProfile.h, src/core/CameraProfile.cpp | loadCameraProfile / loadCameraProfiles: one VimbaX settings XML per profile. Hashes the XML, regenerates the .txt record when it changes, and reports which settings moved. A missing XML throws — the record is never a fallback |
| include/fgc/CameraSettingsXml.h, src/core/CameraSettingsXml.cpp | Parses the Viewer export's <RemoteDevice> features (ordered, selector-aware), renders the human-readable .txt record, reads one back, and diffs two settings lists. Used for display and change detection only — never to configure a camera |
| include/fgc/ChannelTopics.h, src/core/ChannelTopics.cpp | The MQTT wire format in one place: topic builders for the GGS/FWT/<tower>/… tree and the CamEvent/status payload builders. Pure, so the documented format is unit-tested rather than assembled inline at each publish site |
| include/fgc/ChannelHistory.h, src/core/ChannelHistory.cpp | Ring buffer of recently published messages (time, topic, payload, outcome) plus the delivery counters — sent, failed, connects, losses, last status. Feeds the expanded Comms view so a publish can be confirmed without a broker-side subscriber |
| include/fgc/Hash.h, src/core/Hash.cpp | FNV-1a 64 over a string or a file, with an algorithm-prefixed label. Change detection for the profile records, deliberately not a crypto hash |
| include/fgc/CaptureScheduler.h, src/core/CaptureScheduler.cpp | Capture state machine over the interfaces; injectable clock; setGeometry adopts a recalibration |
| include/fgc/Application.h, 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, 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, src/ui/HeadlessUi.cpp | Default line console: stdin → command sink; logs via the default stdout/stderr writer |
| ini.c, ini.h | Bundled third-party inih INI parser |
Interfaces
| File | Interface | Shared structs |
|---|---|---|
| include/fgc/IMotorController.h | IMotorController |
MotorTelemetry |
| include/fgc/IControlChannel.h | IControlChannel |
ControlCommand, CamEvent |
| include/fgc/ICameraSource.h | ICameraSource |
Frame |
| include/fgc/IImuSource.h | IImuSource (incl. config(), configProblems(), refreshConfig(), noRotation(), headingReset(), setFilterProfile()) |
ImuSample (from MtiProtocol.h) |
| include/fgc/IEnvSensor.h | IEnvSensor (connected(), sample(), name()) |
EnvSample |
| include/fgc/ui/IUserInterface.h | IUserInterface |
UiSnapshot |
Real implementations (SDK-gated)
| File | Implements | Built when |
|---|---|---|
| src/serial/SerialMotorController.cpp | IMotorController over Boost.Asio serial (pImpl) |
always |
| 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 | Thin /dev/i2c-N wrapper (open/close, write, writeRead via I2C_SLAVE ioctl) |
always |
| 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 | IControlChannel over Eclipse Paho |
WITH_MQTT |
| src/ui/TuiUi.cpp | IUserInterface over FTXUI: panels, key bar, log pane |
WITH_TUI |
| src/camera/VimbaCameraSource.cpp | ICameraSource over Vimba X (pImpl): free-running stream + keep-latest, restart-on-stall, and per-camera profiles applied with the SDK's LoadSettings on the Viewer's settings XML |
WITH_VIMBA |
| src/camera/JpegXlEncoder.cpp | libjxl encode-to-file | always |
| src/camera/ImagePipeline.cpp | frame → rotate → encode → write → CamEvent (worker thread). The pose is sampled in submit() and carried with the frame, so it belongs to the pixels rather than to whenever the encode finished |
always |
Mock / null implementations
| File | Implements |
|---|---|
| include/fgc/mock/MockMotorController.h | Simulated sweeping gimbal (incl. canned DUMP) |
| include/fgc/mock/NullControlChannel.h | No-op channel; auto-sweep |
| include/fgc/mock/MockCameraSource.h | Synthetic gradient frames |
| include/fgc/mock/MockImuSource.h | Synthetic IMU (sinusoidal orientation, gravity on accZ) |
| include/fgc/mock/MockEnvSensor.h | Synthetic ambient sensor (slow sinusoidal temp/humidity, never stale); --mock-env |
Entry point & scripts
| File | Role |
|---|---|
| main.cpp | CLI parsing → AppConfig + RuntimeOptions → Application::run() |
| scripts/run.sh | Path-independent launcher |
| scripts/fire-gimbal-control.service | systemd unit template |
Data structures
MotorTelemetry / AxisTelemetry (IMotorController.h)
Per-axis yaw and pitch segments (pitch_present flag), each an AxisTelemetry: state
(AxisState B/R/H/A/E/C — the last being NEEDCFG, a firmware waiting to be told its drivetrain), xactual, xenc (encoder counts), drv_status, sg/cs/pwm, and flags
standstill/stall/deviation/overtemp/endstop_l/endstop_r, with moving()/ready() helpers.
Parsed from the firmware ST Y:...[ P:...] line; degrees are derived via Geometry.
deviation (flag d) is the TMC5160's hardware encoder-deviation latch — set when |xactual − xenc|
exceeds ENC_DEVIATION, i.e. the shaft is not where it was commanded to be. It is the usable stall signal
on this rig and is what the TUI's STALL badge shows. stall (flag s) is StallGuard2, which is inert
because production runs StealthChop; it is still parsed in case that changes. The latch is cleared by
homing or gimbal reset, so a lit badge means "position untrustworthy since the last home".
Mode, PanSpec (OperatingMode.h)
Mode is auto (0), pan (1) or manual (2) - the values StatusCode carries. PanSpec is a pan's
heading, span and pitch in degrees; panHeadings() turns it into the bearings visited, and
allowedIn() says which commands a mode accepts.
CamEvent (IControlChannel.h)
tower, camera (RGB/ACR/NIR), heading_decideg (yaw×10), pitch_decideg (pitch×10) and
timestamp_ms (which matches the image filename). Serialized to the CamEvent JSON payload (see mqtt-api.md).
EnvSample (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)
Owned pixel buffer + width, height, channels (1 or 3), timestamp_ms, cam_id.
ImuSample (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
(−180..180, exactly as the device reports it), sample_counter. parseMTData applies no sign flip and no re-ranging to any axis.
gimbal heading fit phase-unwraps the sighted bearings (unwrapNear) so a set crossing 0/360 still fits a clean
line.
ImuDeviceConfig (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 | <output_dir>/<label>/<unix_ms>.jxl |
JPEG XL, rotated per that camera's rotation_deg; <label> is [Camera.<name>] label |
| Demo placeholder | bin/x64/Release/test_smoke.jxl |
copied verbatim in demo mode |
| Diagnostics log | <data_dir>/fire_gimbal_control/logs/diag_<ts>.log |
raw DG stream + parsed summary |
| Calibration log | <data_dir>/fire_gimbal_control/logs/calib_<ts>.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.