Dashboard (web/hand_web.py) - Record/replay panel driving g1_record_replay.py as a pty child: take library (replay/download/duplicate/rename/delete/upload/delete-all), pause & resume, and in-take key buttons that grey out in the --fingers modes the recorder ignores (measured: in touch mode the keys change nothing at all). - /api/restart is container-aware: it kills inspire_g1 and lets the supervisor relaunch it. It used to run manage.sh, which started a SECOND inspire_g1 beside the supervised one - two writers on one RS-485 bus - and never returned. - Shape/combo libraries take a .bak on every write, with an undo button. Both files are rewritten in full, so deleting the last entry was unrecoverable. recorder/ - The recorder lives in this project now: one source of truth for the CLI and the dashboard, with pause/resume added to replay. - record.sh picks a runtime by itself (a python with the SDK, the vendored SDK, or the inspire-hand container). bundle.sh packs a ~340KB portable kit. tools/ - preflight.sh: read-only readiness report for a new robot (hardware, docker, build prerequisites, per-robot settings) ending in an install-path verdict. - fetch_deps.sh: stage build dependencies, verifying the libs are aarch64. - export_ui.py: regenerate an embedding app's vendored copy of the UI. docker/ - build_image.sh resolves its dependencies from several layouts: deps/ inside the project, /usr/local, a source install prefix, or a ROS2 colcon workspace (where the idl headers live when /usr/local has none). - web/ is copied in the last layer, so dashboard edits skip the C++ rebuild. - restart=always, and start.sh always builds so an edit cannot silently run a stale image. deps/unitree_sdk2 is vendored so a robot that has never seen the SDK can build. Docs: README quickstart + embedding notes, SETUP_G1 corrected (that udev rule stopped creating /dev/inspire_* symlinks a while ago), ROBOT_README describing a live install.
189 lines
8.3 KiB
Bash
Executable File
189 lines
8.3 KiB
Bash
Executable File
#!/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 <src>/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 <left-port> 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
|