4、ROS2基础编程:C++与Python节点的创建、编译与运行
好,咱们进入正题。这一章是ROS2实战的基石——创建节点。说白了,节点就是ROS2世界里干活的最小单元。你想想看,自动驾驶系统里,激光雷达要发数据、规划模块要算路径、控制模块要发指令,每个模块都是一个节点。
我个人习惯,学一个新框架,先跑通一个最简单的节点。这就像学编程先写"Hello World"一样。今天我们就用C++和Python各写一个节点,然后编译、运行,把整个流程走一遍。
4.1 工作空间与功能包:先搭好架子
在写代码之前,得先把工程目录搭好。ROS2里,所有代码都放在工作空间里。我刚开始用ROS2时,总搞不清工作空间和功能包的关系。其实很简单:
- 工作空间:一个顶层目录,里面放多个功能包
- 功能包:一个独立的模块,包含节点代码、配置文件等
创建工作空间的命令如下:
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws
colcon build
嗯,这里要注意。第一次建工作空间,src目录下是空的,colcon build会报个警告。别慌,这是正常的。等我们放进去功能包就好了。
接下来创建功能包。ROS2提供了两种模板:
| 语言 | 创建命令 | 生成的文件 |
|---|---|---|
| C++ | ros2 pkg create --build-type ament_cmake my_cpp_pkg |
CMakeLists.txt, src/, include/ |
| Python | ros2 pkg create --build-type ament_python my_py_pkg |
setup.py, my_py_pkg/, resource/ |
我在项目中遇到过一个问题:有人把C++和Python混在一个功能包里。虽然技术上可行,但维护起来很痛苦。我建议一个功能包只用一种语言,这样编译和调试都清爽。
4.2 C++节点:从零开始写一个发布者
好,我们写一个最简单的C++节点。它的任务很简单:每隔1秒发布一条"Hello ROS2"消息。
先进入功能包的src目录,创建一个cpp文件:
// src/my_first_node.cpp
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
auto node = std::make_shared<rclcpp::Node>("my_cpp_node");
auto publisher = node->create_publisher<std_msgs::msg::String>("topic_hello", 10);
rclcpp::WallRate rate(1); // 1 Hz
while (rclcpp::ok())
{
auto msg = std_msgs::msg::String();
msg.data = "Hello ROS2 from C++";
RCLCPP_INFO(node->get_logger(), "Publishing: '%s'", msg.data.c_str());
publisher->publish(msg);
rclcpp::spin_some(node);
rate.sleep();
}
rclcpp::shutdown();
return 0;
}
代码写完了,但别急着编译。你得告诉CMake这个文件的存在。打开CMakeLists.txt,找到find_package部分,确保依赖都加上了:
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
add_executable(my_first_node src/my_first_node.cpp)
ament_target_dependencies(my_first_node rclcpp std_msgs)
install(TARGETS
my_first_node
DESTINATION lib/${PROJECT_NAME}
)
我曾经犯过一个低级错误:忘了加install指令。结果编译成功了,但运行时报"找不到可执行文件"。折腾了半小时才发现是安装路径没配。嗯,这种坑踩过一次就记住了。
4.3 Python节点:同样的逻辑,更简洁的写法
Python版本的节点就直观多了。在功能包的my_py_pkg目录下创建:
# my_first_node.py
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class MyFirstNode(Node):
def __init__(self):
super().__init__('my_py_node')
self.publisher_ = self.create_publisher(String, 'topic_hello', 10)
self.timer = self.create_timer(1.0, self.timer_callback)
def timer_callback(self):
msg = String()
msg.data = 'Hello ROS2 from Python'
self.get_logger().info(f'Publishing: "{msg.data}"')
self.publisher_.publish(msg)
def main(args=None):
rclpy.init(args=args)
node = MyFirstNode()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Python节点有个好处:不用显式写while循环。ROS2的create_timer会自动帮你调度。你想想看,如果每个节点都要自己管理循环,代码得多乱。
别忘了在setup.py里注册入口点:
entry_points={
'console_scripts': [
'my_first_node = my_py_pkg.my_first_node:main',
],
},
4.4 编译与运行:见证奇迹的时刻
代码写好了,接下来编译。回到工作空间根目录:
cd ~/ros2_ws
colcon build --packages-select my_cpp_pkg my_py_pkg
这里我建议用--packages-select指定包名。如果你直接colcon build,它会编译整个工作空间。项目大了之后,每次编译都要等很久。只编译改动的包,能省不少时间。
编译成功后,记得source环境:
source install/setup.bash
然后就可以运行了:
# 终端1:运行C++节点
ros2 run my_cpp_pkg my_first_node
# 终端2:运行Python节点
ros2 run my_py_pkg my_first_node
你会看到两个节点都在发布消息。用ros2 topic echo /topic_hello可以查看发布的内容。
核心要点:C++和Python节点在ROS2里是等价的。它们可以互相通信,订阅同一个话题。选择哪种语言,取决于你的场景:
- C++:性能敏感,比如激光雷达驱动、控制算法
- Python:快速原型、数据处理、调试工具
4.5 避坑指南:我踩过的那些坑
坑1:编译时找不到头文件
我曾经在CMakeLists.txt里忘了加ament_target_dependencies。结果编译报错说找不到rclcpp/rclcpp.hpp。记住,ROS2的依赖必须通过这个宏来传递,光写find_package是不够的。
坑2:Python节点运行时报模块找不到
这通常是因为setup.py里的entry_points写错了路径。检查一下:路径是从功能包名开始的,不是从src开始的。比如my_py_pkg.my_first_node:main,而不是src.my_py_pkg.my_first_node:main。
小技巧:调试时可以用ros2 node list查看当前有哪些节点在运行。如果看不到你的节点,说明节点启动失败了。再用ros2 node info /节点名查看详细信息。
4.6 本章知识体系
下面这张图总结了C++和Python节点创建的核心流程。你可以看到,两种语言的步骤是对应的:
说白了,C++和Python的节点创建流程是对称的。区别只在于:C++需要手动管理CMakeLists.txt,Python则通过setup.py自动处理。我个人更推荐初学者从Python入手,等理解了ROS2的通信机制,再切换到C++做性能优化。
好,这一章的内容就到这里。记住:节点是ROS2的基石。把节点的创建、编译、运行练熟了,后面的话题、服务、动作才能玩得转。