zoukankan      html  css  js  c++  java
  • ROS naviagtion analysis: move_base

    博客转载自: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_ = 0;
        }

    然后检查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的使用,简化了各种具体实现,因此在接下来的文章中着重分析这几部分。

  • 相关阅读:
    select + 回调 + 事件循环
    进程间通信
    多进程复习
    concurrent.futures 使用及解析
    多线程复习 Rlock ,Condition,Semaphore
    生成器读取大文件应用
    VS远程调试与附加调试
    Linux后台有个systemd-r进程,占用5355等端口
    linux中 shell编程 判断服务是否运行
    使用Keepalived实现linux高可用集群
  • 原文地址:https://www.cnblogs.com/flyinggod/p/9081194.html
Copyright © 2011-2022 走看看