ROS1基础入门:从零搭建机器人通信系统(Python/C++)

文章目录

  • 前言
  • 一、ROS核心概念:5分钟理解机器人通信架构
      1. 节点(Node):功能最小单元
      1. 主节点(Master):通信调度中心
      1. 话题(Topic):异步消息总线
      1. 服务(Service):同步请求-响应
      1. 参数服务器(Parameter Server):全局配置仓库
  • 二、实战第一步:创建ROS工作空间
      1. 创建目录结构
      1. 编译空工作空间
      1. 配置环境变量
    • 4.工作空间组成
  • 三、实战2:话题通信(Python/C++)
      1. Python实现(ros-noetic默认python版本为Python3,18.04的系统安装的是melodic,默认版本为Python2)
      1. C++实现
      • (1)发布者节点(talker.cpp)
      • (2)订阅者节点(listener.cpp)
      1. 编译与运行
  • 四、实战3:服务通信(Python/C++)
      1. Python实现
      • (1)服务端(add_two_ints_server.py)
      • (2)客户端(add_two_ints_client.py)
      1. C++实现
      • (1)服务端(add_two_ints_server.cpp)
      • (2)客户端(add_two_ints_client.cpp)
  • 五、实战4:参数服务器(Python/C++)
      1. Python实现
      1. C++实现
  • 六、调试神器:ROS命令行工具箱
      1. 基础命令
      1. 可视化工具
  • 七、总结与进阶路线
    • 核心结论
    • 进阶方向

前言

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

七、总结与进阶路线

核心结论

  1. 话题:适合传感器数据流(如激光雷达、摄像头)。
  2. 服务:适合需要即时反馈的操作(如导航目标设定)。
  3. 参数服务器:适合全局配置管理(如机器人物理参数)。

进阶方向

  1. 消息记录与回放 :使用rosbag保存/复现实验数据。
  2. 动态参数配置 :通过dynamic_reconfigure实时调整参数。
  3. 多机通信:配置ROS_MASTER_URI实现多机器人协同。

通过掌握这些技能,你已经具备了开发复杂机器人系统的核心能力。下一步可以尝试:

  • 集成摄像头实现目标检测
  • 使用SLAM算法构建地图
  • 开发自主导航功能

遇到问题时,善用rostopicrosservice等命令行工具,或查阅http://wiki.ros.org/。祝你在机器人世界探索愉快! 🚀

相关推荐
水天需01038 分钟前
C++ 三种指针转换深度解析
c++
二川bro1 小时前
模型部署实战:Python结合ONNX与TensorRT
开发语言·python
秋邱1 小时前
AI + 社区服务:智慧老年康养助手(轻量化落地方案)
人工智能·python·重构·ar·推荐算法·agi
rising start1 小时前
三、FastAPI :POST 请求、用户接口设计与 Requests 测试
python·网络协议·http·fastapi
言言的底层世界1 小时前
c++中STL容器及算法等
开发语言·c++·经验分享·笔记
Mr_WangAndy2 小时前
C++17 新特性_第一章 C++17 语言特性___has_include,u8字符字面量
c++·c++40周年·c++17新特性·__has_include·u8字面量
liu****2 小时前
八.函数递归
c语言·开发语言·数据结构·c++·算法
CM莫问2 小时前
详解机器学习经典模型(原理及应用)——岭回归
人工智能·python·算法·机器学习·回归
SunnyRivers2 小时前
Python打包指南:编写你的pyproject.toml
python·打包·toml
计算机毕设小月哥2 小时前
【Hadoop+Spark+python毕设】中式早餐店订单数据分析与可视化系统、计算机毕业设计、包括数据爬取、数据分析、数据可视化
后端·python