fwt_software/docs/modules-reference.md

23 KiB
Raw Permalink Blame History

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/IniEdit.h, src/core/IniEdit.cpp Editing config.ini and hardware.ini without breaking them, as pure functions on file contents: set keys keeping comments, order and one ;prev generation, comment out keys hardware.ini owns (without their value, so deploy.sh's placeholder guard stays quiet), and refuse lines too long for inih to read whole. tools/fgc_config.cpp does the file IO
include/fgc/SingleInstance.h, src/core/SingleInstance.cpp Refuses a second running copy of the program, which would fight the first over the motor's serial port and the camera: an advisory flock on a lockfile holding the owner's PID, taken in the binary so every way of starting it is covered
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); encoderPitchDeg (the CamEvent pitch when there is no IMU — the fixed deg_at_center on a yaw-only tower)
include/fgc/ScanGrid.h, src/core/ScanGrid.cpp Capture waypoints (CSV only, never generated; a bare yaw line is pitch 0) + one-way wrapping cursor ([Scan]); repeatedBearings for the yaw-only audit
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; parseAim (<yaw>[,<pitch>] for gimbal move/steps) and moveCommand (the firmware MOVE: the pair, or MOVE Y without a pitch)
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/TestRunner.h, src/core/TestRunner.cpp The test command: resolves test [<subsystem> [<leaf>]] to a set of leaf tests, knows which need a homed gimbal (the test hardware leaves deliberately do not), and runs them on a worker thread with live progress, handing back a TestReport
include/fgc/TestReport.h, src/core/TestReport.cpp A self-test run's result as plain data: formats a human-readable text report that parseTestReport reads back, so a run can be compared with the baseline or the previous one
include/fgc/HostMetrics.h, src/core/HostMetrics.cpp Tower computer health for test host: CPU temperature, disk, memory and load. Pure parsers split from the live /sys and /proc readers so they unit-test anywhere; readers report ok = false rather than throw
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/sensors/McuEnvSensor.h, src/sensors/McuEnvSensor.cpp IEnvSensor for the older towers' DHT11, wired to the motor board ([Hardware] env = dht11): serves the firmware's latest ENV line, and no reading after a failed checksum or 2.5 read periods of silence
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 Connectivity view so a publish can be confirmed without a broker-side subscriber
include/fgc/RemoteCommands.h, src/core/RemoteCommands.cpp Console commands that arrive over MQTT: run through the same handler as typed ones, on the control thread, and answered on CommandReply with every line they logged. Retained messages are never run, a repeated id is answered again rather than re-run, each is audited in the log, and a reply that depends on the firmware (gimbal dump) waits for its answer
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: move, standstill, settle wait (+ SettleRule), the rest of the interval since the last frame, keep; holds while saving is behind; injectable clock; setGeometry adopts a recalibration
include/fgc/SettleRule.h, src/core/SettleRule.cpp The settle_rule = brightness keep rule, pure: fed one frame's brightness, exposure and gain at a time, says when the image has settled (in the camera's target band and steady for 3 frames) or is pinned at the auto-exposure limits
include/fgc/RawImage.h, src/core/RawImage.cpp Pixel layouts the program accepts (Mono8, RGB8, BayerRG8, BayerRG12p), their sizes and bit depths, unpack12p, and sampleBrightness - the cheap per-frame sample the keep rule reads
include/fgc/CameraStall.h, src/core/CameraStall.cpp When a camera stream delivers nothing new: wait, restart the stream, or reset the camera. Pure policy, scaled by the exposure and frame period so a slow frame is not taken for a stall
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/Scroll.h, src/ui/Scroll.cpp Viewport arithmetic for the scrollable TUI panes, line-anchored. Pure integer maths with no FTXUI types, so it is in fgc_core and unit-tested without a terminal
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 RemoteCommand, CommandReply, 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, 8-bit or up to 16-bit values (a 12-bit frame keeps its depth); distance 0 is lossless always
src/camera/ImagePipeline.cpp frame → de-Bayer (raw profiles) → rotate → encode → write → CamEvent (save_workers background workers; canAccept() is the scan's save gate). 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()
tools/fgc_config.cpp fgc-config: writes and edits a tower's config.ini and hardware.ini (seed, set, get, check) so what lands on disk is always a file the program accepts. Used by scripts/install-tower; the rules are in IniEdit
tools/camera-bench/ Camera and CPU measurements behind the capture design (frame rates, losses, settle time, encode cost). Not part of the build; results in camera-capture-measurements.md
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. MotorTelemetry::pitchAbsent() is true for a yaw-only tower — a status line has arrived without a P: segment — and is what the host checks before refusing a pitch command. 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 (s 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 s 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
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.