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

7.2 KiB
Raw Blame History

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.pydo 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_temp is null on the X2, permanently. PmuState.msg is the only aimdk_msgs definition with any temperature field, and it is the power unit temperature — not per-motor. aimdk_msgs/msg/JointState is name/position/velocity/effort/error_code. Reporting pmu_temp as motor_temp would be fabricating data, so it stays null = "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 (01 source) or percent (0100 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:

  • identitysn, name, mac, brand: agibot, type, model
  • software / firmware — OS, kernel, arch, python, board, L4T (via /host)
  • storage — real host disk usage
  • mapspgm+yaml sets rendered to PNG, RTAB-Map .db sent as-is
  • logs — the agent's own lines, shipped every 60 s with requeue on failure
  • remotessh <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.