Navigation源码阅读之move_base

本文深入解析move_base模块,它是ROS导航堆栈的核心组件,负责小车的全局路径规划与局部控制。文章阐述了其工作流程,包括actionlib交互、状态机控制、全局路径规划、速度计算及避障策略。此外,还探讨了move_base与代价地图、本地规划器的紧密合作,以及如何通过动态重配置调整参数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

move_base是小车控制的核心模块,这一块从宏观上调控了整个导航过程。先瞄一眼头文件:

typedef actionlib::SimpleActionServer<move_base_msgs::MoveBaseAction> MoveBaseActionServer;

  enum MoveBaseState
  {
    PLANNING,
    CONTROLLING,
    CLEARING
  };

  enum RecoveryTrigger
  {
    PLANNING_R,
    CONTROLLING_R,
    OSCILLATION_R
  };

它声明了一个actionlib,关于它的详细介绍点击https://www.cnblogs.com/feixiao5566/p/4757916.html的博文,另外,状态机告诉我们小车运行的状态分为规划路径中,控制中(运行中),清理中(recovery behaviors)三种。

一、类的主文件,MoveBase的构造函数只接受tf变换(base_link->map),在初始化列表中利用pluginlib加载了nav_core下面的三个基类,并初始化了全局代价地图和局部代价地图,这几个参数对整个运行过程至关重要。

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

actionlib服务器绑定了成员函数 MoveBase::executeCb,当有client对server寻求服务时,该成员函数将进行反应。

另外,注意这个新建的线程,它负责新的全局路径的规划,无论是一段导航刚开始时、中途换目标、避障绕行运行,都会调用该线程。

planner_thread_ = new boost::thread(boost::bind(&MoveBase::planThread, this));

构造函数的末尾,状态机被初始化为planning,并开启了dynamic_reconfigure,这样可以在rqt中动态调整句柄ros::NodeHandle("~")下的参数。

dsrv_ = new dynamic_reconfigure::Server<move_base::MoveBaseConfig>(ros::NodeHandle("~"));
dynamic_reconfigure::Server<move_base::MoveBaseConfig>::CallbackType cb = boost::bind(&MoveBase::reconfigureCB, this, _1, _2);
dsrv_->setCallback(cb);

二、当一个goal发出时(例如在rviz中点击nav goal),它首先被MoveBase::goalCB函数捕获,这个函数很简单,它将geometry_msgs::PoseStamped格式的goal转化为actionlib格式的move_base_msgs::MoveBaseActionGoal发布,便于actionlib处理。

三、当actionlib格式发布后,MoveBase::executeCb函数开始运行,它先检查这个goal是否合法,再通过条件变量通知挂起的MoveBase::planThread干活。

planner_cond_.notify_one();

 1.看看MoveBase::planThread是怎么规划出全局路径的。这个函数乍看之下有些费解,需要对线程进行复习~

boost::unique_lock<boost::recursive_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;
  }
…………

这一段代表着锁对象传给了条件变量的wait函数中,该线程会被加入等待序列中,互斥量可以被其他线程申请获得。当条件变量被满足时该线程会再次被唤醒并获得互斥量。

当然一开始规划全局路径时线程被唤醒了,调用MoveBase::makePlan函数:

bool gotPlan = n.ok() && makePlan(temp_goal, *planner_plan_);

当然一般这个布尔量会是true的,这样会保存该次产生的全局路径,以及上一次产生的路径。当然如果路径没有产生,说明遇到问题了,例如小车陷进代价地图中,若没有及时解救会触发recovery behaviors。

2.MoveBase::makePlan是通过当前的全局代价地图产生全局路径的,它首先将小车当前位置设为start:

geometry_msgs::PoseStamped start;
tf::poseStampedTFToMsg(global_pose, start);

再调用global_planner的makePlan函数去计算自主导航路径,在类初始化时,默认使用的global_planner是navfn_ros,即NavFnROS::makePlan。这样便获得了新鲜出炉的全局路径。

3.再回到MoveBase::executeCb函数被岔开的地方,设置好时间戳后,开始了漫长的大循环,这个循环直到导航结束或者目的点被抢占。当出现了新目的点时,planThread被再次叫醒干活,重复一遍之前讲述的过程。当然,会再次检查目的点的合法性(格式、frame_id)。如果没有出现打断循环的信息,循环将调用MoveBase::executeCycle函数检查每个周期是否正常。

4.MoveBase::executeCycle这个函数,它不断确定小车当前位置交给actionlib(虽然这一点不是太明白)。

tf::Stamped<tf::Pose> global_pose;
getRobotPose(global_pose, planner_costmap_ros_);
geometry_msgs::PoseStamped current_position;
tf::poseStampedTFToMsg(global_pose, current_position);

接着,需要确认小车不会来回震荡,这通过判断是否运行了超过一定距离实现;并且,需要确认代价地图是否时刻在更新:

if(!controller_costmap_ros_->isCurrent())
{
  ROS_WARN("[%s]:Sensor data is out of date, we're not going to allow commanding of the base for safety",ros::this_node::getName().c_str());
  publishZeroVelocity();
  return false;
}

讲道理这个警告我看过很多次,有可能是代价地图更新的频率比较低,也有可能是改写时其他函数劫持了传感器信息导致。总之不保证代价地图的实时性小车将无法运行。

在第一次执行executeCycle或者遇到新的plan之后,需要调用DWAPlannerROS::setPlan(假设使用DWA产生速度)。当在正常执行导航过程中,进入状态机的判断:

(1)状态机是PLANNING,这可能会出现在避障过程不顺利的情况下,一个经典场景就是小车被很多移动的人围住,自然无法在一个控制周期内产生新路径。在这个情况,planThread会再次被唤醒。

(2)状态机是CONTROLLING,这属于正常情况,它先判断是否已到达终点,再进入本函数的关键部分——速度计算部分。

if(tc_->computeVelocityCommands(cmd_vel))
{
  ROS_DEBUG_NAMED( "move_base", "Got a valid command from the local planner: %.3lf, %.3lf, %.3lf",cmd_vel.linear.x, cmd_vel.linear.y, cmd_vel.angular.z );
  last_valid_control_ = ros::Time::now();
  //make sure that we send the velocity command to the base
  vel_pub_.publish(cmd_vel);
  if(recovery_trigger_ == CONTROLLING_R) recovery_index_ = 0;
}

它调用base_local_planner(默认为DWA)的计算速度函数,成功则下发给base_controller。如果失败,说明遇到了障碍物需要重新规划。当然重新规划有个次数限制,超过了就宣告失败,进入清扫过程了,按源码的逻辑,这次导航就快没救了,需要关掉节点再重来。。。

if(ros::Time::now() > attempt_end)
{
  getRobotPose(global_pose, controller_costmap_ros_);
  lastControl_x=global_pose.getOrigin().x();
  lastControl_y=global_pose.getOrigin().y();
  lastControl_orien_z=global_pose.getRotation().getZ();

  //we'll move into our obstacle clearing mode
  publishZeroVelocity();
  state_ = CLEARING;
  recovery_trigger_ = CONTROLLING_R;
}

下面这一段是避障绕行的关键,这里直接粗暴地发布一个零速度让车停下,再重新唤醒planThread,实操发现这样对车的电机加速度要求很高,很容易抽风~

else
{
  //otherwise, if we can't find a valid control, we'll go back to planning
  last_valid_plan_ = ros::Time::now();
  planning_retries_ = 0;
  state_ = PLANNING;
  publishZeroVelocity();
 //enable the planner thread in case it isn't running on a clock
  boost::unique_lock<boost::recursive_mutex> lock(planner_mutex_);
  runPlanner_ = true;
  planner_cond_.notify_one();
  lock.unlock();
}

(3)状态机是CLEARING,它默认执行温柔清空——暴力清空——放弃的过程,在这里改写的地方很多,可以通过自定义清扫过程来改变小车的clearing状态,例如倒退一小段距离等等。。。

这就是MoveBase::executeCycle的周期过程,如果小车没有到终点,是会返回false的,我们再回到executeCb被岔开的地方。

5.

bool done = executeCycle(goal, global_plan);

//if we're done, then we'll return from execute
if(done)
  return;

//check if execution of the goal has completed in some way

ros::WallDuration t_diff = ros::WallTime::now() - start;
ROS_DEBUG_NAMED("move_base","Full control cycle time: %.9f\n", t_diff.toSec());

r.sleep();
//make sure to sleep for the remainder of our cycle time
if(r.cycleTime() > ros::Duration(1 / controller_frequency_) && state_ == CONTROLLING)
 ROS_WARN("Control loop missed its desired rate of %.4fHz... the loop actually took %.4f seconds", controller_frequency_, r.cycleTime().toSec());

当executeCycle返回true,executeCb的大循环就被跳出了。否则,计算一下本次循环的执行时间,超过了控制频率(可设置),会报警告。这个警告我也经常看见,多半是由于自主寻迹的算法耗费的时间有点多,或者DWA计算速度的打分筛选过程太久,我们也不一定要用源码设计的A*,Dijkstra算法,大胆尝试一些别的算法~

四、这就是一次完整导航过程的调用流程,由于我以前的一些操作失误,经常让小车陷入clear与abort的陷阱中,对recovery behavior都有心理阴影了,这一块先不详细分析了。。。总之,move_base是导航的关键模块,调用了其他模块的很多接口,任何用navigation源码做导航算法的朋友都应该对其非常了解,这一块自由创作的空间也很大,让小车如臂使指般运行吧~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值