博客转载自:https://blog.csdn.net/u013158492/article/details/50483123

这是navigation的第一篇文章,主要通过分析ROS代码级实现,了解navigation。本文从move_base入手。

机器人导航主要框架

Navigation Stack主要组成部分:move_base

用户调用movebase是通过传入带tf参数的构造函数:

move_base::MoveBase move_base( tf );

以下分析move_base的构造函数:

MoveBase::MoveBase(tf::TransformListener& tf):
tf_(tf),
as_(NULL),
planner_costmap_ros_(NULL), controller_costmap_ros_(NULL),
bgp_loader_("nav_core", "nav_core::BaseGlobalPlanner"),
blp_loader_("nav_core", "nav_core::BaseLocalPlanner"),
recovery_loader_("nav_core", "nav_core::RecoveryBehavior"),
planner_plan_(NULL), latest_plan_(NULL), controller_plan_(NULL),
runPlanner_(false), setup_(false), p_freq_change_(false), c_freq_change_(false), new_global_plan_(false) `

这部分是构造函数的初始化列表,可以看到几个重要的参数:

planner_costmap_ros_(NULL), controller_costmap_ros_(NULL),
bgp_loader_("nav_core", "nav_core::BaseGlobalPlanner"),
blp_loader_("nav_core", "nav_core::BaseLocalPlanner"),
recovery_loader_("nav_core", "nav_core::RecoveryBehavior")

planner_costmap_ros_是用于全局导航的地图,controller_costmap_ros_ 是局部导航用的地图,地图类型为经过ROS封装的costmap_2d::Costmap2DROS* 。关于地图类型的分析会在接下来的文章中进行。 
bgp_loader_ 是global planner, blp_loader_ 是local planner。二者的声明为:

pluginlib::ClassLoader<nav_core::BaseGlobalPlanner> bgp_loader_;
pluginlib::ClassLoader<nav_core::BaseLocalPlanner> blp_loader_;

是属于一个模板类ClassLoader,实例化为BaseGlobalPlanner或者BaseLocalPlanner。关于pluginlib的分析也有在接下来的文章中进行。 bgp_loader_ 和 blp_loader_ 的作用是为以下类成员提供实例化:

boost::shared_ptr<nav_core::BaseLocalPlanner> tc_;
boost::shared_ptr<nav_core::BaseGlobalPlanner> planner_;
//initialize the global planner
try {
planner_ = bgp_loader_.createInstance(global_planner);
planner_->initialize(bgp_loader_.getName(global_planner), planner_costmap_ros_);
} `
` //create a local planner
try {
tc_ = blp_loader_.createInstance(local_planner);
ROS_INFO("Created local_planner %s", local_planner.c_str());
tc_->initialize(blp_loader_.getName(local_planner), &tf_, controller_costmap_ros_);
}

进入构造函数,第一句:

as_ = new MoveBaseActionServer(ros::NodeHandle(), "move_base", boost::bind(&MoveBase::executeCb, this, _1), false);

as_维护movebase的actionServer状态机,并且新建了一个executeCb线程。 接下来从private_nh中获取参数设定值。 设置plan buffer

planner_plan_ = new std::vector<geometry_msgs::PoseStamped>();
latest_plan_ = new std::vector<geometry_msgs::PoseStamped>();
controller_plan_ = new std::vector<geometry_msgs::PoseStamped>();

注意,这三个plan都是global_plan,最终由controller_plan_ 传给 local planner:

Line821 if(!tc_->setPlan(*controller_plan_)){

开启planner thread:

//set up the planner's thread
planner_thread_ = new boost::thread(boost::bind(&MoveBase::planThread, this));

接下来设置一堆publisher, 包括cmd_vel,current_goal,goal,之后是机器人的几何尺寸设定等。 然后实例化planner_costmap_ros_,controller_costmap_ros_

planner_costmap_ros_ = new costmap_2d::Costmap2DROS("global_costmap", tf_);
controller_costmap_ros_ = new costmap_2d::Costmap2DROS("local_costmap", tf_);

开启costmap基于传感器数据的更新

// Start actively updating costmaps based on sensor data
planner_costmap_ros_->start();
controller_costmap_ros_->start();

载入recovery behavior,这部分是状态机的设计问题,可以采用默认的设定,也可以用户指定:

if(!loadRecoveryBehaviors(private_nh)){
loadDefaultRecoveryBehaviors(); }

然后开启actionserver: as_->start(); 开启参数动态配置服务,完了~这就是整个构造函数做的事情。

以下分析各个成员函数:
void MoveBase::clearCostmapWindows(double size_x, double size_y){} 
首先通过planner_costmap_ros_->getRobotPose(global_pose)获取在全局地图的pose,然后以这个点为中心,找到以size_x和size_y 为边长的矩形的四个顶点,调用planner_costmap_ros_->getCostmap()->setConvexPolygonCost(clear_poly, costmap_2d::FREE_SPACE); 完成对机器人所在区域的clear工作。同样的操作在controller_costmap_ros_ 上也操作一遍,这样关于globa costmap 和local costmap都已经在机器人所在的区域完成clear工作。
bool MoveBase::planService(nav_msgs::GetPlan::Request &req, nav_msgs::GetPlan::Response &resp) 
req参数包含了起点和目标信息。首先判断global planner 的costmap是否存在以及是否能够得到机器人所在位置,然后调用clearCostmapWindows 完成对机器人区域的clear,然后调用 if(!planner_->makePlan(start, req.goal, global_plan) || global_plan.empty()){} 这是完成plan计算的核心部分。逻辑判断这个调用是否成功,如果失败,则在目标区域附件搜索,更改req.goal的值,并重新调用makePlan,如果失败,则此次plan失败,无解。

bool MoveBase::makePlan(const geometry_msgs::PoseStamped& goal, std::vector<geometry_msgs::PoseStamped>& plan){}
首先lock 住global costmap: 
boost::unique_lock<costmap_2d::Costmap2D::mutex_t> lock(*(planner_costmap_ros_->getCostmap()->getMutex())); 
然后判断global costmap是否存在以及是否能够获得机器人所在位置,如果失败,则直接返回失败。最后调用核心代码:if(!planner_->makePlan(start, goal, plan) || plan.empty()){} 。

geometry_msgs::PoseStamped MoveBase::goalToGlobalFrame(const geometry_msgs::PoseStamped& goal_pose_msg){}
这个函数的核心功能是获得机器人坐标系和全局地图坐标系的关系: 
tf_.transformPose(global_frame, goal_pose, global_pose); 。

void MoveBase::wakePlanner(const ros::TimerEvent& event) 
planner_cond_.notify_one(); 通过boost thread的条件变量planner_cond_ 唤醒线程 planThread

MoveBase的核心函数是线程planThread,void MoveBase::planThread() 
进入函数首先会将planner_mutex_ 锁住:boost::unique_lock<boost::mutex> lock(planner_mutex_);

while(n.ok()){
//check if we should run the planner (the mutex is locked)
while(wait_for_wake || !runPlanner_){
//if we should not be running the planner then suspend this thread
ROS_DEBUG_NAMED("move_base_plan_thread","Planner thread is suspending");
planner_cond_.wait(lock);
wait_for_wake = false;
}

因此线程继续运行依赖于变量runPlanner_ ,唤醒线程依赖于条件变量planner_cond_ 这两个变量是后续其他函数需要唤醒并执行线程planThread的关键。 然后调用makePlan:

//run planner
planner_plan_->clear();
bool gotPlan = n.ok() && makePlan(temp_goal, *planner_plan_);

如果makePlan成功,则上锁,然后对planner_plan_ 拷贝,过程如下: 
将最新的plan path给latest_plan_,然后将上一次的plan path给planner_plan_,这样这两个buffer就保存了最新的plan 和上一次的plan

void MoveBase::executeCb(const move_base_msgs::MoveBaseGoalConstPtr& move_base_goal) 

首先检查传入的参数合法性,主要是pose.orientation检查。然后调用geometry_msgs::PoseStamped goal = goalToGlobalFrame(move_base_goal->target_pose) 将goal转换为在全局地图下的goal。 
然后开启planner:

 boost::unique_lock<boost::mutex> lock(planner_mutex_);
planner_goal_ = goal;
runPlanner_ = true;
planner_cond_.notify_one();
lock.unlock();

然后进入死循环:首先判断as_是否是可抢占的,检查是否有新的goal,如果有则处理新的goal,判断new goal的合法性,并开启planThread 为new goal 生成plan。继续往下,判断goal的坐标系和planner_costmap_ros_ 的坐标系是否是一致的,如果不是,则调用goal = goalToGlobalFrame(goal) 转换到 planner_costmap_ros_的坐标系下,然后重新开启planThread。最终,我们拿到了global plan, 接下来调用核心代码:

bool done = executeCycle(goal, global_plan); 

在死循环最后sleep足够时间,以满足frequency的要求。如果死循环被退出,则在本函数末尾开启planThread,似的线程能正常执行到末尾,当线程再次在死循环中运行时,检查while(n.ok()){ }会失败,然后线程正常清理退出。bool MoveBase::executeCycle(geometry_msgs::PoseStamped& goal, std::vector<geometry_msgs::PoseStamped>& global_plan){},进入函数,第一行:boost::recursive_mutex::scoped_lock ecl(configuration_mutex_)会 锁住 函数void MoveBase::reconfigureCB(move_base::MoveBaseConfig &config, uint32_t level){ },处理震荡问题

if(distance(current_position, oscillation_pose_) >= oscillation_distance_)
{
last_oscillation_reset_ = ros::Time::now();
oscillation_pose_ = current_position;
if(recovery_trigger_ == OSCILLATION_R)
recovery_index_ = ;
}

然后检查controller_costmap_ros_ 是否是最新的if(!controller_costmap_ros_->isCurrent()){} 。然后检查是否有新的plan,如果有,则 上锁递推 controller_plan_和 latest_plan_。 
然后将global的plan传递给tc_: if(!tc_->setPlan(*controller_plan_)){}, 如果失败则关闭planThread并直接退出。 
然后进入movebase的控制逻辑的状态机: 状态机根据两个变量实现状态表示:{state_, recovery_trigger_} ,这两个变量的取值

enum MoveBaseState{PLANNING, CONTROLLING, CLEARING};
enum RecoveryTrigger{PLANNING_R, CONTROLLING_R,OSCILLATION_R};

RecoveryTrigger 对应的是在相应状态下出现失败。注意,这个状态机和planThread 是在交互的。状态机一旦进入PLANNING状态,就唤醒planThread线程,在planThread线程中一旦找到plan就将state_设置为CONTROLLING,如果没有找到plan,则state_ = CLEARING;recovery_trigger_ = PLANNING_R。如果状态机在CONTROLLING状态,首先检查是否到达目的地if(tc_->isGoalReached()){} ,如果到达目的地,则resetState(); 并关闭planThread runPlanner_ = false; 如果震荡条件满足,则:publishZeroVelocity();state_ = CLEARING; recovery_trigger_ = OSCILLATION_R,然后锁住local costmap

boost::unique_lock<costmap_2d::Costmap2D::mutex_t> lock(*(controller_costmap_ros_->getCostmap()->getMutex()));

然后由baseLocalPlanner计算cmd_vel:

if(tc_->computeVelocityCommands(cmd_vel)){}

成功则可以控制地盘了,如果失败,检查是否超时,如果超时:publishZeroVelocity();state_ = CLEARING;recovery_trigger_ = CONTROLLING_R; 没有超时:state_ = PLANNING;publishZeroVelocity(); 并唤醒planThread线程。 如果状态机在CLEARING状态:

if(recovery_behavior_enabled_ && recovery_index_ < recovery_behaviors_.size())
{
recovery_behaviors_[recovery_index_]->runBehavior();**重点内容**
last_oscillation_reset_ = ros::Time::now();
state_ = PLANNING;
recovery_index_++;
}
else
{
//disable the planner thread
boost::unique_lock<boost::mutex> lock(planner_mutex_);
runPlanner_ = false;
lock.unlock();
resetState();
return true;
}

moveBase基本上主要内容就是这些,其他未分析的函数都比较简单。但是在moveBase的实现中,主要依赖于costmap_2d 以及global planner 和local planner。还有pluginlib的使用,简化了各种具体实现,因此在接下来的文章中着重分析这几部分。

ROS naviagtion analysis: move_base的更多相关文章

  1. ROS naviagtion analysis: costmap_2d--ObstacleLayer

    博客转载自:https://blog.csdn.net/u013158492/article/details/50493676 构造函数 ObstacleLayer() { costmap_ = NU ...

  2. ROS naviagtion analysis: costmap_2d--StaticLayer

    博客转载自:https://blog.csdn.net/u013158492/article/details/50493246 从UML中能够看到,StaticLayer主要是在实现Layer层要求实 ...

  3. ROS naviagtion analysis: costmap_2d--LayeredCostmap

    博客转自:https://blog.csdn.net/u013158492/article/details/50490490 在数据成员中,有两个重要的变量:Costmap2D costmap_和 s ...

  4. ROS naviagtion analysis: costmap_2d--Costmap2DROS

    博客转载自:https://blog.csdn.net/u013158492/article/details/50485418 在上一篇文章中moveBase就有关于costmap_2d的使用: pl ...

  5. ROS naviagtion analysis: costmap_2d--CostmapLayer

    博客转自:https://blog.csdn.net/u013158492/article/details/50493220 这个类是为ObstacleLayer StaticLayer voxelL ...

  6. ROS naviagtion analysis: costmap_2d--Layer

    博客转载自:https://blog.csdn.net/u013158492/article/details/50493113 这个类中有一个LayeredCostmap* layered_costm ...

  7. ROS naviagtion analysis: costmap_2d--Costmap2D

    博客转载自:https://blog.csdn.net/u013158492/article/details/50492506 Costmap2D是存储地图数据的父类.真正的地图数据就存储在数据成员u ...

  8. Navigation源码(一) move_base最全解析

    一.概述 目测是全网最全的解析,花了几个小时通读并整理的,供大家参考学习. 概况的话可以看下古月居 https://www.guyuehome.com/270,其实它是翻译官方的,英语ok的可以去ro ...

  9. 测试social_navigation_layers

    目标:测试social_navigation_layers 方法: 使用move_base接口启动costmap_2d 这样就能直接用configure方法来进行测试不用自己写代码 一.启动move_ ...

随机推荐

  1. MYSQL中防止插入重复记录的解决方案(无重复值更新)

    说明:一般我们使用MYSQL插入记录时,类似于这样的语句: insert into table_name(email,phone,user_id) values(‘test9@163.com’,’99 ...

  2. Weblogic启动成功,控制台打不开

    有时候,我们在linux操作系统上成功启动了weblogic,也查看了7001端口的状态是开启的.但是访问weblogic控制台没有反应,也没有报错. 使用 netstat -ano | grep 7 ...

  3. hdu 4609 3-idiots——FFT

    题目:http://acm.hdu.edu.cn/showproblem.php?pid=4609 答案就是随便选三条边的方案 - 不合法的方案. 不合法的方案就是算出 x+y = k 的方案数 g[ ...

  4. AGC006 C Rabbit Exercise——思路(置换)

    题目:https://agc006.contest.atcoder.jp/tasks/agc006_c 选了 i 位置后 x[ i ] = x[ i-1 ] + x[ i+1 ] - x[ i ] . ...

  5. 在window下 进入系统盘命令

    示例: cd C:\work 查看文件夹直接在当前路径下输入 dir 在当前路径下输入 dir/? 查看帮助

  6. mysql不能使用localhost登录

    解决mysql不能使用localhost or 127.0.0.1登录 参考:http://soft.chinabyte.com/database/409/12669909.shtml 因为root账 ...

  7. C# messagebox 居中父窗体

    在右边项目上点击右键->Add->class,添加MessageBoxEx类: using System; using System.Windows.Forms; using System ...

  8. 用 Python 实现文件查找

    用 Python 实现文件查找(BIF实现及队列实现) (1)利用内置函数实现文件查找 1.功能:返回用户输入的文件的绝对路径 2.设计思路: (1)用户输入在哪个盘进行查找 (2)遍历此盘文件,若为 ...

  9. BroadcastReceiver用法

    动态注册广播接收器 1.创建一个Receiver继承BroadcastReceiver,并重写onReceiver() 2.在Activity的OnCreate()中添加广播接收器想要接收的actio ...

  10. microtime() 测试代码执行时间,提高编码效率

    <?php $b_time = microtime(true); $a = array("); $count = ; foreach ($a as $key => $value) ...