# 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. ```json { "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.[]` 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: ```bash 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: ```bash 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.