#!/usr/bin/env bash # # install.sh - turn a freshly imaged LattePanda IOTA into a fire-watch tower host. # # Run it ON the device: # sudo ./install.sh # everything: deps, preconditions, hardware.ini # sudo ./install.sh --deps # apt packages only # sudo ./install.sh --preconditions # udev + tmpfiles only, applied immediately # ./install.sh --probe # print what this machine looks like, change nothing # sudo ./install.sh --vimba ~/VimbaX_2025-3 # install a staged Vimba X SDK # ./install.sh --stdout # print the hardware.ini it would write # # What it can work out for itself: the board, the motor MCU's stable by-id path, # the camera's DEV_ id, whether the USB preconditions took. What it cannot, and # takes from flags, is what was ASSEMBLED - a missing pitch axis and a broken # pitch axis look identical to a probe: # # --axes yaw (default: yaw,pitch) which axes the gimbal has # --imu none (default: none) IMU model, or none # --env none (default: none) ambient sensor model, or none # --cameras rgb (default: rgb) expected [Camera.] blocks # # Other flags: # --force overwrite an existing hardware.ini (backs it up first) # --no-apt-update skip 'apt-get update' # set -euo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" ETC_DIR=/etc/fire_gimbal_control HW_FILE="$ETC_DIR/hardware.ini" AXES="yaw,pitch" IMU="none" ENV_SENSOR="none" CAMERAS="rgb" DO_DEPS=0 DO_PRE=0 DO_HW=0 DO_VIMBA=0 PROBE_ONLY=0 TO_STDOUT=0 FORCE=0 APT_UPDATE=1 VIMBA_SRC="" EXPLICIT=0 while [[ $# -gt 0 ]]; do case "$1" in --deps) DO_DEPS=1; EXPLICIT=1 ;; --preconditions) DO_PRE=1; EXPLICIT=1 ;; --hardware-ini) DO_HW=1; EXPLICIT=1 ;; --probe) PROBE_ONLY=1; EXPLICIT=1 ;; --stdout) TO_STDOUT=1; DO_HW=1; EXPLICIT=1 ;; --axes) AXES="$2"; shift ;; --imu) IMU="$2"; shift ;; --env) ENV_SENSOR="$2"; shift ;; --cameras) CAMERAS="$2"; shift ;; --vimba) VIMBA_SRC="$2"; DO_VIMBA=1; EXPLICIT=1; shift ;; --force) FORCE=1 ;; --no-apt-update) APT_UPDATE=0 ;; -h|--help) sed -n '2,30p' "$0"; exit 0 ;; *) echo "unknown option: $1" >&2; exit 1 ;; esac shift done if [[ $EXPLICIT -eq 0 ]]; then DO_DEPS=1; DO_PRE=1; DO_HW=1; fi say() { printf '>> %s\n' "$*"; } warn() { printf '!! %s\n' "$*" >&2; } die() { printf 'error: %s\n' "$*" >&2; exit 1; } need_root() { [[ $EUID -eq 0 ]] || die "$1 needs root - re-run with sudo"; } # ---------------------------------------------------------------- probing --- # Board, from DMI. A kit applied to the wrong board is a real mistake, not a # nuisance: the pin map and the device paths below are IOTA-specific. probe_board() { local dmi=/sys/class/dmi/id/product_name [[ -r $dmi ]] || { echo "unknown"; return; } case "$(tr -d '\0' < "$dmi")" in *IOTA*|*Iota*|*iota*) echo "lattepanda-iota" ;; *"3 Delta"*) echo "lattepanda-3-delta" ;; *Sigma*) echo "lattepanda-sigma" ;; *) echo "unknown" ;; esac } # The motor MCU's stable path. /dev/ttyACM0 is enumeration order and moves when # any other USB serial device appears - and this tower has a 5G modem that # claims three ttyUSB nodes. Match on what the MCU calls itself: # RP2040 running the gimbal firmware (arduino-pico) : "Raspberry_Pi_Pico" # RP2040 as shipped, still on MicroPython : "MicroPython_Board" # ATmega32U4 (Delta fleet) : "Arduino_LLC_Arduino_Leonardo" probe_mcu() { local d found=() for d in /dev/serial/by-id/*; do [[ -e $d ]] || continue case "$d" in *Raspberry_Pi_Pico*|*MicroPython_Board*|*Arduino_LLC_Arduino*|*Pico*) found+=("$d") ;; esac done (( ${#found[@]} == 1 )) && { echo "${found[0]}"; return; } (( ${#found[@]} == 0 )) && { echo ""; return; } printf '%s\n' "${found[@]}" } # The camera's DEV_ id, which is what [Camera.] id takes. Only Vimba knows # it - it is NOT the USB serial number - so this needs the SDK installed. # # Two traps, both hit on tower 121: # - The example is called ListCameras_VmbC, not ListCameras. # - The SDK ships a Camera Simulator transport layer that is enabled by # default and reports three cameras that do not exist (DEV_Cam1..3). Ids are # therefore taken only from a block whose transport layer is the USB or GigE # TL. A tower configured with DEV_Cam1 would "find its camera" and capture # synthetic images forever. probe_camera_id() { local tool for tool in /opt/VimbaX*/bin/ListCameras_VmbC /opt/VimbaX*/bin/ListCameras; do [[ -x $tool ]] || continue # Non-interactive shells do not source /etc/profile.d, so the GenTL path # has to be set here or VmbStartup() fails with error -16 (no transport # layer) on a perfectly good installation. local cti for cti in "${GENICAM_GENTL64_PATH:-}" /opt/VimbaX/cti /opt/VimbaX_*/cti; do [[ -d $cti ]] && { export GENICAM_GENTL64_PATH="$cti"; break; } done "$tool" 2>/dev/null | awk ' /Camera ID/ { id = $NF } /Transport Layer Path/ { if (id != "" && $NF !~ /Simulator/) { print id; exit } } ' return done echo "" } probe_report() { local board mcu cam board="$(probe_board)"; mcu="$(probe_mcu)"; cam="$(probe_camera_id)" echo "board : $board" echo "os : $(. /etc/os-release && echo "$PRETTY_NAME") ($(uname -r))" echo "motor MCU : ${mcu:-NOT FOUND}" echo "camera DEV_ id : ${cam:-NOT FOUND (needs the Vimba X SDK installed)}" echo "alvium on usb : $(lsusb 2>/dev/null | grep -c '1ab2:' || true) device(s)" echo "usbfs_memory_mb : $(cat /sys/module/usbcore/parameters/usbfs_memory_mb 2>/dev/null || echo '?') (want >= 1000)" local p for p in /sys/bus/usb/devices/*/port/usb3_lpm_permit; do [[ -r $p ]] || continue local dev; dev="$(basename "$(dirname "$(dirname "$p")")")" local vid; vid="$(cat "/sys/bus/usb/devices/$dev/idVendor" 2>/dev/null || true)" [[ $vid == 1ab2 ]] && echo "camera lpm_permit: $(cat "$p") (want 0)" done echo "i2c buses : $(ls /dev/i2c-* 2>/dev/null | tr '\n' ' ')" echo "ttyS : $(ls /dev/ttyS[0-9] 2>/dev/null | tr '\n' ' ')" } # ------------------------------------------------------------------- deps --- do_deps() { need_root "--deps" local pkgs=() while read -r line; do line="${line%%#*}"; line="$(echo "$line" | tr -d '[:space:]')" [[ -n $line ]] && pkgs+=("$line") done < "$HERE/packages.txt" say "installing ${#pkgs[@]} packages" [[ $APT_UPDATE -eq 1 ]] && apt-get update DEBIAN_FRONTEND=noninteractive apt-get install -y "${pkgs[@]}" say "packages done" } # ---------------------------------------------------------- preconditions --- do_preconditions() { need_root "--preconditions" install -m 0644 "$HERE/preconditions/99-fwt-alvium-usb.rules" /etc/udev/rules.d/ install -m 0644 "$HERE/preconditions/99-fwt-rp2040.rules" /etc/udev/rules.d/ install -m 0644 "$HERE/preconditions/10-fwt-usbfs.conf" /etc/tmpfiles.d/ say "installed 2 udev rules + tmpfiles.d entry" udevadm control --reload udevadm trigger --subsystem-match=usb --action=add systemd-tmpfiles --create /etc/tmpfiles.d/10-fwt-usbfs.conf say "applied to the running system" # Report rather than assume: a rule that did not take is the whole point of # known-issues #19, and it fails quietly. local now; now="$(cat /sys/module/usbcore/parameters/usbfs_memory_mb 2>/dev/null || echo '?')" [[ $now == 1000 ]] && say "usbfs_memory_mb = $now" || warn "usbfs_memory_mb is $now, expected 1000" local p any=0 for p in /sys/bus/usb/devices/*/port/usb3_lpm_permit; do [[ -r $p ]] || continue local dev vid; dev="$(basename "$(dirname "$(dirname "$p")")")" vid="$(cat "/sys/bus/usb/devices/$dev/idVendor" 2>/dev/null || true)" [[ $vid == 1ab2 ]] || continue any=1 [[ "$(cat "$p")" == 0 ]] && say "camera USB3 LPM disabled ($dev)" \ || warn "camera USB3 LPM still $(cat "$p") on $dev" done [[ $any -eq 0 ]] && warn "no Alvium camera on the bus - LPM rule untested until one is plugged in" # The MCU rule is what makes the firmware flashable over ssh, so check it the # same way rather than trusting that the rule took. local mcu_dev mcu_dev="$(grep -l '^2e8a$' /sys/bus/usb/devices/*/idVendor 2>/dev/null | head -1)" if [[ -n $mcu_dev ]]; then local devnum busnum node busnum="$(cat "$(dirname "$mcu_dev")/busnum" 2>/dev/null)" devnum="$(cat "$(dirname "$mcu_dev")/devnum" 2>/dev/null)" node="$(printf '/dev/bus/usb/%03d/%03d' "$busnum" "$devnum")" if [[ -w $node ]]; then say "MCU (2e8a) writable at $node - picotool can flash it" else warn "MCU (2e8a) at $node is not writable; flashing will need sudo" ls -l "$node" >&2 fi else warn "no 2e8a MCU on the bus - the flashing rule is untested until one is plugged in" fi return 0 } # ------------------------------------------------------------------ vimba --- # The Vimba X SDK is proprietary and login-walled, so it cannot be fetched by a # script. Stage it on the device first (rsync it from a machine that has it), # then point this at the staged directory: # # rsync -a ~/Programs/VimbaX_2025-3 ggs@:~/ # from the dev box # sudo ./install.sh --vimba ~ggs/VimbaX_2025-3 # on the tower # do_vimba() { need_root "--vimba" [[ -d $VIMBA_SRC ]] || die "--vimba: no such directory: $VIMBA_SRC" [[ -x $VIMBA_SRC/cti/Install_GenTL_Path.sh ]] || die "--vimba: $VIMBA_SRC does not look like a Vimba X SDK (no cti/Install_GenTL_Path.sh)" local name dest name="$(basename "$VIMBA_SRC")" dest="/opt/$name" if [[ -d $dest && $FORCE -ne 1 ]]; then say "$dest already present - not re-copying (--force to replace)" else say "installing $name to $dest" rm -rf "$dest.new" cp -a "$VIMBA_SRC" "$dest.new" rm -rf "$dest" mv "$dest.new" "$dest" fi # cp -a preserves the staging copy's ownership, which is the ggs user. The # SDK is read-only system software and belongs to root. chown -R root:root "$dest" ln -sfn "$dest" /opt/VimbaX say "/opt/VimbaX -> $dest" # GENICAM_GENTL64_PATH, or VmbStartup() fails with "Could not start Vimba X # API". The SDK's own installer writes /etc/profile.d + the 1ab2 udev rule. "$dest/cti/Install_GenTL_Path.sh" # Non-interactive ssh shells do not source /etc/profile.d, which is why # deploy.sh resolves the path itself - but the loader needs the libraries # findable regardless of how the app was started. echo "$dest/api/lib" > /etc/ld.so.conf.d/vimbax.conf ldconfig say "registered $dest/api/lib with the dynamic loader" udevadm control --reload && udevadm trigger --subsystem-match=usb --action=add local id id="$(probe_camera_id)" if [[ -n $id ]]; then say "camera found: $id" else warn "no camera reported by ListCameras yet" warn "the SDK asks for a reboot before the transport layers are usable" fi return 0 } # ------------------------------------------------------------ hardware.ini --- emit_hardware_ini() { local board mcu cam fw_env board="$(probe_board)" mcu="$(probe_mcu)" cam="$(probe_camera_id)" case "$board" in lattepanda-iota) fw_env="iota_rp2040" ;; lattepanda-3-delta|lattepanda-sigma) fw_env="leonardo" ;; *) fw_env="UNKNOWN" ;; esac if [[ $(wc -l <<< "$mcu") -gt 1 ]]; then warn "several candidate MCU ports found; pick one by hand:" warn "$mcu" mcu="CHOOSE_ONE" fi cat <`. ; The IOTA exposes no PCH-side I2C on the header, so this is normally a USB-I2C ; bridge (MCP2221A), whose bus number depends on enumeration order. i2c_device = SET_THIS_BY_HAND i2c_addr = 0x44 EOF fi local c for c in ${CAMERAS//,/ }; do c="$(echo "$c" | tr -d '[:space:]')" cat <&2 return fi if [[ -f $HW_FILE ]]; then cp -p "$HW_FILE" "$HW_FILE.bak-$(date +%Y%m%d-%H%M%S)" say "backed the previous hardware.ini up" fi emit_hardware_ini > "$HW_FILE" chmod 0644 "$HW_FILE" say "wrote $HW_FILE" grep -n 'SET_THIS_BY_HAND\|CHOOSE_ONE\|UNKNOWN' "$HW_FILE" >/dev/null 2>&1 && { warn "it still has placeholders - edit them before starting the app:" grep -n 'SET_THIS_BY_HAND\|CHOOSE_ONE\|UNKNOWN' "$HW_FILE" | sed 's/^/ /' >&2 } return 0 } # ------------------------------------------------------------------- main --- board="$(probe_board)" if [[ $board != lattepanda-iota ]]; then warn "this kit is for the LattePanda IOTA; this machine reports '$board'" [[ $PROBE_ONLY -eq 1 ]] || die "refusing to apply IOTA preconditions to a $board" fi if [[ $PROBE_ONLY -eq 1 ]]; then probe_report; exit 0; fi # --stdout has to emit the file and nothing else: its whole purpose is being # redirected into one. The banner belongs on stderr in that case, or it lands in # the middle of the hardware.ini somebody just generated. if [[ $TO_STDOUT -eq 1 ]]; then do_hardware_ini exit 0 fi say "LattePanda IOTA setup kit" probe_report | sed 's/^/ /' # if/fi rather than '[[ ]] && cmd': under `set -e` a false test as the last # command of an AND list exits the script. if [[ $DO_DEPS -eq 1 ]]; then do_deps; fi if [[ $DO_VIMBA -eq 1 ]]; then do_vimba; fi if [[ $DO_PRE -eq 1 ]]; then do_preconditions; fi if [[ $DO_HW -eq 1 ]]; then do_hardware_ini; fi say "done"