# 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 the image (the Dockerfile copies `vendor/sanad_pkg/pubkey.ed25519` 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 package onto the robot (so it can build) P1 is **self-contained** — it vendors the Sanad engine under `Sanad_Package_1/vendor`, so you only copy the **package folder** (no `Sanad/` sibling, no `sanad-base`). From the workstation: ```bash rsync -az --exclude __pycache__ \ Project/Packages/Sanad_Package_1 \ unitree@:~/sanad_deploy/ ``` Everything P1 needs to build and run lives under `~/sanad_deploy/Sanad_Package_1`. ## 2. Build the image on the robot ```bash cd ~/sanad_deploy/Sanad_Package_1 # Modern Docker (buildx): docker compose build # Jetson Docker without buildx: DOCKER_BUILDKIT=0 docker build -t sanad-p1:latest . ``` The build vendors the engine and (by default, `WITH_UNITREE_SDK=1`) compiles the chest-audio SDK — first build takes a few minutes; later builds are cached. > **Alternative — registry (build once, deploy to many robots):** build the > `linux/arm64` image on an x86 box with buildx + QEMU, push to a registry, then > on each robot set `SANAD_IMAGE=/sanad-p1:` in `.env` and > `docker compose pull` instead of building. ## 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/Sanad_Package_1 PYTHONPATH=vendor 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` in the monorepo, then run `./sync_vendor.sh` so the new `pubkey.ed25519` is vendored into the package, and rebuild the image so it's baked in.) 4. **Copy to the robot:** ```bash scp sanad.lic unitree@:~/sanad_deploy/Sanad_Package_1/license/sanad.lic ``` ## 4. Configure `.env` and run On the robot, `cd ~/sanad_deploy/Sanad_Package_1` and create `.env` from `.env.example`: ```ini SANAD_LICENSE_FILE=./license/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), uncomment the host machine-id mount** in the `p1` service of `docker-compose.yml` so the container's fingerprint matches the host's: ```yaml - "/etc/machine-id:/etc/machine-id:ro" ``` Then: ```bash docker compose up -d docker compose logs -f # 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) The compose file already sets `restart: unless-stopped`, so you only need the Docker daemon to start at boot: ```bash sudo systemctl enable docker docker compose up -d # once; the container then returns on every boot ``` (If the policy was ever switched back to `on-failure`, restore boot-survival with `docker update --restart unless-stopped sanad-p1`.) The container keeps coming back until you explicitly `docker compose 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/Sanad_Package_1 ExecStart=/usr/bin/docker compose up ExecStop=/usr/bin/docker compose 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.