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 4d5e2e4194
Added scrolling in TUI
2026-08-21 15:25:45 +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 added terminal user interface (TUI) 2026-06-23 13:21:34 +02:00
config camera logic improved 2026-08-07 15:04:47 +02:00
docs Added terminal attachment/detachment 2026-08-07 17:35:32 +02:00
include/fgc Added scrolling in TUI 2026-08-21 15:25:45 +02:00
scripts Added terminal attachment/detachment 2026-08-07 17:35:32 +02:00
src Added scrolling in TUI 2026-08-21 15:25:45 +02:00
tests Added scrolling in TUI 2026-08-21 15:25:45 +02:00
.deploy.env Anchored yaw to 0-360 degrees range 2026-06-29 22:33:00 +02:00
.deploy.env.example Add LattePanda remote deploy workflow; fix MQTT build after move 2026-06-22 21:49:33 +02:00
.gitignore refactor and bug fixes 2026-08-05 17:00:42 +02:00
CMakeLists.txt Added scrolling in TUI 2026-08-21 15:25:45 +02:00
README.md Added terminal attachment/detachment 2026-08-07 17:35:32 +02:00
deploy.sh Added terminal attachment/detachment 2026-08-07 17:35:32 +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 Added terminal attachment/detachment 2026-08-07 17:35:32 +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, the camera and the exposure store. 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 heading/elevation in degrees (calibrated).
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 calib IMU-referenced steps↔degrees calibration (auto-homes if needed; needs the IMU; applied to the session).
gimbal … Other motor controls — steps/nudge/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 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 is per-waypoint and self-correcting: at each settled waypoint the camera is software-triggered for one deliberate frame, which is then measured (brightness, blown highlights, sharpness) and re-shot with corrected exposure/gain until it passes. Accepted settings are remembered per heading, so the next visit starts from a value known to work there instead of waiting for the camera's own auto-exposure to re-converge. If the retry budget runs out the best attempt is kept and flagged degraded rather than losing the waypoint. The reasoning behind each rule is in docs/architecture.md; the knobs are [Capture] 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 calib; 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/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.