#!/usr/bin/env bash # # deploy.sh — sync this project to the LattePanda and build/run it there. # The LattePanda is the machine wired to the gimbal + camera, so the actual # cmake build (and the live run) happen remotely over ssh. We edit locally and # mirror. Mirrors ../firmware/deploy.sh, but for the CMake/C++ side. # # Usage: # ./deploy.sh rsync + remote configure + build # ./deploy.sh --run ... then run the app over ssh (RUN_ARGS, ctrl-c to stop) # ./deploy.sh --clean wipe the remote build dir first (fresh configure) # ./deploy.sh --check-deps only check the remote build dependencies # ./deploy.sh --run --force skip the placeholder-config guard before running # # Flags combine, e.g. ./deploy.sh --clean --run # # Config comes from .deploy.env (see .deploy.env.example). # set -euo pipefail cd "$(dirname "$0")" ENV_FILE=".deploy.env" if [[ ! -f "$ENV_FILE" ]]; then echo "error: $ENV_FILE not found (copy .deploy.env.example and edit it)" >&2 exit 1 fi # shellcheck disable=SC1090 source "$ENV_FILE" : "${REMOTE_HOST:?set REMOTE_HOST in .deploy.env}" : "${REMOTE_DIR:?set REMOTE_DIR in .deploy.env}" CMAKE_ARGS="${CMAKE_ARGS:--DWITH_MQTT=ON -DWITH_VIMBA=ON}" BUILD_JOBS="${BUILD_JOBS:-}" RUN_ARGS="${RUN_ARGS:---start}" CLEAN=0 RUN=0 CHECK_DEPS=0 FORCE=0 for arg in "$@"; do case "$arg" in --clean) CLEAN=1 ;; --run|--start) RUN=1 ;; --check-deps) CHECK_DEPS=1 ;; --force) FORCE=1 ;; *) echo "unknown option: $arg" >&2; exit 1 ;; esac done # Locate cmake on the remote. Non-interactive ssh shells don't source .bashrc, # so allow an explicit REMOTE_CMAKE override in .deploy.env; otherwise search # the usual locations. RESOLVE_CMAKE=" CMAKE='${REMOTE_CMAKE:-}' if [ -z \"\$CMAKE\" ]; then for c in \"\$(command -v cmake 2>/dev/null)\" /usr/bin/cmake /usr/local/bin/cmake; do if [ -x \"\$c\" ]; then CMAKE=\"\$c\"; break; fi done fi if [ -z \"\$CMAKE\" ]; then echo 'error: cmake not found on remote; set REMOTE_CMAKE in .deploy.env' >&2 exit 127 fi " # Vimba X needs the GenTL transport-layer dir on GENICAM_GENTL64_PATH at runtime, # or VmbStartup() fails with "Could not start Vimba X API". The SDK installs that # via /etc/profile.d, which non-interactive ssh shells don't source — so set it # ourselves before launching. Optional VIMBA_CTI_PATH override in .deploy.env. RESOLVE_VIMBA_ENV=" for s in /etc/profile.d/*Vimba*GenTL*.sh; do [ -f \"\$s\" ] && . \"\$s\" done if [ -z \"\${GENICAM_GENTL64_PATH:-}\" ]; then for d in '${VIMBA_CTI_PATH:-}' /opt/VimbaX/cti /opt/VimbaX_*/cti; do if [ -d \"\$d\" ]; then export GENICAM_GENTL64_PATH=\"\$d\"; break; fi done fi " # Preflight: the two heavyweight deps that aren't auto-fetched (Paho is). OpenCV # and Boost.program_options must be present or the configure step fails opaquely. CHECK_DEPS_CMD=" miss='' pkg-config --exists opencv4 2>/dev/null || miss=\"\$miss opencv4\" pkg-config --exists libjxl libjxl_threads 2>/dev/null || miss=\"\$miss libjxl\" [ -f /usr/include/boost/version.hpp ] || ls /usr/include/boost*/boost/version.hpp >/dev/null 2>&1 || miss=\"\$miss boost\" if [ -n \"\$miss\" ]; then echo \"error: missing remote build deps:\$miss\" >&2 echo 'install on the LattePanda (Ubuntu):' >&2 echo ' sudo apt-get install -y build-essential cmake git libopencv-dev libboost-program-options-dev libjxl-dev' >&2 exit 1 fi echo 'remote deps OK (OpenCV, Boost, libjxl; Vimba via cmake/FindVmb; Paho fetched)' " if [[ "$CHECK_DEPS" == "1" ]]; then echo ">> checking remote build deps on $REMOTE_HOST" # shellcheck disable=SC2029 ssh "$REMOTE_HOST" "$CHECK_DEPS_CMD" exit 0 fi # .git is excluded from rsync, so capture build identity locally and pass it on. if git rev-parse --git-dir >/dev/null 2>&1; then GIT_REV="$(git describe --always --dirty 2>/dev/null || echo nogit)" else GIT_REV="nogit" fi echo ">> build id: $GIT_REV" echo ">> ensuring remote dir exists" ssh "$REMOTE_HOST" "mkdir -p '$REMOTE_DIR'" # config.ini and images/ are device-local (runtime config + captured output) and # are deliberately NOT mirrored, so a deploy never clobbers them. build/ is # machine-specific and regenerated remotely. echo ">> syncing to $REMOTE_HOST:$REMOTE_DIR" rsync -avz --delete \ --exclude 'build/' \ --exclude '.git' \ --exclude '.venv' \ --exclude '.vscode' \ --exclude '__pycache__' \ --exclude '.deploy.env' \ --exclude 'config.ini' \ --exclude 'images/' \ ./ "$REMOTE_HOST:$REMOTE_DIR/" # Seed a config.ini on the device from the example if it doesn't have one yet, # so the first run finds a config. Never overwrites an existing device config. echo ">> ensuring remote config.ini exists" ssh "$REMOTE_HOST" "cd '$REMOTE_DIR' && \ if [ ! -f config.ini ]; then \ cp config/config.example.ini config.ini && \ echo ' seeded config.ini from config/config.example.ini — edit it on the device'; \ else echo ' keeping existing device config.ini'; fi" if [[ "$CLEAN" == "1" ]]; then echo ">> wiping remote build dir" ssh "$REMOTE_HOST" "rm -rf '$REMOTE_DIR/build'" fi echo ">> remote preflight + configure + build ($CMAKE_ARGS)" JOBS_FLAG="" [[ -n "$BUILD_JOBS" ]] && JOBS_FLAG="-j$BUILD_JOBS" || JOBS_FLAG="-j\$(nproc)" # shellcheck disable=SC2029 ssh "$REMOTE_HOST" "$RESOLVE_CMAKE $CHECK_DEPS_CMD cd '$REMOTE_DIR' && \ GIT_REV='$GIT_REV' \"\$CMAKE\" -S . -B build $CMAKE_ARGS && \ \"\$CMAKE\" --build build $JOBS_FLAG" echo ">> build complete: $REMOTE_DIR/build/fire_gimbal_control" if [[ "$RUN" == "1" ]]; then # Guard: a freshly seeded config.ini is the example template, with placeholders # that connect nowhere and silently drop to degraded mode. Refuse to launch on # placeholders (unless --force) so it's an explicit failure, not a confusing # half-run. Only enforce placeholders for subsystems this run actually uses, # mirroring Application.cpp's resolution: MQTT off if --no-mqtt, a WITH_MQTT=OFF # build, OR [Features] enable_mqtt is falsey; camera mocked if --mock-camera OR # [Features] mock_camera true / enable_camera false. CLI/build signals are # computed here; the config's own flags are parsed remotely where the file is. if [[ "$FORCE" != "1" ]]; then MQTT_OFF_CLI=0; CAM_MOCK_CLI=0 [[ "$RUN_ARGS" == *--no-mqtt* || "$CMAKE_ARGS" == *WITH_MQTT=OFF* ]] && MQTT_OFF_CLI=1 [[ "$RUN_ARGS" == *--mock-camera* ]] && CAM_MOCK_CLI=1 echo ">> checking device config.ini for unedited placeholders" # shellcheck disable=SC2029 if ! ssh "$REMOTE_HOST" " cfg='$REMOTE_DIR/config.ini' [ -f \"\$cfg\" ] || { echo \"error: \$cfg missing on device\" >&2; exit 2; } # Read an INI value (last assignment wins), strip inline ';' comment + ws, lowercase. ini_val() { grep -iE \"^[[:space:]]*\$1[[:space:]]*=\" \"\$cfg\" | tail -1 \ | sed -E 's/^[^=]*=[[:space:]]*//; s/[[:space:]]*(;.*)?\$//' | tr 'A-Z' 'a-z'; } is_false() { case \"\$(ini_val \"\$1\")\" in 0|false|no|off) return 0;; *) return 1;; esac; } is_true() { case \"\$(ini_val \"\$1\")\" in 1|true|yes|on) return 0;; *) return 1;; esac; } mqtt_on=1; [ '$MQTT_OFF_CLI' = 1 ] && mqtt_on=0; is_false enable_mqtt && mqtt_on=0 cam_real=1; [ '$CAM_MOCK_CLI' = 1 ] && cam_real=0 is_true mock_camera && cam_real=0; is_false enable_camera && cam_real=0 pat='ExampleTower' # tower identity: always [ \"\$mqtt_on\" = 1 ] && pat=\"\$pat|CHANGE_ME\" # MQTT creds: only if MQTT on [ \"\$cam_real\" = 1 ] && pat=\"\$pat|DEV_0000000000\" # camera id: only if real cam if grep -Eq \"\$pat\" \"\$cfg\"; then echo 'error: device config.ini still has example placeholders:' >&2 grep -En \"\$pat\" \"\$cfg\" | sed 's/^/ /' >&2 echo 'edit them on the device, or pass --force to run anyway.' >&2 exit 3 fi "; then echo ">> aborting run; fix the config or re-run with --force" >&2 exit 1 fi fi echo ">> running on $REMOTE_HOST (ctrl-c to stop)" # -t for a real tty so the capture loop's logs stream and ctrl-c propagates. # cwd = REMOTE_DIR so ./config.ini and the output dir resolve. # shellcheck disable=SC2029 ssh -t "$REMOTE_HOST" "$RESOLVE_VIMBA_ENV cd '$REMOTE_DIR' && exec ./build/fire_gimbal_control $RUN_ARGS" fi