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.rgb]
id = DEV_1AB22C0AADED ; or a GigE IP like 192.168.11.101
label = RGB
profile_dir = config/camera_profiles/rgb
profile = standard
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 opens an exit prompt offering q (quit the
program) or b (background — detach and leave it running, the same as Ctrl-b d), with Esc to
cancel. b is offered only inside a scripts/fgc session; outside one there is nothing to detach
into and it is shown dimmed.
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, flush the image queue). 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.
Choosing quit at that prompt 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 and the camera. 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 |