目录
boost::bind(&cb, _1, &server)讲解
前言
本篇博客的案例实现建立在看完了上两篇博客的基础上,会跳过一些以及讲解过的内容。上两篇篇博客涉及到的许多概念,在本文不会详细展开,可以浏览上两篇博客(不想看完上篇博客,本文提及的相关概念都会提供具体位置,方便学习)
动作通信
概念
动作通信的通信模式类似于服务通信,有请求响应的操作,但是,除了请求响应的实时性外,动作通信的服务器端还能连续反馈当前的任务进度(相当于进度条),因此动作通信一般是用于耗时的请求响应通信模式的任务中的;客户端也可以在服务器端完成响应前取消任务。
原理
这个的实现依赖ROS内置的action包
下面两个图都是在网上找的,侵权联系删除
我们只需要实现发送的代码和响应时执行的代码就行了
第一步:相同话题的动作客户端与动作服务器端连接
第二步:动作客户端发布带有任务的请求消息给动作服务器端
(第二步起到结束任务前,动作客户端都可以发送取消的请求消息给动作服务器端以取消任务)
第三步:动作服务器端开始响应,向动作客户端连续发送状态和当前进度,直到第四步
第四步:动作服务器端完成任务,并反馈响应结果
第五步:动作客户端处理响应结果
实现效果
案例实现
案例
使用ROS动作(Action)机制实现目标请求、进度与完成结果的反馈。
要求:编写代码实现ROS中动作请求与进度反馈:创建服务端,注册Action客户端,发送action请求检测40个零件,服务端接收后,每隔1s测一个零件(每检测一个打印一次):实时给客户端返回检测进度(客户端打印进度百分比),并在检测完毕时告知客户端目标完成。如·服务端实时打印:检测1个零件检测2个零件...检测40个零件检测完成客户端实时打印:2.5%5%...100%检测完成
分析
要实现每个1s,在服务端发布一次当前进度并打印完成检查的零件个数,在客户端打印当前进度,也要打印响应消息的结果
实操
建立工作空间和修改配置文件“tasks.json”这里就省略了,由于小编将在同一个工作空间目录下,之前配置好了“tasks.json”,所以这里不需要再配置了。
建立功能包
由于和之前的不太一样,这里再详细的讲一下
右键在工作空间目录下的"src"目录,选择“Create Catkin Package”
在编辑框中输入包名,如小编的"test3_action",按下回车键确认
不同在这里,要多添加依赖 actionlib 和 actionlib_msgs
roscpp rospy std_msgs actionlib actionlib_msgs
建立完毕
配置自定义action文件
建立action文件夹
建立action文件
在action目录下新建文件
建立一个以.action为后缀的文件
配置action文件内容
将下面的内容保存在我们建立的action文件中
# 目标数据变量
int32 goal
---
# 最终响应变量
int32 result
---
# 连续反馈变量
float64 progress_bar
修改“CMakeLists.txt”配置文件
可以直接添加在后面或者修改注释
直接添加
经过测试,可以同时存在两个catkin_package()
add_action_files(
FILES
check.action
)
generate_messages(
DEPENDENCIES
acionlib_msgs
std_msgs
)
catkin_package(
CATKIN_DEPENDS actionlib actionlib_msgs roscpp rospy std_msgs
)
修改注释
点击“CMakeLists.txt”文件,找到add_action_files()和generate_messages()
取消add_action_files的注释,将FILES下面的.action文件改成我们建好的check.action文件
取消generate_message注释,由于两个依赖(action_msgs和std_msgs)都需要,所以两个依赖要添加(位置无所谓)
取消括号中的第三行就可以了
查看配置是否成功
按“Shift+Strl+b”进行编译
点击 devel > share > test3_action > msg,可以看到生成了一堆的"check..."消息文件
C++方面编译成功
点击 devel > include > test3_action,可以看到很多"check..."头文件
Python方面编译成功
点击 devel > lib > test3_action > msg,可以看到一堆python文件
C++实现
修改配置文件
在同一个工作空间目录下,配置过了,这里省略
建立源文件
action服务器端实现
#include "ros/ros.h"
#include "actionlib/server/simple_action_server.h"
#include "test3_action/checkAction.h"
// 重定义:范型为test3_action::checkAction的动作服务器类
typedef actionlib::SimpleActionServer<test3_action::checkAction> Server;
// 回调函数的实现 参数1:目标数据变量指针常量 参数2:服务器对象指针
void cb (const test3_action::checkGoalConstPtr &goal_ptr, Server* server)
{
// 获取目标数据
int goal = goal_ptr->goal;
// 设置频率
ros::Rate rate(1);
// 实时发布当前状态
int result = 0;
for(result = 1; result <= goal; result++)
{
rate.sleep();
// 建立反馈数据的响应消息对象
test3_action::checkFeedback fb;
// 计算当前进度
fb.progress_bar = (double)result/goal * 100;
// 发送当前状态
server->publishFeedback(fb);
}
// 多加了1,要减回来
result--;
//休眠,防止客户端处理最终响应消息的回调函数提前执行(对客户端实现时发现的问题进行优化)
rate.sleep();
// 建立结果数据的响应消息对象
test3_action::checkResult r;
// 这里要随着实现的目标变通
r.result = (result == goal);
// 发送结果数据
server->setSucceeded(r);
// 打印
std::cout << "检测" << result << "个零件" << std::endl;
}
int main(int argc, char *argv[])
{
// 初始化ROS节点
ros::init(argc, argv, "server_action");
// 创建节点句柄
ros::NodeHandle nh;
// 创建动作服务器对象
Server server(nh, "Check", boost::bind(&cb, _1, &server), false);
// 如果server实例化时设置的是不自动启动,想启动服务器需要执行start()函数
server.start();
// spin()回旋
ros::spin();
return 0;
}
没有详细展开讲述的内容在这里讲过了
导包
#include "actionlib/server/simple_action_server.h"
因为是action通信,所以需要导入action相关的头文件使用action的功能
建立服务器端对象
actionlib::SimpleActionServer<test3_action::checkAction> server(nh, "Check", boost::bind(&cb, _1, &server), false);
由于范型为test3_action::checkAction的动作服务器类会被多次使用,这里将其整个重定义为Server类,方便使用。
因为使用到了checkAction(包含了整个action文件),且这个泛型是我们自己定义的,需要引头文件
#include "test3_action/checkAction.h"
实例化时,这个类有多个重载(小编只会使用这种重载,其他的都不知道是建立对象还是什么用),以小编的代码为例,一共四个参数
参数1:节点句柄
参数2:话题名称
参数3:可调用的对象(下面详细讲解)
参数4:布尔值(决定服务器是否自动启动),如果不(false),如果想启动服务器,就要在后面的代码中执行start()函数进行服务器端启动
server.start();
boost::bind(&cb, _1, &server)讲解
boost中封装了一个bind()函数,该函数可以将其他函数或类中的成员方法绑定到特定的参数中,并返回一个可调用对象(是用于ation通信中的发送状态、当前进度、结果数据的消息的对象),具体需要
主参数1:函数指针(这里需要的是:实现的用于处理消息的回调函数)
主参数2:多个参数,函数需要多少个参数,就要有多少个参数,不通过这个函数传入需要占位
在boost::bind(&cb, _1, &server)中,_1是占位符
回调函数的实现
void cb (const test3_action::checkGoalConstPtr &goal_ptr, Server* server)
{
}
回调函数中
参数1:指针常量,动作客户端会发送目标数据的请求消息,由这个参数接收
参数2:服务器端对象指针,通过绑定可以将建立好的服务器端对象指针传入
由于回调函数的实现内容在上面的代码注释中讲解的比较详细,没什么需要补充的
测试
修改配置文件
直接添加在后面或者修改
add_executable(server03 src/server03.cpp)
add_dependencies(server03 ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
target_link_libraries(server03
${catkin_LIBRARIES}
)
编译(按下“Shift+Ctrl+b”)
打开终端“Ctrl+Alt+t”或右键桌面选择“在终端中打开”,进入工作空间目录
cd demo02_ws
分屏或多打开几个终端窗口,分别在不同窗口
启动ROS核心
roscore
刷新环境变量,这里需要用到四五个
source ./devel/setup.bash
启动action服务器端(test3_action是功能包名,server03是映射名称)
rosrun test3_action server03
先查看话题是否有了
rostopic list
都有了
使用这些话题,先运行用来打印当前进度和结果的ROS内置命令
打印当前进度
rostopic echo /Check/feedback
打印结果
rostopic echo /Check/result
再运行模拟客户端的命令
先输入
rostopic pub /C
这里的"C"是Check的首字母,根据你命名的话题改变,在没有重名的请况下可以让系统自动补齐话题
再输入"g",这个是action包定的,应该是一致的名称都是"xxx.../goal"
然后一直按tab键补齐,补到下图为止
修改goal后面的数据(这是可编辑的命令,还需要按回车键确认的),例如40
按下回车键
等到进度变成100%时,打印的结果为True,成功了
action客户端实现
#include "ros/ros.h"
#include "actionlib/client/simple_action_client.h"
#include "test3_action/checkAction.h"
// 结果响应消息的回调函数实现
void done_cb(const actionlib::SimpleClientGoalState &state, const test3_action::checkResultConstPtr &result)
{
// 判断响应状态
if(state.state_ == state.SUCCEEDED)
// 判断是否检测完成
if(result->result)
std::cout << "检测完成" << std::endl;
else
std::cout << "未检测完成" << std::endl;
else
std::cout << "响应失败" << std::endl;
// ***************** 无效代码 ******************
// ros::Rate rate(1);
// rate.sleep();
// if(/*result->result*/state.state_ == state.SUCCEEDED)
// std::cout << "检测完成" << std::endl;
// else
// std::cout << "响应失败" << std::endl;
//*********************************************
}
// 连接激活的回调函数实现
void active_cb()
{
std::cout << "开始检测" << std::endl;
}
// 连续反馈响应消息的回调函数实现
void feedback_cb(const test3_action::checkFeedbackConstPtr &feedback)
{
std::cout << "当前进度" << feedback->progress_bar << "%" <<std::endl;
// ROS_INFO("当前进度:%2.1f%%", feedback->progress_bar);
}
int main(int argc, char* argv[])
{
// 使用ROS_INFO时需要使用以解决中文乱码问题
setlocale(LC_ALL, "");
// 初始化ROS节点
ros::init(argc, argv, "client_action");
// 建立节点句柄
ros::NodeHandle nh;
// 创建动作客户端对象
actionlib::SimpleActionClient<test3_action::checkAction> client(nh, "Check");
// 挂起等待服务器启动
client.waitForServer();
// 静态实现目标数据设置
test3_action::checkGoal goal; // 创建目标数据对象
goal.goal = 40;
//发送目标的请求消息
client.sendGoal(goal, &done_cb, &active_cb, &feedback_cb);
// spin()回旋
ros::spin();
return 0;
}
没有展开的看这里
创建客户端对象
actionlib::SimpleActionClient<test3_action::checkAction> client(nh, "Check");
和创建服务器端对象一样的方式,只是不需要可调用对象(由于只用到了一次,没有重定义)
参数1:节点句柄
参数2:话题名称
参数3:布尔值(用于决定服务器是否自动启动),由于具有默认值(True),所以可以不写参数
发送目标请求消息
client.sendGoal(goal, &done_cb, &active_cb, &feedback_cb);
参数1:目标数据对象
参数2:用于处理结果响应消息的回调函数,最后调用
参数3:用于连接的回调函数,最先调用
参数4:用于处理连续反馈响应消息的回调函数
结果响应消息的回调函数实现
// 判断响应状态
if(state.state_ == state.SUCCEEDED)
// 判断是否检测完成
if(result->result)
std::cout << "检测完成" << std::endl;
else
std::cout << "未检测完成" << std::endl;
else
std::cout << "响应失败" << std::endl;
state中有封装一个成员state_,用于存储真正的响应状态
state中封装了另一个成员SUCCEEDED,是记录了响应成功的状态
将两个成员对比,就能判断响应的状态
其实(result->result)和(state.state_ == state.SUCCEEDED)都是判断响应状态的,但是小编在ubuntu22.04中,无论怎么改代码,让“结果响应消息的回调函数”休眠1秒,都会出现“客户端最终响应执行结束早于连续反馈的回调函数”的情况,就是“检查完成”比“当前进度:100%”早打印出来。
解决:这是服务器端的问题,在循环执行后休眠,再发送最终响应消息
在ubunt18.04中,任意使用(result->result)和(state.state_ == state.SUCCEEDED)判断响应状态(就是小编框起来的“无效代码”),都没有这种情况。
测试
修改配置文件
add_executable(client03 src/client03.cpp)
add_dependencies(client03 ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
target_link_libraries(client03
${catkin_LIBRARIES}
)
编译("Shift+Ctrl+b"),打开ROS核心,刷新环境变量,运行服务器端
运行客户端
rosrun test3_action client03
ubuntu22.04下的运行结果(没优化)
ubuntu22.04进行优化的运行结果
ubuntu18.04下的运行结果
Python实现
修改配置文件
在同一个工作空间目录下,配置过了,这里省略
建立源文件
不会点这里
action服务器端实现
#! /usr/bin/env python
#-*-coding: UTF-8 -*-
import rospy
import actionlib
from test3_action.msg import *
# 类实现
class MyAction:
# 无参构造函数
def __init__(self):
self.server = actionlib.SimpleActionServer("Check", checkAction, self.cb, False)
self.server.start()
# rospy.loginfo("服务器端启动......") # 调试使用
# 成员函数实现(相当于调用此行为与 action server 交互)
def cb(self, goal):
# 储存目标数据
goal_goal = goal.goal
# rospy.loginfo(goal_goal) # 调试使用
# 设置频率
rate = rospy.Rate(1)
# 实现实时发布当前进度
result = 0
for i in range(1, goal_goal+1):
result = i
rate.sleep() # 在打印前面是为了让客户端和服务器端打印进度同步
rospy.loginfo("检测%d个零件", result)
# 建立连续反馈响应消息对象
fb = checkFeedback()
# 设置进度
fb.progress_bar = i / goal_goal * 100
# rospy.loginfo(fb.progress_bar) # 调试使用
# 发布连续反馈的响应消息
self.server.publish_feedback(fb)
rate.sleep() # 休眠防止客户端的处理连续反馈的回调函数提前结束
# 建立最终结果的响应消息对象
r = checkResult()
# 设置最终结响应的数据
r.result = result == goal_goal # 不能理解这样生成的布尔类型数据,没有括号
# 发布最终响应数据
self.server.set_succeeded(r)
# 创建入口
if __name__ == "__main__":
# 初始化ROS节点
rospy.init_node("server_p_action")
# 使用无参构造函数实例化
myAction = MyAction()
# spin()回旋
rospy.spin()
没有详细展开讲述的内容在这里讲过了
这个服务器端的实现和以前的不太一样,但实现的本质方法都是实例化对象,并使用无参构造的方式调用回调函数,进行请求消息的处理和响应
导包
import actionlib
因为是action通信,所以需要导入action相关的包使用action的功能
from test3_action.msg import *
在导入这个包时, 会遇到报错,解决
靠近红色的波浪线,会跳出提示,点击“快速修复...”
点击“将"./devel/lib/python3/dist-packages"添加到extraPaths”中
看到选中的这一个路径被添加到了analysis的额外路径中,下面的"files.associations"不知道怎么多出来的
建立MyAction类
1. 无参构造
self.server = actionlib.SimpleActionServer("Check", checkAction, self.cb, False)
这里需要使用SimpleActionServer类来实例化对象(由于只用到了一次)
参数1:话题名称
参数2:泛型类型,需要导包,其中,import* 是指导入test3_action.msg中的所有包,和添加路径时的 "/** " 类似
from test3_action.msg import *
参数3:类中的行为(成员函数)
参数4:服务器是否自动启动
如果传True,就没什么可提的,但是传False后,想在想要使用时打开需要在后面的代码添加
server.start()
2. 成员函数实现
def cb(self, goal):
由于action服务器对象是类中的成员,所以可以直接调用,只需要一个参数目标数据的请求消息变量。
其他的基本是固定的实现对应固定的函数,而且在上面的代码中注释得也比较详细,就省略了
测试
1. 修改配置文件
还是可以选中直接添加到文件的后面,或者跟着小编改
catkin_install_python(PROGRAMS
scripts/server04.py
DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
2. 运行
编译(按下“Shift+Ctrl+b”)
打开终端“Ctrl+Alt+t”或右键桌面选择“在终端中打开”,进入工作空间目录
cd demo02_ws
分屏或多打开几个终端窗口,分别在不同窗口
启动ROS核心
roscore
刷新环境变量,这里需要用到四五个
source ./devel/setup.bash
运行服务器端(test3_action是功能包,server04.py是源文件名)
rosrun test3_action server04.py
查看话题
rostopic list
运行用来打印当前进度和结果的ROS内置命令
再次刷新环境变量,输入一半的命令行
rostopic pub /C
" C "是自己话题的首字母
按一下"tab"键(补齐,也可以输)
输入"g",下一个要补齐的是goal
大概按三下"tab"键,补齐到如下图内容为止
将goal的数据改成40,按下回车键
下图为开始运行的截图,由于连续反馈是
成功了
action客户端实现
#! /usr/bin/env python
#-*-coding: UTF-8 -*-
import rospy
import actionlib
from test3_action.msg import *
# 处理最终响应数据的回调函数
def done_cb(states, result):
# 判断响应是否成功 (states == actionlib.GoalStates.SUCCEEDED 和 result 的布尔值是一样的,只需要判断一个)
if states == actionlib.GoalStatus.SUCCEEDED:
if result:
rospy.loginfo("检测完成")
else :
rospy.loginfo("检测未完成")
else :
rospy.loginfo("响应失败")
# 激活时启动的回调函数
def active_cb():
rospy.loginfo("开始检测")
# 处理连续反馈消息的回调函数
def feedback_cb(feedback):
rospy.loginfo("%2.1f%%", feedback.progress_bar)
# 建立入口
if __name__ == "__main__":
# 初始化ROS节点
rospy.init_node("client_p_action")
# 创建动作客户端对象
client = actionlib.SimpleActionClient("Check", checkAction)
# 等待服务器启动
client.wait_for_server()
# 静态设置目标数据
goal = checkGoal()
goal.goal = 40
# 发送目标数据请求消息并处理响应
client.send_goal(goal, done_cb, active_cb, feedback_cb)
# spin()回旋
rospy.spin()
创建动作客户端对象
使用SimpleActionClient()函数,
参数1:话题名称
参数2:泛型
静态设置目标数据
动态的设置可以看这个
创建目标数据请求消息对象,调用里面封装的目标数据变量,进行设置
发送目标数据请求消息
client.send_goal(goal, done_cb, active_cb, feedback_cb)
参数1:目标数据请求消息对象
参数2:处理最终响应数据的回调函数,最后调用并结束
参数3:激活时启动的回调函数,最先调用并结束
参数4:处理连续反馈消息的回调函数
回调函数实现
由于大部分实现比较简短且注释详细,就不再讲解了
在实现处理最终响应数据的回调函数时,由于最终的响应消息会将响应的状态和响应的结果按这个顺序发送,所以用states和result的参数顺序接收
在实现处理连续反馈响应消息的回调函数时,因为只有连续反馈响应消息发送,所以用feedback命名接收的变量,容易看懂这个变量怎么用。
测试
先进行编译("Shift+Ctrl+b")
只需要打开ROS核心,并刷新环境变量,再运行服务器端和客户端,就能得到下图
# 如果不在工作空间目录下还要运行,以demo02_ws为例
cd demo02_ws
# 启动ROS核心
roscore
# 刷新环境变量
source ./devel/setup.bash
# 运行服务器端 test3_action是功能包,server04.py是python源文件
rosrun test3_action server04.py
# 运行客户端 test3_action是功能包,client04.py是python源文件
rosrun test3_action client04.py