From 5f0fe3a747ef22e6c6498f3b5143fed721cfb67b Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Thu, 19 Feb 2026 11:37:21 -0600 Subject: [PATCH] Document code deployment workflow, update launch/pull scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CLAUDE.md: Add CODE DEPLOYMENT WORKFLOW section — git push to Gitea then pull on robot, never SFTP. Document repo layout and two xr_teleoperate locations on robot. - pull_on_robot.py: Updated verification markers (webcam, head_R_at_cal, hasattr guards) - start_teleop_webcam_full.py: Use standalone ~/xr_teleoperate path Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 23 ++++++ scripts/pull_on_robot.py | 28 +++---- scripts/start_teleop_webcam_full.py | 111 ++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 14 deletions(-) create mode 100644 scripts/start_teleop_webcam_full.py diff --git a/CLAUDE.md b/CLAUDE.md index 2ca913e..333d156 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -103,6 +103,29 @@ When the user asks you to reason about something novel: - Robot SSH: `unitree@10.0.0.64` password `123` - GB10 SSH: `mitchaiet@10.0.0.68` password `Strat3*gb10` +## CODE DEPLOYMENT WORKFLOW — CRITICAL + +**NEVER deploy code to the robot via direct SFTP upload.** Always use the git workflow: + +1. **Edit locally** in `apps/xr_teleoperate/` (this is a separate git repo, gitignored by unitree-g1) +2. **Commit and push** to Gitea: `cd apps/xr_teleoperate && git push gitea main` +3. **Pull on robot** via paramiko: `cd ~/xr_teleoperate && git pull origin main` + +The helper script `scripts/pull_on_robot.py` automates step 3. + +### Repository layout + +| Location | Repo | Purpose | +|---|---|---| +| `C:\git\unitree-g1` | `gitea.opentesla.org/epilectrik/unitree-g1` | Knowledge base, context, scripts | +| `C:\git\unitree-g1\apps\xr_teleoperate` | `gitea.opentesla.org/epilectrik/teleop` | Teleop source code (forked from unitreerobotics/xr_teleoperate) | +| Robot: `~/xr_teleoperate` | same `epilectrik/teleop` | Standalone clone — **this is the one we run** | +| Robot: `~/g1-control/repos/xr-teleoperate` | Unitree stock | **DO NOT MODIFY** — used by stock `start_teleop.sh` | + +### Launching teleop + +Use `scripts/start_teleop_webcam_full.py` to launch teleop from the standalone `~/xr_teleoperate` path. This bypasses `start_teleop.sh` (which kills the USB webcam driver via `modprobe -r uvcvideo`). + ## DO NOT - Do not assume G1 specs are the same as H1 or other Unitree robots — they differ significantly. diff --git a/scripts/pull_on_robot.py b/scripts/pull_on_robot.py index 0973bc9..640e723 100644 --- a/scripts/pull_on_robot.py +++ b/scripts/pull_on_robot.py @@ -6,7 +6,7 @@ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.connect('10.0.0.64', username='unitree', password='123', timeout=15, look_for_keys=False, allow_agent=False) -# Check current remotes - origin already points to gitea +# Check current remotes print('=== Current remotes ===') _, o, _ = ssh.exec_command('cd ~/xr_teleoperate && git remote -v', timeout=10) print(o.read().decode().strip()) @@ -17,7 +17,7 @@ _, o, _ = ssh.exec_command('cd ~/xr_teleoperate && git status --short', timeout= status = o.read().decode().strip() print(status if status else '(clean)') -# Pull from origin (which is already gitea) +# Pull from origin (which is gitea) print('\n=== Pulling from origin main ===') _, o, e = ssh.exec_command('cd ~/xr_teleoperate && git pull origin main', timeout=30) pull_out = o.read().decode().strip() @@ -26,21 +26,21 @@ print(pull_out) if pull_err: print(pull_err) -# Verify the changes are present -print('\n=== Verifying compute_fk exists ===') -_, o, _ = ssh.exec_command('grep -n "def compute_fk" ~/xr_teleoperate/teleop/robot_control/robot_arm_ik.py', timeout=5) -print(o.read().decode().strip() or 'NOT FOUND') +# Verify key markers +print('\n=== Verifying webcam support ===') +_, o, _ = ssh.exec_command('grep -c "webcam" ~/xr_teleoperate/teleop/teleop_hand_and_arm.py', timeout=5) +print(f"webcam references: {o.read().decode().strip()}") -print('\n=== Verifying --ki arg exists ===') -_, o, _ = ssh.exec_command('grep -n "\\-\\-ki" ~/xr_teleoperate/teleop/teleop_hand_and_arm.py', timeout=5) -print(o.read().decode().strip() or 'NOT FOUND') +print('\n=== Verifying head_R_at_cal ===') +_, o, _ = ssh.exec_command('grep -c "head_R_at_cal" ~/xr_teleoperate/teleop/teleop_hand_and_arm.py', timeout=5) +print(f"head_R_at_cal references: {o.read().decode().strip()}") -print('\n=== Verifying I-term logic exists ===') -_, o, _ = ssh.exec_command('grep -n "I-term" ~/xr_teleoperate/teleop/teleop_hand_and_arm.py', timeout=5) -print(o.read().decode().strip() or 'NOT FOUND') +print('\n=== Verifying hasattr guard ===') +_, o, _ = ssh.exec_command('grep -c "hasattr.*get_wireless_remote" ~/xr_teleoperate/teleop/teleop_hand_and_arm.py', timeout=5) +print(f"hasattr guards: {o.read().decode().strip()}") -print('\n=== Latest commit ===') -_, o, _ = ssh.exec_command('cd ~/xr_teleoperate && git log --oneline -3', timeout=5) +print('\n=== Latest commits ===') +_, o, _ = ssh.exec_command('cd ~/xr_teleoperate && git log --oneline -5', timeout=5) print(o.read().decode().strip()) ssh.close() diff --git a/scripts/start_teleop_webcam_full.py b/scripts/start_teleop_webcam_full.py new file mode 100644 index 0000000..6c05a9f --- /dev/null +++ b/scripts/start_teleop_webcam_full.py @@ -0,0 +1,111 @@ +"""Launch full teleop with webcam (no image server, with hand drivers). + +Replicates start_teleop.sh but skips the image server that kills the UVC driver. +""" +import paramiko +import sys +import time + +sys.stdout.reconfigure(encoding='utf-8', errors='replace') + +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect('10.0.0.64', username='unitree', password='123', + timeout=15, look_for_keys=False, allow_agent=False) + +# ── Step 0: Kill any existing teleop / hand driver processes ── +print("Cleaning up old processes...") +_, o, _ = ssh.exec_command( + 'ps aux | grep -E "teleop_hand_and_arm|ModbusDataHandler|image_server" | grep -v grep', + timeout=5) +procs = o.read().decode().strip() +if procs: + print(f" Found:\n{procs}") + for line in procs.split('\n'): + parts = line.split() + if len(parts) > 1: + ssh.exec_command(f'kill {parts[1]}', timeout=5) + time.sleep(1) + print(" Killed stale processes") +else: + print(" No stale processes") + +# ── Step 1: Start Inspire hand Modbus-to-DDS drivers ── +print("\nStarting Inspire hand drivers...") + +TELEOP_DIR = "/home/unitree/xr_teleoperate/teleop" +CYCLONE_HOME = "/home/unitree/g1-control/repos/cyclonedds/install/cyclonedds" +CYCLONE_URI = "file:///home/unitree/g1-control/repos/cyclonedds/cyclonedds.xml" +PYTHON = "/home/unitree/miniforge3/envs/tv/bin/python" + +env_prefix = ( + f'export CYCLONEDDS_HOME="{CYCLONE_HOME}" && ' + f'export CYCLONEDDS_URI="{CYCLONE_URI}" && ' +) + +# Right hand (also initializes DDS) +right_hand_cmd = ( + f'{env_prefix} ' + f'cd {TELEOP_DIR} && ' + f'nohup {PYTHON} -m inspire_hand.ModbusDataHandler ' + f'--ip 192.168.123.211 --initDDS True > /tmp/right_hand.log 2>&1 &' +) +ssh.exec_command(right_hand_cmd, timeout=10) +print(" Right hand driver started (192.168.123.211)") +time.sleep(2) + +# Left hand (check reachability first) +_, o, _ = ssh.exec_command('ping -c 1 -W 1 192.168.123.210 2>&1 | grep "1 received"', timeout=5) +left_reachable = bool(o.read().decode().strip()) +if left_reachable: + left_hand_cmd = ( + f'{env_prefix} ' + f'cd {TELEOP_DIR} && ' + f'nohup {PYTHON} -m inspire_hand.ModbusDataHandler ' + f'--ip 192.168.123.210 > /tmp/left_hand.log 2>&1 &' + ) + ssh.exec_command(left_hand_cmd, timeout=10) + print(" Left hand driver started (192.168.123.210)") +else: + print(" Left hand not reachable (skipped)") + +time.sleep(2) + +# ── Step 2: Launch teleop with webcam ── +print("\nStarting teleop with webcam...") +print("=" * 60) + +teleop_cmd = ( + f'{env_prefix} ' + f'cd {TELEOP_DIR} && ' + f'{PYTHON} teleop_hand_and_arm.py ' + f'--arm=G1_29 --ee=inspire_ftp --webcam 0 --webcam-res 720p' +) + +stdin, stdout, stderr = ssh.exec_command(teleop_cmd, timeout=300, get_pty=True) + +# Stream output in real-time +start_time = time.time() +while time.time() - start_time < 120: + if stdout.channel.recv_ready(): + data = stdout.channel.recv(4096).decode('utf-8', errors='replace') + print(data, end='') + if stderr.channel.recv_stderr_ready(): + data = stderr.channel.recv_stderr(4096).decode('utf-8', errors='replace') + print(f"[err] {data}", end='') + if stdout.channel.exit_status_ready(): + remaining = stdout.channel.recv(65536).decode('utf-8', errors='replace') + if remaining: + print(remaining, end='') + remaining_err = stderr.channel.recv_stderr(65536).decode('utf-8', errors='replace') + if remaining_err: + print(f"[err] {remaining_err}", end='') + exit_code = stdout.channel.recv_exit_status() + print(f"\n\nProcess exited with code {exit_code}") + break + time.sleep(0.1) +else: + print("\n\nTeleop is running! (2 min output window elapsed)") + print("Connect Quest 3 to: https://10.0.0.64:8012") + +ssh.close()