4.2 KiB
SanadR1 in Docker (R1 EDU backpack)
Containerized SanadR1 — Gemini voice + dashboard — for the Unitree R1 backpack (Jetson Orin NX, aarch64). The BLE LED-mask is removed from this build.
Why these choices
network_mode: host— the robot's CycloneDDS discovery and the on-board mic UDP multicast (239.168.123.161:5555) do not cross a Docker bridge NAT, and the dashboard must bind the host'seth10/wlan0. Host networking is the standard pattern for Unitree/ROS DDS.- CycloneDDS built from source in the image — there is no aarch64 PyPI wheel
for
cyclonedds==0.10.2; it must compile against the C library (as on the host conda env). unitree_sdk2pyvendored (docker/vendor/…whl) — not on PyPI — plus the nativecrc_aarch64.socopied in by hand (the wheel omitsutils/lib/).- Named volumes (
data,config,logs) auto-populate their defaults from the image on first run, then persist the dashboard's writes across rebuilds.
Install on ANY R1 (offline bundle — recommended)
No source, registry, or internet needed on the robot — ship one tarball.
1. Build the bundle (once, on the workstation):
cd Project/R1/SanadR1/docker
./make_bundle.sh # reuses the current sanadr1:latest image
# or: ./make_bundle.sh --build # cross-build the arm64 image first
# → /tmp/sanadr1-bundle-<date>.tar.gz (image + install.sh + run.sh + docs)
2. Install on any R1 backpack:
scp /tmp/sanadr1-bundle-*.tar.gz r1:~/
ssh r1 'tar xzf sanadr1-bundle-*.tar.gz && cd sanadr1-bundle && ./install.sh'
install.sh auto-detects the robot NIC (the 192.168.123.x link), the current
user/home, and USB audio; loads the image; writes .env (prompts for the Gemini
key, or reads $SANAD_GEMINI_API_KEY); installs + enables the systemd auto-start
unit; starts the container and verifies :8001. Nothing is hardcoded to one
robot, and re-running it just updates the image. Non-interactive:
SANAD_GEMINI_API_KEY=AQ... ./install.sh
Target requirements: Docker Engine installed + running, arm64/aarch64.
Build + run (ON THE BACKPACK — native aarch64, dev only)
cd ~/SanadR1/docker
cp .env.example .env # then set SANAD_GEMINI_API_KEY
./build_and_run.sh # builds, disables the old conda service, starts
First build is slow (CycloneDDS compiles). Dashboard: http://<backpack-ip>:8001
(e.g. the WiFi IP 10.255.254.82, or 192.168.123.164 over the cable).
Operate
docker compose logs -f # live logs
docker compose restart # restart
docker compose down # stop + remove container (volumes persist)
docker compose up -d --build # rebuild after a code change
Auto-start on boot is handled by restart: unless-stopped + the enabled
docker.service — no systemd unit of our own is needed.
Runtime facts
- Robot link:
eth10(cable) for DDS. Internet + dashboard access:wlan0. - Movement stays disarmed (
SANAD_NAV_TOOLS=0; loco FSM fixes not yet applied). - The old conda path (
~/start_sanad_r1.sh+sanadr1.service) is superseded. Launch only viabuild_and_run.sh— it disablessanadr1.servicefirst; a baredocker compose upwould fight it for port 8001 and crash-loop. - The container's
entrypoint.shwaits up to 25 s foreth10to be UP with its192.168.123.xIP before starting, so DDS binds the right interface on a cold robot boot (mirrors the oldstart_sanad.shwait). LD_PRELOAD=libgomp.so.1is set to avoid the aarch64 static-TLS import error on numpy/opencv (a Jetson quirk carried over from the conda launcher).
Verify (do NOT trust the healthcheck alone)
The HTTP healthcheck only proves the dashboard bound :8001 — it passes even if the
robot link/voice is dead. After build_and_run.sh, confirm the real paths:
docker compose logs | grep -Ei "eth10 ready|DDS|Action registry|startup complete"
# then speak to it and watch for a Gemini turn in the logs
If you change baked defaults under config/ or data/, an already-initialized
sanadr1_data volume keeps the old copy — docker compose down -v (or
docker volume rm sanadr1_data) to reseed from the new image.