#!/usr/bin/env bash # ============================================================================= # preflight.sh — run this ON A NEW ROBOT before installing the hand stack. # # Answers one question: can this robot run the Inspire hand stack, and if not, # what exactly is missing? Checks hardware, Docker, the build prerequisites and # the per-robot settings that silently misbehave rather than fail loudly. # # ./tools/preflight.sh # # Exit 0 = at least one install path is open (load a prebuilt image, or build # here). Exit 1 = blocked; every blocker is printed with its fix. # # It changes NOTHING. Safe to run on a live robot. # ============================================================================= set -uo pipefail # not -e: a failing probe is a result, not a crash HERE="$(cd "$(dirname "$0")/.." && pwd)" IFACE="${IFACE:-eth0}" IMG="${IMG:-inspire-hand:latest}" PORT="${PORT:-8088}" g=$'\033[32m'; r=$'\033[31m'; y=$'\033[33m'; d=$'\033[2m'; n=$'\033[0m' FAIL=0; WARN=0 pass() { printf ' %sPASS%s %-34s %s\n' "$g" "$n" "$1" "${2:-}"; } fail() { printf ' %sMISS%s %-34s %s\n' "$r" "$n" "$1" "${2:-}"; FAIL=$((FAIL+1)); } warn() { printf ' %sWARN%s %-34s %s\n' "$y" "$n" "$1" "${2:-}"; WARN=$((WARN+1)); } hint() { printf ' %s%s%s\n' "$d" "$1" "$n"; } head_() { printf '\n%s\n' "$1"; } head_ "A. Platform" arch="$(uname -m)" [ "$arch" = "aarch64" ] && pass "architecture" "$arch" \ || warn "architecture" "$arch — the image is arm64; this is not the robot" pass "kernel / os" "$(uname -r) · $( . /etc/os-release 2>/dev/null; echo "${PRETTY_NAME:-unknown}")" free_gb="$(df -Pk / | awk 'NR==2{printf "%.1f", $4/1048576}')" awk -v f="$free_gb" 'BEGIN{exit !(f>4)}' && pass "disk free on /" "${free_gb} GB" \ || warn "disk free on /" "${free_gb} GB — a build wants ~4 GB" head_ "B. Docker" HAVE_DOCKER=0 if command -v docker >/dev/null 2>&1; then if docker info >/dev/null 2>&1; then HAVE_DOCKER=1; pass "docker" "$(docker version --format '{{.Server.Version}}' 2>/dev/null)" else fail "docker" "installed but not usable by $(id -un)" hint "sudo usermod -aG docker $(id -un) # then log out and back in" fi else fail "docker" "not installed" hint "sudo apt install docker.io" fi head_ "C. Hands hardware" ch340="$(lsusb 2>/dev/null | grep -c '1a86:7523')" [ "$ch340" -ge 2 ] && pass "CH340 RS-485 adapters" "$ch340 found" \ || fail "CH340 RS-485 adapters" "$ch340 found, need 2 — check USB + 24V" ttys="$(ls /dev/ttyUSB* 2>/dev/null | tr '\n' ' ')" [ -n "$ttys" ] && pass "serial nodes" "$ttys" || fail "serial nodes" "no /dev/ttyUSB*" # The container runs privileged with /dev mounted, so host permissions do not # block it. They only matter for the native manage.sh path. if [ -n "$ttys" ] && [ ! -r "${ttys%% *}" ]; then warn "tty permissions" "not readable as $(id -un) — fine for Docker, blocks native use" hint "sudo cp udev/99-inspire-hands.rules /etc/udev/rules.d/ && sudo udevadm control --reload" fi if lsusb 2>/dev/null | grep -q '05e3:0749'; then warn "card reader on the hub" "05e3:0749 present — re-enumerates the hands' branch" hint "./start.sh usbfix # installs the udev rule + deauthorizes it" else pass "card reader on the hub" "absent" fi head_ "D. Install path 1 — prebuilt image" IMAGE_OK=0 if [ "$HAVE_DOCKER" = 1 ] && docker image inspect "$IMG" >/dev/null 2>&1; then IMAGE_OK=1 pass "$IMG" "already loaded — no build prerequisites needed" else warn "$IMG" "not loaded on this robot" hint "on a robot that has it: docker save $IMG | gzip > inspire-hand.tar.gz" hint "here: gunzip -c inspire-hand.tar.gz | docker load" fi head_ "E. Install path 2 — build the image here" BUILD_OK=1 need_cmd() { command -v "$1" >/dev/null 2>&1 && pass "$1" "$(command -v "$1")" \ || { fail "$1" "not installed"; BUILD_OK=0; }; } need_cmd git; need_cmd cmake; need_cmd rsync if cc_path="$(command -v gcc || command -v cc)"; then pass "c compiler" "$cc_path" else fail "c compiler" "not installed"; BUILD_OK=0 hint "sudo apt install build-essential" fi SDK="" for c in "${UNITREE_SDK2_DIR:-}" "$HOME/unitree_sdk2" "$HERE/deps/unitree_sdk2"; do [ -n "$c" ] && [ -f "$c/lib/aarch64/libunitree_sdk2.a" ] && { SDK="$c"; break; } done if [ -n "$SDK" ]; then pass "unitree_sdk2 (C++)" "$SDK" else fail "unitree_sdk2 (C++)" "no lib/aarch64/libunitree_sdk2.a anywhere"; BUILD_OK=0 hint "git clone --depth 1 https://github.com/unitreerobotics/unitree_sdk2 ~/unitree_sdk2" hint "or ship it in the project at deps/unitree_sdk2 (build_image.sh looks there)" fi CDDS_INC="" for c in /usr/local/include "$HERE/deps/cyclonedds/install/include"; do [ -d "$c/ddscxx" ] && { CDDS_INC="$c"; break; } done if [ -n "$CDDS_INC" ]; then miss="" for h in dds ddsc ddscxx iceoryx; do [ -d "$CDDS_INC/$h" ] || miss="$miss $h"; done [ -z "$miss" ] && pass "CycloneDDS headers" "$CDDS_INC" \ || { fail "CycloneDDS headers" "missing:$miss"; BUILD_OK=0; } else fail "CycloneDDS headers" "not in /usr/local/include nor deps/cyclonedds"; BUILD_OK=0 hint "build CycloneDDS 0.10.x with -DCMAKE_INSTALL_PREFIX=/usr/local," hint "or stage its install/ prefix at deps/cyclonedds (build_image.sh looks there)" fi # The idl/ headers are the ones a normal C++ install leaves out, and without # them the image build dies late — inside the pip cyclonedds compile. CDDS_SRC="${CYCLONEDDS_SRC:-$HOME/cyclonedds}" IDL_INC="" for c in "${CYCLONEDDS_IDL_INCLUDE:-}" /usr/local/include "$CDDS_SRC/install/include" \ "$HERE/deps/cyclonedds/install/include" "$HOME/cyclonedds_ws/install/cyclonedds/include"; do [ -n "$c" ] && [ -f "$c/idl/retcode.h" ] && { IDL_INC="$c/idl"; break; } done if [ -n "$IDL_INC" ]; then pass "CycloneDDS idl headers" "$IDL_INC" else fail "CycloneDDS idl headers" "no idl/retcode.h in any known prefix"; BUILD_OK=0 hint "a ROS2 colcon workspace keeps them at ~/cyclonedds_ws/install/cyclonedds/include/idl," hint "a source build at /install/include/idl; or set CYCLONEDDS_IDL_INCLUDE" fi ls /usr/local/lib/libddsc.so* >/dev/null 2>&1 && pass "CycloneDDS runtime libs" "/usr/local/lib" \ || { fail "CycloneDDS runtime libs" "no libddsc.so"; BUILD_OK=0; } if curl -fsS -m 6 -o /dev/null https://pypi.org/simple/ 2>/dev/null; then pass "internet (pypi)" "reachable — needed by the in-image pip build" else warn "internet (pypi)" "unreachable — a build here will fail at the python DDS step" fi head_ "F. This project" for f in docker/build_image.sh docker/run.sh web/hand_web.py \ recorder/g1_record_replay.py vendor/unitree_sdk2_python/setup.py; do [ -e "$HERE/$f" ] && pass "$f" || { fail "$f" "missing from the copy"; } done head_ "G. Per-robot settings" ip -brief addr show "$IFACE" >/dev/null 2>&1 \ && pass "DDS interface $IFACE" "$(ip -brief addr show "$IFACE" | awk '{print $2, $3}')" \ || warn "DDS interface $IFACE" "not present — arm actions need it (fingers do not)" # The USB pin is this-robot-specific and fails SILENTLY on different hardware: # a wrong pin swaps left and right with no error at all. RIGHT_PATH="${INSPIRE_RIGHT_PATH:-$(sed -n 's/^RIGHT_PATH=.*:-\([^}]*\)}.*/\1/p' "$HERE/docker/run.sh" 2>/dev/null | head -1)}" if [ -n "${RIGHT_PATH:-}" ]; then if [ -d "/sys/bus/usb/devices/$RIGHT_PATH" ]; then warn "INSPIRE_RIGHT_PATH=$RIGHT_PATH" "exists here — but verify it IS the right hand" else warn "INSPIRE_RIGHT_PATH=$RIGHT_PATH" "does NOT exist on this robot" fi hint "best fix, once per robot: give the LEFT hand RS-485 id 2 —" hint " docker exec inspire-hand ./build/hand_setid 1 2" hint " then id (not USB path) decides left/right, permanently." fi if command -v ss >/dev/null 2>&1 && ss -ltn 2>/dev/null | grep -q ":$PORT "; then warn "port $PORT" "already in use — something else is serving it" else pass "port $PORT" "free" fi printf '\n%s\n' "Verdict" if [ "$IMAGE_OK" = 1 ] && [ "$HAVE_DOCKER" = 1 ]; then printf ' %sREADY%s — image is loaded. Run: ./start.sh\n' "$g" "$n"; exit 0 elif [ "$BUILD_OK" = 1 ] && [ "$HAVE_DOCKER" = 1 ]; then printf ' %sREADY%s — no image yet, but this robot can build one. Run: ./start.sh\n' "$g" "$n"; exit 0 else printf ' %sBLOCKED%s — %d missing, %d warnings.\n' "$r" "$n" "$FAIL" "$WARN" printf ' Either load a prebuilt image (section D) or install the section E items.\n' exit 1 fi