#!/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; } # 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) have_image || 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 ;; *) sed -n '2,26p' "$0" | sed 's/^# \{0,1\}//' exit 1 ;; esac