agi_fleet/agent/AGENT_README.md
2026-08-04 15:14:59 +04:00

175 lines
7.2 KiB
Markdown
Raw 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.

# sanad_api_x2 — AGIBOT X2 fleet agent
Reports the **AGIBOT X2**'s live state to the YS Lootah fleet server over the
fleet's five ingest endpoints, with the standard 27-field payload and per-robot
Bearer-token auth. Deploy it with the fleet installer (`../../fleet_install.sh`).
Generated from the canonical `agents/g1/` by `tools/gen_agents.py` — **do not
hand-edit this folder's `sanad_api_x2.py`**. Everything except the state source
is byte-identical logic to the G1/R1/Go2 agents.
---
## The one thing that is different
**The X2 exposes no fixed topic contract**, so this agent hard-codes nothing:
it picks a state-source backend at runtime and reads every topic name and field
path from `.env`.
Retargeting it at the real interface is an **`.env` edit + restart** — no code
change, no image rebuild.
| `X2_SOURCE` | what it does | needs |
|---|---|---|
| `auto` *(default)* | `http` if `X2_STATE_URL` is set → else `ros2` if `rclpy` imports → else `aimrt` if `aimrt_py` imports → else `none` | — |
| `http` | polls `X2_STATE_URL` for one JSON object | nothing (slim image) |
| `ros2` | subscribes `X2_TOPIC_*` | a ROS base image (see below) |
| `aimrt` | **not natively bound** — see below | — |
| `none` | never reads; always heartbeat | — |
Whatever the backend, `AgiBotSource` fills one six-key snapshot — `bms`,
`state_age`, `temps`, `max_vel`, `xy`, `fw` — which is the entire seam between
this robot and the shared telemetry / fault / status / alert pipeline.
### On the `aimrt` backend
AgiBot's open-source X1 stack is built on **AimRT**, and it is the likely
middleware here. It is deliberately **not** implemented natively: the X2's AimRT
channel names and message definitions aren't public, and guessing them yields an
agent that imports cleanly, builds, and posts perfect-looking heartbeats with
`battery: null` forever — a failure you'd only find on the robot.
The supported path today is **AimRT's ROS 2 plugin**: enable it and run with
`X2_SOURCE=ros2` against the bridged topics. If you get the native channel
definitions from AgiBot, implement `_start_aimrt()` in `tools/gen_agents.py`
(not here — this file is generated). The only contract to satisfy is calling
`_ingest_battery` / `_ingest_joints` / `_ingest_odom` with the incoming messages.
---
## Quick start
**1. Discover the robot's interface** (read-only — safe on a live robot):
```bash
./fleet_install.sh probe x2 <ip> --user <ssh-user> # or: tools/probe_x2.sh <ip> <ssh-user>
```
It prints the platform, serial-number candidates, NICs, ROS topics **with their
message types**, listening ports, running containers and map directories — each
labelled with the `.env` variable it feeds.
**2. Install**, using the real serial from the probe:
```bash
./fleet_install.sh install x2 <ip> --sn <serial> \
--token <device-token> --server-url https://eco.yslootahrobotics.com
```
For the `ros2` backend, build against a base matching the **robot's** ROS distro
(a mismatch means ROS 2 discovery silently sees no topics):
```bash
X2_BASE_IMAGE=ros:humble-ros-base ./fleet_install.sh install x2 <ip> --sn ...
```
**3. Map the fields** you found, then restart:
```bash
ssh <user>@<ip> 'nano ~/sanad_api_x2/.env'
ssh <user>@<ip> 'systemctl --user restart sanad-api-x2'
./fleet_install.sh data x2 <ip>
```
**Updating later?** Always pass `--keep-token`, or `--token` defaults to
`test-token` and the feed dies with `401`.
---
## Field mapping
Paths are dotted and work over **both** ROS message objects and plain JSON
dicts, with list indices: `pose.pose.position.x`, `battery.cell_temp[0]`,
`percentage`. A missing link yields `null` — a wrong path degrades one field, it
never crashes the agent.
| var | default (standard ROS msg) | feeds |
|---|---|---|
| `X2_FIELD_SOC` | `percentage` | `battery` |
| `X2_FIELD_VOLTAGE` / `_CURRENT` | `voltage` / `current` | `battery_detail`, `charging` |
| `X2_FIELD_TEMP` / `_SOH` / `_CYCLES` | `temperature` / — / — | `battery_detail` |
| `X2_FIELD_TEMPS` | *(empty)* | `motor_temp`, `MOTOR_OVERTEMP` |
| `X2_FIELD_VEL` | `velocity` | `status: moving` |
| `X2_FIELD_X` / `_Y` | `pose.pose.position.{x,y}` | `position` |
| `X2_FIELD_FSM` / `_FW` | *(empty)* | `control.fsm_id`, `firmware` *(http only)* |
> **`motor_temp` is `null` on the X2, permanently.** `PmuState.msg` is the only
> `aimdk_msgs` definition with any temperature field, and it is the *power unit*
> temperature — not per-motor. `aimdk_msgs/msg/JointState` is
> `name/position/velocity/effort/error_code`. Reporting `pmu_temp` as
> `motor_temp` would be fabricating data, so it stays `null` = "not receiving".
## Position — a second, independent source
The Control Dash exposes no odometry, but ROS does. `X2_POSITION_SOURCE=ros2`
runs **alongside** `X2_SOURCE=http`: battery over HTTP, position over ROS 2.
```
X2_POSITION_SOURCE=ros2
X2_TOPIC_ODOM=/aima/mc/leg_odometry # nav_msgs/msg/Odometry
X2_ROS_QOS=best_effort
```
**`X2_ROS_QOS` must stay `best_effort`.** The X2 publishes that topic as
`BEST_EFFORT`, and a `RELIABLE` subscriber receives **nothing** from a
`BEST_EFFORT` publisher — rclpy creates the subscription, raises no error, and
position silently stays `null`. A `BEST_EFFORT` subscriber reads from either
kind of publisher, which is why it's the default.
This requires `rclpy`, so the systemd unit sources the ROS overlay before
starting the agent:
```ini
ExecStart=/bin/bash -c 'set +u; . /opt/ros/humble/setup.bash >/dev/null 2>&1 || true; \
exec /usr/bin/python3 -u /home/agi/sanad_api_x2/sanad_api_x2.py'
```
### Verify these two on real hardware
Both are unit conventions that look plausible when wrong:
| var | symptom if wrong | fix |
|---|---|---|
| `X2_SOC_SCALE` | battery reads `0` or `100` when it should be `47` | `fraction` (01 source) or `percent` (0100 source) |
| `X2_CURRENT_SIGN` | `charging` is inverted | `-1` |
| `X2_VOLTAGE_SCALE` / `X2_CURRENT_SCALE` | `voltage_v: 48200` instead of `48.2` | `0.001` for mV/mA sources |
---
## What works without any mapping
These need no X2-specific configuration and are live from the first install —
so the robot is useful on the dashboard **before** the state map is known:
- **identity** — `sn`, `name`, `mac`, `brand: agibot`, `type`, `model`
- **software / firmware** — OS, kernel, arch, python, board, L4T (via `/host`)
- **storage** — real host disk usage
- **maps** — `pgm`+`yaml` sets rendered to PNG, RTAB-Map `.db` sent as-is
- **logs** — the agent's own lines, shipped every 60 s with requeue on failure
- **remote** — `ssh <user>@<ip>` registration
- **alerts** — log-pattern scanning
Fields that depend on the **Sanad app** degrade to `null` automatically if the
X2 doesn't run it: `project_logs`, `remote.web`, `control`. No configuration
needed — the schema stays identical, the dashboard just shows fewer cards.
---
## Status
**Unverified on hardware.** The full pipeline is verified end-to-end against
`fleet_test_server.py` — all five endpoints, correct Bearer auth, 27-field
payload, field mapping, unit scaling, fault derivation and map upload. What is
*not* verified is which backend and which field paths the real X2 needs; that is
what `probe_x2.sh` and the `.env` mapping exist to resolve.