kassam 2a78f1b609 Record/replay studio, manual recorder kit, and new-robot install tooling
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.
2026-08-28 20:27:28 +04:00

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