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 copiesvendor/sanad_pkg/pubkey.ed25519to/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/arm64image on an x86 box with buildx + QEMU, push to a registry, then on each robot setSANAD_IMAGE=<reg>/sanad-p1:<tag>in.envanddocker compose pullinstead 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.
- Read the new robot's fingerprint on the host (matches what the container
will see once
/etc/machine-idis mounted):cd ~/sanad_deploy/Sanad_Package_1 PYTHONPATH=vendor python3 -c 'from sanad_pkg import license as L; print(L.machine_fingerprint())' - 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" } - Sign it (private key never leaves the workstation):
(First time only, if no keypair yet:python licensing/sign_license.py sign \ --key licensing/privkey.ed25519 --in claims.json --out sanad.licpython licensing/sign_license.py gen-keys --out-dir licensingin the monorepo, then run./sync_vendor.shso the newpubkey.ed25519is vendored into the package, and rebuild the image so it's baked in.) - 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):
- Open
:8011→ Gemini API key card → paste the customer's key (orPOST /api/p1/api-key). It persists + restarts the live session. - Persona card → set who the robot is + the language/dialect (saving restarts the live session so it applies immediately).
- 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-failureonly 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;expiresin the future; ifSANAD_LICENSE_BIND=1, the license fingerprint matches step 3b.1 and/etc/machine-idis 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
builtinprofile 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 setSANAD_AUDIO_PROFILE=plugged(works with no SDK). docker compose buildfails on buildx — use theDOCKER_BUILDKIT=0 docker build …commands in step 2.