Update 2026-06-17 16:25:16
This commit is contained in:
parent
aba3118dfc
commit
e43581b089
224
NEW_ROBOT_SETUP.md
Normal file
224
NEW_ROBOT_SETUP.md
Normal file
@ -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@<NEWROBOT>:~/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=<reg>/ 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": "<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):
|
||||
```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@<NEWROBOT>:~/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 <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)
|
||||
```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://<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.
|
||||
Loading…
x
Reference in New Issue
Block a user