4、Python与C++节点编写实战:Publisher与Subscriber的Python/C++实现,自定义消息接口
好,咱们进入实战环节。
前面几章我们把ROS2的基础概念、环境搭建、工作空间都捋了一遍。现在,是时候亲手写代码了。
这一章的核心就三件事:用Python写一个发布者(Publisher)、用C++写一个订阅者(Subscriber),以及自定义一套自己的消息接口。说白了,就是让两个节点能说上话,而且说的还是我们自己定义的语言。
本章核心目标:
- 掌握Python与C++节点的基本编写流程
- 理解Publisher与Subscriber的通信机制
- 学会自定义msg消息接口
4.1 先搭架子:创建功能包
我个人习惯,每次写新节点前都会先确认工作空间已经source好了。你想想看,如果环境没加载,后面编译报错都不知道错在哪。
咱们在 src 目录下创建一个新功能包,同时依赖 rclcpp 和 rclpy:
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_cmake chapt4_interfaces \
--dependencies rclcpp rclpy std_msgs
嗯,这里要注意:虽然我们后面会写Python节点,但功能包的构建类型我选了 ament_cmake。为什么?因为C++节点和自定义消息接口都需要CMake来编译,Python节点可以放在里面一起管理。这个坑我刚开始踩过——分开建两个包,结果依赖关系搞得乱七八糟。
4.2 自定义消息接口:让数据按我们的规矩来
ROS2自带的 std_msgs/String 虽然能用,但实际项目中远远不够。比如你要发一个带有速度、转向角、时间戳的车辆状态消息,就得自己定义。
在 chapt4_interfaces 包下创建 msg 文件夹,新建 VehicleStatus.msg:
# chapt4_interfaces/msg/VehicleStatus.msg
float32 speed # 车速,单位 m/s
float32 steering_angle # 转向角,单位 rad
builtin_interfaces/Time stamp # 时间戳
string vehicle_id # 车辆ID
定义好之后,记得修改 CMakeLists.txt 和 package.xml。我直接给你看关键配置:
# CMakeLists.txt 中
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/VehicleStatus.msg"
DEPENDENCIES builtin_interfaces
)
<!-- package.xml 中 -->
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
我曾经犯过的错: 忘记在 package.xml 里添加 rosidl_default_generators 依赖,结果编译时死活找不到消息生成器。折腾了半小时才发现是依赖没写全。
编译一下:
cd ~/ros2_ws
colcon build --packages-select chapt4_interfaces
source install/setup.bash
验证是否成功:
ros2 interface show chapt4_interfaces/msg/VehicleStatus
如果能看到你定义的那些字段,恭喜,自定义消息接口搞定了。
4.3 Python Publisher:把数据发出去
在 chapt4_interfaces 包下创建 scripts 文件夹,新建 vehicle_publisher.py:
import rclpy
from rclpy.node import Node
from chapt4_interfaces.msg import VehicleStatus
from builtin_interfaces.msg import Time
import time
class VehiclePublisher(Node):
def __init__(self):
super().__init__('vehicle_publisher')
self.publisher_ = self.create_publisher(VehicleStatus, 'vehicle_status', 10)
self.timer = self.create_timer(1.0, self.timer_callback)
self.get_logger().info('车辆状态发布节点已启动')
def timer_callback(self):
msg = VehicleStatus()
msg.speed = 25.5
msg.steering_angle = 0.15
now = self.get_clock().now()
msg.stamp = now.to_msg()
msg.vehicle_id = 'AV-001'
self.publisher_.publish(msg)
self.get_logger().info(f'发布: speed={msg.speed}, steering={msg.steering_angle}')
def main(args=None):
rclpy.init(args=args)
node = VehiclePublisher()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
这里有个细节:self.get_clock().now().to_msg() 是ROS2获取当前时间并转为消息格式的标准做法。我见过有人直接用 time.time() 赋值,结果类型不匹配报错。
别忘了给脚本加执行权限,并在 setup.py 里注册入口点:
entry_points={
'console_scripts': [
'vehicle_publisher = chapt4_interfaces.vehicle_publisher:main',
],
},
4.4 C++ Subscriber:把数据接住
在 src 目录下新建 vehicle_subscriber.cpp:
#include "rclcpp/rclcpp.hpp"
#include "chapt4_interfaces/msg/vehicle_status.hpp"
class VehicleSubscriber : public rclcpp::Node
{
public:
VehicleSubscriber() : Node("vehicle_subscriber")
{
subscription_ = this->create_subscription<chapt4_interfaces::msg::VehicleStatus>(
"vehicle_status", 10,
std::bind(&VehicleSubscriber::topic_callback, this, std::placeholders::_1));
RCLCPP_INFO(this->get_logger(), "车辆状态订阅节点已启动");
}
private:
void topic_callback(const chapt4_interfaces::msg::VehicleStatus::SharedPtr msg)
{
RCLCPP_INFO(this->get_logger(),
"收到: speed=%.2f, steering=%.3f, id=%s",
msg->speed, msg->steering_angle, msg->vehicle_id.c_str());
}
rclcpp::Subscription<chapt4_interfaces::msg::VehicleStatus>::SharedPtr subscription_;
};
int main(int argc, char *argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<VehicleSubscriber>());
rclcpp::shutdown();
return 0;
}
然后在 CMakeLists.txt 里添加编译规则:
add_executable(vehicle_subscriber src/vehicle_subscriber.cpp)
ament_target_dependencies(vehicle_subscriber rclcpp chapt4_interfaces)
install(TARGETS vehicle_subscriber DESTINATION lib/${PROJECT_NAME})
小技巧: C++里消息头文件的路径是 chapt4_interfaces/msg/vehicle_status.hpp,注意是下划线命名。Python里则是 chapt4_interfaces.msg.VehicleStatus,驼峰命名。这个区别我一开始老搞混。
4.5 跑起来看看效果
编译整个包:
colcon build --packages-select chapt4_interfaces
source install/setup.bash
开两个终端:
终端1(发布者):
ros2 run chapt4_interfaces vehicle_publisher
终端2(订阅者):
ros2 run chapt4_interfaces vehicle_subscriber
如果一切正常,你会看到订阅者终端每秒打印一次车辆状态信息。这说明Python节点和C++节点通过自定义消息成功通信了。
验证通信的另一种方式:
你也可以用命令行工具查看话题:
ros2 topic echo /vehicle_status
这样不用写订阅者也能看到发布的数据,调试时特别方便。
4.6 避坑指南与个人经验
- 消息接口修改后必须重新编译:我经常改了msg文件忘了编译,结果节点运行时说找不到类型。记住,改完接口一定要
colcon build并source。 - Python节点的可执行权限:
chmod +x这一步容易忘,忘了就会报Permission denied。 - C++节点的依赖声明:
CMakeLists.txt里ament_target_dependencies一定要写上自定义消息包名,否则链接时会报未定义引用。 - 话题名称要一致:发布者和订阅者的话题字符串必须完全一样,大小写敏感。我见过有人把
vehicle_status写成VehicleStatus,结果两边对不上。
嗯,这一章的内容就这些。从自定义消息到Python发布者,再到C++订阅者,整个流程走下来,你应该对ROS2的节点通信有了切身体会。说白了,就是定义好数据格式,然后一个发一个收,跨语言也能无缝对接。
可可精品资料库