..
2026-07-13 10:28:59 +04:00
2026-07-13 10:28:59 +04:00
2026-07-13 10:28:59 +04:00
2026-07-13 10:28:59 +04:00
2026-07-13 10:28:59 +04:00
2026-07-13 10:28:59 +04:00
2026-07-13 10:28:59 +04:00
2026-07-13 10:28:59 +04:00
2026-07-13 10:28:59 +04:00
2026-07-13 10:28:59 +04:00
2026-07-13 10:28:59 +04:00
2026-07-13 10:28:59 +04:00

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's eth10 / 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_sdk2py vendored (docker/vendor/…whl) — not on PyPI — plus the native crc_aarch64.so copied in by hand (the wheel omits utils/lib/).
  • Named volumes (data, config, logs) auto-populate their defaults from the image on first run, then persist the dashboard's writes across rebuilds.

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 via build_and_run.sh — it disables sanadr1.service first; a bare docker compose up would fight it for port 8001 and crash-loop.
  • The container's entrypoint.sh waits up to 25 s for eth10 to be UP with its 192.168.123.x IP before starting, so DDS binds the right interface on a cold robot boot (mirrors the old start_sanad.sh wait).
  • LD_PRELOAD=libgomp.so.1 is 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.