agi_fleet/tools/probe_x2.sh
2026-08-04 15:14:59 +04:00

160 lines
7.5 KiB
Bash

#!/usr/bin/env bash
# probe_x2.sh — read-only discovery for an AGIBOT X2 (or any unknown robot).
#
# tools/probe_x2.sh <ip> <ssh-user>
# ./fleet_install.sh probe x2 <ip> --user <ssh-user> # same, via the installer
#
# WHY THIS EXISTS
# The X2 exposes no fixed topic contract, so sanad_api_x2.py reads every topic
# name and field path from .env instead of hard-coding them. This script finds
# the real values to put there.
#
# It ONLY READS: no install, no write, no package manager, no motion. Safe to run
# on a live robot. Everything it prints maps to a variable in agents/x2/.env.example.
set -uo pipefail
# No default login user is assumed for the X2 — its login account is a property
# of the robot, not something to guess. Pass it explicitly.
IP="${1:-}"; USER_="${2:-}"
[ -n "$IP" ] && [ -n "$USER_" ] \
|| { echo "usage: $0 <ip> <ssh-user>" >&2
echo " the X2's login user is not assumed — pass the account you SSH in as" >&2
exit 1; }
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new)
[ -n "${FLEET_SSH_KEY:-}" ] && SSH_OPTS+=(-i "$FLEET_SSH_KEY")
ssh -n "${SSH_OPTS[@]}" "$USER_@$IP" true 2>/dev/null \
|| { echo "ERROR: cannot SSH to $USER_@$IP (key auth?)" >&2; exit 1; }
echo "=============================================================="
echo " AGIBOT X2 probe — $USER_@$IP"
echo "=============================================================="
# NOTE: no -n here. The probe body is piped in on stdin, and -n would redirect
# stdin from /dev/null, so `bash -s` would read an empty script and print nothing.
ssh "${SSH_OPTS[@]}" "$USER_@$IP" 'bash -s' <<'REMOTE'
set -uo pipefail
sec(){ echo; echo "── $* ──────────────────────────────────────────"; }
have(){ command -v "$1" >/dev/null 2>&1; }
sec "1. PLATFORM -> software.* / firmware.* in telemetry"
. /etc/os-release 2>/dev/null && echo "os: ${PRETTY_NAME:-?} (VERSION_ID=${VERSION_ID:-?})"
echo "kernel: $(uname -r)"
echo "arch: $(uname -m) # arm64 => build natively on the robot"
for f in /sys/firmware/devicetree/base/model /proc/device-tree/model; do
[ -r "$f" ] && { echo "board: $(tr -d '\0' < "$f")"; break; }
done
[ -r /etc/nv_tegra_release ] && echo "l4t: $(head -1 /etc/nv_tegra_release)"
echo "python3: $(python3 -V 2>&1)"
sec "2. DEPLOY PREREQS -> can fleet_install.sh work here?"
if have docker; then
echo "docker: $(docker --version 2>&1)"
docker ps >/dev/null 2>&1 \
&& echo " OK '$USER' can talk to dockerd (no sudo needed)" \
|| echo " !! '$USER' CANNOT use docker — add to the docker group"
else
echo "docker: !! NOT INSTALLED — required"
fi
systemctl --user show-environment >/dev/null 2>&1 \
&& echo "systemd: OK user bus available (user-level service, no sudo)" \
|| echo "systemd: !! no user bus — auto-start needs another mechanism"
echo "linger: $(loginctl show-user "$USER" 2>/dev/null | grep -i linger || echo '?')"
sec "3. NETWORK -> MAC_INTERFACE, REMOTE_HOST"
ip -o -4 addr show 2>/dev/null | awk '{printf " %-10s %s\n", $2, $4}'
echo " MACs:"
for n in /sys/class/net/*; do
i="$(basename "$n")"; [ "$i" = lo ] && continue
echo " $(printf '%-10s' "$i") $(cat "$n/address" 2>/dev/null)"
done
sec "4. SERIAL NUMBER -> SN (keys the robot on the fleet server)"
for f in /sys/class/dmi/id/product_serial /proc/device-tree/serial-number \
/etc/machine-id /etc/robot_sn /etc/agibot/sn; do
[ -r "$f" ] && echo " $f = $(tr -d '\0' < "$f" 2>/dev/null | head -1)"
done
echo " (prefer the serial printed on the robot / in AgiBot's tooling over machine-id)"
sec "5. ROS 2 -> X2_SOURCE=ros2, X2_TOPIC_*, ROS_DOMAIN_ID"
if [ -d /opt/ros ]; then
echo "distros: $(ls /opt/ros 2>/dev/null | tr '\n' ' ')"
D="${ROS_DISTRO:-$(ls /opt/ros 2>/dev/null | head -1)}"
echo "using: $D ROS_DOMAIN_ID=${ROS_DOMAIN_ID:-0}"
if [ -f "/opt/ros/$D/setup.bash" ]; then
# ROS setup.bash reads unbound vars (AMENT_TRACE_SETUP_FILES, COLCON_TRACE,
# …). Under `set -u` that is FATAL and kills this whole probe silently, so
# relax nounset across the source and restore it after.
set +u
# shellcheck disable=SC1090
. "/opt/ros/$D/setup.bash" 2>/dev/null || true
echo "--- ros2 topic list (5s) ---"
timeout 5 ros2 topic list 2>/dev/null | sed 's/^/ /' || echo " (none / timed out)"
echo "--- types for likely telemetry topics ---"
for t in $(timeout 5 ros2 topic list 2>/dev/null | grep -Ei 'batt|power|bms|joint|motor|odom|imu|state|fsm|mode'); do
printf " %-42s %s\n" "$t" "$(timeout 3 ros2 topic info "$t" 2>/dev/null | awk -F': ' '/Type/{print $2; exit}')"
done
set -u
fi
else
echo " no /opt/ros — the ros2 backend is unavailable unless AgiBot ships one"
fi
sec "6. AIMRT / VENDOR SDK -> X2_SOURCE=aimrt (or the ros2 bridge)"
python3 -c 'import aimrt_py, sys; print(" aimrt_py importable:", aimrt_py.__file__)' 2>/dev/null \
|| echo " aimrt_py not importable for this python3"
ls -d /opt/aimrt /opt/agibot /usr/local/aimrt ~/aimrt* ~/agibot* 2>/dev/null | sed 's/^/ path: /'
python3 -c 'import agibot, sys; print(" agibot module:", agibot.__file__)' 2>/dev/null || true
sec "7. LISTENING PORTS -> X2_STATE_URL (http backend), REMOTE_PORTS"
(ss -lntp 2>/dev/null || netstat -lntp 2>/dev/null) | sed 's/^/ /' | head -30
echo " probe a candidate: curl -s http://127.0.0.1:<port>/ | head -c 400"
sec "8. RUNNING WORKLOAD -> PROJECT_LOG_CONTAINER, what publishes state"
have docker && docker ps --format ' {{.Names}} {{.Status}} ({{.Image}})' 2>/dev/null
echo " top processes:"
ps -eo comm,pcpu --sort=-pcpu 2>/dev/null | head -12 | sed 's/^/ /'
sec "9. MAPS -> MAPS_DIR / EXTRA_MAP_DIRS"
find "$HOME" -maxdepth 5 \( -name '*.pgm' -o -name 'map*.yaml' -o -name '*.db' -o -name '*.posegraph' \) \
2>/dev/null | head -20 | sed 's/^/ /'
echo " (the agent wants the DIRECTORY holding a matching .pgm + .yaml pair, or a .db)"
sec "10. DISK -> storage.* in telemetry"
df -h / 2>/dev/null | sed 's/^/ /'
REMOTE
cat <<'NEXT'
==============================================================
NEXT STEPS
==============================================================
1. Pick a backend from what appeared above:
section 5 found battery/joint/odom topics -> X2_SOURCE=ros2
section 7 found a vendor JSON API -> X2_SOURCE=http
neither -> X2_SOURCE=none
(identity + maps + logs
still work; battery null)
2. Install with the real serial from section 4:
./fleet_install.sh install x2 <ip> --sn <serial> \
--token <device-token> --server-url https://eco.yslootahrobotics.com
ros2 backend? build against a ROS base matching the robot's distro:
X2_BASE_IMAGE=ros:humble-ros-base ./fleet_install.sh install x2 <ip> ...
3. Put the discovered names in the robot's .env and restart:
ssh <user>@<ip> 'nano ~/sanad_api_x2/.env' # X2_TOPIC_* / X2_FIELD_*
ssh <user>@<ip> 'systemctl --user restart sanad-api-x2'
4. Confirm what it is actually sending:
./fleet_install.sh data x2 <ip>
5. VERIFY THE TWO UNIT CONVENTIONS on real hardware:
- battery % looks right -> else set X2_SOC_SCALE=fraction|percent
- "charging" true while charging -> else set X2_CURRENT_SIGN=-1
NEXT