diff --git a/NEW_ROBOT_SETUP.md b/NEW_ROBOT_SETUP.md new file mode 100644 index 0000000..726b56f --- /dev/null +++ b/NEW_ROBOT_SETUP.md @@ -0,0 +1,224 @@ +# Setting up P1 (Basic Communication) on a NEW G1 + +This is the end-to-end procedure for bringing P1 up on a **fresh** Unitree G1 / +Jetson that has never run Sanad. The only genuinely new step vs. an existing +robot is the **license** — a license can be locked to one robot's hardware +fingerprint, so a new G1 needs its own `sanad.lic` (or an unbound one). + +Roles: +- **Vendor side** (your workstation) — holds the Ed25519 **private** key + (`licensing/privkey.ed25519`) and signs licenses. Never goes on the robot. +- **Robot side** (the new Jetson) — runs the container; carries only the signed + `sanad.lic`. The **public** key is already baked into every image + (`sanad-base` copies it to `/etc/sanad/pubkey.ed25519`). + +--- + +## 0. Prerequisites on the new Jetson + +```bash +# Docker present + your user can use it (re-login or `newgrp docker` after): +sudo usermod -aG docker "$USER" +# eth0 link to the G1 firmware up (needed for chest audio + DDS). Containers run +# network_mode: host, so the container sees the host eth0 / G1 link directly. +# Optional: plug a USB speaker/mic (Anker) if you'll use the `plugged` profile. +``` + +## 1. Get the code onto the robot (so it can build) + +`sanad-base` bakes the canonical `Sanad/` source, so the build needs **both** +`Sanad/` and `Packages/` present. From the workstation: + +```bash +rsync -az --exclude __pycache__ \ + Project/Sanad Project/Packages \ + unitree@:~/sanad_deploy/ +``` + +This lays out `~/sanad_deploy/Sanad` + `~/sanad_deploy/Packages` (siblings — the +compose file mounts `../Sanad/data`, so they must sit side by side). + +## 2. Build the images on the robot + +```bash +cd ~/sanad_deploy/Packages +# If this Docker has buildx (modern): +docker compose --profile base build # shared base (incl. chest-audio SDK) +docker compose --profile p1 build # P1 image + +# If this Docker has NO buildx (e.g. Jetson Docker 26.x) use legacy build: +DOCKER_BUILDKIT=0 docker build -f sanad-base/Dockerfile -t sanad-base:latest .. # context = ~/sanad_deploy +DOCKER_BUILDKIT=0 docker build -f Sanad_Package_1/Dockerfile -t sanad-p1:latest . # context = Packages/ +``` + +> **Alternative — registry (build once, deploy to many robots):** on an x86 box +> with buildx + QEMU run +> `SANAD_REGISTRY=/ SANAD_TAG=1.0.0 PUSH=1 ./scripts/build_and_push.sh base p1`, +> then on each robot set `SANAD_REGISTRY`/`SANAD_TAG` in `.env` and +> `docker compose --profile p1 pull`. + +## 3. License THIS robot + +Choose one: + +### 3a. Unbound license (quick — runs on any robot) +Fine for internal / trial use. Keep `SANAD_LICENSE_BIND=0` and sign a license +with `"machine_fingerprint": null`. The signature + expiry are still enforced; +the hardware lock just isn't. + +### 3b. Bound license (recommended for delivery — locked to this G1) +The fingerprint = `sha256(eth0 MAC | /etc/machine-id)`. **Inside a container, +`/etc/machine-id` is the image's, not the host's**, so for binding you must mount +the host's machine-id (step 4 does this) and compute the fingerprint the same way. + +1. **Read the new robot's fingerprint on the host** (matches what the container + will see once `/etc/machine-id` is mounted): + ```bash + cd ~/sanad_deploy/Packages + PYTHONPATH=. python3 -c 'from sanad_pkg import license as L; print(L.machine_fingerprint())' + ``` +2. **On the workstation**, write `claims.json`: + ```json + { + "robot_id": "G1-SN-XXXX", + "machine_fingerprint": "", + "packages": {"P1": true, "P2": false, "P3": false, "P4": false}, + "features": {"language": "ar"}, + "issued": "2026-06-17", + "expires": "2030-01-01" + } + ``` +3. **Sign it** (private key never leaves the workstation): + ```bash + python licensing/sign_license.py sign \ + --key licensing/privkey.ed25519 --in claims.json --out sanad.lic + ``` + (First time only, if no keypair yet: `python licensing/sign_license.py gen-keys --out-dir licensing` + — then rebuild images so the new `pubkey.ed25519` is baked in.) +4. **Copy to the robot:** + ```bash + scp sanad.lic unitree@:~/sanad_deploy/Packages/licensing/sanad.lic + ``` + +## 4. Configure `.env` and run + +On the robot, `cd ~/sanad_deploy/Packages` and create `.env` from `.env.example`: + +```ini +SANAD_LICENSE_FILE=./licensing/sanad.lic # the one you signed (3b) — or sanad.lic.example +SANAD_LICENSE_BIND=1 # 1 to enforce the fingerprint; 0 = unbound +SANAD_AUDIO_PROFILE=builtin # chest mic+speaker | plugged for USB/Anker +SANAD_LANGUAGE=ar # optional; else license feature / persona +``` + +**If using a bound license (3b), add the host machine-id mount** to the `p1` +service in `docker-compose.yml` (under `volumes:`) so the container's fingerprint +matches the host's: +```yaml + - "/etc/machine-id:/etc/machine-id:ro" +``` + +Then: +```bash +docker compose --profile p1 up -d +docker compose --profile p1 logs -f p1 # should print "[P1] entitled — lang=… port=8011" +``` +Dashboard: **http://<NEWROBOT>:8011** + +## 5. First-use configuration (per customer) + +P1 ships **keyless** (the vendor Gemini key is stripped from the image at build): +1. Open `:8011` → **Gemini API key** card → paste the customer's key (or + `POST /api/p1/api-key`). It persists + restarts the live session. +2. **Persona** card → set who the robot is + the language/dialect (saving restarts + the live session so it applies immediately). +3. **Audio** card → pick chest vs USB/Anker, volume, mute. + +## 6. Verify + +```bash +./test_p1.sh :8011 # expect 11/11 PASS +curl http://:8011/api/package +# license.valid: true packages: {"P1": true} key: NONE (until added) +``` +Then click **Start** in the Conversation card and speak — the robot should reply +through the chest (or USB) speaker. + +## 7. Auto-run on boot (required for a delivered robot) + +> **Important:** the compose default `restart: on-failure` only restarts P1 after +> a *crash* — it does **not** bring it back after a reboot/power-cycle. For +> auto-run you need a boot-surviving restart policy **and** the Docker daemon +> enabled at boot. Pick **one** of the two options below. + +### Option A — Docker-native (simplest, no extra files) +```bash +# 1) make the daemon start at boot: +sudo systemctl enable docker +# 2) make P1 survive reboot + crash (overrides the on-failure policy on the +# live container — no compose edit needed): +docker update --restart unless-stopped sanad-p1 +``` +(Equivalent permanent form: change the `p1` service's `restart: on-failure` → +`restart: unless-stopped` in `docker-compose.yml`, then `docker compose --profile +p1 up -d`.) After this, `docker compose --profile p1 up -d` once and the container +returns on every boot until you explicitly `docker compose --profile p1 down`. + +### Option B — systemd unit (clean start/stop/status, mirrors the Sanad unit) +Create `/etc/systemd/system/sanad-p1.service` (adjust `User=` and the path to +your deploy dir): +```ini +[Unit] +Description=Sanad Package 1 (Basic Communication) — Docker +Requires=docker.service +After=docker.service network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=unitree +WorkingDirectory=/home/unitree/sanad_deploy/Packages +ExecStart=/usr/bin/docker compose --profile p1 up +ExecStop=/usr/bin/docker compose --profile p1 down +Restart=on-failure +RestartSec=5 +TimeoutStopSec=30 +KillSignal=SIGINT + +[Install] +WantedBy=multi-user.target +``` +Install + enable: +```bash +sudo systemctl daemon-reload +sudo systemctl enable --now sanad-p1.service +systemctl status sanad-p1.service +journalctl -u sanad-p1.service -f # watch logs +sudo systemctl restart sanad-p1.service # after a license/config change +``` +With Option B, systemd owns the lifecycle (runs `compose up` in the foreground and +`compose down` on stop), so don't also use `docker update --restart` — leave the +compose policy at `on-failure`. + +### Verify auto-run +```bash +sudo reboot +# after it comes back: +curl http://:8011/api/health # expect 200 without touching anything +``` + +--- + +### Troubleshooting +- **Container exits immediately, log says "not licensed"** — signature/expiry/ + fingerprint failed. Check: license actually mounted at `/etc/sanad/sanad.lic`; + `expires` in the future; if `SANAD_LICENSE_BIND=1`, the license fingerprint + matches step 3b.1 **and** `/etc/machine-id` is mounted (step 4). +- **Fingerprint mismatch only under Docker** — you computed it on the host but + didn't mount `/etc/machine-id`, so the container hashed a different machine-id. + Add the mount (step 4) and re-read the fingerprint with the mount in place. +- **No chest audio** — the `builtin` profile needs the Unitree SDK in the image + (`WITH_UNITREE_SDK=1`, the default) and eth0 reachable to the G1. As a fallback, + plug a USB speaker/mic and set `SANAD_AUDIO_PROFILE=plugged` (works with no SDK). +- **`docker compose build` fails on buildx** — use the `DOCKER_BUILDKIT=0 + docker build …` commands in step 2.