fwt_software/deploy.sh

204 lines
8.5 KiB
Bash
Executable File

#!/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