摘要:这篇文章主要介绍ROS常用工具、ROSPY和ROSCPP常用模块,完全看完三篇文章,可以说ROS就基本入门,可以自己动手做实验了。
ROS常用工具
ROS工具的 功能大概有以下几个方向:仿真、调试、可视化。这里主要介绍常用工具作用,如何添加模块,设置参数B站都有视频,这里不会有过多解读。
gazebo 是一种最常用的机器人仿真工具,模拟器,也是目前独立的开源机器人仿真平台。gazebo可以进行机器人的动力学仿真,可以模仿机器人常用的传感器(比如激光雷达、摄像头、IMU等),也可以加载自定义的环境和场景。
RViz 是可视化工具,是将接收到的信息呈现出来;rviz和gazebo非常相似,但是gazebo实现的是仿真,提供一个虚拟世界,RVIZ实现的是可视化,呈现接收到的信息。
rqt 主要作用和RVIZ一致都是可视化,有了它我们可以直观的看到消息的通信架构和流通路径;
常用命令:
rqt_graph :显示通信架构
rqt_plot :绘制曲线
rqt_console :查看日志
rosbag 是一套用于记录和回放ROS主题的工具,此外还提供代码API,对包进行操作编写。
常用命令:cheak 确定一个包是否可以在当前系统中进行,或者是否可以迁移。decompress 压缩一个或多个包文件。filter 解压一个或多个包文件。fix 在包文件中修复消息,以便在当前系统中播放。help 获取相关命令指示帮助信息info 总结一个或多个包文件的内容。play 以一种时间同步的方式回放一个或多个包文件的内容。record 用指定主题的内容记录一个包文件。reindex 重新索引一个或多个包文件。
rosbridge 是一个用在ROS系统和其他系统之间的一个功能包,就像是它的名字一样,起到 一个”桥梁”的作用,使得ros系统和其他系统能够进行交互.Rosbridge为非ROS程序提供了 一个JSON API,有许多与Rosbridge进行交互的前端,包括一个用于Web浏览器交互的 WebSocket服务器。Rosbridge_suite是一个包含Rosbridge的元程序包,用于Rosbridge 的各种前端程序包(如WebSocket程序包)和帮助程序包。
moveit 它融合了研究者在运动规划、操纵、3D感知、运动学、控制和导航方面的最新进展,为操作 者提供了一个易于使用的平台,使用它可以开发先进的机器人应用,也被广泛应用于工业, 商业,研发和其他领域。move结构图如下:
roscpp代码演示 ROS为我们机器人开发者提供了不同语言的 接口,比如roscpp是C++语言ROS接口(目前最广泛应用的ROS客户端库,执行效率高),rospy是python语言的ROS接口(开发效率高,通常用在对运行时间没有太大要求的场合,例如配置、初始化等操作),rosjava是java语言的ROS接口(测试版本) 我们直接调用它所 提供的函数就可以实现topic、service等通信功能。
roscpp位于 /opt/ros/kinetic 之下,用C++实现了ROS通信。在ROS中,C++的代码是通过 catkin这个编译系统(扩展的CMake)来进行编译构建的。每一个node的节点功能可能不一样,但是都包含初始化、销毁、句柄等操作。
常见的ros代码逻辑包含下面的执行步骤:
1.调用ros::init()函数初始化节点的名称和其他信息
2.创建ros::NodeHandle对象,也就是节点句柄,用于创建Pub、Sub(NodeHandle就是节点资源的描述,需要借助“把手”才能操作资源)
3调用ros::shutdown()手动关闭节点(一般是系统自动帮我们完成)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include<ros/ros.h> int main(int argc, char** argv) { ros::init(argc, argv, "your_node_name"); ros::NodeHandle nh; //....节点功能 //创建话题的publisher ros::Publisher advertise(const string &topic, uint32_t queue_size, bool latch=false); //创建话题的subscriber ros::Subscriber subscribe(const string &topic, uint32_t queue_size, void(*)(M)); //创建服务的server,提供服务 ros::ServiceServer advertiseService(const string &service, bool(*srv_func)(Mreq &, Mre s &)); //创建服务的client ros::ServiceClient serviceClient(const string &service_name, bool persistent=false); //查询某个参数的值 bool getParam(const string &key, std::string &s); bool getParam (const std::string &key, double &d) const; bool getParam (const std::string &key, int &i) const; //给参数赋值 void setParam (const std::string &key, const std::string &s) const; void setParam (const std::string &key, const char *s) const; void setParam (const std::string &key, int i) const; //.... ros::spin();//用于触发topic、service的响应队列 return 0; }
roscpp的主要部分包括:ros::init() : 解析传入的ROS参数,创建node第一步需要用到的函数** ros::NodeHandle : 和topic、service、param等交互的公共接口**ros::master : 包含从master查询信息的函数 ros::this_node:包含查询这个进程(node)的函数ros::service:包含查询服务的函数** ros::param:包含查询参数服务器的函数,而不需要用到NodeHandle**ros::names:包含处理ROS图资源名称的函数
具体可见:http://docs.ros.org/api/roscpp/html/index.html
以上功能可以分为以下几类:Initialization and Shutdown 初始与关闭Topics 话题Services 服务Parameter Server 参数服务器Timers 定时器NodeHandles 节点句柄Callbacks and Spinning 回调和自旋(或者翻译叫轮询?)Logging 日志Names and Node Information 名称管理Time 时钟Exception 异常
回调函数与spin()方法
CallBack回调函数与ros::spin() 方法需要配合使用,当消息传来时,只指定回调函数,系统不会自动触发,必须要 ros::spin() 或 者 ros::spinOnce() 才能真正使回调函数生效。 处理流程:回调函数一般作为参数传到另外一个函数(一般是函数指针),当消息message到达时,先会把消息放到一个队列中,当有spin函数执行时,就会处理消息队列队首的信息。spin具体的处理方法可以分成阻塞/非阻塞,单线程/多线程两种。
| spin方法 | 阻塞 | 线程 | | —————————– | —— | —— | | ros::spin() | 阻塞 | 单线程 | | ros::spinOnce() | 非阻塞 | 单线程 | | ros::MultiTreadedSpin() | 阻塞 | 多线程 | | ros::AsyncMultiThreadedSpin() | 非阻塞 | 多线程 |
ROS节点编写
基本流程,首先创建一个工作空间workplace,然后根据实际需要创建相应的包package,编写相应的需求文件,如源文件;根据编译运行需要,补充CMakeLists.txt、package.xml相应说明,如添加依赖,查找相关包,运行所需要的包,消息类型等等。
1 2 3 4 mkdir -p xxx_ws //创建工作空间 catkin_make //编译工作空间 //打开.bashrc 设置xxx_ws工作空间的环境变量 catkin_create_pkg xxxx(包) xx(依赖) //创建包
\1. 编写一个talker的node
在工作空间的src/目录下,第1步,创建一个talker的包study:$ catkin_create_pkg study roscpp第2步,打开vcode(或者其他ide),study/src,创建源文件study_node.cpp,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include"ros/ros.h" #include"std_msgs/String.h" #include<sstream> //编写一个node并发布出来 int main(int argc,char **argv){ ros::init(argc,argv,"study_talker");//定义node的属性 ros::NodeHandle n;//ros提供的一个类,可以实例化publisher,进行发布数据 ros::Publisher study_pub=n.advertise<std_msgs::String>("/study_topic",10); //定义node要发布的topic的名称,以及发布的有效率(表示10为有效的,如果很久都没有缓存进去,那么早期发布的就会被释放掉) ros::Rate loop_rate(10);//每秒发布10次 int count=0;//记录发布的次数 while(ros::ok()){ //ros::ok的作用是,只要ros master正常运行,如果没有中断操作,都会继续运行的 std_msgs::String msg;//定义要发布到ros里面的变量名称,或者实例化一个对象来保存要发布的消息 std::stringstream ss;//定义c++变量来保存输出的变量 ss<<"hello study world!"<<count;//输出的字符串以及次数 count++;//次数自加 msg.data=ss.str();//发布的字符串封装到ros变量 study_pub.publish(msg);//把消息通过ros上面 定义的publisher发布出去 loop_rate.sleep();//暂停0.1秒 } }
第3步,设置CMakeLists.txt&package.xmlCMakeLists.txt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 find_package(catkin REQUIRED COMPONENTS roscpp std_msgs )//告诉系统编译本包时,需要找到这两个包 catkin_package( # INCLUDE_DIRS include # LIBRARIES study CATKIN_DEPENDS roscpp std_msgs # DEPENDS system_lib )//声明依赖本包同时需要里面这两个ros包 add_executable(${PROJECT_NAME}_node src/study_node.cpp)//编译本包生成的可执行文件 add_dependencies(${PROJECT_NAME}_node ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})//链接可执行文件和依赖库 //一般情况而已,CMakeLists.txt是创建包同时,系统自动生成的,然后,我们需要的工作,一般情况就是把上面基本地方去掉#号就行了(目的告诉系统,关于该包,在哪,依赖是啥)
package.xml:
1 2 3 4 5 <build_depend>roscpp</build_depend> <build_depend>std_msgs</build_depend> <exec_depend>roscpp</exec_depend> <exec_depend>std_msgs</exec_depend> //主要修改类似样式,当然,本例子比较简单,系统生成的,不需要做其他修改,但是,如果节点添加新的依赖,需要在这些地方添加相应的包
第4步,编译
1 $ catkin_make #注意在要做工作空间的一级目录下使用该指令
第5步,验证,终端运行
1 2 3 4 5 6 7 8 9 10 $ roscore & #后台运行一个ROS master $ rosrun study study_node & #后台运行包名+节点名 $ rostopic list # /study_topic //表示定义的topic正常发布 /rosout /rosout_agg $ rostopic echo #查看study_topic data: "hello study world!2360"//表示发布成功 --- data: "hello study world!2361"
这时,从零开始创建的talker节点node,成功创建完成
\2. 编写一个listener的node
在工作空间的src/目录下,第1步,创建一个listener的包study_listen:
1 $ catkin_create_pkg study_listen roscpp
第2步,study_listen/src,创建源文件study_listen_node.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include"ros/ros.h" #include"std_msgs/String.h" //创建一个listener的node void studyCallback(const std_msgs::String::ConstPtr& msg){ //回调函数一定是要求是无返回类型 ROS_INFO("I can see you again,%s",msg->data.c_str()); } int main(int argc,char ** argv){ ros::init(argc,argv,"study_listener");//初始化这么一个node ros::NodeHandle n;//命名空间 ros::Subscriber sub=n.subscribe("study_topic",10,studyCallback); //表示聆听study_topic这个主题,每次听到就会启动回调函数,这里的10也表示一个缓冲数量,多了,前面的会被是放掉 //这里一定要注意,聆听的topic一定要和发布的topic的名称对应上,否则,是没办法接收的 ros::spin(); return 0; }
第3步,设置CMakeLists.txt&package.xmlCMakeLists.txt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 find_package(catkin REQUIRED COMPONENTS roscpp std_msgs )//告诉系统编译本包时,需要找到这两个包 catkin_package( # INCLUDE_DIRS include # LIBRARIES study CATKIN_DEPENDS roscpp std_msgs # DEPENDS system_lib )//声明依赖本包同时需要里面这两个ros包 add_executable(${PROJECT_NAME}_node src/study_listen_node.cpp)//编译本包生成的可执行文件 add_dependencies(${PROJECT_NAME}_node ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})//链接可执行文件和依赖库 //一般情况而已,CMakeLists.txt是创建包同时,系统自动生成的,然后,我们需要的工作,一般情况就是把上面基本地方去掉#号就行了(目的告诉系统,关于该包,在哪,依赖是啥)
package.xml:
1 2 3 4 5 <build_depend>roscpp</build_depend> <build_depend>std_msgs</build_depend> <exec_depend>roscpp</exec_depend> <exec_depend>std_msgs</exec_depend> //主要修改类似样式,当然,本例子比较简单,系统生成的,不需要做其他修改,但是,如果节点添加新的依赖,需要在这些地方添加相应的包
第4步,编译
1 $ catkin_make #注意在要做工作空间的一级目录下使用该指令
第5步,验证
1 2 3 4 5 6 7 8 9 $ roscore & #运行一个ROS master $ rosrun study study_node & #包名+节点名;由于测试listener的节点,所以需要启动一个talker的node才行 $ rostopic echo #study_topic data: "hello study world!432" --- data: "hello study world!433" //表示发布成功 #开启一个新终端 $ rosrun study study_listen_node #包名+节点名 [ INFO] [1606043574.419247936]: I can see you again,hello study world!178 //表示监听成功
这时,从零开始创建的listener节点node,成功创建完成
\3. 自定义消息类型并发布演示
这里主要就是定义一个消息类型是重点,然后,在上面创建的talker以及listener包里面头文件包含,然后在CMakeLists.txt&package.xml两文件中,添加新的头文件类型即可使用新定义的消息类型了。
第一步,创建一个包:
1 $ catkin_create_pkg study_msgs std_msgs roscpp
第二步,在包创建一个msg文件夹(注意文件夹名称一定是这样子的,否则,在cmakelists要修改很多 )
第三步,在文件夹中新建文件,StudyMsg.msg
(注意命名方式以及后缀)
注意,自定义的消息类型,记得一定不能写其他说明性内容进去,否则发生编译错误
第四步,设置CMakeLists.txt&package.xmlCMakeLists.txt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 find_package(catkin REQUIRED COMPONENTS roscpp std_msgs message_generation ) ## Generate messages in the 'msg' folder//这里可以看出,系统只会在msg文件夹中查找,对应第二步 add_message_files( FILES StudyMsg.msg ) ## Generate added messages and services with any dependencies listed here generate_messages( DEPENDENCIES std_msgs )
package.xml:
1 2 <build_depend>message_generation</build_depend> <exec_depend>message_runtime</exec_depend>
第五步,编译,catkin_make
第六步,查看
1 2 3 $ cd include/study_msgs $ ls StudyMsg.h //表示该头文件生成成功
这时候,自定义消息类型生成成功,并且,可以被其他包通过头文件#include”study_msgs/StudyMsg.h”方式,就可以正常使用。下面,以发布一个talker节点来展示,并且发布的topic名称为/study_topic_new当然,CMakeLists.txt&package.xml要做相应的补充,CMakeLists.txt:
1 2 3 4 5 6 7 8 9 10 11 12 find_package(catkin REQUIRED COMPONENTS roscpp std_msgs study_msgs ) catkin_package( # INCLUDE_DIRS include # LIBRARIES study CATKIN_DEPENDS roscpp std_msgs study_msgs # DEPENDS system_lib ) //两者都是添加新消息类型study_msgs
package.xml:
1 2 3 <build_depend>study_msgs</build_depend> <exec_depend>study_msgs</exec_depend> //添加新消息类型study_msgs
此时,study_node.cpp为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include"ros/ros.h" #include"std_msgs/String.h" #include<sstream> #include"study_msgs/StudyMsg.h"//使用自定义消息类型 int main(int argc,char **argv){ ros::init(argc,argv,"study_talker");//node's attribute name ros::NodeHandle n; ros::Publisher study_pub=n.advertise<std_msgs::String>("/study_topic",10);//define publisher's topic ros::Publisher study_pub_new=n.advertise<study_msgs::StudyMsg>("/study_topic_new",10);//新定义消息发布的主题名称定义 ros::Publisher study_pub_param=n.advertise<study_msgs::StudyMsg>("/params_topic",10); ros::Rate loop_rate(10);//10 per/s int count=0; while(ros::ok()){ std_msgs::String msg; std::stringstream ss; //std::stringstream ss_new; ss<<"hello study world!"<<count; count++; msg.data=ss.str(); study_pub.publish(msg); //使用自定义消息类型发布消息 study_msgs::StudyMsg studyMsg; studyMsg.id=count;//发布内容的id studyMsg.detail="hello study world! new";//自定义消息类型发布的内容datail study_pub_new.publish(studyMsg); loop_rate.sleep();//pause 0.1s } }
终端查看:
1 2 3 4 5 $ rostopic echo /study_topic_new --- detail: "hello study world! new" id: 29975 ---
\4. 参数中心rosparam的使用演示
开始展示之前,一定要记得在study_listen包的CMakeLists.txt&package.xml做好跟上面的talker包study一样准备,保证可以使用自定义消息类型。
study_node.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include"ros/ros.h" #include"std_msgs/String.h" #include<sstream> #include"study_msgs/StudyMsg.h" int main(int argc,char **argv){ ros::init(argc,argv,"study_talker");//node's attribute name ros::NodeHandle n; ros::Publisher study_pub=n.advertise<std_msgs::String>("/study_topic",10);//define publisher's topic ros::Publisher study_pub_new=n.advertise<study_msgs::StudyMsg>("/study_topic_new",10);//测试新定义消息类型 ros::Publisher study_pub_param=n.advertise<study_msgs::StudyMsg>("/params_topic",10);//测试参数中心 ros::Rate loop_rate(10);//10 per/s int count=0; while(ros::ok()){ std_msgs::String msg; std::stringstream ss; //std::stringstream ss_new; ss<<"hello study world!"<<count; count++; msg.data=ss.str(); study_pub.publish(msg); //发布新定义消息类型 study_msgs::StudyMsg studyMsg; studyMsg.id=count; studyMsg.detail="hello study world! new"; study_pub_new.publish(studyMsg); //测试参数中心 study_msgs::StudyMsg studyMsg_param; std::string param_string; n.param<std::string>("myparam",param_string,"cc");//通过该myparam可以重新定义发布的消息,否则 会一直使用用的是默认值 cc studyMsg_param.detail=param_string; studyMsg_param.id=count; study_pub_param.publish(studyMsg_param); loop_rate.sleep();//pause 0.1s } }
study_listen_node.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include"ros/ros.h" #include"std_msgs/String.h" #include"study_msgs/StudyMsg.h" void studyCallback(const std_msgs::String::ConstPtr& msg){ ROS_INFO("I can see you again,%s ",msg->data.c_str()); }//测试是监听study_topic成功与否 void studyCallback_new(const study_msgs::StudyMsg::ConstPtr& msg){ ROS_INFO("I think I cant forget you,%s,%d",msg->detail.c_str(),msg->id); }//测试监听自定义消息成功与否 void studyCallback_param(const study_msgs::StudyMsg::ConstPtr& msg){ ROS_INFO("I think I cant forget you,%s,%d",msg->detail.c_str(),msg->id); }//测试rosparam参数中心是否工作成功 int main(int argc,char ** argv){ ros::init(argc,argv,"study_listener"); ros::NodeHandle n; ros::Subscriber sub=n.subscribe("/study_topic",10,studyCallback); ros::Subscriber sub_new=n.subscribe("study_topic_new",10,studyCallback_new); ros::Subscriber sub_param=n.subscribe("/params_topic",10,studyCallback_param); ros::spin(); return 0; }
修改了上面的两个文件,接下来就是编译整个工作空间并成功,接着在终端执行
1 2 3 4 5 6 $ roscore & #启动ros master $ rosrun study study_node & #启动talker节点 $ rosrun study_listen study_listen_node #启动listener节点 [ INFO] [1606047990.464340880]: I can see you again,hello study world!99 [ INFO] [1606047990.464436530]: I think I cant forget you,hello study world! new,100 [ INFO] [1606047990.476383810]: I think I cant forget you,cc,100
这时候我们发现,上面我们定义的回调函数,都成功调用了(表示上面的talker/listener/自定义消息 类型都是编写并运行正常的)当我们在终端2启动参数中心rosparam设置参数:
1 $ rosparam set /myparam "yqy"
终端1的显示调整为:
1 2 3 [ INFO] [1606048163.371016634]: I can see you again,hello study world!368 [ INFO] [1606048163.371113447]: I think I cant forget you,hello study world! new,369 [ INFO] [1606048163.394940978]: I think I cant forget you,yqy,369
也就是将周期第三个,cc调整为yqy了。
本章小结:创建一个包,需要定义好CmakeLists.txt 、package.xml两个文件,主要就是告诉系统,我这个包编译和运行时依赖是什么;talker和listener的编写也是基本套路一样的,重新发布的主题以及消息类型以及内容就发布ok了;listener的回调函数,注意是无返回类型的;重新自定义一个消息类型,注意文件夹命名以及文件后缀名称。基本是这些。