# sanad_api_x2 — AGIBOT X2. Copy to .env and fill in. # ONE agent = telemetry + map + logs + alerts + remote (see fleet/README.md). # # READ THIS FIRST — the X2 exposes no fixed topic contract, so EVERY topic name # and field path used to read robot state is an env var in this file. Nothing is # hard-coded in the agent. Discover the real ones with: # # ./fleet_install.sh probe x2 (or: tools/probe_x2.sh @) # # then set them below and restart — no code change, no image rebuild. # ── fleet server (REQUIRED — YS Lootah gives you these two) ────────────────── SERVER_URL=https://eco.yslootahrobotics.com DEVICE_TOKEN=REPLACE_WITH_DEVICE_TOKEN # ── identity ───────────────────────────────────────────────────────────────── # The robot's REAL serial — keys the robot on the server ({sn} routes). SN=REPLACE_WITH_X2_SERIAL # Friendly display name shown on the dashboard (default _). ROBOT_NAME=x2_10 ROBOT_BRAND=agibot ROBOT_TYPE=humanoid ROBOT_MODEL=x2 # Maps subdir (//…) + X-Robot-Name header. ROBOT=sanad # Optional: app data dir whose size is reported in storage (as /host/ # when the installer's read-only /:/host mount is used). Empty = omit. STORAGE_DATA_PATH= # ═════════════════════════════════════════════════════════════════════════════ # X2 STATE SOURCE — the one robot-specific section # ═════════════════════════════════════════════════════════════════════════════ # auto = http if X2_STATE_URL is set, else ros2 if rclpy imports, else aimrt if # aimrt_py imports, else none (heartbeat: battery null, status offline). # http = poll X2_STATE_URL for one JSON object; map fields with X2_FIELD_* . # ros2 = subscribe X2_TOPIC_* (needs a ROS base image — see Dockerfile). # aimrt = AgiBot's runtime. NOT natively bound (channel/message defs are not # public). Enable AimRT's ROS 2 plugin and use X2_SOURCE=ros2 instead. # none = never read; always heartbeat. Useful to bring a robot online on the # dashboard (identity, storage, maps, logs) before the state map is known. X2_SOURCE=auto # ── backend: http ──────────────────────────────────────────────────────────── # A vendor endpoint returning one JSON object with the robot's state. X2_STATE_URL= X2_HTTP_INTERVAL=1 # ── backend: ros2 ──────────────────────────────────────────────────────────── # Topic names from `ros2 topic list` on the robot. Blank = don't subscribe. # The TYPE must match `ros2 topic info `; custom vendor messages work as # long as their python package is importable inside the container. X2_TOPIC_BATTERY=/battery_state X2_TYPE_BATTERY=sensor_msgs/msg/BatteryState X2_TOPIC_JOINTS=/joint_states X2_TYPE_JOINTS=sensor_msgs/msg/JointState X2_TOPIC_ODOM=/odom X2_TYPE_ODOM=nav_msgs/msg/Odometry # Must match the robot's domain or ROS 2 discovery silently sees nothing. ROS_DOMAIN_ID=0 # ── field mapping (applies to EVERY backend) ───────────────────────────────── # Dotted paths into the message/JSON. They work over both ROS message objects # and plain dicts, support list indices ("bms.cell_temp[0]"), and yield null for # any missing link — a wrong path degrades a field, it never crashes the agent. # Defaults below are the STANDARD ROS messages: correct for a stock ros2 setup, # almost certainly wrong for a vendor http payload. Empty = field unavailable. X2_FIELD_SOC=percentage X2_FIELD_VOLTAGE=voltage X2_FIELD_CURRENT=current X2_FIELD_TEMP=temperature X2_FIELD_SOH= X2_FIELD_CYCLES= # Standard sensor_msgs/JointState has NO temperature array — leave empty unless # the X2 publishes a custom message that carries one (motor_temp reports null). X2_FIELD_TEMPS= # Joint velocities → the "moving" status (max |velocity| > 0.15). X2_FIELD_VEL=velocity X2_FIELD_X=pose.pose.position.x X2_FIELD_Y=pose.pose.position.y # http backend only: an FSM id and a {name: version} firmware dict, if exposed. X2_FIELD_FSM= X2_FIELD_FW= # ── unit conventions (verify these on the real robot!) ─────────────────────── # auto = a value in (0, 1] is treated as a 0..1 fraction and scaled to %. # fraction = always scale x100. percent = never scale. X2_SOC_SCALE=auto # +1 = positive current means CHARGING (the ROS BatteryState convention). # Set -1 if the X2 reports the opposite sign, or "charging" will be inverted. X2_CURRENT_SIGN=1 # The telemetry schema is VOLTS and AMPS. BMS firmware often publishes mV/mA # instead — set 0.001 for those, so battery_detail isn't off by 1000x. X2_VOLTAGE_SCALE=1 X2_CURRENT_SCALE=1 # ── position / status ──────────────────────────────────────────────────────── # none | ros2 | rosbridge. ros2 is INDEPENDENT of X2_SOURCE: the Control Dash # carries no odometry, but ROS publishes it — so http battery + ros2 position # run side by side. Needs rclpy, i.e. the unit must source the ROS overlay. # On the X2 as deployed: /aima/mc/leg_odometry (nav_msgs/msg/Odometry). X2_POSITION_SOURCE=ros2 ROSBRIDGE_URL=ws://127.0.0.1:9090 # Subscription reliability. MUST stay best_effort unless you know otherwise: # the X2 publishes /aima/mc/leg_odometry as BEST_EFFORT, and a RELIABLE # subscriber receives NOTHING from a BEST_EFFORT publisher — the subscription # is created, no error is raised, and position silently stays null forever. # A BEST_EFFORT subscriber reads from either kind of publisher. X2_ROS_QOS=best_effort # There is no known X2 loco FSM RPC — leave 0. X2_READ_FSM=0 # ── identity / networking ──────────────────────────────────────────────────── # Which NIC's MAC is reported as the robot identity. MAC_INTERFACE=eth0 # ── fault thresholds ───────────────────────────────────────────────────────── LOW_SOC=50 # Only meaningful when X2_FIELD_TEMPS is mapped; otherwise motor_temp is null. MOTOR_TEMP_MAX=85 # ── cadence / transport ────────────────────────────────────────────────────── POLL_INTERVAL=2 VERIFY_TLS=1 HTTP_TIMEOUT=30 # ── map sync (uploaded ONCE per content; status shown in telemetry "map") ──── # pgm+yaml sets are rendered to PNG with pure stdlib; RTAB-Map .db is sent # as-is (skipped above the server's upload cap). MAPS_DIR=/data/maps EXTRA_MAP_DIRS=/data/nav2_maps DATA_DIR=/data/web_data STATE_DIR=/data/state MAP_SELECT=all MAP_UPLOAD_MODE=multipart MAP_POLL_INTERVAL=30 MAP_MAX_UPLOAD_MB=7 # ── logs + alerts ──────────────────────────────────────────────────────────── LOGS_INTERVAL=60 # auto = find a RUNNING sanad* container via the /host mount and tail its # json-log. If the X2 doesn't run the Sanad app, telemetry.project_logs is null # and only the agent's own lines ship — no configuration needed. PROJECT_LOG_CONTAINER=auto PROJECT_LOG_PATH= PROJECT_LOG_LABEL= ALERT_SCAN_INTERVAL=10 ALERT_LOG_COOLDOWN=300 # ── remote (register a dashboard URL + ssh for the fleet UI) ───────────────── # If the X2 exposes no dashboard, remote.web is null and only ssh registers. REMOTE_ENABLE=1 REMOTE_PORTS=8001,8014,8011,8012,8013,8000,8080 REMOTE_KIND=web REMOTE_INTERVAL=60 SSH_REGISTER=1 # The X2's LOGIN user. The installer writes the user it deployed with; set it # here for manual runs. # Left empty, ssh registration is skipped rather than registering a wrong user. SSH_USER= # ── control panel (READ-ONLY; remote mode-SWITCH is off by design) ─────────── # Blank URL = use the auto-discovered dashboard port. No known X2 FSM id map, so # control.mode reports "unknown" until the ids are filled into the agent. CONTROL_STATUS_URL= CONTROL_ENABLE=0