ROS1基础入门:从零搭建机器人通信系统(Python/C++)
文章目录
- 前言
- 一、ROS核心概念:5分钟理解机器人通信架构
- 1. 节点(Node):功能最小单元
- 2. 主节点(Master):通信调度中心
- 3. 话题(Topic):异步消息总线
- 4. 服务(Service):同步请求-响应
- 5. 参数服务器(Parameter Server):全局配置仓库
- 二、实战第一步:创建ROS工作空间
- 1. 创建目录结构
- 2. 编译空工作空间
- 3. 配置环境变量
- 4.工作空间组成
- 三、实战2:话题通信(Python/C++)
- 1. Python实现(ros-noetic默认python版本为Python3,18.04的系统安装的是melodic,默认版本为Python2)
- (1)发布者节点(talker.py)
- (2)订阅者节点(listener.py)
- 2. C++实现
- (1)发布者节点(talker.cpp)
- (2)订阅者节点(listener.cpp)
- 3. 编译与运行
- 四、实战3:服务通信(Python/C++)
- 1. Python实现
- (1)服务端(add_two_ints_server.py)
- (2)客户端(add_two_ints_client.py)
- 2. C++实现
- (1)服务端(add_two_ints_server.cpp)
- (2)客户端(add_two_ints_client.cpp)
- 五、实战4:参数服务器(Python/C++)
- 1. Python实现
- 2. C++实现
- 六、调试神器:ROS命令行工具箱
- 1. 基础命令
- 2. 可视化工具
- 七、总结与进阶路线
- 核心结论
- 进阶方向
前言
ROS(Robot Operating System)是机器人领域的“瑞士军刀”,通过分布式节点通信和模块化设计,解决了机器人软件开发中的复杂性问题。本文将从工作空间搭建开始,手把手带你实现话题发布/订阅、服务请求/响应和参数服务器配置,并提供完整的Python/C++代码示例与调试技巧。
一、ROS核心概念:5分钟理解机器人通信架构
1. 节点(Node):功能最小单元
- 作用:独立运行的功能模块(如摄像头驱动、PID控制器)。
- 特点:
- 每个节点是一个可执行程序(如
talker_node)。 - 支持多语言(C++/Python/Java)。
- 通过节点名唯一标识(如
/camera_node)。
- 每个节点是一个可执行程序(如
2. 主节点(Master):通信调度中心
- 作用:
- 注册节点信息(如节点名、提供的服务)。
- 匹配话题发布者与订阅者、服务请求者与服务提供者。
- 特点:
- 单实例运行(通过
roscore启动)。 - 无主节点时,节点间无法通信。
- 单实例运行(通过
3. 话题(Topic):异步消息总线
- 通信模式:
- 发布-订阅(Pub/Sub):一对多广播(如传感器数据流)。
- 特点:
- 数据自动序列化传输。
- 发布者与订阅者无需直接交互。
- 支持多语言混合通信(Python发布者+C++订阅者)。
4. 服务(Service):同步请求-响应
- 通信模式:
- 请求-响应(RPC):一对一调用(如查询电池电量)。
- 特点:
- 客户端发送请求后立即阻塞,等待服务端返回结果。
- 适用于需要确定性结果的场景。
5. 参数服务器(Parameter Server):全局配置仓库
- 作用:
- 存储机器人全局配置(如最大速度、传感器校准参数)。
- 支持动态参数更新(无需重启节点)。
- 访问方式:
- 通过
rosparam命令行工具。 - 在代码中通过节点句柄读写。
- 通过
二、实战第一步:创建ROS工作空间
1. 创建目录结构
# 创建工作空间根目录(catkin_ws可以任意替换你喜欢的名字)
mkdir -p ~/catkin_ws/src
cd ~/catkin_ws/
2. 编译空工作空间
# 编译(生成开发环境)
catkin_make
我创建了ros_learn/ros1用于存放ros1的相关代码

3. 配置环境变量
# 临时生效
source devel/setup.bash# 永久生效(添加到~/.bashrc,根据自己的情况进行替换即可)
echo "source ~/ros_lean/ros1/devel/setup.bash" >> ~/.bashrc
source ~/.bashrc
4.工作空间组成
catkin_ws/(工作空间根目录)
src/(源码包目录,放置 ROS 的包)
build/(编译时的构建空间,生成中间文件)
devel/(开发空间,包含可直接运行的产物、环境设置脚本)
install/(安装空间,可选;完整安装后对外暴露的产物放在这里)
logs/(构建/运行日志,某些工具会生成)
三、实战2:话题通信(Python/C++)
对于ros1而已,一个工作空间下可以放多个程序包实现,源代码一般放在src目录下。
包级目录示例:my_pkg/
CMakeLists.txt(包的构建脚本)
package.xml(包的元数据与依赖声明)
src/(实现源码,可按需要放子目录)
include/(头文件,便于对外暴露的接口)
msg/(自定义消息定义,.msg 文件)
srv/(自定义服务定义,.srv 文件)
action/(自定义动作定义,.action 文件,若有)
launch/(启动文件,.launch 文件,使用 roslaunch 启动多个节点)
config/(配置文件,如 YAML/JSON,用于参数化节点)
scripts/(Python 脚本等可执行程序)
test/(测试用例)
1. Python实现(ros-noetic默认python版本为Python3,18.04的系统安装的是melodic,默认版本为Python2)
这里我创建名为talk_listen/scripts存放以下两个python代码。
可以在vsode中管理你的代码并安装喜欢的插件,往往有事半功倍的效果。

(1)发布者节点(talker.py)
#!/usr/bin/env python3
import rospy
from std_msgs.msg import Stringdef talker():# 初始化节点(名称唯一)rospy.init_node('talker', anonymous=True)# 创建话题发布者(队列大小10)pub = rospy.Publisher('chatter', String, queue_size=10)# 设置发布频率(10Hz)rate = rospy.Rate(10)count = 0while not rospy.is_shutdown():msg = f"Hello ROS {count}"rospy.loginfo(msg)pub.publish(msg)rate.sleep()count += 1if __name__ == '__main__':try:talker()except rospy.ROSInterruptException:pass
(2)订阅者节点(listener.py)
#!/usr/bin/env python3
import rospy
from std_msgs.msg import Stringdef callback(msg):rospy.loginfo(f"Received: {msg.data}")def listener():rospy.init_node('listener', anonymous=True)rospy.Subscriber('chatter', String, callback)rospy.spin()if __name__ == '__main__':listener()

这里我们简单的实现最基础的话题通信。
代码写好后我们可以直接使用python命令,无需编译。因为python3已经安装了ros的相关包
我们可以直接执行看一下效果。
ros1的程序执行都依赖于roscore。因此我们需要开三个终端进行测试。

单独执行talk.py,每0.1秒发送一次,Hello ROS并计数

执行listener.py,以同样的频率进行接受

你会发现在网络良好的状态下接受和发送接近同步
2. C++实现
python执行ros1代码并不依赖于本身的工作空间,但是在实际生产过程中,我们需要通过各种ros包的相互依赖关系完成作业。python只是快速实现我们所需功能的方法,它简单易上手,能够快速观察到想要的结果。
这里我将讲述正确创建一个ros包的过程。
#首先让我们进入工作空间下创建的src文件夹,将之前创建的talk_listen文件夹删除,在src目录下输入这样一行指令
#示例包名为 talk_listen,依赖 roscpp、std_msgs,
catkin_create_pkg talk_listen roscpp std_msgs

此时才正式出现了我在python代码中所写的正常的ros包组成。scripts可以自己创建。
接着我们在刚自动创建的src文件夹下写好我们的代码
(1)发布者节点(talker.cpp)
#include "ros/ros.h"
#include "std_msgs/String.h"int main(int argc, char **argv) {ros::init(argc, argv, "talker");ros::NodeHandle nh;ros::Publisher pub = nh.advertise<std_msgs::String>("chatter", 10);ros::Rate rate(10); // 10Hzint count = 0;while (ros::ok()) {std_msgs::String msg;msg.data = "Hello ROS " + std::to_string(count++);ROS_INFO("%s", msg.data.c_str());pub.publish(msg);rate.sleep();}return 0;
}
(2)订阅者节点(listener.cpp)
#include "ros/ros.h"
#include "std_msgs/String.h"void callback(const std_msgs::String::ConstPtr& msg) {ROS_INFO("Received: %s", msg->data.c_str());
}int main(int argc, char **argv) {ros::init(argc, argv, "listener");ros::NodeHandle nh;ros::Subscriber sub = nh.subscribe("chatter", 10, callback);ros::spin();return 0;
}

红色波浪线可以不用管,哈哈,这是找不到依赖头文件,我们会使用cmake工具来找到我们的依赖,你也可以试着去解决,如果你觉得看着别扭。实际上这不影响我们代码的实现。
3. 编译与运行
对于c/c++代码而已,需要将代码编译链接为可执行文件。这里我们需要用到cmake工具。实际上一个ros包乃至一个工作空间一般都是用cmake工具作为依赖管理的。

创建ros包后会自动在ros包的目录下创建cmakelist文件,里面写了各种示例,英语好的同学肯定能够看懂~
一个典型的ROS包中的CMakeLists.txt包含以下部分:
cmake_minimum_required(VERSION 3.0.2) # 指定CMake最低版本
project(my_ros_package) # 定义项目名称(需与包名一致)# 查找依赖的CMake包(包括ROS组件)
find_package(catkin REQUIRED COMPONENTSroscppstd_msgsmessage_generation # 若需生成消息/服务,需添加此依赖
)# 配置catkin相关参数(如安装路径、系统依赖等)
catkin_package(CATKIN_DEPENDS roscpp std_msgs # 声明本包依赖的ROS包DEPENDS system_lib # 声明非ROS系统依赖(如Eigen、OpenCV)
)# 添加头文件路径
include_directories(include ${catkin_INCLUDE_DIRS})# 定义可执行文件
add_executable(my_node src/my_node.cpp)
target_link_libraries(my_node ${catkin_LIBRARIES})# 安装规则(将文件安装到系统目录)
install(TARGETS my_nodeRUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
find_package(catkin REQUIRED COMPONENTS …)
用于声明构建本包所需的ROS依赖包(如roscpp、std_msgs)。
若依赖包未安装,CMake会报错。
catkin_package()
配置包的元信息,供其他包调用时使用。重要参数:
INCLUDE_DIRS:声明本包的头文件路径。
LIBRARIES:声明本包生成的库。
CATKIN_DEPENDS:声明本包运行时依赖的ROS包。
其他的相信聪明的你肯定会自己下去偷偷学习了
这里就直接给大家现成的cmake文件去使用
cmake_minimum_required(VERSION 3.0.2)
project(talk_listen)# 查找依赖的ROS包
find_package(catkin REQUIRED COMPONENTSroscppstd_msgs
)# 配置catkin包
catkin_package(CATKIN_DEPENDS roscpp std_msgs
)# 设置包含目录
include_directories(include${catkin_INCLUDE_DIRS}
)# 创建C++可执行文件
add_executable(talk_node src/talker.cpp)
target_link_libraries(talk_node ${catkin_LIBRARIES})add_executable(listener_node src/listener.cpp)
target_link_libraries(listener_node ${catkin_LIBRARIES})# 安装Python脚本
install(PROGRAMSscripts/listener.pyscripts/talker.pyDESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)# 安装可执行文件
install(TARGETS talk_nodelistener_nodeRUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
# 回到工作空间目录下,编译工作空间
catkin_make

如果没有报错,编译百分百通过


此时会出现build和devel文件夹存放我们编译过程和产物
此时我们重新打开一个终端或者在当前终端中刷新环境变量
source ~/.bashrc
仍然打开三个终端
#终端一
roscore
#终端二
rosrun talk_listen talk_node
#终端三
rosrun talk_listen listener_node

如果rosrun执行提示找不到某文件,可能需要重新刷新环境变量哦~
如果你觉得一次开三个终端太麻烦,那么你可以学习launch文件的使用。
Launch文件是ROS(Robot Operating System)中的一种XML格式的配置文件,用于批量启动和管理多个ROS节点。它允许通过单个命令启动整个ROS应用程序,而不需要手动逐个启动每个节点。(一条指令管饱)
在原有cmake文件的结尾添加
# 安装launch文件
install(DIRECTORY launch/DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}/launch
)
这里给出启动两种代码launch文件,在ros包中创建launch文件夹并创建文件即可

<launch><!-- 同时启动C++版本的talker和listener --><node pkg="talk_listen" type="talk_node" name="talker" output="screen"/><node pkg="talk_listen" type="listener_node" name="listener" output="screen"/>
</launch>
<launch><!-- 启动Python版本的节点 --><node pkg="talk_listen" type="talker.py" name="python_talker" output="screen"/><node pkg="talk_listen" type="listener.py" name="python_listener" output="screen"/>
</launch>
来到工作空间目录重新编译(每次修改cmake文件或者c++文件等都需要进行编译哦~)
catkin_make
source ~/.bashrc
roslaunch talk_listen talk_listen.launch

launch文件会自动检测是否加载ros核心如果没有会自动创建。

之前说过,我们写的python代码没有包含在ros包中,现在我们在cmake中添加了增加.py文件的支持
我们可以使用rosrun代码执行python代码
在执行代码前我们需要进行权限赋予,否则会权限报错
sudo chmod +x scripts/*
roscore
rosrun talk_listen talker.py
rosrun talk_listen listener.py


文章后面的代码就不给大家示例和教程了,相信大家可以自己补全,我只把最后结果贴出来。
四、实战3:服务通信(Python/C++)
1. Python实现
(1)服务端(add_two_ints_server.py)
#!/usr/bin/env python3
import rospy
from rospy_tutorials.srv import AddTwoInts, AddTwoIntsResponsedef handle_add(req):result = req.a + req.brospy.loginfo("Adding %d + %d = %d", req.a, req.b, result)return AddTwoIntsResponse(result)def add_two_ints_server():rospy.init_node('add_two_ints_server')s = rospy.Service('add_two_ints', AddTwoInts, handle_add)rospy.loginfo("AddTwoInts server ready.")rospy.spin()if __name__ == '__main__':add_two_ints_server()
(2)客户端(add_two_ints_client.py)
#!/usr/bin/env python3
import rospy
from rospy_tutorials.srv import AddTwoIntsdef add_two_ints_client(a, b):rospy.wait_for_service('add_two_ints', timeout=10)try:add_two_ints = rospy.ServiceProxy('add_two_ints', AddTwoInts)response = add_two_ints(a, b)return response.sumexcept rospy.ServiceException as e:rospy.logerr("Service call failed: %s", e)return Noneif __name__ == '__main__':rospy.init_node('add_two_ints_client')result = add_two_ints_client(10, 20)if result is not None:print("10 + 20 = %d" % result)else:print("Service call failed")

2. C++实现
(1)服务端(add_two_ints_server.cpp)
#include "ros/ros.h"
#include "std_srvs/AddTwoInts.h"bool handle_add(std_srvs::AddTwoInts::Request &req,std_srvs::AddTwoInts::Response &res) {res.sum = req.a + req.b;ROS_INFO("Request: a=%ld, b=%ld", (long int)req.a, (long int)req.b);ROS_INFO("Sending back sum: %ld", (long int)res.sum);return true;
}int main(int argc, char **argv) {ros::init(argc, argv, "add_two_ints_server");ros::NodeHandle nh;ros::ServiceServer service = nh.advertiseService("add_two_ints", handle_add);ROS_INFO("Ready to add two ints.");ros::spin();return 0;
}
(2)客户端(add_two_ints_client.cpp)
#include "ros/ros.h"
#include "std_srvs/AddTwoInts.h"int main(int argc, char **argv) {ros::init(argc, argv, "add_two_ints_client");ros::NodeHandle nh;if (argc != 3) {ROS_ERROR("Usage: add_two_ints_client X Y");return 1;}ros::ServiceClient client = nh.serviceClient<std_srvs::AddTwoInts>("add_two_ints");std_srvs::AddTwoInts srv;srv.request.a = atoll(argv[1]);srv.request.b = atoll(argv[2]);if (client.call(srv)) {ROS_INFO("Sum: %ld", (long int)srv.response.sum);} else {ROS_ERROR("Service call failed");return 1;}return 0;
}



五、实战4:参数服务器(Python/C++)
1. Python实现
#!/usr/bin/env python3
import rospydef param_demo():rospy.init_node('param_demo')rospy.loginfo("Parameter Demo Started")try:# 设置参数rospy.set_param("robot_name", "Robo1")rospy.set_param("/global_speed", 5.0)rospy.set_param("~private_param", True)# 获取参数(带默认值)robot_name = rospy.get_param("robot_name", "DefaultRobot")global_speed = rospy.get_param("/global_speed", 1.0)private_param = rospy.get_param("~private_param", False)rospy.loginfo("Robot Name: %s", robot_name)rospy.loginfo("Global Speed: %.1f", global_speed)rospy.loginfo("Private Param: %s", str(private_param))# 检查参数是否存在if rospy.has_param("global_speed"):rospy.loginfo("global_speed exists")else:rospy.loginfo("global_speed deleted")# 搜索参数param_names = rospy.get_param_names()rospy.loginfo("Available parameters:")for name in param_names:rospy.loginfo(" %s", name)# 获取所有参数all_params = rospy.get_param("/")rospy.loginfo("All parameters on root: %s", str(all_params))# 删除参数rospy.delete_param("global_speed")# 验证删除if rospy.has_param("global_speed"):rospy.logwarn("global_speed still exists!")else:rospy.loginfo("global_speed successfully deleted")# 设置参数列表和字典rospy.set_param("sensor_ids", [1, 2, 3, 4])rospy.set_param("robot_config", {"max_speed": 10.0, "min_speed": 0.5})# 获取复杂参数sensor_ids = rospy.get_param("sensor_ids", [])robot_config = rospy.get_param("robot_config", {})rospy.loginfo("Sensor IDs: %s", str(sensor_ids))rospy.loginfo("Robot Config: %s", str(robot_config))except rospy.ROSException as e:rospy.logerr("ROS error occurred: %s", str(e))except Exception as e:rospy.logerr("Unexpected error: %s", str(e))if __name__ == '__main__':param_demo()

2. C++实现
#include "ros/ros.h"int main(int argc, char **argv) {ros::init(argc, argv, "param_demo");ros::NodeHandle nh;// 设置参数nh.setParam("robot_name", "Robo1");nh.setParam("/global_speed", 5.0);nh.setParam("~private_param", true);// 获取参数std::string robot_name;double global_speed;bool private_param;nh.getParam("robot_name", robot_name);nh.getParam("/global_speed", global_speed);nh.getParam("~private_param", private_param);ROS_INFO("Robot Name: %s", robot_name.c_str());ROS_INFO("Global Speed: %.1f", global_speed);ROS_INFO("Private Param: %d", private_param);// 删除参数nh.deleteParam("global_speed");// 检查参数是否存在if (nh.hasParam("global_speed")) {ROS_INFO("global_speed exists");} else {ROS_INFO("global_speed deleted");}return 0;
}

六、调试神器:ROS命令行工具箱
1. 基础命令
# 查看活跃节点
rosnode list# 查看节点详情
rosnode info /talker# 查看当前话题
rostopic list# 查看话题消息类型
rostopic type /chatter# 查看实时数据(文本模式)
rostopic echo /chatter# 查看服务列表
rosservice list# 调用服务(手动输入参数)
rosservice call /add_two_ints "a: 10\nb: 20"
2. 可视化工具
# 启动rqt图形界面
rqt# 使用rqt_plot实时绘图
rqt_plot /chatter# 启动rviz三维可视化
rviz
七、总结与进阶路线
核心结论
- 话题:适合传感器数据流(如激光雷达、摄像头)。
- 服务:适合需要即时反馈的操作(如导航目标设定)。
- 参数服务器:适合全局配置管理(如机器人物理参数)。
进阶方向
- 消息记录与回放:使用
rosbag保存/复现实验数据。 - 动态参数配置:通过
dynamic_reconfigure实时调整参数。 - 多机通信:配置ROS_MASTER_URI实现多机器人协同。
通过掌握这些技能,你已经具备了开发复杂机器人系统的核心能力。下一步可以尝试:
- 集成摄像头实现目标检测
- 使用SLAM算法构建地图
- 开发自主导航功能
遇到问题时,善用rostopic、rosservice等命令行工具,或查阅http://wiki.ros.org/。祝你在机器人世界探索愉快! 🚀
