Sanad_Package_1/NEW_ROBOT_SETUP.md

8.5 KiB

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

# 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:

rsync -az --exclude __pycache__ \
  Project/Packages/Sanad_Package_1 \
  unitree@<NEWROBOT>:~/sanad_deploy/

Everything P1 needs to build and run lives under ~/sanad_deploy/Sanad_Package_1.

2. Build the image on the robot

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=<reg>/sanad-p1:<tag> 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.

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):
    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:
    {
      "robot_id": "G1-SN-XXXX",
      "machine_fingerprint": "<hex from step 1>",
      "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):
    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:
    scp sanad.lic unitree@<NEWROBOT>:~/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:

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:

      - "/etc/machine-id:/etc/machine-id:ro"

Then:

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 :8011Gemini 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

./test_p1.sh <NEWROBOT>:8011          # expect 11/11 PASS
curl http://<NEWROBOT>: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:

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):

[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:

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

sudo reboot
# after it comes back:
curl http://<NEWROBOT>: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.