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

121 lines
5.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# =============================================================================
# record.sh — run the recorder WITHOUT the web dashboard.
#
# The backup plan: it finds a working way to run g1_record_replay.py and uses it,
# instead of you remembering which python has the Unitree SDK on which machine.
#
# ./record.sh doctor what's available here, what's missing
# ./record.sh record --output wave --seconds 20
# ./record.sh replay --input wave --speed 0.5
# ./record.sh eth0 record --output wave # explicit interface (else auto)
#
# Any flag the script takes is passed straight through — this adds nothing to the
# recorder, it only starts it.
#
# How it picks a runtime, best first:
# 1. $RECORDER_PY — you said which python to use
# 2. a python that can already import unitree_sdk2py (conda g1_env, system, …)
# 3. ../vendor/unitree_sdk2_python on PYTHONPATH, if that makes it importable
# 4. the inspire-hand container, which has the SDK baked in
# Use --how to see which one it chose without running anything.
#
# Env: IFACE (default: eth0 on the robot, enp3s0 elsewhere)
# =============================================================================
set -uo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
SCRIPT="$HERE/g1_record_replay.py"
VENDOR="$(cd "$HERE/.." 2>/dev/null && pwd)/vendor/unitree_sdk2_python"
CONTAINER="${CONTAINER:-inspire-hand}"
[ "$(uname -m)" = "aarch64" ] && DEF_IFACE=eth0 || DEF_IFACE=enp3s0
IFACE="${IFACE:-$DEF_IFACE}"
g=$'\033[32m'; r=$'\033[31m'; y=$'\033[33m'; d=$'\033[2m'; n=$'\033[0m'
have_sdk() { # $1 = python, $2 = optional PYTHONPATH
PYTHONPATH="${2:-}" "$1" -c 'import unitree_sdk2py' >/dev/null 2>&1
}
# ---- pick a runtime ---------------------------------------------------------
MODE=""; PY=""; PYPATH=""
if [ -n "${RECORDER_PY:-}" ] && have_sdk "$RECORDER_PY"; then
MODE=local; PY="$RECORDER_PY"
else
for cand in "${RECORDER_PY:-}" "$HOME/miniconda3/envs/g1_env/bin/python3" \
"$HOME/anaconda3/envs/g1_env/bin/python3" python3; do
[ -n "$cand" ] || continue
command -v "$cand" >/dev/null 2>&1 || [ -x "$cand" ] || continue
if have_sdk "$cand"; then MODE=local; PY="$cand"; break; fi
# The vendored SDK is pure python, but it still needs the compiled
# cyclonedds bindings — so this only helps where those already exist.
if [ -d "$VENDOR" ] && have_sdk "$cand" "$VENDOR"; then
MODE=local; PY="$cand"; PYPATH="$VENDOR"; break
fi
done
fi
if [ -z "$MODE" ] && command -v docker >/dev/null 2>&1 \
&& docker exec "$CONTAINER" true >/dev/null 2>&1; then
MODE=docker
fi
describe() {
case "$MODE" in
local) printf ' %sruntime%s local python: %s%s\n' "$g" "$n" "$PY" \
"${PYPATH:+ (PYTHONPATH=$PYPATH)}"
printf ' %stakes%s %s/DataG1\n' "$d" "$n" "$PWD" ;;
docker) printf ' %sruntime%s the %s container (SDK baked in)\n' "$g" "$n" "$CONTAINER"
printf ' %stakes%s /opt/recorder/DataG1 in the container = ~/hand_data/DataG1 on the host\n' "$d" "$n" ;;
*) printf ' %sno runtime%s — no python here can import unitree_sdk2py, and the %s container is not running\n' \
"$r" "$n" "$CONTAINER" ;;
esac
}
# ---- doctor -----------------------------------------------------------------
if [ "${1:-}" = "doctor" ] || [ "${1:-}" = "--how" ]; then
printf '\nRecorder runtime\n'; describe
printf '\nDetail\n'
printf ' script %s\n' "$([ -f "$SCRIPT" ] && echo "$SCRIPT" || echo "MISSING")"
printf ' vendored SDK %s\n' "$([ -d "$VENDOR" ] && echo "$VENDOR" || echo "not next to this folder")"
printf ' home pose %s\n' "$([ -f "$HERE/arm_home.jsonl" ] && echo "arm_home.jsonl" || echo "MISSING — replay skips return-to-home")"
printf ' interface %s\n' "$IFACE"
for c in "$HOME/miniconda3/envs/g1_env/bin/python3" python3; do
[ -x "$c" ] || command -v "$c" >/dev/null 2>&1 || continue
if have_sdk "$c"; then s="${g}has unitree_sdk2py${n}"; else s="${y}no unitree_sdk2py${n}"; fi
printf ' python %-46s %s\n' "$c" "$s"
done
if [ "$MODE" = "" ]; then
printf '\n%sFix one of these%s\n' "$y" "$n"
printf ' · conda activate g1_env (workstation — the env that has the SDK)\n'
# Only suggest it where it exists: a standalone bundle has no start.sh.
[ -f "$HERE/../start.sh" ] && \
printf ' · start the hand stack: ../start.sh (then this uses the container)\n'
printf ' · install the bindings: pip install "cyclonedds==0.10.2" && pip install -e %s\n' "$VENDOR"
exit 1
fi
exit 0
fi
[ -f "$SCRIPT" ] || { echo "${r}missing${n} $SCRIPT"; exit 1; }
if [ -z "$MODE" ]; then
printf '%sno way to run the recorder here%s — try: %s doctor\n' "$r" "$n" "$0" >&2
exit 1
fi
# ---- interface: accept it as $1, or insert the default ----------------------
# The recorder takes the interface as its first positional arg. Typing it every
# time is noise, so if the first argument is already an action we insert one.
case "${1:-}" in
record|replay) set -- "$IFACE" "$@" ;;
esac
describe >&2
printf '%s>> %s %s%s\n' "$d" "$(basename "$SCRIPT")" "$*" "$n" >&2
if [ "$MODE" = "docker" ]; then
# -it: the in-take keys (o/c/[/]/;/' and 1-9) and the pause key need a TTY.
exec docker exec -it -w /opt/recorder "$CONTAINER" python3 g1_record_replay.py "$@"
fi
cd "$PWD" || exit 1 # takes land in the CALLER's DataG1, not the script's
exec env ${PYPATH:+PYTHONPATH="$PYPATH"} "$PY" "$SCRIPT" "$@"