agi_fleet/README.md
2026-08-04 15:14:59 +04:00

8.1 KiB

AGI Fleet — AGIBOT X2 agent

On-robot agent that reports the AGIBOT X2's live state to the YS Lootah fleet server. Self-contained: it shares no code with the Unitree (G1 / R1 / Go2) agents and is deployed and managed independently.

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

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.
  • mapno_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

# 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

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.

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

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:

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:

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 .gitignored 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.