ros2 teleoperate
This commit is contained in:
parent
b568de35ad
commit
504748c430
|
@ -0,0 +1,148 @@
|
|||
# ROS2 Distributed Teleoperation
|
||||
|
||||
This tutorial explains how to set up a distributed teleoperation system using ROS2, allowing a leader robot on one computer to control a follower robot on another computer over a network.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. ROS2 (Foxy or newer) installed on both computers
|
||||
2. Network connectivity between the leader and follower computers
|
||||
3. lerobot package installed on both computers
|
||||
|
||||
## Installation
|
||||
|
||||
Ensure you have the required ROS2 packages for sensor messages:
|
||||
|
||||
```bash
|
||||
pip install sensor_msgs
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Configure ROS2 Domain ID (optional but recommended)
|
||||
|
||||
To isolate your ROS2 network from others, set the same domain ID on both computers:
|
||||
|
||||
```bash
|
||||
# On both leader and follower computers
|
||||
export ROS_DOMAIN_ID=42 # Choose any number between 0-232
|
||||
```
|
||||
|
||||
### 2. Network Configuration
|
||||
|
||||
Ensure both computers can communicate over the network:
|
||||
|
||||
1. Connect both computers to the same network
|
||||
2. Verify they can ping each other
|
||||
3. Make sure any firewalls allow ROS2 communication (default ports)
|
||||
|
||||
## Running the Distributed Teleoperation
|
||||
|
||||
### 1. Leader Robot Computer
|
||||
|
||||
On the computer connected to the leader robot:
|
||||
|
||||
```bash
|
||||
python lerobot/scripts/run_ros2_leader.py \
|
||||
--robot.type=koch \
|
||||
--robot.leader_arms.main.port=/dev/ttyUSB0 \
|
||||
--ros2_config.node_name=lerobot_teleop \
|
||||
--ros2_config.topic_name=joint_states \
|
||||
--ros2_config.publish_rate_hz=200.0
|
||||
```
|
||||
|
||||
Replace `/dev/ttyUSB0` with the actual port of your leader robot.
|
||||
|
||||
### 2. Follower Robot Computer
|
||||
|
||||
On the computer connected to the follower robot:
|
||||
|
||||
```bash
|
||||
python lerobot/scripts/run_ros2_follower.py \
|
||||
--robot.type=koch \
|
||||
--robot.follower_arms.main.port=/dev/ttyUSB0 \
|
||||
--ros2_config.node_name=lerobot_teleop \
|
||||
--ros2_config.topic_name=joint_states
|
||||
```
|
||||
|
||||
Replace `/dev/ttyUSB0` with the actual port of your follower robot.
|
||||
|
||||
### Note on Topic Names
|
||||
|
||||
Make sure both leader and follower use the same topic name (default is `joint_states`).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### 1. Check ROS2 Discovery
|
||||
|
||||
Verify nodes can discover each other:
|
||||
|
||||
```bash
|
||||
# On either computer
|
||||
ros2 topic list
|
||||
```
|
||||
|
||||
You should see `/joint_states` in the list.
|
||||
|
||||
### 2. Monitor Message Flow
|
||||
|
||||
Check if messages are being published:
|
||||
|
||||
```bash
|
||||
ros2 topic echo /joint_states
|
||||
```
|
||||
|
||||
### 3. Network Issues
|
||||
|
||||
If nodes can't discover each other:
|
||||
- Ensure both computers are on the same network
|
||||
- Check if ROS_DOMAIN_ID is set to the same value on both computers
|
||||
- Verify firewall settings
|
||||
- Try using the ROS_LOCALHOST_ONLY=0 environment variable
|
||||
|
||||
### 4. Debug Node Status
|
||||
|
||||
```bash
|
||||
ros2 node list
|
||||
ros2 node info /lerobot_teleop_leader # or follower
|
||||
```
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Quality of Service Settings
|
||||
|
||||
For teleoperation over unreliable networks, you can adjust QoS settings:
|
||||
|
||||
```bash
|
||||
# Use reliable QoS instead of best effort (might increase latency but improve reliability)
|
||||
--ros2_config.use_best_effort_qos=false
|
||||
```
|
||||
|
||||
### Publishing Rate
|
||||
|
||||
Adjust the rate at which joint states are published:
|
||||
|
||||
```bash
|
||||
# On leader robot, set a lower rate for unreliable networks
|
||||
--ros2_config.publish_rate_hz=100.0
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Using with myCobot 280
|
||||
|
||||
For MyCobot robots, ensure you've set up the correct motor types:
|
||||
|
||||
Leader:
|
||||
```bash
|
||||
python lerobot/scripts/run_ros2_leader.py \
|
||||
--robot.type=mycobot \
|
||||
--robot.leader_arms.main.port=/dev/ttyUSB0 \
|
||||
--ros2_config.publish_rate_hz=100.0
|
||||
```
|
||||
|
||||
Follower:
|
||||
```bash
|
||||
python lerobot/scripts/run_ros2_follower.py \
|
||||
--robot.type=mycobot \
|
||||
--robot.follower_arms.main.port=/dev/ttyUSB0
|
||||
```
|
|
@ -0,0 +1,203 @@
|
|||
#!/usr/bin/env python3
|
||||
# Copyright 2024 The HuggingFace Inc. team. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""ROS2 nodes for robot teleoperation between leader and follower robots."""
|
||||
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import numpy as np
|
||||
import rclpy
|
||||
from rclpy.node import Node
|
||||
from rclpy.qos import QoSProfile, QoSReliabilityPolicy
|
||||
from sensor_msgs.msg import JointState
|
||||
import torch
|
||||
|
||||
from lerobot.common.robot_devices.control_utils import busy_wait
|
||||
from lerobot.common.robot_devices.robots.manipulator import ManipulatorRobot
|
||||
from lerobot.common.robot_devices.robots.errors import RobotDeviceNotConnectedError
|
||||
|
||||
|
||||
@dataclass
|
||||
class ROS2TeleopConfig:
|
||||
"""Configuration for ROS2 teleoperation."""
|
||||
node_name: str = "lerobot_teleop"
|
||||
topic_name: str = "joint_states"
|
||||
publish_rate_hz: float = 200.0 # Default publish rate
|
||||
use_best_effort_qos: bool = True # Use best effort QoS for real-time performance
|
||||
|
||||
|
||||
class LeaderNode(Node):
|
||||
"""ROS2 node for the leader robot that publishes joint states."""
|
||||
|
||||
def __init__(self, robot: ManipulatorRobot, config: ROS2TeleopConfig):
|
||||
super().__init__(config.node_name + "_leader")
|
||||
|
||||
self.robot = robot
|
||||
self.config = config
|
||||
|
||||
# Set up QoS profile - for real-time control, best effort is often better than reliable
|
||||
qos = QoSProfile(depth=10)
|
||||
if config.use_best_effort_qos:
|
||||
qos.reliability = QoSReliabilityPolicy.BEST_EFFORT
|
||||
|
||||
# Create publisher
|
||||
self.publisher = self.create_publisher(
|
||||
JointState,
|
||||
config.topic_name,
|
||||
qos
|
||||
)
|
||||
|
||||
# Create timer for publishing at specified rate
|
||||
period = 1.0 / config.publish_rate_hz
|
||||
self.timer = self.create_timer(period, self.publish_joint_states)
|
||||
|
||||
self.get_logger().info(f"Leader node initialized, publishing to {config.topic_name}")
|
||||
|
||||
def publish_joint_states(self):
|
||||
"""Read joint states from leader robot and publish them."""
|
||||
if not self.robot.is_connected:
|
||||
self.get_logger().error("Robot is not connected")
|
||||
return
|
||||
|
||||
# Read positions from all leader arms
|
||||
leader_pos = {}
|
||||
joint_names = []
|
||||
positions = []
|
||||
|
||||
try:
|
||||
for name in self.robot.leader_arms:
|
||||
pos = self.robot.leader_arms[name].read("Present_Position")
|
||||
leader_pos[name] = pos
|
||||
|
||||
# Extend the joint names and positions lists
|
||||
motor_names = self.robot.leader_arms[name].motor_names
|
||||
joint_names.extend([f"{name}_{motor_name}" for motor_name in motor_names])
|
||||
positions.extend(pos.tolist())
|
||||
|
||||
# Create and publish the message
|
||||
msg = JointState()
|
||||
msg.header.stamp = self.get_clock().now().to_msg()
|
||||
msg.name = joint_names
|
||||
msg.position = positions
|
||||
|
||||
self.publisher.publish(msg)
|
||||
except Exception as e:
|
||||
self.get_logger().error(f"Failed to publish joint states: {e}")
|
||||
|
||||
|
||||
class FollowerNode(Node):
|
||||
"""ROS2 node for the follower robot that subscribes to joint states."""
|
||||
|
||||
def __init__(self, robot: ManipulatorRobot, config: ROS2TeleopConfig):
|
||||
super().__init__(config.node_name + "_follower")
|
||||
|
||||
self.robot = robot
|
||||
self.config = config
|
||||
self.joint_positions = {} # Store the latest joint positions
|
||||
|
||||
# Set up QoS profile
|
||||
qos = QoSProfile(depth=10)
|
||||
if config.use_best_effort_qos:
|
||||
qos.reliability = QoSReliabilityPolicy.BEST_EFFORT
|
||||
|
||||
# Create subscription
|
||||
self.subscription = self.create_subscription(
|
||||
JointState,
|
||||
config.topic_name,
|
||||
self.joint_states_callback,
|
||||
qos
|
||||
)
|
||||
|
||||
self.get_logger().info(f"Follower node initialized, subscribing to {config.topic_name}")
|
||||
|
||||
def joint_states_callback(self, msg: JointState):
|
||||
"""Process incoming joint states and command the follower robot."""
|
||||
if not self.robot.is_connected:
|
||||
self.get_logger().error("Robot is not connected")
|
||||
return
|
||||
|
||||
try:
|
||||
# Parse the message and organize by arm name
|
||||
arm_positions = {}
|
||||
|
||||
for i, joint_name in enumerate(msg.name):
|
||||
# Joint names are expected to be in format: "arm_name_motor_name"
|
||||
parts = joint_name.split("_", 1)
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
|
||||
arm_name, _ = parts
|
||||
|
||||
if arm_name not in arm_positions:
|
||||
arm_positions[arm_name] = []
|
||||
|
||||
arm_positions[arm_name].append(msg.position[i])
|
||||
|
||||
# Send positions to follower arms that match leader arms
|
||||
for name, positions in arm_positions.items():
|
||||
if name in self.robot.follower_arms:
|
||||
# Convert to numpy array
|
||||
pos_array = np.array(positions, dtype=np.float32)
|
||||
|
||||
# Store the latest positions
|
||||
self.joint_positions[name] = pos_array
|
||||
|
||||
# Send to the follower robot
|
||||
self.robot.follower_arms[name].write("Goal_Position", pos_array)
|
||||
|
||||
except Exception as e:
|
||||
self.get_logger().error(f"Failed to process joint states: {e}")
|
||||
|
||||
|
||||
def run_ros2_leader(robot: ManipulatorRobot, config: Optional[ROS2TeleopConfig] = None):
|
||||
"""Run the ROS2 leader node until interrupted."""
|
||||
if not robot.is_connected:
|
||||
robot.connect()
|
||||
|
||||
if config is None:
|
||||
config = ROS2TeleopConfig()
|
||||
|
||||
rclpy.init()
|
||||
node = LeaderNode(robot, config)
|
||||
|
||||
try:
|
||||
rclpy.spin(node)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
node.destroy_node()
|
||||
rclpy.shutdown()
|
||||
|
||||
|
||||
def run_ros2_follower(robot: ManipulatorRobot, config: Optional[ROS2TeleopConfig] = None):
|
||||
"""Run the ROS2 follower node until interrupted."""
|
||||
if not robot.is_connected:
|
||||
robot.connect()
|
||||
|
||||
if config is None:
|
||||
config = ROS2TeleopConfig()
|
||||
|
||||
rclpy.init()
|
||||
node = FollowerNode(robot, config)
|
||||
|
||||
try:
|
||||
rclpy.spin(node)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
node.destroy_node()
|
||||
rclpy.shutdown()
|
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env python3
|
||||
# Copyright 2024 The HuggingFace Inc. team. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import sys
|
||||
import logging
|
||||
from pprint import pformat
|
||||
from dataclasses import asdict, dataclass
|
||||
|
||||
from lerobot.common.config import parse_and_overload, parser
|
||||
from lerobot.common.robot_devices.control_configs import ControlPipelineConfig
|
||||
from lerobot.common.robot_devices.robots.utils import make_robot_from_config
|
||||
from lerobot.common.robot_devices.ros2_teleop import ROS2TeleopConfig, run_ros2_follower
|
||||
from lerobot.common.logging_utils import init_logging
|
||||
|
||||
@dataclass
|
||||
class ROS2FollowerConfig:
|
||||
"""Configuration for ROS2 follower teleoperation."""
|
||||
node_name: str = "lerobot_teleop"
|
||||
topic_name: str = "joint_states"
|
||||
use_best_effort_qos: bool = True
|
||||
|
||||
@parser.wrap()
|
||||
def run_follower(cfg: ControlPipelineConfig, ros2_config: ROS2FollowerConfig = None):
|
||||
"""Run a robot as a ROS2 follower that subscribes to joint states."""
|
||||
init_logging()
|
||||
|
||||
if ros2_config is None:
|
||||
ros2_config = ROS2FollowerConfig()
|
||||
|
||||
teleop_config = ROS2TeleopConfig(
|
||||
node_name=ros2_config.node_name,
|
||||
topic_name=ros2_config.topic_name,
|
||||
use_best_effort_qos=ros2_config.use_best_effort_qos
|
||||
)
|
||||
|
||||
logging.info("ROS2 Follower Configuration:")
|
||||
logging.info(pformat(asdict(teleop_config)))
|
||||
logging.info("Robot Configuration:")
|
||||
logging.info(pformat(asdict(cfg.robot)))
|
||||
|
||||
robot = make_robot_from_config(cfg.robot)
|
||||
|
||||
try:
|
||||
run_ros2_follower(robot, teleop_config)
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Follower node stopped by user")
|
||||
finally:
|
||||
if robot.is_connected:
|
||||
robot.disconnect()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_follower()
|
|
@ -0,0 +1,66 @@
|
|||
#!/usr/bin/env python3
|
||||
# Copyright 2024 The HuggingFace Inc. team. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import sys
|
||||
import logging
|
||||
from pprint import pformat
|
||||
from dataclasses import asdict
|
||||
|
||||
from lerobot.common.config import parse_and_overload, parser
|
||||
from lerobot.common.robot_devices.control_configs import ControlPipelineConfig
|
||||
from lerobot.common.robot_devices.robots.utils import make_robot_from_config
|
||||
from lerobot.common.robot_devices.ros2_teleop import ROS2TeleopConfig, run_ros2_leader
|
||||
from lerobot.common.logging_utils import init_logging
|
||||
|
||||
@dataclass
|
||||
class ROS2LeaderConfig:
|
||||
"""Configuration for ROS2 leader teleoperation."""
|
||||
node_name: str = "lerobot_teleop"
|
||||
topic_name: str = "joint_states"
|
||||
publish_rate_hz: float = 200.0
|
||||
use_best_effort_qos: bool = True
|
||||
|
||||
@parser.wrap()
|
||||
def run_leader(cfg: ControlPipelineConfig, ros2_config: ROS2LeaderConfig = None):
|
||||
"""Run a robot as a ROS2 leader that publishes joint states."""
|
||||
init_logging()
|
||||
|
||||
if ros2_config is None:
|
||||
ros2_config = ROS2LeaderConfig()
|
||||
|
||||
teleop_config = ROS2TeleopConfig(
|
||||
node_name=ros2_config.node_name,
|
||||
topic_name=ros2_config.topic_name,
|
||||
publish_rate_hz=ros2_config.publish_rate_hz,
|
||||
use_best_effort_qos=ros2_config.use_best_effort_qos
|
||||
)
|
||||
|
||||
logging.info("ROS2 Leader Configuration:")
|
||||
logging.info(pformat(asdict(teleop_config)))
|
||||
logging.info("Robot Configuration:")
|
||||
logging.info(pformat(asdict(cfg.robot)))
|
||||
|
||||
robot = make_robot_from_config(cfg.robot)
|
||||
|
||||
try:
|
||||
run_ros2_leader(robot, teleop_config)
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Leader node stopped by user")
|
||||
finally:
|
||||
if robot.is_connected:
|
||||
robot.disconnect()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_leader()
|
Loading…
Reference in New Issue