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

7.0 KiB
Raw Permalink Blame History

AGIBOT X2 — what the robot actually exposes

Everything here was read off the live robot (10.255.254.84) with tools/probe_x2.sh and ros2 introspection. It is the reference for why the .env mapping looks the way it does, so nobody has to rediscover it.


1. Platform

Board NVIDIA Jetson Orin NX Engineering Reference Developer Kit Super V2Board
Arch / OS aarch64 · Ubuntu 22.04.5 LTS · kernel 5.15.148-tegra
L4T R36.4.3
Python 3.10.12 (/usr/bin/python3)
ROS Humble, ROS_DOMAIN_ID=0
Docker 27.5.1 installed, daemon inactive + masked (no passwordless sudo)
systemd user bus available · linger=no (polkit denies enabling it unprivileged)

Network interfaces

iface address note
wifi0 10.255.254.84 fleet-reachable → MAC_INTERFACE=wifi0
develop0 10.0.1.41
sensor0 10.11.1.1
ssh0 10.0.200.41

Serial number

X230028C5Z0058 — the AgiBot device serial, found in /home/agi/agibot_report.py.

Prefer it over /proc/device-tree/serial-number (1421126035442) and over /etc/machine-id, which are board identifiers rather than the robot's identity.


2. The Control Dash — the practical state source

A dashboard listens on :8770 and serves a single JSON object at /api/state. This is the cleanest source on the robot: battery is already percent / volts / amps, no protobuf decoding, no ROS dependency.

{
  "mode": "STAND_DEFAULT", "mode_desc": "Stable stand", "mode_status": "Running",
  "battery_pct": 68.0, "battery_voltage": 51.996, "battery_current": 3.544,
  "battery_temp": 41.6, "battery_cycles": 14, "charging": true,
  "pmu_temp": 40.17, "fan_rpm": 6960.0, "fan_pct": 68,
  "rails": { "bus_48v": {...}, "output_48v": {...}, "output_12v": {...},
             "head_power": {...}, "orin": {...}, "rk3588": {...} },
  "imu":  { "chest": { "roll":, "pitch":, "yaw":, "accel_x":, "gyro_x": },
            "torso": {  } },
  "joints": { "head": [2], "waist": [3], "arm": [14], "leg": [12] },
  "hand_type": "None", "hand_state": { "left": [], "right": [] },
  "connection": { "online": true, "transport": "agent", "host": "127.0.0.1:8781",
                  "ros_domain_id": 0, "uptime_s":  }
}

Each joints.<group>[] element is {name, position, velocity, effort, error}an array of objects, not numbers. That is why the velocity mapping needs the wildcard form joints.leg[*].velocity; a plain path would resolve to a list of dicts and silently yield nothing.

Units confirmed on hardware: battery_pct is already 0100 (→ X2_SOC_SCALE=percent), voltage is volts and current is amps (→ both scales 1), and a positive battery_current accompanies charging: true (→ X2_CURRENT_SIGN=1).

Other listening ports: 8781 (agent transport, no HTTP), 50080, 21274/21275, 39101, 11511 — none served a usable state document.


3. ROS 2 topics

The X2 runs AgiBot's aima stack. ros2 topic list shows ~70 topics; the telemetry-relevant ones:

topic type use
/aima/mc/leg_odometry nav_msgs/msg/Odometry position — in use
/slam/localization/odometry nav_msgs/msg/Odometry not publishing
/pnc/estimate_odom nav_msgs/msg/Odometry not publishing
/aima/hal/pmu/state aimdk_msgs/msg/PmuState voltages + currents, no SOC
/aima/battery_state/pb_3Aaimdk_2Eprotocol_2EBmsState ros2_plugin_proto/msg/RosMsgWrapper protobuf-wrapped BMS — opaque
/aima/hal/joint/{leg,arm,head,waist}/state aimdk_msgs/msg/JointStateArray joint state
/aima/hal/imu/{chest,torso}/state sensor_msgs/msg/Imu
/aima/sm/system_state aimdk_msgs/msg/SmSystemState

⚠ QoS — the trap that costs an afternoon

/aima/mc/leg_odometry publishes with:

Reliability: BEST_EFFORT
Durability:  TRANSIENT_LOCAL

rclpy defaults subscribers to RELIABLE, which is incompatible with a BEST_EFFORT publisher. The failure mode is silent: the subscription is created, no exception is raised, no warning is logged — messages simply never arrive and the field stays null forever. Meanwhile ros2 topic echo works fine, so the topic looks healthy.

A BEST_EFFORT subscriber can read from either publisher type, so the agent defaults to it (X2_ROS_QOS=best_effort). Only set reliable if a specific topic demands it.

aimdk_msgs is not in base ROS

It lives in an overlay. To introspect these types:

source /opt/ros/humble/setup.bash
source /home/agi/aimdk/install/local_setup.bash   # ← required for aimdk_msgs
ros2 interface show aimdk_msgs/msg/PmuState

Note ros2 interface show fails for a few types (JointNoRealTimeStateArray, ros2_plugin_proto/msg/RosMsgWrapper) — their .idl isn't installed. Read the .msg files under /home/agi/aimdk/install/aimdk_msgs/share/aimdk_msgs/msg/ instead.

Sourcing setup.bash under set -u aborts — it reads unbound variables (AMENT_TRACE_SETUP_FILES, COLCON_TRACE). Always set +u first. This is why the systemd unit and the probe script both do so.


4. Why motor_temp is permanently null

Searching every message definition in the overlay:

grep -rl -i 'temp' /home/agi/aimdk/install/aimdk_msgs/share/aimdk_msgs/msg/*.msg
# -> PmuState.msg   (only this one)

PmuState carries battery_voltage, six rail currents, and an over-temperature status bit — it is the power unit, not the motors. The per-joint message is:

aimdk_msgs/msg/JointState:
  string name
  float64 position
  float64 velocity
  float64 effort
  uint16 error_code

No temperature field anywhere. The X2 does not publish per-motor temperatures, so motor_temp reports null. Putting pmu_temp in that field would be fabricating data.


5. Alternative: full ROS 2 backend

If the Control Dash is ever unavailable, the agent can read state over ROS 2 instead (X2_SOURCE=ros2). Caveats:

  • Battery SOC is not available over ROS in plain form — PmuState has voltage/current only, and the real BMS arrives protobuf-wrapped inside ros2_plugin_proto/msg/RosMsgWrapper, which needs AgiBot's protobuf definitions to decode.
  • Custom types must be importable inside whatever runs the agent, i.e. the aimdk overlay must be sourced.
  • The same X2_ROS_QOS=best_effort rule applies to every subscription.

For those reasons http remains the recommended state source, with ROS 2 used only for position.


6. AimRT

aimrt_py is installed (/usr/local/lib/python3.10/dist-packages/aimrt_py/), and AgiBot's open-source X1 stack is built on AimRT — so it is very likely the underlying middleware here.

The agent deliberately does not bind AimRT channels natively: the X2's channel names and message definitions are not public, and guessing them yields an agent that imports cleanly, builds, and heartbeats battery: null forever — a failure only visible on the robot. AimRT ships a ROS 2 plugin, and the bridged topics above are what the agent uses instead.