Add an installer that sets up a tower from scratch or repairs one
This commit is contained in:
parent
86f930a671
commit
32ea820877
|
|
@ -218,6 +218,7 @@ echo ">> syncing to $REMOTE_HOST:$REMOTE_DIR"
|
||||||
rsync -avz --delete \
|
rsync -avz --delete \
|
||||||
--exclude 'build*/' \
|
--exclude 'build*/' \
|
||||||
--exclude '.git' \
|
--exclude '.git' \
|
||||||
|
--exclude '.revision' \
|
||||||
--exclude '.venv' \
|
--exclude '.venv' \
|
||||||
--exclude '.vscode' \
|
--exclude '.vscode' \
|
||||||
--exclude '__pycache__' \
|
--exclude '__pycache__' \
|
||||||
|
|
@ -230,6 +231,11 @@ rsync -avz --delete \
|
||||||
--exclude 'images/' \
|
--exclude 'images/' \
|
||||||
./ "$REMOTE_HOST:$REMOTE_DIR/"
|
./ "$REMOTE_HOST:$REMOTE_DIR/"
|
||||||
|
|
||||||
|
# .git is not synced, so leave the revision beside the tree for the tools that
|
||||||
|
# run THERE and need to know what they are building (install-tower, flash.sh).
|
||||||
|
# shellcheck disable=SC2029
|
||||||
|
ssh "$REMOTE_HOST" "printf '%s\\n' '$GIT_REV' > '$REMOTE_DIR/.revision'"
|
||||||
|
|
||||||
# Seed a config.ini on the device from the example if it doesn't have one yet,
|
# 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.
|
# so the first run finds a config. Never overwrites an existing device config.
|
||||||
# Skipped entirely under --push-config: the rsync above just placed one, and the
|
# Skipped entirely under --push-config: the rsync above just placed one, and the
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,20 @@ actually been exercised and which are still on paper.
|
||||||
> the board's preconditions, the Vimba X SDK, `hardware.ini`, and the motor
|
> the board's preconditions, the Vimba X SDK, `hardware.ini`, and the motor
|
||||||
> firmware.
|
> firmware.
|
||||||
|
|
||||||
|
> **`scripts/install-tower` now does the steps below**, on the tower, in this
|
||||||
|
> order: it surveys the machine, asks for what it cannot work out (every
|
||||||
|
> question can also be answered with `--answer KEY=VALUE`), shows the plan, and
|
||||||
|
> applies it after one yes. `--dry-run` shows the plan and changes nothing;
|
||||||
|
> `--verify` runs the checks only. It never invents a drivetrain value: answer
|
||||||
|
> `unknown` and it writes a `; TODO`, and refuses to flash firmware an axis of
|
||||||
|
> which would then sit in `NEEDCFG`. Homing is offered only at the end, and only
|
||||||
|
> to someone who says they are at the rig.
|
||||||
|
>
|
||||||
|
> `[verified]` as a re-run on the already-configured tower 121 (2026-09-23): it
|
||||||
|
> changed only what was out of date and passed `test hardware`. `[untested]` on a
|
||||||
|
> freshly imaged board - the fresh path has been rehearsed with `--dry-run` only.
|
||||||
|
> What follows stays the reference for what each step does and why.
|
||||||
|
|
||||||
**Status legend** — each step says where it stands:
|
**Status legend** — each step says where it stands:
|
||||||
`[verified]` run end to end on a real tower and the result checked ·
|
`[verified]` run end to end on a real tower and the result checked ·
|
||||||
`[partial]` run, with something still open · `[untested]` written from the
|
`[partial]` run, with something still open · `[untested]` written from the
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,551 @@
|
||||||
|
# shellcheck shell=bash
|
||||||
|
#
|
||||||
|
# kit.sh - what every board's setup kit shares. Sourced, never run: each
|
||||||
|
# hardware/<board>/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)
|
||||||
|
|
||||||
|
# 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" ;;
|
||||||
|
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"
|
||||||
|
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.<name>] 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)"
|
||||||
|
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
|
||||||
|
mcu_dev="$(grep -l "^$KIT_MCU_VID\$" /sys/bus/usb/devices/*/idVendor 2>/dev/null | head -1)"
|
||||||
|
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 ))
|
||||||
|
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:]]*$//'
|
||||||
|
}
|
||||||
|
|
||||||
|
# `--pin k=v` values as a [Pins] block, or the kit's reference loom, or a TODO.
|
||||||
|
emit_pins() {
|
||||||
|
if [[ $LOOM == standard ]]; then
|
||||||
|
[[ -r $KIT_DIR/pins.ini ]] || die "--loom standard: this kit has no pins.ini"
|
||||||
|
cat "$KIT_DIR/pins.ini"
|
||||||
|
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 <<EOF
|
||||||
|
; hardware.ini - what this machine IS. Generated by hardware/$KIT_BOARD/install.sh
|
||||||
|
; on $(date -Is) from what the machine reports, plus the assembly
|
||||||
|
; facts given to it. See config/hardware.example.ini for the full reference,
|
||||||
|
; and docs/hardware-config-plan.md for why this file exists.
|
||||||
|
|
||||||
|
[Board]
|
||||||
|
model = $board
|
||||||
|
firmware_env = $KIT_FW_ENV
|
||||||
|
|
||||||
|
[Hardware]
|
||||||
|
; DECLARED, not probed: these are what the tower was built with, so that a part
|
||||||
|
; that stops answering is a fault and not a silent reconfiguration.
|
||||||
|
axes = $AXES
|
||||||
|
imu = $IMU
|
||||||
|
env = $ENV_SENSOR
|
||||||
|
cameras = $CAMERAS
|
||||||
|
|
||||||
|
[Serial]
|
||||||
|
; The motor controller MCU, by stable id - /dev/ttyACM* moves when the modem
|
||||||
|
; enumerates first. The name changes when the MCU is flashed.
|
||||||
|
device = ${mcu:-NOT_FOUND_SET_THIS_BY_HAND}
|
||||||
|
baud = 115200
|
||||||
|
|
||||||
|
EOF
|
||||||
|
emit_pins
|
||||||
|
|
||||||
|
if [[ $IMU != none ]]; then
|
||||||
|
printf '\n[IMU]\n; %s\ndevice = %s\n' "$KIT_IMU_NOTE" "${IMU_DEVICE:-$KIT_IMU_DEVICE}"
|
||||||
|
fi
|
||||||
|
if [[ $ENV_SENSOR != none ]]; then
|
||||||
|
printf '\n[Env]\n'
|
||||||
|
printf '%s\n' "$KIT_ENV_NOTE" | sed 's/^/; /'
|
||||||
|
printf 'i2c_device = %s\ni2c_addr = 0x44\n' "${I2C_DEVICE:-${KIT_ENV_DEVICE:-SET_THIS_BY_HAND}}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# One [Motor.<axis>] 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
|
||||||
|
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 <<EOF
|
||||||
|
|
||||||
|
[Camera.$c]
|
||||||
|
; The Vimba DEV_ id of the physical camera fitted to this tower. Not the USB
|
||||||
|
; serial: read it with VimbaX's ListCameras, which is what this probe does.
|
||||||
|
id = ${cam:-NOT_FOUND_SET_THIS_BY_HAND}
|
||||||
|
EOF
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
do_hardware_ini() {
|
||||||
|
if [[ $TO_STDOUT -eq 1 ]]; then emit_hardware_ini; return; fi
|
||||||
|
need_root "--hardware-ini"
|
||||||
|
mkdir -p "$ETC_DIR"
|
||||||
|
if [[ -f $HW_FILE && $FORCE -ne 1 ]]; then
|
||||||
|
warn "$HW_FILE exists - leaving it alone (--force to replace)"
|
||||||
|
warn "the file it would write instead:"
|
||||||
|
emit_hardware_ini | sed 's/^/ /' >&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 <<EOF
|
||||||
|
hardware/$KIT_BOARD/install.sh - set up a $KIT_BOARD tower host. Run 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 DIR install a staged Vimba X SDK
|
||||||
|
./install.sh --stdout print the hardware.ini it would write
|
||||||
|
|
||||||
|
What was ASSEMBLED (a probe cannot tell missing from broken):
|
||||||
|
--axes yaw|yaw,pitch (default: $KIT_DEFAULT_AXES)
|
||||||
|
--imu none|mti --env none|sht41 --cameras rgb|none
|
||||||
|
Overrides for what is otherwise probed:
|
||||||
|
--serial PATH the MCU's /dev/serial/by-id path
|
||||||
|
--camera-id DEV_... the camera's Vimba id
|
||||||
|
--imu-device PATH --i2c-device PATH
|
||||||
|
Wiring ([Pins], which the firmware is built from):
|
||||||
|
--loom standard this kit's reference loom (pins.ini)
|
||||||
|
--pin KEY=N one pin at a time instead (yaw_cs=1, spi_sck=2, ...)
|
||||||
|
The drivetrain, per axis - anything not given is written as a ; TODO:
|
||||||
|
--motor AXIS.KEY=VALUE KEY one of: ${KIT_AXIS_KEYS[*]}
|
||||||
|
--r-sense, --run-current, --homing-current, --travel-deg, --steps-per-rev,
|
||||||
|
--sw-mode the same, for the FIRST axis in --axes
|
||||||
|
Other:
|
||||||
|
--force overwrite an existing hardware.ini (backs it up first)
|
||||||
|
--no-apt-update skip 'apt-get update'
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
kit_main() {
|
||||||
|
AXES="$KIT_DEFAULT_AXES" IMU="none" ENV_SENSOR="none" CAMERAS="rgb"
|
||||||
|
SERIAL_DEVICE="" CAMERA_ID="" IMU_DEVICE="" I2C_DEVICE="" LOOM=""
|
||||||
|
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=""
|
||||||
|
declare -gA MOTOR=() PINS=()
|
||||||
|
PIN_ORDER=()
|
||||||
|
local explicit=0
|
||||||
|
local -A first_axis=() # the legacy single-axis flags, applied once --axes is known
|
||||||
|
|
||||||
|
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 ;;
|
||||||
|
--vimba) VIMBA_SRC="$2"; DO_VIMBA=1; explicit=1; shift ;;
|
||||||
|
--axes) AXES="$2"; shift ;;
|
||||||
|
--imu) IMU="$2"; shift ;;
|
||||||
|
--env) ENV_SENSOR="$2"; shift ;;
|
||||||
|
--cameras) CAMERAS="$2"; shift ;;
|
||||||
|
--serial) SERIAL_DEVICE="$2"; shift ;;
|
||||||
|
--camera-id) CAMERA_ID="$2"; shift ;;
|
||||||
|
--imu-device) IMU_DEVICE="$2"; shift ;;
|
||||||
|
--i2c-device) I2C_DEVICE="$2"; shift ;;
|
||||||
|
--loom) LOOM="$2"; shift ;;
|
||||||
|
--pin)
|
||||||
|
[[ $2 == *=* ]] || die "--pin takes KEY=N"
|
||||||
|
[[ -z ${PINS[${2%%=*}]+x} ]] && PIN_ORDER+=("${2%%=*}")
|
||||||
|
PINS[${2%%=*}]="${2#*=}"; shift ;;
|
||||||
|
--motor)
|
||||||
|
[[ $2 == *.*=* ]] || die "--motor takes AXIS.KEY=VALUE"
|
||||||
|
MOTOR[${2%%=*}]="${2#*=}"; shift ;;
|
||||||
|
--r-sense) first_axis[r_sense_ohm]="$2"; shift ;;
|
||||||
|
--run-current) first_axis[run_current_ma]="$2"; shift ;;
|
||||||
|
--homing-current) first_axis[homing_current_ma]="$2"; shift ;;
|
||||||
|
--travel-deg) first_axis[travel_deg]="$2"; shift ;;
|
||||||
|
--steps-per-rev) first_axis[steps_per_rev]="$2"; shift ;;
|
||||||
|
--sw-mode) first_axis[sw_mode]="$2"; shift ;;
|
||||||
|
--force) FORCE=1 ;;
|
||||||
|
--no-apt-update) APT_UPDATE=0 ;;
|
||||||
|
-h|--help) kit_usage; exit 0 ;;
|
||||||
|
*) die "unknown option: $1 (--help lists them)" ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
[[ $explicit -eq 0 ]] && { DO_DEPS=1; DO_PRE=1; DO_HW=1; }
|
||||||
|
[[ -z $LOOM || $LOOM == standard ]] || die "--loom takes 'standard' (or use --pin)"
|
||||||
|
[[ -n $LOOM && ${#PINS[@]} -gt 0 ]] && die "--loom and --pin are alternatives; give one"
|
||||||
|
|
||||||
|
local first key
|
||||||
|
first="$(echo "${AXES%%,*}" | tr -d '[:space:]')"
|
||||||
|
for key in "${!first_axis[@]}"; do
|
||||||
|
[[ -z ${MOTOR[$first.$key]+x} ]] && MOTOR[$first.$key]="${first_axis[$key]}"
|
||||||
|
done
|
||||||
|
for key in "${!MOTOR[@]}"; do
|
||||||
|
local ax="${key%%.*}" k="${key#*.}" ok=0 known
|
||||||
|
[[ ",${AXES// /}," == *",$ax,"* ]] || die "--motor $key: $ax is not in --axes $AXES"
|
||||||
|
for known in "${KIT_AXIS_KEYS[@]}"; do [[ $k == "$known" ]] && ok=1; done
|
||||||
|
[[ $ok -eq 1 ]] || die "--motor $key: unknown key (one of: ${KIT_AXIS_KEYS[*]})"
|
||||||
|
done
|
||||||
|
|
||||||
|
local board; board="$(probe_board)"
|
||||||
|
if [[ $board != "$KIT_BOARD" ]]; then
|
||||||
|
warn "this kit is for the $KIT_BOARD; this machine reports '$board'"
|
||||||
|
# --probe and --stdout change nothing on this machine. Refusing them on
|
||||||
|
# another board only means the generator can never be exercised off a
|
||||||
|
# tower, which is how a setup kit ends up untested.
|
||||||
|
[[ $PROBE_ONLY -eq 1 || $TO_STDOUT -eq 1 ]] ||
|
||||||
|
die "refusing to apply $KIT_BOARD preconditions to a $board"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $PROBE_ONLY -eq 1 ]]; then probe_report; exit 0; fi
|
||||||
|
# --stdout emits the file and nothing else: it is meant to be redirected into one.
|
||||||
|
if [[ $TO_STDOUT -eq 1 ]]; then do_hardware_ini; exit 0; fi
|
||||||
|
|
||||||
|
say "$KIT_BOARD 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"
|
||||||
|
}
|
||||||
|
|
@ -1,519 +1,33 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# install.sh - turn a freshly imaged LattePanda IOTA into a fire-watch tower host.
|
# install.sh - turn a freshly imaged LattePanda IOTA into a fire-watch tower host.
|
||||||
|
# Run it ON the device; --help lists every option. scripts/install-tower drives
|
||||||
|
# this for a whole install, prompting for what it needs; this stays usable alone.
|
||||||
#
|
#
|
||||||
# Run it ON the device:
|
# sudo ./install.sh --deps --preconditions
|
||||||
# sudo ./install.sh # everything: deps, preconditions, hardware.ini
|
# ./install.sh --probe change nothing, report
|
||||||
# sudo ./install.sh --deps # apt packages only
|
# ./install.sh --stdout --axes yaw --loom standard --motor yaw.r_sense_ohm=0.075 ...
|
||||||
# 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,
|
# Everything shared with the other boards' kits is in ../common/kit.sh. What is
|
||||||
# the camera's DEV_ id, whether the USB preconditions took. What it cannot, and
|
# here is what differs on the IOTA.
|
||||||
# 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.<name>] blocks
|
|
||||||
#
|
|
||||||
# The drivetrain, per axis. The firmware REFUSES TO MOVE until it has been told
|
|
||||||
# these (docs/configuration.md, the config handshake), so an axis left undeclared
|
|
||||||
# is an axis that will sit in NEEDCFG. Pass what you know; what you omit is
|
|
||||||
# written as a commented line naming what has to be measured:
|
|
||||||
#
|
|
||||||
# --r-sense 0.075 sense resistor on that axis's TMC board (ohms)
|
|
||||||
# --run-current 756 coil current for normal motion (mA RMS)
|
|
||||||
# --homing-current 200 coil current while hunting an endstop (mA RMS)
|
|
||||||
# --travel-deg 340 mechanical angle between the hard stops
|
|
||||||
# --steps-per-rev 1210778 measured full travel, in counts
|
|
||||||
# --sw-mode 0 TMC SW_MODE: 0 = no switches (StallGuard homing)
|
|
||||||
#
|
|
||||||
# These take ONE value, applied to the first axis in --axes. A second axis gets a
|
|
||||||
# commented template: nothing here can be probed, and inventing a sense resistor
|
|
||||||
# is how a driver gets cooked.
|
|
||||||
#
|
|
||||||
# Other flags:
|
|
||||||
# --force overwrite an existing hardware.ini (backs it up first)
|
|
||||||
# --no-apt-update skip 'apt-get update'
|
|
||||||
#
|
#
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
KIT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
ETC_DIR=/etc/fire_gimbal_control
|
KIT_BOARD=lattepanda-iota
|
||||||
HW_FILE="$ETC_DIR/hardware.ini"
|
KIT_FW_ENV=iota_rp2040
|
||||||
|
KIT_MCU_VID=2e8a
|
||||||
|
KIT_DEFAULT_AXES=yaw
|
||||||
|
KIT_IMU_DEVICE=/dev/ttyS0
|
||||||
|
KIT_IMU_NOTE="On the IOTA the only PCH serial on the header is COM1; confirm it enumerates first."
|
||||||
|
# No PCH-side I2C reaches the header, so an ambient sensor needs a USB-I2C bridge
|
||||||
|
# (MCP2221A) whose bus number depends on enumeration order: nothing to default to.
|
||||||
|
KIT_ENV_DEVICE=""
|
||||||
|
KIT_ENV_NOTE="MUST be a real I2C adapter, not SMBus-only: check with \`i2cdetect -F <n>\`.
|
||||||
|
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."
|
||||||
|
|
||||||
AXES="yaw,pitch"
|
# shellcheck source=../common/kit.sh
|
||||||
IMU="none"
|
. "$KIT_DIR/../common/kit.sh"
|
||||||
ENV_SENSOR="none"
|
# scripts/install-tower sources this file for the facts and probes alone.
|
||||||
CAMERAS="rgb"
|
[[ ${KIT_NO_MAIN:-0} == 1 ]] || kit_main "$@"
|
||||||
# Empty means "not known at install time" -> written commented out, never as a
|
|
||||||
# sentinel: these are numeric keys, and a non-numeric value makes the program
|
|
||||||
# refuse to start rather than report the axis as undeclared.
|
|
||||||
R_SENSE="" RUN_CURRENT="" HOMING_CURRENT="" TRAVEL_DEG="" STEPS_PER_REV="" SW_MODE=""
|
|
||||||
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 ;;
|
|
||||||
--r-sense) R_SENSE="$2"; shift ;;
|
|
||||||
--run-current) RUN_CURRENT="$2"; shift ;;
|
|
||||||
--homing-current) HOMING_CURRENT="$2"; shift ;;
|
|
||||||
--travel-deg) TRAVEL_DEG="$2"; shift ;;
|
|
||||||
--steps-per-rev) STEPS_PER_REV="$2"; shift ;;
|
|
||||||
--sw-mode) SW_MODE="$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.<name>] 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@<tower>:~/ # 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 "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.
|
|
||||||
# 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. The long-standing 500000 default suits the Delta's
|
|
||||||
# ~89k half-travel and does not survive a gear ratio several times higher.
|
|
||||||
center_note() {
|
|
||||||
local steps="$1" ax="$2"
|
|
||||||
[[ -z $steps ]] && return 0
|
|
||||||
local half=$(( steps / 2 ))
|
|
||||||
local want=$(( (half / 1000000 + 1) * 1000000 ))
|
|
||||||
echo "; NOTE: half this travel is $half counts, so config.ini needs"
|
|
||||||
echo "; [Motor] ${ax}_center_count = $want (or anything above $half)"
|
|
||||||
echo "; The 500000 default would put the negative end at $(( 500000 - half ))."
|
|
||||||
}
|
|
||||||
|
|
||||||
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. A generated file that cannot be
|
|
||||||
# read is worse than one with a gap in it.
|
|
||||||
printf '; TODO %s\n' "$key"
|
|
||||||
printf '%s\n' "$why" | fold -s -w 72 | sed 's/^/; /' | sed 's/[[:space:]]*$//'
|
|
||||||
}
|
|
||||||
|
|
||||||
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 <<EOF
|
|
||||||
; hardware.ini - what this machine IS. Generated by hardware/$board/install.sh
|
|
||||||
; on $(date -Is) from what the machine reports, plus the assembly
|
|
||||||
; facts given on the command line. See config/hardware.example.ini for the full
|
|
||||||
; reference, and docs/hardware-config-plan.md for why this file exists.
|
|
||||||
|
|
||||||
[Board]
|
|
||||||
model = $board
|
|
||||||
firmware_env = $fw_env
|
|
||||||
|
|
||||||
[Hardware]
|
|
||||||
; DECLARED, not probed: these are what the tower was built with, so that a part
|
|
||||||
; that stops answering is a fault and not a silent reconfiguration.
|
|
||||||
axes = $AXES
|
|
||||||
imu = $IMU
|
|
||||||
env = $ENV_SENSOR
|
|
||||||
cameras = $CAMERAS
|
|
||||||
|
|
||||||
[Serial]
|
|
||||||
; The motor controller MCU, by stable id - /dev/ttyACM* moves when the modem
|
|
||||||
; enumerates first.
|
|
||||||
device = ${mcu:-NOT_FOUND_SET_THIS_BY_HAND}
|
|
||||||
baud = 115200
|
|
||||||
EOF
|
|
||||||
|
|
||||||
if [[ $IMU != none ]]; then
|
|
||||||
cat <<'EOF'
|
|
||||||
|
|
||||||
[IMU]
|
|
||||||
; On the IOTA the only PCH serial on the header is COM1. Confirm it enumerates
|
|
||||||
; and at what levels before trusting this path.
|
|
||||||
device = /dev/ttyS0
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
if [[ $ENV_SENSOR != none ]]; then
|
|
||||||
cat <<'EOF'
|
|
||||||
|
|
||||||
[Env]
|
|
||||||
; MUST be a real I2C adapter, not SMBus-only: check with `i2cdetect -F <n>`.
|
|
||||||
; 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
|
|
||||||
|
|
||||||
# One [Motor.<axis>] block per axis. The firmware holds none of these itself
|
|
||||||
# any more - it asks, and refuses to move until answered - so an axis without
|
|
||||||
# a block here is an axis that will not turn.
|
|
||||||
#
|
|
||||||
# A value we were not given is written COMMENTED OUT, with what to measure.
|
|
||||||
# Not as a placeholder string: these keys are numeric, and a non-numeric value
|
|
||||||
# makes the program refuse to start, which is a worse failure than the axis
|
|
||||||
# reporting itself undeclared.
|
|
||||||
local ax first=1
|
|
||||||
for ax in ${AXES//,/ }; do
|
|
||||||
ax="$(echo "$ax" | tr -d '[:space:]')"
|
|
||||||
[[ -z $ax ]] && continue
|
|
||||||
echo
|
|
||||||
echo "[Motor.$ax]"
|
|
||||||
if [[ $first -eq 1 ]]; then
|
|
||||||
kv_or_todo "r_sense_ohm" "$R_SENSE" \
|
|
||||||
"the resistor fitted to THIS driver board - read it off the board, do not assume"
|
|
||||||
kv_or_todo "run_current_ma" "$RUN_CURRENT" \
|
|
||||||
"coil current for normal motion, mA RMS, from the motor's nameplate"
|
|
||||||
kv_or_todo "homing_current_ma" "$HOMING_CURRENT" \
|
|
||||||
"current while hunting an endstop. LOWER than run_current wherever the stop can be out-pushed: on this rig a screw-head stop was sheared straight past its mate at full torque"
|
|
||||||
kv_or_todo "travel_deg" "$TRAVEL_DEG" \
|
|
||||||
"mechanical angle between the hard stops. Rough is fine to start; every bearing inherits its error proportionally until 'gimbal heading setup' supersedes it"
|
|
||||||
kv_or_todo "steps_per_rev" "$STEPS_PER_REV" \
|
|
||||||
"measured full travel IN COUNTS - read it off a homing run. The only guard against a PHANTOM endstop"
|
|
||||||
center_note "$STEPS_PER_REV" "$ax"
|
|
||||||
kv_or_todo "sw_mode" "$SW_MODE" \
|
|
||||||
"TMC SW_MODE. 0 = no physical switches (StallGuard homing); set the bits if this axis has REFL/REFR wired"
|
|
||||||
else
|
|
||||||
echo "; Nothing here can be probed, and this is not the first axis, so the"
|
|
||||||
echo "; values below are a template. Measure them on THIS axis."
|
|
||||||
kv_or_todo "r_sense_ohm" "" "sense resistor on this axis's driver board"
|
|
||||||
kv_or_todo "run_current_ma" "" "coil current for normal motion, mA RMS"
|
|
||||||
kv_or_todo "homing_current_ma" "" "current while hunting an endstop"
|
|
||||||
kv_or_todo "travel_deg" "" "mechanical angle between the hard stops"
|
|
||||||
kv_or_todo "steps_per_rev" "" "measured full travel, in counts"
|
|
||||||
kv_or_todo "sw_mode" "" "TMC SW_MODE (0 = no physical switches)"
|
|
||||||
fi
|
|
||||||
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. The
|
|
||||||
; host converts through counts_per_deg and sends SPEED.
|
|
||||||
scan_speed_deg_s = 8.0
|
|
||||||
homing_speed_deg_s = 4.0
|
|
||||||
center_speed_deg_s = 6.0
|
|
||||||
; |XACTUAL - XENC| that means "position untrustworthy". 1000 on this mechanism:
|
|
||||||
; the free-running gap measured 199-229 counts on tower 121, so this is ~4x
|
|
||||||
; headroom. Re-measure whenever the homing CURRENT changes - the gap is coupled
|
|
||||||
; to it, and notably NOT to speed (229 counts at VMAX 6000, 199 at 30000).
|
|
||||||
; Lowering is the safe direction; raising it blunts the only stall signal there is.
|
|
||||||
enc_deviation = 1000
|
|
||||||
; Is this axis's encoder aligned to XACTUAL and usable for hold and DIAG?
|
|
||||||
has_encoder = true
|
|
||||||
EOF
|
|
||||||
first=0
|
|
||||||
done
|
|
||||||
|
|
||||||
local c
|
|
||||||
for c in ${CAMERAS//,/ }; do
|
|
||||||
c="$(echo "$c" | tr -d '[:space:]')"
|
|
||||||
cat <<EOF
|
|
||||||
|
|
||||||
[Camera.$c]
|
|
||||||
; The Vimba DEV_ id of the physical camera fitted to this tower. Not the USB
|
|
||||||
; serial: read it with VimbaX's ListCameras, which is what this probe does.
|
|
||||||
id = ${cam:-NOT_FOUND_SET_THIS_BY_HAND}
|
|
||||||
EOF
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
do_hardware_ini() {
|
|
||||||
if [[ $TO_STDOUT -eq 1 ]]; then emit_hardware_ini; return; fi
|
|
||||||
need_root "--hardware-ini"
|
|
||||||
mkdir -p "$ETC_DIR"
|
|
||||||
if [[ -f $HW_FILE && $FORCE -ne 1 ]]; then
|
|
||||||
warn "$HW_FILE exists - leaving it alone (--force to replace)"
|
|
||||||
warn "the file it would write instead:"
|
|
||||||
emit_hardware_ini | sed 's/^/ /' >&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 and --stdout change nothing on this machine: one reports what it
|
|
||||||
# found, the other prints the file it WOULD write. Refusing them on a
|
|
||||||
# non-IOTA only means the generator can never be exercised off a tower,
|
|
||||||
# which is how a setup kit ends up untested.
|
|
||||||
[[ $PROBE_ONLY -eq 1 || $TO_STDOUT -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"
|
|
||||||
|
|
|
||||||
19
scripts/fgc
19
scripts/fgc
|
|
@ -106,6 +106,24 @@ resolve_vimba_env() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# MQTT credentials, from the same file the systemd unit names as its
|
||||||
|
# EnvironmentFile. Read here, inside the pane, for the reason above: a tmux server
|
||||||
|
# that already exists gives a new session ITS environment, not the caller's, so a
|
||||||
|
# unit's EnvironmentFile would silently not reach the app. Parsed as KEY=VALUE
|
||||||
|
# lines rather than sourced, so a password with $ or quotes in it stays literal.
|
||||||
|
# Only the variables the program reads are taken.
|
||||||
|
load_credentials() {
|
||||||
|
local f=/etc/fire_gimbal_control/credentials.env line k v
|
||||||
|
[[ -r "$f" ]] || return 0
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
[[ "$line" =~ ^[[:space:]]*(#|$) ]] && continue
|
||||||
|
k="${line%%=*}"; v="${line#*=}"
|
||||||
|
case "$k" in
|
||||||
|
FGC_MQTT_USER|FGC_MQTT_PW) export "$k=$v" ;;
|
||||||
|
esac
|
||||||
|
done < "$f"
|
||||||
|
}
|
||||||
|
|
||||||
cmd_start() {
|
cmd_start() {
|
||||||
need_tmux
|
need_tmux
|
||||||
if running; then
|
if running; then
|
||||||
|
|
@ -198,6 +216,7 @@ cmd_start() {
|
||||||
# exec'd, 3/3 clean when parented.
|
# exec'd, 3/3 clean when parented.
|
||||||
cmd__run() {
|
cmd__run() {
|
||||||
resolve_vimba_env
|
resolve_vimba_env
|
||||||
|
load_credentials
|
||||||
|
|
||||||
# <&0 is required: bash redirects a background command's stdin from
|
# <&0 is required: bash redirects a background command's stdin from
|
||||||
# /dev/null unless it is given one explicitly, which would leave the
|
# /dev/null unless it is given one explicitly, which would leave the
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,869 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# install-tower - take a freshly imaged tower host to a working fire-watch tower,
|
||||||
|
# or check and repair one that is already configured. Run it ON the tower, as the
|
||||||
|
# user the program runs as (ggs), from a checkout of this repository with the
|
||||||
|
# firmware repository beside it (../firmware).
|
||||||
|
#
|
||||||
|
# scripts/install-tower survey, ask what it cannot work out, show
|
||||||
|
# the plan, then do it after one yes
|
||||||
|
# scripts/install-tower --dry-run survey and show the plan; change nothing
|
||||||
|
# scripts/install-tower --verify run the checks only; change nothing
|
||||||
|
# scripts/install-tower --non-interactive --answers FILE
|
||||||
|
# fail instead of prompting; FILE holds
|
||||||
|
# key=value lines, as --answer does
|
||||||
|
#
|
||||||
|
# Every question has a key, so any of them can be answered up front with
|
||||||
|
# --answer KEY=VALUE (repeatable) - e.g. --answer tower_name=north
|
||||||
|
# --answer yaw.r_sense_ohm=0.075. A drivetrain value may be answered `unknown`,
|
||||||
|
# which leaves a commented ; TODO in hardware.ini and that axis unable to move.
|
||||||
|
# Nothing here ever makes one up: a wrong sense resistor cooks a driver, and a
|
||||||
|
# wrong homing current shears an endstop - both have happened on this project.
|
||||||
|
#
|
||||||
|
# What it does, in order (each step is skipped when it is already true):
|
||||||
|
# packages, USB preconditions, Vimba X SDK, host build, hardware.ini,
|
||||||
|
# config.ini (+ credentials.env), PlatformIO, camera profiles, firmware build +
|
||||||
|
# flash, test hardware, [a person at the rig says yes] homing + a short capture,
|
||||||
|
# boot service.
|
||||||
|
# The order is the safe one for an existing tower too: the host and the files it
|
||||||
|
# reads are in place and tested BEFORE any firmware is flashed, so a new firmware
|
||||||
|
# never boots into a host that cannot answer it.
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO="$(cd "$here/.." && pwd)"
|
||||||
|
FW="$(cd "$REPO/.." && pwd)/firmware"
|
||||||
|
# The two overrides exist to rehearse a FRESH install with --dry-run on a tower
|
||||||
|
# that is already configured, without its real files being in the way.
|
||||||
|
ETC="${INSTALL_TOWER_ETC:-/etc/fire_gimbal_control}"
|
||||||
|
HW="$ETC/hardware.ini"
|
||||||
|
CRED="$ETC/credentials.env"
|
||||||
|
INSTALL_LOG="$ETC/install.log"
|
||||||
|
CFG="${INSTALL_TOWER_CONFIG:-$REPO/config.ini}"
|
||||||
|
UNIT=/etc/systemd/system/fire-gimbal-control.service
|
||||||
|
LOGDIR="${XDG_DATA_HOME:-$HOME/.local/share}/fire_gimbal_control/logs"
|
||||||
|
BIN="$REPO/build/fire_gimbal_control"
|
||||||
|
FGC_CONFIG="$REPO/build/fgc-config"
|
||||||
|
FGC="$REPO/scripts/fgc"
|
||||||
|
WORK="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$WORK"' EXIT
|
||||||
|
|
||||||
|
MODE=install NONINTERACTIVE=0
|
||||||
|
declare -A ANS=()
|
||||||
|
|
||||||
|
usage() { sed -n '2,33p' "$0" | sed 's/^# \{0,1\}//'; }
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--dry-run) MODE=dry-run ;;
|
||||||
|
--verify) MODE=verify ;;
|
||||||
|
--non-interactive) NONINTERACTIVE=1 ;;
|
||||||
|
--answer)
|
||||||
|
[[ ${2:-} == *=* ]] || { echo "install-tower: --answer takes KEY=VALUE" >&2; exit 1; }
|
||||||
|
ANS[${2%%=*}]="${2#*=}"; shift ;;
|
||||||
|
--answers)
|
||||||
|
[[ -r ${2:-} ]] || { echo "install-tower: cannot read ${2:-}" >&2; exit 1; }
|
||||||
|
while IFS= read -r line || [[ -n $line ]]; do
|
||||||
|
[[ $line =~ ^[[:space:]]*(#|$) ]] && continue
|
||||||
|
[[ $line == *=* ]] && ANS[${line%%=*}]="${line#*=}"
|
||||||
|
done < "$2"; shift ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "install-tower: unknown option: $1 (--help)" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
say() { printf '>> %s\n' "$*"; }
|
||||||
|
warn() { printf '!! %s\n' "$*" >&2; }
|
||||||
|
die() { printf 'install-tower: %s\n' "$*" >&2; exit 1; }
|
||||||
|
hr() { printf '\n== %s ==\n' "$*"; }
|
||||||
|
|
||||||
|
[[ $EUID -ne 0 ]] || die "run as the tower's user (ggs), not root: builds and ~/.platformio must belong to it. It uses sudo where it has to."
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- questions ---
|
||||||
|
# Answers come from --answer / --answers first; otherwise from the terminal. With
|
||||||
|
# --non-interactive a question that has a default takes it, and one that has
|
||||||
|
# none stops the run naming the key to pass.
|
||||||
|
tty_ok() { [[ -r /dev/tty && -w /dev/tty ]] && { : > /dev/tty; } 2>/dev/null; }
|
||||||
|
|
||||||
|
REPLY_VAL=""
|
||||||
|
ask() {
|
||||||
|
local key="$1" prompt="$2" def="${3-}"
|
||||||
|
if [[ -n ${ANS[$key]+x} ]]; then REPLY_VAL="${ANS[$key]}"; return 0; fi
|
||||||
|
if [[ $NONINTERACTIVE -eq 1 ]] || ! tty_ok; then
|
||||||
|
[[ -n $def ]] && { REPLY_VAL="$def"; ANS[$key]="$def"; return 0; }
|
||||||
|
die "no answer for '$key' ($prompt). Pass --answer $key=..."
|
||||||
|
fi
|
||||||
|
local a
|
||||||
|
printf '%s%s: ' "$prompt" "${def:+ [$def]}" > /dev/tty
|
||||||
|
IFS= read -r a < /dev/tty || die "no terminal to ask '$key' on"
|
||||||
|
REPLY_VAL="${a:-$def}"
|
||||||
|
ANS[$key]="$REPLY_VAL"
|
||||||
|
}
|
||||||
|
|
||||||
|
ask_secret() {
|
||||||
|
local key="$1" prompt="$2"
|
||||||
|
if [[ -n ${ANS[$key]+x} ]]; then REPLY_VAL="${ANS[$key]}"; return 0; fi
|
||||||
|
[[ $NONINTERACTIVE -eq 0 ]] && tty_ok || die "no answer for '$key' ($prompt). Pass --answer $key=..."
|
||||||
|
local a
|
||||||
|
printf '%s: ' "$prompt" > /dev/tty
|
||||||
|
IFS= read -rs a < /dev/tty || die "no terminal"
|
||||||
|
printf '\n' > /dev/tty
|
||||||
|
REPLY_VAL="$a"; ANS[$key]="$a"
|
||||||
|
}
|
||||||
|
|
||||||
|
# y/n, with a default. Returns 0 for yes.
|
||||||
|
ask_yn() {
|
||||||
|
local key="$1" prompt="$2" def="$3"
|
||||||
|
while :; do
|
||||||
|
ask "$key" "$prompt (y/n)" "$def"
|
||||||
|
case "${REPLY_VAL,,}" in
|
||||||
|
y|yes) return 0 ;;
|
||||||
|
n|no) return 1 ;;
|
||||||
|
esac
|
||||||
|
unset "ANS[$key]"
|
||||||
|
[[ $NONINTERACTIVE -eq 1 ]] && die "'$key' must be y or n"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# A number, or "unknown". No default is ever offered for these.
|
||||||
|
ask_value() {
|
||||||
|
local key="$1" prompt="$2" pattern="$3"
|
||||||
|
while :; do
|
||||||
|
ask "$key" "$prompt (or 'unknown')"
|
||||||
|
[[ ${REPLY_VAL,,} == unknown ]] && { REPLY_VAL=""; return 0; }
|
||||||
|
[[ $REPLY_VAL =~ $pattern ]] && return 0
|
||||||
|
warn "'$REPLY_VAL' is not a valid value for $key"
|
||||||
|
unset "ANS[$key]"
|
||||||
|
[[ $NONINTERACTIVE -eq 1 ]] && die "bad value for '$key'"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# One wrapped, indented explanation line before a question.
|
||||||
|
explain() { printf '%s\n' "$1" | fold -s -w 72 | sed '1s/^/ - /; 2,$s/^/ /'; }
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------- reading ---
|
||||||
|
# A value as inih reads it: last assignment wins, and an inline comment is a ';'
|
||||||
|
# preceded by whitespace. For reading only - every write goes through fgc-config.
|
||||||
|
ini_get() {
|
||||||
|
[[ -r $1 ]] || return 1
|
||||||
|
awk -v s="$2" -v k="$3" '
|
||||||
|
/^[[:space:]]*[;#]/ { next }
|
||||||
|
/^[[:space:]]*\[/ { sec = $0; sub(/^[[:space:]]*\[/, "", sec); sub(/\].*$/, "", sec); next }
|
||||||
|
sec == s {
|
||||||
|
i = index($0, "="); if (!i) next
|
||||||
|
key = substr($0, 1, i - 1); gsub(/^[ \t]+|[ \t]+$/, "", key)
|
||||||
|
if (key != k) next
|
||||||
|
v = substr($0, i + 1); sub(/[ \t]+;.*$/, "", v); gsub(/^[ \t]+|[ \t]+$/, "", v)
|
||||||
|
val = v; found = 1
|
||||||
|
}
|
||||||
|
END { if (found) print val; else exit 1 }' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
latest_log() { ls -t "$LOGDIR"/fgc_*.log 2>/dev/null | head -1; }
|
||||||
|
|
||||||
|
pio_path() {
|
||||||
|
local c
|
||||||
|
for c in "$(command -v pio 2>/dev/null || true)" "$HOME/.platformio/penv/bin/pio" \
|
||||||
|
"$HOME/.local/bin/pio" /usr/local/bin/pio; do
|
||||||
|
[[ -n $c && -x $c ]] && { echo "$c"; return 0; }
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
py_serial() {
|
||||||
|
local c
|
||||||
|
for c in "$HOME/.platformio/penv/bin/python" "$(command -v python3 2>/dev/null || true)"; do
|
||||||
|
[[ -n $c && -x $c ]] && "$c" -c 'import serial' 2>/dev/null && { echo "$c"; return 0; }
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
app_running() { "$FGC" status >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
# The arguments the running app was started with, from its log header, so a
|
||||||
|
# restart puts it back the way it was.
|
||||||
|
app_args() {
|
||||||
|
local l; l="$(latest_log)" || return 0
|
||||||
|
[[ -n $l ]] || return 0
|
||||||
|
sed -n 's/^argv *//p' "$l" | head -1 | awk '{ for (i = 2; i <= NF; i++) printf "%s%s", $i, (i < NF ? " " : "") }'
|
||||||
|
}
|
||||||
|
|
||||||
|
# What the firmware on the board reports: "build=... spi=... Y:cs,en P:cs,en".
|
||||||
|
# From the running app's log when it holds the port, else asked directly.
|
||||||
|
firmware_report() {
|
||||||
|
local text=""
|
||||||
|
if app_running; then
|
||||||
|
local l; l="$(latest_log)"
|
||||||
|
# A just-started app has not dumped yet; give it the few seconds it needs.
|
||||||
|
for _ in $(seq 1 15); do
|
||||||
|
[[ -n $l ]] && grep -q '^DUMP BEGIN' "$l" && break
|
||||||
|
sleep 1; l="$(latest_log)"
|
||||||
|
done
|
||||||
|
[[ -n $l ]] && text="$(grep -E '^DUMP (BEGIN|[YP] state)' "$l" | tail -3)"
|
||||||
|
else
|
||||||
|
local py port; py="$(py_serial)" || return 0
|
||||||
|
port="$(ini_get "$HW" Serial device 2>/dev/null || true)"
|
||||||
|
[[ -z $port || ! -e $port ]] && port="$(probe_mcu | head -1)"
|
||||||
|
[[ -n $port && -e $port && $port != *MicroPython* ]] || return 0
|
||||||
|
text="$("$py" - "$port" <<'PYEOF' 2>/dev/null || true
|
||||||
|
import sys, time, serial
|
||||||
|
s = serial.Serial(sys.argv[1], 115200, timeout=1)
|
||||||
|
time.sleep(1.5); s.reset_input_buffer(); s.write(b"DUMP\r\n")
|
||||||
|
end = time.time() + 3
|
||||||
|
while time.time() < end:
|
||||||
|
l = s.readline().decode(errors="replace").strip()
|
||||||
|
if l.startswith("DUMP BEGIN") or (l.startswith("DUMP ") and " state=" in l): print(l)
|
||||||
|
if l.startswith("DUMP END"): break
|
||||||
|
PYEOF
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
[[ -n $text ]] || return 0
|
||||||
|
local build spi y p
|
||||||
|
build="$(sed -n 's/.*DUMP BEGIN build=\([^ ]*\).*/\1/p' <<< "$text" | tail -1)"
|
||||||
|
spi="$(sed -n 's/.*DUMP BEGIN.* spi=\([0-9,]*\).*/\1/p' <<< "$text" | tail -1)"
|
||||||
|
y="$(sed -n 's/^DUMP Y .* cs=\([0-9]*\) en=\([0-9]*\).*/\1,\2/p' <<< "$text" | tail -1)"
|
||||||
|
p="$(sed -n 's/^DUMP P .* cs=\([0-9]*\) en=\([0-9]*\).*/\1,\2/p' <<< "$text" | tail -1)"
|
||||||
|
echo "build=${build:-?} spi=${spi:--} Y=${y:--} P=${p:--}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The same, as hardware.ini declares it, for comparison.
|
||||||
|
declared_pins() {
|
||||||
|
local spi="-" y="-" p="-" a b c
|
||||||
|
a="$(ini_get "$HW" Pins spi_sck 2>/dev/null || true)"
|
||||||
|
b="$(ini_get "$HW" Pins spi_mosi 2>/dev/null || true)"
|
||||||
|
c="$(ini_get "$HW" Pins spi_miso 2>/dev/null || true)"
|
||||||
|
[[ -n $a ]] && spi="$a,$b,$c"
|
||||||
|
a="$(ini_get "$HW" Pins yaw_cs 2>/dev/null || true)"; b="$(ini_get "$HW" Pins yaw_en 2>/dev/null || true)"
|
||||||
|
[[ -n $a ]] && y="$a,$b"
|
||||||
|
a="$(ini_get "$HW" Pins pitch_cs 2>/dev/null || true)"; b="$(ini_get "$HW" Pins pitch_en 2>/dev/null || true)"
|
||||||
|
[[ -n $a ]] && p="$a,$b"
|
||||||
|
echo "spi=$spi Y=$y P=$p"
|
||||||
|
}
|
||||||
|
|
||||||
|
hw_todos() { [[ -r $HW ]] && grep -E '^; TODO' "$HW" | sed 's/^; TODO //' || true; }
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ survey ---
|
||||||
|
hr "survey"
|
||||||
|
[[ -d $FW ]] || die "the firmware repository is not beside this one ($FW). Clone it there: git clone $(git -C "$REPO" remote get-url origin 2>/dev/null | sed 's/fwt_software/fwt_firmware/') $FW"
|
||||||
|
|
||||||
|
# The board decides the kit; the kit supplies the probes and the facts.
|
||||||
|
. "$REPO/hardware/common/kit.sh"
|
||||||
|
S_BOARD="$(probe_board)"
|
||||||
|
KIT="$REPO/hardware/$S_BOARD"
|
||||||
|
[[ -r $KIT/install.sh ]] || die "no setup kit for this board ('$S_BOARD'): hardware/$S_BOARD/ does not exist"
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
KIT_NO_MAIN=1 . "$KIT/install.sh"
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
S_MCU="$(probe_mcu)"
|
||||||
|
S_MCU_N=$(grep -c . <<< "$S_MCU" || true)
|
||||||
|
S_CAM="$(probe_camera_id)"
|
||||||
|
S_LPM="$(probe_camera_lpm_path)"
|
||||||
|
S_USBFS="$(cat /sys/module/usbcore/parameters/usbfs_memory_mb 2>/dev/null || echo '?')"
|
||||||
|
S_PIO="$(pio_path || true)"
|
||||||
|
S_RUNNING=0; app_running && S_RUNNING=1
|
||||||
|
S_ARGS="$(app_args)"
|
||||||
|
S_SVC_ENABLED="$(systemctl is-enabled fire-gimbal-control 2>/dev/null || true)"
|
||||||
|
S_SVC_ACTIVE="$(systemctl is-active fire-gimbal-control 2>/dev/null || true)"
|
||||||
|
S_LINGER="$(loginctl show-user "$USER" -p Linger --value 2>/dev/null || echo no)"
|
||||||
|
# A git checkout knows its revision; a tree synced by deploy.sh (which leaves
|
||||||
|
# .git behind) carries it in .revision.
|
||||||
|
revision_of() { git -C "$1" describe --always --dirty 2>/dev/null || cat "$1/.revision" 2>/dev/null || echo nogit; }
|
||||||
|
S_FW_REV="$(revision_of "$FW")"
|
||||||
|
S_SW_REV="$(revision_of "$REPO")"
|
||||||
|
S_FW_NOW="$(firmware_report)"
|
||||||
|
S_MODE="none"
|
||||||
|
case "$S_MCU" in
|
||||||
|
*MicroPython*) S_MODE="factory MicroPython image" ;;
|
||||||
|
*Raspberry_Pi_Pico*|*Pico*) S_MODE="gimbal firmware" ;;
|
||||||
|
*Arduino*) S_MODE="gimbal firmware (Leonardo)" ;;
|
||||||
|
esac
|
||||||
|
lsusb -d 2e8a:0003 >/dev/null 2>&1 && S_MODE="in its bootloader"
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
board : $S_BOARD (kit hardware/$S_BOARD, firmware env $KIT_FW_ENV)
|
||||||
|
software / fw : $S_SW_REV / $S_FW_REV
|
||||||
|
motor MCU : ${S_MCU:-NOT FOUND} [$S_MODE]
|
||||||
|
running firmware : ${S_FW_NOW:-unknown}
|
||||||
|
camera : ${S_CAM:-not found}${S_LPM:+ (LPM $(cat "$S_LPM") at $S_LPM)}
|
||||||
|
usbfs_memory_mb : $S_USBFS
|
||||||
|
hardware.ini : $([[ -r $HW ]] && echo "present, $(hw_todos | grep -c . || true) TODO(s)" || echo absent)
|
||||||
|
config.ini : $([[ -r $CFG ]] && echo "present, tower_name $(ini_get "$CFG" General tower_name 2>/dev/null || echo '?')" || echo absent)
|
||||||
|
app : $([[ $S_RUNNING -eq 1 ]] && echo "running (${S_ARGS:-no arguments})" || echo "not running")
|
||||||
|
service : ${S_SVC_ENABLED:-absent} / ${S_SVC_ACTIVE:-inactive}, linger ${S_LINGER}
|
||||||
|
PlatformIO : ${S_PIO:-not installed}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------- verify ---
|
||||||
|
TEST_RC=""
|
||||||
|
run_hardware_test() {
|
||||||
|
# The app holds the serial port, so a test runs INSIDE it when it is up, and
|
||||||
|
# as a one-shot run of the binary when it is not.
|
||||||
|
if app_running; then
|
||||||
|
local pane before after
|
||||||
|
pane="$(tmux list-panes -t "=${FGC_SESSION:-fgc}" -F '#{pane_id}' | head -1)"
|
||||||
|
before="$(ls -t "$LOGDIR"/test_*.log 2>/dev/null | head -1)"
|
||||||
|
if tmux capture-pane -p -t "$pane" | grep -q '╭'; then
|
||||||
|
tmux send-keys -t "$pane" ':'; sleep 0.5
|
||||||
|
fi
|
||||||
|
tmux send-keys -t "$pane" 'test hardware' Enter
|
||||||
|
for _ in $(seq 1 60); do
|
||||||
|
after="$(ls -t "$LOGDIR"/test_*.log 2>/dev/null | head -1)"
|
||||||
|
[[ -n $after && $after != "$before" ]] && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
[[ -n $after && $after != "$before" ]] || { warn "no test report appeared"; TEST_RC=5; return; }
|
||||||
|
grep -E '^\[| ; ' "$after" | sed 's/^/ /'
|
||||||
|
grep -q '^result = PASS' "$after" && TEST_RC=0 || TEST_RC=4
|
||||||
|
else
|
||||||
|
local out="$WORK/test.out"
|
||||||
|
TEST_RC=0
|
||||||
|
( cd "$REPO" && GENICAM_GENTL64_PATH="${GENICAM_GENTL64_PATH:-/opt/VimbaX/cti}" \
|
||||||
|
"$BIN" --no-mqtt --test hardware ) > "$out" 2>&1 < /dev/null || TEST_RC=$?
|
||||||
|
grep -E '\-\-test|test hw_|test overall' "$out" | sed 's/^/ /'
|
||||||
|
local rep; rep="$(ls -t "$LOGDIR"/test_*.log 2>/dev/null | head -1)"
|
||||||
|
[[ -n $rep ]] && grep -E ' ; ' "$rep" | sed 's/^/ /'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
verify() {
|
||||||
|
hr "verify"
|
||||||
|
local fails=0
|
||||||
|
check() { # name, ok(0/1), detail
|
||||||
|
if [[ $2 -eq 0 ]]; then printf ' ok %-22s %s\n' "$1" "$3"
|
||||||
|
else printf ' FAIL %-22s %s\n' "$1" "$3"; fails=$((fails + 1)); fi
|
||||||
|
}
|
||||||
|
check "usbfs_memory_mb" "$([[ $S_USBFS -ge 1000 ]] 2>/dev/null && echo 0 || echo 1)" "$S_USBFS (want >= 1000)"
|
||||||
|
if [[ -n $S_LPM ]]; then
|
||||||
|
check "camera USB3 LPM" "$([[ $(cat "$S_LPM") == 0 ]] && echo 0 || echo 1)" "$(cat "$S_LPM") (want 0)"
|
||||||
|
fi
|
||||||
|
if [[ -x $FGC_CONFIG && -r $CFG ]]; then
|
||||||
|
local out rc=0
|
||||||
|
out="$("$FGC_CONFIG" check --config "$CFG" --hardware "$HW" 2>&1)" || rc=$?
|
||||||
|
local shadow; shadow="$(grep -c 'overrides' <<< "$out" || true)"
|
||||||
|
check "config files load" "$rc" "$([[ $rc -eq 0 ]] && echo "cleanly, $shadow shadowed key(s)" || tail -1 <<< "$out")"
|
||||||
|
else
|
||||||
|
check "config files load" 1 "fgc-config or config.ini missing"
|
||||||
|
fi
|
||||||
|
local todo; todo="$(hw_todos | grep -c . || true)"
|
||||||
|
check "hardware.ini complete" "$([[ $todo -eq 0 ]] && echo 0 || echo 1)" "$todo TODO(s)"
|
||||||
|
local now; now="$(firmware_report)"
|
||||||
|
local want_pins; want_pins="$(declared_pins)"
|
||||||
|
check "firmware pins" "$([[ -n $now && ${now#* } == "$want_pins" ]] && echo 0 || echo 1)" \
|
||||||
|
"running ${now:-unknown}; declared $want_pins"
|
||||||
|
run_hardware_test
|
||||||
|
check "test hardware" "$TEST_RC" "$([[ $TEST_RC == 0 ]] && echo PASS || echo "exit $TEST_RC")"
|
||||||
|
# Only once there IS a service: a tower run by hand is a choice, not a fault.
|
||||||
|
if [[ -r $UNIT ]]; then
|
||||||
|
check "boot service" "$([[ $S_SVC_ENABLED == enabled ]] && echo 0 || echo 1)" "$S_SVC_ENABLED"
|
||||||
|
check "user lingering" "$([[ $S_LINGER == yes ]] && echo 0 || echo 1)" \
|
||||||
|
"$S_LINGER (without it a service-started app and an ssh session disagree on the lock file)"
|
||||||
|
else
|
||||||
|
printf ' -- %-22s %s\n' "boot service" "not installed: the program does not start at boot"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
[[ $fails -eq 0 ]] && say "all checks pass" || warn "$fails check(s) failed"
|
||||||
|
return "$fails"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ $MODE == verify ]]; then
|
||||||
|
verify && exit 0 || exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------------------- planning ---
|
||||||
|
# Each step records what it will do; nothing changes until the plan is accepted.
|
||||||
|
declare -a STEPS=()
|
||||||
|
declare -A DO=() WHAT=()
|
||||||
|
plan() { STEPS+=("$1"); DO[$1]="$2"; WHAT[$1]="$3"; }
|
||||||
|
|
||||||
|
hr "questions"
|
||||||
|
|
||||||
|
# Packages.
|
||||||
|
mapfile -t PKGS < <(kit_packages)
|
||||||
|
MISSING=()
|
||||||
|
for p in "${PKGS[@]}"; do
|
||||||
|
dpkg-query -W -f='${Status}' "$p" 2>/dev/null | grep -q 'install ok installed' || MISSING+=("$p")
|
||||||
|
done
|
||||||
|
if (( ${#MISSING[@]} )); then plan packages change "install ${MISSING[*]}"
|
||||||
|
else plan packages skip "all ${#PKGS[@]} present"; fi
|
||||||
|
|
||||||
|
# Preconditions.
|
||||||
|
STALE=()
|
||||||
|
while read -r src dst; do cmp -s "$src" "$dst" || STALE+=("$(basename "$dst")"); done < <(kit_precondition_targets)
|
||||||
|
if (( ${#STALE[@]} )); then plan preconditions change "install ${STALE[*]}"
|
||||||
|
else plan preconditions skip "installed"; fi
|
||||||
|
|
||||||
|
# Vimba X SDK. Login-walled, so the operator brings a copy.
|
||||||
|
VIMBA_SRC=""
|
||||||
|
if [[ -d /opt/VimbaX/cti ]]; then
|
||||||
|
plan vimba skip "$(readlink -f /opt/VimbaX)"
|
||||||
|
else
|
||||||
|
staged="$(ls -d "$HOME"/VimbaX_* 2>/dev/null | head -1 || true)"
|
||||||
|
echo "The Vimba X SDK is not installed. It cannot be downloaded by a script: copy it"
|
||||||
|
echo "onto this machine (USB stick or scp) and give its directory, or 'none' for a"
|
||||||
|
echo "tower with no camera yet."
|
||||||
|
ask vimba_src "Staged Vimba X directory" "${staged:-none}"
|
||||||
|
if [[ $REPLY_VAL == none ]]; then plan vimba skip "no camera SDK (answered none)"
|
||||||
|
else
|
||||||
|
[[ -x $REPLY_VAL/cti/Install_GenTL_Path.sh ]] || die "$REPLY_VAL does not look like a Vimba X SDK"
|
||||||
|
VIMBA_SRC="$REPLY_VAL"; plan vimba change "install $VIMBA_SRC"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
HAVE_VIMBA=0; [[ -d /opt/VimbaX/cti || -n $VIMBA_SRC ]] && HAVE_VIMBA=1
|
||||||
|
|
||||||
|
plan build change "cmake build of $S_SW_REV (MQTT on, Vimba $([[ $HAVE_VIMBA -eq 1 ]] && echo on || echo off))"
|
||||||
|
|
||||||
|
# hardware.ini: what this tower IS.
|
||||||
|
HW_NEW="" # a whole new file, when there is none
|
||||||
|
declare -a HW_SETS=() # key edits, when there is one
|
||||||
|
declare -a HW_FILLS=() # "axis key value": a ; TODO answered now, filled in place
|
||||||
|
TODO_LEFT=0
|
||||||
|
HW_APPEND_PINS=0
|
||||||
|
choose_pins() {
|
||||||
|
echo
|
||||||
|
echo "Wiring ([Pins]) - the firmware is built from it, and a chip-select on the"
|
||||||
|
echo "wrong pin writes into a driver that was never meant to hear it. The standard"
|
||||||
|
echo "loom for this board is hardware/$S_BOARD/pins.ini:"
|
||||||
|
sed -n '/^\[Pins\]/,$p' "$KIT/pins.ini" | sed 's/^/ /'
|
||||||
|
ask_yn loom "Is this tower wired exactly to that loom?" n
|
||||||
|
}
|
||||||
|
if [[ ! -r $HW ]]; then
|
||||||
|
echo
|
||||||
|
echo "There is no hardware.ini yet. The next questions are what was ASSEMBLED -"
|
||||||
|
echo "declared, not probed: a missing part and a broken one look the same to a probe."
|
||||||
|
ask axes "Axes fitted (yaw, or yaw,pitch)" "$KIT_DEFAULT_AXES"; AXES_A="${REPLY_VAL// /}"
|
||||||
|
[[ $AXES_A == yaw || $AXES_A == yaw,pitch ]] || die "axes must be 'yaw' or 'yaw,pitch' (the firmware's axis 0 is yaw)"
|
||||||
|
ask imu "IMU model, or none" none; IMU_A="$REPLY_VAL"
|
||||||
|
ask env "Ambient sensor model, or none" none; ENV_A="$REPLY_VAL"
|
||||||
|
ask cameras "Cameras (rgb, or none)" "$([[ $HAVE_VIMBA -eq 1 ]] && echo rgb || echo none)"; CAMS_A="$REPLY_VAL"
|
||||||
|
KIT_ARGS=(--stdout --axes "$AXES_A" --imu "$IMU_A" --env "$ENV_A" --cameras "$CAMS_A")
|
||||||
|
if (( S_MCU_N > 1 )); then
|
||||||
|
echo "Several MCU candidates:"; nl -w2 -s') ' <<< "$S_MCU"
|
||||||
|
ask serial "Which is the motor MCU (full path)"
|
||||||
|
KIT_ARGS+=(--serial "$REPLY_VAL")
|
||||||
|
elif (( S_MCU_N == 0 )); then
|
||||||
|
ask serial "No motor MCU found - its /dev/serial/by-id path"
|
||||||
|
KIT_ARGS+=(--serial "$REPLY_VAL")
|
||||||
|
fi
|
||||||
|
if [[ $CAMS_A != none && -z $S_CAM ]]; then
|
||||||
|
ask camera_id "Camera DEV_ id (none found; the SDK may need a reboot first)"
|
||||||
|
KIT_ARGS+=(--camera-id "$REPLY_VAL")
|
||||||
|
fi
|
||||||
|
[[ $IMU_A != none ]] && { ask imu_device "IMU serial device" "$KIT_IMU_DEVICE"; KIT_ARGS+=(--imu-device "$REPLY_VAL"); }
|
||||||
|
if [[ $ENV_A != none ]]; then
|
||||||
|
ask i2c_device "Ambient sensor I2C bus (check it with i2cdetect -F)" "$KIT_ENV_DEVICE"
|
||||||
|
KIT_ARGS+=(--i2c-device "$REPLY_VAL")
|
||||||
|
fi
|
||||||
|
if choose_pins; then KIT_ARGS+=(--loom standard)
|
||||||
|
else
|
||||||
|
echo "Then give each pin (GPn number):"
|
||||||
|
keys=(); [[ $KIT_FW_ENV == *rp2040* ]] && keys+=(spi_sck spi_mosi spi_miso)
|
||||||
|
for ax in ${AXES_A//,/ }; do keys+=("${ax}_cs" "${ax}_en"); done
|
||||||
|
for k in "${keys[@]}"; do ask "pin.$k" " $k"; KIT_ARGS+=(--pin "$k=$REPLY_VAL"); done
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
echo "The drivetrain, per axis. None of these can be probed and none is guessed:"
|
||||||
|
echo "answer 'unknown' for anything not yet measured, and the axis will refuse to"
|
||||||
|
echo "move until it is filled in (a wrong value can cook a driver or shear a stop)."
|
||||||
|
for ax in ${AXES_A//,/ }; do
|
||||||
|
for key in "${KIT_AXIS_KEYS[@]}"; do
|
||||||
|
case $key in
|
||||||
|
has_encoder) pat='^(true|false)$' ;;
|
||||||
|
sw_mode) pat='^[0-9]+$' ;; # decimal: the host reads no hex here
|
||||||
|
*) pat='^[0-9]+(\.[0-9]+)?$' ;;
|
||||||
|
esac
|
||||||
|
explain "$(kit_axis_todo "$key")"
|
||||||
|
ask_value "$ax.$key" " $ax $key" "$pat"
|
||||||
|
[[ -n $REPLY_VAL ]] && KIT_ARGS+=(--motor "$ax.$key=$REPLY_VAL")
|
||||||
|
done
|
||||||
|
done
|
||||||
|
"$KIT/install.sh" "${KIT_ARGS[@]}" > "$WORK/hardware.ini" 2> "$WORK/kit.err" || { cat "$WORK/kit.err" >&2; die "the kit could not generate hardware.ini"; }
|
||||||
|
HW_NEW="$WORK/hardware.ini"
|
||||||
|
if grep -q '^\[Pins\]' "$HW_NEW"; then
|
||||||
|
python3 "$FW/tools/gen_wiring.py" --check "$HW_NEW" --env "$KIT_FW_ENV" > /dev/null ||
|
||||||
|
die "those pins are not usable on this board (above); nothing was written"
|
||||||
|
fi
|
||||||
|
plan hardware.ini create "$HW ($(grep -c '^; TODO' "$HW_NEW" || true) TODO)"
|
||||||
|
else
|
||||||
|
# An existing file is the tower's record: never regenerated, only topped up.
|
||||||
|
if ! grep -q '^\[Pins\]' "$HW"; then
|
||||||
|
echo; echo "hardware.ini has no [Pins], and the firmware cannot be built without it."
|
||||||
|
choose_pins && HW_APPEND_PINS=1 ||
|
||||||
|
warn "add [Pins] to $HW by hand (the format is in config/hardware.example.ini)"
|
||||||
|
fi
|
||||||
|
cur_serial="$(ini_get "$HW" Serial device 2>/dev/null || true)"
|
||||||
|
if (( S_MCU_N == 1 )) && [[ $S_MCU != "$cur_serial" && $S_MCU != *MicroPython* ]]; then
|
||||||
|
echo; echo "The motor MCU is now at $S_MCU, but hardware.ini says ${cur_serial:-nothing}."
|
||||||
|
ask_yn fix_serial "Update [Serial] device?" y && HW_SETS+=("Serial.device=$S_MCU")
|
||||||
|
fi
|
||||||
|
mapfile -t TODOS < <(hw_todos | grep -v '^\[Pins\]' || true)
|
||||||
|
TODO_LEFT=${#TODOS[@]}
|
||||||
|
if (( ${#TODOS[@]} )); then
|
||||||
|
echo; echo "hardware.ini still has gaps. An axis with any of these missing will not move."
|
||||||
|
# Which [Motor.<axis>] each TODO sits in.
|
||||||
|
while read -r ax key; do
|
||||||
|
[[ -z $key ]] && continue
|
||||||
|
case $key in
|
||||||
|
has_encoder) pat='^(true|false)$' ;; sw_mode) pat='^[0-9]+$' ;;
|
||||||
|
*) pat='^[0-9]+(\.[0-9]+)?$' ;;
|
||||||
|
esac
|
||||||
|
explain "$(kit_axis_todo "$key")"
|
||||||
|
ask_value "$ax.$key" " $ax $key" "$pat"
|
||||||
|
[[ -n $REPLY_VAL ]] && { HW_FILLS+=("$ax $key $REPLY_VAL"); TODO_LEFT=$((TODO_LEFT - 1)); }
|
||||||
|
done < <(awk '/^\[Motor\./ { ax = $0; gsub(/^\[Motor\.|\].*$/, "", ax) }
|
||||||
|
/^; TODO / && ax != "" { print ax, $3 }' "$HW")
|
||||||
|
fi
|
||||||
|
what=""
|
||||||
|
[[ $HW_APPEND_PINS -eq 1 ]] && what+="add the standard [Pins]; "
|
||||||
|
(( ${#HW_FILLS[@]} )) && what+="fill ${#HW_FILLS[@]} TODO(s); "
|
||||||
|
(( ${#HW_SETS[@]} )) && what+="set ${HW_SETS[*]}"
|
||||||
|
[[ -n $what ]] && plan hardware.ini change "$what" || plan hardware.ini skip "present, nothing to change"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# What hardware.ini will say once written - later steps depend on it.
|
||||||
|
HW_VIEW="$HW"; [[ -n $HW_NEW ]] && HW_VIEW="$HW_NEW"
|
||||||
|
steps_for() { ini_get "$HW_VIEW" "Motor.$1" steps_per_rev 2>/dev/null || true; }
|
||||||
|
declare -A STEPS_OVR=()
|
||||||
|
for f in "${HW_FILLS[@]}"; do # a TODO answered now counts too
|
||||||
|
read -r fa fk fv <<< "$f"
|
||||||
|
[[ $fk == steps_per_rev ]] && STEPS_OVR[$fa]="$fv"
|
||||||
|
done
|
||||||
|
declared_axes="$(ini_get "$HW_VIEW" Hardware axes 2>/dev/null | tr -d ' ' || echo yaw)"
|
||||||
|
|
||||||
|
# config.ini: what this tower DOES.
|
||||||
|
declare -a CFG_SETS=()
|
||||||
|
CFG_SEED=0 CRED_WRITE=0
|
||||||
|
if [[ ! -r $CFG ]]; then
|
||||||
|
CFG_SEED=1
|
||||||
|
echo; echo "There is no config.ini yet."
|
||||||
|
while :; do
|
||||||
|
ask tower_name "Tower name (it is in every MQTT topic)"
|
||||||
|
case "${REPLY_VAL,,}" in ""|dev|exampletower|change_me) warn "that is a placeholder, not a tower name"; unset "ANS[tower_name]"
|
||||||
|
[[ $NONINTERACTIVE -eq 1 ]] && die "tower_name is a placeholder" ;; *) break ;; esac
|
||||||
|
done
|
||||||
|
CFG_SETS+=("General.tower_name=$REPLY_VAL")
|
||||||
|
if ask_yn mqtt "Report over MQTT?" y; then
|
||||||
|
CFG_SETS+=("Features.enable_mqtt=true")
|
||||||
|
ask broker "Broker address (each tower runs its own RabbitMQ)" 127.0.0.1
|
||||||
|
CFG_SETS+=("Network.zkms_server_ip=$REPLY_VAL")
|
||||||
|
if [[ ! -r $CRED ]] && ! sudo test -e "$CRED" 2>/dev/null; then
|
||||||
|
ask mqtt_user "MQTT user"; MQTT_USER="$REPLY_VAL"
|
||||||
|
ask_secret mqtt_pw "MQTT password"; MQTT_PW="$REPLY_VAL"
|
||||||
|
CRED_WRITE=1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
CFG_SETS+=("Features.enable_mqtt=false")
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
tn="$(ini_get "$CFG" General tower_name 2>/dev/null || true)"
|
||||||
|
case "${tn,,}" in ""|dev|exampletower)
|
||||||
|
echo; echo "config.ini's tower_name is '$tn' - a placeholder, or another rig's copy."
|
||||||
|
ask tower_name "Tower name"; CFG_SETS+=("General.tower_name=$REPLY_VAL") ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
# The centre each axis needs: its range is centre +/- half the travel, so a
|
||||||
|
# centre below half the travel puts the negative end under zero.
|
||||||
|
for ax in ${declared_axes//,/ }; do
|
||||||
|
steps="${STEPS_OVR[$ax]:-$(steps_for "$ax")}"
|
||||||
|
[[ $steps =~ ^[0-9]+$ ]] || continue
|
||||||
|
have="$(ini_get "$CFG" Motor "${ax}_center_count" 2>/dev/null || echo 500000)"
|
||||||
|
if (( have <= steps / 2 )); then CFG_SETS+=("Motor.${ax}_center_count=$(kit_center_for "$steps")"); fi
|
||||||
|
done
|
||||||
|
# The LPM node, so `test hardware camera` checks it rather than skipping.
|
||||||
|
if [[ -n $S_LPM ]] && [[ -z $(ini_get "$CFG" Test usb3_lpm_path 2>/dev/null || true) ]]; then
|
||||||
|
CFG_SETS+=("Test.usb3_lpm_path=$S_LPM")
|
||||||
|
fi
|
||||||
|
if [[ $CFG_SEED -eq 1 ]]; then plan config.ini create "seed from the example: ${CFG_SETS[*]}"
|
||||||
|
elif (( ${#CFG_SETS[@]} )); then plan config.ini change "set ${CFG_SETS[*]}"
|
||||||
|
else plan config.ini skip "present, nothing to change"; fi
|
||||||
|
[[ $CRED_WRITE -eq 1 ]] && plan credentials create "$CRED (0640 root:$(id -gn))"
|
||||||
|
|
||||||
|
# PlatformIO.
|
||||||
|
if [[ -n $S_PIO ]] && "$S_PIO" --version >/dev/null 2>&1; then plan platformio skip "$("$S_PIO" --version)"
|
||||||
|
else plan platformio change "install PlatformIO Core into ~/.platformio"; fi
|
||||||
|
|
||||||
|
# Camera profiles: the program will not start a real camera without them.
|
||||||
|
PROFDIR="$REPO/config/camera_profiles/rgb"
|
||||||
|
cams="$(ini_get "$HW_VIEW" Hardware cameras 2>/dev/null || echo none)"
|
||||||
|
if [[ $cams == none || -z $cams ]]; then plan profiles skip "no camera declared"
|
||||||
|
elif [[ -r $PROFDIR/standard.xml && -r $PROFDIR/night.xml ]]; then plan profiles skip "standard + night present"
|
||||||
|
elif [[ $HAVE_VIMBA -eq 0 ]]; then plan profiles skip "no Vimba SDK: export later"
|
||||||
|
else plan profiles change "export night, then standard, from the camera"; fi
|
||||||
|
|
||||||
|
# Firmware: flash only what differs, and never onto an axis that would sit in
|
||||||
|
# NEEDCFG because hardware.ini cannot answer it.
|
||||||
|
FW_BLOCK=""
|
||||||
|
if [[ -n $HW_NEW ]]; then
|
||||||
|
todo_n="$(grep -c '^; TODO' "$HW_NEW" || true)"
|
||||||
|
else
|
||||||
|
todo_n=$TODO_LEFT
|
||||||
|
grep -q '^\[Pins\]' "$HW" || [[ $HW_APPEND_PINS -eq 1 ]] || todo_n=$((todo_n + 1))
|
||||||
|
fi
|
||||||
|
(( todo_n > 0 )) && FW_BLOCK="hardware.ini still has $todo_n gap(s): the firmware would sit in NEEDCFG, or not build"
|
||||||
|
want_pins="$( (HW="$HW_VIEW"; [[ $HW_APPEND_PINS -eq 1 ]] && { cat "$HW" "$KIT/pins.ini" > "$WORK/hw_pins.ini"; HW="$WORK/hw_pins.ini"; }; declared_pins) )"
|
||||||
|
if [[ -n $FW_BLOCK ]]; then plan firmware blocked "$FW_BLOCK"
|
||||||
|
elif [[ -n $S_FW_NOW && ${S_FW_NOW%% *} == "build=$S_FW_REV" && ${S_FW_NOW#* } == "$want_pins" ]]; then
|
||||||
|
plan firmware skip "running $S_FW_REV, built with the declared pins"
|
||||||
|
else
|
||||||
|
plan firmware change "build $S_FW_REV and flash it (running: ${S_FW_NOW:-unknown}; declared $want_pins)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
plan test run "test hardware (moves nothing)"
|
||||||
|
plan homing ask "asked at the end: home the gimbal and take a short capture"
|
||||||
|
|
||||||
|
UNIT_TEXT="[Unit]
|
||||||
|
Description=Fire Gimbal Control
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
# fgc runs the program in a tmux session, so 'scripts/fgc attach' still opens
|
||||||
|
# the dashboard. oneshot + RemainAfterExit: fgc start returns once it is up.
|
||||||
|
Type=oneshot
|
||||||
|
RemainAfterExit=yes
|
||||||
|
User=$USER
|
||||||
|
WorkingDirectory=$REPO
|
||||||
|
# --init homes at every start: the firmware keeps no position across a power cycle.
|
||||||
|
ExecStart=$REPO/scripts/fgc start --init --start
|
||||||
|
ExecStop=$REPO/scripts/fgc stop
|
||||||
|
TimeoutStartSec=120
|
||||||
|
TimeoutStopSec=60
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target"
|
||||||
|
if [[ -r $UNIT ]] && [[ "$(cat "$UNIT")" == "$UNIT_TEXT" ]] && [[ $S_SVC_ENABLED == enabled && $S_LINGER == yes ]]; then
|
||||||
|
plan service skip "installed and enabled"
|
||||||
|
else
|
||||||
|
echo
|
||||||
|
echo "The boot service starts the program at every boot WITH --init: it homes the"
|
||||||
|
echo "gimbal unattended each time the tower powers up."
|
||||||
|
if ask_yn service "Install and enable it?" y; then
|
||||||
|
plan service change "write $UNIT, enable it, enable lingering for $USER"
|
||||||
|
else plan service skip "declined"; fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------- plan ---
|
||||||
|
hr "plan"
|
||||||
|
for s in "${STEPS[@]}"; do printf ' %-14s %-8s %s\n' "$s" "${DO[$s]}" "${WHAT[$s]}"; done
|
||||||
|
if [[ -n $HW_NEW ]]; then echo; echo "hardware.ini to be written:"; sed 's/^/ | /' "$HW_NEW"; fi
|
||||||
|
if [[ $MODE == dry-run ]]; then echo; say "dry run: nothing changed"; exit 0; fi
|
||||||
|
echo
|
||||||
|
ask_yn confirm "Apply this plan?" n || { say "nothing changed"; exit 0; }
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ apply ---
|
||||||
|
CHANGED=()
|
||||||
|
done_step() { CHANGED+=("$1"); }
|
||||||
|
# Not `sudo -v`: that asks for a password whenever ANY of the user's sudo rules
|
||||||
|
# needs one, even beside a NOPASSWD rule that covers every command used here.
|
||||||
|
sudo -n true 2>/dev/null || { tty_ok && sudo true < /dev/tty; } || die "sudo is needed for the system steps"
|
||||||
|
|
||||||
|
# The app holds the serial port and the camera. Stopped once, restarted once at
|
||||||
|
# the end, the way it was running.
|
||||||
|
WAS_RUNNING=$S_RUNNING
|
||||||
|
stop_app() {
|
||||||
|
if [[ $S_SVC_ACTIVE == active ]]; then sudo systemctl stop fire-gimbal-control; S_SVC_ACTIVE=inactive
|
||||||
|
elif app_running; then "$FGC" stop; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ ${DO[packages]} == change ]]; then hr packages; sudo "$KIT/install.sh" --deps; done_step packages; fi
|
||||||
|
if [[ ${DO[preconditions]} == change ]]; then hr preconditions; sudo "$KIT/install.sh" --preconditions; done_step preconditions; fi
|
||||||
|
if [[ ${DO[vimba]} == change ]]; then hr vimba; sudo "$KIT/install.sh" --vimba "$VIMBA_SRC"; done_step vimba; fi
|
||||||
|
|
||||||
|
hr "host build"
|
||||||
|
( cd "$REPO" && GIT_REV="$S_SW_REV" cmake -S . -B build -DWITH_MQTT=ON \
|
||||||
|
-DWITH_VIMBA=$([[ -d /opt/VimbaX/cti ]] && echo ON || echo OFF) > "$WORK/cmake.log" 2>&1 ) ||
|
||||||
|
{ tail -20 "$WORK/cmake.log" >&2; die "cmake configure failed"; }
|
||||||
|
( cd "$REPO" && cmake --build build -j"$(nproc)" 2>&1 | tail -3 )
|
||||||
|
[[ -x $BIN && -x $FGC_CONFIG ]] || die "the build did not produce the program and fgc-config"
|
||||||
|
done_step build
|
||||||
|
|
||||||
|
if [[ ${DO[hardware.ini]} == create ]]; then
|
||||||
|
hr hardware.ini
|
||||||
|
sudo install -d -m 0755 "$ETC"
|
||||||
|
sudo install -m 0644 "$HW_NEW" "$HW"
|
||||||
|
say "wrote $HW"; done_step hardware.ini
|
||||||
|
elif [[ ${DO[hardware.ini]} == change ]]; then
|
||||||
|
hr hardware.ini
|
||||||
|
sudo cp -p "$HW" "$HW.bak-$(date +%Y%m%d-%H%M%S)"
|
||||||
|
if [[ $HW_APPEND_PINS -eq 1 ]]; then
|
||||||
|
{ printf '\n'; cat "$KIT/pins.ini"; } | sudo tee -a "$HW" > /dev/null; say "added [Pins]"
|
||||||
|
fi
|
||||||
|
if (( ${#HW_FILLS[@]} )); then
|
||||||
|
# In place of the ; TODO and its explanation, so the value sits where the
|
||||||
|
# gap was - not appended in a second [Motor.<axis>] block at the end.
|
||||||
|
printf '%s\n' "${HW_FILLS[@]}" > "$WORK/fills"
|
||||||
|
awk 'NR == FNR { v[$1 "." $2] = $3; next }
|
||||||
|
/^\[/ { ax = ""; if ($0 ~ /^\[Motor\./) { ax = $0; gsub(/^\[Motor\.|\].*$/, "", ax) } skip = 0 }
|
||||||
|
skip && /^; / { next }
|
||||||
|
{ skip = 0 }
|
||||||
|
/^; TODO / && ((ax "." $3) in v) { printf "%-18s = %s\n", $3, v[ax "." $3]; skip = 1; next }
|
||||||
|
{ print }' "$WORK/fills" "$HW" > "$WORK/hw_filled"
|
||||||
|
sudo install -m 0644 "$WORK/hw_filled" "$HW"
|
||||||
|
say "filled ${#HW_FILLS[@]} TODO(s)"
|
||||||
|
fi
|
||||||
|
if (( ${#HW_SETS[@]} )); then
|
||||||
|
args=(); for s in "${HW_SETS[@]}"; do args+=(--set "$s"); done
|
||||||
|
# hardware.ini is validated together with config.ini when there is one.
|
||||||
|
cfg_for_check="$CFG"; [[ -r $CFG ]] || cfg_for_check="$REPO/config/config.example.ini"
|
||||||
|
sudo "$FGC_CONFIG" set "$HW" --config "$cfg_for_check" --hardware "$HW" "${args[@]}" \
|
||||||
|
--provenance "$(date -u +%Y-%m-%dT%H:%MZ), install-tower"
|
||||||
|
fi
|
||||||
|
done_step hardware.ini
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${DO[credentials]:-} == create ]]; then
|
||||||
|
hr credentials
|
||||||
|
printf 'FGC_MQTT_USER=%s\nFGC_MQTT_PW=%s\n' "$MQTT_USER" "$MQTT_PW" > "$WORK/cred"
|
||||||
|
sudo install -m 0640 -o root -g "$(id -gn)" "$WORK/cred" "$CRED"
|
||||||
|
say "wrote $CRED"; done_step credentials
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${DO[config.ini]} != skip ]]; then
|
||||||
|
hr config.ini
|
||||||
|
args=(); for s in "${CFG_SETS[@]}"; do args+=(--set "$s"); done
|
||||||
|
if [[ $CFG_SEED -eq 1 ]]; then
|
||||||
|
"$FGC_CONFIG" seed --example "$REPO/config/config.example.ini" --out "$CFG" --hardware "$HW" \
|
||||||
|
"${args[@]}" \
|
||||||
|
--drop "Network.mqtt_user=comes from $CRED (FGC_MQTT_USER)" \
|
||||||
|
--drop "Network.mqtt_pw=comes from $CRED (FGC_MQTT_PW)"
|
||||||
|
else
|
||||||
|
"$FGC_CONFIG" set "$CFG" --config "$CFG" --hardware "$HW" "${args[@]}" \
|
||||||
|
--provenance "$(date -u +%Y-%m-%dT%H:%MZ), install-tower"
|
||||||
|
fi
|
||||||
|
done_step config.ini
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${DO[platformio]} == change ]]; then
|
||||||
|
hr platformio
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/platformio/platformio-core-installer/master/get-platformio.py -o "$WORK/get-platformio.py"
|
||||||
|
# Its temp-dir finalizer throws a harmless traceback after the work is done;
|
||||||
|
# the install is judged by whether pio answers.
|
||||||
|
python3 "$WORK/get-platformio.py" > "$WORK/pio.log" 2>&1 || true
|
||||||
|
S_PIO="$(pio_path)" && "$S_PIO" --version || { tail -20 "$WORK/pio.log" >&2; die "PlatformIO did not install"; }
|
||||||
|
done_step platformio
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${DO[profiles]} == change ]]; then
|
||||||
|
hr "camera profiles"
|
||||||
|
stop_app
|
||||||
|
[[ -x $HOME/.venvs/vmbpy/bin/python ]] || {
|
||||||
|
python3 -m venv "$HOME/.venvs/vmbpy"
|
||||||
|
"$HOME/.venvs/vmbpy/bin/pip" install -q /opt/VimbaX/api/python/vmbpy-*.whl
|
||||||
|
}
|
||||||
|
for prof in night standard; do
|
||||||
|
( cd "$REPO" && GENICAM_GENTL64_PATH=/opt/VimbaX/cti "$HOME/.venvs/vmbpy/bin/python" \
|
||||||
|
scripts/export_camera_profile.py --profile "$prof" )
|
||||||
|
done
|
||||||
|
done_step profiles
|
||||||
|
fi
|
||||||
|
|
||||||
|
hr "config check"
|
||||||
|
"$FGC_CONFIG" check --config "$CFG" --hardware "$HW"
|
||||||
|
|
||||||
|
if [[ ${DO[firmware]} == change ]]; then
|
||||||
|
hr firmware
|
||||||
|
stop_app
|
||||||
|
port="$(ini_get "$HW" Serial device 2>/dev/null || true)"
|
||||||
|
[[ -e $port ]] || port="$(probe_mcu | head -1)"
|
||||||
|
PIO="$S_PIO" GIT_REV="$S_FW_REV" "$FW/tools/flash.sh" --env "$KIT_FW_ENV" --port "$port"
|
||||||
|
# The by-id name comes from the running image, so a first flash changes it.
|
||||||
|
sleep 3
|
||||||
|
new="$(probe_mcu)"
|
||||||
|
if [[ $(grep -c . <<< "$new") -eq 1 && $new != "$port" ]]; then
|
||||||
|
say "the MCU is now $new"
|
||||||
|
sudo "$FGC_CONFIG" set "$HW" --config "$CFG" --hardware "$HW" --set "Serial.device=$new" \
|
||||||
|
--provenance "$(date -u +%Y-%m-%dT%H:%MZ), install-tower, after flashing"
|
||||||
|
fi
|
||||||
|
done_step firmware
|
||||||
|
elif [[ ${DO[firmware]} == blocked ]]; then
|
||||||
|
warn "firmware not flashed: $FW_BLOCK"
|
||||||
|
fi
|
||||||
|
|
||||||
|
hr "test hardware"
|
||||||
|
S_SVC_ENABLED="$(systemctl is-enabled fire-gimbal-control 2>/dev/null || true)"
|
||||||
|
run_hardware_test
|
||||||
|
[[ $TEST_RC == 0 ]] && say "test hardware: PASS" || warn "test hardware: exit $TEST_RC - read the report above"
|
||||||
|
|
||||||
|
# Before the gate, so that homing - if a person says yes - is done by the
|
||||||
|
# service itself, and happens once rather than again at its first start.
|
||||||
|
if [[ ${DO[service]} == change ]]; then
|
||||||
|
hr "boot service"
|
||||||
|
printf '%s\n' "$UNIT_TEXT" > "$WORK/unit"
|
||||||
|
sudo install -m 0644 "$WORK/unit" "$UNIT"
|
||||||
|
# Without lingering, /run/user/<uid> does not exist until someone logs in, so
|
||||||
|
# a service-started app takes its lock in /tmp, and an ssh session then looks
|
||||||
|
# in /run/user, sees nothing, and `fgc status` says "not running".
|
||||||
|
sudo loginctl enable-linger "$USER"
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable fire-gimbal-control
|
||||||
|
done_step service
|
||||||
|
fi
|
||||||
|
SVC_ON=0; [[ $(systemctl is-enabled fire-gimbal-control 2>/dev/null) == enabled ]] && SVC_ON=1
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- the gate ---
|
||||||
|
HOMED="not attempted"
|
||||||
|
hr "homing"
|
||||||
|
echo "Homing drives each axis into its endstops. On a new or changed drivetrain it"
|
||||||
|
echo "is the step that can break something, so it needs a person at the rig."
|
||||||
|
if [[ $TEST_RC != 0 ]]; then
|
||||||
|
warn "not offered: test hardware did not pass"
|
||||||
|
elif ask_yn home "Someone is at the rig with a hand on the power - home now and take a short capture?" n; then
|
||||||
|
stop_app
|
||||||
|
before="$(latest_log)"
|
||||||
|
if [[ $SVC_ON -eq 1 ]]; then sudo systemctl start fire-gimbal-control
|
||||||
|
else ( cd "$REPO" && "$FGC" start --init --start ) > /dev/null; fi
|
||||||
|
say "homing (this can take a few minutes)"
|
||||||
|
log=""; for _ in $(seq 1 30); do log="$(latest_log)"; [[ -n $log && $log != "$before" ]] && break; sleep 1; done
|
||||||
|
outdir=""; ok=0
|
||||||
|
for _ in $(seq 1 420); do
|
||||||
|
grep -q 'Gimbal homed' "$log" 2>/dev/null && ok=1
|
||||||
|
outdir="$(sed -n 's/.*capture output dir: //p' "$log" 2>/dev/null | tail -1)"
|
||||||
|
if [[ $ok -eq 1 && -n $outdir ]] && find "$outdir" -name '*.jxl' -newer "$log" 2>/dev/null | grep -q .; then ok=2; break; fi
|
||||||
|
app_running || break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
grep -E 'homed .* travel|counts_per_deg|Gimbal homed|ERROR' "$log" 2>/dev/null | tail -6 | sed 's/^/ /'
|
||||||
|
case $ok in
|
||||||
|
2) HOMED="homed and captured ($(find "$outdir" -name '*.jxl' -newer "$log" | wc -l) image(s) in $outdir)"; say "$HOMED" ;;
|
||||||
|
1) HOMED="homed, but no image within the wait"; warn "$HOMED" ;;
|
||||||
|
*) HOMED="did not home - read the log above"; warn "$HOMED" ;;
|
||||||
|
esac
|
||||||
|
# Left running: that is the tower in service.
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Put the program back if it is not running. Not under the service unless it
|
||||||
|
# has just homed with someone watching: the service homes by itself.
|
||||||
|
hr "restart"
|
||||||
|
if app_running; then
|
||||||
|
say "running"
|
||||||
|
elif [[ $WAS_RUNNING -eq 1 ]]; then
|
||||||
|
read -r -a restart_args <<< "${S_ARGS:-}"
|
||||||
|
( cd "$REPO" && "$FGC" start "${restart_args[@]}" ) > /dev/null && say "restarted as it was: fgc start${S_ARGS:+ $S_ARGS}"
|
||||||
|
else
|
||||||
|
say "left stopped, as it was"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------- report ---
|
||||||
|
S_SVC_ENABLED="$(systemctl is-enabled fire-gimbal-control 2>/dev/null || true)"
|
||||||
|
S_SVC_ACTIVE="$(systemctl is-active fire-gimbal-control 2>/dev/null || true)"
|
||||||
|
S_LINGER="$(loginctl show-user "$USER" -p Linger --value 2>/dev/null || echo no)"
|
||||||
|
S_USBFS="$(cat /sys/module/usbcore/parameters/usbfs_memory_mb 2>/dev/null || echo '?')"
|
||||||
|
S_LPM="$(probe_camera_lpm_path)"
|
||||||
|
verify_rc=0; verify || verify_rc=$?
|
||||||
|
|
||||||
|
hr "still open"
|
||||||
|
open=0
|
||||||
|
while read -r t; do [[ -n $t ]] && { echo " - hardware.ini: $t"; open=1; }; done < <(hw_todos)
|
||||||
|
[[ $HOMED == homed* ]] || { echo " - homing: $HOMED"; open=1; }
|
||||||
|
[[ $HOMED == homed* ]] && { echo " - calibration: run 'gimbal heading setup' to tie yaw to true north"; open=1; }
|
||||||
|
[[ $(ini_get "$CFG" Features enable_mqtt 2>/dev/null) == true ]] &&
|
||||||
|
{ echo " - MQTT: confirm topics reach the broker (a tower with no broker runs degraded, reporting to nobody)"; open=1; }
|
||||||
|
echo " - reboot once and run: scripts/install-tower --verify (the USB preconditions must survive it)"
|
||||||
|
|
||||||
|
printf '%s %s sw %s fw %s changed: %s test %s homing: %s\n' \
|
||||||
|
"$(date -Is)" "$USER" "$S_SW_REV" "$S_FW_REV" "${CHANGED[*]:-nothing}" "${TEST_RC:-?}" "$HOMED" |
|
||||||
|
sudo tee -a "$INSTALL_LOG" > /dev/null
|
||||||
|
say "recorded in $INSTALL_LOG"
|
||||||
|
exit $(( verify_rc > 0 ? 1 : 0 ))
|
||||||
Loading…
Reference in New Issue