230 lines
8.0 KiB
Markdown
230 lines
8.0 KiB
Markdown
# AGI Fleet — AGIBOT X2 agent
|
|
|
|
On-robot agent that reports the **AGIBOT X2**'s live state to the **YS Lootah
|
|
fleet server_ Eco system**.
|
|
|
|
> **Status: LIVE on production.** Running on `10.255.254.84`, posting to
|
|
> `https://eco.yslootahrobotics.com` every 2 s.
|
|
|
|
```
|
|
telemetry ok: battery=68 charging=True status=charging pos={'x': 1.222, 'y': 0.437} -> HTTP 200
|
|
```
|
|
|
|
---
|
|
|
|
## 1. Layout
|
|
|
|
```
|
|
agi_fleet/
|
|
├── README.md ← this file
|
|
├── agent/
|
|
│ ├── sanad_api_x2.py ← THE AGENT (deployed as-is — do not edit casually)
|
|
│ ├── .env.example ← every setting, documented
|
|
│ ├── requirements.txt ← requests (+ websocket-client, optional)
|
|
│ ├── AGENT_README.md ← agent internals: backends, field mapping
|
|
│ ├── Dockerfile ← optional container path (NOT used — see §7)
|
|
│ ├── entrypoint.sh ← container entrypoint (unused)
|
|
│ └── .dockerignore
|
|
├── tools/
|
|
│ └── probe_x2.sh ← read-only robot discovery
|
|
└── docs/
|
|
└── X2_INTERFACE.md ← what the X2 actually exposes (topics, QoS, msgs)
|
|
```
|
|
|
|
The agent filename stays `sanad_api_x2.py` because that is exactly what is
|
|
deployed and running on the robot (`~/sanad_api_x2/`, unit `sanad-api-x2`).
|
|
Renaming it would mean redeploying a working production feed for cosmetic
|
|
reasons — not worth it.
|
|
|
|
---
|
|
|
|
## 2. The robot
|
|
|
|
| | |
|
|
|---|---|
|
|
| Host | `10.255.254.84` (NIC `wifi0`) |
|
|
| SSH | `agi@10.255.254.84`, key `~/.ssh/agibot_x2_ed25519` (alias `agix2`) |
|
|
| Serial (`SN`) | `X230028C5Z0058` |
|
|
| Display name | `x2_84` |
|
|
| Hardware | NVIDIA Jetson Orin NX, arm64, L4T R36.4.3 |
|
|
| OS | Ubuntu 22.04.5 LTS, kernel 5.15.148-tegra |
|
|
| ROS | Humble (`/opt/ros/humble`), `ROS_DOMAIN_ID=0` |
|
|
| Install dir | `~/sanad_api_x2/` |
|
|
| Service | `systemctl --user sanad-api-x2` |
|
|
|
|
---
|
|
|
|
## 3. How it gets its data
|
|
|
|
Two independent sources, running at the same time:
|
|
|
|
| what | source |
|
|
|---|---|
|
|
| battery, charging, voltage, current, temp, cycles | **HTTP** — the robot's *AGIBOT X2 Control Dash* at `http://127.0.0.1:8770/api/state` |
|
|
| position `{x, y}` | **ROS 2** — `/aima/mc/leg_odometry` (`nav_msgs/msg/Odometry`) |
|
|
| locomotion → `status: moving` | `joints.leg[*].velocity` from the same dash payload |
|
|
| OS, kernel, arch, board, L4T, storage, MAC | read directly from the host |
|
|
|
|
That split matters: the Control Dash carries no odometry, and ROS carries no
|
|
tidy battery percentage — so `X2_SOURCE=http` and `X2_POSITION_SOURCE=ros2` run
|
|
side by side. Neither depends on the other.
|
|
|
|
### The field mapping actually in use
|
|
|
|
```ini
|
|
X2_SOURCE=http
|
|
X2_STATE_URL=http://127.0.0.1:8770/api/state
|
|
X2_FIELD_SOC=battery_pct X2_FIELD_VOLTAGE=battery_voltage
|
|
X2_FIELD_CURRENT=battery_current X2_FIELD_TEMP=battery_temp
|
|
X2_FIELD_CYCLES=battery_cycles X2_FIELD_VEL=joints.leg[*].velocity
|
|
X2_SOC_SCALE=percent X2_CURRENT_SIGN=1
|
|
|
|
X2_POSITION_SOURCE=ros2
|
|
X2_TOPIC_ODOM=/aima/mc/leg_odometry
|
|
X2_ROS_QOS=best_effort # ← must stay best_effort, see docs/X2_INTERFACE.md
|
|
|
|
MAC_INTERFACE=wifi0
|
|
REMOTE_PORTS=8770,8001,8000,8080
|
|
```
|
|
|
|
Nothing above is hard-coded in the agent — it is all `.env`. Retargeting is an
|
|
`.env` edit + restart, never a code change.
|
|
|
|
---
|
|
|
|
## 4. What it sends
|
|
|
|
One JSON object every 2 s to `POST /api/v1/fleet/ingest/telemetry` with
|
|
`Authorization: Bearer <DEVICE_TOKEN>`, plus `/{sn}/alert`, `/{sn}/logs` and
|
|
`/{sn}/remote`. Verified field-by-field against the robot's own readings:
|
|
|
|
| robot ground truth | agent sends |
|
|
|---|---|
|
|
| dash `battery_pct: 68.0` | `battery: 68` |
|
|
| dash `battery_voltage: 51.996` | `voltage_v: 52.0` |
|
|
| dash `battery_current: 3.544` | `current_a: 3.54` |
|
|
| dash `battery_temp: 41.6` · `cycles: 14` | `temp_c: 42` · `cycles: 14` |
|
|
| dash `charging: true` | `charging: true`, `status: "charging"` |
|
|
| ROS odom `x: 1.2202 y: 0.4363` | `position: {x: 1.222, y: 0.437}` |
|
|
|
|
Also registers the Control Dash (`http://10.255.254.84:8770`) and
|
|
`ssh agi@10.255.254.84` for the fleet UI.
|
|
|
|
### Fields that are always `null` — and why
|
|
|
|
- **`motor_temp`** — the X2 publishes **no per-motor temperatures at all**.
|
|
`PmuState.msg` is the only `aimdk_msgs` definition containing any temperature
|
|
field and it is the *power unit*, not the motors. Reporting `pmu_temp` there
|
|
would be fabricating data.
|
|
- **`map`** — `no_map`; there are no saved maps on this robot.
|
|
- **`project_logs` / `control`** — these are Sanad-app integrations; the X2 does
|
|
not run that app, so they degrade to `null` automatically.
|
|
|
|
`null` means "not available", never "zero".
|
|
|
|
---
|
|
|
|
## 5. Operating it
|
|
|
|
```bash
|
|
# live log
|
|
ssh agix2 'journalctl --user -u sanad-api-x2 -f'
|
|
|
|
# what it is sending right now
|
|
ssh agix2 "journalctl --user -u sanad-api-x2 -n 20 --no-pager | grep -oE 'telemetry ok:.*'"
|
|
|
|
# service control
|
|
ssh agix2 'systemctl --user restart sanad-api-x2'
|
|
ssh agix2 'systemctl --user status sanad-api-x2'
|
|
|
|
# change a setting (then restart)
|
|
ssh agix2 'nano ~/sanad_api_x2/.env && systemctl --user restart sanad-api-x2'
|
|
```
|
|
|
|
### Updating the agent code
|
|
|
|
```bash
|
|
scp -i ~/.ssh/agibot_x2_ed25519 agent/sanad_api_x2.py \
|
|
agi@10.255.254.84:~/sanad_api_x2/sanad_api_x2.py
|
|
ssh agix2 'systemctl --user restart sanad-api-x2'
|
|
```
|
|
|
|
`.env` is never overwritten by this, so the device token stays put.
|
|
|
|
### Rotating the device token
|
|
|
|
The fleet server issues a **Universal Connector** token — regenerating it on the
|
|
server invalidates the previous one for *every* robot that uses it.
|
|
|
|
```bash
|
|
ssh agix2 "sed -i 's#^DEVICE_TOKEN=.*#DEVICE_TOKEN=<new-token>#' ~/sanad_api_x2/.env \
|
|
&& chmod 600 ~/sanad_api_x2/.env && systemctl --user restart sanad-api-x2"
|
|
```
|
|
|
|
### Re-discovering the robot's interface
|
|
|
|
```bash
|
|
FLEET_SSH_KEY=~/.ssh/agibot_x2_ed25519 bash tools/probe_x2.sh 10.255.254.84 agi
|
|
```
|
|
|
|
Read-only — no install, no writes, safe on a live robot. Every line it prints is
|
|
labelled with the `.env` variable it feeds.
|
|
|
|
---
|
|
|
|
## 6. ⚠ Open item — reboot survival
|
|
|
|
`linger` is **off**, so the agent will **not** start again after a power cycle.
|
|
It does survive crashes (systemd `Restart=always`, verified by `kill -9`).
|
|
|
|
polkit denies `loginctl enable-linger` for a non-root user on this image, so it
|
|
needs one privileged command **on the robot**:
|
|
|
|
```bash
|
|
ssh agix2 'sudo loginctl enable-linger agi' # prompts for the password
|
|
ssh agix2 'loginctl show-user agi | grep Linger' # expect Linger=yes
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Why it runs natively, not in Docker
|
|
|
|
Docker is installed on the X2 but the daemon is **`inactive` and `masked`**, and
|
|
`agi` has no passwordless sudo — so the container path cannot start. The agent's
|
|
only hard dependency is `requests`, so it runs directly under a user-level
|
|
systemd unit instead. Same auto-start model, same `.env`, no root.
|
|
|
|
The unit sources the ROS overlay first so `rclpy` is importable for position:
|
|
|
|
```ini
|
|
ExecStart=/bin/bash -c 'set +u; . /opt/ros/humble/setup.bash >/dev/null 2>&1 || true; \
|
|
exec /usr/bin/python3 -u /home/agi/sanad_api_x2/sanad_api_x2.py'
|
|
```
|
|
|
|
`set +u` is required — ROS's `setup.bash` reads unbound variables and would abort
|
|
the unit under `set -u`.
|
|
|
|
`agent/Dockerfile` and `agent/entrypoint.sh` are kept for the day dockerd is
|
|
unmasked; nothing currently uses them.
|
|
|
|
---
|
|
|
|
## 8. Security
|
|
|
|
- **Outbound only.** No inbound port is opened on the robot; every call is an
|
|
HTTPS POST carrying `Authorization: Bearer <token>`, keyed by `sn`.
|
|
- **Read-only toward the robot.** It reads an HTTP status page and subscribes to
|
|
one ROS topic. It never commands motion.
|
|
- **Token handling.** Lives only in `~/sanad_api_x2/.env` on the robot, mode
|
|
`600`, and is `.gitignore`d here. Only `.env.example` (placeholders) is in this
|
|
repo.
|
|
- **TLS verified** (`VERIFY_TLS=1`) against the production server.
|
|
|
|
---
|
|
|
|
## 9. Relationship to the Unitree fleet
|
|
|
|
None. This project shares no files with `fleet/` (the G1 / R1 / Go2 agents).
|
|
`sanad_api_x2.py` is standalone — it is not generated by `fleet/tools/gen_agents.py`
|
|
and contains zero Unitree/DDS code. The two can be changed independently.
|