# Teleop Bridge

> Wire a `geniesim_teleop` VR/Pico session into a running Genie Sim RT Engine scene — pick the right scene yaml, launch `wbc.launch.py` with `use_ros2_control:=false` so move_group serves `/compute_ik` while the teleop publisher owns `/joint_command`, and confirm no topic fighting. Trigger: When the user asks to "connect teleop to the sim", "桥接 teleop", "drive the engine with VR", "use teleop instead of ros2_control", "stop the controllers from fighting teleop", or has a teleop loop ready and a scene up but the robot jitters / flicks.

- Skill: `agibottech/teleop-bridge` (Agent Skill)
- Install (CLI): `npx skillmds@latest add agibottech/teleop-bridge`
- Raw SKILL.md: https://api.skillmd.com/api/skills/agibottech/teleop-bridge/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MPL-2.0
- Author: agibottech (https://skillmd.com/u/agibottech)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/agibottech/teleop-bridge

---


## When to Use

- VR teleop loop (`geniesim_teleop` skill `run-teleop`) and an RT
  Engine scene (`launch-scene` skill) need to share `/joint_command`
  without the ros2_control hardware interface rebroadcasting it at
  the controller-manager update rate.
- User sees "the arm jitters / payloads get flung" — that's the
  classic two-publisher symptom.
- External motion script (e.g. `wok_flip_cmds.py`) and MoveIt are
  both interested in the same topic.

Do **not** use for:
- Starting the teleop loop itself → `run-teleop` skill in
  `geniesim_teleop`.
- MoveIt planning with the simulator's controllers driving →
  `moveit-wbc` skill (default mode).
- Recording the teleop session → `record-episode` skill.

## Critical Patterns

1. **`use_ros2_control:=false` is the bridge switch.** With it,
   `wbc.launch.py` starts `move_group` only (`/compute_ik`,
   `/compute_fk`) and skips the ros2_control node + controller
   spawners. `/joint_command` then has exactly one source: teleop.
2. **Match scene gripper to teleop config.** `run-teleop` defaults
   to `G2_omnipicker.json`. Use `scene_pnp_g2_op` (or any
   `scene_*_g2_op`) so the engine is built with the same gripper
   the teleop loop expects. Mismatched grippers produce a robot
   MoveIt can plan against but the engine refuses to drive.
3. **Three processes, three shells.** Scene + MoveIt + teleop each
   run as a long-lived ROS node. Don't try to background them in one
   shell — the engine and teleop both want stdin / SIGINT semantics.
4. **`use_sim_time:=true` is engine-side**, but the teleop loop
   publishes wall-clock by default. The bridge uses sim time inside
   the scene; the publisher rate adapts. Don't override unless you
   know what you're doing.

## Workflow

### Step 1 — Confirm the prerequisites

- Scene is up via `launch-scene` skill (e.g.
  `scene_pnp_g2_op` × `launcher_ovrtx_isaac_physx`).
- Teleop config matches the scene's gripper.
- VR device reachable, default port `8080`.

### Step 2 — Launch MoveIt without controllers

In a fresh shell:

```bash
source devel/setup.bash

ros2 launch genie_sim_moveit wbc.launch.py \
  arm:=crsB \
  gripper:=omnipicker \
  use_ros2_control:=false
```

Console banner should print:

```
[wbc.launch.py] use_ros2_control=false → move_group only (no ros2_control
node / controllers); drive the arm via /joint_command.
```

### Step 3 — Start teleop

In another shell (`geniesim docker into`):

```bash
geniesim teleop run \
  --device_type=pico \
  --port=8080 \
  --robot_config=G2_omnipicker.json
```

### Step 4 — Verify single-publisher on `/joint_command`

```bash
ros2 topic info /joint_command -v
# Publishers should list exactly one: the teleop node.
```

If you see two publishers, you forgot `use_ros2_control:=false` —
stop MoveIt and relaunch with the flag.

### Step 5 — Plan-then-execute combo (optional)

Even without ros2_control, `/compute_ik` is still available. So you
can:
- Use MoveIt's RViz markers to plan and call `/compute_ik` for
  manual goals.
- Hand the resulting joint target to your teleop publisher (or skip
  MoveIt entirely for direct VR drive).

## Commands (copy-paste summary for the user)

```bash
# Shell 1 — scene
source devel/setup.bash
ros2 launch genie_sim_bringup app.launch.py \
  scene:=scene_pnp_g2_op \
  launcher_config:=launcher_ovrtx_isaac_physx \
  headless:=false   # local workstation with a screen; flip to true on a remote/headless host

# Shell 2 — MoveIt, no controllers
source devel/setup.bash
ros2 launch genie_sim_moveit wbc.launch.py \
  arm:=crsB gripper:=omnipicker use_ros2_control:=false

# Shell 3 — teleop loop
geniesim teleop run --device_type=pico --port=8080 \
  --robot_config=G2_omnipicker.json

# Verify single publisher
ros2 topic info /joint_command -v
```

## Notes

- `simple_body_controller` is configured but not auto-spawned even in
  the controller mode — it conflicts with
  `simple_waist_controller` + `simple_torso_controller` on the same
  body-joint resources. The teleop bridge avoids that ambiguity by
  not spawning any of them.
- For wok-flip / scripted motion (`wok_flip_cmds.py`,
  benchmark replay scripts), the same `use_ros2_control:=false`
  flag applies — anything that owns `/joint_command` directly needs
  the controllers out of the way.
- The teleop loop has its own per-episode recording hooks
  (`--record-dir`). For raw-topic recording on top, see the
  `record-episode` skill.

## Resources

- **Teleop CLI + skill**: [source/geniesim_teleop/skills/run-teleop/SKILL.md](../../../geniesim_teleop/skills/run-teleop/SKILL.md)
- **MoveIt launch with the bridge switch**: [source/geniesim_ros/src/ros_ws/src/genie_sim_moveit/launch/wbc.launch.py](../../src/ros_ws/src/genie_sim_moveit/launch/wbc.launch.py)
- **Related skills**: `launch-scene`, `moveit-wbc`, `record-episode`

