一周看完了古月老师的ROS入门21讲,理清了ROS工作空间和各个功能包是如何相互通讯的,还有如何自建功能包编写内部的代码等。本文跳过Linux命令行、ROS安装等环境搭建问题,重点记录如何写ROS的程序。
在home中创建项目文件夹,在项目文件夹中再创建src文件夹,随后进入src文件夹中进行工作空间的初始化,和功能包的创建,最后到工作空间文件夹中进行编译,最后还需要设置环境变量。具体的命令行代码如下:
mkdir -p ~/catkin_ws/src
cd ~/catkin_ws/src
catkin_init_workspace
catkin_pkg_create pkg_name depend1 depend2 ....
cd ..
catkin_make
source ~/catkin_ws/devel/setup.bash
echo "source /WORKSPACE/devel/setup.bash">>~/.bashrc //添加到系统环境变量中
后续的所有代码编写都会在此工作空间下和功能包内进行。
话题是发布者Publisher和订阅者Subscriber相互之间传输消息Messages的通道,是一种单向的通讯,一个话题可以包括多个msg对象,需要掌握消息,发布者和订阅者的编写方法。可以用C++或者Python编写发布者和订阅者,C++需要在CMakelist文件中进行修改,使得编译成功;而Python无需编译但需要改执行权限。以下代码使用C++编写,下面做整体的总结。
在功能包内创建msg文件夹,并在其中创建.msg文件,文件名即对象名,如asoul.msg即创建asoul这个新对象。 随后需要在功能包内的CMakelist.txt和package.xml文件中配置依赖和编译规则。
每个消息都是一种严格的数据结构,支持标准数据类型(整型、浮点型、布尔型等),也支持嵌套结构和数组(类似于C语言的结构体struct),还可以根据需求由开发者自主定义。——《ROS机器人开发实践》
MESSAGE.msg
string name
uint8 age
…………
CMakelist.txt
find_package(..... message_generation)
add_message_files(FILES MESSAGE.msg)
generate_messages(DEPENDENCIES std_msgs)
catkin_package(... message_runtime)
package.xml
<build_depend>message_generationbuild_depend>
<exec_depend>message_runtimeexec_depend>
需要在功能包的src文件下创建.cpp文件,文件名最好与发布名相同,以便后续的使用。不同于message,发布者仅需要对CMakelist.txt文件进行配置。
PUBLISHER.cpp
#include
#include //发布某个功能包下的某个消息
int main(int argc,char *argv){
// 节点初始化
ros::init(argc,argv,"node_name");
//创建节点句柄
ros::NodeHandle n;
//创建Publisher对象,发布名为/topic_name的topic,此处topic可以选择已存在的topic,消息类型是msgs定义的类,队列长度为10
ros::Publisher publisher_name = n.advertise<msgs的类型>("/topic_name",10);
//设置循环频率,10ms
ros::Rate loop_rate(10);
while(ros::ok()){
//初始化msgs下类型的消息,及新建msg对象并给对象变量赋值
msgs::asoul idol;
idol.name = "";
idol.age = "";
//发布消息
publisher_name.publish(idol);
ROS_INFO() //日志内容
//按照循环频率延时
loop_rate.sleep();
}
return 0;
}
CMakelist.txt
add_executable(PUBLISHER src/PUBLISHER.cpp)
target_link_libraries(PUBLISHER ${catkin_LIBRARIES})
与发布者相同,需要在功能包的src文件夹下创建.cpp文件,也需要对CMakelist.txt文件进行配置。
SUBSCRIBER.cpp
#include
#include //订阅某个功能包下的某个消息
// 接收到订阅的消息类型后,会进入回调函数
void callback(const package::msg_name::ConstPtr& msg){
//回调函数下的任务不可以过于复杂,否则会在处理数据的时候丢失发布者的实时数据
ROS_INFO();
}
int main(int argc,char *argv){
// 节点初始化
ros::init(argc,argv,"node_name");
//创建节点句柄
ros::NodeHandle n;
//创建Subscriber对象,订阅/topic_name的topic,注册回调函数
ros::Subscriber subscriber_name = n.subscribe("/topic_name",10,callback);
//循环等待回调函数
ros::spin();
return 0;
}
CMakelist.txt
add_executable(SUBSCRIBER src/SUBSCRIBER.cpp)
target_link_libraries(SUBSCRIBER ${catkin_LIBRARIES})
服务不同于话题,是一种同步通信的方式,允许客户端(Client)向服务端发送请求(Request),同时服务端(Server)处理后反馈应答(Response)。与话题相似也需要掌握服务数据,客户端和服务端的编写方法,编写完成也需要进行文件配置。
在功能包内创建srv文件夹,并在其中创建.srv文件,srv文件包括请求和应答两个数据域,数据类型与消息msg类似,但需要使用---
进行数据域的分割。同样也需要在功能包内的package.xml和CMakelist.txt文件中配置依赖和编译规则。
Test.srv
int64 id
............... //客户端发送请求的数据类型
---
String name
............... //服务端发送应答的数据类型
CMakelist.txt
find_package(..... message_generation)
add_message_files(FILES Test.srv)
generate_messages(DEPENDENCIES std_msgs)
catkin_package(... message_runtime)
package.xml
<build_depend>message_generationbuild_depend>
<exec_depend>message_runtimeexec_depend>
同样需要在功能包下的src文件夹内创建.cpp文件,
CLIENT.cpp
#include
#include //发布某个功能包下的某个服务数据
int main(int argc, char** argv){
//初始化ROS节点
ros::init(argc, argv, "Node_NAME");
//创建节点句柄
ros::NodeHandle n;
//等待SERVER服务端启动,创建客户端并连接
ros::service::waitForService("/SERVER_NAME");
ros::ServiceClient client = n.serviceClient<package_name::srv_name>("/SERVER_NAME");
//初始化请求数据,即给srv请求数据赋值
package_name::srv_name srv;
srv.request.a = 2;
.....
//请求服务调用 call waitForService spin均为阻塞型函数,若没有反馈后续代码不会运行
client.call(srv);
//如果得到服务器的反馈,则可以运行查看是否调用成功srv.response
ROS_INFO("%s",srv.response.name.c_str());
return 0;
}
CMakelist.txt
add_executable(CLIENT src/CLIENT.cpp)
target_link_libraries(CLIENT ${catkin_LIBRARIES})
古月老师在21讲的服务端加入了发布者,来实现小乌龟的绕圈运动。服务和话题可以同时写入一个文件中使用,从而实现我们想要的功能。由于是总结性笔记,此处仅仅编写如何接受客户端发送的请求,和如何将回应发出,具体代码如下。
SERVER.cpp
#include
#include //发布某个功能包下的某个服务数据
//回调函数,输入参数req,输出参数res
void callback(package_name::srv_name::Request &req, package_name::srv_name::Response &res){
//与订阅者的回调函数相似,区别在于有两个数据进入函数,可以获取请求数据进行查询或计算,还可以设置反馈数据
ROS_INFO("%d", req.id);
res.name = "JR";
.......
}
int main(int argc, char **argv){
//ROS节点初始化
ros::init(argc, argv, "Node_Name");
//创建节点句柄
ros::NodeHandle n;
//创建名为/Server_name的server,注册回调函数
ros::ServiceServer server = n.advertiseService("/Server_name", callback);
//循环等待回调函数
ROS_INFO("");
ros::spin();
//如果在while循环内查询队列,则需要使用ros::spinOnce()方法,使得while正常运行
return 0;
}
CMakelist.txt
add_executable(SERVER src/SERVER.cpp)
target_link_libraries(SERVER ${catkin_LIBRARIES})
在机器人设计和机器人应用过程中,都会涉及到不同组件的位置和姿态,这就需要引入坐标系以及坐标变换的概念。TF功能包是一个让用户随时间跟踪多个坐标系的功能包,使用树形数据结构,根据时间缓冲并维护多个坐标系之间的坐标变换关系,可以帮助我们在任意时间完成坐标系之间的点、向量等坐标变换。而使用TF功能包需要编写广播(Broadcaster)和监听(Listener)两个部分。
古月老师使用了小乌龟示例,更容易理解,此处仅编写如何将坐标系和坐标变换广播出去。同样的需要在功能包的src下建立.cpp文件,对功能包内的CMakelist.txt配置编译规则。
Broadcaster.cpp
#include
#include
#include //此处需要添加需要广播的坐标数据
//回调函数中的msg可能包含x,y,z和r,p,y,存在则用msg->赋值
void poseCallback(const package_name::pose_msgConstPtr& msg){
//创建tf的广播器
static tf::TransformBroadcaster br;
//初始化tf数据,即创建坐标变换值
tf::Transform transform;
transform.setOrigin(tf::Vector3(msg->x,msg->y,msg->z)); //设置坐标变换平移
tf::Quaternion q; //此处采用RPY设置坐标变换旋转,即xyz三轴旋转的方式,还可以直接使用四元数
q.setRPY(roll, pitch, yaw);
transform.setRotation(q) //transform.setRotation(tf::Quaternion( , , , ));四元数方法
//广播两个坐标系之间的tf数据
br.sendTransform(tf::StampedTransform(transform, ros::Time::now(), "frame1", "frame2"));
}
int main(int argc, char** argv){
//初始化ROS节点
ros::init(argc, argv, "node_name");
//创建节点句柄
ros::NodeHandle n;
//订阅存有坐标信息的位姿话题
ros::Subscriber sub = n.subscribe("",10.&poseCallback);
//循环等待回调函数
ros::spin();
return 0;
}
CMakelist.txt
add_executable(Broadcaster src/Broadcaster.cpp)
target_link_libraries(Broadcaster ${catkin_LIBRARIES})
监听器可以用于监听和计算tf数据,在此同样仅仅写出监听相关的代码,不对具体的应用做拓展。
Listener.cpp
#include
#include
int main(int argc, char **argv){
//初始化ROS节点
ros::init(argc, argv, "node_name");
//创建节点句柄
ros::NodeHandle n;
//获取frame1和frame2之间的坐标变换关系
tf::TransformListener listener;
while(ros.ok()){
tf::StampedTransform transform;
try{
listener.waitForTransform("/frame1", "/frame2", ros::Time(0), ros::Duration(3.0)); //
listener.lookupTransform("/frame1", "/frame2", ros::Time(0),transform);
}catch{
ROS_ERROR("%s",ex.what());
ros::Duration(1.0).sleep();
continue;
}
//使用transform中的数据进行运算
float angular = atan2(transform.getOrigin().x(),transform.getOrigin().y());
float linear = sqrt(pow(transform.getOrigin().x(),2))+
pow(transform.getOrigin().y(),2));
ROS_INFO(angular, linear);
}
return 0;
}
CMakelist.txt
add_executable(Listener src/Listener.cpp)
target_link_libraries(Listener ${catkin_LIBRARIES})
启动文件可以同时启动多个节点并自动启动ROS Master节点管理器,并且可以实现每个节点的配置,为多节点的操作提供很大的便利性。在此写一个示例,简单的列举下基本元素如node、launch、param、arg等。每个元素内部的属性作用需要参考古月老师的《ROS机器人开发实践》或是参考launch文件解析
Test.launch
<launch>
<---param为全局字典中的参数,arg为launch文件下使用的参数--->
<param name="param_name" value=""/>
<arg name="argument_name" value="">
<node pkg="package_name" type="node_file_name" name="node_name">
<param name="argTest" value="&(arg argument_name)"/>
<rosparam file="FILE_PATH/test.yaml" command=load/>
node>
<node pkg="package_name" type="node_file_name" name="node_name" args="argument" output="screen"/>
launch>
至此算是了解了ROS编程的一些皮毛,仍然需要进一步在应用中实践。上述并非21讲全部内容,Ubuntu和ROS的安装在古月老师的21讲中也有详细介绍,此外还有Qt、Rivz、Gezabo等可视化工具的应用。十分感谢古月老师的课程和书籍,让ROS的学习之路变得相对平坦。
怕什么真理无穷,进一步有进一步的欢喜。