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

191 lines
7.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.<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:
```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.