ROS2一站通指南
温馨提示: ros2的安装既可以用vm开创一个虚拟机
ros2的安装也可以用双系统
想看这两个方式有什么区别的请看:区别一览
基础知识
-
下载vmtools(这样就能使主机和虚拟机相互粘贴复制):
sudo apt update sudo apt install open-vm-tools open-vm-tools-desktop -y sudo reboot 在 VMware 中启用剪贴板共享: 打开 VMware Workstation/Player 选择你的 Ubuntu 虚拟机 进入 虚拟机 → 设置(Settings) → 选项(Options) 选择 共享功能(Guest Isolation) 勾选: ✅ 启用拖放(Enable Drag and Drop) ✅ 启用剪贴板共享(Enable Copy and Paste) 保存并重启 Ubuntu
-
linux:常见命令
ls(list):列出目录及文件名
-a :全部的文件,连同隐藏文件( 开头为 . 的文件) 一起列出
-d :仅列出目录本身,而不是列出目录内的文件数据
-l :长数据串列出,包含文件的属性与权限等等数据;
eg: ls -al将目录下的所有文件列出来(含属性与隐藏档)
cd(change directory):切换目录
mkdir(make directory):创建一个新的目录
cp(copy file):复制文件或者目录
rm(remove):删除文件或目录,如果要删除文件夹,则使用rm -R 文件夹,
mv(move file):移动或者修改文件或目录
sudo apt install:安装app
cd…:回到上一级
wget http://fishros.com/install -O fishros && . fishros #鱼香大佬的一键安装
温馨提示:本篇文章的所有代码都是在vscode中执行的,详细下载教程已经在开头给的ubuntu安装ros的连接中有!
-
每次使用工作空间都要source一下(但是不需要每次都colcon build,除非你有对代码进行改动,才需要colcon build使代码生效)
#如果不想每次都source环境变量,就在终端输入 gedit ~/bashrc #然后划到最后一行,加入该功能包的环境变量,比如我的功能包的环境变量放在主文件夹下的dev_ws/install/localsetup.sh,那么就写入 source ~/dev_ws/install/localsetup.sh --merge #这样就算随便打开一个终端,都写入了该功能包的环境变量。
这样就算随便打开一个终端,都写入了该功能包的环境变量。
节点的创建:
以下是一个节点创建的例子:import rclpy #ros2的python 接口库 from rclpy.node import Node #ros2节点类 import time #加入时间模块,使hello world可以重复打印 def main(args=None) #ros2节点主入口main函数 rclpy.init(args=args) #ros2 Python 接口初始化 node=Node('node_helloworld') #创建ros2节点对象并初始化 while rclpy.ok(): #ros2系统是否正常进行 node.get_logger().info('helloworld') #ros2日常输出 time.sleep(0.5) #休眠控制循环时间 node.destroy_node() #摧毁节点对象 rclpy.shutdown() #关闭ros2的python接口
注:为了通过ros2 run 命令来执行,需要在功能包里面做一个程序入口的配置,在功能包里有一个setup点,在这个里面有一个程序入口的配置,每增加一个功能就要在里面增加一个节点,
综上所述:
colcon build #打开工作台 source install/local_setup.sh #加载环境变量 ros2 run....(运行相应文件的节点)
-
节点:一个节点往往是一个可执行程序(c++,python等),负责执行一个特定的单一任务,比如发送图像数据的节点,控制车辆运动的节点。
下面示例一个机器视觉的节点:
colcon build source install/local_setup.sh ros2 run learning_node node_object #注意修改图片的路径,节点运行的图片路径要和vs中的节点路径一致。
机器人应该有眼睛,接下来用节点读取摄像头的图像动态(首先要打开虚拟机的可移动设备的摄像头:
ros2 run (节点所在的文件)(节点的名称)
-
话题:两个节点通信,最重要的方式就是话题(topic),话题可以看作是一个数据流,此时引入发布、订阅的概念,数据的生产者称为发布,数据的接受叫做订阅,话题上的数据是实时传输的,如果没有及时接受,数据就会丢失,话题可以看作是不同节点之信息传递的桥梁。(是.msg文件)
####基本概念:发布,订阅,服务,通信接口,动作,参数,分布式通信,launch
-
发布:信息的生产者。
一个简单的发布应该包含以下数据
如果想把运行的节点画出来可以使用
rqt_graph
拿摄像头举例,发布手段既可以通过opencv,也可以用ros自带的
#opencv版 ros2 run lerning_topic topic_webcam_pub(使用lerning_topic文件夹下的topic_webcam_pub节点) #ros版 ros2 run usb_cam usb_cam_node_exe
订阅:信息的消费者。
一个简单的订阅应该包含以下数据
服务:节点间的你问我答。引申出客户端/服务端(cs)x模型,服务器是唯一的,但是客户端可以有多个,客户端向服务端发送请求,服务端将结果反馈给客户端。.srv
文件定义了请求和响应的具体字段类型,ROS会根据 .srv
文件自动生成编程语言(如Python、C++)的代码,开发者可以直接使用这些代码处理服务通信。
创建服务端的流程
以下示例一个通过服务实现一个加法求解器的功能:
#加法服务器的服务端程序:
import rclpy #导入ROS 2 Python客户端库的核心模块
from rclpy.node import Node #从rclpy.node模块导入Node基类,所有ROS节点都继承自此类
from learning_interface.srv import AddTwoInts #从自定义服务接口中导入AddTwoInts服务类型
class AddServer(Node): # 定义服务端节点类,继承自Node
def __init__(self, name):
super().__init__(name)#继承父类Node,在此基础上改写需要用super
self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.adder_callback)
#srv文件创建服务,包含3个参数分别为AddInts(这个其实就是定义两个数a,b,sum方便后面用a,b代替request,respond,计算sum),add_two_ints(服务的唯一标识符(名称),客户端通过此名称找到服务端),self.adder_callback(求sum)
def adder_callback(self, request, response):
response.sum = request.a + request.b
self.get_logger().info(f'Received request: a={request.a}, b={request.b}')
return response
# 程序主入口函数
def main(args=None):
rclpy.init(args=args) # 初始化ROS 2 Python客户端库
node = AddServer('service_adder_server') # 创建服务端节点实例,参数 'add_two_ints_server' 是节点名称,需要符合命名规范
rclpy.spin(node) #spin() 会阻塞当前线程,直到收到终止信号
node.destroy_node() # 保持节点运行,等待服务请求,
rclpy.shutdown() # 关闭ROS 2 Python客户端库
#客户端:
import sys
import rclpy
from rclpy.node import Node
from learning_interface.srv import AddTwoInts #自定义的服务接口
class AdderClient(Node):
def __init__(self, name):
super().__init__(name)
self.client = self.create_client(AddTwoInts, 'add_two_ints') # 服务名与服务器端一致
while not self.client.wait_for_service(timeout_sec=1.0):
self.get_logger().info('service not available, waiting again...')
self.request = AddTwoInts.Request()
def send_request(self):
try:
self.request.a = int(sys.argv[1]) #sys为内置函数,将终端里输入的参数传送
self.request.b = int(sys.argv[2])
except IndexError:
self.get_logger().error('Please provide two integers as arguments!')
return
except ValueError:
self.get_logger().error('Arguments must be integers!')
return
self.future = self.client.call_async(self.request)
def main(args=None):
rclpy.init(args=args)
node = AdderClient('service_adder_client')
node.send_request()
while rclpy.ok(): #一些错误处理
rclpy.spin_once(node)
if node.future.done():
try:
response = node.future.result()
except Exception as e:
node.get_logger().error(f'Service call failed: {e}')
else:
node.get_logger().info(f'Result of {node.request.a} + {node.request.b} = {response.sum}')
break
node.destroy_node()
rclpy.shutdown()
通信接口:数据传递的标准结构,是节点之间交换数据的格式协议,它定义了数据的结构和类型,确保不同节点能正确解析彼此发送的信息,通过 .msg
, .srv
, .action
文件,实现跨节点、跨语言的标准化通信。
动作:完整行为的流程管理(.action文件定义),实际上是基于服务和话题的复合通信机制,动作 = 服务(目标/结果) + 话题(反馈) + 取消机制。
动作由三部分组成:
- 目标(Goal)
- 客户端向服务器发送的任务请求(例如:“移动到坐标 (1,2)”)。
- 类似服务的请求(Request),但支持异步执行。
- 反馈(Feedback)
- 服务器在执行过程中持续发送的进度信息(例如:“当前已移动 50%”)。
- 类似话题的持续发布(Publisher)。
- 结果(Result)
- 任务完成后的最终输出(例如:“已成功到达目标位置”)。
- 类似服务的响应(Response)。
实操指南:学了这么多干货了,纸上得来终觉浅,绝知此事要躬行,下面来一段完整的ros2项目流程演示:通过代码实现动作的编程,接下来以客户端发送一个指令控制机器人运动,并且进行周期反馈,最后反馈整体结束动作的一个信息。
工作区构建 :
#1:接口定义(要在.action文件夹中写!此处定义在robot_action/Moverobot.action文件中):
float32 target_position # 目标:机器人需要移动到的目标位置(单位:米)
---
float32 final_position # 结果:最终实际到达的位置
---
float32 current_position # 反馈:当前已移动的距离
---
#2:在srv文件夹中的Cmakelist.txt里添加这个action文件(承接操作一的那个.action文件)
#3:动作服务器(文件位置为robot_action/robot_action_client.py)
import rclpy
from rclpy.action import ActionServer
from rclpy.node import Node
from robot_action.action import MoveRobot
import time
class RobotActionServer(Node):
def __init__(self):
super().__init__('robot_action_server')
self._action_server = ActionServer(
self,
MoveRobot, #接口类型
'move_robot', #动作名
self.execute_callback #回调函数
)
def execute_callback(self, goal_handle): #执行收到目标后的处理函数
self.get_logger().info('开始执行移动任务...')
target = goal_handle.request.target_position
current_position = 0.0
step = 0.1 # 模拟每次移动的步长
# 发布反馈
while current_position < target:
current_position += step
if current_position > target:
current_position = target
self.get_logger().info(f'当前位置: {current_position:.2f} 米')
goal_handle.publish_feedback(MoveRobot.Feedback(current_position=current_position))
time.sleep(0.5) # 模拟移动耗时
goal_handle.succeed()
result = MoveRobot.Result()
result.final_position = current_position
self.get_logger().info('任务完成!')
return result
def main(args=None):
rclpy.init(args=args)
action_server = RobotActionServer()
rclpy.spin(action_server)
action_server.destroy_node()
rclpy.shutdown()
#4:动作客户端
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
from robot_action.action import MoveRobot
import sys
class RobotActionClient(Node):
def __init__(self):
super().__init__('robot_action_client')
self._action_client = ActionClient(self, MoveRobot, 'move_robot') #创建动作客户端的接口类型,动作名。
def send_goal(self, target_position):
goal_msg = MoveRobot.Goal()
goal_msg.target_position = target_position #传入目标
self._action_client.wait_for_server()
self._send_goal_future = self._action_client.send_goal_async( #异步方式发送动作的目标
goal_msg, #动作目标
feedback_callback=self.feedback_callback #处理周期反馈的回调函数
)
self._send_goal_future.add_done_callback(self.goal_response_callback) #设置一个服务器收到目标之后的反馈时的回调函数
def goal_response_callback(self, future):
goal_handle = future.result()
if not goal_handle.accepted: #如果动作被拒绝执行
self.get_logger().info('目标被拒绝!')
return
self.get_logger().info('目标已接受,开始移动...')
self._get_result_future = goal_handle.get_result_async() #异步获取动作最终执行的结果反馈
self._get_result_future.add_done_callback(self.get_result_callback) #设置一个收到最终结果的回调函数
def feedback_callback(self, feedback_msg): #周期处理函数
feedback = feedback_msg.feedback
self.get_logger().info(f'反馈: 当前已移动 {feedback.current_position:.2f} 米')
def get_result_callback(self, future): #最终结果反馈函数
result = future.result().result
self.get_logger().info(f'最终位置: {result.final_position:.2f} 米')
rclpy.shutdown()
def main(args=None):
rclpy.init(args=args)
if len(sys.argv) < 2:
print("请输入目标位置(例如:5.0)")
return
target_position = float(sys.argv[1])
action_client = RobotActionClient()
action_client.send_goal(target_position)
rclpy.spin(action_client)
ros2 action list #如果在终端中想查看有哪些ros动作正在执行,可以用这个
ros2 action info /执行的动作 #这个可以更进一步查看关于这个动作的详细情况
参数:是节点(Node)的可配置变量,允许在运行时动态调整节点的行为。参数通过 参数服务器(Parameter Service) 集中管理,支持动态查询、修改和持久化存储。
参数的使用步骤:
1:在节点代码中声明参数并定义默认值:
class MyNode(Node):
def __init__(self):
super().__init__('my_node')
# 声明参数并设置默认值
self.declare_parameter('my_int_param', 42)
self.declare_parameter('my_string_param', 'hello')
2:读取参数值:在代码中通过 get_parameter()
读取参数:
# 读取参数值
my_int = self.get_parameter('my_int_param').value
my_str = self.get_parameter('my_string_param').value
3:监听参数变化(可选),为参数注册回调函数,当参数被修改时触发(注意:只能修改数值,不能修改类型):
# 注册参数变更回调
self.add_on_set_parameters_callback(self.param_callback)
def param_callback(self, params):
for param in params:
if param.name == 'my_int_param':
self.get_logger().info(f'参数 {param.name} 已修改为: {param.value}')
return rclpy.Parameter.SetParametersResult(successful=True)
一些tips:
ros2 param list #查看节点参数列表
ros2 param get /my_node my_int_param #此处以int型的参数举例
ros2 param set /my_node my_int_param 100 #把我的参数修改为100
一个完整的参数代码实现:
import rclpy
from rclpy.node import Node
from rcl_interfaces.msg import SetParametersResult
class ParamDemoNode(Node):
def __init__(self):
super().__init__('param_demo')
# 声明参数
self.declare_parameter('speed', 1.0)
self.declare_parameter('enable_motor', True)
self.declare_parameter('sensor_ids', [1, 2, 3])
# 注册参数回调(外部修改参数就会触发)
self.add_on_set_parameters_callback(self.param_callback)
# 定时器读取参数(定期检查参数是否变化)
self.timer = self.create_timer(2.0, self.print_params)
def param_callback(self, params):
for param in params:
self.get_logger().info(f'参数 {param.name} 被修改为: {param.value}')
return SetParametersResult(successful=True)
def print_params(self):
speed = self.get_parameter('speed').value
enable = self.get_parameter('enable_motor').value
ids = self.get_parameter('sensor_ids').value
self.get_logger().info(f'当前参数: speed={speed}, enable={enable}, ids={ids}')
def main(args=None):
rclpy.init(args=args)
node = ParamDemoNode()
rclpy.spin(node)
rclpy.shutdown()
if __name__ == '__main__':
main()
分布式通信:多计算平台的任务分配,因为机器人功能繁多,有时候会遇到算力不够的情况,这个时候把任务分给多个计算机平台,也就是说把节点分配给不同的计算机里,就可以实现多计算机平台的任务分配。
eg:可以桥接一个板子,然后配置网络,要保持使用同一个网络,然后就能像之前的发布者订阅者一样,一个端口是本机端,一个是芯片端(前提是要输入id名),进行信息的沟通。
由上可知,只要网络相同,就能实现ros里的话题通讯,问题是,如果有很多计算机都是用同一个网络,但是不希望他们可以互相联通,就可以分组通讯。以下为步骤:
1:打开一个终端输入
vi bashrc(划到最后一行输入export ROS_DOMAIN_ID=随便设置一个数)
source .bashrc #使脚本生效
运行节点
2:打开另一个终端:
输入id号
如果直接运行节点,不会接收到上面那个终端发出的消息,只有修改脚本才行:
vi。bashrc(设置同样的数,同上)
source .bashrc
高级用法示例:用计算机或者芯片去连接一个摄像头,采集实时图像,通过分布式节点将图像发送给处理节点
DDS:在 ROS2 中,底层通信的核心机制确实是基于 DDS(Data Distribution Service) 实现的。DDS 是 ROS2 默认的通信中间件,负责节点之间的发现、数据传输和服务调用。最大的优点就是实时性。
DDS 的关键配置:ROS2 允许通过 QoS 策略优化通信行为,例如:
- 可靠性(Reliability):
RELIABLE
:确保数据不丢失(适用于关键指令)。BEST_EFFORT
:允许丢包(适用于高频传感器数据)。
- 持久性(Durability):
VOLATILE
:仅发送给在线的订阅者。TRANSIENT_LOCAL
:缓存数据供离线节点恢复后使用
下面示例演示如何通过自定义 QoS 配置和 DDS 实现优化节点通信:
#发布者节点:
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from rclpy.qos import QoSProfile, QoSDurabilityPolicy, QoSReliabilityPolicy, QoSHistoryPolicy
class VideoPublisher(Node):
def __init__(self):
super().__init__('video_publisher')
# 自定义 QoS:允许丢包 + 缓存最近5帧
custom_qos = QoSProfile(
depth=5, # 队列深度
reliability=QoSReliabilityPolicy.BEST_EFFORT, # 允许丢包
durability=QoSDurabilityPolicy.TRANSIENT_LOCAL, # 为新订阅者保留缓存
history=QoSHistoryPolicy.KEEP_LAST # 保留最后N条消息
)
self.publisher = self.create_publisher(Image, 'video_frames', custom_qos)
self.timer = self.create_timer(0.033, self.publish_frame) # 30Hz发布
def publish_frame(self):
msg = Image()
# 填充模拟图像数据(此处简化)
msg.data = bytes([i % 256 for i in range(640 * 480 * 3)])
self.publisher.publish(msg)
self.get_logger().info('发布一帧图像')
def main(args=None):
rclpy.init(args=args)
node = VideoPublisher()
rclpy.spin(node)
rclpy.shutdown()
#订阅者节点
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from rclpy.qos import QoSProfile, QoSReliabilityPolicy
class VideoSubscriber(Node):
def __init__(self):
super().__init__('video_subscriber')
# 匹配发布者的 QoS
custom_qos = QoSProfile(
depth=5,
reliability=QoSReliabilityPolicy.BEST_EFFORT
)
self.subscription = self.create_subscription(
Image,
'video_frames',
self.frame_callback,
custom_qos
)
self.get_logger().info('已订阅视频流')
def frame_callback(self, msg):
self.get_logger().info(f'收到图像尺寸: {len(msg.data)} 字节')
def main(args=None):
rclpy.init(args=args)
node = VideoSubscriber()
rclpy.spin(node)
rclpy.shutdown()
launch:多节点启动和配置脚本。
由于之前说过构造工作区间把环境变量加载到.bashrc中,后面发现会导致环境变量错误,用不了launch,所以在这里要
sudo apt install ros-jazzy-joint-state-publisher
sudo apt install ros-jazzy-xacro
source 环境就能用了
from launch import LaunchDescriptiopn 定义启动流程,它相当于一个容器,将需要启动的节点、参数配置、条件逻辑等操作组合成一个完整的启动描述。
from launch_ros.action import Node #引入节点配置
get generate_launch(): #自动生成launch文件的函数
return LaunchDescription( #分会launch文件的描述性息
[
Node( #配置一个节点的启动
package='learning_helloworld' , #节点所在的功能包
executable='topic_hellowolrd_pub' #节点的可执行文件
)
NOde(
package='learning_helloworld',
executable='topic_helloworld_sub'
)
]
)
注意:要将文件保存为(.launch.py)文件
偷懒小方法:当你编写了一套机器代码,但你开发另一套机器人的时候,就可以直接重映射重命名,此时就直接进入.launch文件,对资源进行重命名,以下为示例:
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description(): # 定义统一的重映射规则
common_remappings = [
("/camera/image", "/acd/image"), #把/camera/image这个话题改成了/acd/image
("/detect_object", "/ai/detect") #把/detect_object改成了/ai/detect
]
return LaunchDescription([
Node(
package="my_package",
executable="camera_node",
remappings=common_remappings
),
Node(
package="my_package",
executable="ai_processor_node",
remappings=common_remappings # 此处引用了函数,将话题成功修改
)
])
此外:在launch文件中,还能加入参数的使用,还能同时启动其他功能包里面的launch文件,来做到同时启动多个launch文件
下面举例一段代码,同时实现参数和多launch文件的功能(启动一个机器人系统,包含底盘和摄像头,当然我这里是软件模拟,无需真实连接摄像头和地盘,通过终端查看进程的信息,后续在gazebo仿真的时候可以用虚拟机器人来模拟真实情况):
代码很长,慎看!:
1:首先,为大家梳理这个项目所需文件的构建框架:这些功能包的构建 不熟悉的可以回顾文章最上面,有写怎么构建功能包的,构建功能包完毕之后,大家可以colcon build 一下,要是不想每次都加载环境变量,可以直接加到.bashrc中。
2:节点代码编写;
camera:
import rclpy
from rclpy.node import Node #导入ROS2核心库
from sensor_msgs.msg import Image
from cv_bridge import CvBridge #导入ROS图像消息类型和OpenCV转换工具
import numpy as np# 导入数学和图像处理库
class VirtualCameraNode(Node):
def __init__(self):
super().__init__('virtual_camera')
self.declare_parameter('publish_rate', 1.0) # 默认1Hz
self.declare_parameter('resolution', '640x480') # 默认分辨率
# 读取参数值(自动转换为浮点数和字符串)
rate = self.get_parameter('publish_rate').value
resolution = self.get_parameter('resolution').value
self.width, self.height = map(int, resolution.split('x')) #将分辨率字符串分割为宽高数值(例如 "1280x720" → 1280 和 720)
# ROS通信设置,创建图像发布者,话题名为 'virtual_image',队列长度10
self.publisher = self.create_publisher(Image, 'virtual_image', 10)
# 创建定时器,周期为 1/rate 秒,触发发布图像的回调函数
self.timer = self.create_timer(1.0 / rate, self.publish_frame)
# 初始化OpenCV到ROS图像的转换器
self.bridge = CvBridge()
# 打印启动日志
self.get_logger().info(f'虚拟摄像头启动: {resolution},{rate}Hz')
def publish_frame(self): #定时器回调函数:生成并发布虚拟图像
# 生成随机噪声图像(模拟摄像头画面),尺寸为 height x width x 3通道(RGB),数据类型为8位无符号整数
frame = np.random.randint(0, 255, (self.height, self.width, 3), dtype=np.uint8)
# 将OpenCV图像转换为ROS图像消息
msg = self.bridge.cv2_to_imgmsg(frame, encoding='bgr8')
# 设置消息时间戳为当前ROS时间
msg.header.stamp = self.get_clock().now().to_msg()
self.publisher.publish(msg)
# 打印调试信息(带时间戳)
self.get_logger().info(f'发布虚拟图像: {msg.header.stamp.sec}.{msg.header.stamp.nanosec}')
def main(args=None):
rclpy.init(args=args)
node = VirtualCameraNode()
rclpy.spin(node) # 保持节点运行,直到收到终止信号
rclpy.shutdown()
chassis(底盘):
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist #导入速度指令消息类型
class VirtualChassisNode(Node):
def __init__(self):
super().__init__('virtual_chassis')
self.declare_parameter('max_speed', 1.0) #声明最大速度参数,默认值1.0 m/s
self.max_speed = self.get_parameter('max_speed').value # 读取参数值
self.subscription = self.create_subscription( # ROS通信设置,创建速度指令订阅者,话题名为 'cmd_vel',队列长度10
Twist, # 消息类型
'cmd_vel', # 话题名称
self.velocity_callback, # 回调函数
10 # 队列长度
)
self.get_logger().info(f'虚拟底盘启动,最大速度: {self.max_speed} m/s')
def velocity_callback(self, msg): #速度指令回调函数
actual_speed = min(abs(msg.linear.x), self.max_speed) # 计算实际执行速度(不超过最大速度限制)
direction = '前进' if msg.linear.x >=0 else '后退' # 判断运动方向
self.get_logger().info(
f'收到速度指令: {direction} {actual_speed:.2f} m/s'
)
def main(args=None):
rclpy.init(args=args)
node = VirtualChassisNode()
rclpy.spin(node)
rclpy.shutdown()
3:camera.launch.py
from launch import LaunchDescription
from launch_ros.actions import Node
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
def generate_launch_description():
return LaunchDescription([
DeclareLaunchArgument(
'camera_res',
default_value='1280x720',
description='摄像头分辨率'
),
DeclareLaunchArgument(
'camera_fps',
default_value='30.0',
description='帧率 (Hz)'
),
Node(
package='virtual_camera',
executable='virtual_camera_node',
name='virtual_camera',
output='screen',
parameters=[{
'resolution': LaunchConfiguration('camera_res'),
'frame_rate': LaunchConfiguration('camera_fps')
}]
)
])
chassis.launch.py
from launch import LaunchDescription
from launch_ros.actions import Node
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
def generate_launch_description():
return LaunchDescription([
DeclareLaunchArgument(
'max_speed',
default_value='2.0',
description='最大线速度 (m/s)'
),
Node(
package='virtual_chassis',
executable='virtual_chassis_node',
name='virtual_chassis',
output='screen',
parameters=[{
'max_speed': LaunchConfiguration('max_speed')
}]
)
])
主launch
from launch import LaunchDescription, actions, conditions
from launch.substitutions import LaunchConfiguration
from launch_ros.substitutions import FindPackageShare
def generate_launch_description():
pkg_share = FindPackageShare('virtual_robot') # 复用包路径查询
return LaunchDescription([ #包含的内容
actions.DeclareLaunchArgument('global_max_speed', default_value='2.0'), #将底盘的默认速度设置为2.0
actions.DeclareLaunchArgument('enable_camera', default_value='true'), #将摄像头的状态默认设置为开启
# 底盘模块
actions.IncludeLaunchDescription( #引用路径
launch_description_source=actions.PythonLaunchDescriptionSource([
pkg_share, '/launch/chassis.launch.py']), #找到/launch/chassis.launch.py这个python文件
launch_arguments={'max_speed': LaunchConfiguration('global_max_speed')}.items()
),
# 摄像头模块(条件启动)
actions.IncludeLaunchDescription(
launch_description_source=actions.PythonLaunchDescriptionSource([
pkg_share, '/launch/camera.launch.py']),
launch_arguments={
'publish_rate': '5.0',
'resolution': '1280x720'
}.items(),
condition=conditions.IfCondition(LaunchConfiguration('enable_camera')) #ifcondition相当于if
)
])
可视化软件的使用:
TF2:作用:
-
坐标变换:
-
自动计算任意两个坐标系之间的变换关系
例如:已知机械臂基座到摄像头的变换,摄像头到目标的变换,自动计算机械臂基座到目标的变换
-
-
时间追溯:
- 保留历史坐标数据(默认10秒)
- 可以查询过去某个时刻的坐标系关系
-
多坐标系管理:
- 维护坐标系之间的树状结构
- 自动检测坐标系循环依赖等错误
安装tf2(我的是jazzy版本,如果是其他版本的可以从其他地方找教程):
sudo apt update && sudo apt upgrade -y
sudo apt install ros-jazzy-tf2-ros ros-jazzy-tf2-tools
sudo apt install ros-jazzy-tf2-py
sudo apt install ros-jazzy-rviz2
#验证安装:
# 终端1:发布静态坐标变换
ros2 run tf2_ros static_transform_publisher 0 0 0.5 0 0 0 base_link camera_link
# 终端2:查看实时变换
ros2 run tf2_ros tf2_echo base_link camera_link
#输出结果应该是:At time 123456.789
- Translation: [0.000, 0.000, 0.500]
- Rotation: in Quaternion [0.0, 0.0, 0.0, 1.0]
使用tf2:创建两个坐标系:parent_link
和 child_link
。通过广播变换来定义它们之间的位置关系。然后,我们将创建一个监听器来获取这个变换并打印出来
import rclpy # 导入 ROS 2 Python 客户端库
from rclpy.node import Node # 导入 ROS 2 节点类
import tf2_ros # 导入 tf2_ros 库,用于处理坐标系变换
import geometry_msgs.msg # 导入几何消息类型,用于描述变换
class TfBroadcaster(Node):
def __init__(self):
super().__init__('tf_broadcaster') # 初始化 ROS 2 节点,节点名为 'tf_broadcaster'
self.broadcaster = tf2_ros.TransformBroadcaster(self) # 创建一个变换广播器对象
self.timer = self.create_timer(0.1, self.broadcast_transform) # 每 0.1 秒广播一次变换
def broadcast_transform(self):
t = geometry_msgs.msg.TransformStamped() # 创建一个变换对象
t.header.stamp = self.get_clock().now().to_msg() # 设置当前时间戳
t.header.frame_id = "parent_link" # 设置父坐标系为 'parent_link'
t.child_frame_id = "child_link" # 设置子坐标系为 'child_link'
# 设置变换的平移部分(x, y, z)
t.transform.translation.x = 1.0 # 1 米
t.transform.translation.y = 0.0 # 0 米
t.transform.translation.z = 0.0 # 0 米
# 设置旋转部分(单位四元数),此处没有旋转
t.transform.rotation.x = 0.0
t.transform.rotation.y = 0.0
t.transform.rotation.z = 0.0
t.transform.rotation.w = 1.0
# 广播变换
self.broadcaster.sendTransform(t)
class TfListener(Node):
def __init__(self):
super().__init__('tf_listener') # 初始化 ROS 2 节点,节点名为 'tf_listener'
self.tf_buffer = tf2_ros.Buffer() # 创建 tf 缓冲区对象
self.tf_listener = tf2_ros.TransformListener(self.tf_buffer, self) # 创建变换监听器对象
self.timer = self.create_timer(1.0, self.get_transform) # 每 1 秒调用一次获取变换的方法
def get_transform(self):
try:
# 获取 'parent_link' 到 'child_link' 的变换
transform = self.tf_buffer.lookup_transform('parent_link', 'child_link', rclpy.time.Time())
self.get_logger().info('Transform: {}'.format(transform)) # 打印获取的变换
except tf2_ros.TransformException as e:
self.get_logger().warn('Could not get transform: {}'.format(e)) # 如果获取失败,打印警告信息
def main(args=None):
rclpy.init(args=args) # 初始化 ROS 2 系统
broadcaster_node = TfBroadcaster() # 创建广播变换的节点
listener_node = TfListener() # 创建监听变换的节点
rclpy.spin(broadcaster_node) # 启动广播变换的节点
rclpy.spin(listener_node) # 启动监听变换的节点
rclpy.shutdown() # 关闭 ROS 2 系统
if __name__ == '__main__':
main() # 执行主函数
URDF:ros机器人建模方法,也就是一种机器人的描述方式,向ros介绍这个机器人,机器人一般需要包含joint(关节)还有link(连接体)这两个特征,URDF 是 XML 格式。
link用来定义机器人的各个部分,比如底座、手臂、轮子等,它们具有几何形状和质量等属性。
joint用来定义链接之间的连接方式,比如旋转关节、平移关节等。
origin定义了链接或关节的坐标位置和朝向,axis和limit 定义了关节的运动限制。
eg:一个简单的机器人,保存为.xacro的文件,Xacro是ROS中优化URDF建模的核心工具,通过宏和参数化极大提升了机器人模型的开发效率,它可以定义很多宏,不需要重复定义,在ros中,可以把xacro文件转化为urdf文件,用ros2 run xacro xacro robot.xacro > robot.urdf
<robot name="simple_robot">
<link name="base_link"> #连接体名称
<visual>
<geometry>
<box size="0.5 0.5 0.1"/> #一个体积为0.5*0.5*0.1的盒子
</geometry>
<material name="blue"/> #颜色是蓝色
</visual>
</link>
<!-- 定义关节 -->
<joint name="base_to_arm" type="revolute"> #定义了一个叫base_to_arm的关节,关节的活动方式是旋转
<parent link="base_link"/> #父关节
<child link="arm"/> #子关节
<origin xyz="0 0 0.1" rpy="0 0 0"/> #设置该关节的原点,表示关节的旋转中心坐标
<axis xyz="0 1 0"/> #关节的旋转轴,表示旋转的方向,这里是绕 Y 轴旋转
<limit effort="100" velocity="1.0" lower="0" upper="1.57"/> #设置关节的限制:力矩最大值为 100,最大旋转速度为 1.0,旋转角度范围从 0 到 1.57 弧度(注意这里是弧度制而不是角度值)
</joint>
<!-- 定义第二个链接 -->
<link name="arm">
<visual>
<geometry>
<cylinder radius="0.05" length="0.5"/>
</geometry>
<material name="green"/>
</visual>
</link>
</robot>
Gazebo和moveit2:gazebo是一个三维物理仿真平台,而moveit2是运动规划框架,一般两者搭配使用更加强大。
- Gazebo 单独使用:测试机器人底盘导航算法、验证传感器噪声的影响。
- MoveIt2 单独使用:控制真实机械臂执行抓取任务(无需仿真环境)。
- 协同使用:开发机械臂抓取系统时,先在 Gazebo 中模拟环境,再通过 MoveIt2 规划避障路径
- 安装gazebo:
1:sudo apt-get install curl lsb-release gnupg
2:sudo curl https://packages.osrfoundation.org/gazebo.gpg --output /usr/share/keyrings/pkgs-osrf-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/pkgs-osrf-archive-keyring.gpg] http://packages.osrfoundation.org/gazebo/ubuntu-stable $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/gazebo-stable.list > /dev/null
sudo apt-get update
sudo apt-get install gz-harmonic
3:sudo apt install ros-jazzy-gazebo-*
4:sudo apt-get install ros-jazzy-ros-gz
gz sim --version #查看gazebo的版本
我这里下载的是二进制gazebo harmonic,这是符合jazzy版本的。如果要卸载用
sudo apt remove gz-harmonic && sudo apt autoremove。
因为我的vmware不支持opengl4.3还有gpu渲染,只能每次都强制使用cpu渲染,但是会非常卡顿,如果可以的话,后续安装一个双系统,双系统可以最大程度的发挥gpu的性能。用 LIBGL_ALWAYS_SOFTWARE=1
进行 CPU 渲染。
eg:
LIBGL_ALWAYS_SOFTWARE=1 gz sim my_world.sdf
-
使用gazebo:定义一个简单的 sdf文件(building_robot.sdf)然后将机器人模型加载到 Gazebo 中
<?xml version="1.0" ?> <sdf version="1.10"> <world name="car_world"> <physics name="1ms" type="ignored"> <max_step_size>0.001</max_step_size> <real_time_factor>1.0</real_time_factor> </physics> <plugin filename="gz-sim-physics-system" name="gz::sim::systems::Physics"> </plugin> <plugin filename="gz-sim-user-commands-system" name="gz::sim::systems::UserCommands"> </plugin> <plugin filename="gz-sim-scene-broadcaster-system" name="gz::sim::systems::SceneBroadcaster"> </plugin> <light type="directional" name="sun"> <cast_shadows>true</cast_shadows> <pose>0 0 10 0 0 0</pose> <diffuse>0.8 0.8 0.8 1</diffuse> <specular>0.2 0.2 0.2 1</specular> <attenuation> <range>1000</range> <constant>0.9</constant> <linear>0.01</linear> <quadratic>0.001</quadratic> </attenuation> <direction>-0.5 0.1 -0.9</direction> </light> <model name="ground_plane"> <static>true</static> <link name="link"> <collision name="collision"> <geometry> <plane> <normal>0 0 1</normal> </plane> </geometry> </collision> <visual name="visual"> <geometry> <plane> <normal>0 0 1</normal> <size>100 100</size> </plane> </geometry> <material> <ambient>0.8 0.8 0.8 1</ambient> <diffuse>0.8 0.8 0.8 1</diffuse> <specular>0.8 0.8 0.8 1</specular> </material> </visual> </link> </model> </world> </sdf>
保存文件,导航到保存文件的目录,然后启动模拟器gazebo: gz sim building_robot.sdf
rviz:一个三维可视化显示平台。
下面介绍一些命令的使用
ros2 run rviz2 rviz2 #打开软件
ros2 run rviz2 rviz2 -d (放置.rviz的路径) #这样就能打开rviz的配置页面
RQT:可视化模块化工具
保存文件,导航到保存文件的目录,然后启动模拟器gazebo: gz sim building_robot.sdf
rviz:一个三维可视化显示平台。
下面介绍一些命令的使用
ros2 run rviz2 rviz2 #打开软件
ros2 run rviz2 rviz2 -d (放置.rviz的路径) #这样就能打开rviz的配置页面
RQT:可视化模块化工具
后面在开发无人机仿真的时候会着重讲这些可视化软件的使用。感兴趣的UU们点个关注。