发布时间:2025-12-09 19:02:44 浏览次数:5
ROS 全称为 Robot Operating System (机器人操作系统)
ROS 的系统架构可分为三层
ROS 的文件系统本质上都还是操作系统文件,因此可以使用 Linux 命令来操作这些文件。不过,在 ROS 中为了更好的用户体验,ROS 专门提供了一些类似于 Linux 的命令,这些命令较之于 Linux 原生命令,更为简洁、高效。
ROS 文件结构是磁盘上 ROS 程序的存储结构,是静态的,而 ros 程序运行之后,不同的节点之间是错综复杂的,ROS 中提供了一个实用的工具:rqt_graph
rqt_graph 能够创建一个显示当前系统运行情况的动态图形。ROS 分布式系统中不同进程需要进行数据交互,计算图可以以点对点的网络形式表现数据交互过程。rqt_graph 是 rqt 程序包中的一部分
运行 rat_graph 可通过下面命令执行:
rosrun rqt_graph rqt_graph# 或者直接输入rqt_graphROS 可是使用 C++ 或者 Python,但是具体选择哪种语言需要视需求而定:C++ 运行效率高但是编码效率低,而 Python 则反之。基于二者互补的特点,ROS 设计者分别设计了 roscpp 与 rospy 库,前者旨在成为 ROS 的高性能库,而后者则一般用于对性能无要求的场景,旨在提高开发效率
返回顶层工作空间目录并编译(生成 build 和 devel):catkin_make
执行
命令行输出: HelloWorld!
返回顶层工作空间目录并编译:catkin_make
执行
输出结果:HelloWorld!
另外,因为 Python 文件运行不需要编译,所以在编写完脚本后,可以直接运行节点
rosrun 包名 文件名.py或者直接运行 Python 文件
python 文件名.py机器人是一种高度复杂的系统性实现,在机器人上可能集成各种传感器(雷达、摄像头、GPS…)以及运动控制实现,为了解耦合,在 ROS 中每一个功能点都是一个单独的进程,每一个进程都是独立运行的。更确切的讲,ROS 是进程(也称为 Nodes)的分布式框架。 因为这些进程甚至还可分布于不同主机,不同主机协同工作,从而分散计算压力。而为了实现不同进程之间的通信,ROS 提供了三种方式:
话题通信理论模型中涉及到三个角色。其中,ROS Master 负责保管 Talker 和 Listener 注册的信息,并匹配话题相同的 Talker 与 Listener,帮助 Talker 与 Listener 建立连接,连接建立后,Talker 可以发布消息,且发布的消息会被 Listener 订阅
发布方
#include "ros/ros.h"#include "std_msgs/String.h" //普通文本类型的消息#include <sstream>int main(int argc, char *argv[]) {setlocale(LC_ALL, "");ros::init(argc,argv,"talker");ros::NodeHandle nh;//泛型: 发布的消息类型//参数1: 要发布到的话题//参数2: 队列中最大保存的消息数,超出此阀值时,先进的先销毁ros::Publisher pub = nh.advertise<std_msgs::String>("chatter",10);std_msgs::String msg;std::string msg_front = "Hello!";int count = 0;//逻辑(一秒10次)ros::Rate r(10);while (ros::ok()) {std::stringstream ss;ss << msg_front << count;msg.data = ss.str();pub.publish(msg);ROS_INFO("发送消息: %s", msg.data.c_str());//根据前面制定的发送频率自动休眠 休眠时间 = 1/频率;r.sleep();count++;//循环结束前,让 count 自增}return 0;}订阅方
#include "ros/ros.h"#include "std_msgs/String.h"void doMsg(const std_msgs::String::ConstPtr& msg_p) {ROS_INFO("接受信息: %s",msg_p->data.c_str());}int main(int argc, char *argv[]) {setlocale(LC_ALL,"");ros::init(argc,argv,"listener");ros::NodeHandle nh;ros::Subscriber sub = nh.subscribe<std_msgs::String>("chatter", 10, doMsg);while (ros::ok()) {//循环调用回调函数ros::spinOnce();r.sleep();}return 0;}接受信息一般使用 ros::spin() 或者 ros::spinOnce()。两者区别在于,ros::spin() 在调用后不会再返回,而 ros::spinOnce() 后者在调用后还可以继续执行之后的程序。因此,ros::spin() 函数一般不会出现在循环中,后面除了 return 0; 外不能有其他语句
CMakeLists.txt
add_executable(Hello_pubsrc/Hello_pub.cpp)add_executable(Hello_subsrc/Hello_sub.cpp)target_link_libraries(Hello_pub${catkin_LIBRARIES})target_link_libraries(Hello_sub${catkin_LIBRARIES})发布方
#! /usr/bin/env pythonimport rospyfrom std_msgs.msg import Stringif __name__ == "__main__":rospy.init_node("talker_p")pub = rospy.Publisher("chatter", String, queue_size=10)msg = String()msg_front = "hello"count = 0rate = rospy.Rate(10)while not rospy.is_shutdown():msg.data = msg_front + str(count)pub.publish(msg)rate.sleep()rospy.loginfo("发送信息:%s",msg.data)count += 1订阅方
#! /usr/bin/env pythonimport rospyfrom std_msgs.msg import Stringdef doMsg(msg):rospy.loginfo("接受信息:%s", msg.data)if __name__ == "__main__":rospy.init_node("listener_p")sub = rospy.Subscriber("chatter", String, doMsg, queue_size=10)rospy.spin()添加可执行权限
chmod +x *.pyCMakeLists.txt
catkin_install_python(PROGRAMSscripts/talker_p.pyscripts/listener_p.pyDESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION})spin 是忙等待,一旦接受缓冲区有数据就会立即处理,这样处理信息的速度较快,但是忙等待会占用一定硬件资源。而 spinOnce 处理接受缓存区的速度由订阅节点循环的频率决定,并且每次都直接处理缓存区里的所有数据。
注意,在 Python 实现中,或者说在 rospy 中并没有 spinOnce,这是因为 rospy 中 spin 的作用与 roscpp 不同,这里的 spin 只是用来确保不让程序在节点关闭前退出,而不想 roscpp 中的那样会影响处理信息的回调函数,所以 rospy 中只需要 spin 就可以了。
但是这样就会产生一个问题,如果想实现在循环中处理接受信息调用回调函数,并执行发布消息的任务,则可以通过新开一个线程来解决循环问题,但是如果不需要循环,则可以直接在订阅的回调函数中发布信息
#!/usr/bin/env python#-*- coding: utf-8 -*-import rospyimport thread, timefrom geometry_msgs.msg import Twistfrom std_srvs.srv import Trigger, TriggerResponsepubCommand = Falseturtle_vel_pub = rospy.Publisher('/turtle1/cmd_vel', Twist, queue_size=10)def command_thread():while True:if pubCommand:vel_msg = Twist()vel_msg.linear.x = 0.5vel_msg.angular.z = 0.2turtle_vel_pub.publish(vel_msg)time.sleep(0.1)def commandCallback(req):global pubCommandpubCommand = bool(1 - pubCommand)return TriggerResponse(1, "Change turtle command state!")def turtle_command_server():rospy.init_node('turtle_command_server')s = rospy.Service('/turtle_command', Trigger, commandCallback)thread.start_new_thread(command_thread, ())rospy.spin()if __name__ == "__main__":turtle_command_server()注意,只有多个主题都有数据的时候才可以触发回调函数。如果其中一个主题的发布节点崩溃了,则整个回调函数永远无法触发回调;并且,当多个主题频率一致的时候也无法保证回调函数的频率等于订阅主题的频率,一般会很低。实际测试订阅两个需要同步的主题频率为 50Hz 和 50Hz,而回调函数的频率只有 24Hz 左右。
C++
#include <ros/ros.h>#include "sensor_msgs/LaserScan.h"#include "geometry_msgs/PoseWithCovarianceStamped.h"#include <message_filters/subscriber.h>#include <message_filters/synchronizer.h>#include <message_filters/sync_policies/approximate_time.h>#include <message_filters/time_synchronizer.h>#include <boost/thread/thread.hpp>#include <iostream>void multi_callback(const sensor_msgs::LaserScanConstPtr& scan, const geometry_msgs::PoseWithCovarianceStampedConstPtr& pose){std::cout << "同步完成!" << std::endl;}int main(int argc, char** argv){ros::init(argc, argv, "multi_receiver");ros::NodeHandle n;message_filters::Subscriber<sensor_msgs::LaserScan> subscriber_laser(n,"scan",1000,ros::TransportHints().tcpNoDelay());message_filters::Subscriber<geometry_msgs::PoseWithCovarianceStamped> subscriber_pose(n,"car_pose",1000,ros::TransportHints().tcpNoDelay());typedef message_filters::sync_policies::ApproximateTime<sensor_msgs::LaserScan, geometry_msgs::PoseWithCovarianceStamped> syncPolicy;//message_filters::TimeSynchronizer<sensor_msgs::LaserScan,geometry_msgs::PoseWithCovarianceStamped> sync(subscriber_laser, subscriber_pose, 10);message_filters::Synchronizer<syncPolicy> sync(syncPolicy(10), subscriber_laser, subscriber_pose); sync.registerCallback(boost::bind(&multi_callback, _1, _2));std::cout << "hahah" << std::endl;ros::spin();return 0;}Python
#!/usr/bin/env python# coding=UTF-8import rospy,math,randomimport numpy as npimport message_filtersfrom std_msgs.msg import Stringfrom sensor_msgs.msg import LaserScanfrom laser_geometry import LaserProjectionfrom geometry_msgs.msg import PoseWithCovarianceStampeddef multi_callback(Subcriber_laser,Subcriber_pose):print "同步完成!"if __name__ == '__main__':rospy.init_node('multi_receive',anonymous=True)subcriber_laser = message_filters.Subscriber('scan', LaserScan, queue_size=1)subcriber_pose = message_filters.Subscriber('car_pose', PoseWithCovarianceStamped, queue_size=1)sync = message_filters.ApproximateTimeSynchronizer([subcriber_laser, subcriber_pose],10,0.1,allow_headerless=True)sync.registerCallback(multi_callback)rospy.spin()在 ROS 通信协议中,数据载体是一个较为重要组成部分,ROS 中通过 std_msgs 封装了一些原生的数据类型,比如:String、Int32、Int64、Char、Bool、Empty… 但是,这些数据一般只包含一个 data 字段,结构的单一意味着功能上的局限性,当传输一些复杂的数据,如激光雷达的信息时,可以使用自定义的消息类型
msgs 只是简单的文本文件,每行具有字段类型和字段名称,可以使用的字段类型有:
int8, int16, int32, int64 (或者无符号类型: uint*)float32, float64stringtime, durationother msg filesvariable-length array[] and fixed-length array[C]ROS 中还有一种特殊类型:Header,标头包含时间戳和 ROS 中常用的坐标帧信息
首先在与功能包 src 同级的 msg 目录下编辑自定义消息文件 Person.msg
string nameuint16 agefloat64 height然后编辑配置文件
package.xml
<build_depend>message_generation</build_depend><exec_depend>message_runtime</exec_depend>CMakeLists.txt(也可以在 msg 文件夹内再定义一个 CMake 文件,用来统一加入消息文件)
find_package(catkin REQUIRED COMPONENTSroscpprospystd_msgs # 需要加入 message_generation,必须有 std_msgsmessage_generation)# 配置 msg 源文件add_message_files(FILESPerson.msg)# 生成消息时依赖于 std_msgsgenerate_messages(DEPENDENCIESstd_msgs)#执行时依赖catkin_package(CATKIN_DEPENDS roscpp rospy std_msgs message_runtime)最后进行编译,会生成供 C++ 和 Python 使用的中间文件,此时调用这些中间文件就能发送自定义消息
在一个简单字符串通信任务中,使用话题时平均延迟为 0.7ms,但是使用服务时平均延迟为 3ms。考虑到服务通信中 client 与 server 之间维持的是一个持久连接,那么服务通信应该要比话题通信快。但实际上,服务通信中 client 发出请求后需要先等 server 唤醒,而 server 唤醒时间就会占整体时间的 80%,即实际上因为服务通信是通过持久性连接发送请求的,所以服务通信的请求发送时间要比话题通信快,但是服务通信中服务唤醒的时间要更大,因此整体上服务通信延迟要比话题通信大
不同于话题通信可以只发送某个字段作为消息,服务通信包含请求和响应两个过程,因此使用服务通信前必须先定义服务类型
首先在与功能包 src 同级的 srv 目录下编辑自定义消息文件。其中,请求和响应使用---分割
# 客户端请求时发送的两个数字int32 num1int32 num2---# 服务器响应发送的数据int32 sum编辑 package.xml 文件,添加编译依赖与执行依赖
<build_depend>message_generation</build_depend><exec_depend>message_runtime</exec_depend><!-- exce_depend 以前对应的是 run_depend 现在非法-->编辑 CMakeLists.txt 相关配置
find_package(catkin REQUIRED COMPONENTSroscpprospystd_msgsmessage_generation)# 需要加入 message_generation,必须有 std_msgsadd_service_files(FILESAddInts.srv)generate_messages(DEPENDENCIESstd_msgs)最后进行编译,会生成供 C++ 和 Python 使用的中间文件,此时调用这些中间文件就能发送自定义消息
服务端
#include "ros/ros.h"#include "demo03_server_client/AddInts.h"// bool 返回值由于标志是否处理成功bool doReq(demo03_server_client::AddInts::Request& req,demo03_server_client::AddInts::Response& resp) {int num1 = req.num1;int num2 = req.num2;ROS_INFO("服务器接收到的请求数据为:num1 = %d, num2 = %d",num1, num2);//逻辑处理if (num1 < 0 || num2 < 0) {ROS_ERROR("提交的数据异常:数据不可以为负数");return false;}//如果没有异常,那么相加并将结果赋值给 respresp.sum = num1 + num2;return true;}int main(int argc, char *argv[]) {setlocale(LC_ALL,"");ros::init(argc,argv,"AddInts_Server");ros::NodeHandle nh;ros::ServiceServer server = nh.advertiseService("AddInts",doReq);ROS_INFO("服务已经启动....");ros::spin();return 0;}客户端
#include "ros/ros.h"#include "demo03_server_client/AddInts.h"int main(int argc, char *argv[]) {setlocale(LC_ALL,"");if (argc != 3) {ROS_ERROR("请提交两个整数");return 1;}ros::init(argc,argv,"AddInts_Client");ros::NodeHandle nh;ros::ServiceClient client = nh.serviceClient<demo03_server_client::AddInts>("AddInts");//方式1ros::service::waitForService("AddInts");//方式2// client.waitForExistence();// 组织请求数据demo03_server_client::AddInts ai;ai.request.num1 = atoi(argv[1]);ai.request.num2 = atoi(argv[2]);// 发送请求,返回 bool 值,标记是否成功bool flag = client.call(ai);// 处理响应if (flag) {ROS_INFO("请求正常处理,响应结果:%d",ai.response.sum);}else {ROS_ERROR("请求处理失败....");return 1;}return 0;}在客户端发送请求前添加: client.waitForExistence(); 或: ros::service::waitForService("AddInts");。这是一个阻塞式函数,只有服务启动成功后才会继续执行。client.call() 也是一个阻塞式函数
服务端
#! /usr/bin/env pythonimport rospyfrom demo03_server_client.srv import AddInts,AddIntsRequest,AddIntsResponsedef doReq(req):sum = req.num1 + req.num2rospy.loginfo("提交的数据:num1 = %d, num2 = %d, sum = %d",req.num1, req.num2, sum)resp = AddIntsResponse(sum)return respif __name__ == "__main__":rospy.init_node("addints_server_p")# 创建服务对象server = rospy.Service("AddInts",AddInts,doReq)rospy.spin()客户端
#! /usr/bin/env pythonimport rospyfrom demo03_server_client.srv import *import sysif __name__ == "__main__":if len(sys.argv) != 3:rospy.logerr("请正确提交参数")sys.exit(1)rospy.init_node("AddInts_Client_p")# 创建请求对象client = rospy.ServiceProxy("AddInts",AddInts)# 方式1:# rospy.wait_for_service("AddInts")# 方式2client.wait_for_service()# 发送请求,接收并处理响应# 方式1# resp = client(3,4)# 方式2# resp = client(AddIntsRequest(1,5))# 方式3req = AddIntsRequest()# req.num1 = 100# req.num2 = 200 req.num1 = int(sys.argv[1])req.num2 = int(sys.argv[2]) resp = client.call(req)rospy.loginfo("响应结果:%d",resp.sum)rosparam:https://blog.csdn.net/u014695839/article/details/78348600
一个程序中可能需要启动多个节点,比如:ROS 内置的小乌龟案例,如果要控制乌龟运动,要启动多个窗口,分别启动 roscore、乌龟界面节点、键盘控制节点。如果每次都调用 rosrun 逐一启动,显然效率低下,因此官方给出的优化策略是使用 launch 文件,可以一次性启动多个 ROS 节点。
运行结果:一次性启动了多个节点
launch 标签规定了一片区域,所有的 launch 文件都由 <launch> 开头,由 </launch> 结尾,所有的描述标签都要写在 <launch> 和 </launch>之间
<launch>...</launch>node 标签里包括了 ROS 中节点的名称属性 name、该节点所在的包名 pkg、节点的类型 type 以及日志输出位置属性 output
| name=“NODE_NAME” | 为节点指派名称,这将会覆盖掉 ros::init() 定义的 node_name |
| pkg=“PACKAGE_NAME” | 节点所在的包名 |
| type=“FILE_NAME” | 执行文件的名称。如果是用 Python 编写则为 xxx.py,如果是 C++ 则为生成的可执行文件名 |
| output=“screen” | 终端输出转储在当前的控制台上,而不是在日志文件中。在节点内有日志输出时该属性才会起作用 |
| respawn=“true” | 当 roslaunch 启动完所有该启动的节点之后,会监测每一个节点,保证它们正常的运行状态。对于任意节点,当它终止时,roslaunch 会将该节点重启 |
| required=“true” | 当被此属性标记的节点终止时,roslaunch 会将其他的节点一并终止。注意此属性不可以与 respawn=“true” 一起描述同一个节点 |
| launch-prefix = “command-prefix” | 相当于在执行启动命令时加上一段命令前缀 |
| ns = “NAME_SPACE” | 在自定义的命名空间里运行节点 |
该标签可以导入另一个 roslaunch XML 文件到当前文件
| file ="$(find pkg-name)/path/filename.xml" | 指明想要包含进来的文件 |
| ns=“NAME_SPACE” | 相对NAME_SPACE命名空间导入文件 |
ROS 支持 topic 的重映射,remap 标签里包含一个 original-name 和一个 new-name,及原名称和新名称
比如现在拿到一个节点,这个节点订阅了 “/chatter” 话题,然而自己写的节点只能发布到 “/demo/chatter” 话题,由于这两个 topic 的消息类型是一致的,想让这两个节点进行通讯,那么可以在 launch 文件中这样写:
<remap from="chatter" to="demo/chatter"/>这样就可以直接把 /chatter 重映射到 /demo/chatter,让两个节点进行通讯
如果 remap 标签写在与 node 元素的同一级,而且在 launch 元素内的最顶层。 那么这个重映射将会作用于 launch 文件中所有的节点
param 标签的作用相当于命令行中的 rosparam set。比如现在在参数服务器中添加一个名为 demo_param,值为 666 的参数
<param name="demo_param" type="int" value="666"/>rosparam 标签允许从 YAML 文件中一次性导入大量参数
<rosparam command="load" file="$(find pkg-name)/path/name.yaml"/>arg 标签用来在 launch 文件中定义参数,arg 和 param 在 ROS 里有根本性的区别,就像局部变量和全局变量的区别一样。arg 不储存在参数服务器中,不能提供给节点使用,只能在 launch 文件中使用。param 则是储存在参数服务器中,可以被节点使用。
<arg name="demo"/>像上面这样,就简单地声明了一个参数,名叫 demo,但是声明不等于定义,在赋值之后参数才能够发挥作用。
<arg name="demo" value="666"/><arg name="demo" default="666"/> roslaunch demo demo.launch demo:=6666arg 还有更加高级,也更加灵活的用法:$(arg arg_name)
当 $(arg arg_name) 出现在 launch 文件任意位置时,将会自动替代为所给参数的值
group 标签可以将若干个节点同时划分进某个工作空间
<group ns="demo_1"><node name="demo_1" pkg="demo_1" type="demo_pub_1" output="screen"/><node name="demo_1" pkg="demo_1" type="demo_sub_1" output="screen"/></group><group ns="demo_2"><node name="demo_2" pkg="demo_2" type="demo_pub_2" output="screen"/><node name="demo_2" pkg="demo_2" type="demo_sub_2" output="screen"/></group>group 标签还可以做到对 node 的批量管理。比如可以同时终止在同一个 group 中的节点
<group if="1-or-0">………………</group><group unless="1-or-0">………………</group>第一种情况,当 if 属性的值为 0 的时候将会忽略掉 <group></group> 之间的标签
第二种恰好相反,当 if 属性的值为 1 的时候将会忽略掉 <group></group> 之间的标签
但是通常不会直接用 1 或 0 来定义 if 标签。因为这样不够灵活。
通常会搭配 $(arg arg_name) 来使用 demo.launch 文件
include.launch文件
<launch><arg name="demo_arg"/><group if="$(demo_arg)"><node name="demo" pkg="demo" type="demo_pub" output="screen"/><node name="demo" pkg="demo" type="demo_sub" output="screen"/></group></launch>https://zhuanlan.zhihu.com/p/157526418