博客转载自: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. 设计模式之访问者(visitor)模式

    在患者就医时,医生会根据病情开具处方单,很多医院都会存在以下这个流程:划价人员拿到处方单之后根据药品名称和数量计算总价,而药房工作人员根据药品名称和数量准备药品,如下图所示. 在软件开发中,有时候也需 ...

  2. java多线程:线程体往外抛出异常的处理机制实践

    1当线程的线程体内部无捕获异常,将异常抛出线程体外,不同情况下,程序处理机制 测试类 package com.ehking.bankchannel.domesticremit.facade.impl; ...

  3. Bootstrap中时间(时间控件)的设计

    运用bootstrap的时间控件,生成时间选择器. 1.截图:有以下这些样式 10年视图        年视图         月视图         日视图         小时视图 2.视图设计: ...

  4. .NET系统框架

    本书是一本讲解.NET技术的书籍,目标读者群也是在.NET框架(.NET Framework)下进行开发的程序员,因此我们无法回避的问题就是:什么是.NET框架?它包含了哪些内容?为开发程序提供了哪些 ...

  5. Web缓存技术

    本章导读 缓存主要是为了提高数据的读取速度.因为服务器和应用客户端之间存在着流量的瓶颈,所以读取大容量数据时,使用缓存来直接为客户端服务,可以减少客户端与服务器端的数据交互,从而大大提高程序的性能. ...

  6. 8 函数类型——《Swift3.0从入门到出家

    Swift语言中每一个函数都有它特定的数据类型,称其为函数类型 函数类型和基本数据类型一样,可以定义变量或者常量,可以定义函数形参,也可以做为函数的返回值类型 函数类型的格式为:参数列表的数据类型—& ...

  7. bzoj 4032 [HEOI2015]最短不公共子串——后缀自动机

    题目:https://www.lydsy.com/JudgeOnline/problem.php?id=4032 不是 b 的子串的话就对 b 建后缀自动机,在 a 上枚举从每个位置开始的子串或者找子 ...

  8. dubbox消费者启动成功,却无法连接注册中心

    使用dubbox作为服务提供端很好实现,因为git的说明和网上有很多的例子可供参考,但是消费端都一笔带过,简单得很,初学者往往以为只要配置如下3样东西就够了: <?xml version=&qu ...

  9. 【openCV学习笔记】在Mac上配置openCV步骤详解

    (1)安装Homebrew:(需要Ruby) 注:因为snow leopard 以后已经自带Ruby了,所有可以不用自己安装Ruby. 看一下Homebrew的官网: http://mxcl.gith ...

  10. codeforce 980B - Marlin(构造)

    Marlin time limit per test 1 second memory limit per test 256 megabytes input standard input output ...