目录
工作空间的覆盖
同一个工作空间下,不允许出现同名的功能包,否则无法编译。不同工作空间下,允许出现同名的功能包,但会出现工作空间覆盖的现象。
ros工作空间的路径记录在ROS_PACKAGE_PATH环境变量中,可以通过env命令查看计算机中的环境变量,再利用管道过滤出有关ros的环境变量。
env | grep ros
新添加的工作空间路径会自动放置在ROS_PACKAGE_PATH环境变量最前端。ros运行时,会按照顺序从前往后查找工作空间中是否存在指定的功能包。一旦找到指定的功能包就不再查找后面的工作空间。因此,如果我们的系统当中存在名称相同的功能包,就会出现工作空间向后覆盖的问题。当我们使用rospack find命令查找功能包的时候,只能查找到最新路径下的同名功能包,而一旦有第三方功能包依赖于原来的同名功能包,就有可能会出现问题。
通信方式的c++实现
话题
发布者实现步骤大致如下:
1、初始化ROS节点
2、向ROS Master注册节点信息,包括发布的话题名,消息类型
3、设置一定的频率循环发布消息
#include <sstream>
#include "ros/ros.h"
#include "std_msgs/String.h"
int main(int argc,char **argv)
{
ros::init(argc,argv,"talker"); //ROS节点的初始化,指定节点名称为talker
ros::NodeHandle n; //创建节点句柄
ros::Publisher pub=n.advertise<std_msgs::String>("greet",1000);
//创建一个发布者pub,指定消息类型为std_msgs::String,话题名称为greet,1000为缓存队列的长度
ros::Rate loop_rate(10); //设置循环的频率
int count=0;
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());
pub.publish(msg); //发布消息
ros::spinOnce(); //等待消息调用一次回调函数
loop_rate.sleep(); //按照循环频率休眠
++count;
}
return 0;
}
订阅者实现步骤大致如下:
1、初始化ROS节点
2、向ROS Master注册节点信息,包括订阅的话题名,回调函数
3、循环等待话题消息,接收到消息后进入回调函数
4、在回调函数中处理消息
#include "ros/ros.h"
#include "std_msgs/String.h"
void greetCallback(const std_msgs::String::ConstPtr& msg)
// 接收到订阅的消息后,会进入消息回调函数
{
ROS_INFO("I heard: [%s]", msg->data.c_str());// 将接收到的消息打印出来
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "listener");// 初始化ROS节点
ros::NodeHandle n;// 创建节点句柄
ros::Subscriber sub = n.subscribe("greet", 1000, greetCallback);
// 创建一个Subscriber,订阅名为greet的topic,注册回调函数greetCallback
ros::spin();// 循环等待消息调用回调函数
return 0;
}
修改CMakeLists文件
1)、设置要编译的文件
add_executable(${PROJECT_NAME}_node src/a_node.cpp)
其中,${PROJECT_NAME}_node 代表节点名称,a_node.cpp 表示对应的c++文件,修改这两个参数即可。当然有时候一个节点可能对应着多个c++文件,这种情况需要把所有需要的文件名都列出来。
add_executable(talker src/talker.cpp)
add_executable(listener src/talker.cpp)
2)、设置链接库
target_link_libraries(${PROJECT_NAME}_node
${catkin_LIBRARIES}
)
其中,${PROJECT_NAME}_node 表示节点名称,这里只需要修改这一个参数即可,因为没有使用第三方的链接库。${catkin_LIBRARIES} 代表的是默认的ros catkin链接库,如果我们用到了第三方库的时候可以将第三方库的名称放到默认库后面即可。
target_link_libraries(talker ${catkin_LIBRARIES} )
target_link_libraries(listener ${catkin_LIBRARIES} )
3)、设置依赖
add_dependencies(${PROJECT_NAME}_node
${${PROJECT_NAME}_EXPORTED_TARGETS}
${catkin_EXPORTED_TARGETS}
)
其中,${PROJECT_NAME}_node 表示节点名称,${${PROJECT_NAME}_EXPORTED_TARGETS} 表示在本package中依赖的消息,${catkin_EXPORTED_TARGETS} 表示对其他package消息的依赖。
当使用自定义的message、service、action时注意添加。这里的例子当中并没有使用自定义的消息,所以不用添加依赖。
编译功能包
回到工作空间,运行catkin_make对功能包进行编译。编译完成后即可生成talker和listener两个可执行文件,设置好工作空间的环境变量之后就可以通过rosrun来运行了,效果如下:
ldz@MSI:~$ rosrun a talker
[ INFO] [1537440776.481837500]: hello world0
[ INFO] [1537440776.582547600]: hello world1
[ INFO] [1537440776.682226900]: hello world2
[ INFO] [1537440776.781844500]: hello world3
[ INFO] [1537440776.882372400]: hello world4
[ INFO] [1537440776.982076300]: hello world5
[ INFO] [1537440777.082023100]: hello world6
[ INFO] [1537440777.181946400]: hello world7
[ INFO] [1537440777.283714400]: hello world8
ldz@MSI:~$ rosrun a listener
[ INFO] [1537863017.967429700]: I heard: [hello world75]
[ INFO] [1537863018.067197800]: I heard: [hello world76]
[ INFO] [1537863018.166951500]: I heard: [hello world77]
[ INFO] [1537863018.267036300]: I heard: [hello world78]
[ INFO] [1537863018.367167600]: I heard: [hello world79]
[ INFO] [1537863018.467182300]: I heard: [hello world80]
[ INFO] [1537863018.567118800]: I heard: [hello world81]
[ INFO] [1537863018.666904400]: I heard: [hello world82]
[ INFO] [1537863018.767226700]: I heard: [hello world83]
自定义话题消息
1)、定义 .msg 文件
话题消息的定义格式:
定义一个变量列表即可,类似于c语言结构体。定义变量:数据类型+变量名 eg:我们自定义一个学生基本信息的消息stu.msg
如上图,自定义消息格式中还可以定义符号常量,类似于c语言中的define。即male代表1,female代表2,可以直接将这两个符号常量赋给sex。
2)、修改package.xml文件,添加功能包依赖
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
indigo之前的ros版本需要将exec_depend改成run_depend。
3)、修改CMakeLists.txt文件,添加编译选项
find_package(...... message_generation) #生成自定义的消息需要使用message_generation功能包
catkin_package(CATKIN_DEPENDS geometry_msgs roscpp rospy std_msgs message_runtime) #添加编译依赖message_runtime
add_message_files(FILES Person.msg) #添加消息文件
generate_messages(DEPENDENCIES std_msgs) #添加生成依赖
经过以上三个步骤,我们就可以使用catkin_make对自定义消息进行编译了。编译完成之后,会在工作空间的 devel/include/【功能包名】路径下生成自定义消息的头文件 stu.h 。编写节点时只要包含了这个头文件( #include<【功能包名】/ stu.h> )就可以使用这个消息进行通信了。使用该消息类型创建消息对象的方式为: 【功能包名】:: stu 对象名; 。
服务
自定义服务消息
1)、定义 .srv 文件
服务类型的定义格式:
变量的定义方式与话题消息一致,即:数据类型+变量名,但与消息不同的是,需要用三个短横线(---)把请求数据和应答数据分隔开。 eg:我们自定义一个加法的服务addition.srv
2)、修改package.xml文件,添加功能包依赖(同msg操作)
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
3)、修改CMakeLists.txt文件,添加编译选项(除添加的文件不同,其他与msg操作相同)
find_package(...... message_generation) #生成自定义的消息需要使用message_generation功能包
catkin_package(CATKIN_DEPENDS geometry_msgs roscpp rospy std_msgs message_runtime) #添加编译依赖message_runtime
add_service_files(FILES addition.srv) #添加服务类型文件addition.srv
generate_messages(DEPENDENCIES std_msgs) #添加生成依赖
经过以上三个步骤,我们就可以使用catkin_make对自定义服务进行编译了。编译完成之后,会在工作空间的 devel/include/【功能包名】路径下生成自定义服务的头文件 addition.h、additionRequest.h、additionResponse.h 。编写节点时只要包含第一个头文件( #include<【功能包名】/ addition.h> )就可以使用这个服务进行通信了。使用该服务类型创建服务对象的方式为: 【功能包名】:: addition 对象名; 。
服务器实现步骤大致如下:
1、初始化ROS节点
2、创建server实例
3、循环等待服务请求,接收到请求后进入回调函数
4、在回调函数中处理服务请求,并反馈应答数据
#include "ros/ros.h"
#include "a/addition.h"
// service回调函数,输入参数req,输出参数res
bool add(a::addition::Request &req, a::addition::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::init(argc, argv, "add_two_ints_server"); // ROS节点初始化
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();// 循环等待回调函数
return 0;
}
客户端实现步骤大致如下:
1、初始化ROS节点
2、创建client实例
3、发布服务请求
4、等待server处理之后的应答结果
#include <cstdlib>
#include "ros/ros.h"
#include "a/addition.h"
int main(int argc, char **argv)
{
ros::init(argc, argv, "add_two_ints_client");// ROS节点初始化
// 从终端命令行获取两个数
if (argc != 3)
{
ROS_INFO("usage: add_two_ints_client X Y");
return 1;
}
ros::NodeHandle n;// 创建节点句柄
// 创建一个client,请求add_two_int service,service消息类型是a::addition
ros::ServiceClient client = n.serviceClient<a::addition>("add_two_ints");
a::addition srv;// 创建a::addition类型的service消息
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;
}
修改CMakeLists文件
1)、设置要编译的文件(同话题)
add_executable(server src/server.cpp)
add_executable(client src/client.cpp)
2)、设置链接库(同话题)
target_link_libraries(server ${catkin_LIBRARIES} )
target_link_libraries(client ${catkin_LIBRARIES} )
3)、设置依赖(同话题)
add_dependencies(server ${PROJECT_NAME}_gencpp)
add_dependencies(client ${PROJECT_NAME}_gencpp)
编译功能包
回到工作空间,运行catkin_make对功能包进行编译。编译完成后即可生成server和client两个可执行文件,然后就可以通过rosrun来运行了,效果如下:
ldz@MSI:~$ rosrun a client 1 2
[ INFO] [1537523575.202664700]: Sum: 3
ldz@MSI:~$ rosrun a server
[ INFO] [1537523561.898041700]: Ready to add two ints.
[ INFO] [1537523575.202092800]: request: x=1, y=2
[ INFO] [1537523575.202301100]: sending back response: [3]
动作
自定义动作消息
1)、定义 .action文件
服务类型的定义格式:
变量的定义方式与话题以及服务消息一致,即:数据类型+变量名,但与消息和服务不同的是,需要动作消息需要用两条由三个短横线(---)构成的虚线把目标信息、结果信息和反馈信息分隔开。 eg:我们自定义一个洗碗机的服务DoDishes.srv
第一部分定义的是目标数据,使用哪一台洗碗机;第二部分定义的是结果数据,任务成功完成与否;第三部分定义的是周期反馈的状态数据,任务完成的进度。
2)、修改package.xml文件,添加功能包依赖
<build_depend>actionlib</build_depend>
<build_depend>actionlib_msgs</build_depend>
<exec_depend>actionlib</exec_depend>
<exec_depend>actionlib_msgs</exec_depend>
3)、修改CMakeLists.txt文件,添加编译选项(除添加的文件不同,其他与msg操作相同)
find_package(...... actionlib_msgs actionlib) #生成自定义的消息需要使用 actionlib_msgs、actionlib功能包
add_action_files(FILES DoDishes.action) #添加动作消息文件DoDishes.action
generate_messages(DEPENDENCIES actionlib_msgs) #添加生成依赖
经过以上三个步骤,我们就可以使用catkin_make对自定义动作进行编译了。编译完成之后,会在工作空间的 devel/include/【功能包名】路径下生成自定义动作的头文件 DoDishesAction.h、DoDishesActionFeedback.h、DoDishesActionResult.h 、DoDishesResult.h、DoDishesActionGoal.h、DoDishesFeedback.h、DoDishesGoal.h。编写节点时只要包含第一个头文件( #include<【功能包名】/ DoDishesAction.h> )就可以使用这个动作进行通信了。
服务器实现步骤大致如下:
1、初始化ROS节点
2、创建action server实例
3、启动服务器,等待动作请求
4、接收到请求后进入回调函数处理动作请求,并反馈进度信息
5、动作完成,发送结束信息
#include <ros/ros.h>
#include <actionlib/server/simple_action_server.h>
#include "a/DoDishesAction.h"
typedef actionlib::SimpleActionServer<a::DoDishesAction> Server;
// 收到action的goal后调用该回调函数
void execute(const a::DoDishesGoalConstPtr& goal, Server* as)
{
ros::Rate r(1);
a::DoDishesFeedback feedback;
ROS_INFO("Dishwasher %d is working.", goal->dishwasher_id);
// 假设洗盘子的进度平均为10%,并且按照1hz的频率发布进度feedback
for(int i=1; i<=10; i++)
{
feedback.percent_complete = i * 10;
as->publishFeedback(feedback);
r.sleep();
}
// 当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、创建action client实例
3、连接动作服务器
4、发送动作目标
5、根据服务器的不同反馈信息,处理回调函数
#include <actionlib/client/simple_action_client.h>
#include "a/DoDishesAction.h"
typedef actionlib::SimpleActionClient<a::DoDishesAction> Client;
// 当action完成后会调用该回调函数一次
void doneCb(const actionlib::SimpleClientGoalState& state,
const a::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 a::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.");
a::DoDishesGoal goal; // 创建一个action的goal
goal.dishwasher_id = 1;
// 发送action的goal给服务器端,并且设置回调函数
client.sendGoal(goal, &doneCb, &activeCb, &feedbackCb);
ros::spin();
return 0;
}
修改CMakeLists文件
1)、设置要编译的文件(同话题)
add_executable(DoDishes_client src/DoDishes_client.cpp)
add_executable(DoDishes_server src/DoDishes_server.cpp)
2)、设置链接库(同话题)
target_link_libraries(DoDishes_client ${catkin_LIBRARIES} )
target_link_libraries(DoDishes_server ${catkin_LIBRARIES} )
3)、设置依赖(同话题)
add_dependencies(DoDishes_client ${${PROJECT_NAME}_EXPORTED_TARGETS})
add_dependencies(DoDishes_server ${${PROJECT_NAME}_EXPORTED_TARGETS})
编译功能包
回到工作空间,运行catkin_make对功能包进行编译。编译完成后即可生成DoDishes_client和DoDishes_server两个可执行文件,然后就可以通过rosrun来运行了,效果如下:
ldz@MSI:~$ rosrun b DoDishes_client
[ INFO] [1537541980.774761400]: Waiting for action server to start.
[ INFO] [1537541981.204468700]: Action server started, sending goal.
[ INFO] [1537541981.205188000]: Goal just went active
[ INFO] [1537541981.206087300]: percent_complete : 10.000000
[ INFO] [1537541982.205776000]: percent_complete : 20.000000
[ INFO] [1537541983.205963000]: percent_complete : 30.000000
[ INFO] [1537541984.205360000]: percent_complete : 40.000000
[ INFO] [1537541985.205488100]: percent_complete : 50.000000
[ INFO] [1537541986.205560800]: percent_complete : 60.000000
[ INFO] [1537541987.205739900]: percent_complete : 70.000000
[ INFO] [1537541988.205424100]: percent_complete : 80.000000
[ INFO] [1537541989.205597000]: percent_complete : 90.000000
[ INFO] [1537541990.205898500]: percent_complete : 100.000000
[ INFO] [1537541991.208537400]: Yay! The dishes are now clean
ldz@MSI:~$ rosrun b DoDishes_server
[ INFO] [1537541981.205257600]: Dishwasher 1 is working.
[ INFO] [1537541991.207887100]: Dishwasher 1 finish working.
分布式通信
前面我们介绍ros的时候提到,它是一种分布式的软件框架,节点之间通过松耦合的方式进行组合。
多机通讯
设置IP地址,确认网络连通性
首先需要将 对方主机的ip地址和主机名 添加到 本机的 /etc/hosts 文件 中:
sudo vi /etc/hosts
然后在终端中使用 ping 命令测试网络连通性。
ldz@MSI:~$ ping ldz-dell
PING ldz-dell (192.168.43.144) 56(84) bytes of data.
64 bytes from ldz-dell (192.168.43.144): icmp_seq=1 ttl=64 time=1415 ms
64 bytes from ldz-dell (192.168.43.144): icmp_seq=2 ttl=64 time=1587 ms
64 bytes from ldz-dell (192.168.43.144): icmp_seq=3 ttl=64 time=1450 ms
ldz@ldz-dell:~$ ping MSI
PING MSI (192.168.43.64) 56(84) bytes of data.
64 bytes from MSI (192.168.43.64): icmp_seq=1 ttl=64 time=1351 ms
64 bytes from MSI (192.168.43.64): icmp_seq=2 ttl=64 time=1597 ms
64 bytes from MSI (192.168.43.64): icmp_seq=3 ttl=64 time=1692 ms
在从机中设置节点管理器所在的地址(ROS_MASTER_URI)
在 .bashrc文件中追加一句,设置节点管理器的地址在主控机的11311端口,11311为节点管理器的默认端口,这里的MSI是我的主控机主机名。
echo "export ROS_MASTER_URI=http://MSI:11311" >> ~/.bashrc
source ~/.bashrc
OK,经过以上配置,我们的多机通讯环境就搭建好了。只要我们就在主控机上运行节点管理器,所有从机上就都可以运行节点了,而不必在每个机器上都运行节点管理器。