ROS2之话题

功能包的大致流程:setup.py进行注册然后在colcon build进行构建,然后source install/setup.sh 更新环境最后ros2 run 包名 注册名 进行运行

ROS2 中,话题(Topic) 是一种用于节点间 发布/订阅通信 的机制。

可以把话题理解为一个 命名的数据通道

  • 节点(Node) 可以通过 发布者(Publisher) 将消息发送到某个话题上。

  • 其他节点 可以通过 订阅者(Subscriber) 订阅同一个话题,从而接收这些消息。

  • 消息(Message) 是在话题中传递的数据,通常有固定的数据结构(例如:位置、速度、图像等)。

特点

  1. 解耦通信:发布者和订阅者不需要知道对方是谁,只要约定好话题名称和消息类型,就能通信。

  2. 异步传输:发布者发送消息后不需要等待,订阅者在收到时会自动回调。

  3. 多对多关系

    • 一个话题可以有多个发布者。

    • 一个话题也可以有多个订阅者。

    • 所有订阅者都会收到相同的消息副本。

image

image

打开小海龟模拟器,然后输出其节点信息

Subscribers(订阅者)

表示这个节点正在 监听/接收 的话题:

  • /parameter_events:监听参数变化事件。

  • /turtle1/cmd_vel:接收速度控制指令(geometry_msgs/msg/Twist),这个就是你用来控制小乌龟运动的入口。

Publishers(发布者)

表示这个节点会向外 发布 的话题:

  • /parameter_events:发布参数变化事件。

  • /rosout:发布日志信息,供 ros2 topic echo /rosout 查看。

  • /turtle1/color_sensor:发布小乌龟当前位置的颜色(turtlesim/msg/Color)。

  • /turtle1/pose:发布小乌龟的位姿(turtlesim/msg/Pose)。

节点信息概括

订阅者 ——> 话题名 : 消息类型  发布者 ——> 话题名 : 消息类型  

服务端 ——> 服务名 : 服务类型  服务客户端 ——> 服务名 : 服务类型  

动作服务端 ——> 动作名 : 动作类型  动作客户端 ——> 动作名 : 动作类型

一个节点可以包含多个发布者和订阅者,用于将消息发布到不同的话题或者从不同的话题接受消息,如果节点没有该话题的发布者和订阅者那该节点就无法从该话题发布和接收消息

发布者和订阅者之间是异步的,发布与订阅互不等待,由Executor 负责轮询和调度

这些为该节点关联的话题,输出其中一个话题的信息,ros2 topic echo /turtle1/pose,输出小乌龟的位姿的信息

image

ros2 topic info /turtle1/cmd_vel,查看该话题的详细信息,包含了话题类型,该话题发布者和订阅者的数量

image

ROS2 中,interface 的作用是定义和描述节点之间通信所使用的数据结构,包括 消息(msg)服务(srv)动作(action)。它相当于通信的“协议说明书”,规定了话题传输的数据格式、服务的请求与响应格式,以及动作的目标、反馈和结果格式,使得不同节点之间能够在约定的数据结构下进行解耦通信。

ros2 interface 命令是用来 列出和查看 ROS2 中定义的各种通信接口(msg/srv/action),方便我们理解节点通信时的数据结构。

# 查看所有接口(包括消息、服务、动作)
ros2 interface list

# 只看消息类型
ros2 interface list | grep msg

# 只看服务类型
ros2 interface list | grep srv

# 只看动作类型
ros2 interface list | grep action

# 查看某个接口的定义(结构体内容)
ros2 interface show <interface_type>

通过话题发布小说

import rclpy
import requests
from bs4 import BeautifulSoup
from rclpy.node import Node
from example_interfaces.msg import String
from queue import Queue


class NovelPubNode(Node):
    
    def __init__(self,node_name):        
        super().__init__(node_name)
        self.get_logger().info(f'{node_name},start!')
        # 小说行队列
        self.novel_queue = Queue()
        # 发布者创建,话题名是 novel,消息类型是 String,10为消息队列长度,最多能储存10条消息
        self.novel_publisher = self.create_publisher(String,'novel',10)
        # 间隔5秒发布一行小说,设置一个定时器,每 5 秒触发一次 timer_callback 回调函数
        self.create_timer(5,self.timer_callback)
        
    # 回调函数
    def timer_callback(self):
        #每次定时器触发,从队列里取出一行小说。
        if self.novel_queue.qsize() > 0:
            line =self.novel_queue.get()
            msg = String()
            # 把这行小说封装成 String 消息,发布到 novel 话题
            msg.data = line
            self.novel_publisher.publish(msg)
            self.get_logger().info(f'publish {msg}')
            
    # 下载小说并按行分割
    def download(self,url):
        response = requests.get(url)
        response.encoding = 'utf-8'
        soup = BeautifulSoup(response.text, 'html.parser')
        div = soup.find("div", class_="article")
        lines = div.get_text(separator="\n", strip=True).split("\n")
        self.get_logger().info(f'{url} -> {len(lines)}')
        for line in lines:
            self.novel_queue.put(line)

def main():
    rclpy.init()
    node = NovelPubNode('novel_pub_node')
    node.download('https://www.qimao.com/shuku/1747899/')
    rclpy.spin(node)
    rclpy.shutdown()

image

以上便是通过python构建一个发布者逐行发布小说内容,接下来我们构建一个订阅者订阅小说并转换为语音朗读出来

安装两个关键语音库

pip install espeakng

sudo apt install espeak-ng

msg是一样的,即使可能这两个msg不在同一个进程甚至不在同一个电脑中运行的程序,只要节点在同一个 ROS2 网络(DDS 域)中运行,相同话题下的得到的msg就是一样的

ROS2默认读取的都是系统的python环境,用虚拟的python环境容易出问题,于是我把虚拟环境退出来了,同时发现之前的网站通过反爬机制导致我爬取失败,于是我直接把网站源码下载了下来进行一个读取解析

# 退出虚拟环境

conda deactivate

image

发布者的回调函数靠设置定时器触发,而订阅者的回调函数则是靠接受到消息来进行触发

import rclpy
from bs4 import BeautifulSoup
from rclpy.node import Node
from example_interfaces.msg import String
from queue import Queue
import sys
print("当前 Python 解释器:", sys.executable)

class NovelPubNode(Node):
    def __init__(self,node_name):        
        super().__init__(node_name)
        self.get_logger().info(f'{node_name},start!')
        self.novel_queue = Queue()
        self.novel_publisher = self.create_publisher(String,'novel',10)
        self.create_timer(5,self.timer_callback)
        

    def timer_callback(self):
        if self.novel_queue.qsize() > 0:
            line =self.novel_queue.get()
            msg = String()
            msg.data = line
            self.novel_publisher.publish(msg)
            self.get_logger().info(f'publish {msg}')

    def download(self,url):
        with open(url, "r", encoding="utf-8") as f:
            html = f.read()
        soup = BeautifulSoup( html, 'html.parser')
        div = soup.find("div", class_="article")
        if div:
            lines = div.get_text(separator="\n", strip=True).split("\n")
            self.get_logger().info(f'{url} -> {len(lines)}')
            for line in lines:
                self.novel_queue.put(line)
        else:
             self.get_logger().error(f'{div} -> null\n {soup.getText()}')

def main():
    rclpy.init()
    node = NovelPubNode('novel_pub_node')
    node.download('novel.html')
    rclpy.spin(node)
    rclpy.shutdown()
import rclpy
import espeakng
import threading
import time
from rclpy.node import Node
from example_interfaces.msg import String
from queue import Queue
import sys
print("当前 Python 解释器:", sys.executable)


class NovelSubNode(Node):
    def __init__(self,node_name):        
        super().__init__(node_name)
        self.get_logger().info(f'{node_name},start!')
        self.novel_queue = Queue()
        self.novel_subscription = self.create_subscription(String,'novel',self.novel_callback,10)
        # 创建新的线程来读取小说内容,因为小说队列的更新和读取小说内容应当是同步进行的,如果只有一个线程读取的话,在读小说的同时后续发布的小说内容无法及时接收到队列里面,导致内容丢失
        self.specch_thread = threading.Thread(target=self.speake_thread)
        self.specch_thread.start()

    def novel_callback(self,msg):
        self.novel_queue.put(msg.data)

    def speake_thread(self):
        speaker = espeakng.Speaker()
        speaker.voice = 'zh'
        # ROS 上下文 = ROS 程序运行的全局环境,查看上下文是否正常
        while rclpy.ok():
            if self.novel_queue.qsize() > 0:
                line = self.novel_queue.get()
                self.get_logger().info(f'speake {line}')
                speaker.say(line)
                speaker.wait()
            else:
                time.sleep(1)

def main():
    rclpy.init()
    node = NovelSubNode('novel_sub_node')
    rclpy.spin(node)
    rclpy.shutdown()
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>demo_python_topic</name>
  <version>0.0.0</version>
  <description>TODO: Package description</description>
  <maintainer email="lyy@todo.todo">lyy</maintainer>
  <license>Apache-2.0</license>

  <depend>rclpy</depend>
  <depend>example_interfaces</depend>
  <depend>espeakng</depend>
  <depend>bs4</depend>

  <test_depend>ament_copyright</test_depend>
  <test_depend>ament_flake8</test_depend>
  <test_depend>ament_pep257</test_depend>
  <test_depend>python3-pytest</test_depend>

  <export>
    <build_type>ament_python</build_type>
  </export>
</package>
    entry_points={
        'console_scripts': [
            'novelPubNode = demo_python_topic.novel_pub_node:main',
            'novelSubNode = demo_python_topic.novel_sub_node:main'
        ],
    },

image

C++订阅和发布话题

使用C++创建发布者控制海龟画圈,再创建订阅者订阅海龟的位置实现闭环控制

先采用ROS2命令创建功能包,加上依赖包,入下cmake所示

cmake_minimum_required(VERSION 3.8)
project(demo_cpp_topic)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# find dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(geometry_msgs REQUIRED)
find_package(turtlesim REQUIRED)

add_executable(turtleCircle src/turtle_circle.cpp)

ament_target_dependencies(turtleCircle rclcpp geometry_msgs turtlesim)

if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  # the following line skips the linter which checks for copyrights
  # comment the line when a copyright and license is added to all source files
  set(ament_cmake_copyright_FOUND TRUE)
  # the following line skips cpplint (only works in a git repo)
  # comment the line when this package is in a git repo and when
  # a copyright and license is added to all source files
  set(ament_cmake_cpplint_FOUND TRUE)
  ament_lint_auto_find_test_dependencies()
endif()

install(TARGETS turtleCircle
DESTINATION lib/${PROJECT_NAME}
)

ament_package()

创建发布者,控制海龟画圆,设置角速度控制画圆的大小,设置线速度控制画圆的快慢

bind:C++11 引入的函数适配器,用来 把一个可调用对象(函数/成员函数/函数对象/λ表达式)绑定成另一个函数对象,搭配 std::placeholders::_1, _2, ... 保留一部分参数给以后传入。

#include <rclcpp/rclcpp.hpp>
#include <geometry_msgs/msg/twist.hpp>
#include <chrono>
using namespace std;
using namespace rclcpp;

class TurtlrCircleNode:public Node
{
    private:
    // 一个智能指针,指向一个定时器对象
    TimerBase::SharedPtr timer;
    // 一个智能指针,指向一个发布者对象,其消息类型为geometry_msgs::msg::Twist
    Publisher<geometry_msgs::msg::Twist>::SharedPtr publisher;
    
    public:
    // 构造函数, explicit用于防止隐式格式转换
    explicit TurtlrCircleNode(const string& node_name):Node(node_name){
        publisher = this->create_publisher<geometry_msgs::msg::Twist>("/turtle1/cmd_vel", 10);
        // 1000ms触发一次回调函数将指令发布到话题中
        timer = this->create_wall_timer(1000ms,bind(&TurtlrCircleNode::timer_callback,this));
    }

    void timer_callback(){
        auto msg = geometry_msgs::msg::Twist();
        // 设置线速度和角速度
        msg.linear.x = 1.0;
        msg.angular.z = 0.5;
        publisher->publish(msg);
    }

};

int main(int argc,char** argv){
    init(argc,argv);
    auto node = make_shared<TurtlrCircleNode>("turtle_circle");
    RCLCPP_INFO(node->get_logger(),"turtle_circle start");
    spin(node);
    shutdown();
    return 0;

}

大圆的角速度为0.5,小圆的角速度为1.5

image

#include <rclcpp/rclcpp.hpp>
#include <turtlesim/msg/pose.hpp>
#include <geometry_msgs/msg/twist.hpp>
using namespace std;
using namespace rclcpp;

class TurtlrControlNode:public Node
{
    private:
    Publisher<geometry_msgs::msg::Twist>::SharedPtr publisher;
    Subscription<turtlesim::msg::Pose>::SharedPtr subsciber;
    // 设置目标地点的坐标
    double target_x {1.0};
    double target_y {1.0};
    double k {1.0};
    double max_speed {3.0};
    public:
    explicit TurtlrControlNode(const string& node_name):Node(node_name){
        publisher = this->create_publisher<geometry_msgs::msg::Twist>("/turtle1/cmd_vel", 10);
        // 从话题中订阅得到乌龟的位置信息来进行方向和速度的调整
        subsciber = this->create_subscription<turtlesim::msg::Pose>("/turtle1/pose", 10,bind(&TurtlrControlNode::pose_callback,this,placeholders::_1));
    }

    void pose_callback(const turtlesim::msg::Pose::SharedPtr pose){
        auto current_x = pose->x;
        auto current_y = pose->y;
        RCLCPP_INFO(this->get_logger(),"x=%f y=%f \n",current_x,current_y);
        // 计算欧几里得距离
        auto distance = sqrt((target_x-current_x)*(target_x-current_x)+(target_y-current_y)*(target_y-current_y));
        // 计算需要旋转的角度,通过反三角函数由arctan得到
        auto angle = atan2((target_y-current_y),(target_x-current_x)) - pose->theta;
        auto msg = geometry_msgs::msg::Twist();
        // 当距离很大时调节角度和速度
        if(distance>0.1){
        // 如果角度偏差很大调节角度
            if(fabs(angle)>0.2){
                msg.angular.z = fabs(angle);
            }else{
        // 角度偏差还运训范围内则调剂速度
                msg.linear.x = k*distance;
            }
        }
        // 限制最大速度
        if(msg.linear.x > max_speed){
            msg.linear.x = max_speed;
        }
        // 将调节完的角度和速度发布到话题中控制海龟向目标点移动
        publisher->publish(msg);

    }

};

int main(int argc,char** argv){
    init(argc,argv);
    auto node = make_shared<TurtlrControlNode>("turtle_control");
    RCLCPP_INFO(node->get_logger(),"turtle_control start");
    spin(node);
    shutdown();
    return 0;

}

修改cmake

cmake_minimum_required(VERSION 3.8)
project(demo_cpp_topic)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# find dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(geometry_msgs REQUIRED)
find_package(turtlesim REQUIRED)

# 多功能包配置
add_executable(turtleCircle src/turtle_circle.cpp)
add_executable(turtleControl src/turtle_control.cpp)
ament_target_dependencies(turtleCircle rclcpp geometry_msgs turtlesim)
ament_target_dependencies(turtleControl rclcpp geometry_msgs turtlesim)

if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  # the following line skips the linter which checks for copyrights
  # comment the line when a copyright and license is added to all source files
  set(ament_cmake_copyright_FOUND TRUE)
  # the following line skips cpplint (only works in a git repo)
  # comment the line when this package is in a git repo and when
  # a copyright and license is added to all source files
  set(ament_cmake_cpplint_FOUND TRUE)
  ament_lint_auto_find_test_dependencies()
endif()

install(TARGETS turtleCircle turtleControl
DESTINATION lib/${PROJECT_NAME}
)

ament_package()

image C++成功实现小海龟的闭环控制向目标移动,从上面可以看出创建发布者一般需要的都是三个参数,消息类型,话题名称和消息队列的长度,而订阅者一般会多一个回调函数用于处理接收到的消息。

posted @ 2025-09-23 14:52  突破铁皮  阅读(43)  评论(0)    收藏  举报