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.
121 lines
5.6 KiB
Bash
Executable File
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" "$@"
|