Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
480 changes: 480 additions & 0 deletions scripts/r1lite_test/BRINGUP_LOG.md

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions scripts/r1lite_test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# R1 Lite Integration — Bring-up Test Suite (scaffolding)

Robot-side ROS 2 validation for the Galaxea R1 Lite, ported from
[`scripts/r1pro_test/`](../r1pro_test/) (Mustafa's R1 Pro work). These
scripts talk **plain ROS 2** to the robot — no dimos — so we can prove the
robot obeys commands before layering the dimos connection Module on top.

> **Status: SCAFFOLDING.** Every topic name, joint count, domain ID, and
> pose in [`r1lite_config.py`](r1lite_config.py) is inherited from the R1
> Pro and marked `TODO(recon)`. Nothing here has run against R1 Lite
> hardware yet. **Run `test_00_recon.py` first and reconcile the config.**

## Known R1 Lite differences from R1 Pro (verify during recon)

| Aspect | R1 Pro | R1 Lite (expected) |
|---|---|---|
| Arm DOF | 7 per arm | **6 per arm** (Galaxea spec) |
| Upper-body joints | 18 (4+7+7) | likely **16** (4+6+6) — confirm |
| ROS | ROS 2 Humble | docs ship ROS1 **and** ROS2 tracks — confirm which is installed |
| Chassis 3-gate problem | yes | unknown — test_03 assumes it may differ |
| ROS_DOMAIN_ID | 41 | unknown — run `domain_scan.py` |

## Order of operations

```bash
# 0. Find the robot's domain (robot stack must be running)
python3 scripts/r1lite_test/domain_scan.py

# 1. Recon: dump topics/nodes/rates/joint-counts; then edit r1lite_config.py
export ROS_DOMAIN_ID=<from step 0>
python3 scripts/r1lite_test/test_00_recon.py

# 2. Once config is reconciled, run the graded suite (single DDS session)
python3 scripts/r1lite_test/run_all_tests.py --skip-chassis --skip-arm # read-only first
python3 scripts/r1lite_test/run_all_tests.py # full, robot moves
```

All commands run **inside the dev container** (`ghcr.io/dimensionalos/ros-dev:dev`,
`--network host`) with `source /opt/ros/humble/setup.bash` and the py3.10
venv active — same environment as the R1 Pro bring-up.

## Files

| File | Purpose |
|---|---|
| `r1lite_config.py` | Single source of truth for topics/DOF/domain — **the file to edit after recon** |
| `domain_scan.py` | Sweep ROS_DOMAIN_ID 0-101 to find the robot |
| `test_00_recon.py` | Read-only graph dump: topics, nodes, rates, joint counts |
| `test_01_topic_discovery.py` | Assert expected topics are visible |
| `test_02_read_arm_feedback.py` | Read left-arm JointState feedback |
| `test_03_chassis_command.py` | Drive chassis briefly (robot moves) |
| `test_04_arm_joint_command.py` | Move one arm joint and return (robot moves) |
| `test_06_torso_command.py` | Move torso to home pose and back (robot moves) |
| `run_all_tests.py` | Graded runner, one rclpy session, safety confirms between steps |

## Open questions the recon must close

1. ROS 1 or ROS 2 on this unit? (If ROS 1: apply Galaxea's ROS2 upgrade first.)
2. Real topic names + message types vs the R1 Pro `/hdas/*` + `/motion_target/*` set.
3. Arm/torso DOF and safe torso home pose (from the robot's startup scripts).
4. Does the chassis need a gatekeeper / have the 3-gate problem?
5. RMW implementation + FastDDS version.
115 changes: 115 additions & 0 deletions scripts/r1lite_test/RUNBOOK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# R1 Lite Session Runbook

Everything validated 2026-07-02/03, with exact recreate commands.
Consoles: **[laptop]** = plain laptop shell · **[robot]** = `ssh r1lite` ·
**[container]** = shell inside `dimos-dev-r1lite`.

---

## One-time setup (already done — recorded for rebuild-from-scratch)

| What | Command | Status |
|---|---|---|
| ghcr access (needs `read:packages`) | `gh auth refresh -h github.com -s read:packages` then `gh auth token \| docker login ghcr.io -u KrishnaH96 --password-stdin` | ✅ |
| Pull ROS-track dev image (NOT `dev:latest` — that one has no ROS) | `docker pull ghcr.io/dimensionalos/ros-dev:dev` | ✅ |
| Create container | `docker run -d --name dimos-dev-r1lite --network host -v /home/krishnah/krishnah/dimos:/app -v /tmp/.X11-unix:/tmp/.X11-unix -v $HOME/.Xauthority:/root/.Xauthority:rw -e PYTHONUNBUFFERED=1 -e PYTHONPATH=/app -e DISPLAY=$DISPLAY -it ghcr.io/dimensionalos/ros-dev:dev /bin/bash` | ✅ |
| Rebuild venv as py3.10 (Humble/rclpy) — in container | `cd /app && ln -sf .envrc.r1pro .envrc && rm -rf .venv && UV_PYTHON=3.10 uv sync --all-extras --no-extra dds --no-extra unitree-dds` | ✅ |
| SSH alias + key | `~/.ssh/config` Host `r1lite` → `r1lite@192.168.1.85`; `ssh-copy-id r1lite` (password: `1`) | ✅ |
| Persistent laptop IP (2026-07-03) | `nmcli connection modify lidar +ipv4.addresses 10.42.0.100/24` — "lidar" profile now carries 192.168.1.5 AND 10.42.0.100 on every link-up | ✅ |

## Every-session bring-up

1. **[laptop] Verify cable link** (IP is persistent since 2026-07-03 — no nmcli needed):
```bash
ping -c 2 10.42.0.2 # robot onboard PC over the cable
```
2. **[laptop] Container** (after reboot/suspend):
```bash
docker start dimos-dev-r1lite
docker exec -it dimos-dev-r1lite bash # one per terminal needed
```
3. **[container] Per-shell env** (every new shell):
```bash
source .venv/bin/activate && source /opt/ros/humble/setup.bash && export ROS_DOMAIN_ID=2
```
4. **[robot] Stack** (if `tmux ls` shows nothing):
```bash
cd ~/galaxea/install/startup_config/share/startup_config/script
./robot_startup.sh boot ../sessions.d/ATCStandard/R1LITEBody.d
# wait ~30s (arms/grippers twitch = HDAS health sign), then:
tmux kill-session -t r1lite_teleop # keep GELLO teleop off the arms
```
Shutdown: `./robot_startup.sh kill`

## Validated tests (all ✅, safe to recreate)

**Discovery / read-only:**
```bash
# [container] full graph recon (topics, nodes, rates, DOF):
python scripts/r1lite_test/test_00_recon.py

# [container] formal topic assertion (12 expected topics):
python scripts/r1lite_test/test_01_topic_discovery.py

# [container] live position of any segment:
ros2 topic echo /hdas/feedback_arm_left --field position
# ("A message was lost!!!" spam on 200-488Hz topics = slow CLI subscriber, benign)
```

**Gripper (first actuation ✅ — 0–100 units, 0=closed, 100=open):**
```bash
# [container] shell B monitor:
ros2 topic echo /hdas/feedback_gripper_left --field position
# [container] shell A — MUST STREAM (-r 10); one-shot --once is ignored.
# Motion stops the moment you Ctrl-C (dead-man).
ros2 topic pub -r 10 /motion_target/target_position_gripper_left sensor_msgs/msg/JointState "{header: {stamp: now}, name: [''], position: [85.0], velocity: [0.0], effort: [0.0]}"
# reopen: same with position: [101.8]
```

**Arm wrist-roll (✅ — serial joints, independently commandable):**
```bash
# [container] shell B monitor:
ros2 topic echo /hdas/feedback_arm_left --field position
# [container] shell A — joints 1-5 held at rest values, joint 6 → 0.2 rad:
ros2 topic pub -r 10 /motion_target/target_joint_state_arm_left sensor_msgs/msg/JointState "{header: {stamp: now}, name: [''], position: [0.065, 0.0, 0.0, 0.008, -0.001, 0.2], velocity: [0.5, 0.5, 0.5, 0.5, 0.5, 0.5], effort: [0.0]}"
# return home: same with last value 0.001
```

## NEVER recreate

- **Torso joint targets** (`target_joint_state_torso`) — parallelogram
linkage; single-joint deltas made the robot SHAKE (2026-07-03).
Galaxea docs: joint+velocity torso signals conflict, disable_torso=true
by default. test_06 is hard-guarded. Torso motion later = task-space
`target_speed_torso` only, as a designed experiment.
- **`findrobot_server.sh` on the robot** — rewrites DDS config to
discovery-server mode and reboots; would break our multicast setup.

## Chassis: partially solved, one blocker (see BRINGUP_LOG for full saga)

Validated: chassis node accepts streamed targets in RC **mode 5** (RC ON,
all 4 switches position 1), ramp reaches full speed with acc_limit +
brake=false streamed (test_03 does all gates itself). RC mode map:
all-pos1=5 (software), sw1@2+sw2@3=2 (brake), +sw3@mid=3 (RC manual drive).

UNRESOLVED: wheels refuse to turn (both software AND RC manual) despite
healthy RC/VCU/battery/e-stop. **Next session, check FIRST: the separate
wireless E-STOP REMOTE** (R1 Lite ships 3 remotes: torso/chassis/e-stop —
a pressed or dead e-stop fob = failsafe stop, explains everything).
Fallbacks: Galaxea "Wheel Motor Zero-Point Calibration" doc,
product@galaxea-dynamics.com.

⚠️ RULES learned the hard way:
- Chassis node LATCHES the last target forever (no dead-man!) — ALWAYS
end chassis work with a zero-velocity stream.
- Never power the robot on with e-stop pressed (whole-session inhibit).
- On-robot `ros2 topic list/echo` may show nothing after `ros2 daemon
stop` — daemon cache warming, not an outage; wait or use --no-daemon.

## Not yet done

- Chassis wheel-level unblock (e-stop remote lead above), then test_03 PASS.
- Torso task-space experiment (target_speed_torso, MPC path) — designed
session; joint path is guarded off.
- Mesh copy for LFS: `scp -r r1lite:~/galaxea/install/mobiman/share/mobiman/urdf/R1_Lite/meshes /tmp/`
- `R1LiteConnection` module (all facts in `r1lite_config.py`).
Empty file added scripts/r1lite_test/__init__.py
Empty file.
48 changes: 48 additions & 0 deletions scripts/r1lite_test/domain_scan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""Sweep ROS_DOMAIN_ID 0-101 and report domains with foreign participants.

DDS participant discovery is multicast and fast on a wired LAN, so a short
dwell per domain is enough to spot a robot whose domain we don't know.
"""

import sys
import time

import rclpy
from rclpy.context import Context
from rclpy.node import Node


def scan_domain(domain_id: int, dwell_s: float = 0.7) -> list[str]:
ctx = Context()
rclpy.init(context=ctx, domain_id=domain_id)
try:
node = Node(f"domain_scan_{domain_id}", context=ctx)
try:
time.sleep(dwell_s)
names = [
f"{ns}/{name}" if ns != "/" else f"/{name}"
for name, ns in node.get_node_names_and_namespaces()
]
return [n for n in names if f"domain_scan_{domain_id}" not in n]
finally:
node.destroy_node()
finally:
rclpy.shutdown(context=ctx)


def main() -> None:
hits = {}
for d in range(0, 102):
others = scan_domain(d)
if others:
hits[d] = others
print(f"DOMAIN {d}: {len(others)} foreign node(s): {others[:8]}", flush=True)
if d % 20 == 0:
print(f"...scanned up to domain {d}", file=sys.stderr, flush=True)
if not hits:
print("No foreign ROS 2 participants found on any domain 0-101.")


if __name__ == "__main__":
main()
85 changes: 85 additions & 0 deletions scripts/r1lite_test/r1lite_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""R1 Lite hardware constants — single source of truth for the test suite.

Reconciled against live hardware 2026-07-02/03 (test_00_recon.py, boot
profile ATCStandard/R1LITEBody.d, unit hostname "r1lite"). Remaining
unknowns are marked TODO(recon). Full evidence trail: BRINGUP_LOG.md.

Verified robot identity (/opt/galaxea/body/hardware.json): R1-LITE,
ARM=A1X, TORSO=T0, ECU=E03, CHASSIS=C1(+CS1), ARM_END=EE0,
HEAD_CAMERA=HC0 (stereo, no depth topic), WRIST_CAMERA=WC1 (RealSense).
Onboard computer: x86_64, Ubuntu 22.04.5, ROS 2 Humble, user "r1lite".
"""

# --- Network (verified) ------------------------------------------------
ROBOT_ETH_IP = "10.42.0.2" # enp2s0, factory-assigned; laptop uses 10.42.0.100
ROBOT_WIFI_IP = "192.168.1.85" # office WiFi (wlo1)
ROS_DOMAIN_ID = 2 # robot .bashrc: ROS_DOMAIN_ID=02; plain multicast

# --- Kinematics (verified via feedback rates/counts) --------------------
ARM_DOF = 6 # /hdas/feedback_arm_*: 6 joints @ 200 Hz
TORSO_DOF = 4 # /hdas/feedback_torso: 4 joints @ 488 Hz
CHASSIS_FB_DOF = 3 # /hdas/feedback_chassis: 3 joints @ 200 Hz
GRIPPER_DOF = 1 # /hdas/feedback_gripper_*: 1 joint @ 200 Hz
# /joint_states aggregate: 25 joints @ 500 Hz (naming TODO below)
# /hdas/feedback_hand_*: exist but silent (no dexterous hands on this unit)

# TODO(recon): safe torso "home" pose — read it from the robot's
# start_mobiman_torso_speed_control.sh (R1LITE arg) before running test_06.
TORSO_HOME_POSE: list[float] = [0.0] * TORSO_DOF

# TODO(recon): joint NAMES for each feedback topic (echo --field name),
# and the 25-joint /joint_states naming; needed for dimos joint mapping.

# --- Topics (verified live 2026-07-03) ----------------------------------
FEEDBACK_ARM = "/hdas/feedback_arm_{side}" # JointState, 200 Hz
FEEDBACK_TORSO = "/hdas/feedback_torso" # JointState, 488 Hz
FEEDBACK_CHASSIS = "/hdas/feedback_chassis" # JointState, 200 Hz
FEEDBACK_GRIPPER = "/hdas/feedback_gripper_{side}" # JointState, 200 Hz
IMU_CHASSIS = "/hdas/imu_chassis" # sensor_msgs/Imu
IMU_TORSO = "/hdas/imu_torso" # sensor_msgs/Imu
CMD_ARM = "/motion_target/target_joint_state_arm_{side}" # JointState
CMD_TORSO = "/motion_target/target_joint_state_torso" # JointState
CMD_TORSO_SPEED = "/motion_target/target_speed_torso" # TwistStamped (new vs R1 Pro)
CMD_GRIPPER = "/motion_target/target_position_gripper_{side}" # JointState
CMD_CHASSIS_SPEED = "/motion_target/target_speed_chassis" # TwistStamped
CHASSIS_ACC_LIMIT = "/motion_target/chassis_acc_limit" # TwistStamped
BRAKE_MODE = "/motion_target/brake_mode" # std_msgs/Bool
CHASSIS_SPEED_FB = "/motion_control/chassis_speed" # TwistStamped (Gate-1 analog)
# TODO(recon): R1 Pro's gatekeeper /cmd_vel does NOT exist here — chassis
# command path is CMD_CHASSIS_SPEED directly; 3-gate behavior unverified
# (all three gate suspects exist: /controller topic, brake_mode, acc_limit).
CMD_VEL_GATEKEEPER = CMD_CHASSIS_SPEED

# Perception (verified): wrist cams = full RealSense stacks; head = stereo
# RGB pair, NO depth topic; NO chassis cameras; NO lidar topic in this
# boot profile (livox driver installed but not launched).
HEAD_LEFT_COMPRESSED = "/hdas/camera_head/left_raw/image_raw_color/compressed"
HEAD_RIGHT_COMPRESSED = "/hdas/camera_head/right_raw/image_raw_color/compressed"
WRIST_COLOR_COMPRESSED = "/hdas/camera_wrist_{side}/color/image_raw/compressed"
WRIST_DEPTH_ALIGNED = "/hdas/camera_wrist_{side}/aligned_depth_to_color/image_raw"
ROBOT_DESCRIPTION = "/robot_description" # URDF published on-topic (pubs=2)

# Topics that must exist for test_01 to pass.
EXPECTED_TOPICS = [
FEEDBACK_ARM.format(side="left"),
FEEDBACK_ARM.format(side="right"),
FEEDBACK_CHASSIS,
FEEDBACK_TORSO,
FEEDBACK_GRIPPER.format(side="left"),
FEEDBACK_GRIPPER.format(side="right"),
IMU_CHASSIS,
IMU_TORSO,
CMD_CHASSIS_SPEED,
CMD_ARM.format(side="left"),
CMD_ARM.format(side="right"),
CMD_TORSO,
]

# --- Remaining open questions -------------------------------------------
# 1. Joint names per topic + /joint_states 25-joint composition.
# 2. Torso safe home pose.
# 3. Chassis gating: does r1_lite_chassis_control_node need subscriber/
# brake/acc-limit unlocks like the R1 Pro's 3 gates? (test_03 will tell.)
# 4. Head depth: stereo-only? (calib/head_{left,right} suggest onboard
# stereo depth is possible but not published.)
# 5. Lidar: present on unit? (No topic; livox driver installed.)
Loading