14 KiB
Configuration & Commands
Behaviour is controlled by three layers: the config.ini file, command-line flags (which override the
config), and interactive console commands typed while running.
Config file resolution
There are no hardcoded paths. The file is resolved in this order (src/core/Paths.cpp):
--config <path>CLI flag$FGC_CONFIGenvironment variable./config.ini(current directory)<executable dir>/config.ini$XDG_CONFIG_HOME/fire_gimbal_control/config.ini(else~/.config/...)
If none exist, the program prints every location it searched and exits. Start from the template:
cp config/config.example.ini config.ini
config.ini keys
Parsed and validated by ConfigLoader (src/core/Config.cpp) into a typed
AppConfig. Invalid types/values fail fast with a clear message.
| Section | Key | Type | Default | Meaning |
|---|---|---|---|---|
General |
tower_name |
string | Unnamed |
Tower identity; used in all MQTT topics/payloads |
General |
image_interval |
int > 0 | 5 |
Seconds between captures (→ image_rate = 1/interval) |
General |
debug |
bool | false |
Start with debug-level logging |
Network |
zkms_server_ip |
string | 127.0.0.1 |
MQTT broker address |
Network |
mqtt_user / mqtt_pw |
string | — | MQTT credentials (see secrets below) |
Serial |
device |
string | /dev/ttyACM0 |
Motor-controller serial device |
Serial |
baud |
int | 115200 |
Serial baud rate |
Camera |
id_Cam1..id_Cam4 |
string | — | Camera IDs (GigE IP or USB DEV_...); non-empty ones used in order |
Paths |
output_dir |
string | $XDG_DATA_HOME/fire_gimbal_control/images |
Image output dir; supports ~/$ENV |
Features |
enable_mqtt |
bool | true |
Use MQTT (vs null channel) |
Features |
enable_camera |
bool | true |
(reserved) |
Features |
enable_serial |
bool | true |
(reserved) |
Features |
enable_imu |
bool | false |
Use the Xsens MTi orientation/IMU |
Features |
mock_camera |
bool | false |
Use the simulated camera |
Features |
mock_serial |
bool | false |
Use the simulated motor controller |
Features |
mock_imu |
bool | false |
Use the simulated IMU instead of the MTi |
IMU |
device |
string | — | MTi serial device (see [IMU] note); required when enable_imu |
IMU |
baud |
int | 115200 |
MTi serial baud rate |
Logging |
level |
enum | info |
Linear log level (--log-level overrides) |
Logging |
trace |
csv | — | Wire-trace categories, off by default (--trace overrides) |
UI |
enable_tui |
bool | false |
Full-screen terminal dashboard (--tui/--no-tui override; needs WITH_TUI=ON) |
Motor |
yaw_counts_per_deg / pitch_counts_per_deg |
float | 983.33 / — |
Encoder counts per degree (calibrate; may be negative to flip) |
Motor |
yaw_zero_count / pitch_zero_count |
int | 500000 / 0 |
xenc value that = 0° |
Motor |
yaw_min_deg/yaw_max_deg/pitch_* |
float | -90/90/… |
Soft clamp on commanded degrees |
Scan |
grid_file |
string | — | CSV of yaw_deg,pitch_deg waypoints; empty → generate |
Scan |
yaw_intervals |
int | 56 |
Generated yaw positions across [yaw_min_deg, yaw_max_deg] |
Scan |
yaw_min_deg/yaw_max_deg |
float | -90/90 |
Generated yaw arc |
Scan |
pitch_levels |
csv | 0 |
Generated pitch elevations (deg) |
[Motor] calibration & [Scan] grid
The firmware reports only encoder counts; [Motor] maps them to the heading/elevation degrees
used by MQTT (target_HDG) and CamEvent. Calibrate *_counts_per_deg / *_zero_count against
real xenc readings after homing (MOVE a known angle, read the resulting xenc).
The capture scan grid is the ordered (yaw,pitch) waypoints auto-sweep visits (ping-pong). Set
[Scan] grid_file to an editable CSV (config/scan.csv) to define exact
coordinates, or leave it blank to generate yaw_intervals × pitch_levels points.
Camera index → output subfolder defaults to RGB, ACR, NIR (CameraConfig::labels).
[IMU] — Xsens MTi orientation sensor
Enable with [Features] enable_imu = true and point [IMU] device at the MTi's serial node. On the
LattePanda the MTi is wired to the RS-232 header, i.e. an onboard hardware UART — a stable
/dev/ttyS* node (e.g. /dev/ttyS4), not a USB device and not /dev/ttyUSB0-3 (those are the
modem). Find it with ls /dev/ttyS* / dmesg | grep -iE 'ttyS|LPSS', or probe for the 0xFA-framed
stream. At startup the host reconfigures the MTi (GoToConfig → SetOutputMode → SetOutputSettings → GoToMeasurement) to a 100 Hz Euler + calibrated stream — so the device will start streaming even if it
was left in Config state. Yaw is reported as a 0..360 heading (not the MTi's native −180..180). Set
[Features] mock_imu = true to use a synthetic IMU on dev machines (no hardware). Protocol/units are
documented in the modules reference (MtiProtocol).
Secrets
mqtt_user / mqtt_pw are read from the environment variables FGC_MQTT_USER / FGC_MQTT_PW first,
falling back to the config file. Keep credentials out of config.ini (which is gitignored anyway) by exporting
them or using a systemd EnvironmentFile.
Command-line flags
Parsed by Boost.Program_options (main.cpp). Flags override [Features].
| Flag | Effect |
|---|---|
-h, --help |
Show help and exit |
-c, --config <path> |
Explicit config file path |
-i, --init |
Run the endstop-finding init sequence before the loop |
-s, --start |
Start capture automatically |
-d, --demo |
Demo mode: copy the placeholder image instead of encoding |
--no-mqtt |
Disable MQTT (use the null channel) |
--mock-camera |
Use the simulated camera |
--mock-serial |
Use the simulated motor controller |
--tui |
Show the full-screen terminal dashboard (overrides [UI] enable_tui) |
--no-tui |
Force the headless line console (overrides config; wins over --tui) |
--log-level <lvl> |
trace/debug/info/warn/error/off |
--trace <cats> |
Verbatim wire trace; comma list serial,mqtt,camera,control,all,none |
Typical headless dev run: scripts/run.sh --mock-serial --mock-camera --no-mqtt --start.
Init sequence (--init)
Sends ENABLE Y, ENABLE P, HOME, then polls telemetry until both axes report READY
(firmware ST state A), bounded by the firmware's 60 s homing timeout, then sets SPEED Y/P
(src/core/Application.cpp, runInitSequence).
Interactive console commands
Lines on stdin are parsed by parseCommand (src/core/CommandParser.cpp) — a
whitespace tokenizer (<verb> [device] [option] [value]) that replaced the old fragile Boost.Spirit grammar.
Handled in Application::Impl::handleCommand.
| Type this | Meaning |
|---|---|
start |
Start camera acquisition + capture |
stop |
Stop acquisition |
debug |
Toggle debug logging |
trace <cat> [on|off] |
Toggle a wire-trace category (serial/mqtt/camera/control) |
trace all / trace off |
Enable every category / silence all |
set camera jxlq <v> |
JPEG XL distance (0 = lossless) |
set camera jxle <v> |
JPEG XL effort |
set camera display <0|1> |
Toggle OpenCV preview window |
set camera fps <v> |
Camera acquisition frame rate (real camera only) |
set fps <v> |
Capture interval rate (images/second) |
set motorctl <cmd> |
Forward a raw command to the motor controller (e.g. set motorctl MOVE Y 20000) |
exit |
Quit (Ctrl-D also works) |
Terminal dashboard (TUI)
An optional full-screen interface (--tui, or [UI] enable_tui = true) renders the tower
status as sectioned, colored panels updated in place, with a scrolling log pane and a nano-style
key bar. It is built on FTXUI (fetched via
cmake/Ftxui.cmake when WITH_TUI=ON, the default) and is fully decoupled
from application logic: the control loop publishes a plain UiSnapshot
(include/fgc/ui/UiSnapshot.h) that the UI renders, and the UI
forwards keystrokes/typed commands back through the same command queue the console uses. Headless
operation is unchanged and remains the default — the same binary runs under systemd/ssh/pipes with
logs on stdout.
Dashboard panels: Gimbal (per-axis state, heading, encoder counts, flag badges, target),
Sensors (DHT11 still pending; the Xsens MTi shows live roll/pitch/yaw + temp once
enable_imu), Camera (count, capture state, rate, last capture), Connectivity (MQTT state,
broker, tower, control mode, target heading). Adding a panel later is a struct in UiSnapshot.h plus
one node in src/ui/TuiUi.cpp.
Expanded takeover views replace the dashboard body full-screen (Esc or the same key closes):
- Gimbal (
g) — both axes side by side: live telemetry, the decoded firmware register dump (drequests a fresh one), the homing limits, and the last calibration result (per-axiscounts_per_deg/zero_count/ R² / age). - IMU (
i) — every MTi channel with units: orientation (°), acceleration (m/s²), rate-of-turn (rad/s), magnetic field (a.u.), temperature, sample counter.
Activity strip — a compact section between the log and the key bar that shows the
currently-running special operation with live progress (gimbal calib, gimbal diag, homing, capture
scan) and persists the last calibration/diagnostics result (PASS/FAIL, age) so it doesn't scroll
away in the log. It only appears once something has run.
When a gimbal calib completes it is applied to the live session and the strip shows a highlighted
yes/no prompt — Save this calibration to config as the new default? (y / n). Press y to
write the fitted [Motor] *_counts_per_deg / *_zero_count back into the config.ini the program
was launched with (replacing those keys in place, preserving everything else) so it persists across
restarts; press n to keep it for this session only. The keys are active only while the prompt is
showing. (Until you answer y, calibration remains session-only — see
known-issues.md.)
Keys (bottom bar): s start · x stop · h home · r reset · g gimbal view · i IMU view ·
arrow keys nudge the gimbal (yaw ±5 % / pitch ±10 %) · : open a command line (any console/gimbal …
command) · ? help · q quit. Plain letters are used rather than Ctrl chords so terminal
flow-control (Ctrl-S/Ctrl-Q XON/XOFF) can't swallow them. In TUI mode all log output is diverted
from stdout into the on-screen log pane via a Logger sink, so the screen is never corrupted.
Build without it (-DWITH_TUI=OFF) for a smaller, dependency-free binary; --tui then warns and
runs headless.
Logging: level vs. wire-trace categories
Two independent controls, each settable via config ([Logging]), CLI, and a console command:
- Linear level —
[Logging] level,--log-level, consoledebug. Filters ordinary messages (trace<debug<info<warn<error<off). Defaultinfo; an active capture is nearly silent atinfo. - Wire-trace categories —
[Logging] trace,--trace, consoletrace. Each enabled category (serial,mqtt,camera,control) logs every message exchanged with that subsystem, verbatim. They are off by default and independent of the level:--trace serialprints all firmware serial traffic even atinfo. Only leveloffsilences them. Thecameracategory is high-rate (one line per frame, metadata only — no pixel data).
Precedence at startup: config applies first, then CLI overrides (--trace replaces the config set,
it does not merge). At runtime the trace console command edits categories incrementally.
Trace lines carry a category tag and a TX/RX direction so one subsystem is easy to follow/grep:
[SERIAL] TX MOVE -90,0 (command to firmware: yaw,pitch counts)
[SERIAL] RX ST Y:A,982,969,80084000,0,8,8,Se P:A,... (status from firmware)
[MQTT] PUB GGS/FWT/Tower/StatusCode 0 / [MQTT] RX GGS/FWT/Tower/target_HDG 180
[CAMERA] TX trigger cam0 / [CAMERA] RX frame cam0 1936x1216 7064576B
[CONTROL] sweep -> grid yaw=-90 pitch=0 (scheduler decisions + inbound console commands)
Motor command vocabulary (emitted by the software)
The firmware speaks full-word, newline-terminated commands in absolute encoder counts
(see ../firmware/docs/protocol.md). The host converts degrees↔counts via the [Motor] calibration.
| Command | When | Meaning |
|---|---|---|
ENABLE Y / ENABLE P |
init | energize the axis coils |
HOME |
init | home all axes (firmware runs it non-blocking; watch ST state R→H→A) |
SPEED Y|P <vel> |
init | set the production move speed (VMAX, counts/s) |
MOVE <yaw>,<pitch> |
each capture point | drive both axes to absolute counts (combined form) |
MOVE Y|P <pos> |
— | single-axis absolute move (set motorctl) |
STOP Y|P|ALL |
— | ramp to a controlled stop |
Capture is a move → settle → trigger cycle: the scheduler issues a MOVE, waits until both
axes report standstill at the target, then triggers the cameras. ControlCode 0 walks the scan
grid (ping-pong); ControlCode 1 drives yaw to target_HDG (pitch held). Telemetry arrives as
firmware ST lines (per-axis state + encoder counts), parsed by
TelemetryParser.