ROS2直通车

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文件定义),实际上是基于服务和话题的复合通信机制,动作 = 服务(目标/结果) + 话题(反馈) + 取消机制。

动作由三部分组成:

  1. 目标(Goal)
    • 客户端向服务器发送的任务请求(例如:“移动到坐标 (1,2)”)。
    • 类似服务的请求(Request),但支持异步执行。
  2. 反馈(Feedback)
    • 服务器在执行过程中持续发送的进度信息(例如:“当前已移动 50%”)。
    • 类似话题的持续发布(Publisher)。
  3. 结果(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中。

image-20250307212141599

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:作用:

  1. 坐标变换

    • 自动计算任意两个坐标系之间的变换关系

      例如:已知机械臂基座到摄像头的变换,摄像头到目标的变换,自动计算机械臂基座到目标的变换

  2. 时间追溯

    • 保留历史坐标数据(默认10秒)
    • 可以查询过去某个时刻的坐标系关系
  3. 多坐标系管理

    • 维护坐标系之间的树状结构
    • 自动检测坐标系循环依赖等错误

安装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_linkchild_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们点个关注。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值