代码模板 c++ 模板
1 2 3 4 5 6 7 8 9 10 #include "ros/ros.h" int main (int argc, char *argv[]) { setlocale (LC_ALL,"" ); ros::init (argc,argv,"" ); ros::NodeHandle nh; return 0 ; }
python 模板
1 2 3 4 5 6 7 import rospyif __name__ == "__main__" : rospy.init_node("" )
ROS环境搭建与初认识 ROS安装 具体细节看教程 1.2 ROS安装
采用虚拟机安装 ubuntu,再安装 ROS
虚拟机软件:virtualbox(免费) 官网下载软件安装包以及拓展包
ubuntu版本:ubuntu-20.04.6 镜像文件下载地址
在下载过程中可能需要修改软件与更新中的下载自(以后安装库的时候经常会碰上)
在虚拟机中如果出现右键没有反应的情况,按住shift再点
ROS版本:Noetic
1 sudo apt install ros-noetic-desktop-full
配置环境变量,方便在任意终端中使用 ROS:
1 2 echo "source /opt/ros/noetic/setup.bash" >> ~/.bashrc source ~/.bashrc
安装构建依赖:
1 sudo apt install python3-rosdep python3-rosinstall python3-rosinstall-generator python3-wstool build-essential
测试ROS安装环境:
启动三个命令行(ctrl + alt + T)
命令行1键入:roscore
命令行2键入:rosrun turtlesim turtlesim_node
(此时会弹出图形化界面)
命令行3键入:rosrun turtlesim turtle_teleop_key
(可以控制小乌龟运动)
以下命令行内容中[ ]包围的为替换内容
补充ubuntu创建右键快捷键方法:
在用户主目录里找到“模板“文件夹
右键在终端打开,sudo gedit 文本文件
保存,可在任何文件夹下创建文本文件了
ROS集成开发环境 在刚刚的过程会发现ubuntu频繁需要密码,打开terminal,输入seahorse,点击左上角➕号,新建一个空钥匙环,建立完成后设置为默认,打开一些软件不再提示输入密码了
终端 在 ROS 中,需要频繁的使用到终端,且可能需要同时开启多个窗口,推荐使用Terminator
安装:
1 sudo apt install terminator
常用快捷键:
1 2 3 4 5 6 Alt+Up //移动到上面的终端 Alt+Down //移动到下面的终端 Alt+Left //移动到左边的终端 Alt+Right //移动到右边的终端 Ctrl+Shift+O //水平分割终端 Ctrl+Shift+E //垂直分割终端
VScode 下载:vscode 下载 选择适用于linux系统的.deb文件
安装:
方式1:双击安装即可(或右击选择安装) [可能会出现没反应或闪退行为]
方式2:sudo dpkg -i xxxx.deb
复制具体的下载文件名称
vscode 下载插件:c++,CMake,python,ROS
快捷键配置:向上向下复制行;向上向下移动
vscode 使用_基本配置:
创建 ROS 工作空间
1 2 3 mkdir -p [工作空间名称]/src(必须得有 src) cd [工作空间名称] catkin_make
启动 vscode
vscode 中编译 ros
快捷键 ctrl + shift + B 调用编译,选择catkin_make:build
右边的小齿轮配置
修改.vscode/tasks.json
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "version" : "2.0.0" , "tasks" : [ { "label" : "catkin_make:debug" , "type" : "shell" , "command" : "catkin_make" , "args" : [ ] , "group" : { "kind" : "build" , "isDefault" : true } , "presentation" : { "reveal" : "always" } , "problemMatcher" : "$msCompile" } ] }
创建 ROS 功能包
选定 src 右击 —> create catkin package
设置包名(helloworld) 添加依赖(roscpp rospy std_msgs)
C++ 实现
在功能包的 src 下新建 cpp 文件
1 2 3 4 5 6 7 8 9 #include "ros/ros.h" int main (int argc, char *argv[]) { setlocale (LC_ALL,"" ); ros::init (argc,argv,"hello_c" ); ROS_INFO ("hello vscode" ); return 0 ; }
如果没有代码提示
修改 .vscode/c_cpp_properties.json
,设置 “cppStandard”: “c++17”
则ctrl+shift+空格提示函数格式
python 实现
在 功能包 下新建 scripts 文件夹,添加 python 文件
1 2 3 4 5 import rospyif __name__ == "__main__" : rospy.init_node("hello_p" ) rospy.loginfo("hello vscode! it's python" )
在scripts文件夹下打开终端 并添加可执行权限
配置 CMakeLists.txt
1 2 3 4 5 6 7 8 9 10 add_executable([自定义名称] src/helloworld_c.cpp ) target_link_libraries([自定义名称] ${catkin_LIBRARIES} ) catkin_install_python(PROGRAMS scripts/[文件名].py DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} )
编译执行
编译:ctrl + shift + B
执行:在 VScode 中添加终端
1 source ./devel/setup.bash
c++:rosrun [包名] [自定义名称 ]
python:rosrun [包名] [文件名称 ]
如果不编译直接执行 python 文件,会抛出异常
解决方案:
第一行解释器声明,可以使用绝对路径定位到 python3 的安装路径 #! /usr/bin/python3(不建议)
创建一个链接符号到 python 命令:sudo ln -s /usr/bin/python3 /usr/bin/python
(建议)
launch文件 解决需要启动多个节点的效率问题
实现:
选定功能包右击 —> 添加 launch 文件夹
选定 launch 文件夹右击 —> 添加 launch 文件
编辑 launch 文件内容
以小乌龟示例程序启动为例:
1 2 3 4 5 <launch> <!-- 乌龟GUI --> <node pkg="turtlesim" type="turtlesim_node" name="turtle_GUI" /> <node pkg="turtlesim" type="turtle_teleop_key" name="turtle_key" /> </launch>
node —> 包含的某个节点
pkg —–> 功能包
type —-> 被运行的节点文件
name –> 为节点命名
output-> 设置日志的输出目标
运行 launch 文件
1 roslaunch [功能包名] [launch文件名]
运行结果:一次性启动了多个节点
ROS架构 文件系统
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 WorkSpace --- 自定义的工作空间 |--- build:编译空间,用于存放CMake和catkin的缓存信息、配置信息和其他中间文件。 |--- devel:开发空间,用于存放编译后生成的目标文件,包括头文件、动态&静态链接库、可执行文件等。 |--- src: 源码 |-- package:功能包(ROS基本单元)包含多个节点、库与配置文件,包名所有字母小写,只能由字母、数字与下划线组成 |-- CMakeLists.txt 配置编译规则,比如源文件、依赖项、目标文件 |-- package.xml 包信息,比如:包名、版本、作者、依赖项...(以前版本是 manifest.xml) |-- scripts 存储python文件 |-- src 存储C++源文件 |-- include 头文件 |-- msg 消息通信格式文件 |-- srv 服务通信格式文件 |-- action 动作格式文件 |-- launch 可一次性运行多个节点 |-- config 配置信息 |-- CMakeLists.txt: 编译的基本配置
文件系统相关命令
增
catkin_create_pkg 自定义包名 依赖包 === 创建新的ROS功能包
sudo apt install xxx === 安装 ROS功能包
删
sudo apt purge xxx === 删除某个功能包
查
rospack list === 列出所有功能包
rospack find 包名 === 查找某个功能包是否存在,如果存在返回安装路径
roscd 包名 === 进入某个功能包
rosls 包名 === 列出某个包下的文件
apt search xxx === 搜索某个功能包
rosed 包名 文件名 === 修改功能包文件
需要安装 vim
比如:rosed turtlesim Color.msg
执行
roscore === 是 ROS 的系统先决条件节点和程序的集合, 必须运行 roscore 才能使 ROS 节点进行通信
roscore 将启动:
ros master
ros 参数服务器
rosout 日志节点
rosrun 包名 可执行文件名 === 运行指定的ROS节点
比如:rosrun turtlesim turtlesim_node
roslaunch 包名 launch文件名 === 执行某个包下的 launch 文件
计算图 rqt_graph能够创建一个显示当前系统运行情况的动态图形
计算图可以以点对点的网络形式表现数据交互过程
运行程序后,启动新终端,键入:
1 rosrun rqt_graph rqt_graph
可以看到网络拓扑图,显示不同节点之间的关系
ROS通信机制 话题通信 话题通信是ROS中使用频率最高的一种通信模式,基于发布订阅 模式,即一个节点发布消息,另一个节点订阅该消息
以激光雷达信息的采集处理为例,在 ROS 中有一个节点需要时时的发布当前雷达采集到的数据,导航模块中也有节点会订阅并解析雷达数据
以此类推,像雷达、摄像头、GPS…. 等等一些传感器数据的采集都使用了话题通信
话题通信适用于不断更新、少逻辑处理的数据传输场景
在工作空间下功能包中,基本每一个可执行文件都会初始化一个节点(node),并唯一命名
节点之间通过话题(topic)进行通信
话题类型就是发布的消息(msg)
传输的内容需要关注消息类型
普通文本 需求: 实现基本的话题通信,一方发布数据,一方接收数据
c++实现
在模型实现中,ROS master 不需要实现,而连接的建立也已经被封装了,需要关注:
发布方
接收方
数据
发布方:
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 43 44 45 46 47 48 49 #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; ros::Publisher pub = nh.advertise <std_msgs::String>("chatter" ,10 ); std_msgs::String msg; ros::Rate rate (10 ) ; int count = 0 ; while (ros::ok ()) { std::stringstream ss; ss << "hello ---" << count; msg.data = ss.str (); pub.publish (msg); ROS_INFO ("发布的数据: %s" ,ss.str ().c_str ()); count++; rate.sleep (); ros::spinOnce (); } return 0 ; }
接收方:
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 #include "ros/ros.h" #include "std_msgs/String.h" void doMsg (const std_msgs::String::ConstPtr& msg) { ROS_INFO ("我听见: %s" ,msg->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 ("chatter" ,10 ,doMsg); ros::spin (); return 0 ; }
ConstPtr
是 boost::shared_ptr
(智能指针),即 msg
是一个指针
必须用 ->
访问其成员的 data
字段
配置CmakeList.txt
1 2 3 4 5 6 7 8 9 add_executable(demo01_pub src/demo01_pub.cpp) add_executable(demo02_sub src/demo02_sub.cpp) target_link_libraries(demo01_pub ${catkin_LIBRARIES} ) target_link_libraries(demo02_sub ${catkin_LIBRARIES} )
vscode 中的 main 函数 声明 int main(int argc, char const *argv[]){}
,默认生成 argv 被 const 修饰,需要去除该修饰符
python实现
发布方:
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 """ 实现流程: 1.导包 2.初始化 ROS 节点:命名(唯一) 3.实例化 发布者 对象 4.组织被发布的数据,并编写逻辑发布数据 """ import rospyfrom std_msgs.msg import Stringif __name__ == "__main__" : rospy.init_node("talker_p" ) pub = rospy.Publisher("chatter_p" ,String,queue_size= 10 ) msg = String() rate = rospy.Rate(1 ) count = 0 while not rospy.is_shutdown(): count+=1 msg.data = "hello---" +str (count) pub.publish(msg) rate.sleep() rospy.loginfo("写入的数据:%s" ,msg.data)
订阅方:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 """ 实现流程: 1.导包 2.初始化 ROS 节点:命名(唯一) 3.实例化 订阅者 对象 4.处理订阅的消息(回调函数) 5.设置循环调用回调函数 """ import rospyfrom std_msgs.msg import Stringdef doMsg (msg ): rospy.loginfo("I heard:%s" ,msg.data) if __name__ == "__main__" : rospy.init_node("listener_p" ) sub = rospy.Subscriber("chatter_p" ,String,doMsg,queue_size=10 ) rospy.spin()
添加可执行权限
终端下进入 scripts 执行:chmod +x *.py
配置 CMakeLists.txt:
1 2 3 4 5 catkin_install_python(PROGRAMS scripts/demo01_pub_p.py scripts/demo02_sub_p.py DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} )
自定义msg msgs只是简单的文本文件,每行具有字段类型和字段名称
如果需要自定义消息类型,则需要自定义msg
流程:
按照固定格式创建 msg 文件
编辑配置文件
编译生成可以被 Python 或 C++ 调用的中间文件
创建自定义消息
定义msg文件
功能包下新建 msg 目录,添加文件 Person.msg
1 2 3 string name uint16 age float64 height
编辑配置文件
package.xml 中添加编译依赖与执行依赖
1 2 <build_depend>message_generation</build_depend> <exec_depend>message_runtime</exec_depend>
CMakeLists.txt 编辑 msg 相关配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs message_generation ) # 需要加入 message_generation,必须有 std_msgs ... # 配置 msg 源文件 add_message_files( FILES Person.msg ) ... # 执行时依赖 catkin_package( # INCLUDE_DIRS include # LIBRARIES demo02_talker_listener CATKIN_DEPENDS roscpp rospy std_msgs message_runtime # DEPENDS system_lib )
编译依赖find_package内的包
find_package依赖catkin_package内的包
如果catkin_package内的包不对,可能编译成功但运行失败
编译
编译后查看
C++ 需要调用的中间文件(…/工作空间/devel/include/包名/xxx.h)
Python 需要调用的中间文件(…/工作空间/devel/lib/python3/msg/xxx.py)
c++实现
需要先配置 vscode,将前面生成的 head 文件路径配置进 c_cpp_properties.json 的 includepath属性
发布方:
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 #include "ros/ros.h" #include "plumbing_pub_sub/Person.h" int main (int argc, char *argv[]) { setlocale (LC_ALL,"" ); ROS_INFO ("发布方:" ); ros::init (argc,argv,"talker_person" ); ros::NodeHandle nh; ros::Publisher pub = nh.advertise <plumbing_pub_sub::Person>("chatter_person" ,1000 ); plumbing_pub_sub::Person p; p.name = "孙悟空" ; p.age = 2000 ; p.height = 1.45 ; ros::Rate r (1 ) ; while (ros::ok ()) { pub.publish (p); p.age += 1 ; ROS_INFO ("我叫:%s,今年%d岁,高%.2f米" , p.name.c_str (), p.age, p.height); r.sleep (); ros::spinOnce (); } return 0 ; }
订阅方:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include "ros/ros.h" #include "plumbing_pub_sub/Person.h" void doPerson (const plumbing_pub_sub::Person::ConstPtr& person) { ROS_INFO ("订阅的人信息:%s,%d,%.2f" ,person->name.c_str (),person->age,person->height); } int main (int argc, char *argv[]) { setlocale (LC_ALL,"" ); ROS_INFO ("订阅方:" ); ros::init (argc,argv,"listener_person" ); ros::NodeHandle nh; ros::Subscriber sub = nh.subscribe ("chatter_person" ,1000 ,doPerson); ros::spin (); return 0 ; }
配置CMakeList:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 add_executable(person_talker src/person_talker.cpp) add_executable(person_listener src/person_listener.cpp) // 额外配置,避免编译报错 ## Add cmake target dependencies of the executable ## same as for the library above add_dependencies(person_talker ${PROJECT_NAME}_generate_messages_cpp) add_dependencies(person_listener ${PROJECT_NAME}_generate_messages_cpp) target_link_libraries(person_talker ${catkin_LIBRARIES} ) target_link_libraries(person_listener ${catkin_LIBRARIES} )
add_dependencies如果考虑简单,可以仅将注释的内容
1 # add_dependencies(${PROJECT_NAME}_node ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
中${PROJECT_NAME}_node
处改为节点名称,其它保留原来格式也是可以的
python实现
将前面生成的 python 文件路径配置进 settings.json
1 2 3 4 "python.autoComplete.extraPaths" : [ "/opt/ros/noetic/lib/python3/dist-packages" , "/home/ros/demo03_ws/devel/lib/python3/dist-packages" ] ,
发布方:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import rospyfrom plumbing_pub_sub.msg import Personif __name__ == "__main__" : rospy.init_node("talker_person_p" ) pub = rospy.Publisher("chatter_person" ,Person,queue_size=1000 ) p = Person() p.name = "葫芦娃" p.age = 18 p.height =0.75 rate = rospy.Rate(1 ) while not rospy.is_shutdown(): pub.publish(p) rate.sleep() rospy.loginfo("姓名:%s, 年龄:%d, 身高:%.2f" ,p.name,p.age,p.height)
订阅方:
1 2 3 4 5 6 7 8 9 10 11 import rospyfrom plumbing_pub_sub.msg import Persondef doPerson (p ): rospy.loginfo("订阅信息:%s,%d,%.2f" ,p.name,p.age,p.height) if __name__ == "__main__" : rospy.init_node("listener_person_p" ) sub = rospy.Subscriber("chatter_person" ,Person,doPerson,queue_size=1000 ) rospy.spin()
终端下进入 scripts 执行:chmod +x *.py
配置 CMakeLists.txt:
1 2 3 4 5 6 7 catkin_install_python(PROGRAMS scripts/talker_p.py scripts/listener_p.py scripts/person_talker.py scripts/person_listener.py DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} )
服务通信 服务通信也是ROS中一种极其常用的通信模式,服务通信是基于请求响应 模式的,是一种应答机制
即一个节点A向另一个节点B发送请求,B接收处理请求并产生响应结果返回给A
服务通信更适用于对时时性有要求、具有一定逻辑处理的应用场景
案例:
实现两个数字的求和,客户端节点,运行会向服务器发送两个数字,服务器端节点接收两个数字求和并将结果响应回客户端
自定义srv srv 文件内的可用数据类型与 msg 文件一致,且定义 srv 实现流程与自定义 msg 实现流程类似:
按照固定格式创建srv文件
编辑配置文件
编译生成中间文件
流程:
定义srv文件
功能包下新建 srv 目录,添加 xxx.srv 文件,请求和响应数据类型之间用---
分开
1 2 3 4 5 6 # 客户端请求时发送的两个数字 int32 num1 int32 num2 --- # 服务器响应发送的数据 int32 sum
编译配置文件
package.xml 中添加编译依赖与执行依赖
1 2 <build_depend>message_generation</build_depend> <exec_depend>message_runtime</exec_depend>
同话题通信自定义
CMakeLists.txt 编辑 srv 相关配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs message_generation ) # 需要加入 message_generation,必须有 std_msgs add_service_files( FILES AddInts.srv ) generate_messages( DEPENDENCIES std_msgs )
官网示例没有在 catkin_package 中配置 message_runtime,经测试配置也可以
编译,查看包方式同之前
需要先配置 vscode,将前面生成的 head 文件路径配置进 c_cpp_properties.json 的 includepath属性
"includePath": [
"/opt/ros/noetic/include/**",
"/usr/include/**",
"/home/ros/demo03_ws/devel/include/**" //增加的
],
c++
服务端:
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 "plumbing_server_client/AddInts.h" bool doNums (plumbing_server_client::AddInts::Request &req, plumbing_server_client::AddInts::Response &resp) { int num1 = req.num1; int num2 = req.num2; ROS_INFO ("服务器接收到的数据为: num1 = %d,num2 = %d" ,num1,num2); int sum = num1 + num2 ; resp.sum = sum; ROS_INFO ("处理结果: sum = %d" ,sum); return true ; } int main (int argc, char *argv[]) { setlocale (LC_ALL,"" ); ROS_INFO ("服务器启动" ); ros::init (argc,argv,"AddInts_Server" ); ros::NodeHandle nh; ros::ServiceServer server = nh.advertiseService ("AddInts" ,doNums); ros::spin (); }
服务端的回调函数的参数是 demo03_server_client::AddInts::Request& req
,没有ConstPtr
, req
是请求对象的引用 ,不是指针
引用 (req
) 的行为和对象实例一致,直接用 .
访问成员,因为服务是“一问一答”模式,不需要共享所有权,直接修改传入的请求和响应对象即可
话题通信中将接收到的消息封装为 boost::shared_ptr
(智能指针)传递给回调函数,目的是为了高效管理内存(避免拷贝)
客户端:
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 43 44 45 46 47 48 49 50 51 52 53 54 #include "ros/ros.h" #include "plumbing_server_client/AddInts.h" int main (int argc, char *argv[]) { setlocale (LC_ALL,"" ); if (argc != 3 ) { ROS_INFO ("提交的参数个数不对" ); return 1 ; } ros::init (argc,argv,"AddInts_Client" ); ros::NodeHandle nh; ros::ServiceClient client = nh.serviceClient <plumbing_server_client::AddInts>("AddInts" ); plumbing_server_client::AddInts ai; ai.request.num1 = atoi (argv[1 ]); ai.request.num2 = atoi (argv[2 ]); ros::service::waitForService ("AddInts" ); bool flag = client.call (ai); if (flag) { ROS_INFO ("响应成功" ); ROS_INFO ("响应结果 = %d" ,ai.response.sum); } else { ROS_INFO ("响应失败" ); } }
配置CMakeLists
1 2 3 4 5 6 7 8 9 10 11 12 add_executable(AddInts_Server src/AddInts_Server.cpp) add_executable(AddInts_Client src/AddInts_Client.cpp) add_dependencies(AddInts_Server ${PROJECT_NAME}_gencpp) add_dependencies(AddInts_Client ${PROJECT_NAME}_gencpp) target_link_libraries(AddInts_Server ${catkin_LIBRARIES} ) target_link_libraries(AddInts_Client ${catkin_LIBRARIES} )
python
服务端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import rospyfrom plumbing_server_client.srv import AddInts,AddIntsRequest,AddIntsResponsedef doReq (req ): num1 = req.num1 num2 = req.num2 sum = num1 + num2 resp = AddIntsResponse() resp.sum = sum rospy.loginfo("服务器解析结果:%d" ,sum ) return resp if __name__ == "__main__" : rospy.init_node("AddInts_Server_p" ) server = rospy.Service("AddInts" ,AddInts,doReq) rospy.loginfo("服务器启动" ) rospy.spin()
客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import rospyfrom plumbing_server_client.srv import AddInts,AddIntsRequest,AddIntsResponseimport sysif __name__ == "__main__" : if len (sys.argv) != 3 : rospy.logerr("请正确提交参数" ) sys.exit(1 ) rospy.init_node("AddInts_Client_p" ) client = rospy.ServiceProxy("AddInts" ,AddInts) client.wait_for_service() req = AddIntsRequest() req.num1 = int (sys.argv[1 ]) req.num2 = int (sys.argv[2 ]) resp = client.call(req) rospy.loginfo("响应结果:%d" ,resp.sum )
配置CMakeLists
1 2 3 4 5 catkin_install_python(PROGRAMS scripts/demo01_server_p.py scripts/demo02_client_p.py DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} )
参数服务器 以共享的方式实现不同节点之间数据交互的通信模式,存储一些多节点共享的数据,类似于全局变量。
案例:实现参数增删改查操作
ROS Master 作为一个公共容器保存参数,Talker 可以向容器中设置参数,Listener 可以获取参数
注意:参数服务器不是为高性能而设计的,因此最好用于存储静态的非二进制的简单数据
c++
实现参数服务器数据的增删改查,可以通过两套 API 实现:
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 #include "ros/ros.h" int main (int argc, char *argv[]) { setlocale (LC_ALL,"" ); ros::init (argc,argv,"set_param_c" ); std::vector<std::string> stus; stus.push_back ("zhangsan" ); stus.push_back ("lisi" ); std::map<std::string,std::string> friends; friends["guo" ] = "huang" ; friends["yuang" ] = "xiao" ; ros::NodeHandle nh; nh.setParam ("nh_int" ,10 ); nh.setParam ("nh_double" ,3.14 ); nh.setParam ("nh_bool" ,true ); nh.setParam ("nh_string" ,"hello NodeHandle" ); nh.setParam ("nh_vector" ,stus); nh.setParam ("nh_map" ,friends); ros::param::set ("param_int" ,20 ); ros::param::set ("param_double" ,3.14 ); ros::param::set ("param_string" ,"Hello Param" ); ros::param::set ("param_bool" ,false ); ros::param::set ("param_vector" ,stus); return 0 ; }
在 roscpp 中提供了两套 API 实现参数操作
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
python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import rospyif __name__ == "__main__" : rospy.init_node("set_update_paramter_p" ) rospy.set_param("p_int" ,10 ) rospy.set_param("p_double" ,3.14 ) rospy.set_param("p_bool" ,True ) rospy.set_param("p_string" ,"hello python" ) rospy.set_param("p_list" ,["hello" ,"haha" ,"xixi" ]) rospy.set_param("p_dict" ,{"name" :"hulu" ,"age" :8 }) rospy.set_param("p_int" ,100 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 """ rospy.init_node("get_param_p") 参数服务器操作之查询_Python实现: get_param(键,默认值) 当键存在时,返回对应的值,如果不存在返回默认值 int_value = rospy.get_param("p_int",10000) get_param_cached(键,默认值)--提高变量获取效率 int_cached = rospy.get_param_cached("p_int") get_param_names--获取所有的键 names = rospy.get_param_names() for name in names: rospy.loginfo("name = %s",name) has_param 是否包含某个键,存在返回 true,否则返回 false flag = rospy.has_param("p_int") search_param--寻找某个键,并返回完整的路径 key = rospy.search_param("p_int") rospy.loginfo("搜索的键 = %s",key) """
1 2 3 4 5 6 7 8 9 10 """ rospy.delete_param("键") 键存在时,可以删除成功,键不存在时,会抛出异常 rospy.init_node("delete_param_p") try: rospy.delete_param("p_int") except Exception as e: rospy.loginfo("删除失败") """
常用命令 和之前介绍的文件系统操作命令比较,文件操作命令是静态的,操作的是磁盘上的文件,而上述命令是动态的,在ROS程序启动后,可以动态的获取运行中的节点或参数的相关信息
ROS/CommandLineTools - ROS Wiki
rosnode - ROS Wiki 用于获取节点信息的命令
rosnode list 列出所有的活动节点
rosnode ping [node_name] 测试到节点的连接状态
rosnode info [node_name] 打印节点信息
rosnode machine [machine_name] 列出指定设备上的节点
rosnode kill [node_name] 杀死某个节点
rosnode cleanup 清除无用节点,比如小乌龟示例
rostopic - ROS Wiki 用于显示有关ROS话题的调试信息,包括发布者,订阅者,发布频率和ROS消息
rostopic list 显示所有活动状态下的话题
rostpic echo [topic-name] 获取指定话题当前发布的消息
rostopic pub 直接调用命令向订阅者发布消息 [这类指令都需要先source 一下]
1 2 rostopic pub -r [频率] /话题名称 消息类型 消息内容 rostopic pub -r 10 chatter_person plumbing_pub_sub/Person [按两次Tab补齐]
rostopic info [topic-name] 获取当前话题的相关信息
rostopic type [topic-name] 打印话题类型
rostopic hz [topic-name] 显示话题的发布频率
rosmsg - ROS Wiki 用于显示有关 ROS消息类型的工具
rosmsg list 列出所有消息
rosmsg list | grep -i [消息种类] 显示消息的具体路径
rosmsg show [消息种类] 显示消息描述
rosmsg info [消息种类] 作用与 rosmsg show 一样
rosservice - ROS Wiki 用于列出和查询ROS Services 的工具
rosservice list 列出所有活动的服务
rosservice call [service-name] 使用提供的参数调用服务
rosservice info [service-name] 打印有关服务的信息
rosservice type [service-name] 打印服务类型
rossrv是用于显示有关ROS服务类型的信息的命令行工具,与 rosmsg 使用语法高度一致
rosparam - ROS Wiki 用于使用YAML编码文件在参数服务器上获取和设置ROS参数
通信机制比较 三种通信机制中
参数服务器是一种数据共享机制,可以在不同的节点之间共享数据
话题通信与服务通信是在不同的节点之间传递数据的
话题通信:Publisher→Subscriber [Topic] [msg]
服务通信:Client→Server [Service] [srv]
二者的实现流程相似,都是两个节点通过话题关联到一起,并使用某种类型的数据载体实现数据传输
Topic(话题)
Service(服务)
通信模式
发布/订阅
请求/响应
同步性
异步(无序)
同步(有序)
底层协议
ROSTCP/ROSUDP
ROSTCP/ROSUDP
缓冲区
有(queue_size 队列)
无
时时性
弱
强
节点关系
多对多
一对多(一个 Server)
通信数据
msg
srv
使用场景
连续高频的数据发布与接收:雷达、里程计
偶尔调用或执行某一项特定功能:拍照、语音识别
ROS通信机制进阶 roscpp rospy
常用API 建议参考官方API文档或参考源码
在运行程序时rosrun 后接着写 _[参数名] = [参数值] 可以给节点
初始化函数 ros::init(argc, argv, “name”, [options])
argc — 封装实参的个数(n+1)
argv — 封装参数的数组
name — 为节点命名(唯一性)
options — 节点启动选项 ros::init_options::AnonymousName
给前面的节点命名后增加一个随机数,使得同一个节点能够重复启动
否则重名情况下,第二次启动时第一次会自动停止
返回值: void
rospy.init_node(name, argv=None, anonymous=False)
name — 节点名称
argv — 封装节点调用时传递的参数
anonymous — 取值为 true 时,为节点名称后缀随机编号
话题与服务 在 roscpp 中,话题和服务的相关对象一般由 ros::NodeHandle nh 创建,python中不用专门创建NodeHandle
发布对象
ros::Publisher pub = nh.advertise< type >(“topic”, queue_size, [latch])
type — 消息类型
topic — 话题名称
queue_size — 队列长度
latch — 设置为ture时,该话题发布的最后一条消息将被保存,并且当有订阅者连接时会将该消息发送给订阅者
以静态地图发送为例(短时间不变的数据)
方案1:可以使用固定频率发送地图数据,但是效率低
方案2:可以将地图发布对象的latch设置为true,并且发布方只发送一次数据,每当订阅者连接时,将地图数据发送给订阅者(只发送一次),这样提高了数据的发送效率
pub = rospy.Publisher(“topic”, type, queue_size, [lathc])
订阅对象
ros::Subscriber sub = nh.subscribe(“topic”, queue_size, callback);
void callback(const std_msgs::String::ConstPtr &msg)
sub = rospy.Subscriber(“topic”, type, callback, queue_size)
def callback(msg):
服务对象
ros::ServiceServer server = nh.advertiseService(“service”, callback);
bool callback(srv type::Request &req, srv type::Response &resp)
server = rospy.Service(“service”, srv type, callback)
def callback(req):
return resp
客户端对象
ros::ServiceClient client = nh.serviceClient< srv type >(“service”);
等待服务函数1 ros::service::waitForService(“service”)
等待服务函数2 client.waitForExistence();
请求写入:srv type req; req.request.input1 = atoi(argv[1]); (类推写完输入)
client = rospy.ServiceProxy(“service”, srv type)
等待服务函数 client.wait_for_service()
请求写入:req = srv typeRequets() req.input1 = sys.argv[1] (类推写完输入)
回调函数 spinOnce() 处理一轮回调,一般用于循环体内
spin() 进入循环去处理回调
不同点:在ros::spin() 后的语句不会执行到,而 ros::spinOnce() 后的语句可以执行
python 只有 rospy.spin()
时间 时刻
获取时刻,或是设置指定时刻
ros::Time::now()
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 #include "ros/ros.h" int main (int argc, char *argv[]) { setlocale (LC_ALL,"" ); ros::init (argc,argv,"hello_time" ); ros::NodeHandle nh; ros::Time current_time = ros::Time::now (); ROS_INFO ("当前时刻:%.2f" ,current_time.toSec ()); ROS_INFO ("当前时刻:%d" ,current_time.sec); std::time_t raw_time = current_time.toSec (); std::tm* time_info = std::localtime (&raw_time); char buffer[80 ]; std::strftime (buffer, sizeof (buffer), "%Y-%m-%d %H:%M:%S" , time_info); ROS_INFO ("当前时间: %s" ,buffer); ros::Time t1 (100 ,100000000 ) ; ROS_INFO ("时刻:%.2f" ,t1. toSec ()); ros::Time t2 (100.1 ) ; ROS_INFO ("时刻:%.2f" ,t2. toSec ()); return 0 ; }
命名增加时间戳方法:
1 2 3 4 5 std::time_t raw_time = ros::Time::now ().toSec (); std::tm* time_info = std::localtime (&raw_time); char buffer[80 ];std::strftime (buffer, sizeof (buffer), "%Y%m%d_%H%M%S" , time_info);
后续直接+buffer即可
rospy.Time.now()
1 2 3 4 5 6 7 8 9 10 11 current_time = rospy.Time.now() rospy.loginfo("当前时刻:%.2f" ,right_now.to_sec()) rospy.loginfo("当前时刻:%.2f" ,right_now.to_nsec()) time1 = rospy.Time(100 ,100000000 ) time2 = rospy.Time(100.1 ) time3 = rospy.Time.from_sec(100.1 ) rospy.loginfo("设置时刻1:%.2f" ,time1.to_sec()) rospy.loginfo("设置时刻2:%.2f" ,time2.to_sec()) rospy.loginfo("设置时刻2:%.2f" ,time3.to_sec())
持续时间
设置一个时间区间(间隔)
ros::Duration du()
1 2 3 4 ROS_INFO ("开始休眠:%.2f" ,ros::Time::now ().toSec ());ros::Duration du (4.5 ) ;du.sleep (); ROS_INFO ("结束休眠:%.2f" ,ros::Time::now ().toSec ());
du = rospy.Duration()
1 2 3 4 rospy.loginfo("持续时间测试开始....." ) du = rospy.Duration(3.3 ) rospy.sleep(du) rospy.loginfo("持续时间测试结束....." )
时刻运算
1 2 3 4 5 ros::Time begin = ros::Time::now (); ros::Duration du1 (5 ) ; ros::Time stop = begin + du1; ROS_INFO ("开始时刻:%.2f" ,begin.toSec ());ROS_INFO ("结束时刻:%.2f" ,stop.toSec ());
time 与 duration 可以+/-,duration 之间也可以+/-
但time 之间只可以 - ,不可以 + ,返回的是 ros::Duration 类型
python与c++相同
设置运行频率
1 2 3 4 5 ros::Rate rate (1 ) ; while (true ) { rate.sleep (); }
1 2 3 4 rate = rospy.Rate(1 ) while not rospy.is_shutdown(): rate.sleep() rospy.loginfo("+++++++++++++++" )
定时器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void cb (const ros::TimerEvent &event) { ROS_INFO ("----" ); ROS_INFO ("调用时刻:%.2f" ,event.current_real.toSec ()); } ros::Timer timer = nh.createTimer (ros::Duration (1 ),cb); ros::spin ();
1 2 3 4 5 6 7 8 9 10 11 12 def cb (event ): rospy.loginfo("+++++++++++" ) rospy.loginfo("当前时刻:%.2f" ,event.current_real.to_sec()) """ rospy.Timer(rospy.Duration(period), // 时间间隔 callback, // 回调函数 oneshot=False, // 是否一次性 reset=False) """ rospy.Timer(rospy.Duration(1 ),cb) rospy.spin()
其它函数 在发布实现时,一般会循环发布消息,循环的判断条件一般由节点状态来控制
C++中可以通过 ros::ok() 来判断节点状态是否正常,而 python 中则通过 rospy.is_shutdown() 来实现判断,导致节点退出的原因主要有如下几种:
节点接收到了关闭信息,比如常用的 ctrl + c(z) 快捷键就是关闭节点的信号
同名节点启动,导致现有节点退出
程序中的其他部分调用了节点关闭相关的API(C++中是ros::shutdown(),python中是rospy.signal_shutdown())
日志函数:
1 2 3 4 5 ROS_DEBUG ("hello,DEBUG" ); ROS_INFO ("hello,INFO" ); ROS_WARN ("Hello,WARN" ); ROS_ERROR ("hello,ERROR" );ROS_FATAL ("hello,FATAL" );
1 2 3 4 5 rospy.logdebug("hello,debug" ) rospy.loginfo("hello,info" ) rospy.logwarn("hello,warn" ) rospy.logerr("hello,error" ) rospy.logfatal("hello,fatal" )
头文件与源文件 核心内容在于CMakeLists.txt文件的配置
自定义头文件调用 流程:
编写头文件;
编写可执行文件(同时也是源文件);
编辑配置文件并执行。
头文件
在功能包下的include/功能包名
目录下新建头文件: hello.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #ifndef _HELLO_H #define _HELLO_H namespace hello_ns{class MyHello {public : void run () ; }; } #endif
为了后续包含头文件时不抛出异常,配置 .vscode 下 _cpp_properties.json 的 includepath属性(和之前配置服务srv文件生成的头文件过程相似)
1 2 3 4 "includePath" : [ ... "/home/ros/demo03_ws/src/plumbing_head/include/**" ] ,
源文件
src目录下新建 hello.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include "ros/ros.h" #include "plumbing_head/hello.h" namespace hello_ns{void MyHello::run () { ROS_INFO ("run函数执行...." ); } } int main (int argc, char *argv[]) { setlocale (LC_ALL,"" ); ros::init (argc,argv,"hello_head" ); hello_ns::MyHello myhello; myhello.run (); return 0 ; }
配置文件
与之前配置多一步
1 2 3 4 5 6 7 8 9 10 11 12 include_directories( include // 这里解除注释 ${catkin_INCLUDE_DIRS} ) add_executable(hello src/hello.cpp) add_dependencies(hello ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS}) target_link_libraries(hello ${catkin_LIBRARIES} )
自定义源文件调用 流程:
编写头文件;
编写源文件;
编写可执行文件;
编辑配置文件并执行。
可执行文件
src目录下新建 use_head.cpp
1 2 3 4 5 6 7 8 9 10 #include "ros/ros.h" #include "plumbing_head/hello.h" int main (int argc, char *argv[]) { setlocale (LC_ALL,"" ); ros::init (argc,argv,"hello_head_src" ); hello_ns::MyHello my; my.run (); return 0 ;
配置文件
头文件与源文件相关配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 include_directories( include // 这里解除注释 ${catkin_INCLUDE_DIRS} ) ## 声明C++库 ## Declare a C++ library # add_library(${PROJECT_NAME} # src/${PROJECT_NAME}/plumbing_pub_sub.cpp # ) add_library(head_src include/${PROJECT_NAME}/hello.h src/hello.cpp ) add_dependencies(head_src ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS}) target_link_libraries(head_src ${catkin_LIBRARIES} )
可执行文件配置:
1 2 3 4 5 6 7 8 9 add_executable(use_hello src/use_hello.cpp) add_dependencies(use_hello ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS}) #此处需要添加之前设置的库 target_link_libraries(use_hello head_src // 库声明 ${catkin_LIBRARIES} )
Python模块导入 实现:
新建两个Python文件,使用 import 实现导入关系;
添加可执行权限、编辑配置文件并执行UseA
tool.py文件
use_tool.py文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import osimport sysimport rospypath = os.path.abspath("." ) sys.path.insert(0 ,path + "/src/plumbing_head/scripts" ) import toolif __name__ == "__main__" : rospy.init_node("tool" ) """ 原因: rosrun 执行时,参考路径是工作空间的路径,在工作空间下无法查找依赖的模块 解决:可以声明python的环境变量,当依赖某个模块时,先去指定环境变量中查找依赖 """ rospy.loginfo("num=%d" ,tool.num)
ROS运行管理 元功能包 完成ROS中一个系统性的功能,可能涉及到多个功能包,逐一安装功能包的效率低下,在ROS中,提供了一种方式可以将不同的功能包打包成一个功能包,当安装某个功能模块时,直接调用打包后的功能包即可,该包又称之为元功能包
MetaPackage
是Linux的一个文件管理系统的概念,是ROS中的一个虚包,里面没有实质性的内容,但是它依赖了其他的软件包,通过这种方法可以把其他包组合起来,可以认为它是一本书的目录索引,告诉我们这个包集合中有哪些子包,并且该去哪里下载
例如:sudo apt install ros-noetic-desktop-full 命令安装ros时就使用了元功能包
实现
新建一个功能包,不需要添加依赖,修改package.xml
1 2 3 4 5 6 7 8 9 <buildtool_depend > catkin</buildtool_depend > <exec_depend > plumbing_pub_sub</exec_depend > <exec_depend > plumbing_server_client</exec_depend > <exec_depend > plumbing_param_server</exec_depend > <export > <metapackage /> </export >
修改 CMakeLists.txt
1 2 3 4 cmake_minimum_required(VERSION 3.0.2) project(plumbing_my) find_package(catkin REQUIRED) catkin_metapackage()
不能出现换行以及注释
catkin/package.xml - ROS Wiki
launch文件 在功能包下添加 launch目录, 目录下新建 xxxx.launch 文件,编辑 launch 文件
1 2 3 4 <launch> <node pkg="turtlesim" type="turtlesim_node" name="myTurtle" output="screen" /> <node pkg="turtlesim" type="turtle_teleop_key" name="myTurtleContro" output="screen" /> </launch>
调用 launch 文件
roslaunch 命令执行launch文件时,首先会判断是否启动了 roscore,如果启动了,则不再启动,否则,会自动调用 roscore
launch标签
<launch>
标签是所有 launch 文件的根标签,充当其他标签的容器
deprecated 告知用户当前 launch 文件已经弃用
1 <launch deprecated="此文件可能过时!">
node标签
<node>
标签用于指定 ROS 节点,是最常见的标签,但 roslaunch 命令不能保证按照 node 的声明顺序来启动节点(节点的启动是多进程的)
pkg=”包名”
节点所属的功能包
type=”nodeType”
节点类型,即与之相同名称的可执行文件,python文件需要带上.py
,cpp文件不需要,类似CMakeList配置
name=”nodeName”
节点名称,在 ROS 网络拓扑中节点的名称
machine=”机器名”
在指定机器上启动节点
respawn=”true | false” (可选)
如果节点退出,是否自动重启
respawn_delay=” N” (可选)
如果 respawn 为 true,那么延迟 N 秒后启动节点
required=”true | false” (可选)
如果为 true,那么该节点退出,将杀死整个 roslaunch,不能和respawn一起用,会报错
ns=”xxx” (可选)
在指定命名空间 xxx 中启动节点 ,主要用来避免重名
node list -> /命名空间/name
output=”log | screen” (可选)
默认log,但一般改成screen
1 <node pkg="turtlesim" type="turtlesim_node" name="myTurtle" output="screen"/>
include标签
用于将另一个 xml 格式的 launch 文件导入到当前文件
file=”$(find 包名)/xxx/xxx.launch”
1 2 3 4 5 <launch> <!-- 包含 --> <include file = "$(find launch01_basic)/launch/start_turtle.launch"/> <!-- 其他节点 --> </launch>
用于复用
remap标签
用于话题重命名
1 2 3 4 5 <node pkg="turtlesim" type="turtlesim_node" name="myTurtle" output="screen"> <remap from="/turtle1/cmd_vel" to = "/cmd_vel" /> </node> <!-- 在launch中的语法是每个标签结束后才标斜杠,没结束还有子标签的话就不标 -->
from = “原始话题名称” to = “目标话题名称”
此时原来的控制失效,因为turtle_teleop_key 是以/turtle1/cmd_vel 发布
1 <node pkg="turtlesim" type="turtle_teleop_key" name="myTurtleContro" output="screen" />
这里需要下载一个控制包
1 sudo apt-get install ros-noetic-teleop-twist-keyboard
param标签
<param>
标签主要用于在参数服务器上设置参数
在<node>
标签中时,相当于私有参数
在<node>
标签外时,相当于全局参数
name=”[命名空间]/参数名”
参数名称,可以包含命名空间
value=”xxx” (可选)
定义参数值,如果此处省略,必须指定外部文件作为参数源
type=”str | int | double | bool | yaml” (可选)
指定参数类型,如果未指定,roslaunch 会尝试确定参数类型,规则如下:
如果包含 ‘.’ 的数字解析未浮点型,否则为整型
“true” 和 “false” 是 bool 值(不区分大小写)
其他是字符串
1 2 3 4 5 <param name="param_A" type = "int" value ="100" /> <node pkg="turtlesim" type="turtlesim_node" name="myTurtle" output="screen"> <remap from="/turtle1/cmd_vel" to = "/cmd_vel" /> <param name="param_B" type = "double" value ="3.14" /> </node>
rosparam标签
<rosparam>
标签可以从 YAML 文件导入参数,或将参数导出到 YAML 文件,也可以用来删除参数
command=”load | dump | delete” (可选,默认 load)
file=”$(find 功能包)/xxx/yyy….”
param=”参数名称”
ns=”命名空间” (可选)
1 2 <!-- 导入参数 --> <rosparam command = "load" file = "$(find launch01_basic)/launch/param.yaml"/>
由于launch文件也不是按顺序进行,导出虽然写在launch文件较后的位置也可能无法正常导出,所以最好分两个launch文件,在完成后再进行导出
1 2 3 4 <launch> <!-- 导出参数 --> <rosparam command = "dump" file = "$(find launch01_basic)/launch/param_out.yaml"/> </launch>
删除操作:
1 <rosparam command = "delete" param = "bg_B" />
group标签
<group>
标签可以对节点分组,具有 ns 属性,可以让节点归属某个命名空间
这个时候即使在node中name属性相同也不会报错,因为group使得前面加上前缀,不会产生相同的节点名称
1 2 3 4 5 6 7 8 9 10 <launch > <group ns = "first"> <node pkg="turtlesim" type="turtlesim_node" name="myTurtle" output="screen" /> <node pkg="turtlesim" type="turtle_teleop_key" name="myTurtleContro" output="screen" /> </group> <group ns = "second"> <node pkg="turtlesim" type="turtlesim_node" name="myTurtle" output="screen" /> <node pkg="turtlesim" type="turtle_teleop_key" name="myTurtleContro" output="screen" /> </group> </launch>
arg标签
<arg>
标签是用于动态传参,类似于函数的参数
name=”参数名称”
default=”默认值” (可选)
value=”数值” (可选)
不可以与 default 并存
doc=”描述”
命令行调用传参
1 roslaunch hello.launch xxx:=值
工作空间覆盖 所谓工作空间覆盖,是指不同工作空间中,存在重名的功能包的情形
比如:自定义工作空间A存在功能包 turtlesim,自定义工作空间B也存在功能包 turtlesim,当然系统内置空间也存在turtlesim,如果调用turtlesim包,会调用哪个工作空间中的呢
新建工作空间A与工作空间B,两个工作空间中都创建功能包: turtlesim
1 2 3 4 5 6 7 8 #include "ros/ros.h" int main(int argc, char *argv[]) { ros::init(argc,argv,"hello_ws1"); ROS_INFO("demo01_ws"); return 0; }
为了在任何工作空间下调用demo01_ws和demo02_ws,需要在根目录下的.bashrc文件中进行修改
1 2 3 4 source /opt/ros/noetic/setup.bash # 新增 source /home/ros/demo01_ws/devel/setup.bash source /home/ros/demo02_ws/devel/setup.bash
刷新环境变量
运行
会发现直接跳出hello_ws2
是因为在.bashrc中后刷新的会覆盖前面的
查看ROS环境环境变量
ROS_PACKAGE_PATH 中的值,和 .bashrc 的配置顺序相反—>后配置的优先级更高
功能包重名时,会按照 ROS_PACKAGE_PATH 查找,配置在前的会优先执行
隐患
比如当前工作空间B优先级更高,意味着当程序调用 turtlesim 时,不会调用工作空间A也不会调用系统内置的 turtlesim,如果工作空间A在实现时有其他功能包依赖于自身的 turtlesim,而按照ROS工作空间覆盖的涉及原则,那么实际执行时将会调用工作空间B的turtlesim,从而导致执行异常,出现安全隐患
BUG 说明:
在 .bashrc 文件中 source 多个工作空间后,可能出现在 ROS PACKAGE PATH 中只包含两个工作空间,可以删除自定义工作空间的 build 与 devel 目录,重新 catkin_make,然后重新载入 .bashrc 文件,问题解决
目前没有特别的解决方案
节点名称重名 ros::init(argc,argv,””) rospy.init_node(“”) 定义节点名称
在ROS的网络拓扑中,是不可以出现重名的节点的,也不可以启动重名节点或者同一个节点多次
在ROS中给出的解决策略是使用命名空间或名称重映射
命名空间就是为名称添加前缀,名称重映射是为名称起别名
rosrun实现 rosrun设置命名空间
语法: rosrun 包名 节点名 __ns:=新名称
1 2 3 rosrun turtlesim turtlesim_node __ns:=t1 rosrun turtlesim turtlesim_node __ns:=t2 # 注意,这里=后面不可以有空格
rosnode list
1 2 /t1/turtlesim /t2/turtlesim
rosrun名称重映射
语法: rosrun 包名 节点名 __name:=新名称
1 2 rosrun turtlesim turtlesim_node __name:=t1 rosrun turtlesim turtlesim_node __name:=t2
设置命名空间同时名称重映射
1 rosrun turtlesim turtlesim_node __ns:=w1 __name:=t1
使用环境变量也可以设置命名空间,启动节点前在终端键入如下命令
1 2 export ROS_NAMESPACE=sw1 rosrun turtlesim turtlesim_node
launch实现 1 2 3 4 5 6 7 8 9 <launch > <node pkg="turtlesim" type="turtlesim_node" name="turtlesim" output="screen"/> <!-- 名称重映射 --> <node pkg="turtlesim" type="turtlesim_node" name="t1" output="screen"/> <!-- 命名空间 --> <node pkg="turtlesim" type="turtlesim_node" name="turtlesim" output="screen" ns="p1" /> <!-- 命名空间 + 称重映射 --> <node pkg="turtlesim" type="turtlesim_node" name="t2" output="screen" ns="p2" /> </launch>
在 node 标签中,name 属性是必须的,ns 可选
rosnode list
查看节点信息
1 2 3 4 5 /p1/turtlesim /p2/t2 /rosout /t1 /turtlesim
编码实现 c++
重映射:(设置别名)
1 ros::init(argc,argv,"name",ros::init_options::AnonymousName);
在名称后面添加时间戳
命名空间:
1 2 3 std::map<std::string, std::string> map; map["__ns"] = "name"; ros::init(map,"");
python
重映射:(设置别名)
1 rospy.init_node("name",anonymous=True)
话题名称重名 rosrun实现 语法: rorun 包名 节点名 话题名:=新话题名称
实现teleop_twist_keyboard与乌龟显示节点通信方案
方案1:将 teleop_twist_keyboard 节点的话题设置为/turtle1/cmd_vel
1 rosrun teleop_twist_keyboard teleop_twist_keyboard.py /cmd_vel:=/turtle1/cmd_vel
这个时候rosrun turtlesim turtle_teleop_key
也能正常控制,相当于两个发布
方案2:将乌龟显示节点的话题设置为 /cmd_vel
1 rosrun turtlesim turtlesim_node /turtle1/cmd_vel:=/cmd_vel
launch实现 在之前launch文件标签那里讲过,这里就给个代码
语法<remap from="原话题" to="新话题" />
1 2 3 4 5 6 <launch> <node pkg="turtlesim" type="turtlesim_node" name="t1" /> <node pkg="teleop_twist_keyboard" type="teleop_twist_keyboard.py" name="key"> <remap from="/cmd_vel" to="/turtle1/cmd_vel" /> </node> </launch>
1 2 3 4 5 6 <launch> <node pkg="turtlesim" type="turtlesim_node" name="t1"> <remap from="/turtle1/cmd_vel" to="/cmd_vel" /> </node> <node pkg="teleop_twist_keyboard" type="teleop_twist_keyboard.py" name="key" /> </launch>
编码实现 话题的名称与节点的命名空间、节点的名称是有一定关系的,话题名称大致可以分为三种类型:
全局(话题参考ROS系统,与节点命名空间平级)
相对(话题参考的是节点的命名空间,与节点名称平级)
私有(话题参考节点名称,是节点名称的子级)
c++ :
全局名称:以/
开头,和节点名称无关
1 ros::Publisher pub = nh.advertise <std_msgs::String>("/chatter" ,1000 );
相对名称:非/
开头,参考命名空间(与节点名称平级)来确定话题名称
1 ros::Publisher pub = nh.advertise <std_msgs::String>("chatter" ,1000 );
__ns:=设置命名空间后,则话题位于 /name/chatter
私有名称:以~
开头的名称
需要在节点handle创建时候做出些改变
1 2 ros::NodeHandle nh ("~" ) ;ros::Publisher pub = nh.advertise <std_msgs::String>("chatter" ,1000 );
但如果话题以/
开头则话题仍为全局(全局话题优先级更高)
1 2 ros::NodeHandle nh ("~" ) ;ros::Publisher pub = nh.advertise <std_msgs::String>("/chatter" ,1000 );
python :
全局名称:以/
开头,和节点名称无关
1 pub = rospy.Publisher("/chatter" ,String,queue_size=1000 )
相对名称:非/
开头,参考命名空间(与节点名称平级)来确定话题名称
1 pub = rospy.Publisher("chatter" ,String,queue_size=1000 )
私有名称:以~
开头的名称
1 pub = rospy.Publisher("~chatter" ,String,queue_size=1000 )
参数名称重名 在ROS中节点名称话题名称可能出现重名的情况,同理参数名称也可能重名
当参数名称重名时,那么就会产生覆盖
关于参数重名的处理,没有重映射实现,为了尽量的避免参数重名,都是使用为参数名添加前缀的方式,实现类似于话题名称,有全局、相对、和私有三种类型之分
rosrun实现 语法:rosrun 包名 节点名称 _参数名:=参数值
1 rosrun turtlesim turtlesim_node _A:=100
rosparam list 查看节点信息
这种方法创建出来的参数为私有模式
launch实现 在launch文件那部分细讲过了,不再赘述
1 2 3 4 5 6 <launch> <param name="radius" value="0.2" /> <node pkg="turtlesim" type="turtlesim_node" name="t1" ns="xxx"> <param name="radius" value="0.08" /> </node> </launch>
编码实现 c++
使用 ros::param 或者 ros::NodeHandle 来设置参数
具体内容查看 - ROS通信机制 -> 参数服务器
ros::param::set,参数1传入参数名称,参数2传入参数值
1 2 3 ros::param::set ("/set_A" ,100 ); ros::param::set ("set_B" ,100 ); ros::param::set ("~set_C" ,100 );
ros::NodeHandle,首先需要创建 NodeHandle 对象,然后调用该对象的 setParam 函数,该函数参数1为参数名,参数2为要设置的参数值
1 2 3 4 5 6 7 ros::NodeHandle nh; nh.setParam ("/nh_A" ,100 ); nh.setParam ("nh_B" ,100 ); ros::NodeHandle nh_private ("~" ) ;nh_private.setParam ("nh_C" ,100 );
python
1 2 3 rospy.set_param("/py_A" ,100 ) rospy.set_param("py_B" ,100 ) rospy.set_param("~py_C" ,100 )
分布式通信 ROS是一个分布式计算环境,一个运行中的ROS系统可以包含分布在多台计算机上多个节点
ROS对网络配置有某些要求:
所有端口上的所有机器之间必须有完整的双向连接
每台计算机必须通过所有其他计算机都可以解析的名称来公告自己
实现
准备
先要保证不同计算机处于同一网络中,最好分别设置固定IP,如果为虚拟机,需要将网络适配器改为桥接模式
修改有线连接的IPv4
下载包
1 sudo apt install net-tools
查看网络信息
1 2 3 4 5 6 # ros-VirtualBox (ROS1) inet 10.129.163.127 # ip地址 netmask 255.255.248.0 # 子网掩码 # ROS2 inet 10.129.164.127 # ip地址 netmask 255.255.248.0 # 子网掩码
网关和地址一样,但是最后一组数只留1
配置文件修改
分别修改不同计算机的 /etc/hosts 文件,在该文件中加入对方的IP地址和计算机名
1 2 cd /etc sudo gedit hosts
可以通过hostname查看计算机名
设置完毕,可以通过 ping 命令测试网络通信是否正常
1 2 ping 10.129.164.127 #地址 ping ROS2 #计算机名
配置主机
~/.bashrc 追加
1 2 3 4 5 # export ROS_MASTER_URI=http://主机IP:11311 # export ROS_HOSTNAME=主机IP export ROS_MASTER_URI=http://10.129.163.127:11311 export ROS_HOSTNAME=10.129.163.127
配置从机
从机可以有多台,每台都做设置,~/.bashrc 追加
1 2 3 4 5 # export ROS_MASTER_URI=http://主机IP:11311 # export ROS_HOSTNAME=从机IP export ROS_MASTER_URI=http://10.129.163.127:11311 export ROS_HOSTNAME=10.129.164.127
source .bashrc 刷新环境变量
测试
主机启动 roscore(必须)
主机启动订阅节点,从机启动发布节点,测试通信是否正常
反向测试,主机启动发布节点,从机启动订阅节点,测试通信是否正常
ROS常用组件 章主要介绍ROS中内置的如下组件:
TF坐标变换,实现不同类型的坐标系之间的转换;
rosbag 用于录制ROS节点的执行过程并可以重放该过程;
rqt 工具箱,集成了多款图形化的调试工具
案例演示: 小乌龟跟随实现
1 2 3 roslaunch turtle_tf2 turtle_tf2_demo_cpp.launch roslaunch turtle_tf2 turtle_tf2_demo.launch # 如果报错 sudo ln -s /usr/bin/python3 /usr/bin/python
TF坐标变换 机器人系统上有多个传感器,如激光雷达、摄像头等,有的是可以感知机器人周边的物体方位(坐标,横向、纵向、高度的距离信息)的,以协助机器人定位障碍物
但不可以直接将物体相对该传感器的方位信息,等价于物体相对于机器人系统或机器人其它组件的方位信息
ROS 中直接封装了相关的模块: 坐标变换(TransForm Frame,TF)
tf2常用功能包有:
可以借助于rviz显示坐标系关系
新建窗口输入命令rviz
在启动的 rviz 中设置Fixed Frame 为 base_link;
点击左下的 add 按钮,在弹出的窗口中选择 TF 组件,即可显示坐标关系
坐标msg消息 在坐标转换实现中常用的 msg:
geometry_msgs/TransformStamped 传输坐标系相关位置信息
geometry_msgs/PointStamped 传输坐标系内坐标点的信息
c++: 调用PointStamped需要调用tf2_geometry_msgs/tf2_geometry_msgs.h
python: 需要from tf2_geometry_msgs import PointStamped调用
命令行键入:rosmsg info geometry_msgs/TransformStamped
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 std_msgs/Header header #头信息 uint32 seq #|-- 序列号 time stamp #|-- 时间戳 string frame_id #|-- 坐标 ID string child_frame_id #子坐标系的 id geometry_msgs/Transform transform #坐标信息 geometry_msgs/Vector3 translation #偏移量 float64 x #|-- X 方向的偏移量 float64 y #|-- Y 方向的偏移量 float64 z #|-- Z 方向上的偏移量 geometry_msgs/Quaternion rotation #四元数 float64 x float64 y float64 z float64 w # 四元数用于表示坐标的相对姿态
命令行键入:rosmsg info geometry_msgs/PointStamped
1 2 3 4 5 6 7 8 std_msgs/Header header #头 uint32 seq #|-- 序号 time stamp #|-- 时间戳 string frame_id #|-- 所属坐标系的 id geometry_msgs/Point point #点坐标 float64 x #|-- x y z 坐标 float64 y float64 z
静态坐标变换 静态坐标变换,是指两个坐标系之间的相对位置是固定的
需求 :
现有一机器人模型,核心构成包含主体与雷达,各对应一坐标系,坐标系的原点分别位于主体与雷达的物理中心,已知雷达原点相对于主体原点位移关系如下: x 0.2 y0.0 z0.5。当前雷达检测到一障碍物,在雷达坐标系中障碍物的坐标为 (2.0 3.0 5.0),请问,该障碍物相对于主体的坐标是多少?
添加依赖
1 roscpp rospy std_msgs geometry_msgs tf2 tf2_ros tf2_geometry_msgs
c++
发布方:
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 #include "ros/ros.h" #include "tf2_ros/static_transform_broadcaster.h" #include "geometry_msgs/TransformStamped.h" #include "tf2/LinearMath/Quaternion.h" int main (int argc, char *argv[]) { setlocale (LC_ALL,"" ); ros::init (argc,argv,"static_pub" ); ros::NodeHandle nh; tf2_ros::StaticTransformBroadcaster pub; geometry_msgs::TransformStamped tfs; tfs.header.stamp = ros::Time::now (); tfs.header.frame_id = "base_link" ; tfs.child_frame_id = "laser" ; tfs.transform.translation.x = 0.2 ; tfs.transform.translation.y = 0.0 ; tfs.transform.translation.z = 0.5 ; tf2::Quaternion qtn; qtn.setRPY (0 ,0 ,0 ); tfs.transform.rotation.x = qtn.getX (); tfs.transform.rotation.y = qtn.getY (); tfs.transform.rotation.z = qtn.getZ (); tfs.transform.rotation.w = qtn.getW (); pub.sendTransform (tfs); ros::spin (); return 0 ; }
欧拉角:翻滚roll,俯仰pitch,偏航yaw,对应绕xyz轴旋转多少度
转化为四元数是为了避免出现万向节死锁,当俯仰角达到90°时(机头垂直向下),此时横滚(X轴)和偏航(Z轴)重合为同一旋转轴
,无法区分左右倾斜和水平转向
四元数:由1个实部 + 3个虚部组成,这个转化过程计算太复杂了,会用就行
订阅方:
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 #include "ros/ros.h" #include "tf2_ros/transform_listener.h" #include "tf2_ros/buffer.h" #include "geometry_msgs/PointStamped.h" #include "tf2_geometry_msgs/tf2_geometry_msgs.h" int main (int argc, char *argv[]) { setlocale (LC_ALL,"" ); ros::init (argc,argv,"static_sub" ); ros::NodeHandle nh; tf2_ros::Buffer buffer; tf2_ros::TransformListener listener (buffer) ; ros::Rate r (1 ) ; while (ros::ok ()) { geometry_msgs::PointStamped point_laser; point_laser.header.frame_id = "laser" ; point_laser.header.stamp = ros::Time::now (); point_laser.point.x = 2.0 ; point_laser.point.y = 3.0 ; point_laser.point.z = 5.0 ; try { geometry_msgs::PointStamped point_base; point_base = buffer.transform (point_laser,"base_link" ); ROS_INFO ("转换后的坐标值:(%.2f,%.2f,%.2f),参考的坐标系:%s" , point_base.point.x, point_base.point.y, point_base.point.z, point_base.header.frame_id.c_str ()); } catch (const std::exception& e) { ROS_INFO ("程序异常....." ); } r.sleep (); ros::spinOnce (); } return 0 ; }
tos::NodeHandle nh 注册节点到ROS Master,提供访问ROS功能(如话题、服务、参数)的接口,如如果删除 ros::NodeHandle nh,TransformListener
的订阅失败,buffer
收不到TF数据
python
发布方:
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 import rospyimport tf.transformationsimport tf2_rosimport tffrom geometry_msgs.msg import TransformStampedif __name__ == "__main__" : rospy.init_node("static_pub_p" ) pub = tf2_ros.StaticTransformBroadcaster() tfs = TransformStamped() tfs.header.stamp = rospy.Time.now() tfs.header.frame_id = "base_link" tfs.child_frame_id = "laser" tfs.transform.translation.x = 0.2 tfs.transform.translation.y = 0.0 tfs.transform.translation.z = 0.5 qtn = tf.transformations.quaternion_from_euler(0 ,0 ,0 ) tfs.transform.rotation.x = qtn[0 ] tfs.transform.rotation.y = qtn[1 ] tfs.transform.rotation.z = qtn[2 ] tfs.transform.rotation.w = qtn[3 ] pub.sendTransform(tfs) rospy.spin()
订阅方:
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 import rospyimport tf2_rosfrom tf2_geometry_msgs import PointStampedif __name__ == "__main__" : rospy.init_node("static_sub_p" ) buffer = tf2_ros.Buffer() listener = tf2_ros.TransformListener(buffer) rate = rospy.Rate(1 ) while not rospy.is_shutdown(): point_laser = PointStamped() point_laser.header.frame_id = "laser" point_laser.header.stamp = rospy.Time.now() point_laser.point.x = 2.0 point_laser.point.y = 3.0 point_laser.point.z = 5.0 try : point_base = PointStamped() point_base = buffer.transform(point_laser,"base_link" ) rospy.loginfo("转换后的坐标值:(%.2f,%.2f,%.2f),参考的坐标系:%s" , point_base.point.x, point_base.point.y, point_base.point.z, point_base.header.frame_id) except Exception as e: rospy.logerr("异常:%s" ,e) rate.sleep()
python 这里不写spin是因为python没有spinOnce,无法跳过循环
当坐标系之间的相对位置固定时,那么所需参数也是固定的,实现逻辑相同,参数不同,那么 ROS 系统就已经封装好了专门的节点,使用方式如下:
1 rosrun tf2_ros static_transform_publisher x偏移量 y偏移量 z偏移量 z偏航角度 y俯仰角度 x翻滚角度 父级坐标系 子级坐标系
其实父系就是代码中的原点坐标系,子级就是代码中的部件坐标系
1 rosrun tf2_ros static_transform_publisher 0.2 0 0.5 0 0 0 /base_link /laser
建议使用该种方式直接实现静态坐标系相对信息发布
动态坐标变换 需求 :
启动 turtlesim_node,该节点中窗体有一个世界坐标系(左下角为坐标系原点),乌龟是另一个坐标系,键盘控制乌龟运动,将两个坐标系的相对位置动态发布
依赖同静态
c++
发布方:
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 43 44 45 46 47 48 49 #include "ros/ros.h" #include "turtlesim/Pose.h" #include "tf2_ros/transform_broadcaster.h" #include "geometry_msgs/TransformStamped.h" #include "tf2/LinearMath/Quaternion.h" void doPose (const turtlesim::Pose::ConstPtr& pose) { static tf2_ros::TransformBroadcaster pub; geometry_msgs::TransformStamped tfs; tfs.header.frame_id = "world" ; tfs.header.stamp = ros::Time::now (); tfs.child_frame_id = "turtle1" ; tfs.transform.translation.x = pose->x; tfs.transform.translation.y = pose->y; tfs.transform.translation.z = 0 ; tf2::Quaternion qtn; qtn.setRPY (0 ,0 ,pose->theta); tfs.transform.rotation.x = qtn.getX (); tfs.transform.rotation.y = qtn.getY (); tfs.transform.rotation.z = qtn.getZ (); tfs.transform.rotation.w = qtn.getW (); pub.sendTransform (tfs); } int main (int argc, char *argv[]) { setlocale (LC_ALL,"" ); ros::init (argc,argv,"dynamic_pub" ); ros::NodeHandle nh; ros::Subscriber sub = nh.subscribe ("turtle1/pose" ,100 ,doPose); ros::spin (); return 0 ; }
订阅方:
代码和静态的类似,可以直接复制粘贴改
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 43 44 45 46 47 48 49 50 51 52 53 54 55 #include "ros/ros.h" #include "tf2_ros/transform_listener.h" #include "tf2_ros/buffer.h" #include "geometry_msgs/PointStamped.h" #include "tf2_geometry_msgs/tf2_geometry_msgs.h" int main (int argc, char *argv[]) { setlocale (LC_ALL,"" ); ros::init (argc,argv,"dynamic_sub" ); ros::NodeHandle nh; tf2_ros::Buffer buffer; tf2_ros::TransformListener listener (buffer) ; ros::Rate r (1 ) ; geometry_msgs::PointStamped point_turtle; point_turtle.header.frame_id = "turtle1" ; while (ros::ok ()) { point_turtle.header.stamp = ros::Time (0.0 ); point_turtle.point.x = 1 ; point_turtle.point.y = 1 ; point_turtle.point.z = 0 ; try { geometry_msgs::PointStamped point_world; point_world = buffer.transform (point_turtle,"world" ); ROS_INFO ("转换后的坐标值:(%.2f,%.2f,%.2f),参考的坐标系:%s" , point_world.point.x, point_world.point.y, point_world.point.z, point_world.header.frame_id.c_str ()); } catch (const std::exception& e) { ROS_INFO ("程序异常....." ); } r.sleep (); ros::spinOnce (); } return 0 ; }
注意这里ros::Time
不可以用now 了,因为动态使得部件位置一直在发生变化,buffer接收有延迟,使得部件相对位置和检测物体位置的时间戳匹配不上,这个时候就会报错,设置ros::time(0.0)或者空数值就可以避免这个问题(不考虑时间戳继续转化)
python
订阅方:
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 import rospyfrom turtlesim.msg import Poseimport tf2_rosfrom geometry_msgs.msg import TransformStampedimport tf.transformations""" 发布方:需要订阅乌龟的位姿信息,转换成相对于窗体的坐标关系,并发布 准 备 话题:/turtle1/pose 消息:/turtlesim/Pose """ def doPose (pose ): pub = tf2_ros.TransformBroadcaster() tfs = TransformStamped() tfs.header.frame_id = "world" tfs.header.stamp = rospy.Time.now() tfs.child_frame_id = "turtle1" tfs.transform.translation.x = pose.x tfs.transform.translation.y = pose.y tfs.transform.translation.z = 0 qtn = tf.transformations.quaternion_from_euler(0 ,0 ,pose.theta) tfs.transform.rotation.x = qtn[0 ] tfs.transform.rotation.y = qtn[1 ] tfs.transform.rotation.z = qtn[2 ] tfs.transform.rotation.w = qtn[3 ] pub.sendTransform(tfs) pass if __name__ == "__main__" : rospy.init_node("dynamic_pub_p" ) sub = rospy.Subscriber("/turtle1/pose" ,Pose,doPose,queue_size=100 ) rospy.spin()
订阅方:
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 import rospyimport tf2_rosfrom tf2_geometry_msgs import PointStampedif __name__ == "__main__" : rospy.init_node("dynamic_sub_p" ) buffer = tf2_ros.Buffer() listener = tf2_ros.TransformListener(buffer) rate = rospy.Rate(1 ) point_turtle = PointStamped() point_turtle.header.frame_id = "turtle1" while not rospy.is_shutdown(): point_turtle.header.stamp = rospy.Time() point_turtle.point.x = 1 point_turtle.point.y = 1 point_turtle.point.z = 1 try : point_world = buffer.transform(point_turtle,"world" ) rospy.loginfo("转换后的坐标值:(%.2f,%.2f,%.2f)" , point_world.point.x, point_world.point.y, point_world.point.z,) except Exception as e: rospy.loginfo("异常:%s" ,e) rate.sleep()
多坐标系变换 需求 :
现有坐标系统,父级坐标系统 world,下有两子级系统 son1,son2,son1 相对于 world,以及 son2 相对于 world 的关系是已知的,求 son1原点在 son2中的坐标,又已知在 son1中一点的坐标,要求求出该点在 son2 中的坐标
发布方:
1 2 3 4 <launch> <node pkg="tf2_ros" type="static_transform_publisher" name="son1" args="5 1 0 0 0 0 /world /son1" output="screen"/> <node pkg="tf2_ros" type="static_transform_publisher" name="son2" args="-5 -1 0 0 0 0 /world /son2" output="screen"/> </launch>
使用launch文件建立两个子坐标系
订阅方:
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #include "ros/ros.h" #include "tf2_ros/transform_listener.h" #include "tf2_ros/buffer.h" #include "geometry_msgs/PointStamped.h" #include "tf2_geometry_msgs/tf2_geometry_msgs.h" #include "geometry_msgs/TransformStamped.h" int main (int argc, char *argv[]) { setlocale (LC_ALL,"" ); ros::init (argc,argv,"tfs_sub" ); ros::NodeHandle nh; tf2_ros::Buffer buffer; tf2_ros::TransformListener sub (buffer) ; ros::Rate rate (10 ) ; geometry_msgs::PointStamped psAtSon1; psAtSon1. header.frame_id = "son1" ; while (ros::ok ()){ psAtSon1. header.stamp = ros::Time::now (); psAtSon1. point.x = 1.0 ; psAtSon1. point.y = 2.0 ; psAtSon1. point.z = 3.0 ; try { geometry_msgs::PointStamped psAtSon2; geometry_msgs::TransformStamped son1Toson2 = buffer.lookupTransform ("son2" ,"son1" ,ros::Time (0 )); ROS_INFO ("son1 相对 son2 的信息: 父级:%s,子级:%s, 偏移量:(%.2f,%.2f,%.2f)" , son1Toson2. header.frame_id.c_str (), son1Toson2. child_frame_id.c_str (), son1Toson2. transform.translation.x, son1Toson2. transform.translation.y, son1Toson2. transform.translation.z ); psAtSon2 = buffer.transform (psAtSon1,"son2" ); ROS_INFO ("坐标点在Son2中的值:(%.2f,%.2f,%.2f)" , psAtSon2. point.x, psAtSon2. point.y, psAtSon2. point.z); } catch (const std::exception& e) { ROS_INFO ("错误提示:%s" ,e.what ()); } } return 0 ; }
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 43 44 45 46 47 48 49 50 51 import rospyimport tf2_rosfrom geometry_msgs.msg import TransformStampedfrom tf2_geometry_msgs import PointStampedif __name__ == "__main__" : rospy.init_node("frames_sub_p" ) buffer = tf2_ros.Buffer() listener = tf2_ros.TransformListener(buffer) rate = rospy.Rate(1 ) while not rospy.is_shutdown(): try : tfs = buffer.lookup_transform("son2" ,"son1" ,rospy.Time(0 )) rospy.loginfo("son1 与 son2 相对关系:" ) rospy.loginfo("父级坐标系:%s" ,tfs.header.frame_id) rospy.loginfo("子级坐标系:%s" ,tfs.child_frame_id) rospy.loginfo("相对坐标:x=%.2f, y=%.2f, z=%.2f" , tfs.transform.translation.x, tfs.transform.translation.y, tfs.transform.translation.z, ) point_source = PointStamped() point_source.header.frame_id = "son1" point_source.header.stamp = rospy.Time.now() point_source.point.x = 1 point_source.point.y = 1 point_source.point.z = 1 point_target = buffer.transform(point_source,"son2" ,rospy.Duration(0.5 )) rospy.loginfo("point_target 所属的坐标系:%s" ,point_target.header.frame_id) rospy.loginfo("坐标点相对于 son2 的坐标:(%.2f,%.2f,%.2f)" , point_target.point.x, point_target.point.y, point_target.point.z ) except Exception as e: rospy.logerr("错误提示:%s" ,e) rate.sleep()
坐标系关系查看 在机器人系统中,涉及的坐标系有多个,为了方便查看,ros 提供了专门的工具,可以用于生成显示坐标系关系的 pdf 文件,该文件包含树形结构的坐标系图谱
安装指令
1 sudo apt install ros-noetic-tf2-tools
启动坐标系广播程序之后,运行如下命令:
1 rosrun tf2_tools view_frames.py
会产生类似于下面的日志信息:
1 2 [INFO] [1592920556.827549]: Listening to tf data during 5 seconds... [INFO] [1592920561.841536]: Generating graph in frames.pdf file...
查看当前目录会生成一个 frames.pdf 文件
可以直接进入目录打开文件,或者调用命令查看文件:evince frames.pdf
rosbag 机器人传感器获取到的信息,有时我们可能需要时时处理,有时可能只是采集数据,事后分析
机器人导航实现中,可能需要绘制导航所需的全局地图,地图绘制实现,有两种方式
方式1:控制机器人运动,将机器人传感器感知到的数据时时处理,生成地图信息
方式2:控制机器人运动,将机器人传感器感知到的数据留存,事后再重新读取数据,生成地图信息
两种方式比较,显然方式2使用上更为灵活方便
在ROS中关于数据的留存以及读取实现,提供了专门的工具: rosbag
rosbag是用于录制和回放 ROS 主题的一个工具集
本质也是ros的节点,当录制时,rosbag是一个订阅节点,可以订阅话题消息并将订阅到的数据写入磁盘文件;当重放时,rosbag是一个发布节点,可以读取磁盘文件,发布文件中的话题消息
通过命令行 创建目录保存录制的文件 比如根目录下创建bsgs文件夹
开始录制(-a指的是全部)
1 rosbag record -a -O [bags/turtle.bag]
操作小乌龟一段时间,结束录制使用 ctrl + c,在创建的目录中会生成bag文件
此时文件命名为turtle_[时间戳]
查看文件
1 rosbag info [bags/turtle TAB]
回放文件
1 rosbag play [bags/turtle TAB]
1 rosrun rosbag play [bags/turtle TAB]
重启乌龟节点,会发现,乌龟按照录制时的轨迹运动
通过编码 c++
写:
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 "rosbag/bag.h" #include "std_msgs/String.h" int main (int argc, char *argv[]) { setlocale (LC_ALL,"" ); ros::init (argc,argv,"bag_write" ); ros::NodeHandle nh; rosbag::Bag bag; bag.open ("hello.bag" ,rosbag::BagMode::Write); std_msgs::String msg; msg.data = "hello xxx" ; bag.write ("/chatter" ,ros::Time::now (),msg); bag.close (); return 0 ; }
读:
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 #include "ros/ros.h" #include "rosbag/bag.h" #include "rosbag/view.h" #include "std_msgs/String.h" #include "std_msgs/Int32.h" int main (int argc, char *argv[]) { setlocale (LC_ALL,"" ); ros::init (argc,argv,"bag_read" ); ros::NodeHandle nh; rosbag::Bag bag; bag.open ("hello.bag" ,rosbag::BagMode::Read); for (auto &&m : rosbag::View (bag)) { std::string topic = m.getTopic (); ros::Time time = m.getTime (); std_msgs::StringPtr p = m.instantiate <std_msgs::String>(); ROS_INFO ("解析的数据为:话题:%s,时间戳:%.2f,消息:%s" , topic.c_str (), time.toSec (), p->data.c_str ()); } bag.close (); return 0 ; }
python
写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import rospyimport rosbagfrom std_msgs.msg import Stringif __name__ == "__main__" : rospy.init_node("write_bag_p" ) bag = rosbag.Bag("hello_p.bag" ,'w' ) msg = String() msg.data = "hello bag" bag.write("/chatter" ,msg) bag.close()
读:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import rospyimport rosbagfrom std_msgs.msg import Stringif __name__ == "__main__" : rospy.init_node("read_bag_p" ) bag = rosbag.Bag("hello_p.bag" ,'r' ) msgs = bag.read_messages(topics="/chatter" ) for topic,msg,time in msgs: rospy.loginfo("话题:%s,消息:%s,时间:%s" ,topic,msg,time) bag.close()
rqt工具箱 ROS基于 QT 框架,针对机器人开发提供了一系列可视化的工具,这些工具的集合就是rqt
rqt 工具箱组成有三大部分
rqt——核心实现,开发人员无需关注
rqt_common_plugins——rqt 中常用的工具套件
rqt_robot_plugins——运行中和机器人交互的插件(比如: rviz)
基本使用 一般只要安装的是desktop-full版本就会自带工具箱
如果需要安装可以以如下方式安装
1 2 sudo apt-get install ros-noetic-rqt sudo apt-get install ros-noetic-rqt-common-plugins
rqt
的启动方式有两种:
方式1:rqt
方式2:rosrun rqt_gui rqt_gui
启动 rqt 之后,可以通过 plugins 添加所需的插件
常用插件
rqt_graph:plugins/introspection/Node Graph or rqt_graph
圆框是节点,方框是话题,箭头是数据的订阅发布方向
rqt_console:plugins/Logging/Console or rqt_console
ROS 中用于显示和过滤日志的图形化插件
编写 Node 节点输出各个级别的日志信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include "ros/ros.h" int main (int argc, char *argv[]) { ros::init (argc,argv,"log_demo" ); ros::NodeHandle nh; ros::Rate r (0.3 ) ; while (ros::ok ()) { ROS_DEBUG ("Debug message d" ); ROS_INFO ("Info message oooo" ); ROS_WARN ("Warn message wwww" ); ROS_ERROR ("Erroe message EEEE" ); ROS_FATAL ("Fatal message FFFF" ); r.sleep (); } return 0 ; }
rqt_plot:plugins/Visualization/plot or rqt_plot
图形绘制插件,可以以 2D 绘图的方式绘制发布在 topic 上的数据
rqt_bag:plugins/Logging/Bag or rqt_bag
录制和重放 bag 文件的图形化插件