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.
165 lines
6.2 KiB
Bash
Executable File
165 lines
6.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# =============================================================================
|
|
# start.sh — ONE command to bring up the whole hand stack.
|
|
#
|
|
# Starts the Docker container that runs all three pieces together — the RS-485
|
|
# service (inspire_g1), the TCP->DDS bridge (hand_bridge) and the web dashboard
|
|
# on :8088 — then waits until the dashboard actually answers before telling you
|
|
# it is up.
|
|
#
|
|
# ./start.sh start it (builds the image the first time)
|
|
# ./start.sh stop stop the container
|
|
# ./start.sh restart stop + start
|
|
# ./start.sh status is it up, what is it serving, are the hands alive
|
|
# ./start.sh logs follow the container log
|
|
# ./start.sh rebuild force an image rebuild, then start
|
|
#
|
|
# RUN IT ON THE ROBOT. Run it on the workstation and it rsyncs this folder to
|
|
# the robot and runs itself there — the hands, the DDS interface and Docker are
|
|
# all on the robot, so there is nothing for it to do locally.
|
|
#
|
|
# Env: ROBOT=unitree@192.168.123.164 (override for the wifi IP)
|
|
# PORT=8088
|
|
# =============================================================================
|
|
set -euo pipefail
|
|
|
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
ROBOT="${ROBOT:-unitree@192.168.123.164}"
|
|
REMOTE_DIR='~/DFX_inspire_service'
|
|
PORT="${PORT:-8088}"
|
|
IMG="${IMG:-inspire-hand:latest}"
|
|
CMD="${1:-start}"
|
|
|
|
say() { printf '\033[36m>>\033[0m %s\n' "$*"; }
|
|
ok() { printf '\033[32m>>\033[0m %s\n' "$*"; }
|
|
die() { printf '\033[31m>>\033[0m %s\n' "$*" >&2; exit 1; }
|
|
|
|
# The robot is the only place this can run: it owns the hands (/dev/ttyUSB*), the
|
|
# DDS interface to the arm, and Docker. aarch64 is the reliable tell — the
|
|
# workstation is x86_64 and the image is built for arm64.
|
|
is_robot() { [ "$(uname -m)" = "aarch64" ] && command -v docker >/dev/null 2>&1; }
|
|
|
|
remote() {
|
|
say "not the robot — deploying to $ROBOT and running there"
|
|
command -v rsync >/dev/null || die "rsync not installed"
|
|
# build/ is excluded: the image compiles its own binaries, and the host copy is
|
|
# 55MB of artifacts that only the (optional) native path uses.
|
|
rsync -az --delete --exclude='build/' --exclude='docker/.ctx/' \
|
|
-e "ssh -o ConnectTimeout=8" "$HERE/" "$ROBOT:$REMOTE_DIR/"
|
|
# shellcheck disable=SC2029
|
|
ssh -o ConnectTimeout=8 -t "$ROBOT" "cd $REMOTE_DIR && ./start.sh $CMD"
|
|
exit $?
|
|
}
|
|
|
|
have_image() { docker image inspect "$IMG" >/dev/null 2>&1; } # (kept for status checks)
|
|
|
|
# run.sh tails the container log, which spills the dashboard's HTTP lines over this
|
|
# script's own output. Keep it unless something actually fails.
|
|
run_quiet() {
|
|
local log="/tmp/hand_start.$$.log"
|
|
if ! "$HERE/docker/run.sh" >"$log" 2>&1; then
|
|
cat "$log"; rm -f "$log"; die "docker run failed"
|
|
fi
|
|
rm -f "$log"
|
|
}
|
|
|
|
build() {
|
|
say "building $IMG (compiles the binaries + python DDS in-image; a few minutes)"
|
|
"$HERE/docker/build_image.sh"
|
|
}
|
|
|
|
wait_up() {
|
|
# Poll the dashboard rather than trusting `docker run` — the container can be up
|
|
# while the service inside is still opening the serial ports, and reporting a URL
|
|
# that 404s is worse than waiting three more seconds for it.
|
|
say "waiting for the dashboard..."
|
|
for _ in $(seq 1 40); do
|
|
if curl -fsS "http://127.0.0.1:$PORT/api/status" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
return 1
|
|
}
|
|
|
|
report() {
|
|
local s
|
|
s="$(curl -fsS "http://127.0.0.1:$PORT/api/status" 2>/dev/null || true)"
|
|
[ -n "$s" ] || { echo " dashboard not answering on :$PORT"; return 1; }
|
|
python3 - "$s" <<'PY' 2>/dev/null || echo " $s"
|
|
import json, sys
|
|
d = json.loads(sys.argv[1])
|
|
print(" service=%s bridge=%s iface=%s ports=%s"
|
|
% (d.get("service"), d.get("bridge"), d.get("iface"), ", ".join(d.get("ports") or []) or "none"))
|
|
if d.get("mismatch"):
|
|
print(" WARNING: port drift — the service opened different ttys than are live now")
|
|
PY
|
|
local r
|
|
r="$(curl -fsS "http://127.0.0.1:$PORT/api/rec/status" 2>/dev/null || true)"
|
|
case "$r" in
|
|
*'"ready":true'*) echo " recorder: ready (record/replay panel enabled)" ;;
|
|
*'"ready":false'*) echo " recorder: NOT found — rebuild with ./start.sh rebuild" ;;
|
|
esac
|
|
# Skip docker's own bridge addresses (172.17-19.x) — nothing outside the robot can
|
|
# reach those, and printing them as if they were the dashboard URL wastes a click.
|
|
for ip in $(hostname -I 2>/dev/null); do
|
|
case "$ip" in 172.1[789].*|127.*) continue ;; esac
|
|
echo " http://$ip:$PORT"
|
|
done
|
|
}
|
|
|
|
is_robot || remote
|
|
|
|
case "$CMD" in
|
|
start|restart)
|
|
# ALWAYS build, never "build only if the image is missing". Docker's layer cache makes
|
|
# this a second or two when nothing changed, and seconds when only web/ changed — but
|
|
# skipping it means an edited dashboard silently keeps running the old image, which is
|
|
# a very expensive minute of confusion when a fix "does not work".
|
|
build
|
|
run_quiet
|
|
wait_up || die "dashboard did not come up — ./start.sh logs"
|
|
ok "up"
|
|
report
|
|
;;
|
|
rebuild)
|
|
build
|
|
run_quiet
|
|
wait_up || die "dashboard did not come up — ./start.sh logs"
|
|
ok "rebuilt and up"
|
|
report
|
|
;;
|
|
stop)
|
|
"$HERE/docker/stop.sh"
|
|
;;
|
|
status)
|
|
docker ps --filter name=inspire-hand --format ' {{.Names}} · {{.Status}} · {{.Image}}' | grep . \
|
|
|| echo " inspire-hand is not running"
|
|
report || true
|
|
;;
|
|
logs)
|
|
docker logs -f --tail 60 inspire-hand
|
|
;;
|
|
usbfix)
|
|
# The card reader (05e3:0749) shares the hands' USB hub and power-cycles constantly with
|
|
# no card in it, re-enumerating the branch and renumbering /dev/ttyUSB*. Deauthorizing
|
|
# needs root, which this box asks a password for — hence a subcommand rather than
|
|
# something ./start.sh does silently.
|
|
say "installing the udev rule + deauthorizing the card reader (asks for your password)"
|
|
sudo cp "$HERE/udev/99-inspire-hub-cardreader.rules" /etc/udev/rules.d/
|
|
sudo udevadm control --reload
|
|
for d in /sys/bus/usb/devices/*/; do
|
|
[ "$(cat "$d/idVendor" 2>/dev/null)" = "05e3" ] && \
|
|
[ "$(cat "$d/idProduct" 2>/dev/null)" = "0749" ] && {
|
|
echo 0 | sudo tee "$d/authorized" >/dev/null
|
|
ok "deauthorized $(basename "$d")"
|
|
}
|
|
done
|
|
ls /dev/ttyUSB* 2>/dev/null | sed 's/^/ still present: /'
|
|
;;
|
|
*)
|
|
sed -n '2,26p' "$0" | sed 's/^# \{0,1\}//'
|
|
exit 1
|
|
;;
|
|
esac
|