操作系统期末复习笔记
ros

编译工作空间 CMake
首先在工作空间 catkin_ws/src/ 下递归的查找其中每一个ROS的package。
2.package中会有 package.xml 和 CMakeLists.txt 文件,Catkin(CMake)编译系统依据 CMakeLists.txt 文件,从而生成 makefiles (放在 catkin_ws/build/)。
3.然后 make 刚刚生成的 makefiles 等文件,编译链接生成可执行文件(放在 catkin_ws/devel )。
也就是说,Catkin就是将 cmake 与 make 指令做了一个封装从而完成整个编译过程的工具。catkin有比较突出的优点,主要是:操作更加简单一次配置,多次使用跨依赖项目编译。
要用catkin编译一个工程或软件包,只需要用 catkin_make 指令。一般当我们写完代码,执行一次 catkin_make 进行编译,调用系统自动完成编译和链接过程,构建生成目标文件
cd ~/ros_workspace/
catkin_make
source ~/ros_workspace/devel/setup.bash
任何ROS程序只有组织成package功能包才能编译
Package结构:
一个package下常见的文件、路径有:
├── CMakeLists.txt #package的编译规则(必须)
├── package.xml #package的描述信息(必须)
├── src/ #源代码文件
├── include/ #C++头文件
├── scripts/ #可执行脚本
├── msg/ #自定义消息
├── srv/ #自定义服务
├── models/ #3D模型文件
├── urdf/ #urdf文件
├── launch/ #launch文件
其中定义package的是 CMakeLists.txt 和 package.xml ,这两个文件是package中必不可少的。catkin编译系统在编译前,首先就要解析这两个文件。这两个文件就定义了一个package。
- CMakeLists.txt: 定义package的包名、依赖、源文件、目标文件等编译规则,是package不可少的成分;
- package.xml: 描述package的包名、版本号、作者、依赖等信息,是package不可少的成分;
- src/: 存放ROS的源代码,包括C++的源码和(.cpp)以及Python的module(.py);
- include/: 存放C++源码对应的头文件;
- scripts/: 存放可执行脚本,例如shell脚本(.sh)、Python脚本(.py);
- msg/: 存放自定义格式的消息(.msg);
- srv/: 存放自定义格式的服务(.srv);
- models/: 存放机器人或仿真场景的3D模型(.sda, .stl, .dae等);
- urdf/: 存放机器人的模型描述(.urdf或.xacro);
- launch/: 存放launch文件(.launch或.xml);
Package功能包创建
catkin_create_pkg package depends
cd ~/ros_workspace /src #切到功能包创建目录
catkin_create_pkg test_pkg roscpp rospy std_msgs #创建名字为test_pkg 的功能包,依赖了roscpp,rospy,std_msgs
这样就会在当前路径下新建 test_pkg 软件包,包括:
├── CMakeLists.txt
├── include
│ └── test_pkg
├── package.xml
└── src
catkin_create_pkg 帮你完成了软件包的初始化,填充好了 CMakeLists.txt 和 package.xml ,并且将依赖项填进了这两个文件中。
Package相关命令
rospack:
rospack是对package管理的工具,命令的用法如下:
表2.1 rospack命令
rospack命令 | 作用 |
---|---|
rospack help | 显示rospack的用法 |
rospack list | 列出本机所有package |
rospack depends [package] | 显示package的依赖包 |
rospack find [package] | 定位某个package |
rospack profile | 刷新所有package的位置记录 |
roscd:
roscd 命令类似与Linux系统的 cd ,改进之处在于 roscd 可以直接 cd 到ROS的软件包。
表2.2 roscd命令
roscd命令 | 作用 |
---|---|
roscd [pacakge] | cd到ROS package所在路径 |
rosls:
rosls 也可以视为Linux指令 ls 的改进版,可以直接 ls ROS软件包的内容。
表2.3 rosls命令
rosls命令 | 作用 |
---|---|
rosls [pacakge] | 列出ROS package下的文件 |
rosdep:
rosdep 是用于管理ROS package依赖项的命令行工具,用法如下:
表2.4 rosdep命令
一个较常使用的命令是 rosdep install –from-paths src –ignore-src –rosdistro=kinetic -y ,用于安装工作空间中 src 路径下所有package的依赖项(由pacakge.xml文件指定)。
node
最小的进程单元就是节点(可执行文件在运行之后就成了一个进程(process),这个进程在ROS中就叫做节点)
从程序角度来说,node就是一个可执行文件(通常为C++编译生成的可执行文件、Python脚本)被执行,加载到了内存之中;
当执行一个ROS程序,就被加载到了内存中,就成为了一个进程,在ROS里叫做节点
分布式
节点管理器master
ROS为机器人开发者们提供了不同语言的编程接口,比如C++接口叫做roscpp,Python接口叫做rospy,Java接口叫做rosjava。
cpp编写
#include "ros/ros.h"
#include "std_msgs/String.h"
#include "sstream"
int main(int argc, char **argv)
{// 初始化 ROS 节点,节点名为 "talker"
ros::init(argc, argv, "talker");// 创建一个节点句柄
ros::NodeHandle n;// 创建一个 Publisher,发布到话题 "chatter",消息类型为 std_msgs::String,队列大小为 1000
ros::Publisher chatter_pub = n.advertise<std_msgs::String>("chatter", 1000);// 设置循环频率为 10Hz
ros::Rate loop_rate(10);
int count = 0;// 当 ROS 节点正常运行时,进入循环
while (ros::ok())
{// 创建一个 std_msgs::String 类型的消息
std_msgs::String msg;
std::stringstream ss;// 将 "hello world" 和当前计数值 count 写入字符串流
ss << "hello world " << count;
msg.data = ss.str();// 打印消息内容到终端
ROS_INFO("%s", msg.data.c_str());// 发布消息到 "chatter" 话题
chatter_pub.publish(msg);// 调用一次回调函数,处理所有已经到来的消息
ros::spinOnce();// 按照设定的频率休眠
loop_rate.sleep();// 计数值加 1
++count;
}
return 0;
}
每秒发布 10 条内容为 “hello world x” 的消息,其中 x
是从 0 开始递增的计数值。
CMakeLists.txt文件修改
add_executable(${PROJECT_NAME}_node src/test_pkg_node.cpp){PROJECT_NAME}_node为生成可执行程序的名字,该名字可以任意指定。
src/node.cpp:为编译要使用的源码的文件名。
target_link_libraries(${PROJECT_NAME}_node ${catkin_LIBRARIES})
{PROJECT_NAME}_node是在add_executable中生成的可执行文件
编译
roscore//打开Master
rosrun test_pkg test_pkg_node
rosnode命令
头文件引用
打开终端输入以下命令进入要创建头文件的目录:
cd ~/ros_workspace/src/test_pkg/include/test_pkg
输入以下命令创建头文件:
gedit test_pkg.h
输入以下代码:
#ifndef _TEST_PKG_
#define _TEST_PKG_
#define TEST_PKG_VER "1.0.0"
#define INIT_COUNT 100
int addTwoNum(int a,int b);
#endif
完成后保存关闭头文件,修改test.cpp,如下代码:
#include "ros/ros.h"
#include "std_msgs/String.h"
#include "test_pkg/test_pkg.h" //自定义头文件
#include <sstream>
int addTwoNum(int a,int b)
{
return a+b;
}//头文件函数实现部分
int main(int argc, char *argv[])
{
ros::init(argc, argv, "talker");//这个名字是给系统看的
ros::NodeHandle n;
ros::Publisher chatter_pub = n.advertise <std_msgs::String >("chatter", 1000);
ros::Rate loop_rate(10);
int count = INIT_COUNT;//调用了头文件中的宏定义
ROS_INFO("test_pkg version:%s",TEST_PKG_VER);
while (ros::ok())
{
std_msgs::String msg;
std::stringstream ss;
ss << "hello world " << count;
msg.data = ss.str();
ROS_INFO("%s", msg.data.c_str());chatter_pub.publish(msg);
ros::spinOnce();
loop_rate.sleep();
++count;
}
return 0;
}
需要修改修改test_pkg 中的 CMakeLists.txt
此处的include是一个相对路径,指的是当前功能包test_pkg下的include
引用同一工作空间内其他软件包的头文件
my_pkg的目的是要引用test_pkg中的自定义头文件,因此这里创建my_pkg的时候要对test_pkg进行依赖。
my_pkg/src路径下创建源码文件my_pkg.cpp
#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>
#include "test_pkg/test_pkg.h" //自定义头文件
int main(int argc, char *argv[])
{
ros::init(argc, argv, "my_pkg");
ros::NodeHandle n;
ros::Publisher chatter_pub = n.advertise<std_msgs::String>("my_chatter", 1000);
ros::Rate loop_rate(10);
int count = INIT_COUNT;//调用了头文件中的宏定义
ROS_INFO("test_pkg version:%s,init count:%d",TEST_PKG_VER,INIT_COUNT);//将其他软件包头文件中声明的宏定义打印出来
while (ros::ok())
{
std_msgs::String msg;
std::stringstream ss;
ss << "my_pkg " << count;
msg.data = ss.str();
ROS_INFO("%s", msg.data.c_str());
chatter_pub.publish(msg);
ros::spinOnce();
loop_rate.sleep();
++count;
}
return 0;
}
对工作空间进行编译:
cd ~/ros_workspace/
catkin_make
编译完成之后可以运行节点查看结果。
roscore
rosrun my_pkg my_pkg_node
launch文件编写
roslaunch工具来调用launch文件,执行这个脚本文件就一次性启动所有的节点程序。
launch文件同样也遵循着xml格式规范,是一种标签文本。
每个XML文件都必须要包含一个根元素,根元素由一对launch标签定义:
<launch>
<node pkg="test_pkg" type="test_pkg_node" name="test_pkg_node" output="screen"/>
<node pkg="my_pkg" type="my_pkg_node" name="my_pkg_node" output="screen"/>
</launch>
其中,output=“screen”属性可以将单个节点的标准输出到终端而不是存储在日志文件中。
<launch>
<node pkg="test_pkg" type="test_pkg_node" name="test_pkg_node" respawn="true" output="screen"/>
<node pkg="my_pkg" type="my_pkg_node" name="my_pkg_node" required="true" output="screen"/>
<include file="$(find third_pkg)/launch/third_pkg.launch"/>
</launch>
#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>
int main(int argc, char **argv)
{// 初始化 ROS 节点,节点名为 "third_pkg"
ros::init(argc, argv, "third_pkg");
// 创建一个节点句柄
ros::NodeHandle n;
// 创建一个 Publisher,发布到话题 "third_pkg",消息类型为 std_msgs::String,队列大小为 1000
ros::Publisher chatter_pub = n.advertise<std_msgs::String>("third_pkg", 1000);
// 设置循环频率为 10Hz
ros::Rate loop_rate(10);
int count = 0;
// 当 ROS 节点正常运行时,进入循环
while (ros::ok())
{// 创建一个 std_msgs::String 类型的消息
std_msgs::String msg;
std::stringstream ss;
// 将 "third pkg:" 和当前计数值 count 写入字符串流
ss << "third pkg: " << count;
msg.data = ss.str();
// 打印消息内容到终端
ROS_INFO("%s", msg.data.c_str());
// 发布消息到 "third_pkg" 话题
chatter_pub.publish(msg);
// 调用一次回调函数,处理所有已经到来的消息
ros::spinOnce();
// 按照设定的频率休眠
loop_rate.sleep();
// 计数值加 1
++count;
}
return 0;
}
话题通讯 -话题
topic通讯原理
topic是一种点对点的单向通信方式,这里的“点”指的是node,也就是说node之间可以通过topic方式来传递信息
topic要经历下面几步的初始化过程:
首先publisher节点和subscriber节点都要到节点管理器进行注册;
然后publisher会发布topic,subscriber在master的指挥下会订阅该topic;
从而建立起sub-pub之间的通信。
注意整个过程是单向的。其结构示意图如下。
Subscriber接收消息会进行处理,一般这个过程叫做回调(Callback)。所谓回调就是提前定义好了一个处理函数(写在代码中),当有消息来就会触发这个处理函数,函数会对消息进行处理。
topic通信属于一种异步的通信方式 各司其责,不存在协同工作,我们称这样的通信方式是异步的
ROS是一种分布式的架构,一个topic可以被多个节点同时发布,也可以同时被多个节点接收
分布式系统通信的好处:扩展性好、软件复用率高
- topic通信方式是异步的,发送时调用publish()方法,发送完成立即返回,不用等待反馈。
- subscriber通过回调函数的方式来处理消息。
- topic可以同时有多个subscribers,也可以同时有多个publishers。
通讯示例
输入以下命令在ros_workspace中创建第四个功能包subscribe_pkg。
cd ~/ros_workspace/src/
catkin_create_pkg subscribe_pkg rospy roscpp std_msgs
#include "ros/ros.h"
#include "std_msgs/Int32.h"
void chatterCallback(const std_msgs::Int32::ConstPtr& msg)
{
ROS_INFO("I heard name:[%d]",msg->data);
}
int main(int argc,char **argv)
{
ros::init(argc,argv,"subscribe_node");
ros::NodeHandle n;
ros::Subscriber sub = n.subscribe("third_pkg_topic",1000,chatterCallback);
ros::spin();
return 0;
}
当有消息发布到该话题时,调用回调函数 chatterCallback
并打印消息的内容
#include "ros/ros.h"
#include "std_msgs/Int32.h"
int main(int argc, char **argv)
{
// 初始化 ROS 节点,节点名为 "third_pkg"
ros::init(argc, argv, "third_pkg");
// 创建一个节点句柄
ros::NodeHandle n;
// 创建一个 Publisher,发布到话题 "third_pkg_topic",队列大小为 1000
ros::Publisher chatter_pub = n.advertise<std_msgs::Int32>("third_pkg_topic", 1000);
// 设置循环频率为 2Hz
ros::Rate loop_rate(2);
// 初始化计数器
int count = 0;
// 当 ROS 仍在运行时,循环执行以下操作
while (ros::ok())
{
// 创建一个消息对象
std_msgs::Int32 msg;
// 将计数器的值赋给消息的数据字段
msg.data = count;
// 打印消息的数据
ROS_INFO("%d", msg.data);
// 将消息发布到话题 "third_pkg_topic"
chatter_pub.publish(msg);
// 处理回调函数(如果有的话)
ros::spinOnce();
// 按照设定的频率休眠
loop_rate.sleep();
// 计数器递增,且在 0 到 4 之间循环
count = (++count) % 5;
}
return 0;
}
(0 到 4)到 “third_pkg_topic” 话题,并且每秒发布 2 次。
理解自定义消息类型
话题是ROS提供的一种重要通讯方式,话题上传输的信息内容即为消息
话题的类型由消息的数据类型决定,ROS提供了组成消息的基本数据类型。
int8, int16, int32, int64 (plus uint*)
float32, float64
string
time, duration
other msg files
variable-length array[] and fixed-length array[C]
在third_pkg功能包下创建msg文件夹并在msg文件夹下新建myTestMsg.msg文件。
打开myTestMsg.msg文件输入如图4.20内容。
图4.20 myTestMsg.msg文件内容
图4.21 CMakeLists.txt文件修改内容
图4.22 package.xml文件修改内容
修改third_pkg.cpp
#include "ros/ros.h"
#include "third_pkg/myTestMsg.h"//添加自定义消息的头文件
int main(int argc,char **argv)
{
ros::init(argc,argv,"third_pkg");
ros::NodeHandle n;// 更新话题的消息格式为自定义的消息格式
ros::Publisher chatter_pub = n.advertise<third_pkg::myTestMsg>("third_pkg_topic",1000);
ros::Rate loop_rate(2);
while(ros::ok())
{
third_pkg::myTestMsg msg;//声明一个自定义消息的对象
msg.name = "corvin";//填充消息内容
msg.age = 20;
msg.handsome = true;
msg.salary = 123.45;
chatter_pub.publish(msg);//将消息发布到话题中
ros::spinOnce();
loop_rate.sleep();
}
return 0;
}
修改订阅端代码subscribe.cpp如下,黑色字体为修改部分要格外注意:
#include "ros/ros.h"
#include "third_pkg/myTestMsg.h"
// 回调函数,当接收到消息时调用
void chatterCallback(const third_pkg::myTestMsg::ConstPtr& msg)
{
// 打印接收到的消息内容
ROS_INFO("I heard - name: %s, age: %d, isHandsome: %d, salary: %f",
msg->name.c_str(), msg->age, msg->handsome, msg->salary);
}
int main(int argc, char **argv)
{
// 初始化 ROS 节点,节点名为 "subscribe_node"
ros::init(argc, argv, "subscribe_node");
// 创建一个节点句柄
ros::NodeHandle n;
// 创建一个 Subscriber,订阅 "third_pkg_topic" 话题,队列大小为 1000,回调函数为 chatterCallback
ros::Subscriber sub = n.subscribe("third_pkg_topic", 1000, chatterCallback);
// 进入循环等待回调
ros::spin();
return 0;
}
ROS通讯机制-服务
service方式在通信模型上与topic做了区别。Service通信是双向的,它不仅可以发送消息,同时还会有反馈。所以service包括两部分,一部分是请求方(Clinet),另一部分是应答方/服务提供方(Server)。这时请求方(Client)就会发送一个request,要等待server处理,反馈回一个reply,这样通过类似“请求-应答”的机制完成整个服务通信。
Node B是server(应答方),提供了一个服务的接口,叫做 /Service ,我们一般都会用string类型来指定service的名称,类似于topic。Node A向Node B发起了请求,经过处理后得到了反馈。
Service是同步通信方式,所谓同步就是说,此时Node A发布请求后会在原地等待reply,直到Node B处理完了请求并且完成了reply,Node A才会继续执行。Node A等待过程中,是处于阻塞状态的成通信。这样的通信模型没有频繁的消息传递,没有冲突与高系统资源的占用,只有接受请求才执行服务,简单而且高效。
表5.1 topic与service的对比
注意:远程过程调用(Remote Procedure Call,RPC),可以简单通俗的理解为在一个进程里调用另一个进程的函数。
示例
在third_pkg功能包下创建srv工作目录并在该目录下创建myTestSrv.srv文件
打开myTestSrv.srv文件输入以下内容:
int32 index
---
int32 result
两个部分用“—”分开修改third_pkg功能包的CMakeLists.txt
这里和自定义消息的修改方法类似:
- 添加message_generation依赖
- 添加.srv服务文件
- 添加消息生成依赖
修改third_pkg.cpp
int main(int argc, char **argv)
{// 初始化 ROS 节点,节点名为 "third_pkg"
ros::init(argc, argv, "third_pkg");// 创建一个节点句柄
ros::NodeHandle n;// 创建一个 Publisher,发布自定义消息到 "third_pkg_topic" 话题,队列大小为1000
ros::Publisher chatter_pub = n.advertise<third_pkg::myTestMsg>("third_pkg_topic", 1000);// 定义并初始化服务端,服务名为 "batteryStatus",服务回调函数为 checkBattery
ros::ServiceServer service = n.advertiseService("batteryStatus", checkBattery);// 设置发送话题的频率为 2Hz,即每0.5秒发送一条消息
ros::Rate loop_rate(2);// 循环运行,直到 ROS 关闭
while (ros::ok())
{// 电池电量减少
battery--;// 如果电池电量小于10,则重置为100
if (battery < 10) battery = 100;// 声明一个自定义消息对象
third_pkg::myTestMsg msg;// 填充消息数据
msg.name = "corvin";
msg.age = 20;
msg.handsome = true;
msg.salary = 123.45;// 将消息发布到话题中
chatter_pub.publish(msg);// 处理回调函数
ros::spinOnce();// 休眠以维持循环频率
loop_rate.sleep();
}
return 0;
}
修改订阅者subscribe.cpp
#include "ros/ros.h"
#include "third_pkg/myTestMsg.h" // 自定义消息的头文件
#include "third_pkg/myTestSrv.h" // 自定义服务的头文件
// 订阅者的回调函数,用于处理接收到的消息
void chatterCallback(const third_pkg::myTestMsg::ConstPtr& msg)
{
ROS_INFO("I heard-name:%s,age:%d,ishandsome:%d,salary:%f",
msg->name.c_str(), msg->age, msg->handsome, msg->salary);}
int main(int argc, char **argv)
{// ROS 初始化,节点名为 "subscribe_node"
ros::init(argc, argv, "subscribe_node");// 创建节点句柄
ros::NodeHandle n;// 设置循环频率为 1Hz
ros::Rate loop_rate(1);// 创建一个订阅者,订阅 "third_pkg_topic" 话题,队列大小为 1000,回调函数为 chatterCallback
ros::Subscriber sub = n.subscribe("third_pkg_topic", 1000, chatterCallback);
// 创建一个服务客户端,连接到 "batteryStatus" 服务
ros::ServiceClient client = n.serviceClient<third_pkg::myTestSrv>("batteryStatus");// 创建一个服务请求对象
third_pkg::myTestSrv mySrv;
mySrv.request.index = 1; // 设置服务请求的参数
while (ros::ok())
{// 调用服务并处理响应
if (client.call(mySrv))
{
ROS_WARN("Client Get Battery: %d", mySrv.response.result); // 打印获得的电
}
else
{
ROS_ERROR("Failed to call batteryStatus service"); // 打印错误信息
return 1; // 返回错误码
}// 处理回调函数
ros::spinOnce();// 休眠以保持循环频率
loop_rate.sleep();}
return 0;}
1****服务端的定义与初始化
ros::ServiceServer service = n.advertiseService(“batteryStatus”,checkBattery);
参数1:”batteryStatus”:发布服务的名称,类型:字符串,可以任意指定;
参数2:checkBattery:指定回调函数,类型:函数指针;
2 服务回调函数写法
bool checkBattery(third_pkg::myTestSrv::Request &req,third_pkg::myTestSrv::Response &res)
参数1:third_pkg::myTestSrv::Request &req,请求部分参数.
如上图所示:
1部分:服务的类型,写上要发布的服务类型。
2部分:固定写法,表征此部分为请求部分;
3部分:变量名称,可以随便写;
在此处,由于服务类型third_pkg::myTestSrv的定义如下:
因此,req的成员变量只有一个为index,即:req.index
参数2:third_pkg::myTestSrv::Response &res,响应部分参数。
1部分:服务的类型,写上要发布的服务类型。
2部分:固定写法,表征此部分为响应部分;
3部分:变量名称,可以随便写;
在此处,由于服务类型third_pkg::myTestSrv的定义如下:
因此res的成员变量只有一个为:result,即:res.result。
3 客户端定义
ros::ServiceClient client = n.serviceClient<third_pkg::myTestSrv>(“batteryStatus”);
其中:
ros::ServiceClient为客户端定义类;
1部分:客户端初始化函数;
2部分:服务类型,这里为:third_pkg::myTestSrv;
3部分:要调用的服务名字,类型为字符串;
4 服务调用:
third_pkg::myTestSrv mySrv;//定义变量
mySrv.request.index = 1;//填充数据
client.call(mySrv);//调用服务
移动机器人
四大组成部分:执行机构 驱动系统 传感系统 控制系统
控制 - 驱动 -执行 -对象 -传感器 - 控制
| |
传感器
ros组成
ros = 通信机制 + 开发工具 + 应用功能 + 生态系统
分布式通信框架 功能独立又相互联系
节点 功能的执行单元
节点 节点管理器
话题和消息
服务
参数
topic通信属于一种异步的通信方式 各司其责,不存在协同工作,我们称这样的通信方式是异步的
ROS是一种分布式的架构,一个topic可以被多个节点同时发布,也可以同时被多个节点接收
分布式系统通信的好处:扩展性好、软件复用率高
- topic通信方式是异步的,发送时调用publish()方法,发送完成立即返回,不用等待反馈。
- subscriber通过回调函数的方式来处理消息。
- topic可以同时有多个subscribers,也可以同时有多个publishers。
ros常用工具
- rostopic 话题相关
- rosservice 服务
- rosnode 节点
- rosparam 参数
- rosmsg 话题消息
- rossrv 服务消息