7.2 KiB
sanad_api_x2 — AGIBOT X2 fleet agent
Reports the AGIBOT X2's live state to the YS Lootah fleet server over the
fleet's five ingest endpoints, with the standard 27-field payload and per-robot
Bearer-token auth. Deploy it with the fleet installer (../../fleet_install.sh).
Generated from the canonical agents/g1/ by tools/gen_agents.py — do not
hand-edit this folder's sanad_api_x2.py. Everything except the state source
is byte-identical logic to the G1/R1/Go2 agents.
The one thing that is different
The X2 exposes no fixed topic contract, so this agent hard-codes nothing:
it picks a state-source backend at runtime and reads every topic name and field
path from .env.
Retargeting it at the real interface is an .env edit + restart — no code
change, no image rebuild.
X2_SOURCE |
what it does | needs |
|---|---|---|
auto (default) |
http if X2_STATE_URL is set → else ros2 if rclpy imports → else aimrt if aimrt_py imports → else none |
— |
http |
polls X2_STATE_URL for one JSON object |
nothing (slim image) |
ros2 |
subscribes X2_TOPIC_* |
a ROS base image (see below) |
aimrt |
not natively bound — see below | — |
none |
never reads; always heartbeat | — |
Whatever the backend, AgiBotSource fills one six-key snapshot — bms,
state_age, temps, max_vel, xy, fw — which is the entire seam between
this robot and the shared telemetry / fault / status / alert pipeline.
On the aimrt backend
AgiBot's open-source X1 stack is built on AimRT, and it is the likely
middleware here. It is deliberately not implemented natively: the X2's AimRT
channel names and message definitions aren't public, and guessing them yields an
agent that imports cleanly, builds, and posts perfect-looking heartbeats with
battery: null forever — a failure you'd only find on the robot.
The supported path today is AimRT's ROS 2 plugin: enable it and run with
X2_SOURCE=ros2 against the bridged topics. If you get the native channel
definitions from AgiBot, implement _start_aimrt() in tools/gen_agents.py
(not here — this file is generated). The only contract to satisfy is calling
_ingest_battery / _ingest_joints / _ingest_odom with the incoming messages.
Quick start
1. Discover the robot's interface (read-only — safe on a live robot):
./fleet_install.sh probe x2 <ip> --user <ssh-user> # or: tools/probe_x2.sh <ip> <ssh-user>
It prints the platform, serial-number candidates, NICs, ROS topics with their
message types, listening ports, running containers and map directories — each
labelled with the .env variable it feeds.
2. Install, using the real serial from the probe:
./fleet_install.sh install x2 <ip> --sn <serial> \
--token <device-token> --server-url https://eco.yslootahrobotics.com
For the ros2 backend, build against a base matching the robot's ROS distro
(a mismatch means ROS 2 discovery silently sees no topics):
X2_BASE_IMAGE=ros:humble-ros-base ./fleet_install.sh install x2 <ip> --sn ...
3. Map the fields you found, then restart:
ssh <user>@<ip> 'nano ~/sanad_api_x2/.env'
ssh <user>@<ip> 'systemctl --user restart sanad-api-x2'
./fleet_install.sh data x2 <ip>
Updating later? Always pass --keep-token, or --token defaults to
test-token and the feed dies with 401.
Field mapping
Paths are dotted and work over both ROS message objects and plain JSON
dicts, with list indices: pose.pose.position.x, battery.cell_temp[0],
percentage. A missing link yields null — a wrong path degrades one field, it
never crashes the agent.
| var | default (standard ROS msg) | feeds |
|---|---|---|
X2_FIELD_SOC |
percentage |
battery |
X2_FIELD_VOLTAGE / _CURRENT |
voltage / current |
battery_detail, charging |
X2_FIELD_TEMP / _SOH / _CYCLES |
temperature / — / — |
battery_detail |
X2_FIELD_TEMPS |
(empty) | motor_temp, MOTOR_OVERTEMP |
X2_FIELD_VEL |
velocity |
status: moving |
X2_FIELD_X / _Y |
pose.pose.position.{x,y} |
position |
X2_FIELD_FSM / _FW |
(empty) | control.fsm_id, firmware (http only) |
motor_tempisnullon the X2, permanently.PmuState.msgis the onlyaimdk_msgsdefinition with any temperature field, and it is the power unit temperature — not per-motor.aimdk_msgs/msg/JointStateisname/position/velocity/effort/error_code. Reportingpmu_tempasmotor_tempwould be fabricating data, so it staysnull= "not receiving".
Position — a second, independent source
The Control Dash exposes no odometry, but ROS does. X2_POSITION_SOURCE=ros2
runs alongside X2_SOURCE=http: battery over HTTP, position over ROS 2.
X2_POSITION_SOURCE=ros2
X2_TOPIC_ODOM=/aima/mc/leg_odometry # nav_msgs/msg/Odometry
X2_ROS_QOS=best_effort
⚠ X2_ROS_QOS must stay best_effort. The X2 publishes that topic as
BEST_EFFORT, and a RELIABLE subscriber receives nothing from a
BEST_EFFORT publisher — rclpy creates the subscription, raises no error, and
position silently stays null. A BEST_EFFORT subscriber reads from either
kind of publisher, which is why it's the default.
This requires rclpy, so the systemd unit sources the ROS overlay before
starting the agent:
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'
Verify these two on real hardware
Both are unit conventions that look plausible when wrong:
| var | symptom if wrong | fix |
|---|---|---|
X2_SOC_SCALE |
battery reads 0 or 100 when it should be 47 |
fraction (0–1 source) or percent (0–100 source) |
X2_CURRENT_SIGN |
charging is inverted |
-1 |
X2_VOLTAGE_SCALE / X2_CURRENT_SCALE |
voltage_v: 48200 instead of 48.2 |
0.001 for mV/mA sources |
What works without any mapping
These need no X2-specific configuration and are live from the first install — so the robot is useful on the dashboard before the state map is known:
- identity —
sn,name,mac,brand: agibot,type,model - software / firmware — OS, kernel, arch, python, board, L4T (via
/host) - storage — real host disk usage
- maps —
pgm+yamlsets rendered to PNG, RTAB-Map.dbsent as-is - logs — the agent's own lines, shipped every 60 s with requeue on failure
- remote —
ssh <user>@<ip>registration - alerts — log-pattern scanning
Fields that depend on the Sanad app degrade to null automatically if the
X2 doesn't run it: project_logs, remote.web, control. No configuration
needed — the schema stays identical, the dashboard just shows fewer cards.
Status
⚠ Unverified on hardware. The full pipeline is verified end-to-end against
fleet_test_server.py — all five endpoints, correct Bearer auth, 27-field
payload, field mapping, unit scaling, fault derivation and map upload. What is
not verified is which backend and which field paths the real X2 needs; that is
what probe_x2.sh and the .env mapping exist to resolve.