news 2026/7/15 8:00:53

ROS话题通信C++实现:从广播电台模型到工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ROS话题通信C++实现:从广播电台模型到工程实践

1. 项目概述:从零理解ROS话题通信的C++实现

如果你刚开始接触ROS,听到“话题通信”这个词可能会觉得有点抽象。简单来说,你可以把它想象成一个“广播电台”系统。在一个机器人系统里,传感器(比如摄像头、激光雷达)就像一个个电台,它们不断地向外广播自己的数据(比如图像、距离信息)。而其他需要这些数据的节点(比如导航、避障程序)就像收音机,可以调到对应的频道去收听这些数据。ROS的话题通信,就是这套广播与收听机制的实现。它最大的特点就是松耦合:发布者只管发,订阅者只管收,双方不需要知道对方是谁、在哪,甚至对方存不存在。这种设计让机器人系统的模块化变得非常容易,也是ROS生态如此繁荣的基石。

而C++,作为ROS官方支持的核心语言之一,以其高性能和系统级控制能力,在需要实时处理、计算密集型的机器人模块(如SLAM、运动控制)中扮演着关键角色。用C++来实现话题通信,意味着你能在最底层高效地操控数据的流动,这对于追求稳定性和性能的机器人应用至关重要。今天,我们就抛开复杂的理论,直接上手,通过一个完整的例子,看看如何用C++在ROS里搭建一个“电台”和“收音机”。

2. 环境准备与工程创建

在开始写代码之前,我们需要一个“工作台”。对于ROS开发来说,这个工作台就是一个Catkin工作空间。Catkin是ROS的官方构建系统,它帮你管理代码的编译、依赖和安装。假设你已经按照“鱼香ROS”等一键安装脚本或者官方教程,在Ubuntu系统上安装好了ROS Noetic或ROS2 Foxy等版本(本文以ROS1 Noetic为例,原理相通),那么接下来就是创建工作空间。

打开终端,我们一步步来。首先,创建一个名为catkin_ws的文件夹,并在其下建立标准的src子目录。所有你自己的ROS功能包(Package)都将放在src里。

mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src

接下来,我们初始化工作空间。这个命令会在src目录下生成一个CMakeLists.txt的链接文件,它是Catkin构建系统的总入口。

catkin_init_workspace

然后回到工作空间根目录,进行第一次编译。即使src里还没有任何代码,这个步骤也能建立起基本的编译环境。

cd ~/catkin_ws catkin_make

编译成功后,最重要的一步是配置环境变量。每次打开新的终端,你都需要让系统知道你的ROS工作空间在哪里。执行下面的source命令即可。为了方便,通常我们会把这条命令加到~/.bashrc文件末尾,这样每次启动终端都会自动配置好。

source devel/setup.bash # 建议将上行命令添加到 ~/.bashrc 文件末尾

现在,工作台搭好了。我们在src目录下创建本次实验的功能包。一个功能包是ROS中组织代码的基本单元。我们给它起名叫cpp_topic_demo,并指明它依赖roscppstd_msgsroscpp是ROS的C++客户端库,是本次实现的核心;std_msgs则包含了像整数、浮点数、字符串这样的标准消息类型。

cd ~/catkin_ws/src catkin_create_pkg cpp_topic_demo roscpp std_msgs

命令执行后,你会看到生成了一个cpp_topic_demo文件夹,里面自动创建了package.xmlCMakeLists.txt两个关键文件。前者声明了包的元信息和依赖,后者则指导如何编译你的C++代码。我们的代码文件将放在这个包下的src目录里。

注意:很多新手会忘记source devel/setup.bash,导致在终端中运行rosrun命令时提示找不到包。这是一个高频踩坑点。请务必确保在每个用于运行ROS命令的终端里,都执行了这条命令,或者已经将其添加到了.bashrc中。

3. 话题通信的核心:消息类型定义与选择

话题通信传递的不是原始字节流,而是有结构的、带类型的消息。你可以把消息理解为一封信的固定格式,比如“信封上必须写明收件人、发件人,信纸内必须按顺序写下:时间戳、数据1、数据2”。ROS提供了丰富的标准消息类型,也允许用户自定义。

对于入门,我们最常用的是std_msgs包里的基础类型。例如:

  • std_msgs::String: 字符串消息
  • std_msgs::Int32: 32位整数消息
  • std_msgs::Float64: 64位浮点数消息
  • std_msgs::Bool: 布尔值消息

这些消息在C++中对应着特定的结构体。比如std_msgs::String,其核心数据成员是一个std::string类型的data。在发布和订阅时,我们操作的就是这些结构体对象。

为什么要有这么严格的消息类型?主要有两个原因:一是序列化,ROS需要将C++结构体转换成能在网络上传输的字节流,统一的格式是序列化和反序列化的前提;二是接口契约,明确的类型定义了发布者和订阅者之间的数据契约,订阅者知道该以何种“格式”去解析收到的数据,避免了混乱。

在我们的第一个例子里,我们将使用std_msgs::String来传递简单的文本信息。这足够简单,能让我们聚焦于通信流程本身。后续当你需要传递更复杂的数据,比如激光雷达的点云(sensor_msgs::PointCloud2)或机器人的关节状态(sensor_msgs::JointState)时,原理是完全一样的,只是消息类型更复杂。

实操心得:在自定义复杂消息时,务必仔细规划消息结构。一个常见的坏习惯是把所有数据塞进一个庞大的自定义消息里。更好的做法是遵循“高内聚、低耦合”的原则,按逻辑划分成多个小消息,通过多个话题发布。这样每个话题职责单一,订阅者可以按需订阅,系统的灵活性和可维护性会高很多。

4. C++发布者节点实现详解

发布者节点的任务很明确:以固定的频率,向某个话题发布消息。我们将在cpp_topic_demo包的src目录下创建第一个C++文件:talker.cpp

4.1 代码结构与ROS初始化

一个ROS C++节点的代码通常遵循固定的模式。首先是包含必要的头文件。

#include “ros/ros.h” // ROS C++ API的核心头文件,必含 #include “std_msgs/String.h” // 我们要使用的字符串消息头文件 #include <sstream> // 用于构造字符串流

ros/ros.h这个头文件几乎包含了编写ROS节点所需的一切,非常方便。接下来是main函数。

int main(int argc, char **argv) { // 初始化ROS节点,指定节点名称为 “talker” // `argc` 和 `argv` 用于解析ROS传入的参数(如 `__name:=` 重命名节点) ros::init(argc, argv, “talker”); // 创建节点句柄。它是你与ROS系统通信的主要入口点。 // 几乎所有ROS操作(创建发布者/订阅者、访问参数等)都需要通过它。 ros::NodeHandle n; // ... 后续代码 }

这里有两个关键对象:

  1. ros::init: 它初始化ROS客户端库,并给你的进程注册一个节点名(这里是“talker”)。在ROS图(ROS Graph)中,每个节点必须有唯一的名字。你可以通过命令行参数__name:=new_name在启动时覆盖这个名字。
  2. ros::NodeHandle: 节点句柄。它是资源管理的核心。创建发布者、订阅者、查询参数、调用服务等操作都依赖于它。一个节点可以有多个句柄,但通常一个就够了。

4.2 创建发布者对象

初始化完成后,我们需要告诉ROS:我这个节点要作为一个发布者,向哪个话题发送什么类型的消息。

// 创建一个发布者对象,通过节点句柄的 `advertise` 方法。 // 模板参数 <std_msgs::String> 指明了要发布的消息类型。 // 第一个参数 “chatter” 是话题的名称。订阅者将通过这个名字来订阅。 // 第二个参数 1000 是消息队列的长度。如果发布速度过快,超过订阅者处理速度, // ROS会缓存最多1000条消息,旧的会被丢弃。根据实时性要求调整此值。 ros::Publisher chatter_pub = n.advertise<std_msgs::String>(“chatter”, 1000);

advertise函数是核心。它向ROS主节点(Master)注册:“我(talker节点)将在‘chatter’话题上发布std_msgs::String类型的消息”。之后,任何订阅了“chatter”话题的节点都能收到通知并与该发布者建立点对点连接。

注意事项:话题名称是字符串,通常使用小写字母和下划线,如“/robot/velocity”。开头的/表示全局命名空间,但通常我们使用相对名称(如“chatter”),ROS会将其解析为节点所在的私有命名空间下,这有利于功能的封装。

4.3 设置发布频率与主循环

机器人控制常常需要定时循环。ROS提供了ros::Rate对象来控制循环频率。

// 设置循环频率为10Hz,即每秒循环10次。 ros::Rate loop_rate(10); int count = 0; while (ros::ok()) { // ... 构造并发布消息 // 按照之前设定的频率进行休眠,确保循环周期稳定。 loop_rate.sleep(); }

ros::ok()是一个非常重要的条件检查。当以下情况发生时,它会返回false,从而退出循环:

  • 收到了Ctrl+C中断信号。
  • 被另一个同名节点踢出网络。
  • ros::shutdown()被调用。
  • 所有节点的ros::NodeHandle都已被销毁。

使用ros::ok()作为循环条件,能确保节点可以优雅地关闭。

4.4 构造与发布消息

在循环体内,我们构造消息内容并将其发布出去。

// 创建一个 std_msgs::String 类型的消息对象 std_msgs::String msg; // 使用 stringstream 方便地构造字符串 std::stringstream ss; ss << “hello world ” << count; // 将字符串流的内容赋值给消息的 data 字段 msg.data = ss.str(); // 在终端打印出将要发布的消息内容,便于调试 ROS_INFO(“%s”, msg.data.c_str()); // 发布消息!调用发布者对象的 publish 方法,传入消息对象。 chatter_pub.publish(msg); // 非常重要!对于订阅者,通常需要调用 ros::spinOnce() 来处理回调。 // 对于纯发布者,虽然不处理回调,但保留此调用是一个好习惯, // 它允许ROS在后台处理一些事件(如响应服务请求)。 ros::spinOnce(); // 计数器递增 ++count;

ROS_INFO是ROS提供的日志宏,功能类似printf,但输出会带有时间戳、节点名和日志级别(INFO、WARN、ERROR等),信息会打印到终端,同时也可以被rosout节点收集,便于分布式调试。

至此,一个完整的发布者节点代码就完成了。它的逻辑非常清晰:初始化 -> 创建发布者 -> 进入循环(构造消息 -> 发布 -> 休眠)。

5. C++订阅者节点实现详解

订阅者节点的核心是回调函数。它向ROS声明:“我对‘chatter’话题感兴趣,一旦有消息到来,请调用我指定的这个函数来处理”。我们创建另一个文件listener.cpp

5.1 回调函数的设计

回调函数是事件驱动编程的典型体现。它的签名是固定的:接收一个指向常亮消息的指针(或引用)作为参数。

#include “ros/ros.h” #include “std_msgs/String.h” // 消息回调函数 // 参数是一个指向接收到的消息的常量指针。 // 使用指针可以避免不必要的数据拷贝,提高效率。 void chatterCallback(const std_msgs::String::ConstPtr& msg) { // 当有消息发布到 “chatter” 话题时,这个函数会被自动调用。 // 在这里处理接收到的消息。 ROS_INFO(“I heard: [%s]”, msg->data.c_str()); }

这个函数的内容可以很复杂,比如进行坐标变换、更新地图、做出决策等。这里我们只是简单地将消息内容打印出来。

5.2 订阅者的初始化与自旋

订阅者节点的main函数前半部分和发布者类似。

int main(int argc, char **argv) { ros::init(argc, argv, “listener”); ros::NodeHandle n; // 创建一个订阅者对象,通过节点句柄的 `subscribe` 方法。 // 模板参数 <std_msgs::String> 指明了要订阅的消息类型。 // 第一个参数 “chatter” 是话题的名称,必须与发布者发布的话题名一致。 // 第二个参数 1000 是消息队列长度。如果回调函数处理太慢, // 这里会缓存最多1000条消息,超出的会被丢弃。 // 第三个参数是回调函数指针。当消息到达时,ROS会自动调用此函数。 ros::Subscriber sub = n.subscribe(“chatter”, 1000, chatterCallback); // ros::spin() 是一个阻塞调用,它会进入一个循环,不断地处理消息。 // 一旦有消息到达,它就调用对应的回调函数。 // 它会一直运行,直到节点被关闭(例如按 Ctrl+C)。 ros::spin(); return 0; }

ros::spin()是关键。它让程序停留在这里,等待并处理到来的消息。对于只有订阅者的简单节点,这通常就足够了。对于既订阅又发布,或者需要执行其他周期性任务的节点,我们则需要在循环中调用ros::spinOnce(),就像在发布者节点里做的那样,把消息处理权交给ROS,然后继续执行自己的逻辑。

核心区别解析ros::spin()vsros::spinOnce()

  • ros::spin():阻塞式。调用后,程序将一直停留在此处,专心地、持续地处理到来的消息队列,直到节点关闭。适用于纯事件响应型节点。
  • ros::spinOnce():非阻塞式。调用一次,处理一次当前消息队列中所有已到达的消息,然后立即返回。适用于需要在处理消息的同时,还要执行其他循环任务的节点(如控制循环)。我们通常在while(ros::ok())循环中调用它。

6. 编译配置与系统构建

代码写好了,但计算机还不能直接运行。我们需要告诉Catkin构建系统如何编译它们。这通过修改功能包下的CMakeLists.txt文件来实现。

打开~/catkin_ws/src/cpp_topic_demo/CMakeLists.txt,我们需要关注和修改以下几个部分:

  1. 查找依赖包:确保find_package包含了我们需要的catkinroscppstd_msgs。通常创建包时已自动生成。

    find_package(catkin REQUIRED COMPONENTS roscpp std_msgs )
  2. 声明可执行文件与链接库:在文件后面添加以下内容。

    # 声明我们要生成两个可执行文件:talker 和 listener # 它们分别由 src/talker.cpp 和 src/listener.cpp 编译而来 add_executable(talker src/talker.cpp) add_executable(listener src/listener.cpp) # 指定编译这两个可执行文件时需要链接的库。 # ${catkin_LIBRARIES} 是一个变量,它自动包含了 find_package 中找到的所有库(如 roscpp)。 # target_link_libraries 必须放在 add_executable 之后。 target_link_libraries(talker ${catkin_LIBRARIES}) target_link_libraries(listener ${catkin_LIBRARIES}) # 为可执行文件添加依赖,确保先编译消息生成的头文件(对于自定义消息很重要)。 # 本例中使用标准消息,此条非必须,但加上是好习惯。 add_dependencies(talker ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS}) add_dependencies(listener ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})

配置完成后,回到工作空间根目录进行编译。

cd ~/catkin_ws catkin_make

如果一切顺利,你会在~/catkin_ws/devel/lib/cpp_topic_demo/目录下看到生成的可执行文件talkerlistener。同时,catkin_make命令也会自动帮你source当前工作空间的setup.bash文件,使得新编译的包可以被ROS系统找到。

避坑技巧:编译出错时,首先检查CMakeLists.txt的语法,特别是括号配对和变量名拼写。一个常见的错误是忘记添加target_link_libraries,导致链接时报“未定义的引用”错误。另一个高频错误是修改代码后重新编译,但运行发现还是旧的行为,这可能是因为你开了多个终端,而新终端没有source devel/setup.bash。确保每个运行ROS命令的终端都正确配置了环境。

7. 运行测试与核心工具使用

激动人心的时刻到了,让我们看看通信是否成功。你需要打开三个终端

终端1:启动ROS核心(Master)ROS核心是所有节点进行注册和查找的中央管理器。任何ROS通信都必须在它启动后才能进行。

roscore

看到日志输出,显示started core service [/rosout]等,说明启动成功。这个终端需要一直保持运行。

终端2:运行发布者节点确保此终端已source过工作空间的环境。

# 如果没添加到.bashrc,先source source ~/catkin_ws/devel/setup.bash # 运行节点。格式:rosrun [包名] [可执行文件名] rosrun cpp_topic_demo talker

你应该会看到终端开始每秒打印10条 “hello world X” 的信息。

终端3:运行订阅者节点同样需要确保环境已配置。

source ~/catkin_ws/devel/setup.bash rosrun cpp_topic_demo listener

如果一切正常,这个终端会开始打印 “I heard: [hello world X]”,内容与发布者同步。

恭喜!你已经成功实现了ROS话题通信。但作为一个开发者,不能只满足于“跑通”。我们还需要一些工具来洞察系统内部的状态。

7.1 可视化工具:rqt_graph

在终端中输入rqt_graph,会打开一个图形化工具。它展示了当前ROS系统中所有活跃的节点(椭圆)、话题(方框)以及它们之间的连接关系。你应该能看到一个名为/talker的节点和一个名为/listener的节点,通过一个名为/chatter的话题连接在一起。这张图是调试分布式系统、理解节点拓扑的利器。

7.2 命令行工具:rostopic

rostopic是一组强大的命令行工具,让你可以“窥探”话题上的数据流。

  • rostopic list: 列出当前所有活跃的话题。你应该能看到/chatter/rosout等。
  • rostopic echo /chatter: 实时打印/chatter话题上流动的消息内容。这相当于一个命令行版的订阅者,非常方便用于快速测试和调试。
  • rostopic hz /chatter: 统计并显示/chatter话题上消息的发布频率。它会告诉你发布者是否真的在以10Hz的频率运行。
  • rostopic type /chatter: 显示/chatter话题上传输的消息类型。会输出std_msgs/String

7.3 节点信息工具:rosnode

rosnode用于查看节点信息。

  • rosnode list: 列出所有活跃的节点。应包含/talker,/listener,/rosout
  • rosnode info /talker: 查看/talker节点的详细信息,包括它发布和订阅了哪些话题、提供了哪些服务等。

掌握这些工具,你就能像拥有“透视眼”一样,观察和分析ROS系统的内部运行状态,这对于开发复杂的多节点机器人系统至关重要。

8. 进阶话题与深度优化

掌握了基础实现后,我们可以探讨一些更深入的话题,让你的代码更健壮、更高效。

8.1 消息队列深度与丢包问题

在创建发布者和订阅者时,我们都设置了一个队列长度(第二个参数,如1000)。这是一个关键参数。

  • 对于发布者advertise(“topic”, queue_size)。这个队列用于缓存等待发送给已连接订阅者的消息。如果发布速度持续超过网络发送速度,队列会积压,满后最旧的消息会被丢弃。
  • 对于订阅者subscribe(“topic”, queue_size, callback)。这个队列用于缓存已从网络接收但尚未被回调函数处理的消息。如果回调函数处理太慢,队列会积压,满后新到的消息会被丢弃。

如何设置?

  • 实时性要求高:设为较小的值(如1-10)。这能保证订阅者总是收到最新的消息,但可能因处理不及时而丢包。
  • 数据完整性要求高:设为较大的值(如1000)。这能缓存更多历史数据,但订阅者处理的消息会有延迟。
  • 平衡之道:通常设为1,并确保回调函数执行时间远小于消息发布周期。对于图像等大数据,可能需要结合线程模型或降低频率。

8.2 使用类封装节点

上面的例子将逻辑都写在main函数里,对于简单节点没问题。但对于复杂的、有状态的节点,使用C++类进行封装是更好的实践。它可以将数据成员(如配置参数、状态变量)和成员函数(如回调函数、主循环函数)组织在一起,结构更清晰。

class MyTalkerNode { public: MyTalkerNode() : count_(0) { // 在构造函数中初始化节点句柄、创建发布者等 pub_ = nh_.advertise<std_msgs::String>(“chatter”, 10); timer_ = nh_.createTimer(ros::Duration(0.1), &MyTalkerNode::timerCallback, this); // 使用定时器回调 } void timerCallback(const ros::TimerEvent& event) { std_msgs::String msg; msg.data = “Hello from class, count: “ + std::to_string(count_++); pub_.publish(msg); ROS_INFO_STREAM(“Published: “ << msg.data); } private: ros::NodeHandle nh_; ros::Publisher pub_; ros::Timer timer_; int count_; }; int main(int argc, char** argv) { ros::init(argc, argv, “class_talker”); MyTalkerNode node; // 实例化节点类 ros::spin(); // 进入自旋,等待定时器回调触发 return 0; }

使用类的好处是,你可以方便地在多个回调函数间共享数据,管理生命周期,并且代码可读性和可维护性大大增强。

8.3 线程模型与回调队列

默认情况下,ros::spin()ros::spinOnce()是在单线程中处理所有订阅者回调的。这意味着如果你的某个回调函数执行时间很长(比如处理一帧图像),它会阻塞其他回调函数,甚至影响定时发布的准确性。

ROS提供了多线程回调队列的机制来解决这个问题。你可以创建独立的线程来处理特定的回调。

#include <ros/callback_queue.h> // ... 在节点类中 ... ros::CallbackQueue custom_queue; ros::AsyncSpinner async_spinner(2, &custom_queue); // 创建2个线程的自旋器处理custom_queue async_spinner.start(); // 然后,为某些特定的订阅者或定时器指定使用这个自定义队列 ros::SubscribeOptions ops = ros::SubscribeOptions::create<std_msgs::String>( “chatter”, 10, chatterCallback, ros::VoidPtr(), &custom_queue); ros::Subscriber sub = nh_.subscribe(ops);

这样,chatterCallback就会在独立的线程池中被调用,不会阻塞主线程或其他回调。这对于构建高性能、响应快的机器人系统非常重要。

8.4 消息传递的底层:TCPROS vs UDPROS

ROS默认使用TCPROS协议进行话题通信。它基于TCP,提供可靠、有序的数据流传输,确保消息不丢失、不错序。这对于控制指令、状态信息等关键数据是必要的。

但对于某些对延迟极其敏感、且可以容忍少量丢包的数据(如高频摄像头图像),ROS也支持UDPROS协议。UDP速度更快,开销更小,但不保证可靠传输。要使用UDPROS,需要在发布者和订阅者两端进行特殊配置(通过TransportHints),这属于更高级的用法。

9. 常见问题排查与调试技巧实录

在实际开发中,你一定会遇到各种各样的问题。下面是我踩过的一些坑和对应的解决方法。

问题1:节点启动失败,提示 “ERROR: cannot launch node …: Cannot locate node …”

  • 原因:ROS找不到你的可执行文件。最可能的原因是终端没有source你的工作空间devel/setup.bash
  • 解决:在每个运行rosrunroslaunch的终端里,先执行source ~/catkin_ws/devel/setup.bash,或将其添加到~/.bashrc中。

问题2:编译成功,但运行节点后没有任何输出,或者订阅者收不到消息。

  • 排查步骤
    1. 检查roscore:确保roscore正在运行。用rosnode list查看,应该能看到/rosout
    2. 检查节点:运行rosnode list,看看你的/talker/listener节点是否在列表中。如果不在,可能是节点启动后立即崩溃了,检查终端是否有错误输出。
    3. 检查话题连接:运行rqt_graph,查看节点和话题是否连接正确。或者用rostopic list查看/chatter话题是否存在。
    4. 检查话题数据:在一个新终端运行rostopic echo /chatter。如果这里能收到数据,但你的listener收不到,问题可能出在你的订阅者代码(比如回调函数没注册对,或者ros::spin()没调用)。如果rostopic echo也收不到,问题在发布者。
    5. 检查消息类型:用rostopic type /chatter确认消息类型,确保发布和订阅使用的是完全相同的类型(包括包名和消息名,大小写敏感)。

问题3:回调函数没有被调用。

  • 原因A:忘记了调用ros::spin()ros::spinOnce()。没有它们,ROS不会去处理到达的消息队列。
  • 原因B:在类中使用回调函数时,没有正确绑定this指针。确保使用boost::bind或C++11的std::bind(对于ros::Timer)时,传入了正确的对象指针。
  • 解决:仔细检查main函数或主循环中是否有ros::spin()ros::spinOnce()

问题4:程序编译时出现 “undefined reference to …” 链接错误。

  • 原因CMakeLists.txt中缺少target_link_libraries语句,或者链接的库名称不对。
  • 解决:确保add_executabletarget_link_libraries语句都存在,并且target_link_libraries中包含了${catkin_LIBRARIES}

问题5:消息发布频率不稳定,远低于设定的loop_rate

  • 原因:循环体内的处理(如构造消息、执行某些计算)耗时过长,超过了loop_rate设定的周期。loop_rate.sleep()会尽力补偿,但如果处理时间已经超过周期,它就无法保证频率了。
  • 解决
    • 优化回调函数或处理逻辑的代码,减少耗时。
    • 考虑使用多线程,将耗时的处理放到独立线程中,避免阻塞主发布循环。
    • 对于非严格周期的任务,可以考虑使用ros::Timer来代替ros::Rate循环,定时器在单独的线程中触发回调,精度可能更高。

问题6:在回调函数中尝试发布消息,但似乎没发出去。

  • 原因:直接在回调函数中创建新的ros::Publisher对象并发布,这个发布者可能尚未完成与ROS Master的注册,连接尚未建立。
  • 解决:将ros::Publisher对象作为类的成员变量,在节点初始化时(如构造函数中)创建好。在回调函数中直接使用这个已经初始化好的发布者对象来发布消息。

调试ROS程序,除了看日志,一定要善用rqt_graph,rostopic,rosnode这“三板斧”。它们能帮你快速定位问题是出在节点生命周期、话题连接还是消息流上。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/15 8:00:01

从零到一!数电课设实战:基于74LS161与74LS48的交通灯倒计时系统设计

1. 项目背景与设计目标第一次接触数字电路课程设计的同学&#xff0c;常常会对如何将抽象的理论转化为实际电路感到困惑。这次我们要实现的交通灯倒计时系统&#xff0c;正是数字电路中最经典的实践项目之一。这个系统需要完成30秒红灯、5秒黄灯、20秒绿灯、再5秒黄灯的完整循环…

作者头像 李华
网站建设 2026/7/15 7:57:54

用CDF曲线做服务水位分析:从等待时间到SLA落地的工程实践

1. 项目概述&#xff1a;为什么CDF不是统计课本里的“冷知识”&#xff0c;而是数据工程师每天都在调的“服务水位尺”你有没有遇到过这样的场景&#xff1a;客服主管拍着桌子问&#xff0c;“上个月80%的客户到底等了多久才被接通&#xff1f;”运维同学盯着监控面板发愁&…

作者头像 李华
网站建设 2026/7/15 7:56:25

CANN/asc-devkit:asc_e4m32float数据类型转换API

asc_e4m32float (废弃) 【免费下载链接】asc-devkit 本项目是CANN 推出的昇腾AI处理器专用的算子程序开发语言&#xff0c;原生支持C和C标准规范&#xff0c;主要由类库和语言扩展层构成&#xff0c;提供多层级API&#xff0c;满足多维场景算子开发诉求。 项目地址: https://…

作者头像 李华
网站建设 2026/7/15 7:56:13

解读国标中的IDT、MOD、EQV、NEQ:标准采用程度的技术密码

1. 标准采用程度的技术密码&#xff1a;从IDT到NEQ的实战指南第一次看到技术文档里标注的IDT、MOD、EQV、NEQ时&#xff0c;我完全懵了——这简直像在解密码。后来参与跨国项目才明白&#xff0c;这四个缩写是标准工程师的"通关文牒"。它们直接决定了你的产品设计要不…

作者头像 李华
网站建设 2026/7/15 7:55:09

蓝牙 - 从配对到连接:深入解析设备间的“握手”协议

1. 蓝牙配对&#xff1a;设备间的"初次见面"想象一下两个陌生人在派对上第一次见面。他们需要先自我介绍、交换联系方式&#xff0c;才能开始聊天。蓝牙设备之间的配对过程也类似这种"初次见面"。蓝牙配对本质上是两个设备互相确认身份并建立信任关系的过程…

作者头像 李华
网站建设 2026/7/15 7:53:06

如何高效使用NCMconverter:专业开发者的完整音频格式转换指南

如何高效使用NCMconverter&#xff1a;专业开发者的完整音频格式转换指南 【免费下载链接】NCMconverter NCMconverter将ncm文件转换为mp3或者flac文件 项目地址: https://gitcode.com/gh_mirrors/nc/NCMconverter NCMconverter是一款基于Go语言开发的高效NCM音频格式转…

作者头像 李华