12 KiB
Deploying to the Gimbal LattePanda
Step-by-step guide to deploy fire_gimbal_control on a tower's LattePanda Sigma (Linux x86-64). Like the
firmware workflow, this is a repeatable build → install → configure → enable → verify cycle; the
Update / redeploy section at the end is the short loop you run for every new version.
Assumes a Debian/Ubuntu-based Linux on the LattePanda. For Arch/Manjaro swap
aptforpacman(see build-and-setup.md). Run as a normal user withsudorights; the service runs under a dedicatedggsuser.
0. Prerequisites (one-time per device)
Install the toolchain and system libraries:
sudo apt update
sudo apt install -y git cmake build-essential \
libopencv-dev libjxl-dev libboost-all-dev
Install the Allied Vision Vimba X SDK (proprietary, needed for the cameras):
- Download Vimba X for Linux x86-64 from https://www.alliedvision.com/en/products/software/vimba-x-sdk/.
- Unpack to a fixed location, e.g.
/opt/VimbaX_2024-1. - Run the SDK's transport-layer installer so cameras are discoverable, e.g.:
sudo /opt/VimbaX_2024-1/cti/Install_GenTL_Path.sh - Make its runtime libraries findable at run time:
echo /opt/VimbaX_2024-1/api/lib | sudo tee /etc/ld.so.conf.d/vimbax.conf sudo ldconfig
Ensure an MQTT broker is reachable from the tower (the ground-station/ZKMS broker, or a local Mosquitto for bring-up).
1. Get the code
sudo mkdir -p /opt/src && sudo chown "$USER" /opt/src
cd /opt/src
git clone <repo-url> fire_gimbal_control_src
cd fire_gimbal_control_src
# (or: git pull, for an update)
2. Build (full, with cameras + MQTT)
cmake -B build -DCMAKE_BUILD_TYPE=Release \
-DWITH_VIMBA=ON -DWITH_MQTT=ON \
-DVMB_HOME=/opt/VimbaX_2024-1
cmake --build build -j"$(nproc)"
WITH_MQTT=ONfetches + builds Eclipse Paho from source on first configure (needs network).- If a camera build isn't needed yet, use
-DWITH_VIMBA=OFFfor a mock binary to test the pipeline. - Smoke-test the core before installing:
ctest --test-dir build(after adding-DBUILD_TESTING=ON).
3. Install to a fixed location
sudo useradd -r -s /usr/sbin/nologin ggs 2>/dev/null || true
sudo mkdir -p /opt/fire_gimbal_control
sudo cp build/fire_gimbal_control /opt/fire_gimbal_control/
# demo-mode placeholder image (only needed if you ever run --demo)
sudo cp bin/x64/Release/test_smoke.jxl /opt/fire_gimbal_control/ 2>/dev/null || true
4. Configure
sudo cp config/config.example.ini /opt/fire_gimbal_control/config.ini
sudo nano /opt/fire_gimbal_control/config.ini
Set at least:
[General]
tower_name = ExampleTower ; this tower's name (drives all MQTT topics)
image_interval = 3
[Network]
zkms_server_ip = 10.11.12.13 ; the broker address
[Serial]
device = /dev/ttyACM0 ; the motor-controller MCU
baud = 115200
[Camera]
id_Cam1 = DEV_1AB22C0AADED ; or GigE IPs like 192.168.11.101
Credentials go in the environment, not the config file. Create a root-only env file for the service:
sudo install -m 600 /dev/null /etc/fire_gimbal_control/credentials.env 2>/dev/null || \
{ sudo mkdir -p /etc/fire_gimbal_control && sudo install -m 600 /dev/null /etc/fire_gimbal_control/credentials.env; }
printf 'FGC_MQTT_USER=fwt_gimbal\nFGC_MQTT_PW=CHANGE_ME\n' | sudo tee /etc/fire_gimbal_control/credentials.env >/dev/null
sudo chmod 600 /etc/fire_gimbal_control/credentials.env
Give the ggs user access to its files and the image output dir:
sudo chown -R ggs:ggs /opt/fire_gimbal_control
5. Hardware access
Serial (motor controller): let the service user open the port.
sudo usermod -aG dialout ggs
For a stable device name across reboots, add a udev rule keyed on the MCU's USB IDs (find them with
udevadm info -a -n /dev/ttyACM0 | grep -m1 idVendor) and point [Serial] device at the symlink:
echo 'SUBSYSTEM=="tty", ATTRS{idVendor}=="XXXX", ATTRS{idProduct}=="YYYY", SYMLINK+="ttyGimbal"' | \
sudo tee /etc/udev/rules.d/99-gimbal.rules
sudo udevadm control --reload && sudo udevadm trigger
GigE cameras (if used): put the host NIC on the camera subnet (e.g. 192.168.11.0/24), enable jumbo
frames, and confirm discovery with the Vimba X VimbaXViewer/ListCameras tool.
6. First run by hand (verify before enabling the service)
cd /opt/fire_gimbal_control
set -a; . /etc/fire_gimbal_control/credentials.env; set +a
./fire_gimbal_control --config ./config.ini --start --log-level debug
Watch for: Loaded config, Serial controller started, Opened camera ..., MQTT connected, then
Saved .../RGB/<timestamp>.jxl after the first capture. Type exit (or Ctrl-D) to stop. To validate without
cameras first: add --mock-camera --mock-serial --no-mqtt.
This run is attached to your terminal and dies with the ssh session — fine for a first check, not for leaving it running. For that, use section 6b.
6b. Running detached (scripts/fgc)
The tower is operated over ssh, so the program has to survive the connection closing. scripts/fgc runs
it inside a tmux session, which owns the pty:
cd /opt/fire_gimbal_control
scripts/fgc start --start # starts detached, returns as soon as it is up
scripts/fgc status # running? since when? which PID?
scripts/fgc attach # the dashboard — Ctrl-b d detaches, leaves it running
scripts/fgc stop # SIGTERM -> the same clean teardown as `exit`
scripts/fgc peek # snapshot of the pane without attaching
Ctrl-b d detaches. The dashboard's own q key exits the program. That is the one thing to get
right while attached.
scripts/fgc stop sends SIGTERM and waits up to 30s; the program handles it with the same shutdown path
as the exit command (stop capture, close the camera, save the exposure store). Do not kill -9 —
an unclean exit leaves the camera wedged for the next run. If it will not stop, attach and look at why.
scripts/fgc start refuses if a run is already up, and prints the previous run's last output if it died
rather than silently discarding it.
Quitting the dashboard with q while attached ends the tmux session too, so you land straight back in
your shell — exit then closes ssh. A run that fails is the exception: its pane is kept so you can read
why (remain-on-exit failed), and fgc status shows it until fgc stop or the next fgc start clears it.
A kept pane has no process behind it — keys and Ctrl-C do nothing, which reads as a hung ssh session.
Ctrl-b d still detaches (tmux, not the pane, handles it); the session's status line spells that out along
the bottom of the screen. As a last resort Enter ~ . closes the ssh client itself from your end.
Why
_runwaits on the app instead ofexecing it. When the app is itself the pane's process, tmux never records an exit status for it —#{pane_dead_status}comes back empty and the process lingers as a zombie — soremain-on-exit failedcannot tell success from failure and keeps every finished pane, including a cleanq. That is what used to strand an operator on a frozen dead pane. Interposing a shell that reaps the child and exits with its status gives tmux a real status. Measured on the device: 3/3 panes kept when exec'd, 4/4 sessions closed cleanly when parented. The wrapper forwards SIGTERM/SIGINT/SIGHUP, sofgc stopstill reaches the app and runs the full teardown.
6c. The single-instance lock
Two instances would fight over the motor's serial port, the camera, and the exposure store. The binary
prevents that itself: at startup it takes an exclusive flock on
/run/user/<uid>/fire_gimbal_control.lockwhen that directory exists, else/tmp/fire_gimbal_control-<uid>.lock
and, if it is already held, exits with status 2 and:
Cannot start: another instance is already running (PID 1234); attach to it with: scripts/fgc attach
The path is derived from the uid and not from $XDG_RUNTIME_DIR on purpose: that variable is set in an
interactive ssh shell but not in a non-interactive ssh host cmd one, and a lock the two launch routes
disagree about would exclude nothing.
Because the check lives in the binary, it covers every route in — scripts/fgc, systemd, or a bare
./fire_gimbal_control. Nothing needs cleaning up after a crash or a power cut: the kernel releases an
flock when the holder dies, so a leftover lockfile with a dead PID in it never blocks a launch. Use
--no-lock only to run a second mock-only instance during development, and --lock-file <path> to
point at a different lockfile.
7. Install the systemd service
An alternative to section 6b, not a companion to it: systemd gives you start-at-boot and
restart-on-crash, but no attachable dashboard — you get journalctl instead. Pick one. If you run both by
accident the second one to start fails on the single-instance lock (section 6c), so the failure is loud
rather than a pair of processes corrupting each other's state.
sudo cp scripts/fire-gimbal-control.service /etc/systemd/system/
sudoedit /etc/systemd/system/fire-gimbal-control.service
Confirm/adjust these lines:
User=ggs
WorkingDirectory=/opt/fire_gimbal_control
ExecStart=/opt/fire_gimbal_control/fire_gimbal_control --start
Environment=FGC_CONFIG=/opt/fire_gimbal_control/config.ini
EnvironmentFile=/etc/fire_gimbal_control/credentials.env
(Use ExecStart=... --init --start for the first power-on if endstops must be found at boot.)
8. Enable and verify
sudo systemctl daemon-reload
sudo systemctl enable --now fire-gimbal-control
systemctl status fire-gimbal-control
journalctl -u fire-gimbal-control -f # live logs
Confirm end to end:
- Images accumulating under the configured
[Paths] output_dir(default~ggs/.local/share/fire_gimbal_control/images/RGB/). - CamEvents on the broker:
mosquitto_sub -h <broker> -t 'GGS/FWT/<tower>/#' -v.
9. Update / redeploy
The short loop for each new version (mirrors the firmware flash cycle):
cd /opt/src/fire_gimbal_control_src
git pull
cmake --build build -j"$(nproc)" # reuses the configured build dir
sudo systemctl stop fire-gimbal-control
sudo cp build/fire_gimbal_control /opt/fire_gimbal_control/
sudo systemctl start fire-gimbal-control
journalctl -u fire-gimbal-control -n 50 -f # confirm healthy
If CMakeLists.txt or options changed, re-run the cmake -B build ... configure step from
section 2 first. To roll back, keep the previous binary
(fire_gimbal_control.prev) and swap it back.
Troubleshooting
| Symptom | Check |
|---|---|
No config.ini found |
FGC_CONFIG / --config path; service Environment= line |
Failed to open serial port |
MCU plugged in, dialout group, [Serial] device path / udev symlink |
No camera found / Vimba startup error |
SDK installed, GenTL path set, ldconfig, NIC subnet, camera IDs in config |
MQTT connect failed (degraded mode) |
broker reachable, credentials env, firewall |
| Runs but no images | capture started (--start or start command), output_dir writable by ggs |
another instance is already running (exit 2) |
scripts/fgc status; a systemd unit and a tmux session both running (section 6c) |
| Program dies when ssh disconnects | started with run.sh/--foreground instead of scripts/fgc start (section 6b) |
No camera found only when started via scripts/fgc |
GenTL path not resolved in the pane — check scripts/fgc peek, and /etc/profile.d/*Vimba*GenTL*.sh or VIMBA_CTI_PATH |