目录
1.工作空间
1.1什么工作空间:
1.2创建工作空间
1.2.1生成工作空间:
1.2.2编译工作空间:
1.2.3设置环境变量:
1.3功能包
1.3.1创建功能包
1.3.2编译功能包
1.3.3环境变量
1.4工作空间的覆盖
2.ROS通信编程
2.1话题编程
2.1.1话题编程流程:
2.1.2实现一个发布者:
2.1.3实现一个订阅者:
2.1.4编译话题
2.1.5执行话题
2.1.6效果图
2.1.7自定义话题编程
2.2服务编程
2.2.1自定义服务请求与应答
注:添加的顺序问题,防止编译报错
2.2.2服务编程创建流程
2.2.3编译代码
2.2.4执行程序 & 效果图
2.3动作编程
2.3.1动作编程
2.3.2Action的接口
2.3.3自定义动作编程
2.3.4实现一个动作服务器
2.3.5实现一个动作客户端
2.3.6编译代码
2.3.7运行效果图
3.实现分布式通信
3.1实现分布式的多机通信
3.1.1设置IP地址,确保底层链路的联通
3.1.2设置ROS_MASTER_URI
4.ROS中的关键组件
4.1 Launch文件
参数设置
重映射:更改之后原有命名的就不存在
嵌套
4.2TF坐标变换
4.3Qt工具箱
4.4Rviz可视化平台
4.5Gazebo物理仿真平台
工作空间是一个存放工程开发相关文件的文件夹
src:代码空间(Source Space)
build:编译空间(Build Sapce)
devel:开发空间(Development Space)
install:安装空间(Install Space)
mkdir -p ~/catkin_ws/src
cd ~/catkin_ws/
catkin_make
这里有两种方法:一种是临时生效,一种是永远生效。
临时生效的意思是,执行完该命令后,你只能从当前的终端执行相关可执行文件,好处就是方便测试,比如你心血来潮等原因写了一个不重要的功能包,编译后生成了可执行文件,你单纯想试一试这个功能包写的怎么样,并没有长久使用的意思,就可以用临时生效的方法。对于新开启的终端无效。
永久生效就是,每次开机之后,你可以从任意一个命令行中执行相关可执行文件。对于比较重要的功能包比较合适。或者你比较厌烦每次都得执行临时方法,就可以使用永久生效。
临时方法:在devel中有一个setup.bash的文件,用source命令执行即可。
source ~/catkin_ws/devel/setup.bash
永久方法:把临时方法那句话加到~/.bashrc文件中
echo "source ~/catkin_ws/devel/setup.bash">>~/.bashrc
source ~/.bashrc
cd ~/catkin_ws/src/
catkin_create_pkg package_name std_msgs rospy roscpp
//其中std_msgs rospy roscpp是package_name功能包的各类依赖
//格式为 catkin_create_pkg [depend1] [depend2] [depend3]
//同一工作空间下,不允许存在同名功能包
//不同的工作空间下,允许存在同名的功能包
cd ~/workspace/
catkin_make
source ~/.bashrc
工作空间的覆盖是怎么回事呢?
同一个工作空间下,不允许出现同名的功能包,否则无法编译。不同工作空间下,允许出现同名的功能包,但会出现工作空间覆盖的现象。
ros工作空间的路径记录在ROS_PACKAGE_PATH环境变量中,可以通过env命令查看计算机中的环境变量,再利用管道过滤出有关ros的环境变量。
env | grep ros
新添加的工作空间路径会自动放置在ROS_PACKAGE_PATH环境变量最前端。ros运行时,会按照顺序从前往后查找工作空间中是否存在指定的功能包。一旦找到指定的功能包就不再查找后面的工作空间。因此,如果我们的系统当中存在名称相同的功能包,就会出现工作空间向后覆盖的问题。当我们使用rospack find命令查找功能包的时候,只能查找到最新路径下的同名功能包,而一旦有第三方功能包依赖于原来的同名功能包,就有可能会出现问题。
1.创建发布者
2.创建订阅者
3.添加编译选项
4.运行可执行程序
1.初始化ROS节点。
2.向ROS Master注册节点信息,包括发布的话题名和话题中的消息类型。
3.按照一定频率循环发布消息。
/*
//该例程将发布chatter话题,消息类型为String
*/
#include
#include"ros/ros.h" //包括了使用ROS系统最基本的头文件。
#include"std_msgs/String.h"
int main(int argc, char** argv)
{
ros::init(argc, argv, "talker");
ros::NodeHandle n; //为处理的节点创建了一个句柄,第一个创建的节点句柄将会初始化这个节点,最后一个销毁的节点将会释放节点所使用的所有资源。
//告诉主机,我们将会在一个名字为chatter的话题上发布一个std_msgs/String类型的消息,
//这就使得主机告诉了所有订阅了chatter话题的节点,我们将在这个话题上发布数据。
//第二个参数是发布队列的大小,它的作用是缓冲。当我们发布消息很快的时候,它将能缓冲1000条信息。如果慢了的话就会覆盖前面的信息。
ros::Publisher chatter_pub = n.advertise("chatter", 1000);
//NodeHandle::advertise()将会返回ros::Publisher对象,该对象有两个作用,
//首先是它包括一个publish()方法可以在制定的话题上发布消息,其次,当超出范围之外的时候就会自动的处理。
ros::Rate loop_rate(10);
//一个ros::Rate对象允许你制定循环的频率。它将会记录从上次
//调用Rate::sleep()到现在为止的时间,并且休眠正确的时间。在这个例子中,设置的频率为10hz。
int count = 0;
while(ros::ok())
{
//默认情况下,roscpp将会安装一个SIGINT监听,它使当Ctrl-C按下时,ros::ok()将会返回false。
//ros::ok()在以下几种情况下也会返回false:(1)按下Ctrl-C时(2)我们被一个同名同姓的节点从网络中踢出
//(3)ros::shutdown()被应用程序的另一部分调用(4)所有的ros::NodeHandles都被销毁了。一旦ros::ok()返回false,所有的ROS调用都会失败。
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();
//在这个简单的程序中调用ros::spinOnce();是不必要的,因为我们没有收到任何的回调信息。
//然而如果你为这个应用程序添加一个订阅者,并且在这里没有调用ros::spinOnce(),你的回调函数将不会被调用。所以这是一个良好的风格。
loop_rate.sleep();
//休眠一下,使程序满足前面所设置的10hz的要求。
count++;
}
return 0;
}
1.初始化ROS节点
2.订阅需要的话题
3.循环等待话题消息,接受到消息后进入回调函数
4.在回调函数中完成消息处理
#include "ros/ros.h"
#include "std_msgs/String.h"
/**
* This tutorial demonstrates simple receipt of messages over the ROS system.
*/
//当一个消息到达chatter话题时,这个回调函数将会被调用。
void chatterCallback(const std_msgs::String::ConstPtr& msg)
{
ROS_INFO("I heard: [%s]", msg->data.c_str());
}
int main(int argc, char **argv)
{
/**
* The ros::init() function needs to see argc and argv so that it can perform
* any ROS arguments and name remapping that were provided at the command line. For programmatic
* remappings you can use a different version of init() which takes remappings
* directly, but for most command-line programs, passing argc and argv is the easiest
* way to do it. The third argument to init() is the name of the node.
*
* You must call one of the versions of ros::init() before using any other
* part of the ROS system.
*/
ros::init(argc, argv, "listener");
/**
* NodeHandle is the main access point to communications with the ROS system.
* The first NodeHandle constructed will fully initialize this node, and the last
* NodeHandle destructed will close down the node.
*/
ros::NodeHandle n;
/**
* The subscribe() call is how you tell ROS that you want to receive messages
* on a given topic. This invokes a call to the ROS
* master node, which keeps a registry of who is publishing and who
* is subscribing. Messages are passed to a callback function, here
* called chatterCallback. subscribe() returns a Subscriber object that you
* must hold on to until you want to unsubscribe. When all copies of the Subscriber
* object go out of scope, this callback will automatically be unsubscribed from
* this topic.
*
* The second parameter to the subscribe() function is the size of the message
* queue. If messages are arriving faster than they are being processed, this
* is the number of messages that will be buffered up before beginning to throw
* away the oldest ones.
*/
ros::Subscriber sub = n.subscribe("chatter", 1000, chatterCallback);
//订阅chatter话题,当一个新的消息到达时,ROS将会调用chatterCallback()函数。
//第二个参数是对列的长度,如果我们处理消息的速度不够快,会将收到的消息缓冲下来,一共可以缓冲1000条消息,满1000之后,后面的到达的消息将会覆盖前面的消息。
//NodeHandle::subscribe()将会返回一个ros::Subscriber类型的对象,当订阅对象被销毁以后,它将会自动从chatter话题上撤销。
/**
* ros::spin() will enter a loop, pumping callbacks. With this version, all
* callbacks will be called from within this thread (the main one). ros::spin()
* will exit when Ctrl-C is pressed, or the node is shutdown by the master.
*/
ros::spin();
//ros::spin()进入了一个循环,可以尽快的调用消息的回调函数。不要担心,如果它没有什么事情可做时,它也不会浪费太多的CPU。当ros::ok()返回false时,ros::spin()将会退出。这就意味着,当ros::shutdown()被调用,或按下CTRL+C等情况,都可以退出。
return 0;
}
进入到功能包目录下,打开CMakeLists.txt.
在 CMakeLists.txt 文件末尾加入几条语句:
add_executable(talker src/talker.cpp)#设置需要编译的代码和生成的可执行文件
target_link_libraries(talker ${catkin_LIBRARIES})#设置链接库
add_dependencies(talker ${PROJECT_NAME}_generate_messages_cpp)
#设置依赖,本例程中不加也可以。可以确保自定义消息的头文件在被使用之前已经被生成。
#因为 catkin 把所有的 package 并行的编译,
#所以如果你要使用其他 catkin 工作空间中其他 package 的消息,你同样也需要添加对他们各自生成的消息文件的依赖。
add_executable(listener src/listener.cpp)
target_link_libraries(listener ${catkin_LIBRARIES})
add_dependencies(talker ${PROJECT_NAME}_generate_messages_cpp)
保存,退出。
cd ~/catkin
catkin_make
source ~/.bashrc
//1.在终端命令行编写该命令,启动master
roscore
//2.启动发布者节点
rosrun learning_communication talker
//3.启动订阅者节点
rosrun learning_communication listener
定义msg文件:
在功能包目录下新建msg文件夹。
在msg文件夹下新建Person.msg文件。
在文件Person.msg中输入以下内容并保存。
string name
uint8 sex
uint8 age
uint8 unknown = 0
uint8 male = 1
uint8 female = 2
添加功能包依赖,打开功能包目录下的package.xml:(光标处)
message_generation
message_runtime
在CMakeLists.txt添加编译选项:
打开CMakeLists.txt
//第一步,查找到
find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
std_msgs
)
//添加 message_generation进入变成
find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
std_msgs
message_generation
)
//第二步,查找到
catkin_package(
# INCLUDE_DIRS include
# LIBRARIES learning_communication
# CATKIN_DEPENDS roscpp rospy std_msgs
# DEPENDS system_lib
)
//让这段代码变为
catkin_package(
# INCLUDE_DIRS include
# LIBRARIES learning_communication
CATKIN_DEPENDS roscpp rospy std_msgs message_runtime
# DEPENDS system_lib
)
//第三步,复制下面这段代码,放入
add_message_files(FILES Person.msg)
generate_messages(DEPENDENCIES std_msgs)
回到工作空间目录下:
cd ~/catkin_ws
//编译
catkin_make
//显示
rosmsg show Person
效果图:
本次我们将创建一个简单的service节点("add_two_ints_server"),该节点将接收到两个整形数字,并返回它们的和。
再创建一个client节点("add_two_ints_client"),该节点发送两个整形数字,并接收他们的和。
定义src文件:
在package.xml中添加功能包依赖:(若前面已添加相同的依赖,则无需再添加)
message_generation
message_runtime
在CMakeLists.txt添加编译选项:(若前面已添加相同的依赖,则无需再添加)
• find_package( ...... message_generation)
• catkin_package(CATKIN_DEPENDS geometry_msgs roscpp
rospy std_msgs message_runtime)
• add_service_files(FILES AddTwoInts.srv)
报错:
正确:
1.创建服务器
2.创建客户端
3.添加编译选项
4.运行可执行程序
服务中的Server类似于话题中的Subscriber,流程如下:
1.初始化ros节点
2.创建Server实例
3.循环等待服务请求,进入回调函数
4.在回调函数中完成服务功能的处理并反馈应答数据。
/**
* AddTwoInts Server
*/
#include "ros/ros.h"
#include "learning_communication/AddTwoInts.h"
// service回调函数,输入参数req,输出参数res
bool add(learning_communication::AddTwoInts::Request &req,
learning_communication::AddTwoInts::Response &res)
{
// 将输入参数中的请求数据相加,结果放到应答变量中
res.sum = req.a + req.b;
ROS_INFO("request: x=%ld, y=%ld", (long int)req.a, (long int)req.b);
ROS_INFO("sending back response: [%ld]", (long int)res.sum);
return true;
}
int main(int argc, char **argv)
{
// ROS节点初始化
ros::init(argc, argv, "add_two_ints_server");
// 创建节点句柄
ros::NodeHandle n;
// 创建一个名为add_two_ints的server,注册回调函数add()
ros::ServiceServer service = n.advertiseService("add_two_ints", add);
// 循环等待回调函数
ROS_INFO("Ready to add two ints.");
ros::spin(); //https://www.cnblogs.com/liu-fa/p/5925381.html
return 0;
}
服务中的Client类似于话题中的Publisher,流程如下:
1.初始化ROS节点
2.创建一个Client实力
3.发布服务请求数据
4.等待Server处理之后的应答结果
/**
* AddTwoInts Client
*/
#include
#include "ros/ros.h"
#include "learning_communication/AddTwoInts.h"
int main(int argc, char **argv)
{
// ROS节点初始化
ros::init(argc, argv, "add_two_ints_client");
// 从终端命令行获取两个加数
//之所以是3,是因为第一个参数是路径名,即该文件的绝对路径
if (argc != 3)
{
ROS_INFO("usage: add_two_ints_client X Y");
return 1;
}
// 创建节点句柄
ros::NodeHandle n;
// 创建一个client,请求add_two_int service,service消息类型是learning_communication::AddTwoInts
ros::ServiceClient client = n.serviceClient("add_two_ints");
// 创建learning_communication::AddTwoInts类型的service消息
learning_communication::AddTwoInts srv;
srv.request.a = atoll(argv[1]);
srv.request.b = atoll(argv[2]);
// 发布service请求,等待加法运算的应答结果
if (client.call(srv))
{
ROS_INFO("Sum: %ld", (long int)srv.response.sum);
}
else
{
ROS_ERROR("Failed to call service add_two_ints");
return 1;
}
return 0;
}
1.设置需要编译的代码和生成的可执行文件
2.设置链接库
3.设置依赖
add_executable(server src/server.cpp)
target_link_libraries(server ${catkin_LIBRARIES})
add_dependencies(server ${PROJECT_NAME}_gencpp)
add_executable(client src/client.cpp)
target_link_libraries(client ${catkin_LIBRARIES})
add_dependencies(client ${PROJECT_NAME}_gencpp)
1.一种问答通信机制
2.带有连续反馈
3.可以在任务过程中止运行
4.基于ROS的消息机制实现
1.goal 发布任务目标
2.cancel 请求取消任务
3.status 通知客户端当前的状态
4.feedback 周期反馈任务运行的监控数据
5.result 向客户端发送任务的执行结果,只发布一次
定义action文件:在/home/coscj/catkin_ws/src/learning_communication/action/DoDishes.action
#目标信息
uint32 dishwasher_id
---
#结果信息
uint32 total_dishes_cleaned
---
#周期反馈信息
float32 percent_complete
修改 ~/catkin_ws/src/learning_communication/package.xml
actionlib
actionlib
actionlib_msgs
actionlib_msgs
修改 ~/catkin_ws/src/learning_communication/CMakeLists.txt
find_package(catkin REQUIRED actionlib_msgs actionlib)
add_action_files( DIRECTORY action FILES DoDishes.action )
generate_messages( DEPENDENCIES std_msgs actionlib_msgs )
#注意不要直接粘帖进去,编译会报错
编译:判断前面的修改有没有问题,降低错误率
cd ~/catkin_ws
catkin_make
1.初始化ROS节点
2.创建动作服务器实例
3.启动服务器,等待动作请求
4.在回调函数中完成动作服务功能的处理,并反馈进度信息
5.动作完成,发送结束信息
创建动作服务端 ~/catkin_ws/src/learning_communication/src/DoDishes_server.cpp
#include
#include
#include "learning_communication/DoDishesAction.h"
typedef actionlib::SimpleActionServer Server;
// 收到action的goal后调用该回调函数
void execute(const learning_communication::DoDishesGoalConstPtr& goal, Server* as)
{
ros::Rate r(1);
learning_communication::DoDishesFeedback feedback;
ROS_INFO("Dishwasher %d is working.", goal->dishwasher_id);
// 假设洗盘子的进度,并且按照1hz的频率发布进度feedback
for(int i=1; i<=10; i++)
{
feedback.percent_complete = i * 10;
as->publishFeedback(feedback);
r.sleep();//休眠1s
}
// 当action完成后,向客户端返回结果
ROS_INFO("Dishwasher %d finish working.", goal->dishwasher_id);
as->setSucceeded();
}
int main(int argc, char** argv)
{
ros::init(argc, argv, "do_dishes_server");
ros::NodeHandle n;
// 定义一个服务器
Server server(n, "do_dishes", boost::bind(&execute, _1, &server), false);
// 服务器开始运行
server.start();
ros::spin();
return 0;
}
1.初始化ROS节点
2.创建动作客户端实例
3.连接动作服务端
4.发送动作目标
5.根据不同类型的服务端反馈处理回调函数
创建动作客户端 ~/catkin_ws/src/learning_communication/src/DoDishes_client.cpp
#include
#include "learning_communication/DoDishesAction.h"
typedef actionlib::SimpleActionClient Client;
// 当action完成后会调用该回调函数一次
void doneCb(const actionlib::SimpleClientGoalState& state,
const learning_communication::DoDishesResultConstPtr& result)
{
ROS_INFO("Yay! The dishes are now clean");
ros::shutdown();
}
// 当action激活后会调用该回调函数一次
void activeCb()
{
ROS_INFO("Goal just went active");
}
// 收到feedback后调用该回调函数
void feedbackCb(const learning_communication::DoDishesFeedbackConstPtr& feedback)
{
ROS_INFO(" percent_complete : %f ", feedback->percent_complete);
}
int main(int argc, char** argv)
{
ros::init(argc, argv, "do_dishes_client");
// 定义一个客户端
Client client("do_dishes", true);
// 等待服务器端
ROS_INFO("Waiting for action server to start.");
client.waitForServer();//等待服务端的启动
ROS_INFO("Action server started, sending goal.");
// 创建一个action的goal
learning_communication::DoDishesGoal goal;
goal.dishwasher_id = 1;
// 发送action的goal给服务器端,并且设置回调函数
client.sendGoal(goal, &doneCb, &activeCb, &feedbackCb);
ros::spin();
return 0;
}
在CMakeLists.txt修改:
add_executable(DoDishes_client src/DoDishes_client.cpp)
target_link_libraries( DoDishes_client ${catkin_LIBRARIES})
add_dependencies(DoDishes_client ${${PROJECT_NAME}_EXPORTED_TARGETS})
add_executable(DoDishes_server src/DoDishes_server.cpp)
target_link_libraries( DoDishes_server ${catkin_LIBRARIES})
add_dependencies(DoDishes_server ${${PROJECT_NAME}_EXPORTED_TARGETS})
编译工作空间:
cd ~/catkin_ws
catkin_make
ROS是一种分布式软件框架,节点之间通过松耦合的方式进行组合,在很多应用场景下,节点可以运行在不同的计算平台上,通过Topic、Service进行通信。但是“一山不容二虎”,ROS只允许存在一个Master,在多机系统中Master只能运行在一个机器上,其他机器需要通过 ssh 的方式和Master取得联系。所以在多级ROS系统中需要一些配置。
我们以两台计算机为例,介绍分布式多机通信的配置步骤,其中计算机hcx-pc作为主机运行Master,计算机raspi2
作为从机运行节点。
首先需要确定ROS多机系统中的所有计算机处于同一网络,然后分别在计算机hcx-pc、raspi2上使用ifconfig
命令查看计算机的局域网IP地址。
分别在两台计算机系统的/etc/hosts
文件中加入对方的IP地址和对应的计算机名:
# @hcx-pc,/etc/hosts
192.168.31.14 raspi2
# @raspi2,/etc/hosts
192.168.31.198 hcx-pc
设置完毕后,分别在两台计算机上使用ping
命令测试网络是否连通。
# @hcx-pc
ping raspi2
# @raspi2
ping hcx-pc
如果双向网络都畅通,就说明底层网络的通信已经没有问题,接下来设置ROS相关的环境变量。
因为系统中只能存在一个Master,所以从机raspi2需要知道Master的位置。ROS Master的位置可以使用环境变量ROS_MASTER_URI进行定义,在从机raspi2上使用如下命令设置ROS_MASTER_URI:
export ROS_MASTER_URI=http://hcx-pc:11311
但是以上设置只能在输入的终端中生效,为了让所有打开的终端都能识别,最好使用如下命令将环境变量的设置加入终端额配置文件中。
echo ''export ROS_MASTER_URI=http://hcx-pc:11311'' >> ~/.bashrc
注:以上很多零碎知识是参考了很多博客,只是把他们按照课程的顺序归纳一起,方便复习,也记录坑。