文章目录
- 前言
- 一、ROS核心概念:5分钟理解机器人通信架构
-
-
- 节点(Node):功能最小单元
-
- 主节点(Master):通信调度中心
-
- 话题(Topic):异步消息总线
-
- 服务(Service):同步请求-响应
-
- 参数服务器(Parameter Server):全局配置仓库
-
- 二、实战第一步:创建ROS工作空间
-
-
- 创建目录结构
-
- 编译空工作空间
-
- 配置环境变量
- 4.工作空间组成
-
- 三、实战2:话题通信(Python/C++)
-
-
- Python实现(ros-noetic默认python版本为Python3,18.04的系统安装的是melodic,默认版本为Python2)
-
- (1)发布者节点(talker.py)
- (2)订阅者节点(listener.py)
-
- C++实现
-
- (1)发布者节点(talker.cpp)
- (2)订阅者节点(listener.cpp)
-
- 编译与运行
-
- 四、实战3:服务通信(Python/C++)
-
-
- Python实现
-
- (1)服务端(add_two_ints_server.py)
- (2)客户端(add_two_ints_client.py)
-
- C++实现
-
- (1)服务端(add_two_ints_server.cpp)
- (2)客户端(add_two_ints_client.cpp)
-
- 五、实战4:参数服务器(Python/C++)
-
-
- Python实现
-
- C++实现
-
- 六、调试神器:ROS命令行工具箱
-
-
- 基础命令
-
- 可视化工具
-
- 七、总结与进阶路线
-
- 核心结论
- 进阶方向
前言
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. 创建目录结构
bash
# 创建工作空间根目录(catkin_ws可以任意替换你喜欢的名字)
mkdir -p ~/catkin_ws/src
cd ~/catkin_ws/
2. 编译空工作空间
bash
# 编译(生成开发环境)
catkin_make
我创建了ros_learn/ros1用于存放ros1的相关代码

3. 配置环境变量
bash
# 临时生效
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)
python
#!/usr/bin/env python3
import rospy
from std_msgs.msg import String
def talker():
# 初始化节点(名称唯一)
rospy.init_node('talker', anonymous=True)
# 创建话题发布者(队列大小10)
pub = rospy.Publisher('chatter', String, queue_size=10)
# 设置发布频率(10Hz)
rate = rospy.Rate(10)
count = 0
while not rospy.is_shutdown():
msg = f"Hello ROS {count}"
rospy.loginfo(msg)
pub.publish(msg)
rate.sleep()
count += 1
if __name__ == '__main__':
try:
talker()
except rospy.ROSInterruptException:
pass
(2)订阅者节点(listener.py)
python
#!/usr/bin/env python3
import rospy
from std_msgs.msg import String
def 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包的过程。
bash
#首先让我们进入工作空间下创建的src文件夹,将之前创建的talk_listen文件夹删除,在src目录下输入这样一行指令
#示例包名为 talk_listen,依赖 roscpp、std_msgs,
catkin_create_pkg talk_listen roscpp std_msgs

此时才正式出现了我在python代码中所写的正常的ros包组成。scripts可以自己创建。
接着我们在刚自动创建的src文件夹下写好我们的代码
(1)发布者节点(talker.cpp)
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); // 10Hz
int 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)
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包含以下部分:
bash
cmake_minimum_required(VERSION 3.0.2) # 指定CMake最低版本
project(my_ros_package) # 定义项目名称(需与包名一致)
# 查找依赖的CMake包(包括ROS组件)
find_package(catkin REQUIRED COMPONENTS
roscpp
std_msgs
message_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_node
RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
find_package (catkin REQUIRED COMPONENTS ...)
用于声明构建本包所需的ROS依赖包(如roscpp、std_msgs)。
若依赖包未安装,CMake会报错。
catkin_package()
配置包的元信息,供其他包调用时使用。重要参数:
INCLUDE_DIRS:声明本包的头文件路径。
LIBRARIES:声明本包生成的库。
CATKIN_DEPENDS:声明本包运行时依赖的ROS包。
其他的相信聪明的你肯定会自己下去偷偷学习了
这里就直接给大家现成的cmake文件去使用
bash
cmake_minimum_required(VERSION 3.0.2)
project(talk_listen)
# 查找依赖的ROS包
find_package(catkin REQUIRED COMPONENTS
roscpp
std_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(PROGRAMS
scripts/listener.py
scripts/talker.py
DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
# 安装可执行文件
install(TARGETS
talk_node
listener_node
RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
bash
# 回到工作空间目录下,编译工作空间
catkin_make

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


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

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

bash
<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>
bash
<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++文件等都需要进行编译哦~)
bash
catkin_make
source ~/.bashrc
roslaunch talk_listen talk_listen.launch

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

之前说过,我们写的python代码没有包含在ros包中,现在我们在cmake中添加了增加.py文件的支持
我们可以使用rosrun代码执行python代码
在执行代码前我们需要进行权限赋予,否则会权限报错
bash
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)
python
#!/usr/bin/env python3
import rospy
from rospy_tutorials.srv import AddTwoInts, AddTwoIntsResponse
def handle_add(req):
result = req.a + req.b
rospy.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)
python
#!/usr/bin/env python3
import rospy
from rospy_tutorials.srv import AddTwoInts
def 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.sum
except rospy.ServiceException as e:
rospy.logerr("Service call failed: %s", e)
return None
if __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)
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)
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实现
python
#!/usr/bin/env python3
import rospy
def 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++实现
cpp
#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. 基础命令
bash
# 查看活跃节点
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. 可视化工具
bash
# 启动rqt图形界面
rqt
# 使用rqt_plot实时绘图
rqt_plot /chatter
# 启动rviz三维可视化
rviz
七、总结与进阶路线
核心结论
- 话题:适合传感器数据流(如激光雷达、摄像头)。
- 服务:适合需要即时反馈的操作(如导航目标设定)。
- 参数服务器:适合全局配置管理(如机器人物理参数)。
进阶方向
- 消息记录与回放 :使用
rosbag保存/复现实验数据。 - 动态参数配置 :通过
dynamic_reconfigure实时调整参数。 - 多机通信:配置ROS_MASTER_URI实现多机器人协同。
通过掌握这些技能,你已经具备了开发复杂机器人系统的核心能力。下一步可以尝试:
- 集成摄像头实现目标检测
- 使用SLAM算法构建地图
- 开发自主导航功能
遇到问题时,善用rostopic、rosservice等命令行工具,或查阅http://wiki.ros.org/。祝你在机器人世界探索愉快! 🚀