Browse Source

Document code deployment workflow, update launch/pull scripts

- 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 <noreply@anthropic.com>
master
Joe DiPrima 1 month ago
parent
commit
5f0fe3a747
  1. 23
      CLAUDE.md
  2. 28
      scripts/pull_on_robot.py
  3. 111
      scripts/start_teleop_webcam_full.py

23
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.

28
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()

111
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()
Loading…
Cancel
Save