From db757d0b55a7403e2ff1a76f843d6c6fbce98b1c Mon Sep 17 00:00:00 2001 From: kassam Date: Fri, 10 Jul 2026 11:24:55 +0400 Subject: [PATCH] Update 2026-07-10 11:24:53 --- .gitignore | 4 + README.md | 94 ++++ agents/g1/.dockerignore | 5 + agents/g1/.env.example | 43 ++ agents/g1/.gitignore | 4 + agents/g1/Dockerfile | 19 + agents/g1/README.md | 130 +++++ agents/g1/docker-compose.yml | 35 ++ agents/g1/requirements.txt | 2 + agents/g1/sanad_api_g1.py | 485 ++++++++++++++++ agents/go2/.dockerignore | 4 + agents/go2/.env.example | 29 + agents/go2/.gitignore | 3 + agents/go2/Dockerfile | 47 ++ agents/go2/README.md | 42 ++ agents/go2/docker-compose.yml | 20 + agents/go2/requirements.txt | 7 + agents/go2/sanad_api_go2.py | 409 ++++++++++++++ agents/go2/vendor/crc_aarch64.so | Bin 0 -> 7928 bytes agents/go2/vendor/crc_amd64.so | Bin 0 -> 15136 bytes .../unitree_sdk2py-1.0.1-py3-none-any.whl | Bin 0 -> 105041 bytes agents/r1/.dockerignore | 4 + agents/r1/.env.example | 35 ++ agents/r1/.gitignore | 3 + agents/r1/Dockerfile | 47 ++ agents/r1/README.md | 82 +++ agents/r1/docker-compose.yml | 21 + agents/r1/requirements.txt | 7 + agents/r1/sanad_api_r1.py | 529 ++++++++++++++++++ agents/r1/vendor/crc_aarch64.so | Bin 0 -> 7928 bytes agents/r1/vendor/crc_amd64.so | Bin 0 -> 15136 bytes .../unitree_sdk2py-1.0.1-py3-none-any.whl | Bin 0 -> 105041 bytes fleet_install.sh | 265 +++++++++ fleet_test_server.py | 76 +++ 34 files changed, 2451 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 agents/g1/.dockerignore create mode 100644 agents/g1/.env.example create mode 100644 agents/g1/.gitignore create mode 100644 agents/g1/Dockerfile create mode 100644 agents/g1/README.md create mode 100644 agents/g1/docker-compose.yml create mode 100644 agents/g1/requirements.txt create mode 100644 agents/g1/sanad_api_g1.py create mode 100644 agents/go2/.dockerignore create mode 100644 agents/go2/.env.example create mode 100644 agents/go2/.gitignore create mode 100644 agents/go2/Dockerfile create mode 100644 agents/go2/README.md create mode 100644 agents/go2/docker-compose.yml create mode 100644 agents/go2/requirements.txt create mode 100644 agents/go2/sanad_api_go2.py create mode 100644 agents/go2/vendor/crc_aarch64.so create mode 100644 agents/go2/vendor/crc_amd64.so create mode 100644 agents/go2/vendor/unitree_sdk2py-1.0.1-py3-none-any.whl create mode 100644 agents/r1/.dockerignore create mode 100644 agents/r1/.env.example create mode 100644 agents/r1/.gitignore create mode 100644 agents/r1/Dockerfile create mode 100644 agents/r1/README.md create mode 100644 agents/r1/docker-compose.yml create mode 100644 agents/r1/requirements.txt create mode 100644 agents/r1/sanad_api_r1.py create mode 100644 agents/r1/vendor/crc_aarch64.so create mode 100644 agents/r1/vendor/crc_amd64.so create mode 100644 agents/r1/vendor/unitree_sdk2py-1.0.1-py3-none-any.whl create mode 100755 fleet_install.sh create mode 100755 fleet_test_server.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c02264b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.pyc +Logs/ +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..d813ce9 --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +# Sanad Fleet Agents + +Per-robot agents that report to the YS Lootah fleet server, each shipped as +**full Docker** with a user-level **systemd auto-start service**. Deploy, manage, +and remove them over SSH with one interactive script. + +``` +Project/fleet/ + fleet_install.sh # interactive + scriptable deploy/manage tool + fleet_test_server.py # your workstation standing in as the fleet server (tests) + agents/ + g1/ sanad_api_g1.py — MAP uploader (POST …/{sn}/map) + r1/ sanad_api_r1.py — TELEMETRY unitree_hg (POST …/telemetry) + go2/ sanad_api_go2.py — TELEMETRY unitree_go (POST …/telemetry) [unverified on hw] +``` + +All three are self-contained (own `Dockerfile`, `.env.example`). Deploy uses +plain `docker build`/`docker run` — **no docker-compose needed on the robot** +(R1/Go2 don't have it). Source is pushed from this canonical workstation copy via +`rsync`; images build natively on each arm64 robot; a **user systemd unit** +(linger-enabled, **no sudo**) owns start/stop/auto-start-on-boot. + +## Install / manage + +Interactive (asks robot type → IP → detects installed-or-not → right actions): + +```bash +./fleet_install.sh +``` + +Scriptable: + +```bash +./fleet_install.sh install [--sn NAME] [--server-ip IP] [--port N] [--token TOK] +./fleet_install.sh data # show what it's currently sending +./fleet_install.sh status +./fleet_install.sh logs +./fleet_install.sh test # end-to-end vs workstation server +./fleet_install.sh uninstall # service + container + image + dir +``` + +`install`/`uninstall` are detected automatically in interactive mode: if the +agent is already on the robot it offers **data / status / logs / reinstall / +uninstall**; otherwise it prompts for **robot name (SN)**, server IP, port, token +and installs. + +### Auto-start service + +Each install writes `~/.config/systemd/user/sanad-api-.service`, enables +linger (`loginctl enable-linger`, no sudo), and `systemctl --user enable --now`s +it. It starts on boot and restarts on crash. Manage with: + +```bash +ssh unitree@ 'systemctl --user status sanad-api-r1' +ssh unitree@ 'systemctl --user restart sanad-api-r1' +``` + +## Fleet (this deployment) + +| robot | agent | ip | ssh user | arch | DDS iface | +|---|---|---|---|---|---| +| G1 | map | `10.255.254.58` | `unitree` (key) | arm64 | — | +| R1 | telemetry | `10.255.254.82` | `unitree` (key) | arm64 | `eth10` | +| Go2 | telemetry | *(when available)* | `unitree` | arm64 | `eth0` | + +Workstation fleet server (for tests): **10.255.254.83** via `fleet_test_server.py`. + +## What each agent sends, and when + +**G1 map** → `POST /api/v1/fleet/ingest/{sn}/map` — on change (scan ~30 s; +size+mtime→sha256). Sends the RTAB-Map `.db` + places as `multipart` (file `db` + +`meta` JSON `{sn,name,format:"rtabmap_db",size_bytes,sha256,points[…]}`) or +`base64json`. Reads web_nav3 `maps//*.db` + `web/data//places/*.json`. + +**R1 / Go2 telemetry** → `POST /api/v1/fleet/ingest/telemetry` — every ~2 s; +heartbeat (`battery:null,status:offline`) when DDS is silent so it stays online: +```json +{ "sn":"r1_82", "mac":"…", "battery":74, "charging":false, + "status":"idle", "position":{"x":…,"y":…}|null, "faults":[], "ts":… } +``` +- R1 (`unitree_hg`): battery `rt/lf/bmsstate`, faults/liveness `rt/lowstate`. +- Go2 (`unitree_go`): battery from **`rt/lowstate.bms_state`** (nested), no separate BMS topic. +- Both: `status` derived (charging/moving/idle/offline); position optional; read-only (never moves). + +Auth on every request: `Authorization: Bearer `. + +## Notes / follow-ups + +- **Point at the real fleet server:** re-run `install … --server-ip --token --sn `. +- **G1 real maps** live *inside* the `p4_Foxy_sanad` nav container (not host-mounted). + Add a `maps/` bind-mount to Package_4's nav service, or share a volume, so the + uploader sees real maps (it reports 0 until then; `test` seeds a fixture). +- **Go2** agent is written from the `unitree_go` SDK layout but **not yet run on a + Go2** — confirm the `bms_state` current sign (charging) and sportmodestate fields. diff --git a/agents/g1/.dockerignore b/agents/g1/.dockerignore new file mode 100644 index 0000000..ed287fb --- /dev/null +++ b/agents/g1/.dockerignore @@ -0,0 +1,5 @@ +.env +data/ +__pycache__/ +*.pyc +README.md diff --git a/agents/g1/.env.example b/agents/g1/.env.example new file mode 100644 index 0000000..306ae7f --- /dev/null +++ b/agents/g1/.env.example @@ -0,0 +1,43 @@ +# sanad_api_g1 — copy to .env and fill in. docker compose reads it automatically. + +# ── fleet server (REQUIRED — YS Lootah gives you these two) ────────────────── +SERVER_URL=https://fleet.example.com +DEVICE_TOKEN=REPLACE_WITH_DEVICE_TOKEN + +# ── identity ───────────────────────────────────────────────────────────────── +# This robot's fleet id — used as {sn} in POST /api/v1/fleet/ingest/{sn}/map. +SN=g1_7892 +# web_nav3 robot name: the maps subdir (//*.db) + X-Robot-Name. +# Must match web_nav3's robot_config.yaml robot_name (default: sanad). +ROBOT=sanad + +# ── where the map files live on the HOST (bind-mounted read-only) ──────────── +# Point these at the robot's web_nav3 install. Defaults assume a Package_4 +# robot (nav container -> /home/unitree/marcus_nav2_test). For the workstation +# dev copy use .../Project/G1/Nav2_Projects/web_nav3/{maps,web/data}. +MAPS_HOST_DIR=/home/unitree/marcus_nav2_test/maps +DATA_HOST_DIR=/home/unitree/marcus_nav2_test/web/data +STATE_HOST_DIR=./data/state + +# Optional legacy per-robot places file (older sanad setups). Leave blank if +# each map already has its own places under DATA_DIR//places/. +# LEGACY_PLACES=/data/legacy/places.json + +# ── behaviour ──────────────────────────────────────────────────────────────── +# Which maps to upload: all | active | newest. active/newest need WEB_NAV3_URL. +MAP_SELECT=all +# Optional — only used to learn which map is ACTIVE (MAP_SELECT=active). +WEB_NAV3_URL=http://127.0.0.1:8765 + +# Wire format the server accepts on the map endpoint: +# multipart -> multipart/form-data: file field `db` + form field `meta` (JSON) +# base64json -> JSON body with the .db as `db_base64` + the same meta fields +MAP_UPLOAD_MODE=multipart +# Endpoint path template ({sn} substituted). Change only if the server differs. +MAP_ENDPOINT=/api/v1/fleet/ingest/{sn}/map + +# Scan cadence for the loop (seconds). Maps rarely change, so 30s is plenty. +POLL_INTERVAL=30 +# Verify the fleet server's TLS cert (1 recommended; 0 only for self-signed dev). +VERIFY_TLS=1 +HTTP_TIMEOUT=30 diff --git a/agents/g1/.gitignore b/agents/g1/.gitignore new file mode 100644 index 0000000..b6bde21 --- /dev/null +++ b/agents/g1/.gitignore @@ -0,0 +1,4 @@ +.env +data/ +__pycache__/ +*.pyc diff --git a/agents/g1/Dockerfile b/agents/g1/Dockerfile new file mode 100644 index 0000000..2162f1b --- /dev/null +++ b/agents/g1/Dockerfile @@ -0,0 +1,19 @@ +# sanad_api_g1 — G1 fleet MAP uploader. Self-contained, no ROS. +FROM python:3.10-slim-bookworm + +# ca-certificates so outbound HTTPS to the fleet server validates. +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY sanad_api_g1.py . + +# Writable state (upload fingerprints) — override with a volume in compose. +ENV STATE_DIR=/data/state +RUN mkdir -p /data/state + +# Run as the loop by default; override the CMD for --once / --list / --dry-run. +ENTRYPOINT ["python", "-u", "sanad_api_g1.py"] diff --git a/agents/g1/README.md b/agents/g1/README.md new file mode 100644 index 0000000..46539c6 --- /dev/null +++ b/agents/g1/README.md @@ -0,0 +1,130 @@ +# sanad_api_g1 — G1 fleet **map** uploader + +A tiny, self-contained agent that pushes the G1's navigation **map** to the +YS Lootah fleet server. One robot type = one folder = its own Docker image, so +it drops onto any new G1 unchanged. (Sibling folders `sanad_api_r1`, +`sanad_api_go2` will do the same for those robots.) + +This build covers **only the map** — the "Maps sync" row of the fleet spec: + +``` +POST {SERVER_URL}/api/v1/fleet/ingest/{sn}/map Authorization: Bearer +``` + +Telemetry (battery/status/position/faults), commands, alerts and logs are +separate agents and are **not** in this script. + +--- + +## What it uploads — and the one thing the server must do + +The G1's map is built by the **web_nav3** (Nav2 + RTAB-Map) stack and stored on +disk as a **RTAB-Map SQLite `.db`** (`maps//.db`), with each map's +named places in `web/data//places/.json`. **There is no rendered +PNG on disk** — the dashboard draws the occupancy grid live over rosbridge. + +By design this agent uploads the **raw `.db`** (the real map artifact) plus its +places, instead of rendering an image. So: + +> ⚠️ **The fleet server's map endpoint must accept a `rtabmap_db` artifact.** +> The spec's documented body (`image_base64` / `resolution` / `origin`) is for a +> *rendered* map. If you need that instead, render the live `/map` OccupancyGrid +> over rosbridge — that's the telemetry-agent path, not this one. + +Two wire formats are supported (`MAP_UPLOAD_MODE`), pick what your server takes: + +| mode | request | +|---|---| +| `multipart` (default) | `multipart/form-data`: file field **`db`** (the `.db`) + form field **`meta`** (JSON) | +| `base64json` | JSON body: all meta fields + the `.db` as **`db_base64`** | + +`meta` / JSON body shape: + +```json +{ "sn": "g1_7892", "name": "floor-1", "file": "floor-1.db", "format": "rtabmap_db", + "size_bytes": 6994944, "sha256": "…", "mtime": 1731000000, "description": "ground floor", + "points": [ { "name": "dock", "type": "waypoint", "x": 1.2, "y": 3.4, "yaw": 0.0 } ] } +``` + +Each map is fingerprinted (size+mtime, then sha256); a map is (re)uploaded only +**on change**. State lives in `STATE_DIR/uploaded.json` so restarts don't re-push. + +**No ROS, no DDS** — just file reads + outbound HTTPS. web_nav3's HTTP API is +used only (optionally) to learn which map is *active* (`MAP_SELECT=active`). + +--- + +## Install (full Docker) + +One command — builds the image, creates `.env`, starts the container (auto-restarts on boot): + +```bash +./install.sh # creates .env if missing, then build + up -d +# (edit .env when prompted: SERVER_URL, DEVICE_TOKEN, SN, MAPS_HOST_DIR, DATA_HOST_DIR) +./install.sh --logs # follow logs +./install.sh --status # container state + list maps it sees +./install.sh --down # stop + remove +``` + +Or the raw compose flow: + +```bash +cp .env.example .env # set SERVER_URL + DEVICE_TOKEN, and the map paths +docker compose up -d --build +docker compose logs -f +``` + +Handy one-shots (no loop): + +```bash +docker compose run --rm sanad-api-g1 --list # what maps do I see? +docker compose run --rm sanad-api-g1 --dry-run # build payloads, never POST +docker compose run --rm sanad-api-g1 --once # one upload pass, then exit +``` + +Runs bare too (for a quick check on the workstation): + +```bash +pip install -r requirements.txt +SERVER_URL=… DEVICE_TOKEN=… SN=g1_7892 ROBOT=sanad \ + MAPS_DIR=…/web_nav3/maps DATA_DIR=…/web_nav3/web/data \ + python3 sanad_api_g1.py --list +``` + +### Key env (full list in `.env.example`) + +| var | meaning | +|---|---| +| `SERVER_URL`, `DEVICE_TOKEN` | given by YS Lootah — **required** | +| `SN` | fleet id / URL key — default `g1_7892` | +| `ROBOT` | web_nav3 robot name = maps subdir + `X-Robot-Name` — default `sanad` | +| `MAPS_HOST_DIR` / `DATA_HOST_DIR` | host paths bind-mounted read-only (see below) | +| `MAP_SELECT` | `all` (default) · `active` · `newest` | +| `MAP_UPLOAD_MODE` | `multipart` (default) · `base64json` | +| `POLL_INTERVAL` | scan cadence, seconds (default 30) | + +--- + +## ⚠️ Mounting the maps directory + +The uploader needs **read access to wherever web_nav3 actually keeps the `.db` +files.** That location is deployment-specific: + +- **Workstation dev copy:** `Project/G1/Nav2_Projects/web_nav3/{maps,web/data}` +- **Package_4 robot:** the `sanad-nav` container stores named maps *inside itself* + at `/home/unitree/marcus_nav2_test/maps` — that path is **not** bind-mounted to + the host by default. To let this uploader read them, either add a host + bind-mount for `maps/` to the `sanad-nav` service, or put both containers on a + **shared named volume** for the maps dir, then point `MAPS_HOST_DIR` at it. + +Set `MAPS_HOST_DIR` / `DATA_HOST_DIR` in `.env` accordingly. + +--- + +## Porting to R1 / Go2 + +Copy this folder to `sanad_api_r1` / `sanad_api_go2`, rename the script/service, +and change `SN` (`r1_…`, `go2_…`). If those robots store their map the same way +(web_nav3 `.db` + places) nothing else changes. If a robot has no web_nav3 `.db` +(e.g. a different SLAM), adjust `discover_maps()` / `load_points()` for that +robot's on-disk map format. diff --git a/agents/g1/docker-compose.yml b/agents/g1/docker-compose.yml new file mode 100644 index 0000000..6b1d3d3 --- /dev/null +++ b/agents/g1/docker-compose.yml @@ -0,0 +1,35 @@ +# sanad_api_g1 — standalone G1 fleet MAP uploader. +# +# cp .env.example .env # fill SERVER_URL + DEVICE_TOKEN, set the map paths +# docker compose up -d --build +# +# It reads the web_nav3 map files (RTAB-Map .db + per-map places) from the host +# via read-only mounts and POSTs each changed map to the fleet server. No ROS, +# no DDS — only file reads + outbound HTTPS (and an optional localhost call to +# web_nav3:8765 to learn the ACTIVE map, which is why network_mode: host). +services: + sanad-api-g1: + build: + context: . + dockerfile: Dockerfile + image: "${SANAD_API_G1_IMAGE:-sanad-api-g1:latest}" + container_name: sanad-api-g1 + network_mode: host # reach 127.0.0.1:8765 (web_nav3) + outbound HTTPS + restart: unless-stopped + env_file: .env + environment: + # In-container paths the app reads (host paths are the bind mounts below). + MAPS_DIR: /data/maps + DATA_DIR: /data/web_data + STATE_DIR: /data/state + volumes: + # web_nav3 maps dir (holds /*.db + maps_meta.json). READ-ONLY. + # On a Package_4 robot the nav container keeps these under + # /home/unitree/marcus_nav2_test/maps — point MAPS_HOST_DIR at that (or at + # a shared named volume). On the workstation dev copy it is + # /Project/G1/Nav2_Projects/web_nav3/maps. + - "${MAPS_HOST_DIR:-/home/unitree/marcus_nav2_test/maps}:/data/maps:ro" + # web_nav3 web/data dir (per-map places live under /places/*.json). + - "${DATA_HOST_DIR:-/home/unitree/marcus_nav2_test/web/data}:/data/web_data:ro" + # Persistent upload state (so a restart doesn't re-push unchanged maps). + - "${STATE_HOST_DIR:-./data/state}:/data/state" diff --git a/agents/g1/requirements.txt b/agents/g1/requirements.txt new file mode 100644 index 0000000..b143394 --- /dev/null +++ b/agents/g1/requirements.txt @@ -0,0 +1,2 @@ +# sanad_api_g1 map uploader — deliberately tiny (no ROS, no DDS). +requests>=2.31,<3 diff --git a/agents/g1/sanad_api_g1.py b/agents/g1/sanad_api_g1.py new file mode 100644 index 0000000..19378e6 --- /dev/null +++ b/agents/g1/sanad_api_g1.py @@ -0,0 +1,485 @@ +#!/usr/bin/env python3 +"""sanad_api_g1 — G1 fleet MAP uploader. + +Scope (this build): upload the G1's navigation MAP to the YS Lootah fleet +server. Nothing else (no telemetry / commands / logs — those are separate +agents). It is the "Maps sync" row of the fleet spec: + + POST {SERVER_URL}/api/v1/fleet/ingest/{sn}/map (Bearer device token) + +WHAT IT UPLOADS +--------------- +The G1's map is produced by the web_nav3 (Nav2 + RTAB-Map) stack and stored on +disk as a RTAB-Map SQLite ``.db`` file (per web_nav3/web/backend.py: +``MAPS_ROOT//.db`` + a ``maps_meta.json`` sidecar), with each map's +named places kept in ``web/data//places/.json``. + +There is NO rendered PNG on disk — the dashboard draws the occupancy grid live +over rosbridge. By design choice this agent uploads the RAW ``.db`` (the actual +map artifact) plus its places, rather than rendering an image. That means the +FLEET SERVER must accept a ``rtabmap_db`` artifact on the map endpoint (see +MAP_UPLOAD_MODE for the two wire formats). The documented spec body +(image_base64 / resolution / origin) is for a rendered map; switch to the +telemetry-agent's live-rosbridge renderer if you need that instead. + +NO ROS, NO DDS. Pure files + HTTPS, so it drops into any robot as its own +container. It reads the maps/places straight from mounted volumes; the web_nav3 +HTTP API is only used (optionally) to learn which map is "active". + +CHANGE DETECTION +---------------- +Each map's ``.db`` is fingerprinted (size + mtime fast-path, then sha256). A map +is (re)uploaded only when its fingerprint changes — matching the spec's "send +the nav map on change". State persists in ``STATE_DIR/uploaded.json`` so a +restart doesn't re-push unchanged maps. + +CONFIG — all via environment (see .env.example) +----------------------------------------------- + SERVER_URL base URL of the fleet server (required) e.g. https://fleet.example.com + DEVICE_TOKEN per-robot bearer token (required) + SN this robot's fleet id (path key) default g1_7892 + ROBOT web_nav3 robot name (maps subdir + header) default sanad + MAPS_DIR mounted web_nav3 maps/ dir (contains /*.db and/or *.db) + DATA_DIR mounted web_nav3 web/data dir (per-map places live under /places/) + LEGACY_PLACES optional path to a legacy places.json (per-robot, no map scoping) + WEB_NAV3_URL optional http://127.0.0.1:8765 — used only to read the ACTIVE map + MAP_SELECT all | active | newest default all + MAP_UPLOAD_MODE multipart | base64json default multipart + MAP_ENDPOINT path template, {sn} substituted default /api/v1/fleet/ingest/{sn}/map + POLL_INTERVAL seconds between scans (loop mode) default 30 + STATE_DIR writable dir for upload state default /data/state + VERIFY_TLS 1|0 verify server TLS default 1 + HTTP_TIMEOUT per-request timeout seconds default 30 + +CLI +--- + python sanad_api_g1.py # run the loop (default) + python sanad_api_g1.py --once # one scan+upload pass, then exit + python sanad_api_g1.py --list # list discovered maps (no upload) + python sanad_api_g1.py --dry-run # build payloads + report, never POST + python sanad_api_g1.py --force # ignore state; upload even if unchanged +""" +from __future__ import annotations + +import argparse +import base64 +import hashlib +import json +import logging +import math +import os +import sys +import time +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Dict, List, Optional + +import requests + +log = logging.getLogger("sanad_api_g1") + + +# --------------------------------------------------------------------------- # +# tiny .env loader (so it also runs bare, outside docker) — no dependency +# --------------------------------------------------------------------------- # +def _load_dotenv(path: str = ".env") -> None: + p = Path(path) + if not p.exists(): + return + for line in p.read_text().splitlines(): + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + k, _, v = line.partition("=") + k, v = k.strip(), v.strip().strip('"').strip("'") + os.environ.setdefault(k, v) + + +def _env(name: str, default: str = "") -> str: + return os.environ.get(name, default).strip() + + +def _env_bool(name: str, default: bool) -> bool: + v = _env(name, "1" if default else "0").lower() + return v in ("1", "true", "yes", "on") + + +# --------------------------------------------------------------------------- # +# config +# --------------------------------------------------------------------------- # +@dataclass +class Config: + server_url: str + device_token: str + sn: str + robot: str + maps_dir: Path + data_dir: Optional[Path] + legacy_places: Optional[Path] + web_nav3_url: str + map_select: str + upload_mode: str + endpoint_tmpl: str + poll_interval: float + state_dir: Path + verify_tls: bool + http_timeout: float + + @classmethod + def from_env(cls) -> "Config": + server = _env("SERVER_URL").rstrip("/") + token = _env("DEVICE_TOKEN") + missing = [n for n, v in (("SERVER_URL", server), ("DEVICE_TOKEN", token)) if not v] + if missing: + raise SystemExit(f"[config] missing required env: {', '.join(missing)}") + data_dir = _env("DATA_DIR") + legacy = _env("LEGACY_PLACES") + return cls( + server_url=server, + device_token=token, + sn=_env("SN", "g1_7892"), + robot=_env("ROBOT", "sanad"), + maps_dir=Path(_env("MAPS_DIR", "/data/maps")), + data_dir=Path(data_dir) if data_dir else None, + legacy_places=Path(legacy) if legacy else None, + web_nav3_url=_env("WEB_NAV3_URL", "").rstrip("/"), + map_select=_env("MAP_SELECT", "all").lower(), + upload_mode=_env("MAP_UPLOAD_MODE", "multipart").lower(), + endpoint_tmpl=_env("MAP_ENDPOINT", "/api/v1/fleet/ingest/{sn}/map"), + poll_interval=float(_env("POLL_INTERVAL", "30")), + state_dir=Path(_env("STATE_DIR", "/data/state")), + verify_tls=_env_bool("VERIFY_TLS", True), + http_timeout=float(_env("HTTP_TIMEOUT", "30")), + ) + + def map_url(self) -> str: + return self.server_url + self.endpoint_tmpl.format(sn=self.sn) + + def auth_headers(self) -> Dict[str, str]: + return {"Authorization": f"Bearer {self.device_token}"} + + +# --------------------------------------------------------------------------- # +# map artifact +# --------------------------------------------------------------------------- # +@dataclass +class MapArtifact: + path: Path # absolute path to the .db + name: str # file name, e.g. floor-1.db + stem: str # name without .db, e.g. floor-1 + size: int + mtime: int + description: str = "" + sha256: str = "" + points: List[Dict[str, Any]] = field(default_factory=list) + + def fingerprint(self) -> str: + # cheap identity for the change check before we hash the whole file + return f"{self.size}:{self.mtime}" + + +def _sha256(path: Path) -> str: + h = hashlib.sha256() + with path.open("rb") as f: + for chunk in iter(lambda: f.read(1024 * 1024), b""): + h.update(chunk) + return h.hexdigest() + + +# --------------------------------------------------------------------------- # +# discovery — read maps + places straight from the mounted web_nav3 files +# --------------------------------------------------------------------------- # +def _map_key(stem: str) -> str: + """Mirror backend._map_key: safe filename stem for the per-map places file.""" + stem = Path(stem).name + if stem.endswith(".db"): + stem = stem[:-3] + return "".join(c for c in stem if c.isalnum() or c in "_-.") + + +def _read_json(path: Path, default: Any) -> Any: + try: + return json.loads(path.read_text() or "") + except Exception: + return default + + +def _yaw_from_pose(pose: Dict[str, Any]) -> float: + """Yaw (rad) from a places-pose dict. Supports full quaternion, planar + (qz,qw), or an explicit yaw field.""" + if "qw" in pose or "qz" in pose: + qx = float(pose.get("qx", 0.0)); qy = float(pose.get("qy", 0.0)) + qz = float(pose.get("qz", 0.0)); qw = float(pose.get("qw", 1.0)) + return math.atan2(2.0 * (qw * qz + qx * qy), + 1.0 - 2.0 * (qy * qy + qz * qz)) + return float(pose.get("yaw", 0.0)) + + +def _places_files_for(cfg: Config, stem: str) -> List[Path]: + """Candidate on-disk places files for a map, most-specific first.""" + out: List[Path] = [] + key = _map_key(stem) + if cfg.data_dir: + out.append(cfg.data_dir / cfg.robot / "places" / f"{key}.json") + if cfg.legacy_places: + out.append(cfg.legacy_places) + return out + + +def load_points(cfg: Config, stem: str) -> List[Dict[str, Any]]: + """Return the map's saved places as fleet points: {name, type, x, y, yaw}. + + Places store shape (web_nav3): {"": {x,y,z,qx,qy,qz,qw}}. + """ + for pf in _places_files_for(cfg, stem): + data = _read_json(pf, None) if pf.exists() else None + if isinstance(data, dict) and data: + pts: List[Dict[str, Any]] = [] + for name, pose in data.items(): + if not isinstance(pose, dict): + continue + try: + pts.append({ + "name": name, + "type": str(pose.get("type", "waypoint")), + "x": float(pose["x"]), + "y": float(pose["y"]), + "yaw": round(_yaw_from_pose(pose), 4), + }) + except (KeyError, TypeError, ValueError): + continue + log.debug("points for %s: %d (from %s)", stem, len(pts), pf) + return pts + return [] + + +def discover_maps(cfg: Config) -> List[MapArtifact]: + """Find every .db under MAPS_DIR// and MAPS_DIR/ (legacy root).""" + roots = [cfg.maps_dir / cfg.robot, cfg.maps_dir] + meta: Dict[str, Any] = {} + meta_file = cfg.maps_dir / cfg.robot / "maps_meta.json" + if meta_file.exists(): + meta = _read_json(meta_file, {}) or {} + + seen: set[str] = set() + out: List[MapArtifact] = [] + for root in roots: + if not root.exists(): + continue + for p in sorted(root.glob("*.db")): + rp = str(p.resolve()) + if rp in seen: + continue + seen.add(rp) + st = p.stat() + out.append(MapArtifact( + path=p, + name=p.name, + stem=p.stem, + size=st.st_size, + mtime=int(st.st_mtime), + description=(meta.get(p.name) or {}).get("description", ""), + )) + out.sort(key=lambda m: m.mtime, reverse=True) + return out + + +def _active_map_name(cfg: Config) -> Optional[str]: + """Ask web_nav3 which map is currently loaded (optional; None if not set/up).""" + if not cfg.web_nav3_url: + return None + try: + r = requests.get(cfg.web_nav3_url + "/api/status", + headers={"X-Robot-Name": cfg.robot}, + timeout=min(cfg.http_timeout, 5)) + r.raise_for_status() + am = (r.json() or {}).get("active_map") + return _map_key(am) if am else None + except requests.RequestException as e: + log.debug("active-map query failed: %s", e) + return None + + +def select_maps(cfg: Config, maps: List[MapArtifact]) -> List[MapArtifact]: + if not maps: + return [] + if cfg.map_select == "newest": + return maps[:1] + if cfg.map_select == "active": + active = _active_map_name(cfg) + if active: + picked = [m for m in maps if _map_key(m.stem) == active] + if picked: + return picked + log.warning("active map %r not found on disk; falling back to newest", active) + return maps[:1] + return maps # "all" + + +# --------------------------------------------------------------------------- # +# state (which fingerprints already uploaded) +# --------------------------------------------------------------------------- # +def _state_file(cfg: Config) -> Path: + return cfg.state_dir / "uploaded.json" + + +def load_state(cfg: Config) -> Dict[str, str]: + return _read_json(_state_file(cfg), {}) if _state_file(cfg).exists() else {} + + +def save_state(cfg: Config, state: Dict[str, str]) -> None: + try: + cfg.state_dir.mkdir(parents=True, exist_ok=True) + _state_file(cfg).write_text(json.dumps(state, indent=2)) + except Exception as e: + log.warning("could not persist state: %s", e) + + +# --------------------------------------------------------------------------- # +# upload +# --------------------------------------------------------------------------- # +def build_meta(cfg: Config, m: MapArtifact) -> Dict[str, Any]: + return { + "sn": cfg.sn, + "name": m.stem, + "file": m.name, + "format": "rtabmap_db", + "size_bytes": m.size, + "sha256": m.sha256, + "mtime": m.mtime, + "description": m.description, + "points": m.points, + } + + +def upload_map(cfg: Config, m: MapArtifact, session: requests.Session) -> bool: + url = cfg.map_url() + meta = build_meta(cfg, m) + try: + if cfg.upload_mode == "base64json": + body = dict(meta) + body["db_base64"] = base64.b64encode(m.path.read_bytes()).decode("ascii") + resp = session.post(url, json=body, headers=cfg.auth_headers(), + timeout=cfg.http_timeout, verify=cfg.verify_tls) + else: # multipart (default) + with m.path.open("rb") as fh: + files = {"db": (m.name, fh, "application/octet-stream")} + data = {"meta": json.dumps(meta)} + resp = session.post(url, files=files, data=data, + headers=cfg.auth_headers(), + timeout=cfg.http_timeout, verify=cfg.verify_tls) + except requests.RequestException as e: + log.error("upload %s FAILED (transport): %s", m.name, e) + return False + + if not resp.ok: + detail = resp.text[:300] + log.error("upload %s FAILED: HTTP %s %s", m.name, resp.status_code, detail) + return False + log.info("uploaded %s (%.2f MB, %d points) -> HTTP %s", + m.name, m.size / 1024 / 1024, len(m.points), resp.status_code) + return True + + +# --------------------------------------------------------------------------- # +# one pass +# --------------------------------------------------------------------------- # +def run_once(cfg: Config, *, force: bool, dry_run: bool, + session: requests.Session) -> int: + maps = select_maps(cfg, discover_maps(cfg)) + if not maps: + log.info("no .db maps found under %s (robot=%s)", cfg.maps_dir, cfg.robot) + return 0 + + state = load_state(cfg) + uploaded = 0 + for m in maps: + prev = state.get(str(m.path.resolve())) + if not force and prev == m.fingerprint(): + log.debug("unchanged, skip: %s", m.name) + continue + # confirm change with a real content hash (mtime can shift without edits) + m.sha256 = _sha256(m.path) + m.points = load_points(cfg, m.stem) + + if dry_run: + log.info("[dry-run] would upload %s (%.2f MB, sha=%s…, %d points)", + m.name, m.size / 1024 / 1024, m.sha256[:12], len(m.points)) + uploaded += 1 + continue + + if upload_map(cfg, m, session): + state[str(m.path.resolve())] = m.fingerprint() + save_state(cfg, state) + uploaded += 1 + + if uploaded == 0: + log.info("nothing to upload (%d map(s) already current)", len(maps)) + return uploaded + + +def cmd_list(cfg: Config) -> None: + maps = discover_maps(cfg) + active = _active_map_name(cfg) + if not maps: + print(f"(no .db maps under {cfg.maps_dir} for robot '{cfg.robot}')") + return + print(f"{len(maps)} map(s) under {cfg.maps_dir} (robot={cfg.robot}):") + for m in maps: + pts = load_points(cfg, m.stem) + flag = " <-- active" if active and _map_key(m.stem) == active else "" + print(f" {m.name:<28} {m.size/1024/1024:6.2f} MB {len(pts):>3} points" + f" {m.description}{flag}") + + +# --------------------------------------------------------------------------- # +# main +# --------------------------------------------------------------------------- # +def main(argv: Optional[List[str]] = None) -> int: + ap = argparse.ArgumentParser(description="G1 fleet map uploader") + ap.add_argument("--once", action="store_true", help="one scan+upload pass, then exit") + ap.add_argument("--list", action="store_true", help="list discovered maps and exit") + ap.add_argument("--dry-run", action="store_true", help="build payloads but never POST") + ap.add_argument("--force", action="store_true", help="upload even if unchanged") + ap.add_argument("--interval", type=float, default=None, help="override POLL_INTERVAL seconds") + ap.add_argument("-v", "--verbose", action="store_true") + args = ap.parse_args(argv) + + logging.basicConfig( + level=logging.DEBUG if args.verbose else logging.INFO, + format="%(asctime)s %(levelname)s %(name)s: %(message)s", + ) + _load_dotenv() + cfg = Config.from_env() + if args.interval is not None: + cfg.poll_interval = args.interval + + log.info("sanad_api_g1 map uploader — sn=%s robot=%s server=%s mode=%s select=%s", + cfg.sn, cfg.robot, cfg.server_url, cfg.upload_mode, cfg.map_select) + log.info("maps_dir=%s data_dir=%s web_nav3=%s", + cfg.maps_dir, cfg.data_dir, cfg.web_nav3_url or "(disabled)") + + if args.list: + cmd_list(cfg) + return 0 + + session = requests.Session() + if args.once or args.dry_run: + run_once(cfg, force=args.force, dry_run=args.dry_run, session=session) + return 0 + + log.info("loop every %.0fs (Ctrl-C to stop)", cfg.poll_interval) + while True: + try: + run_once(cfg, force=args.force, dry_run=False, session=session) + except Exception as e: # never let the loop die + log.exception("pass failed: %s", e) + try: + time.sleep(cfg.poll_interval) + except KeyboardInterrupt: + log.info("stopped") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/agents/go2/.dockerignore b/agents/go2/.dockerignore new file mode 100644 index 0000000..bce1cad --- /dev/null +++ b/agents/go2/.dockerignore @@ -0,0 +1,4 @@ +.env +__pycache__/ +*.pyc +README.md diff --git a/agents/go2/.env.example b/agents/go2/.env.example new file mode 100644 index 0000000..fe186a7 --- /dev/null +++ b/agents/go2/.env.example @@ -0,0 +1,29 @@ +# sanad_api_go2 — copy to .env and fill in. + +# ── fleet server (REQUIRED — YS Lootah gives you these two) ────────────────── +SERVER_URL=https://fleet.example.com +DEVICE_TOKEN=REPLACE_WITH_DEVICE_TOKEN + +# ── identity ───────────────────────────────────────────────────────────────── +# This robot's fleet id, sent in the telemetry body as "sn". Set the real Go2 id. +SN=go2_0000 + +# ── DDS (reading robot state — unitree_go family) ──────────────────────────── +# Interface that sees the Go2's DDS traffic (Jetson/onboard usually eth0). +DDS_INTERFACE=eth0 +DDS_DOMAIN=0 +# MAC_INTERFACE=eth0 + +# ── position options ───────────────────────────────────────────────────────── +# none (default) | sportmode (rt/lf/sportmodestate) | rosbridge (/odom). +GO2_POSITION_SOURCE=none +ROSBRIDGE_URL=ws://127.0.0.1:9090 + +# ── fault thresholds ───────────────────────────────────────────────────────── +LOW_SOC=15 +MOTOR_TEMP_MAX=85 + +# ── cadence / transport ────────────────────────────────────────────────────── +POLL_INTERVAL=2 +VERIFY_TLS=1 +HTTP_TIMEOUT=10 diff --git a/agents/go2/.gitignore b/agents/go2/.gitignore new file mode 100644 index 0000000..cff5543 --- /dev/null +++ b/agents/go2/.gitignore @@ -0,0 +1,3 @@ +.env +__pycache__/ +*.pyc diff --git a/agents/go2/Dockerfile b/agents/go2/Dockerfile new file mode 100644 index 0000000..49c647d --- /dev/null +++ b/agents/go2/Dockerfile @@ -0,0 +1,47 @@ +# sanad_api_go2 — Go2 fleet telemetry agent (reads DDS: rt/lowstate, unitree_go). +# +# BUILD ON THE GO2 COMPUTE (native arm64). The DDS stack: prebuilt CycloneDDS +# (apt) + the CycloneDDS python binding (pip) + the vendored unitree_sdk2py wheel +# (includes both unitree_go and unitree_hg idl). Run with --network host so the +# robot's DDS is visible (see docker-compose.yml). +FROM python:3.10-slim-bookworm + +# CycloneDDS C lib + idlc (bookworm ships 0.10.2 — matches the robot), build tools +# for the python binding, iproute2 for iface checks, libgomp1 for the TLS guard. +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential cmake \ + cyclonedds-dev cyclonedds-tools \ + iproute2 libgomp1 ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# aarch64 "static TLS block" guard (harmless on amd64). +ENV LD_PRELOAD=libgomp.so.1 +# cyclonedds build helper looks for libddsc.so under $CYCLONEDDS_HOME/lib; Debian +# installs into the multiarch dir — symlink whatever arch built into /usr/lib. +ENV CYCLONEDDS_HOME=/usr +RUN set -e; for lib in libddsc.so libcycloneddsidl.so; do \ + f=$(ls /usr/lib/*/"$lib" 2>/dev/null | head -1); \ + [ -n "$f" ] && ln -sf "$f" /usr/lib/"$lib" || true; \ + done + +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +# CycloneDDS python binding, compiled against the C lib above. 0.10.2's build +# helper imports wheel.bdist_wheel (removed in wheel>=0.46) → pin the toolchain. +RUN pip install --no-cache-dir "setuptools<80" "wheel<0.46" \ + && pip install --no-cache-dir --no-build-isolation cyclonedds==0.10.2 + +# Unitree SDK (vendored wheel — not on PyPI) + native crc lib (wheel omits it). +# Both arch crc libs are copied; unitree_sdk2py loads the one matching the image. +COPY vendor/unitree_sdk2py-*.whl /tmp/ +RUN pip install --no-cache-dir --no-deps /tmp/unitree_sdk2py-*.whl +COPY vendor/crc_aarch64.so vendor/crc_amd64.so \ + /usr/local/lib/python3.10/site-packages/unitree_sdk2py/utils/lib/ + +COPY sanad_api_go2.py . + +ENV PYTHONUNBUFFERED=1 \ + DDS_INTERFACE=eth0 \ + DDS_DOMAIN=0 +ENTRYPOINT ["python", "-u", "sanad_api_go2.py"] diff --git a/agents/go2/README.md b/agents/go2/README.md new file mode 100644 index 0000000..b8798c6 --- /dev/null +++ b/agents/go2/README.md @@ -0,0 +1,42 @@ +# sanad_api_go2 — Go2 fleet **telemetry** agent + +Pushes the Unitree **Go2**'s live status to the YS Lootah fleet server. Sibling of +`sanad_api_r1`; deploy it with the fleet installer (`../../fleet_install.sh`). + +``` +POST {SERVER_URL}/api/v1/fleet/ingest/telemetry Authorization: Bearer +{ "sn":"go2_0000", "mac":"…", "battery":74, "charging":false, + "status":"idle", "position":{"x":…,"y":…}|null, "faults":[], "ts":… } +``` + +Sent every ~2 s; a **heartbeat** (`battery:null, status:offline`) when DDS is +silent so the robot stays online. + +## Data sources (Go2, `unitree_go` DDS) + +| field | source | +|---|---| +| `battery`, `charging` | **`rt/lowstate.bms_state`** (nested — Go2 has no separate BMS topic): `soc` 0–100; charging from `current` sign | +| `faults[]` | `rt/lowstate` motor temps + staleness | +| `status` | derived (charging / moving / idle / offline) | +| `position` | optional: `sportmode` (`rt/lf/sportmodestate`) or `rosbridge` (`/odom`) — default off | +| `mac` | primary NIC | + +> **Safety:** read-only — never commands motion. + +## Run / test + +```bash +../../fleet_install.sh install go2 --sn go2_ +../../fleet_install.sh data go2 +# bare simulate (no robot): +SERVER_URL=… DEVICE_TOKEN=… SN=go2_0 python3 sanad_api_go2.py --simulate -v +``` + +Build on the Go2 compute (arm64); bundles CycloneDDS + the vendored +`unitree_sdk2py` wheel (`vendor/`). `network_mode: host` for DDS. + +> ⚠️ **UNVERIFIED ON HARDWARE.** Written from the `unitree_go` SDK layout but not +> yet run on a real Go2. Confirm the `bms_state` current sign (charging polarity) +> and the `sportmodestate` fields on the robot. Verified so far: image builds, +> `unitree_go` imports, simulate/heartbeat payloads correct. diff --git a/agents/go2/docker-compose.yml b/agents/go2/docker-compose.yml new file mode 100644 index 0000000..e3cd9a7 --- /dev/null +++ b/agents/go2/docker-compose.yml @@ -0,0 +1,20 @@ +# sanad_api_go2 — standalone Go2 fleet TELEMETRY agent. +# (The fleet installer deploys via plain `docker build`/`docker run` + systemd; +# this compose file is for manual local use.) +# +# cp .env.example .env # set SERVER_URL + DEVICE_TOKEN + SN + DDS_INTERFACE +# docker compose up -d --build +# +# Reads the Go2's DDS state (rt/lowstate, unitree_go; battery from bms_state) and +# POSTs telemetry every ~2s. network_mode: host is REQUIRED for DDS visibility. +services: + sanad-api-go2: + build: + context: . + dockerfile: Dockerfile + image: "${SANAD_API_GO2_IMAGE:-sanad-api-go2:latest}" + container_name: sanad-api-go2 + network_mode: host + restart: unless-stopped + env_file: .env + # command: ["--simulate"] # synthetic state, no robot diff --git a/agents/go2/requirements.txt b/agents/go2/requirements.txt new file mode 100644 index 0000000..b462572 --- /dev/null +++ b/agents/go2/requirements.txt @@ -0,0 +1,7 @@ +# sanad_api_r1 telemetry agent. +requests>=2.31,<3 +# only needed if R1_POSITION_SOURCE=rosbridge (reads /odom for position): +websocket-client>=1.6,<2 +# DDS stack (cyclonedds + the vendored unitree_sdk2py wheel) is installed by the +# Dockerfile, not from here — see Dockerfile. The agent degrades to heartbeats if +# unitree_sdk2py is unavailable. diff --git a/agents/go2/sanad_api_go2.py b/agents/go2/sanad_api_go2.py new file mode 100644 index 0000000..8effa79 --- /dev/null +++ b/agents/go2/sanad_api_go2.py @@ -0,0 +1,409 @@ +#!/usr/bin/env python3 +"""sanad_api_go2 — Go2 fleet TELEMETRY agent. + +Pushes the Unitree **Go2**'s live status to the YS Lootah fleet server: + + POST {SERVER_URL}/api/v1/fleet/ingest/telemetry (Bearer device token) + body: { "sn", "mac", "battery", "charging", "status", "position":{x,y}, "faults":[] } + +Same shape/cadence as the R1 agent. The DIFFERENCE is the DDS family: + + * Go2 uses **unitree_go** (not unitree_hg). + * Go2 battery lives INSIDE LowState_ as a nested ``bms_state`` (soc/current) — + there is NO separate rt/lf/bmsstate topic like the G1/R1. So this agent reads + battery straight from rt/lowstate.bms_state. + * position (optional) from SportModeState (rt/lf/sportmodestate) or rosbridge /odom. + +⚠️ UNVERIFIED ON HARDWARE: written from the unitree_go SDK layout but not yet run +on a real Go2. Confirm the bms current sign (charging) and sportmodestate fields +on the robot. Degrades to heartbeats if unitree_sdk2py is unavailable; --simulate +tests the upload path without a robot. SAFETY: never commands motion (read-only). + +CONFIG — environment (see .env.example) +--------------------------------------- + SERVER_URL, DEVICE_TOKEN fleet base URL + bearer token (required) + SN fleet id default go2_0000 + DDS_INTERFACE robot network iface for DDS default eth0 + DDS_DOMAIN DDS domain id default 0 + MAC_INTERFACE iface whose MAC to report default = DDS_INTERFACE + GO2_POSITION_SOURCE none | sportmode | rosbridge default none + ROSBRIDGE_URL ws://127.0.0.1:9090 (position) default ws://127.0.0.1:9090 + LOW_SOC / MOTOR_TEMP_MAX fault thresholds default 15 / 85 + POLL_INTERVAL seconds between posts default 2 + VERIFY_TLS / HTTP_TIMEOUT TLS verify (1) / timeout (10) + +CLI: --simulate | --once | --dry-run | --interval N | -v +""" +from __future__ import annotations + +import argparse +import json +import logging +import os +import sys +import threading +import time +import uuid +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Dict, List, Optional + +import requests + +log = logging.getLogger("sanad_api_go2") + + +def _load_dotenv(path: str = ".env") -> None: + p = Path(path) + if not p.exists(): + return + for line in p.read_text().splitlines(): + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + k, _, v = line.partition("=") + os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'")) + + +def _env(name: str, default: str = "") -> str: + return os.environ.get(name, default).strip() + + +def _env_bool(name: str, default: bool) -> bool: + return _env(name, "1" if default else "0").lower() in ("1", "true", "yes", "on") + + +@dataclass +class Config: + server_url: str + device_token: str + sn: str + dds_interface: str + dds_domain: int + mac_interface: str + position_source: str + rosbridge_url: str + low_soc: int + motor_temp_max: float + poll_interval: float + endpoint: str + verify_tls: bool + http_timeout: float + + @classmethod + def from_env(cls) -> "Config": + server = _env("SERVER_URL").rstrip("/") + token = _env("DEVICE_TOKEN") + missing = [n for n, v in (("SERVER_URL", server), ("DEVICE_TOKEN", token)) if not v] + if missing: + raise SystemExit(f"[config] missing required env: {', '.join(missing)}") + iface = _env("DDS_INTERFACE", "eth0") + return cls( + server_url=server, device_token=token, + sn=_env("SN", "go2_0000"), + dds_interface=iface, dds_domain=int(_env("DDS_DOMAIN", "0")), + mac_interface=_env("MAC_INTERFACE", iface), + position_source=_env("GO2_POSITION_SOURCE", "none").lower(), + rosbridge_url=_env("ROSBRIDGE_URL", "ws://127.0.0.1:9090"), + low_soc=int(_env("LOW_SOC", "15")), + motor_temp_max=float(_env("MOTOR_TEMP_MAX", "85")), + poll_interval=float(_env("POLL_INTERVAL", "2")), + endpoint=_env("TELEMETRY_ENDPOINT", "/api/v1/fleet/ingest/telemetry"), + verify_tls=_env_bool("VERIFY_TLS", True), + http_timeout=float(_env("HTTP_TIMEOUT", "10")), + ) + + def telemetry_url(self) -> str: + return self.server_url + self.endpoint + + def auth_headers(self) -> Dict[str, str]: + return {"Authorization": f"Bearer {self.device_token}"} + + +def read_mac(interface: str) -> str: + p = Path(f"/sys/class/net/{interface}/address") + try: + mac = p.read_text().strip() + if mac and mac != "00:00:00:00:00:00": + return mac.lower() + except Exception: + pass + n = uuid.getnode() + return ":".join(f"{(n >> b) & 0xff:02x}" for b in range(40, -1, -8)) + + +class DDSReader: + """Subscribes rt/lowstate (unitree_go LowState_) and (optionally) sportmodestate. + Battery comes from the nested LowState_.bms_state. Passive reads only.""" + + def __init__(self, cfg: Config): + self.cfg = cfg + self._lock = threading.Lock() + self._bms: Optional[Dict[str, Any]] = None + self._low_ts = 0.0 + self._sport_ts = 0.0 + self._temps: List[float] = [] + self._max_dq = 0.0 + self._xy: Optional[Dict[str, float]] = None + self.ok = False + self._start() + + def _start(self) -> None: + try: + from unitree_sdk2py.core.channel import ( + ChannelFactoryInitialize, ChannelSubscriber) + from unitree_sdk2py.idl.unitree_go.msg.dds_ import LowState_ + SportModeState_ = None + if self.cfg.position_source == "sportmode": + try: + from unitree_sdk2py.idl.unitree_go.msg.dds_ import SportModeState_ + except Exception: + SportModeState_ = None + except Exception as e: + log.warning("unitree_sdk2py unavailable (%s) — telemetry runs in heartbeat mode", e) + return + try: + ChannelFactoryInitialize(self.cfg.dds_domain, self.cfg.dds_interface) + self._low_sub = ChannelSubscriber("rt/lowstate", LowState_) + self._low_sub.Init(self._on_low, 10) + if SportModeState_ is not None: + self._sport_sub = ChannelSubscriber("rt/lf/sportmodestate", SportModeState_) + self._sport_sub.Init(self._on_sport, 10) + self.ok = True + log.info("DDS up: domain=%d iface=%s (rt/lowstate; battery from bms_state)", + self.cfg.dds_domain, self.cfg.dds_interface) + except Exception as e: + log.warning("DDS init failed (%s) — heartbeat mode", e) + + def _on_low(self, msg) -> None: + try: + bms = getattr(msg, "bms_state", None) or getattr(msg, "bms", None) + if bms is not None: + soc = int(getattr(bms, "soc", 0) or 0) + cur = int(getattr(bms, "current", 0) or 0) # mA + with self._lock: + self._bms = {"soc": max(0, min(100, soc)), "current_a": round(cur / 1000.0, 2)} + temps: List[float] = [] + max_dq = 0.0 + for m in (getattr(msg, "motor_state", None) or []): + t = getattr(m, "temperature", None) + if t is not None: + try: + vals = [float(x) for x in t] if hasattr(t, "__iter__") else [float(t)] + temps.extend(v for v in vals if -40 <= v <= 200) + except Exception: + pass + dq = getattr(m, "dq", None) + if dq is not None: + try: + max_dq = max(max_dq, abs(float(dq))) + except Exception: + pass + with self._lock: + self._low_ts = time.monotonic() + self._temps = temps + self._max_dq = max_dq + except Exception: + pass + + def _on_sport(self, msg) -> None: + try: + pos = getattr(msg, "position", None) + if pos is not None and len(pos) >= 2: + with self._lock: + self._xy = {"x": round(float(pos[0]), 3), "y": round(float(pos[1]), 3)} + self._sport_ts = time.monotonic() + except Exception: + pass + + def snapshot(self) -> Dict[str, Any]: + with self._lock: + now = time.monotonic() + return { + "bms": dict(self._bms) if self._bms else None, + "low_age": (now - self._low_ts) if self._low_ts else None, + "temps": list(self._temps), + "max_dq": self._max_dq, + "xy": dict(self._xy) if self._xy else None, + } + + +class RosbridgePosition: + def __init__(self, cfg: Config): + self.cfg = cfg + self._xy: Optional[Dict[str, float]] = None + self._lock = threading.Lock() + self._stop = False + try: + import websocket # noqa: F401 + except Exception as e: + log.warning("websocket-client absent (%s) — position disabled", e) + self._ok = False + return + self._ok = True + threading.Thread(target=self._run, daemon=True).start() + + def _run(self) -> None: + import websocket + sub = json.dumps({"op": "subscribe", "topic": "/odom", + "type": "nav_msgs/Odometry", "throttle_rate": 500}) + while not self._stop: + try: + ws = websocket.create_connection(self.cfg.rosbridge_url, timeout=5) + ws.send(sub) + while not self._stop: + msg = json.loads(ws.recv()) + pos = (((msg.get("msg") or {}).get("pose") or {}).get("pose") or {}).get("position") + if pos: + with self._lock: + self._xy = {"x": round(float(pos["x"]), 3), "y": round(float(pos["y"]), 3)} + except Exception as e: + log.debug("rosbridge position reconnect: %s", e) + time.sleep(3) + + def get(self) -> Optional[Dict[str, float]]: + with self._lock: + return dict(self._xy) if self._xy else None + + +def derive_faults(cfg: Config, snap: Dict[str, Any]) -> List[Dict[str, Any]]: + faults: List[Dict[str, Any]] = [] + bms = snap.get("bms") + if bms and bms.get("soc", 100) <= cfg.low_soc: + faults.append({"code": "LOW_BATTERY", "severity": "warning", "message": f"battery {bms['soc']}%"}) + temps = snap.get("temps") or [] + if temps and max(temps) >= cfg.motor_temp_max: + faults.append({"code": "MOTOR_OVERTEMP", "severity": "warning", "message": f"motor temp {max(temps):.0f}C"}) + if snap.get("low_age") is not None and snap["low_age"] > 3.0: + faults.append({"code": "COMMS_STALE", "severity": "critical", "message": f"no rt/lowstate for {snap['low_age']:.0f}s"}) + return faults + + +def derive_status(cfg: Config, snap: Dict[str, Any]) -> str: + bms = snap.get("bms") + charging = bool(bms and bms.get("current_a", 0.0) > 0.05) + alive = snap.get("low_age") is not None and snap["low_age"] <= 3.0 + if not alive and bms is None: + return "offline" + if charging: + return "charging" + if snap.get("max_dq", 0.0) > 0.15: + return "moving" + return "idle" + + +def build_telemetry(cfg: Config, mac: str, reader: Optional[DDSReader], + pos: Optional[RosbridgePosition], + sim: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + if sim is not None: + snap = {"bms": {"soc": sim["battery"], "current_a": 0.5 if sim["charging"] else -0.3}, + "low_age": 0.1, "temps": [sim.get("temp", 45)], "max_dq": sim.get("max_dq", 0.0), + "xy": sim.get("position")} + else: + snap = reader.snapshot() if reader else {"bms": None, "low_age": None, "temps": [], "max_dq": 0.0, "xy": None} + + bms = snap.get("bms") + battery = bms["soc"] if bms else None + charging = bool(bms and bms.get("current_a", 0.0) > 0.05) + status = derive_status(cfg, snap) + faults = derive_faults(cfg, snap) + + position = snap.get("xy") + if position is None and pos is not None: + position = pos.get() + + return { + "sn": cfg.sn, "mac": mac, + "battery": battery, "charging": charging, "status": status, + "position": position, "faults": faults, "ts": int(time.time()), + } + + +def post_telemetry(cfg: Config, payload: Dict[str, Any], session: requests.Session) -> bool: + try: + r = session.post(cfg.telemetry_url(), json=payload, headers=cfg.auth_headers(), + timeout=cfg.http_timeout, verify=cfg.verify_tls) + except requests.RequestException as e: + log.error("telemetry POST failed (transport): %s", e) + return False + if not r.ok: + log.error("telemetry POST failed: HTTP %s %s", r.status_code, r.text[:200]) + return False + log.info("telemetry ok: battery=%s charging=%s status=%s pos=%s faults=%d -> HTTP %s", + payload["battery"], payload["charging"], payload["status"], + payload["position"], len(payload["faults"]), r.status_code) + return True + + +def _sim_state(i: int) -> Dict[str, Any]: + charging = (i % 6) in (0, 1) + battery = max(5, 90 - (i % 40)) + moving = (i % 3) == 2 and not charging + return {"battery": battery, "charging": charging, "temp": 45 + (i % 10), + "max_dq": 0.4 if moving else 0.0, + "position": {"x": round(1.0 + 0.1 * i, 2), "y": round(2.0 - 0.05 * i, 2)}} + + +def main(argv: Optional[List[str]] = None) -> int: + ap = argparse.ArgumentParser(description="Go2 fleet telemetry agent") + ap.add_argument("--simulate", action="store_true") + ap.add_argument("--once", action="store_true") + ap.add_argument("--dry-run", action="store_true") + ap.add_argument("--interval", type=float, default=None) + ap.add_argument("-v", "--verbose", action="store_true") + args = ap.parse_args(argv) + + logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO, + format="%(asctime)s %(levelname)s %(name)s: %(message)s") + _load_dotenv() + cfg = Config.from_env() + if args.interval is not None: + cfg.poll_interval = args.interval + + mac = read_mac(cfg.mac_interface) + log.info("sanad_api_go2 telemetry — sn=%s mac=%s server=%s iface=%s domain=%d%s", + cfg.sn, mac, cfg.server_url, cfg.dds_interface, cfg.dds_domain, + " [SIMULATE]" if args.simulate else "") + + reader = None + pos = None + if not args.simulate: + reader = DDSReader(cfg) + if cfg.position_source == "rosbridge": + pos = RosbridgePosition(cfg) + time.sleep(1.0) + + session = requests.Session() + tick = 0 + + def one() -> None: + nonlocal tick + sim = _sim_state(tick) if args.simulate else None + payload = build_telemetry(cfg, mac, reader, pos, sim=sim) + if args.dry_run: + log.info("[dry-run] %s", json.dumps(payload)) + else: + post_telemetry(cfg, payload, session) + tick += 1 + + if args.once: + one(); return 0 + if args.dry_run: + for _ in range(3): + one(); time.sleep(min(cfg.poll_interval, 1.0)) + return 0 + + log.info("loop every %.1fs (Ctrl-C to stop)", cfg.poll_interval) + while True: + try: + one() + except Exception as e: + log.exception("tick failed: %s", e) + try: + time.sleep(cfg.poll_interval) + except KeyboardInterrupt: + log.info("stopped"); return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/agents/go2/vendor/crc_aarch64.so b/agents/go2/vendor/crc_aarch64.so new file mode 100644 index 0000000000000000000000000000000000000000..878df74577e4fc4c90ff2d011c2344633c9798da GIT binary patch literal 7928 zcmeHMU2Ggz6+XMOaenMN8&eUJRAgQ9P?~1E2@cprqH8Diy2x>1vk@vnn#}&#Ua~*# z?5wbxh;9H?mEgf4mC^@jD}+QT70N?sE2vtxLMW&_M6^_Sp+r!GrAES01$0I2<(zxZ zSx74K}p`a}EokY=IEsyr<7=*5HNUM#|bSH@FWEW$*7~2W0vcAx5+C9+Y z&~=67e!8Fagpw|`FZ|ifQ2#glkbXL(v;c&P-51f0ADuOBO~Ncr@OYi_a+Tv3%B*^} z+tG)CSA6;;Jh=YcPf!2#%>zefGDni%+R?T5b5klD%HLMJ$W8$vP(u?&t@|InZ|Yv> zyMZ3U@}M3y1NtBwmWZ@q0!hT9Bi(yC;1o%-Ur)ZlIO=28_Sw4l!{i_ zwH?>8z`8$q+{$E~>};;=W}W2mqxn)Xo3vB;td_NxSm_1Zn#mRIeD2{aq@DDq4qNGx zlf`vQ5n;YL4>2Bp*oVK*a>La3+QMqQ76Z6ktJimvf3ijV5}bnb=a3E{y$|5sWq>5|O9MCJ zH5>I;jli>jz>x_wK+4qZi2j8)!ea60fC{eXwD_cIIXE zobPo*?E;Q@Dis@v1>gw!U4nPwHKdo-7~5jC*X#9**YJu!HE}8hBky&`YHMd+Q@ME? zpMm0EG2q$17=gP73>f;;^WRK#LG`x))xJ+W@w7woEsHZR21C_E04^nnf5(8kVPW5i zrvM%bf${n3rRttnAn;Y{51b90sD`V1#zOGWtLydTuR?IX4qc~L*6K+;AJsmhGFLKB zfxX6OqX0nb;-&R^eG=&nq+daLPrY8BN9rPd9O+r4&mj%tMJ@1f0zwNP5A5yPr!e~5 z=h2q@Q6oKiXCFf>hLlrl)ww?6cr-u&abH&QS!P#PEaAeRl z`L9)&cHEp+_nHe(;8;5+BTd>k}%vXl}8L+HO z$(qfVQg+_TxFx4-*%b_=RG80a-E77jIW#=dUWpC>bC&Hm_M(+7y3QiZICddxWh#Zj zBAR$O3pL#)RXj7qpe$=5F?xL58lOH!hmpq~oE|-X|51eTAW}dpQ!ZI^b}>UolH>=) zc$906PqOjku>`1Cz=}^jFg7}6JuoqGVmxUjN5`hd(QU`mN_G0#{)F>Z=c3I=s9=^C z3$C3)>N=XvNv~LPvnI~JnX2USnS;3uC~nR!&w-g)ETW~Rt^=l%&D#WWZ$9sWsh-z# zvkTa#bc30lBnLN1L1m(T~^OyxLZyEd4qav4mV>jKW7=WY4$ zU8-ZL7L@w2)9(o~g74n~m?-!=)`ns)^{vqTY$$c3)S2RM7&}zkioMiJLYG;Q#w8zO zFLh~*aj}=WO6Vz+dmDZZn%X3C0XwV&q!giiSqCh;pLc$@Rt zOC2dx#u0sg{6)5x{CE0Ys*^pPy;6u`-$*1UbV&@E`s05TaXR3Y`IY^h{BGsf7hdRh z(RQcDUh3Rob!xmZ->9OR*a>|SQ3_SYm;dAPyI_^`FL@I?$?K2MmQ=)E{_ieRP}uI$ zq;VH}p>%fcwU_#O4jQFYiC?lO=YKIYh|-+dpL>&8k?rs|bA)+SwoA N0ekkh+n0u4X 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

&M& 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/dev/null | head -1); \ + [ -n "$f" ] && ln -sf "$f" /usr/lib/"$lib" || true; \ + done + +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +# CycloneDDS python binding, compiled against the C lib above. 0.10.2's build +# helper imports wheel.bdist_wheel (removed in wheel>=0.46) → pin the toolchain. +RUN pip install --no-cache-dir "setuptools<80" "wheel<0.46" \ + && pip install --no-cache-dir --no-build-isolation cyclonedds==0.10.2 + +# Unitree SDK (vendored wheel — not on PyPI) + native crc lib (wheel omits it). +# Both arch crc libs are copied; unitree_sdk2py loads the one matching the image. +COPY vendor/unitree_sdk2py-*.whl /tmp/ +RUN pip install --no-cache-dir --no-deps /tmp/unitree_sdk2py-*.whl +COPY vendor/crc_aarch64.so vendor/crc_amd64.so \ + /usr/local/lib/python3.10/site-packages/unitree_sdk2py/utils/lib/ + +COPY sanad_api_r1.py . + +ENV PYTHONUNBUFFERED=1 \ + DDS_INTERFACE=eth10 \ + DDS_DOMAIN=0 +ENTRYPOINT ["python", "-u", "sanad_api_r1.py"] diff --git a/agents/r1/README.md b/agents/r1/README.md new file mode 100644 index 0000000..5e55095 --- /dev/null +++ b/agents/r1/README.md @@ -0,0 +1,82 @@ +# sanad_api_r1 — R1 fleet **telemetry** agent + +Pushes the Unitree **R1 EDU**'s live status to the YS Lootah fleet server. One +robot type = one folder = its own Docker image (sibling of `sanad_api_g1`). + +This build covers the **"Main statuses"** endpoint — the R1 **map is skipped by +request**: + +``` +POST {SERVER_URL}/api/v1/fleet/ingest/telemetry Authorization: Bearer +{ "sn": "r1_0000", "mac": "…", "battery": 74, "charging": false, + "status": "idle", "position": {"x": …, "y": …}|null, "faults": [] } +``` + +Sent every ~2 s. If DDS state can't be read it still sends a **heartbeat** +(`battery: null`, `status: "offline"`) so the robot shows as online. + +## Where each field comes from (R1 EDU, unitree_hg DDS) + +| field | source | +|---|---| +| `battery`, `charging` | `rt/lf/bmsstate` (`BmsState_`): `soc` 0–100; `charging = current > +0.05 A` — same as SanadR1 `arm_controller.get_battery` | +| `faults[]` | `rt/lowstate` (`LowState_`): low battery, motor over-temp, comms-stale | +| `status` | derived (charging / moving / idle / offline); optional loco **FSM** read (ids `0`/`1`/`4`/`811`, GET-only) with `R1_READ_FSM=1` | +| `position` | R1 localizes with stereo **VSLAM** (ROS side). This agent is ROS-free, so position is `null` unless `R1_POSITION_SOURCE=rosbridge` (reads `/odom`) | +| `mac` | primary NIC hardware address | + +> **Safety:** never commands motion — only the read-only `GET_FSM_ID` RPC is ever +> issued to the R1. + +## Install (full Docker) — on the R1 backpack + +One command — builds the DDS image, creates `.env`, starts the container (auto-restarts on boot): + +```bash +./install.sh # creates .env if missing, then build + up -d +# (edit .env when prompted: SERVER_URL, DEVICE_TOKEN, SN, DDS_INTERFACE=eth10) +./install.sh --logs # follow logs +./install.sh --simulate # one synthetic post (server smoke test, no robot) +./install.sh --down # stop + remove +``` + +Or the raw compose flow: + +```bash +cp .env.example .env # SERVER_URL, DEVICE_TOKEN, SN, DDS_INTERFACE=eth10 +docker compose up -d --build +docker compose logs -f +``` + +Test without a robot (synthetic state, exercises the full upload path): + +```bash +docker compose run --rm sanad-api-r1 --simulate --once # one synthetic post +docker compose run --rm sanad-api-r1 --dry-run # print telemetry, no POST +# or bare: +SERVER_URL=… DEVICE_TOKEN=… SN=r1_0000 python3 sanad_api_r1.py --simulate -v +``` + +### Key env (full list in `.env.example`) + +| var | meaning | +|---|---| +| `SERVER_URL`, `DEVICE_TOKEN` | given by YS Lootah — **required** | +| `SN` | fleet id, e.g. `r1_0000` | +| `DDS_INTERFACE` | robot link — **`eth10`** on the R1 backpack | +| `R1_READ_FSM` | `1` = read loco FSM for status (GET-only) | +| `R1_POSITION_SOURCE` | `none` (default) · `rosbridge` | +| `POLL_INTERVAL` | seconds between posts (default 2) | + +## Build note + +Build **on the R1 backpack** (arm64). The image bundles the DDS stack +(CycloneDDS + the vendored `unitree_sdk2py` wheel under `vendor/`), mirroring +SanadR1's recipe. `network_mode: host` is required for DDS visibility. If +`unitree_sdk2py` can't load, the agent logs a warning and sends heartbeats. + +## Porting + +`sanad_api_go2` is the same pattern; the Go2 uses `unitree_go` (not `unitree_hg`) +DDS, so swap the message imports (`BmsState_`/`LowState_` idl path) and topic +names for the Go2 SDK. diff --git a/agents/r1/docker-compose.yml b/agents/r1/docker-compose.yml new file mode 100644 index 0000000..c9b97b9 --- /dev/null +++ b/agents/r1/docker-compose.yml @@ -0,0 +1,21 @@ +# sanad_api_r1 — standalone R1 fleet TELEMETRY agent. +# +# cp .env.example .env # set SERVER_URL + DEVICE_TOKEN + SN + DDS_INTERFACE +# docker compose up -d --build +# +# Reads the R1's DDS state (rt/lowstate + rt/lf/bmsstate) and POSTs telemetry +# every ~2s to the fleet server. network_mode: host is REQUIRED so the robot's +# DDS traffic is visible (it does not traverse a NAT bridge). +services: + sanad-api-r1: + build: + context: . + dockerfile: Dockerfile + image: "${SANAD_API_R1_IMAGE:-sanad-api-r1:latest}" + container_name: sanad-api-r1 + network_mode: host # DDS visibility + outbound HTTPS + restart: unless-stopped + env_file: .env + # Uncomment to run the synthetic-state generator instead of real DDS + # (useful for a server-side smoke test before the robot is wired): + # command: ["--simulate"] diff --git a/agents/r1/requirements.txt b/agents/r1/requirements.txt new file mode 100644 index 0000000..b462572 --- /dev/null +++ b/agents/r1/requirements.txt @@ -0,0 +1,7 @@ +# sanad_api_r1 telemetry agent. +requests>=2.31,<3 +# only needed if R1_POSITION_SOURCE=rosbridge (reads /odom for position): +websocket-client>=1.6,<2 +# DDS stack (cyclonedds + the vendored unitree_sdk2py wheel) is installed by the +# Dockerfile, not from here — see Dockerfile. The agent degrades to heartbeats if +# unitree_sdk2py is unavailable. diff --git a/agents/r1/sanad_api_r1.py b/agents/r1/sanad_api_r1.py new file mode 100644 index 0000000..dcbcd2f --- /dev/null +++ b/agents/r1/sanad_api_r1.py @@ -0,0 +1,529 @@ +#!/usr/bin/env python3 +"""sanad_api_r1 — R1 fleet TELEMETRY agent. + +Scope (this build): push the R1's live status to the YS Lootah fleet server. +This is the "Main statuses" row of the fleet spec (NOT the map — the R1 map is +skipped by request): + + POST {SERVER_URL}/api/v1/fleet/ingest/telemetry (Bearer device token) + body: { "sn", "mac", "battery", "charging", "status", "position":{x,y}, "faults":[] } + +Sent every ~2 s. Per the spec: if state can't be read, still send a heartbeat so +the robot stays "online". + +DATA SOURCES (Unitree R1 EDU, unitree_hg DDS — same family as the G1) +-------------------------------------------------------------------- + battery / charging : rt/lf/bmsstate (BmsState_) soc 0-100; charging = current>+0.05A + (mirrors SanadR1 motion/arm_controller.get_battery) + faults / liveness : rt/lowstate (LowState_) motor temps + message staleness + status : R1 loco FSM via GET RPC 7001 (ids 0 ZeroTorque / 1 Damp / + 4 Locked-Standing / 811 Gait-Running) — READ-ONLY, optional + (R1_READ_FSM=1). Default derives status from BMS + motion. + position {x,y} : OPTIONAL. R1 localizes with stereo VSLAM (ROS side); this + agent has no ROS, so position is read over rosbridge /odom + only when R1_POSITION_SOURCE=rosbridge, else omitted. + mac : primary NIC hardware address. + +SAFETY: never commands motion. Only GET RPCs are ever issued to the R1. + +NO ROS. DDS via unitree_sdk2py (net=host + the robot interface). If unitree_sdk2py +is unavailable it degrades to heartbeats. --simulate feeds synthetic state so the +upload path is testable without a robot. + +CONFIG — environment (see .env.example) +--------------------------------------- + SERVER_URL, DEVICE_TOKEN fleet base URL + bearer token (required) + SN this robot's fleet id default r1_0000 + DDS_INTERFACE robot network iface for DDS default eth0 + DDS_DOMAIN DDS domain id default 0 + MAC_INTERFACE iface whose MAC to report default = DDS_INTERFACE + R1_READ_FSM 1 = read loco FSM for status default 0 + R1_POSITION_SOURCE none | rosbridge default none + ROSBRIDGE_URL ws://127.0.0.1:9090 (position) default ws://127.0.0.1:9090 + LOW_SOC % below which -> LOW_BATTERY fault default 15 + MOTOR_TEMP_MAX °C above which -> OVERTEMP fault default 85 + POLL_INTERVAL seconds between telemetry posts default 2 + VERIFY_TLS / HTTP_TIMEOUT TLS verify (1) / per-req timeout (10) + +CLI +--- + python sanad_api_r1.py # real DDS loop (default) + python sanad_api_r1.py --simulate # synthetic state (no robot) — for testing + python sanad_api_r1.py --once # one read+post, then exit + python sanad_api_r1.py --dry-run # build telemetry, print it, never POST +""" +from __future__ import annotations + +import argparse +import json +import logging +import math +import os +import socket +import sys +import threading +import time +import uuid +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Dict, List, Optional + +import requests + +log = logging.getLogger("sanad_api_r1") + + +# --------------------------------------------------------------------------- # +# env helpers +# --------------------------------------------------------------------------- # +def _load_dotenv(path: str = ".env") -> None: + p = Path(path) + if not p.exists(): + return + for line in p.read_text().splitlines(): + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + k, _, v = line.partition("=") + os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'")) + + +def _env(name: str, default: str = "") -> str: + return os.environ.get(name, default).strip() + + +def _env_bool(name: str, default: bool) -> bool: + return _env(name, "1" if default else "0").lower() in ("1", "true", "yes", "on") + + +# --------------------------------------------------------------------------- # +# config +# --------------------------------------------------------------------------- # +@dataclass +class Config: + server_url: str + device_token: str + sn: str + dds_interface: str + dds_domain: int + mac_interface: str + read_fsm: bool + position_source: str + rosbridge_url: str + low_soc: int + motor_temp_max: float + poll_interval: float + endpoint: str + verify_tls: bool + http_timeout: float + + @classmethod + def from_env(cls) -> "Config": + server = _env("SERVER_URL").rstrip("/") + token = _env("DEVICE_TOKEN") + missing = [n for n, v in (("SERVER_URL", server), ("DEVICE_TOKEN", token)) if not v] + if missing: + raise SystemExit(f"[config] missing required env: {', '.join(missing)}") + iface = _env("DDS_INTERFACE", "eth0") + return cls( + server_url=server, + device_token=token, + sn=_env("SN", "r1_0000"), + dds_interface=iface, + dds_domain=int(_env("DDS_DOMAIN", "0")), + mac_interface=_env("MAC_INTERFACE", iface), + read_fsm=_env_bool("R1_READ_FSM", False), + position_source=_env("R1_POSITION_SOURCE", "none").lower(), + rosbridge_url=_env("ROSBRIDGE_URL", "ws://127.0.0.1:9090"), + low_soc=int(_env("LOW_SOC", "15")), + motor_temp_max=float(_env("MOTOR_TEMP_MAX", "85")), + poll_interval=float(_env("POLL_INTERVAL", "2")), + endpoint=_env("TELEMETRY_ENDPOINT", "/api/v1/fleet/ingest/telemetry"), + verify_tls=_env_bool("VERIFY_TLS", True), + http_timeout=float(_env("HTTP_TIMEOUT", "10")), + ) + + def telemetry_url(self) -> str: + return self.server_url + self.endpoint + + def auth_headers(self) -> Dict[str, str]: + return {"Authorization": f"Bearer {self.device_token}"} + + +# --------------------------------------------------------------------------- # +# mac address +# --------------------------------------------------------------------------- # +def read_mac(interface: str) -> str: + """Stable hardware MAC. Prefer the named NIC (/sys), fall back to uuid.getnode. + + NOTE: with docker network_mode: host the container shares the host net + namespace, so this is the real robot NIC MAC (not a virtual docker MAC).""" + p = Path(f"/sys/class/net/{interface}/address") + try: + mac = p.read_text().strip() + if mac and mac != "00:00:00:00:00:00": + return mac.lower() + except Exception: + pass + n = uuid.getnode() + return ":".join(f"{(n >> b) & 0xff:02x}" for b in range(40, -1, -8)) + + +# --------------------------------------------------------------------------- # +# DDS reader (optional — degrades if unitree_sdk2py is absent) +# --------------------------------------------------------------------------- # +class DDSReader: + """Subscribes rt/lf/bmsstate + rt/lowstate and (optionally) reads the loco + FSM. All reads are passive; the only RPC ever issued is GET_FSM_ID.""" + + def __init__(self, cfg: Config): + self.cfg = cfg + self._lock = threading.Lock() + self._bms: Optional[Dict[str, Any]] = None + self._bms_ts = 0.0 + self._low = None + self._low_ts = 0.0 + self._temps: List[float] = [] + self._max_dq = 0.0 + self._loco = None + self.ok = False + self._start() + + def _start(self) -> None: + try: + from unitree_sdk2py.core.channel import ( + ChannelFactoryInitialize, ChannelSubscriber) + from unitree_sdk2py.idl.unitree_hg.msg.dds_ import LowState_ + try: + from unitree_sdk2py.idl.unitree_hg.msg.dds_ import BmsState_ + except Exception: + BmsState_ = None + except Exception as e: + log.warning("unitree_sdk2py unavailable (%s) — telemetry runs in heartbeat mode", e) + return + + try: + ChannelFactoryInitialize(self.cfg.dds_domain, self.cfg.dds_interface) + self._low_sub = ChannelSubscriber("rt/lowstate", LowState_) + self._low_sub.Init(self._on_low, 10) + if BmsState_ is not None: + self._bms_sub = ChannelSubscriber("rt/lf/bmsstate", BmsState_) + self._bms_sub.Init(self._on_bms, 10) + else: + log.warning("BmsState_ not in this unitree_sdk2py — battery will be null") + if self.cfg.read_fsm: + self._init_loco() + self.ok = True + log.info("DDS up: domain=%d iface=%s (rt/lowstate + rt/lf/bmsstate)", + self.cfg.dds_domain, self.cfg.dds_interface) + except Exception as e: + log.warning("DDS init failed (%s) — heartbeat mode", e) + + def _init_loco(self) -> None: + """Loco client for READ-ONLY FSM id (GET RPC 7001). Never sends motion.""" + try: + from unitree_sdk2py.rpc.client import Client # type: ignore + except Exception as e: + log.warning("loco RPC client unavailable (%s) — status from BMS/motion only", e) + return + try: + # R1 loco service ("loco"), GET_FSM_ID = 7001 (see R1 r1_loco_client). + c = Client("loco", 0) + c.Init() + c.SetTimeout(3.0) + self._loco = c + log.info("loco FSM read enabled (GET-only, no motion)") + except Exception as e: + log.warning("loco client init failed (%s) — status from BMS/motion only", e) + self._loco = None + + # -- callbacks -- + def _on_bms(self, msg) -> None: + try: + soc = int(getattr(msg, "soc", 0) or 0) + cur_mA = int(getattr(msg, "current", 0) or 0) + batt = { + "soc": max(0, min(100, soc)), + "current_a": round(cur_mA / 1000.0, 2), + "soh": int(getattr(msg, "soh", 0) or 0), + "cycle": int(getattr(msg, "cycle", 0) or 0), + } + with self._lock: + self._bms = batt + self._bms_ts = time.monotonic() + except Exception: + pass + + def _on_low(self, msg) -> None: + try: + temps: List[float] = [] + max_dq = 0.0 + ms = getattr(msg, "motor_state", None) or [] + for m in ms: + t = getattr(m, "temperature", None) + if t is not None: + try: + # temperature may be a scalar or a small array (surface/winding) + vals = [float(x) for x in t] if hasattr(t, "__iter__") else [float(t)] + temps.extend(v for v in vals if -40 <= v <= 200) + except Exception: + pass + dq = getattr(m, "dq", None) + if dq is not None: + try: + max_dq = max(max_dq, abs(float(dq))) + except Exception: + pass + with self._lock: + self._low = msg + self._low_ts = time.monotonic() + self._temps = temps + self._max_dq = max_dq + except Exception: + pass + + # -- reads -- + def snapshot(self) -> Dict[str, Any]: + with self._lock: + now = time.monotonic() + return { + "bms": dict(self._bms) if self._bms else None, + "bms_age": (now - self._bms_ts) if self._bms_ts else None, + "low_age": (now - self._low_ts) if self._low_ts else None, + "temps": list(self._temps), + "max_dq": self._max_dq, + } + + def fsm_id(self) -> Optional[int]: + if not self._loco: + return None + try: + code, data = self._loco._Call(7001, "{}") # GET_FSM_ID — read-only + if code == 0 and data: + return int(json.loads(data).get("data", json.loads(data)) if data.strip().startswith("{") else data) + except Exception as e: + log.debug("fsm read failed: %s", e) + return None + + +_FSM_STATUS = {0: "zero_torque", 1: "damping", 4: "standing", 811: "ready"} + + +# --------------------------------------------------------------------------- # +# optional position over rosbridge (/odom) +# --------------------------------------------------------------------------- # +class RosbridgePosition: + def __init__(self, cfg: Config): + self.cfg = cfg + self._xy: Optional[Dict[str, float]] = None + self._lock = threading.Lock() + self._stop = False + try: + import websocket # noqa: F401 (websocket-client) + except Exception as e: + log.warning("websocket-client absent (%s) — position disabled", e) + self._ok = False + return + self._ok = True + threading.Thread(target=self._run, daemon=True).start() + + def _run(self) -> None: + import websocket + sub = json.dumps({"op": "subscribe", "topic": "/odom", + "type": "nav_msgs/Odometry", "throttle_rate": 500}) + while not self._stop: + try: + ws = websocket.create_connection(self.cfg.rosbridge_url, timeout=5) + ws.send(sub) + while not self._stop: + msg = json.loads(ws.recv()) + pos = (((msg.get("msg") or {}).get("pose") or {}).get("pose") or {}).get("position") + if pos: + with self._lock: + self._xy = {"x": round(float(pos["x"]), 3), "y": round(float(pos["y"]), 3)} + except Exception as e: + log.debug("rosbridge position reconnect: %s", e) + time.sleep(3) + + def get(self) -> Optional[Dict[str, float]]: + with self._lock: + return dict(self._xy) if self._xy else None + + +# --------------------------------------------------------------------------- # +# telemetry assembly +# --------------------------------------------------------------------------- # +def derive_faults(cfg: Config, snap: Dict[str, Any]) -> List[Dict[str, Any]]: + faults: List[Dict[str, Any]] = [] + bms = snap.get("bms") + if bms and bms.get("soc", 100) <= cfg.low_soc: + faults.append({"code": "LOW_BATTERY", "severity": "warning", + "message": f"battery {bms['soc']}%"}) + temps = snap.get("temps") or [] + if temps: + hot = max(temps) + if hot >= cfg.motor_temp_max: + faults.append({"code": "MOTOR_OVERTEMP", "severity": "warning", + "message": f"motor temp {hot:.0f}C"}) + if snap.get("low_age") is not None and snap["low_age"] > 3.0: + faults.append({"code": "COMMS_STALE", "severity": "critical", + "message": f"no rt/lowstate for {snap['low_age']:.0f}s"}) + return faults + + +def derive_status(cfg: Config, snap: Dict[str, Any], fsm: Optional[int]) -> str: + if fsm is not None and fsm in _FSM_STATUS: + base = _FSM_STATUS[fsm] + else: + base = None + bms = snap.get("bms") + charging = bool(bms and bms.get("current_a", 0.0) > 0.05) + alive = snap.get("low_age") is not None and snap["low_age"] <= 3.0 + if not alive and bms is None: + return "offline" + if charging: + return "charging" + if snap.get("max_dq", 0.0) > 0.15: + return "moving" + return base or "idle" + + +def build_telemetry(cfg: Config, mac: str, reader: Optional[DDSReader], + pos: Optional[RosbridgePosition], + sim: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + if sim is not None: + snap = {"bms": {"soc": sim["battery"], "current_a": 0.5 if sim["charging"] else -0.3}, + "bms_age": 0.1, "low_age": 0.1, "temps": [sim.get("temp", 45)], "max_dq": sim.get("max_dq", 0.0)} + fsm = sim.get("fsm") + else: + snap = reader.snapshot() if reader else {"bms": None, "low_age": None, "temps": [], "max_dq": 0.0} + fsm = reader.fsm_id() if (reader and cfg.read_fsm) else None + + bms = snap.get("bms") + battery = bms["soc"] if bms else None + charging = bool(bms and bms.get("current_a", 0.0) > 0.05) + status = derive_status(cfg, snap, fsm) + faults = derive_faults(cfg, snap) + + position = None + if sim is not None: + position = sim.get("position") + elif pos is not None: + position = pos.get() + + payload: Dict[str, Any] = { + "sn": cfg.sn, + "mac": mac, + "battery": battery, # null = couldn't read (heartbeat) + "charging": charging, + "status": status, + "position": position, # null when no localization source + "faults": faults, + "ts": int(time.time()), + } + return payload + + +def post_telemetry(cfg: Config, payload: Dict[str, Any], + session: requests.Session) -> bool: + try: + r = session.post(cfg.telemetry_url(), json=payload, + headers=cfg.auth_headers(), + timeout=cfg.http_timeout, verify=cfg.verify_tls) + except requests.RequestException as e: + log.error("telemetry POST failed (transport): %s", e) + return False + if not r.ok: + log.error("telemetry POST failed: HTTP %s %s", r.status_code, r.text[:200]) + return False + log.info("telemetry ok: battery=%s charging=%s status=%s pos=%s faults=%d -> HTTP %s", + payload["battery"], payload["charging"], payload["status"], + payload["position"], len(payload["faults"]), r.status_code) + return True + + +# --------------------------------------------------------------------------- # +# main +# --------------------------------------------------------------------------- # +def _sim_state(i: int) -> Dict[str, Any]: + """Deterministic-ish synthetic state that varies each tick (for testing).""" + charging = (i % 6) in (0, 1) + battery = max(5, 90 - (i % 40)) + moving = (i % 3) == 2 and not charging + return { + "battery": battery, + "charging": charging, + "temp": 45 + (i % 10), + "max_dq": 0.4 if moving else 0.0, + "fsm": 811 if moving else 4, + "position": {"x": round(1.0 + 0.1 * i, 2), "y": round(2.0 - 0.05 * i, 2)}, + } + + +def main(argv: Optional[List[str]] = None) -> int: + ap = argparse.ArgumentParser(description="R1 fleet telemetry agent") + ap.add_argument("--simulate", action="store_true", help="synthetic state (no robot)") + ap.add_argument("--once", action="store_true", help="one read+post, then exit") + ap.add_argument("--dry-run", action="store_true", help="print telemetry, never POST") + ap.add_argument("--interval", type=float, default=None) + ap.add_argument("-v", "--verbose", action="store_true") + args = ap.parse_args(argv) + + logging.basicConfig( + level=logging.DEBUG if args.verbose else logging.INFO, + format="%(asctime)s %(levelname)s %(name)s: %(message)s") + _load_dotenv() + cfg = Config.from_env() + if args.interval is not None: + cfg.poll_interval = args.interval + + mac = read_mac(cfg.mac_interface) + log.info("sanad_api_r1 telemetry — sn=%s mac=%s server=%s iface=%s domain=%d%s", + cfg.sn, mac, cfg.server_url, cfg.dds_interface, cfg.dds_domain, + " [SIMULATE]" if args.simulate else "") + + reader = None + pos = None + if not args.simulate: + reader = DDSReader(cfg) + if cfg.position_source == "rosbridge": + pos = RosbridgePosition(cfg) + time.sleep(1.0) # let first DDS messages land + + session = requests.Session() + tick = 0 + + def one() -> None: + nonlocal tick + sim = _sim_state(tick) if args.simulate else None + payload = build_telemetry(cfg, mac, reader, pos, sim=sim) + if args.dry_run: + log.info("[dry-run] %s", json.dumps(payload)) + else: + post_telemetry(cfg, payload, session) + tick += 1 + + if args.once or (args.dry_run and args.once): + one() + return 0 + if args.dry_run: + for _ in range(3): + one() + time.sleep(min(cfg.poll_interval, 1.0)) + return 0 + + log.info("loop every %.1fs (Ctrl-C to stop)", cfg.poll_interval) + while True: + try: + one() + except Exception as e: + log.exception("tick failed: %s", e) + try: + time.sleep(cfg.poll_interval) + except KeyboardInterrupt: + log.info("stopped") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/agents/r1/vendor/crc_aarch64.so b/agents/r1/vendor/crc_aarch64.so new file mode 100644 index 0000000000000000000000000000000000000000..878df74577e4fc4c90ff2d011c2344633c9798da GIT binary patch literal 7928 zcmeHMU2Ggz6+XMOaenMN8&eUJRAgQ9P?~1E2@cprqH8Diy2x>1vk@vnn#}&#Ua~*# z?5wbxh;9H?mEgf4mC^@jD}+QT70N?sE2vtxLMW&_M6^_Sp+r!GrAES01$0I2<(zxZ zSx74K}p`a}EokY=IEsyr<7=*5HNUM#|bSH@FWEW$*7~2W0vcAx5+C9+Y z&~=67e!8Fagpw|`FZ|ifQ2#glkbXL(v;c&P-51f0ADuOBO~Ncr@OYi_a+Tv3%B*^} z+tG)CSA6;;Jh=YcPf!2#%>zefGDni%+R?T5b5klD%HLMJ$W8$vP(u?&t@|InZ|Yv> zyMZ3U@}M3y1NtBwmWZ@q0!hT9Bi(yC;1o%-Ur)ZlIO=28_Sw4l!{i_ zwH?>8z`8$q+{$E~>};;=W}W2mqxn)Xo3vB;td_NxSm_1Zn#mRIeD2{aq@DDq4qNGx zlf`vQ5n;YL4>2Bp*oVK*a>La3+QMqQ76Z6ktJimvf3ijV5}bnb=a3E{y$|5sWq>5|O9MCJ zH5>I;jli>jz>x_wK+4qZi2j8)!ea60fC{eXwD_cIIXE zobPo*?E;Q@Dis@v1>gw!U4nPwHKdo-7~5jC*X#9**YJu!HE}8hBky&`YHMd+Q@ME? zpMm0EG2q$17=gP73>f;;^WRK#LG`x))xJ+W@w7woEsHZR21C_E04^nnf5(8kVPW5i zrvM%bf${n3rRttnAn;Y{51b90sD`V1#zOGWtLydTuR?IX4qc~L*6K+;AJsmhGFLKB zfxX6OqX0nb;-&R^eG=&nq+daLPrY8BN9rPd9O+r4&mj%tMJ@1f0zwNP5A5yPr!e~5 z=h2q@Q6oKiXCFf>hLlrl)ww?6cr-u&abH&QS!P#PEaAeRl z`L9)&cHEp+_nHe(;8;5+BTd>k}%vXl}8L+HO z$(qfVQg+_TxFx4-*%b_=RG80a-E77jIW#=dUWpC>bC&Hm_M(+7y3QiZICddxWh#Zj zBAR$O3pL#)RXj7qpe$=5F?xL58lOH!hmpq~oE|-X|51eTAW}dpQ!ZI^b}>UolH>=) zc$906PqOjku>`1Cz=}^jFg7}6JuoqGVmxUjN5`hd(QU`mN_G0#{)F>Z=c3I=s9=^C z3$C3)>N=XvNv~LPvnI~JnX2USnS;3uC~nR!&w-g)ETW~Rt^=l%&D#WWZ$9sWsh-z# zvkTa#bc30lBnLN1L1m(T~^OyxLZyEd4qav4mV>jKW7=WY4$ zU8-ZL7L@w2)9(o~g74n~m?-!=)`ns)^{vqTY$$c3)S2RM7&}zkioMiJLYG;Q#w8zO zFLh~*aj}=WO6Vz+dmDZZn%X3C0XwV&q!giiSqCh;pLc$@Rt zOC2dx#u0sg{6)5x{CE0Ys*^pPy;6u`-$*1UbV&@E`s05TaXR3Y`IY^h{BGsf7hdRh z(RQcDUh3Rob!xmZ->9OR*a>|SQ3_SYm;dAPyI_^`FL@I?$?K2MmQ=)E{_ieRP}uI$ zq;VH}p>%fcwU_#O4jQFYiC?lO=YKIYh|-+dpL>&8k?rs|bA)+SwoA N0ekkh+n0u4X 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

&M& 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 [--sn NAME] [--server-ip IP] [--port N] [--token TOK] [--user U] +# ./fleet_install.sh uninstall [--user U] +# ./fleet_install.sh status +# ./fleet_install.sh data # show the data it is sending +# ./fleet_install.sh logs +# ./fleet_install.sh test [--server-ip IP] [--keep-server] +# +# Deploy model: rsync the canonical workstation copy -> robot, `docker build` +# on the robot (native arm64), `docker create` the container, and a user +# systemd unit (linger-enabled) owns start/stop/auto-start. No docker-compose +# needed on the robot; no sudo needed (user-level systemd). +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +AGENTS="$SCRIPT_DIR/agents" +TYPES="g1 r1 go2" +declare -A KIND=( [g1]="MAP uploader" [r1]="TELEMETRY (unitree_hg)" [go2]="TELEMETRY (unitree_go)" ) + +die(){ echo "ERROR: $*" >&2; exit 1; } +img_of(){ echo "sanad-api-$1"; } # image + container + unit share this base +rdir_of(){ echo "sanad_api_$1"; } +unit_of(){ echo "sanad-api-$1.service"; } + +SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new) +rmt(){ ssh -n "${SSH_OPTS[@]}" "$USER_@$IP" "$@"; } # -n: never read our stdin +rmt_in(){ ssh "${SSH_OPTS[@]}" "$USER_@$IP" "$@"; } # stdin passthrough (heredocs) +detect_server_ip(){ [ -n "$SERVER_IP" ] && { echo "$SERVER_IP"; return; } + ip -o route get "$IP" 2>/dev/null | grep -oP 'src \K\S+' | head -1; } + +# ---- per-type docker create args (volumes differ) ---- +run_args(){ + local base="--network host --env-file /home/$USER_/$(rdir_of "$1")/.env" + if [ "$1" = g1 ]; then + echo "$base -v /home/$USER_/$(rdir_of "$1")/maps:/data/maps:ro \ + -v /home/$USER_/$(rdir_of "$1")/web_data:/data/web_data:ro \ + -v /home/$USER_/$(rdir_of "$1")/state:/data/state" + else + echo "$base" + fi +} + +# ---- write the robot-side .env ---- +push_env(){ + local t="$1" sip="$2" rdir; rdir="$(rdir_of "$t")" + if [ "$t" = g1 ]; then + rmt_in "cat > ~/$rdir/.env" < ~/$rdir/.env" </dev/null 2>&1" 2>/dev/null +} + +do_install(){ + local t="$1" img rdir unit sip; img="$(img_of "$t")"; rdir="$(rdir_of "$t")"; unit="$(unit_of "$t")" + sip="$(detect_server_ip)"; [ -n "$sip" ] || die "cannot detect server IP toward $IP (use --server-ip)" + echo "== INSTALL $t ($SN) on $IP → fleet server http://$sip:$PORT ==" + echo ">> rsync agent -> $USER_@$IP:~/$rdir/" + rsync -az --delete --exclude '.env' --exclude '__pycache__' --exclude '*.pyc' \ + --exclude 'data/' --exclude 'state/' \ + -e "ssh ${SSH_OPTS[*]}" "$AGENTS/$t/" "$USER_@$IP:~/$rdir/" || die "rsync failed" + rmt "mkdir -p ~/$rdir/maps/sanad ~/$rdir/web_data ~/$rdir/state ~/.config/systemd/user" + push_env "$t" "$sip" + echo ">> docker build on robot (native arm64; R1/Go2 compile DDS on first build) ..." + rmt "cd ~/$rdir && docker build -t $img:latest ." || die "docker build failed" + echo ">> creating container (systemd will own its lifecycle) ..." + rmt "docker rm -f $img >/dev/null 2>&1; docker create --name $img $(run_args "$t") $img:latest >/dev/null" + echo ">> installing user systemd service $unit (auto-start on boot via linger) ..." + rmt_in "cat > ~/.config/systemd/user/$unit" < $SN +After=docker.service network-online.target + +[Service] +Type=simple +Restart=always +RestartSec=5 +ExecStart=/usr/bin/docker start -a $img +ExecStop=/usr/bin/docker stop -t 10 $img + +[Install] +WantedBy=default.target +EOF + rmt "loginctl enable-linger \$USER >/dev/null 2>&1; \ + systemctl --user daemon-reload; \ + systemctl --user enable --now $unit" + sleep 2 + echo ">> service state:"; rmt "systemctl --user --no-pager -l status $unit | sed -n '1,4p'" + echo "== installed. show data: $0 data $t $IP ==" +} + +do_uninstall(){ + local t="$1" img unit rdir; img="$(img_of "$t")"; unit="$(unit_of "$t")"; rdir="$(rdir_of "$t")" + echo "== UNINSTALL $t on $IP ==" + rmt "systemctl --user disable --now $unit >/dev/null 2>&1; \ + rm -f ~/.config/systemd/user/$unit; systemctl --user daemon-reload; \ + 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" + echo "== uninstalled ==" +} + +do_status(){ + local t="$1" img unit; img="$(img_of "$t")"; unit="$(unit_of "$t")" + echo "== STATUS $t on $IP ==" + rmt "systemctl --user --no-pager status $unit 2>/dev/null | sed -n '1,4p' || echo 'no service'; echo; \ + docker ps -a --filter name=$img --format 'container: {{.Names}} {{.Status}} ({{.Image}})' || echo 'no container'" +} + +do_logs(){ rmt "docker logs --tail 40 -f $(img_of "$1")"; } + +do_data(){ + local t="$1" img; img="$(img_of "$t")" + echo "== DATA $t on $IP (what it is sending to the fleet server) ==" + rmt "systemctl --user --no-pager status $(unit_of "$t") 2>/dev/null | sed -n '1,3p'; echo '--- recent telemetry/map posts ---'; \ + docker logs --tail 12 $img 2>&1 | grep -E 'telemetry ok|uploaded|nothing to upload|heartbeat|POST failed' | tail -8 || docker logs --tail 12 $img 2>&1" +} + +# ---- deploy-less end-to-end test against the workstation server ---- +start_server(){ local reqlog="/tmp/fleet_reqs_${1}_$$.jsonl"; echo "$reqlog" > /tmp/fleet_last_reqlog + PORT=$PORT REQLOG="$reqlog" python3 "$SCRIPT_DIR/fleet_test_server.py" >/tmp/fleet_srv_$$.log 2>&1 & + echo $! > /tmp/fleet_srv_pid; sleep 1; } +stop_server(){ [ -f /tmp/fleet_srv_pid ] && kill "$(cat /tmp/fleet_srv_pid)" 2>/dev/null; rm -f /tmp/fleet_srv_pid; } + +do_test(){ + local t="$1" img sip; img="$(img_of "$t")" + sip="$(detect_server_ip)"; [ -n "$sip" ] || die "cannot detect server IP (use --server-ip)" + echo "== TEST $t via $IP (workstation server http://$sip:$PORT) ==" + rmt "docker image inspect $img:latest >/dev/null 2>&1" || { echo ">> not installed yet — installing first"; do_install "$t"; } + push_env "$t" "$sip" + start_server "$t"; trap 'stop_server' EXIT + local rc; rc=$(rmt "curl -s -o /dev/null -w '%{http_code}' --max-time 6 http://$sip:$PORT/ping" || echo 000) + [ "$rc" = 200 ] || die "robot cannot reach workstation server (http $rc) — firewall on $sip:$PORT?" + echo " reachability OK (HTTP $rc)" + if [ "$t" = g1 ]; then + rmt "mkdir -p ~/$(rdir_of g1)/maps/sanad ~/$(rdir_of g1)/web_data/sanad/places + head -c 4096 /dev/urandom > ~/$(rdir_of g1)/maps/sanad/floor-test.db + printf '%s' '{\"dock\":{\"x\":1.2,\"y\":3.4,\"qz\":0,\"qw\":1}}' > ~/$(rdir_of g1)/web_data/sanad/places/floor-test.json + rm -f ~/$(rdir_of g1)/state/uploaded.json" + rmt "docker run --rm $(run_args g1) $img:latest --once --force" || true + else + echo ">> real DDS --once:"; rmt "docker run --rm $(run_args "$t") $img:latest --once" || true + echo ">> simulate --once:"; rmt "docker run --rm $(run_args "$t") $img:latest --simulate --once" || true + fi + sleep 1; echo; echo ">> workstation server received:" + local reqlog; reqlog="$(cat /tmp/fleet_last_reqlog)"; local result=0 + python3 - "$reqlog" "$t" <<'PY' && result=0 || result=$? +import json,sys,os +recs=[json.loads(l) for l in open(sys.argv[1])] if os.path.exists(sys.argv[1]) else [] +t=sys.argv[2] +for r in recs: + j=r.get("json") + print(" POST",r["path"],"auth="+("yes" if r["auth"].startswith("Bearer") else "NO")) + if j: print(" ",{k:j[k] for k in j if k!="db_base64"}) + if r.get("meta"): print(" meta:",r["meta"][:160]) +ok=any(x["path"].endswith("/map") and x["ctype"]=="multipart/form-data" for x in recs) if t=="g1" \ + else any(x["path"].endswith("/telemetry") for x in recs) +print(f"\n RESULT: {'PASS' if ok else 'FAIL'} ({len(recs)} request(s))") +sys.exit(0 if ok else 1) +PY + [ "${KEEP_SERVER:-0}" = 1 ] && { echo ">> --keep-server: server still up (pid $(cat /tmp/fleet_srv_pid 2>/dev/null))"; trap - EXIT; } + return $result +} + +# --------------------------- arg parsing --------------------------- # +CMD=""; ROBOT=""; IP=""; SERVER_IP=""; PORT=8799; TOKEN="test-token"; SN=""; USER_="unitree"; KEEP_SERVER=0 +POSA=() +while [ $# -gt 0 ]; do case "$1" in + --server-ip) SERVER_IP="$2"; shift 2;; + --port) PORT="$2"; shift 2;; + --token) TOKEN="$2"; shift 2;; + --sn) SN="$2"; shift 2;; + --user) USER_="$2"; shift 2;; + --keep-server) KEEP_SERVER=1; shift;; + -h|--help) grep -E '^#( |$)' "$0" | sed 's/^# \{0,1\}//'; exit 0;; + *) POSA+=("$1"); shift;; +esac; done +CMD="${POSA[0]:-}"; ROBOT="${POSA[1]:-}"; IP="${POSA[2]:-}" + +# --------------------------- interactive --------------------------- # +choose_type(){ + echo "Which robot?" >&2; local i=1 + for t in $TYPES; do echo " $i) $t — ${KIND[$t]}" >&2; i=$((i+1)); done + read -rp "Choice [1-3]: " c + case "$c" in 1) echo g1;; 2) echo r1;; 3) echo go2;; g1|r1|go2) echo "$c";; *) echo "";; esac +} + +interactive(){ + echo "=== Sanad Fleet Agent Installer ===" + ROBOT="$(choose_type)"; [ -n "$ROBOT" ] || die "invalid robot type" + read -rp "Robot IP: " IP; [ -n "$IP" ] || die "no IP" + read -rp "SSH user [$USER_]: " u; [ -n "$u" ] && USER_="$u" + rmt true 2>/dev/null || die "cannot SSH to $USER_@$IP (key auth?)" + if installed "$ROBOT"; then + echo; echo ">> '$(img_of "$ROBOT")' is ALREADY INSTALLED on $IP." + echo " 1) show data 2) status 3) logs 4) reinstall 5) UNINSTALL 6) quit" + read -rp "Choice: " a + case "$a" in + 1) do_data "$ROBOT";; 2) do_status "$ROBOT";; 3) do_logs "$ROBOT";; + 4) prompt_install; do_install "$ROBOT";; + 5) read -rp "Really uninstall $(img_of "$ROBOT") from $IP? [y/N] " y; [ "$y" = y ] && do_uninstall "$ROBOT" || echo "cancelled";; + *) echo "bye";; + esac + else + echo; echo ">> not installed yet — let's install." + prompt_install + do_install "$ROBOT" + fi +} + +prompt_install(){ + local def="${ROBOT}_${IP##*.}" + read -rp "Robot name / SN [$def]: " s; SN="${s:-$def}" + local autos; autos="$(detect_server_ip)" + read -rp "Fleet server IP [${autos:-required}]: " si; [ -n "$si" ] && SERVER_IP="$si" || SERVER_IP="${SERVER_IP:-$autos}" + read -rp "Server port [$PORT]: " p; [ -n "$p" ] && PORT="$p" + read -rp "Device token [$TOKEN]: " tk; [ -n "$tk" ] && TOKEN="$tk" +} + +# --------------------------- dispatch --------------------------- # +[ -z "$CMD" ] && { interactive; exit $?; } +[ -n "$ROBOT" ] && [ -n "$IP" ] || die "usage: $0 [opts] (or run with no args for interactive)" +echo "$TYPES" | grep -qw "$ROBOT" || die "robot must be one of: $TYPES" +[ -d "$AGENTS/$ROBOT" ] || die "agent dir missing: $AGENTS/$ROBOT" +[ -z "$SN" ] && SN="${ROBOT}_${IP##*.}" + +case "$CMD" in + install) do_install "$ROBOT";; + uninstall) do_uninstall "$ROBOT";; + status) do_status "$ROBOT";; + data) do_data "$ROBOT";; + logs) do_logs "$ROBOT";; + test) do_test "$ROBOT";; + *) die "unknown command: $CMD (install|uninstall|status|data|logs|test)";; +esac diff --git a/fleet_test_server.py b/fleet_test_server.py new file mode 100755 index 0000000..8ba8885 --- /dev/null +++ b/fleet_test_server.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""fleet_test_server — stand-in for the YS Lootah fleet server, for deploy tests. + +Runs on the workstation ("assume my workstation is a server"). Accepts the two +ingest endpoints the agents POST to, logs each request (JSON line -> REQLOG) and +prints a live summary. GET /ping returns 200 for reachability checks. + + PORT=8799 REQLOG=/tmp/fleet_reqs.jsonl python3 fleet_test_server.py +""" +import json +import os +import sys +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +PORT = int(os.environ.get("PORT", "8799")) +REQLOG = os.environ.get("REQLOG", "/tmp/fleet_reqs.jsonl") +open(REQLOG, "w").close() + + +class H(BaseHTTPRequestHandler): + def _emit(self, rec): + with open(REQLOG, "a") as f: + f.write(json.dumps(rec) + "\n") + + def do_GET(self): + self.send_response(200) + self.end_headers() + self.wfile.write(b'{"ok":true,"server":"fleet_test_server"}') + + def do_POST(self): + n = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(n) + ctype = (self.headers.get("Content-Type", "") or "").split(";")[0] + rec = {"path": self.path, "ctype": ctype, + "auth": self.headers.get("Authorization", ""), "len": n} + summary = "" + if ctype == "application/json": + try: + d = json.loads(body) + if "db_base64" in d: + d["db_base64"] = f"[{len(d['db_base64'])} chars]" + rec["json"] = d + if self.path.endswith("/telemetry"): + summary = (f"battery={d.get('battery')} charging={d.get('charging')} " + f"status={d.get('status')} pos={d.get('position')} " + f"faults={len(d.get('faults',[]))} sn={d.get('sn')} mac={d.get('mac')}") + except Exception as e: + rec["json_err"] = str(e) + else: + txt = body.decode("latin-1") + i = txt.find('name="meta"') + if i != -1: + meta = txt[i:].split("\r\n\r\n", 1)[-1].split("\r\n", 1)[0] + rec["meta"] = meta + try: + m = json.loads(meta) + summary = (f"map={m.get('name')} format={m.get('format')} " + f"size={m.get('size_bytes')}B points={len(m.get('points',[]))} sn={m.get('sn')}") + except Exception: + pass + self._emit(rec) + print(f"[RECV] POST {self.path} {summary}", flush=True) + self.send_response(200) + self.end_headers() + self.wfile.write(b'{"ok":true}') + + def log_message(self, *a): + pass + + +if __name__ == "__main__": + print(f"fleet_test_server listening on 0.0.0.0:{PORT} (log -> {REQLOG})", flush=True) + try: + ThreadingHTTPServer(("0.0.0.0", PORT), H).serve_forever() + except KeyboardInterrupt: + sys.exit(0)