# shellcheck shell=bash # # kit.sh - what every board's setup kit shares. Sourced, never run: each # hardware//install.sh sets the KIT_* facts below and calls kit_main "$@". # # The facts a kit must set before sourcing: # KIT_BOARD lattepanda-iota | lattepanda-3-delta (matches probe_board) # KIT_FW_ENV the PlatformIO env for this board's MCU # KIT_MCU_VID USB vendor of the motor MCU (2e8a RP2040, 2341 Arduino) # KIT_DEFAULT_AXES what this board's towers are usually built with # KIT_IMU_DEVICE where an IMU normally is on this board # KIT_IMU_NOTE one comment line on that # KIT_ENV_DEVICE the I2C bus an ambient sensor normally uses, or "" if none # KIT_ENV_NOTE comment lines on that (newline-separated) # KIT_DIR the kit's own directory (packages.txt, preconditions/, pins.ini) # # 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 one # look identical to a probe - and every drivetrain value, which is written as a # commented `; TODO` naming what to measure when it is not given. It never # invents one: a wrong sense resistor cooks a driver, and a wrong homing current # shears an endstop. Both have happened on this project. ETC_DIR=/etc/fire_gimbal_control HW_FILE="$ETC_DIR/hardware.ini" 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"; } # The per-axis keys this kit writes, in file order. Every one is a fact about the # drivetrain that no probe can read. KIT_AXIS_KEYS=(r_sense_ohm run_current_ma homing_current_ma travel_deg steps_per_rev sw_mode enc_deviation has_encoder) # Keys with a firmware default that fits most towers: written only when given, # never a TODO. enc_const: a legacy tower's encoder scale (staeffelsberg 12.5). KIT_AXIS_OPTIONAL=(enc_const) # What to measure, per key - the text of each `; TODO`. kit_axis_todo() { case "$1" in r_sense_ohm) echo "the sense resistor fitted to THIS axis's driver board - read it off the board, do not assume" ;; run_current_ma) echo "coil current for normal motion, mA RMS, from the motor's nameplate" ;; homing_current_ma) echo "current while hunting an endstop. LOWER than run_current wherever the stop can be out-pushed: on tower 121 a screw-head stop was sheared straight past its mate at full torque" ;; travel_deg) echo "mechanical angle between the hard stops. Rough is fine to start; every bearing inherits its error proportionally until 'gimbal heading setup' supersedes it" ;; steps_per_rev) echo "measured full travel IN COUNTS - read it off a homing run. The only guard against a PHANTOM endstop" ;; sw_mode) echo "TMC SW_MODE. 0 = no physical switches (StallGuard homing); set the bits if this axis has REFL/REFR wired" ;; enc_deviation) echo "|XACTUAL - XENC| that means 'position untrustworthy'. Measure the free-running gap on THIS axis and set ~4-5x it (tower 121: gap ~200, set 1000). Re-measure whenever the homing current changes" ;; has_encoder) echo "true if an encoder is fitted to this axis and aligned to XACTUAL (usable for hold and DIAG), else false" ;; enc_const) echo "microsteps per encoder count (the TMC's ENC_CONST); leave out for the default 12.8. A legacy tower's own firmware names it (staeffelsberg: 12.5)" ;; esac } # ---------------------------------------------------------------- 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 are board-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 these towers have 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" # the same, running a legacy tower's firmware : "Unknown_LattePanda_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") ;; # The Delta's onboard Leonardo, as a legacy tower's firmware names it. *LattePanda_Leonardo*) 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 "" } # The camera's USB3 port node, whose usb3_lpm_permit must read 0. `test hardware # camera` needs this path in [Test] usb3_lpm_path to check it. probe_camera_lpm_path() { local p for p in /sys/bus/usb/devices/*/port/usb3_lpm_permit; do [[ -r $p ]] || continue local dev; dev="$(basename "$(dirname "$(dirname "$p")")")" [[ "$(cat "/sys/bus/usb/devices/$dev/idVendor" 2>/dev/null)" == 1ab2 ]] && { echo "$p"; return; } done echo "" } probe_report() { local board mcu cam lpm board="$(probe_board)"; mcu="$(probe_mcu)"; cam="$(probe_camera_id)"; lpm="$(probe_camera_lpm_path)" 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)" [[ -n $lpm ]] && echo "camera lpm_permit: $(cat "$lpm") (want 0) $lpm" echo "i2c buses : $(ls /dev/i2c-* 2>/dev/null | tr '\n' ' ')" echo "ttyS : $(ls /dev/ttyS[0-9] 2>/dev/null | tr '\n' ' ')" } # ------------------------------------------------------------------- deps --- kit_packages() { local line while read -r line; do line="${line%%#*}"; line="$(echo "$line" | tr -d '[:space:]')" [[ -n $line ]] && echo "$line" done < "$KIT_DIR/packages.txt" } do_deps() { need_root "--deps" local pkgs; mapfile -t pkgs < <(kit_packages) say "installing ${#pkgs[@]} packages" [[ $APT_UPDATE -eq 1 ]] && apt-get update DEBIAN_FRONTEND=noninteractive apt-get install -y "${pkgs[@]}" say "packages done" } # ---------------------------------------------------------- preconditions --- # Everything in the kit's preconditions/: *.rules go to udev, *.conf to tmpfiles. kit_precondition_targets() { local f for f in "$KIT_DIR"/preconditions/*; do case "$f" in *.rules) echo "$f /etc/udev/rules.d/$(basename "$f")" ;; *.conf) echo "$f /etc/tmpfiles.d/$(basename "$f")" ;; esac done } do_preconditions() { need_root "--preconditions" local src dst n=0 while read -r src dst; do install -m 0644 "$src" "$dst"; n=$((n + 1)) done < <(kit_precondition_targets) say "installed $n precondition file(s)" udevadm control --reload udevadm trigger --subsystem-match=usb --action=add local conf for conf in "$KIT_DIR"/preconditions/*.conf; do [[ -e $conf ]] && systemd-tmpfiles --create "/etc/tmpfiles.d/$(basename "$conf")" done 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 lpm; lpm="$(probe_camera_lpm_path)" # The rule acts when the camera is (re)plugged, and a udev trigger does not # always replay it (on staeffelsberg it did not): write what it would write. if [[ -n $lpm && "$(cat "$lpm")" != 0 ]]; then echo 0 > "$lpm" 2>/dev/null || true; fi if [[ -n $lpm ]]; then [[ "$(cat "$lpm")" == 0 ]] && say "camera USB3 LPM disabled ($lpm)" \ || warn "camera USB3 LPM still $(cat "$lpm") at $lpm" else warn "no Alvium camera on the bus - LPM rule untested until one is plugged in" fi # The MCU has to be writable for flashing over ssh; check it the same way # rather than trusting that the rule took. local mcu_dev # `|| true`: an MCU with another vendor id (the LattePanda's own Leonardo, # 3343) finds nothing here, and under set -e -o pipefail that ended the whole # kit - and the installer with it - without a word. mcu_dev="$(grep -l "^$KIT_MCU_VID\$" /sys/bus/usb/devices/*/idVendor 2>/dev/null | head -1 || true)" if [[ -n $mcu_dev ]]; then local node node="$(printf '/dev/bus/usb/%03d/%03d' "$(cat "$(dirname "$mcu_dev")/busnum")" \ "$(cat "$(dirname "$mcu_dev")/devnum")")" if [[ -w $node ]] || [[ $KIT_MCU_VID == 2341 ]]; then say "MCU ($KIT_MCU_VID) at $node" else warn "MCU ($KIT_MCU_VID) at $node is not writable; flashing will need sudo" ls -l "$node" >&2 fi else warn "no $KIT_MCU_VID 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 (USB stick, or rsync from a machine that # has it), then point this at the staged directory. 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. The SDK is system software. 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, 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 --- # The centre lives in config.ini, not here - it is calibration, and hardware.ini # is merged OVER config.ini, so a shared key would silently discard whatever # `gimbal heading setup` had saved. All this file can do is say what the value # has to be, which it can work out from the travel: the range is centre +/- half # the travel, so a centre below half-travel puts the negative end under zero. kit_center_for() { local steps="$1" echo $(( ((steps / 2) / 1000000 + 1) * 1000000 )) } center_note() { local steps="$1" ax="$2" [[ -z $steps ]] && return 0 local half=$(( steps / 2 )) # Only when it matters: the 500000 default is fine for any travel under a # million counts, and a note demanding a change nobody needs gets acted on. (( half >= 500000 )) || return 0 echo "; NOTE: half this travel is $half counts, so config.ini needs" echo "; [Motor] ${ax}_center_count = $(kit_center_for "$steps") (or anything above $half)" echo "; The 500000 default would put the negative end at $(( 500000 - half ))." } # Emit "key = value", or a commented-out line naming what has to be measured. # # Commented rather than a placeholder string on purpose: these keys are numeric, # and a non-numeric value makes the program refuse to start with a message about # a config key. Absent, it starts and reports the axis as undeclared - naming the # missing keys and which axis will not move, which is the useful failure. kv_or_todo() { local key="$1" value="$2" why="$3" if [[ -n $value ]]; then printf '%-18s = %s\n' "$key" "$value" return fi # Wrapped, and not merely for looks: ini.h sets INI_MAX_LINE to 200, and a # longer line is SPLIT rather than truncated, so its tail becomes a bogus # line and the whole file fails to parse. printf '; TODO %s\n' "$key" printf '%s\n' "$why" | fold -s -w 72 | sed 's/^/; /' | sed 's/[[:space:]]*$//' } # A loom file's lines, keeping only the pins of what this tower declares: a # yaw-only tower drops the pitch lines, one without the DHT11 or fan drops # theirs. The firmware build refuses a pin for an undeclared part, so copying a # loom whole stopped every yaw-only install on the standard Delta loom. kit_loom_file() { case "$1" in standard) echo "$KIT_DIR/pins.ini" ;; legacy) echo "$KIT_DIR/pins-legacy.ini" ;; *) echo "" ;; esac } emit_loom() { local file="$1" axes="$2" env="$3" cooling="$4" awk -v axes=",${axes// /}," -v env="$env" -v cooling="$cooling" ' /^[[:space:]]*[;#]/ || /^[[:space:]]*\[/ || !index($0, "=") { print; next } { k = substr($0, 1, index($0, "=") - 1); gsub(/[[:space:]]/, "", k) if (k ~ /^pitch_/ && index(axes, ",pitch,") == 0) next if (k == "dht" && env != "dht11") next if (k ~ /^fan_/ && cooling != "fan") next print }' "$file" } # `--pin k=v` values as a [Pins] block, or one of the kit's looms, or a TODO. emit_pins() { if [[ -n $LOOM ]]; then local file; file="$(kit_loom_file "$LOOM")" [[ -r $file ]] || die "--loom $LOOM: this kit has no $(basename "${file:-pins-$LOOM.ini}")" emit_loom "$file" "$AXES" "$ENV_SENSOR" "$COOLING" return fi if (( ${#PINS[@]} > 0 )); then echo "; How the MCU is wired to each driver board. The firmware is BUILT from this" echo "; (firmware/tools/gen_wiring.py); the reference loom is hardware/$KIT_BOARD/pins.ini." echo "[Pins]" local k for k in "${PIN_ORDER[@]}"; do printf '%-8s = %s\n' "$k" "${PINS[$k]}"; done return fi echo "; TODO [Pins]: the driver wiring. The firmware build REFUSES to run without it." echo "; Copy hardware/$KIT_BOARD/pins.ini here if this tower follows that loom." } emit_hardware_ini() { local board mcu cam board="$(probe_board)" mcu="${SERIAL_DEVICE:-$(probe_mcu)}" cam="${CAMERA_ID:-$(probe_camera_id)}" if [[ $(wc -l <<< "$mcu") -gt 1 ]]; then warn "several candidate MCU ports found; pick one with --serial:" warn "$mcu" mcu="CHOOSE_ONE" fi cat <] block per axis. The firmware holds none of these itself - # it asks, and refuses to move until answered - so an axis without a block # here is an axis that will not turn. local ax key for ax in ${AXES//,/ }; do ax="$(echo "$ax" | tr -d '[:space:]')" [[ -z $ax ]] && continue echo echo "[Motor.$ax]" for key in "${KIT_AXIS_KEYS[@]}"; do kv_or_todo "$key" "${MOTOR[$ax.$key]:-}" "$(kit_axis_todo "$key")" [[ $key == steps_per_rev ]] && center_note "${MOTOR[$ax.$key]:-}" "$ax" done for key in "${KIT_AXIS_OPTIONAL[@]}"; do [[ -n ${MOTOR[$ax.$key]:-} ]] && printf '%-18s = %s\n' "$key" "${MOTOR[$ax.$key]}" done cat <<'EOF' ; Speeds are DEGREES PER SECOND, not VMAX: a register value means different ; things on different gear ratios, so it is not portable between towers. These ; are deliberately gentle starting points; the host converts them per tower. scan_speed_deg_s = 8.0 homing_speed_deg_s = 4.0 center_speed_deg_s = 6.0 EOF done local c for c in ${CAMERAS//,/ }; do c="$(echo "$c" | tr -d '[:space:]')" [[ -z $c || $c == none ]] && continue 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" if grep -n 'SET_THIS_BY_HAND\|CHOOSE_ONE\|^; TODO' "$HW_FILE" >/dev/null 2>&1; then warn "it still has gaps - fill them before starting the app:" grep -n 'SET_THIS_BY_HAND\|CHOOSE_ONE\|^; TODO' "$HW_FILE" | sed 's/^/ /' >&2 fi return 0 } # ------------------------------------------------------------------- main --- kit_usage() { cat <