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.
122 lines
6.3 KiB
Bash
Executable File
122 lines
6.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Assemble the build context (pull unitree_sdk2 + CycloneDDS from this robot) and
|
|
# build the self-contained image. Run ON the robot (aarch64).
|
|
# ./docker/build_image.sh
|
|
set -euo pipefail
|
|
|
|
HERE="$(cd "$(dirname "$0")" && pwd)" # .../DFX_inspire_service/docker
|
|
REPO="$(dirname "$HERE")" # .../DFX_inspire_service
|
|
CTX="$HERE/.ctx"
|
|
IMG="${IMG:-inspire-hand:latest}"
|
|
|
|
# The C++ SDK can come from three places, best first: an explicit override, the robot's
|
|
# own checkout, or deps/ shipped with this project — so a robot that has never seen the
|
|
# SDK can still build the image. Same idea for CycloneDDS below.
|
|
SDK="${UNITREE_SDK2_DIR:-}"
|
|
if [ -z "$SDK" ]; then
|
|
for c in "$HOME/unitree_sdk2" "$REPO/deps/unitree_sdk2"; do
|
|
[ -f "$c/lib/aarch64/libunitree_sdk2.a" ] && { SDK="$c"; break; }
|
|
done
|
|
fi
|
|
[ -n "$SDK" ] && [ -f "$SDK/lib/aarch64/libunitree_sdk2.a" ] || {
|
|
echo "ERROR: no unitree_sdk2 with lib/aarch64/libunitree_sdk2.a"
|
|
echo " looked in: \$UNITREE_SDK2_DIR, $HOME/unitree_sdk2, $REPO/deps/unitree_sdk2"
|
|
echo " fix: git clone --depth 1 https://github.com/unitreerobotics/unitree_sdk2 ~/unitree_sdk2"
|
|
exit 1; }
|
|
echo ">> unitree_sdk2: $SDK"
|
|
|
|
echo ">> assembling build context in $CTX"
|
|
rm -rf "$CTX"
|
|
mkdir -p "$CTX/deps/usr_local_include" "$CTX/deps/usr_local_lib" "$CTX/deps/unitree_sdk2" "$CTX/app"
|
|
|
|
# unitree_sdk2 headers + static lib
|
|
cp -r "$SDK/include" "$CTX/deps/unitree_sdk2/"
|
|
cp -r "$SDK/lib" "$CTX/deps/unitree_sdk2/"
|
|
|
|
# CycloneDDS headers: C (dds, ddsc), C++ (ddscxx) and iceoryx. /usr/local first (how the
|
|
# robot has it), then a copy staged in deps/ for a machine without a system install.
|
|
CDDS_INC=""
|
|
for c in /usr/local/include "$REPO/deps/cyclonedds/install/include"; do
|
|
[ -d "$c/ddscxx" ] && { CDDS_INC="$c"; break; }
|
|
done
|
|
[ -n "$CDDS_INC" ] || {
|
|
echo "ERROR: no CycloneDDS headers (need ddscxx)"
|
|
echo " looked in: /usr/local/include, $REPO/deps/cyclonedds/install/include"
|
|
echo " fix: build CycloneDDS 0.10.x with -DCMAKE_INSTALL_PREFIX=/usr/local,"
|
|
echo " or stage its install/ prefix at $REPO/deps/cyclonedds"
|
|
exit 1; }
|
|
echo ">> CycloneDDS headers: $CDDS_INC"
|
|
for d in dds ddsc ddscxx iceoryx; do cp -r "$CDDS_INC/$d" "$CTX/deps/usr_local_include/"; done
|
|
|
|
# The IDL headers (idl/, idlc/) are needed ONLY by the python binding: pip building
|
|
# cyclonedds compiles a _idlpy extension against idl/retcode.h, and `make install` of the
|
|
# C++ side does not put them in /usr/local/include. Without them the image build dies at
|
|
# idlpy/src/context.h:17: fatal error: idl/retcode.h: No such file or directory
|
|
# CYCLONEDDS_SRC points at the checkout whose install/ prefix has them (0.10.5 here).
|
|
CDDS_SRC="${CYCLONEDDS_SRC:-$HOME/cyclonedds}"
|
|
# Search every layout these headers turn up in. A robot that got CycloneDDS through a ROS2
|
|
# colcon workspace has them under install/cyclonedds/include, while /usr/local carries the
|
|
# C and C++ headers but not idl/ — the two prefixes complement each other, so take idl from
|
|
# wherever it actually is. CYCLONEDDS_IDL_INCLUDE overrides the search.
|
|
for d in idl idlc; do
|
|
for c in "${CYCLONEDDS_IDL_INCLUDE:-}/$d" "/usr/local/include/$d" \
|
|
"$CDDS_SRC/install/include/$d" "$REPO/deps/cyclonedds/install/include/$d" \
|
|
"$HOME/cyclonedds_ws/install/cyclonedds/include/$d"; do
|
|
[ -n "${c#/}" ] && [ -d "$c" ] && {
|
|
cp -r "$c" "$CTX/deps/usr_local_include/"
|
|
[ "$d" = idl ] && echo ">> idl headers: $c"
|
|
break; }
|
|
done
|
|
done
|
|
[ -f "$CTX/deps/usr_local_include/idl/retcode.h" ] \
|
|
|| echo ">> WARNING: no idl/ headers — the in-image pip cyclonedds build will fail"
|
|
|
|
# CycloneDDS shared runtime libs (same fallback order as the headers)
|
|
CDDS_LIB="/usr/local/lib"
|
|
ls "$CDDS_LIB"/libddsc.so* >/dev/null 2>&1 || CDDS_LIB="$REPO/deps/cyclonedds/install/lib"
|
|
cp -P "$CDDS_LIB"/libddsc* "$CDDS_LIB"/libddscxx* \
|
|
"$CDDS_LIB"/libcyclonedds* "$CDDS_LIB"/libdds_security_* \
|
|
"$CTX/deps/usr_local_lib/" 2>/dev/null || true
|
|
ls "$CTX/deps/usr_local_lib"/libddsc.so* >/dev/null 2>&1 \
|
|
|| echo ">> WARNING: no libddsc.so — the in-image C++ build will not link"
|
|
|
|
# The app, with web/ split into its own context dir. The Dockerfile copies web/ AFTER the
|
|
# C++ build, so editing the dashboard rebuilds in seconds instead of recompiling every
|
|
# binary — and the dashboard is what changes most often.
|
|
rsync -a --exclude 'build/' --exclude 'docker/.ctx/' --exclude 'web/' "$REPO/" "$CTX/app/"
|
|
rsync -a "$REPO/web/" "$CTX/app_web/"
|
|
cp "$HERE/Dockerfile" "$CTX/Dockerfile"
|
|
|
|
# ---- recorder for the dashboard's Record/Replay panel ---------------------------------
|
|
# The panel runs g1_record_replay.py as a CHILD PROCESS, so the script and a python that
|
|
# can import unitree_sdk2py both have to be inside the image. Both dirs are always created
|
|
# (empty is fine) because the Dockerfile COPYs them unconditionally — with an empty one the
|
|
# image still builds and the panel simply reports "recorder not found".
|
|
# Both live IN this project (recorder/ and vendor/), so a clean checkout builds a complete
|
|
# image with no environment set and nothing fetched from elsewhere on the machine. The env
|
|
# vars remain as overrides for anyone pointing at their own working copy.
|
|
REC="${RECORDER_SRC:-$REPO/recorder}"
|
|
SDKPY="${UNITREE_SDK2PY_DIR:-$REPO/vendor/unitree_sdk2_python}"
|
|
mkdir -p "$CTX/deps/recorder/seed" "$CTX/deps/unitree_sdk2_python"
|
|
if [ -f "$REC/g1_record_replay.py" ] && [ -f "$SDKPY/setup.py" ]; then
|
|
cp "$REC/g1_record_replay.py" "$CTX/deps/recorder/"
|
|
# The home pose is seeded rather than shipped in DataG1/, because run.sh bind-mounts
|
|
# DataG1 over the top and would hide anything baked there.
|
|
for h in "$REC/arm_home.jsonl" "$REC/DataG1/arm_home.jsonl"; do
|
|
[ -f "$h" ] && { cp "$h" "$CTX/deps/recorder/seed/"; break; }
|
|
done
|
|
rsync -a --exclude '.git/' --exclude '*.egg-info/' "$SDKPY/" "$CTX/deps/unitree_sdk2_python/"
|
|
echo ">> recorder included: $REC + unitree_sdk2py: $SDKPY"
|
|
else
|
|
echo ">> NOTE: record/replay panel will be DISABLED in this image."
|
|
[ -f "$REC/g1_record_replay.py" ] || echo " missing $REC/g1_record_replay.py (set RECORDER_SRC)"
|
|
[ -f "$SDKPY/setup.py" ] || echo " missing $SDKPY/setup.py (set UNITREE_SDK2PY_DIR)"
|
|
fi
|
|
|
|
echo ">> docker build -t $IMG (this compiles the binaries in-image; takes a few min)"
|
|
cd "$CTX"
|
|
docker build -t "$IMG" .
|
|
rm -rf "$CTX"
|
|
echo ">> done: $IMG"
|
|
echo " run with: $REPO/docker/run.sh"
|