Update 2026-07-17 11:55:48

This commit is contained in:
kassam 2026-07-17 11:55:50 +04:00
parent 6a1ab4bbde
commit c018701512
2 changed files with 104 additions and 6 deletions

View File

@ -70,7 +70,8 @@ Project/fleet/
└── agents/ └── agents/
├── g1/ ← CANONICAL source (edit here) ├── g1/ ← CANONICAL source (edit here)
│ ├── sanad_api_g1.py │ ├── sanad_api_g1.py
│ ├── Dockerfile (lean: python + requests) │ ├── Dockerfile (DDS: CycloneDDS + unitree_sdk2py)
│ ├── vendor/ (unitree_sdk2py wheel + crc libs)
│ ├── requirements.txt · docker-compose.yml (local use only) · .env.example │ ├── requirements.txt · docker-compose.yml (local use only) · .env.example
├── r1/ ← generated · unitree_hg · R1 FSM ids · eth10 ├── r1/ ← generated · unitree_hg · R1 FSM ids · eth10
│ ├── sanad_api_r1.py · Dockerfile (DDS: CycloneDDS + unitree_sdk2py) · vendor/ · .env.example │ ├── sanad_api_r1.py · Dockerfile (DDS: CycloneDDS + unitree_sdk2py) · vendor/ · .env.example
@ -187,11 +188,16 @@ cd Project/fleet
# Interactive — asks robot type, IP, and (if new) name + token + server: # Interactive — asks robot type, IP, and (if new) name + token + server:
./fleet_install.sh ./fleet_install.sh
# …or scripted against the real fleet server: # …or scripted against the real fleet server (FIRST install — needs the token):
./fleet_install.sh install r1 10.255.254.82 \ ./fleet_install.sh install r1 10.255.254.82 \
--sn E39N4000Q6D7E70F --name r1_82 \ --sn E39N4000Q6D7E70F --name r1_82 \
--token <device-token> --server-url https://eco.yslootahrobotics.com --token <device-token> --server-url https://eco.yslootahrobotics.com
# UPDATE an already-installed robot (reuses the token already on it):
./fleet_install.sh install r1 10.255.254.82 \
--sn E39N4000Q6D7E70F --name r1_82 \
--server-url https://eco.yslootahrobotics.com --keep-token
# Inspect / manage: # Inspect / manage:
./fleet_install.sh data r1 10.255.254.82 # what it's currently sending ./fleet_install.sh data r1 10.255.254.82 # what it's currently sending
./fleet_install.sh logs r1 10.255.254.82 # live docker logs ./fleet_install.sh logs r1 10.255.254.82 # live docker logs
@ -248,8 +254,8 @@ a NAT bridge) and so the agent can reach the Sanad dashboard on `127.0.0.1`.
| command | action | | command | action |
|---|---| |---|---|
| `install` | rsync → build → create → install + enable the systemd service | | `install` | rsync → build → create → install + enable the systemd service (also the **update** path — see below) |
| `uninstall` | disable/remove service, container, image, and `~/sanad_api_<type>` | | `uninstall` | disable/remove service, container, image, and `~/sanad_api_<type>` — ⚠ **destroys the token**, see below |
| `status` | systemd service state + container state | | `status` | systemd service state + container state |
| `data` | recent telemetry/map log lines (what it's sending) | | `data` | recent telemetry/map log lines (what it's sending) |
| `logs` | `docker logs -f` (live tail) | | `logs` | `docker logs -f` (live tail) |
@ -266,7 +272,8 @@ a NAT bridge) and so the agent can reach the Sanad dashboard on `127.0.0.1`.
|---|---|---| |---|---|---|
| `--sn SERIAL` | **required** | robot's REAL serial — keys it on the server (e.g. `E39N4000Q6D7E70F`) | | `--sn SERIAL` | **required** | robot's REAL serial — keys it on the server (e.g. `E39N4000Q6D7E70F`) |
| `--name NAME` | `<model>_<last-octet>` | friendly display name (`r1_82`, `g1_58`) | | `--name NAME` | `<model>_<last-octet>` | friendly display name (`r1_82`, `g1_58`) |
| `--token TOK` | `test-token` | device bearer token | | `--token TOK` | `test-token` | device bearer token — ⚠ omitting it on an update **overwrites the real token with `test-token`**; use `--keep-token` |
| `--keep-token` | off | **updating an installed robot**: reuse the `DEVICE_TOKEN` already in the robot's `.env` (never leaves the robot) |
| `--server-url URL` | — | full HTTPS fleet server → `VERIFY_TLS=1` | | `--server-url URL` | — | full HTTPS fleet server → `VERIFY_TLS=1` |
| `--post PATH` | agent default | override the telemetry ingest path | | `--post PATH` | agent default | override the telemetry ingest path |
| `--server-ip IP` | auto (route to robot) | use the local test server instead → `VERIFY_TLS=0` | | `--server-ip IP` | auto (route to robot) | use the local test server instead → `VERIFY_TLS=0` |
@ -274,6 +281,49 @@ a NAT bridge) and so the agent can reach the Sanad dashboard on `127.0.0.1`.
| `--user USER` | `unitree` | SSH user on the robot | | `--user USER` | `unitree` | SSH user on the robot |
| `--keep-server` | off | (test) leave the workstation test server running | | `--keep-server` | off | (test) leave the workstation test server running |
### Updating a robot that's already installed
`install` **is** the update path — it rsyncs the new code, rebuilds (Docker layer
cache makes it fast: only the `.py` layer changes), recreates the container and
restarts the service. Use `--keep-token` so you don't need the token again:
```bash
./fleet_install.sh install g1 10.255.254.58 \
--sn E21D6000PB89GF88 --name g1_58 \
--server-url https://eco.yslootahrobotics.com \
--keep-token
```
> **Always pass `--keep-token` when updating.** `--token` defaults to
> `test-token`, so an update *without* either flag silently overwrites the
> robot's real token and the fleet feed dies with `401`.
`--keep-token` copies the robot's current `.env`, writes the fresh one with a
placeholder, then splices the old `DEVICE_TOKEN` line back **on the robot** by
line surgery (no shell interpolation) — so any token characters are safe and the
token is never transferred or printed. Downtime is a few seconds.
### ⚠ `uninstall` destroys the device token
`uninstall` does `rm -rf ~/sanad_api_<type>`, which includes `.env` — **the only
copy of `DEVICE_TOKEN` on the robot**. After uninstalling you cannot reinstall
without a token from the fleet admin. If you intend to reinstall, back it up
outside the install dir first:
```bash
# BEFORE uninstall — stash the token somewhere rm -rf can't reach
ssh unitree@<ip> 'cp ~/sanad_api_<type>/.env ~/.sanad_api_<type>.env.bak'
./fleet_install.sh uninstall <type> <ip>
# BEFORE reinstall — restore it, then --keep-token picks it up
# (rsync uses --exclude '.env', so a pre-placed .env survives the install)
ssh unitree@<ip> 'mkdir -p ~/sanad_api_<type> && cp ~/.sanad_api_<type>.env.bak ~/sanad_api_<type>/.env'
./fleet_install.sh install <type> <ip> --sn <serial> --server-url <url> --keep-token
ssh unitree@<ip> 'rm -f ~/.sanad_api_<type>.env.bak' # tidy up the extra secret copy
```
--- ---
## 9. The auto-start service (systemd) ## 9. The auto-start service (systemd)
@ -303,6 +353,22 @@ systemctl --user restart sanad-api-r1
journalctl --user -u sanad-api-r1 -f # or: docker logs -f sanad-api-r1 journalctl --user -u sanad-api-r1 -f # or: docker logs -f sanad-api-r1
``` ```
**What `uninstall` removes** (verified end-to-end on the G1):
```
systemctl --user disable --now sanad-api-<t> # stop + un-enable
rm -f ~/.config/systemd/user/sanad-api-<t>.service
systemctl --user daemon-reload
systemctl --user reset-failed sanad-api-<t> # ← clears the stale "failed" entry
docker rm -f sanad-api-<t> ; docker rmi sanad-api-<t>:latest
rm -rf ~/sanad_api_<t> # ← includes .env (the token!)
```
`reset-failed` matters: stopping the unit leaves `docker start -a` exiting
non-zero, so without it systemd keeps a `not-found failed` entry in its runtime
state — a clean uninstall would still look like a broken service in
`systemctl --user --failed` forever.
--- ---
## 10. Configuration reference ## 10. Configuration reference
@ -438,6 +504,9 @@ Production fleet server: **`https://eco.yslootahrobotics.com`**.
| build very slow (r1/go2) | first build compiles CycloneDDS (minutes). Run detached: `setsid bash -c 'cd ~/sanad_api_r1 && docker build -t sanad-api-r1:latest . >build.log 2>&1' </dev/null &` then watch `build.log`. Layer cache survives reboots. | | build very slow (r1/go2) | first build compiles CycloneDDS (minutes). Run detached: `setsid bash -c 'cd ~/sanad_api_r1 && docker build -t sanad-api-r1:latest . >build.log 2>&1' </dev/null &` then watch `build.log`. Layer cache survives reboots. |
| service didn't start after reboot | linger must be on: `loginctl show-user <user> \| grep Linger``Linger=yes` (the installer sets it). | | service didn't start after reboot | linger must be on: `loginctl show-user <user> \| grep Linger``Linger=yes` (the installer sets it). |
| r1/go2 out of sync with g1 | you edited g1 without regenerating — run `python3 tools/gen_agents.py`. | | r1/go2 out of sync with g1 | you edited g1 without regenerating — run `python3 tools/gen_agents.py`. |
| after an update the server returns **401** | you ran `install` without `--token` or `--keep-token`, so `.env` got the default `test-token`. Re-run with `--keep-token` (if the real token is still on the robot) or `--token <real>`. |
| uninstalled and now can't reinstall (no token) | `uninstall` deletes `~/sanad_api_<type>/.env` — the only token copy. Get a fresh token from the fleet admin, or restore a `.env` backup. Back it up **before** uninstalling next time (see [§8](#-uninstall-destroys-the-device-token)). |
| `systemctl --user --failed` shows a dead `sanad-api-*` after uninstall | fixed — `uninstall` now runs `reset-failed`. On an old install, clear it once: `systemctl --user reset-failed sanad-api-<type>.service`. |
--- ---

View File

@ -10,6 +10,11 @@
# ./fleet_install.sh install <g1|r1|go2> <ip> --sn <robot-serial> --token <device-token> \ # ./fleet_install.sh install <g1|r1|go2> <ip> --sn <robot-serial> --token <device-token> \
# --server-url https://fleet.example.com [--post /api/v1/fleet/ingest/telemetry] \ # --server-url https://fleet.example.com [--post /api/v1/fleet/ingest/telemetry] \
# [--name NAME] [--user U] # [--name NAME] [--user U]
#
# UPDATE an already-installed robot without re-entering the token — reuses the
# DEVICE_TOKEN already in the robot's .env (it never leaves the robot):
# ./fleet_install.sh install <g1|r1|go2> <ip> --sn <robot-serial> --keep-token \
# --server-url https://fleet.example.com
# ./fleet_install.sh uninstall <g1|r1|go2> <ip> [--user U] # ./fleet_install.sh uninstall <g1|r1|go2> <ip> [--user U]
# ./fleet_install.sh status <g1|r1|go2> <ip> # ./fleet_install.sh status <g1|r1|go2> <ip>
# ./fleet_install.sh data <g1|r1|go2> <ip> # show the data it is sending # ./fleet_install.sh data <g1|r1|go2> <ip> # show the data it is sending
@ -109,6 +114,14 @@ push_env(){
for c in "/home/$USER_/SanadR1/data" "/home/$USER_/sanad_deploy/Sanad_Package_4/data"; do for c in "/home/$USER_/SanadR1/data" "/home/$USER_/sanad_deploy/Sanad_Package_4/data"; do
if rmt "test -d $c" 2>/dev/null; then dpath="/host$c"; break; fi if rmt "test -d $c" 2>/dev/null; then dpath="/host$c"; break; fi
done done
# --keep-token: updating an already-installed robot without re-entering the
# device token. Snapshot the CURRENT .env on the robot; the old DEVICE_TOKEN
# line is spliced back below. The token never leaves the robot.
if [ "$KEEP_TOKEN" = 1 ]; then
rmt "test -f ~/$rdir/.env && grep -q '^DEVICE_TOKEN=' ~/$rdir/.env" \
|| die "--keep-token: no existing ~/$rdir/.env with a DEVICE_TOKEN on $IP (pass --token instead)"
rmt "cp ~/$rdir/.env ~/$rdir/.env.prev"
fi
# common telemetry env (all agents) # common telemetry env (all agents)
rmt_in "cat > ~/$rdir/.env" <<EOF rmt_in "cat > ~/$rdir/.env" <<EOF
SERVER_URL=$surl SERVER_URL=$surl
@ -141,6 +154,16 @@ CONTROL_ENABLE=0
ALERT_SCAN_INTERVAL=10 ALERT_SCAN_INTERVAL=10
ALERT_LOG_COOLDOWN=300 ALERT_LOG_COOLDOWN=300
EOF EOF
# splice the previous DEVICE_TOKEN back in — done entirely ON the robot, by
# line surgery (no shell interpolation), so a token with any characters is
# safe and it is never printed or transferred.
if [ "$KEEP_TOKEN" = 1 ]; then
rmt "cd ~/$rdir && grep -v '^DEVICE_TOKEN=' .env > .env.new \
&& grep '^DEVICE_TOKEN=' .env.prev >> .env.new \
&& mv .env.new .env && rm -f .env.prev" \
|| die "--keep-token: failed to preserve the existing token on $IP"
say " token: kept the one already on the robot"
fi
} }
installed(){ # 0 = installed (unit file OR container present) installed(){ # 0 = installed (unit file OR container present)
@ -196,8 +219,13 @@ EOF
do_uninstall(){ do_uninstall(){
local t="$1" img unit rdir; img="$(img_of "$t")"; unit="$(unit_of "$t")"; rdir="$(rdir_of "$t")" local t="$1" img unit rdir; img="$(img_of "$t")"; unit="$(unit_of "$t")"; rdir="$(rdir_of "$t")"
echo "== UNINSTALL $t on $IP ==" echo "== UNINSTALL $t on $IP =="
# reset-failed AFTER daemon-reload: stopping the unit leaves `docker start -a`
# exiting non-zero, so systemd keeps a stale "not-found failed" entry in its
# runtime state (visible in `systemctl --user --failed`) even once the unit
# file is gone. Without this the uninstall looks like a broken service forever.
rmt "systemctl --user disable --now $unit >/dev/null 2>&1; \ rmt "systemctl --user disable --now $unit >/dev/null 2>&1; \
rm -f ~/.config/systemd/user/$unit; systemctl --user daemon-reload; \ rm -f ~/.config/systemd/user/$unit; systemctl --user daemon-reload; \
systemctl --user reset-failed $unit >/dev/null 2>&1; \
docker rm -f $img >/dev/null 2>&1; docker rmi $img:latest >/dev/null 2>&1; \ docker rm -f $img >/dev/null 2>&1; docker rmi $img:latest >/dev/null 2>&1; \
rm -rf ~/$rdir; echo ' removed service, container, image, and ~/'$rdir" rm -rf ~/$rdir; echo ' removed service, container, image, and ~/'$rdir"
echo "== uninstalled ==" echo "== uninstalled =="
@ -271,7 +299,7 @@ PY
} }
# --------------------------- arg parsing --------------------------- # # --------------------------- arg parsing --------------------------- #
CMD=""; ROBOT=""; IP=""; SERVER_IP=""; PORT=8799; TOKEN="test-token"; SN=""; NAME=""; USER_="unitree"; KEEP_SERVER=0 CMD=""; ROBOT=""; IP=""; SERVER_IP=""; PORT=8799; TOKEN="test-token"; SN=""; NAME=""; USER_="unitree"; KEEP_SERVER=0; KEEP_TOKEN=0
SERVER_URL_OVERRIDE=""; VERIFY_TLS_OPT=""; POST_ENDPOINT="" SERVER_URL_OVERRIDE=""; VERIFY_TLS_OPT=""; POST_ENDPOINT=""
POSA=() POSA=()
while [ $# -gt 0 ]; do case "$1" in while [ $# -gt 0 ]; do case "$1" in
@ -280,6 +308,7 @@ while [ $# -gt 0 ]; do case "$1" in
--verify-tls) VERIFY_TLS_OPT="$2"; shift 2;; --verify-tls) VERIFY_TLS_OPT="$2"; shift 2;;
--port) PORT="$2"; shift 2;; --port) PORT="$2"; shift 2;;
--token) TOKEN="$2"; shift 2;; --token) TOKEN="$2"; shift 2;;
--keep-token) KEEP_TOKEN=1; shift;; # update in place: reuse the token already on the robot
--sn) SN="$2"; shift 2;; --sn) SN="$2"; shift 2;;
--name) NAME="$2"; shift 2;; # friendly display name (default <model>_<last-octet>) --name) NAME="$2"; shift 2;; # friendly display name (default <model>_<last-octet>)
--post) POST_ENDPOINT="$2"; shift 2;; # ingest POST path (telemetry or map endpoint) --post) POST_ENDPOINT="$2"; shift 2;; # ingest POST path (telemetry or map endpoint)