# 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-<date>.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://<backpack-ip>: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.
