Software for control of 2 axis and 1 axis fire watch towers. To work in conjunction with the firmware at the repository fwt_firmware.
Go to file
pgdalmeida 6f88b00856
Write the drivetrain block at install, since the firmware now demands it
2026-09-18 17:06:34 +02:00
.claude Add CMake build and typed config with path resolution (remove hardcoded paths) 2026-06-22 12:08:18 +02:00
bin/x64/Release Add unit tests, deployment scripts, and refreshed docs 2026-06-22 13:07:03 +02:00
cmake Changed heading logic; added snapshot feature 2026-09-11 17:42:01 +02:00
config Answer the firmware's NEED lines from hardware.ini 2026-09-18 16:16:04 +02:00
docs Write the drivetrain block at install, since the firmware now demands it 2026-09-18 17:06:34 +02:00
hardware Write the drivetrain block at install, since the firmware now demands it 2026-09-18 17:06:34 +02:00
include/fgc Check every declared value, not just the current 2026-09-18 16:44:08 +02:00
scripts Add the LattePanda IOTA setup kit and headless camera profile export 2026-09-16 18:27:39 +02:00
src Write the drivetrain block at install, since the firmware now demands it 2026-09-18 17:06:34 +02:00
tests Check every declared value, not just the current 2026-09-18 16:44:08 +02:00
.deploy.env.example Add LattePanda remote deploy workflow; fix MQTT build after move 2026-06-22 21:49:33 +02:00
.gitignore Stop tracking .deploy.env and ignore it, as its own docs already claim 2026-09-16 18:26:59 +02:00
CMakeLists.txt Answer the firmware's NEED lines from hardware.ini 2026-09-18 16:16:04 +02:00
README.md Move the hardware plans into the repo, where they can have a history 2026-09-16 18:26:13 +02:00
deploy.sh Exclude hardware.ini from the deploy rsync so --delete cannot erase it 2026-09-16 18:27:27 +02:00
ini.c Initial commit 2026-06-22 11:23:01 +02:00
ini.h Initial commit 2026-06-22 11:23:01 +02:00
main.cpp Refuse to start when the rig is missing hardware it declares 2026-09-18 14:50:50 +02:00

README.md

Fire Gimbal Control

Real-time control software for an automated fire-watch gimbal. A C++17 program runs on a tower-mounted PC, rotates a pan gimbal carrying up to four industrial cameras, captures a 360° panorama for wildfire detection, compresses each frame to JPEG XL, and reports to a ground station over MQTT.

A single codebase serves all fire-watch towers (FWT); the tower identity comes from config.ini.

The code is organized around an SDK-independent core (fgc_core: config, logging, capture scheduler, parsers) and swappable I/O implementations behind three interfaces — motor controller, control channel, and camera source — each with a real and a mock/null version. This makes the system deployable on any machine and runnable without hardware or an MQTT broker for development.

State is held in memory (mutex-guarded), configuration is read once from an INI file, images are written to the filesystem as .jxl, and telemetry flows over MQTT. There is no database.

Architecture at a glance

            ┌──────────────────────── fgc_core (no SDKs) ─────────────────────────┐
            │ Config · Logger · CaptureScheduler · TelemetryParser · CommandParser │
            └───────▲─────────────────▲──────────────────▲────────────────────────┘
                    │ IMotorController │ IControlChannel   │ ICameraSource
        ┌───────────┴──────┐  ┌────────┴─────────┐  ┌──────┴────────────────┐
   real │ SerialMotorCtrl  │  │ MqttControlChannel│  │ VimbaCameraSource     │  (WITH_VIMBA/WITH_MQTT)
   mock │ MockMotorCtrl    │  │ NullControlChannel│  │ MockCameraSource      │
        └──────────────────┘  └──────────────────┘  └──────────┬────────────┘
                                                               frames
                                                                ▼
                                                        ImagePipeline → .jxl + CamEvent

See docs/architecture.md for the full design.

Build

cmake -B build                 # configure (needs the Vimba X SDK + Paho for a full build)
cmake --build build            # -> build/fire_gimbal_control

# Development build with NO proprietary SDKs (mocks only):
cmake -B build -DWITH_VIMBA=OFF -DWITH_MQTT=OFF
cmake --build build

Dependencies and the Vimba X SDK setup are in docs/build-and-setup.md.

Run

On a tower the program must outlive the ssh session that started it, so scripts/fgc runs it detached inside a tmux session:

scripts/fgc start --start        # start detached; returns immediately
scripts/fgc attach               # watch the dashboard — Ctrl-b d leaves it running
scripts/fgc status               # running? since when? which PID?
scripts/fgc stop                 # SIGTERM -> clean camera/motor teardown
scripts/fgc peek                 # snapshot of the pane without attaching

Disconnecting from ssh while attached — or closing the laptop — is invisible to the program: tmux owns the pty. When attached, the dashboard's q key exits the program; Ctrl-b d is what detaches.

From a dev box, ./deploy.sh --run builds on the device and starts it this way; --attach, --status and --stop drive it afterwards without re-syncing.

Only one instance may run at a time. The binary takes an exclusive flock on /run/user/<uid>/fire_gimbal_control.lock (or /tmp/fire_gimbal_control-<uid>.lock) at startup and exits with status 2 and another instance is already running (PID N) if it is taken — two instances would fight over the serial port and the camera. The check is in the binary, so a bare ./build/fire_gimbal_control is refused too. Nothing needs cleaning up after a crash or a power cut: the kernel drops the lock when the process dies. --no-lock bypasses it for a second mock-only instance during development.

For development, or for a foreground run you intend to babysit, use run.sh directly:

# Real deployment (needs cameras, motor MCU on the configured serial port, MQTT broker):
scripts/run.sh --start
scripts/run.sh --init --start          # find endstops first

# Development, no hardware or broker:
scripts/run.sh --mock-serial --mock-camera --no-mqtt --start --log-level debug

# Same, but with the full-screen terminal dashboard:
scripts/run.sh --tui --mock-serial --mock-camera --no-mqtt

run.sh resolves the binary relative to the repo, so it works from any checkout; extra args are forwarded to fire_gimbal_control. On launch the program starts the motor controller, opens the camera(s), starts the image pipeline, connects the control channel (continuing in degraded mode if the broker is unreachable), then enters a control loop. Without --start it idles until you type start. Stop with exit or Ctrl-D.

Config is resolved from --config <path>, then $FGC_CONFIG, ./config.ini, the executable's directory, and $XDG_CONFIG_HOME/fire_gimbal_control/config.ini. Copy the template to get started:

cp config/config.example.ini config.ini   # then edit

Command-line options

Flag Effect
-c, --config <path> Use a specific config.ini (otherwise the search path above).
-i, --init Run the endstop-finding init sequence before entering the loop.
-s, --start Begin capturing immediately on launch (else wait for the start command).
-d, --demo Demo mode: copy a placeholder .jxl instead of encoding live frames.
--no-mqtt Disable MQTT; use a null control channel (overrides enable_mqtt).
--mock-camera Use a simulated camera — no Vimba hardware needed.
--mock-serial Use a simulated motor controller — no serial hardware needed.
--tui / --no-tui Show the full-screen terminal dashboard / force the headless console (overrides [UI] enable_tui).
--no-lock Skip the single-instance lock (dev only — for a second mock-only instance).
--lock-file <path> Use a specific lockfile instead of the default per-uid path.
--log-level <lvl> trace, debug, info, warn, error, or off.
--trace <cats> Verbatim wire tracing; comma list of serial,mqtt,camera,control,all,none.
-h, --help Print options and exit.

CLI flags override the matching [Features] / [Logging] keys in config.ini.

Interactive commands

While running, the program reads commands from stdin (one per line):

Command Action
start / stop Start / stop the capture cycle (the gimbal stays powered and homed).
gimbal move <yaw>,<pitch> Aim at an absolute true bearing (0 = north) and elevation, in degrees.
gimbal home [y|p] Run the endstop-finding home sequence.
gimbal dump Request a firmware state dump (shown in the gimbal g view).
gimbal diag [y|p|all] Run the firmware motor self-test (results in the activity strip + logs/).
gimbal heading setup Tie yaw to true north by sighting known landmarks (TUI key H; no IMU needed).
gimbal heading offset <deg> Shift the whole yaw map by a known amount, and save it.
gimbal pitch setup Level pitch against gravity (auto-homes if needed; needs the IMU).
snapshot Take one image now and publish its CamEvent (TUI: Space; manual mode only).
gimbal … Other motor controls — steps/nudge/jog/stop/speed/reset/enable/disable/setpos/status.
gimbal raw "<command>" Low-level: send the quoted text to the firmware verbatim (newline added). Bypasses wrappers/unit conversion — use with care.
debug Toggle debug-level logging on/off.
set fps <n> Capture rate, in images per second.
set camera fps <n> Camera sensor frame rate.
set camera profile [<cam>] <name> Switch a camera's profile (any <name>.xml in its profile_dir).
set camera jxlq <d> JPEG XL quality as butteraugli distance (lower = higher quality / larger files).
set camera jxle <n> JPEG XL encoder effort (higher = slower, smaller).
set camera display <0|1> Toggle the local preview window.
trace <cat> [on|off] Enable/disable a wire-trace category live (e.g. trace serial, trace mqtt off).
trace all / trace off Enable every category / silence all of them.
help [topic] List commands / expand one section.
exit Stop everything and quit (Ctrl-D also works).

Image capture runs the camera continuously and keeps one frame per waypoint. The camera free-runs at the active mode's rate with its own auto-exposure tracking the light; almost every frame is discarded, and their job is simply to keep that exposure controller converged. At each waypoint the gimbal settles, holds still for the mode's settle_wait_ms, and the newest frame is kept — so the saved image was exposed at rest and already correctly metered, with nothing waiting on a convergence loop.

Which rate and which exposure limits apply comes from a camera profile: a settings XML exported from the VimbaX Viewer and handed straight to the Vimba X SDK, so the camera gets exactly the combination the GUI validated. standard runs 10 Hz for daylight, night runs 1 Hz with long exposures; switch live with set camera profile <name>. Details in docs/architecture.md; the mode-file format is [Camera] in docs/configuration.md.

The Xsens MTi IMU (set [Features] enable_imu + [IMU] device) drives the Sensors panel's live roll/pitch/yaw and powers gimbal pitch setup; the SHT41 ([Features] enable_env + [Env] i2c_device, or --mock-env) supplies the panel's ambient temperature and humidity — the MTi's device-internal temperature is expanded-view-only, so the main window shows one unambiguous temperature. In the TUI, g and i open full-screen gimbal and sensors views (the latter covering both the MTi and the SHT41, each reading labelled with its source), and an activity strip below the log shows the running calibration/diagnostics/scan plus the last result. See docs/configuration.md.

A typical first bring-up: scripts/run.sh --init to home the gimbal, then type start once you've confirmed telemetry looks sane; adjust set fps / set camera jxlq live as needed. See docs/configuration.md for the full config.ini reference.

Verbosity & wire tracing

Two independent knobs control output:

  • Log level (--log-level, the debug console toggle, [Logging] level) gates ordinary messages: info (default) shows discrete events only; debug adds per-image saves; trace is the most verbose. At info an active capture is nearly silent.
  • Wire-trace categories (--trace, the trace console command, [Logging] trace) log every message exchanged with a subsystem, verbatim, and are independent of the level — --trace serial shows all firmware serial traffic even at info. Categories are off by default (the camera one is high-rate). Only --log-level off silences them.

Lines are tagged by category with a TX/RX direction, so a single subsystem is easy to follow or grep:

14:02:01.234 [SERIAL]  TX MOVE -90,0
14:02:01.250 [SERIAL]  RX ST Y:A,982,969,80084000,0,8,8,Se P:A,...
14:02:02.000 [MQTT]    PUB GGS/FWT/Tower/StatusCode 0
14:02:02.500 [CAMERA]  TX trigger cam0
14:02:02.910 [CAMERA]  RX frame cam0 1936x1216 7064576B
14:02:02.000 [CONTROL] sweep -> grid yaw=-90 pitch=0

Example — watch only firmware serial traffic on the device: ./deploy.sh --run with RUN_ARGS="--start --trace serial".

Test

cmake -B build -DWITH_VIMBA=OFF -DWITH_MQTT=OFF -DBUILD_TESTING=ON
cmake --build build
ctest --test-dir build --output-on-failure

The unit tests cover the core (config, paths, telemetry/command parsers, capture scheduler) and need no SDKs.

Deploy to the LattePanda

The LattePanda is the machine wired to the gimbal and camera, so it builds and runs there. deploy.sh rsyncs the codebase over ssh and runs the cmake build remotely (mirrors ../firmware/deploy.sh).

cp .deploy.env.example .deploy.env   # then edit REMOTE_HOST / REMOTE_DIR
./deploy.sh --check-deps             # verify the device has OpenCV/Boost/libjxl
./deploy.sh                          # rsync + remote configure + build
./deploy.sh --run                    # ... then run it over ssh (RUN_ARGS, ctrl-c to stop)
./deploy.sh --clean --run            # wipe remote build dir first, then build + run

config.ini and images/ are device-local and never overwritten by a deploy; the first deploy seeds config.ini from config/config.example.ini if the device has none. One-time device prerequisites (Ubuntu): sudo apt-get install -y build-essential cmake git libopencv-dev libboost-program-options-dev libjxl-dev, plus the Vimba X SDK under /opt/VimbaX for -DWITH_VIMBA=ON.

Documentation

Document Contents
docs/architecture.md Components, interfaces, threading, data flow, capture state machine
docs/build-and-setup.md CMake, dependencies, options, Vimba X / Paho, host setup
docs/deployment.md Step-by-step deploy to the gimbal LattePanda (systemd)
docs/configuration.md config.ini keys, CLI flags, console commands
docs/mqtt-api.md MQTT topic catalog and payloads
docs/test-command.md The test hardware self-test suite: subsystems, profiles, reports, baselines
docs/modules-reference.md Per-file reference and data structures
docs/known-issues.md Status of past issues + remaining caveats
docs/roadmap.md Forward-looking project roadmap
docs/hardware-config-plan.md Splitting hardware.ini out of config.ini, and the firmware config handshake
docs/hardware-porting-plan.md Porting the tower to the LattePanda IOTA / RP2040
docs/lattepanda-iota.md Hardware facts for the IOTA board
docs/test-roadmap.md Planned tests blocked on not-yet-integrated components

Repository layout

CMakeLists.txt          Build (options: WITH_VIMBA, WITH_MQTT, BUILD_TESTING)
cmake/                  FindVmb.cmake (Vimba X), Paho.cmake (FetchContent)
config/                 config.example.ini, scan.csv (real config.ini is gitignored)
include/fgc/            Public headers: interfaces, Config, Logger, scheduler, impls
  mock/                 Mock/null implementations
src/
  core/                 Config, Logger, Paths, parsers, Geometry, ScanGrid, CaptureScheduler, Application
  serial/              SerialMotorController
  mqtt/                MqttControlChannel
  camera/              VimbaCameraSource, ImagePipeline, JpegXlEncoder
  main.cpp             Thin entry point (CLI -> Application)
tests/                  doctest unit tests
scripts/                run.sh + systemd unit template
ini.c / ini.h           Bundled inih INI parser

Ownership

Internal tooling for the GGS fire-watch tower network. No license file is present in the repository.