#!/usr/bin/env bash # probe_x2.sh — read-only discovery for an AGIBOT X2 (or any unknown robot). # # tools/probe_x2.sh # ./fleet_install.sh probe x2 --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 " >&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:/ | 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 --sn \ --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 ... 3. Put the discovered names in the robot's .env and restart: ssh @ 'nano ~/sanad_api_x2/.env' # X2_TOPIC_* / X2_FIELD_* ssh @ 'systemctl --user restart sanad-api-x2' 4. Confirm what it is actually sending: ./fleet_install.sh data x2 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