7.0 KiB
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 0–100 (→
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 —
PmuStatehas voltage/current only, and the real BMS arrives protobuf-wrapped insideros2_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
aimdkoverlay must be sourced. - The same
X2_ROS_QOS=best_effortrule 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.