191 lines
7.0 KiB
Markdown
191 lines
7.0 KiB
Markdown
# 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 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.
|