2026-08-04 16:09:17 +04:00

144 lines
5.0 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; }
# 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