If your machine already runs on ROS 2, you keep it. Your drivers keep the hardware, your topics stay where they are, and Torq adds one node per machine that carries joint values between the two over the internet. teleop_bridge_node is a bridge, never a driver. It owns no servo bus, no port and no camera. It reads the topic your leader driver publishes, sends those values to the other machine, and publishes them onto the topic your follower driver subscribes to.

How it fits

You run the same node on both machines, told which side it is on.
The topic hop between this node and your driver is part of the path that moves the arm, not just something to watch. Keep an eye on it with ros2 topic hz as you would any other safety-relevant topic.

Install ROS 2

You need a working ROS 2 installation first. If you do not have one, follow the official installation guide.
There is no ros2 extra on the pip package. rclpy and the message packages come from a ROS distribution through apt, so pip cannot supply them.

Install the bridge

Two steps, because the two halves come from different package systems. The node itself is a pip package. ROS needs a package of its own before ros2 run and ros2 launch can find it.

The node

Install it into the interpreter ROS 2 itself runs on, not whichever one your shell points at. On Ubuntu 24.04 that interpreter is marked externally managed, so this needs --break-system-packages, which still only writes to ~/.local:

The ROS package

videosdk_teleop_ros holds no code. It is a launch file, a default parameter file, and the entry point that lets ROS start the node by name.
Run colcon build --symlink-install twice the first time. The first build succeeds and still leaves ros2 unable to find the package, and the second writes the missing environment hook.
Sourcing a conda environment on top of ROS does not work, because CMake’s FindPython3 honours CONDA_PREFIX before PATH.

Usage

Each terminal starts from a fresh shell with none of the environment:
Nothing moves until that enable call. The leader logs the peer’s shape as it is accepted or refused at arm time, which is the check that catches the two ends disagreeing about units. calibration_profile is where each host’s joint travel comes from. See SO-101 for that, or set joint_min and joint_max directly for any other arm.

Parameters

ros2 param list on a running node is the ground truth. The ones worth knowing before you launch:
The packaged config/videosdk_bridge.yaml sets max_norm_step: 1.0 rather than the node’s 5.0, which turns the bridge’s own rate limit on. Leave it if your driver has no limiter, raise it if it does.ros2 param dump /videosdk_bridge shows what a running node actually resolved, YAML and command line together.

Topics and services

Both roles publish and subscribe only what the parameter table names.
There is no ~/set_torque, and torque_enable() on this bridge does nothing. It powers no hardware, and the default failsafe is HOLD. Cutting power to the motors has to come from your driver, which is the only thing here that touches hardware.

Normalisation

Each machine holds exactly one calibration: its own. The leader divides by its own travel, the follower multiplies by its own, and neither host needs the other’s numbers. Two arms with different travel, such as an SO-101 leader and follower differing by about 19% at the gripper, both still reach their own end stops. For a non-SO-101 robot there is nothing to install. Give joint_min and joint_max in whatever units your topics already carry:
joint_names order is a cross-host contract. joint_min and joint_max are the opposite: per host, and copying them to the other machine gives an arm that tracks but with the wrong gain at the extremes.

Safety

Split by what each end can actually know.
max_norm_step defaults to 5.0, five times full travel in a single tick, so the bridge applies no rate limit. Lower it if your driver has no limiter of its own. Running two limiters in series means the tighter one wins and you have two places to look when the arm feels sluggish.
~/enable is the deadman, and it only exists on the leader. Disarming stops the command stream, the follower’s watchdog expires, and the far arm holds. Heartbeats keep flowing either way, so the follower can tell “operator let go” from “link died”.
A service call is not a held deadman. Nobody is physically holding anything, so an operator who walks away leaves the arm armed until the link or the watchdog intervenes.
On an e-stop the bridge stops publishing commands and your driver’s own stale-command behaviour takes over. Hold time is watchdog_timeout_s plus your driver’s command timeout.

Schema checks

The leader validates the follower’s announced descriptor directly: profile, joint names in order, and normalised units. It refuses to arm otherwise.
Without that check, a bridge paired with a native arm node would decode 0..1 as that node’s degrees, and every joint would drive to the bottom of its travel.

Timestamps

The bridge stamps header.stamp with the local clock on the robot machine, so a downstream driver measuring against it is measuring bridge to hardware. For the operator-to-hardware figure across two hosts, use staleness_ms and the observation-to-applied summary instead.

Joint states

Publish joint states from your driver at 10 Hz or more. Without them the bridge runs open-loop, echoing what it last commanded, and max_misalignment and tracking error stop meaning anything.

Adapters

Neither imports rclpy. A plain callable (sink) and a thread-safe latest-value cache stand in for the ROS graph, so you can exercise them with no ROS installed. See Adapters.

Message shape

The node speaks bare sensor_msgs/JointState in both directions. ros2_control and MoveIt expect a JointTrajectory-shaped controller interface and a URDF, so that route needs a converter node between them.
The lease is first come and unauthenticated. Run this on a trusted network.

SO-101

Read joint travel from a LeRobot calibration instead of typing it in.

Safety

The checks a command passes before it reaches the motors.