160 lines
7.5 KiB
Bash
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
|