# 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. ## 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):** ```bash 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-.tar.gz (image + install.sh + run.sh + docs) ``` **2. Install on any R1 backpack:** ```bash 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: ```bash SANAD_GEMINI_API_KEY=AQ... ./install.sh ``` Target requirements: Docker Engine installed + running, arm64/aarch64. ## Build + run (ON THE BACKPACK — native aarch64, dev only) ```bash 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://:8001` (e.g. the WiFi IP `10.255.254.82`, or `192.168.123.164` over the cable). ## Operate ```bash 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: ```bash 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.