rosbag的读写

头文件

1
2
3
4
5
6
#include <rosbag/bag.h>
#include <rosbag/view.h>
#include <rosbag/message_instance.h>
#include <boost/foreach.hpp>
#define foreach BOOST_FOREACH
#include <nav_msgs/Path.h>

读写bag文件

open函数默认是读模式。

写bag文件:

1
2
3
4
5
6
7
8
9
10
rosbag::Bag bag;
try{
bag.open("/home/user/plan.bag", rosbag::bagmode::Write);
}
catch (const rosbag::BagException& ex) {
ROS_ERROR("open rosbag error: %s", ex.what() );
return;
}
bag.write("record_plan",ros::Time::now(), coverage_plan);
bag.close();

打开文件的模式有:enum rosbag::bagmode::BagMode
1
2
3
Write   
Read
Append

读bag文件一定要用trycatch

下面的例子,bag文件只有一个话题/move_base/transformed_plan,一堆nav_msgs/Path消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
rosbag::Bag bag;
try{
bag.open("/home/user/plan.bag", rosbag::bagmode::Read);
}
catch (const rosbag::BagException& ex) {
ROS_ERROR("open rosbag error: %s", ex.what() );
return;
}
std::vector<std::string> topics;
bool bag_read_once = false;

topics.push_back(std::string("record_plan") );
rosbag::View view(bag, rosbag::TopicQuery(topics) );

BOOST_FOREACH(rosbag::MessageInstance const m, view)
{
nav_msgs::Path::ConstPtr i = m.instantiate<nav_msgs::Path>();
plan_from_bag.header.frame_id = i->header.frame_id;
plan_from_bag.header.stamp = i->header.stamp;
plan_from_bag.header.seq = i->header.seq;

unsigned int path_size = i->poses.size();
ROS_INFO("plan from bag size: %zu",i->poses.size() );
plan_from_bag.poses.reserve(path_size);
if(bag_read_once) break;

plan_from_bag.poses = i->poses;
bag_read_once = true;
}
bag.close();

注意读的时候,topics容器的元素必须还是number,与写文件时一致。如果不一致,读文件会不执行foreach

用到的类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* Create a view and add a query
* param bag The bag file on which to run this query
* param query The actual query to evaluate which connections to include
* param start_time The beginning of the time range for the query
* param end_time The end of the time range for the query
* param reduce_overlap If multiple views return the same messages, reduce them to a single message
*/
View(Bag const& bag, boost::function<bool(ConnectionInfo const*)> query,
ros::Time const& start_time = ros::TIME_MIN, ros::Time const& end_time = ros::TIME_MAX, bool const& reduce_overlap = false);

class ROSBAG_DECL TopicQuery
{
public:
TopicQuery(std::string const& topic);
TopicQuery(std::vector<std::string> const& topics);

bool operator()(ConnectionInfo const*) const;

private:
std::vector<std::string> topics_;
};

rosbag::View的常用函数

  • iterator begin() Simply copy the merge_queue state into the iterator.
  • iterator end() Default constructed iterator signifies end.
  • ros::Time getBeginTime()
  • ros::Time getEndTime()
  • uint32_t size () 获得bag文件中所含消息的个数,不是文件大小


其他常用函数

1
2
3
4
5
6
7
// Get the filename of the bag
std::string getFileName () const

// Get the current size of the bag file (a lower bound) More...
uint64_t getSize () const

bool isOpen () const

问题

bag文件 unindexed


这种情况下如果没有用trycatch就会出错

我查来查去,没发现C++ API里有类似rosbag reindex命令的函数,只能不处理。似乎python里有,没有深入研究,

参考:
rosbag API


Costmap2DROS 类

Costmap2DROS类是2D Costmap的ROS封装类,处理订阅的话题,这些话题提供了对障碍物的观测,方式为点云或激光扫描消息。 costmap_2d::Costmap2DROS给用户提供了纯2D的接口,这意味着查询障碍只能在列上进行。例如,在XY平面上位于同一位置的桌子和鞋,虽然在Z方向上有差异但是它们在costmap_2d::Costmap2DROS对象代价地图中对应的cell上拥有相同的代价值。

A ROS wrapper for a 2D Costmap. Handles subscribing to topics that provide observations about obstacles in either the form of PointCloud or LaserScan messages.

  • Costmap2D * getCostmap ()

实际是return layered_costmap_->getCostmap();,跟getLayeredCostmap()->getCostmap()一样. Return a pointer to the “master” costmap which receives updates from all the layers.

1
costmap_2d::Costmap2D* global_costmap = planner_costmap_ros_->getCostmap();

  • bool getRobotPose(tf::Stamped& global_pose) const;

获取机器人在代价地图的global frame中的位姿,成功获取则返回true

1
2
3
4
5
6
7
8
9
10
11
// 这个返回类型很不方便
tf::Stamped<tf::Pose> pose;
if(!costmap->getRobotPose(pose))
{
ROS_ERROR("Cannot clear map because pose cannot be retrieved");
return;
}
double x = pose.getOrigin().x();
double y = pose.getOrigin().y();
double yaw = tf::getYaw(pose.getRotation());
double yaw_norm = angles::normalize_angle(tf::getYaw(pose.getRotation() ) );

对于全局代价地图,一般是返回在map坐标系下的位姿,对于局部代价地图,一般是在odom或者map下的位姿。
Costmap2DROS::getRobotPose函数调用分析

  • std::string getGlobalFrameID() 和 std::string getBaseFrameID()

这两个返回的就是代价地图yaml里配置的全局和局部坐标系

  • LayeredCostmap* getLayeredCostmap()

  • std::vector<geometry_msgs::Point> getRobotFootprint() 获取机器人边界(在机器人坐标系下,包含padding),返回的是vector<geometry_msgs::Point>,索引顺序就是yaml中设置的顺序

  • std::vector<geometry_msgs::Point>getUnpaddedRobotFootprint() 获取机器人边界(在机器人坐标系下,不包含padding)

  • geometry_msgs::Polygon getRobotFootprintPolygon() 获取机器人边界(在机器人坐标系下,包含padding)


LayeredCostmap类

LayeredCostmap类是Costmap2DROS的类成员,它是主地图,也能够管理各层地图,因为它含有指向各层子地图的指针,能够调用子地图的类方法,开启子地图的更新。 每一层在Costmap2DROS中以插件方式被实例化,并被添加到LayeredCostmap。每一层可以独立编译,且可使用C++接口对代价地图随意修改,即LayerdCostmapCostmap2DROS(用户接口)提供了加载地图层的插件机制,每个插件(即地图层)都是Layer类型的

LayeredCostmap的所有函数其实都是通过Costmap2D和各个Layer的函数执行的。

Layer调用的函数: isCurrent, onFootprintChanged, updateBounds, matchSize

Costmap2D调用的函数: updateOrigin, resizeMap, setDefaultValue

  • getCostmap()也可以获得costmap_2d::Costmap2D *costmap指针

  • getInscribedRadius() 获得内切圆半径

  • getCircumscribedRadius() 获得外接圆半径

  • 地图有效性 current

对于各层,都有current_参数,该参数继承自Layer类,且通过Layer::isCurrent查询。总的地图的current是通过各层current的与操作计算出来的, 可看LayeredCostmap::isCurrent(),主要的操作是对操作的实时性提供保证,提供是否发生超时的信息。

对于静态层,只要初始化(onInitialize)完成后current_就一直为true

对于障碍物层,其current_参数是各个观察缓冲区ObservationBuffer是否current的与操作,而ObservationBuffer的current取决于缓冲区更新时间(新的观测数据到来的时间)与expected_update_rate参数的对比。

对于膨胀层,也是只要初始化完成后current_就一直为true


Costmap2D类的 setConvexPolygonCost 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
bool Costmap2D::setConvexPolygonCost(const std::vector<geometry_msgs::Point>& polygon, unsigned char cost_value)
{
// we assume the polygon is given in the global_frame... we need to transform it to map coordinates
std::vector<MapLocation> map_polygon;
for (unsigned int i = 0; i < polygon.size(); ++i)
{
MapLocation loc;
// 先将世界系下的多边形顶点转换到地图坐标系,并存放进map_polygon数组
if (!worldToMap(polygon[i].x, polygon[i].y, loc.x, loc.y))
{
// ("Polygon lies outside map bounds, so we can't fill it");
return false;
}
map_polygon.push_back(loc);
}
std::vector<MapLocation> polygon_cells;

// 通过机器人顶点坐标数组map_polygon得到多边形边缘及内部的全部cell,存放在 polygon_cells
// get the cells that fill the polygon
convexFillCells(map_polygon, polygon_cells);

// set the cost of those cells
// 通过循环对多边形边缘及内部各cell的cost赋值
for (unsigned int i = 0; i < polygon_cells.size(); ++i)
{
unsigned int index = getIndex(polygon_cells[i].x, polygon_cells[i].y);
costmap_[index] = cost_value;
}
return true;
}

convexFillCells

这个函数用于获取多边形边缘及内部cell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
void Costmap2D::convexFillCells(const std::vector<MapLocation>& polygon, std::vector<MapLocation>& polygon_cells)
{
// we need a minimum polygon of a triangle
if (polygon.size() < 3)
return;

// first get the cells that make up the outline of the polygon
//首先获得轮廓点之间连线的列表,存放在polygon_cells中
polygonOutlineCells(polygon, polygon_cells);

// quick bubble sort to sort points by x
MapLocation swap;
unsigned int i = 0;
// 对边上的cell点的x做排序,使其按x坐标升序排列
while (i < polygon_cells.size() - 1)
{
if (polygon_cells[i].x > polygon_cells[i + 1].x)
{
swap = polygon_cells[i];
polygon_cells[i] = polygon_cells[i + 1];
polygon_cells[i + 1] = swap;

if (i > 0)
--i;
}
else
++i;
}
/* 遍历所有x,对每个相同的x,检查y,获得y最大和最小的polygon cell,
将范围内的所有cell填充进polygon_cells,获得多边形边缘及内部的所有cell */
i = 0;
MapLocation min_pt;
MapLocation max_pt;
unsigned int min_x = polygon_cells[0].x;
unsigned int max_x = polygon_cells[polygon_cells.size() - 1].x;

// walk through each column and mark cells inside the polygon
for (unsigned int x = min_x; x <= max_x; ++x)
{
if (i >= polygon_cells.size() - 1)
break;

if (polygon_cells[i].y < polygon_cells[i + 1].y)
{
min_pt = polygon_cells[i];
max_pt = polygon_cells[i + 1];
}
else
{
min_pt = polygon_cells[i + 1];
max_pt = polygon_cells[i];
}

i += 2;
while (i < polygon_cells.size() && polygon_cells[i].x == x)
{
if (polygon_cells[i].y < min_pt.y)
min_pt = polygon_cells[i];
else if (polygon_cells[i].y > max_pt.y)
max_pt = polygon_cells[i];
++i;
}

MapLocation pt;
// loop though cells in the column
for (unsigned int y = min_pt.y; y < max_pt.y; ++y)
{
pt.x = x;
pt.y = y;
polygon_cells.push_back(pt);
}
}
}

polygonOutlineCells

循环调用raytraceLine函数,不断获取相邻之间的连线,最终组成多边形边上的cell,需要注意的是需要将最后一点和第一点连接起来,形成闭合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void Costmap2D::polygonOutlineCells(const std::vector<MapLocation>& polygon, std::vector<MapLocation>& polygon_cells)
{
PolygonOutlineCells cell_gatherer(*this, costmap_, polygon_cells);
for (unsigned int i = 0; i < polygon.size() - 1; ++i)
{
raytraceLine(cell_gatherer, polygon[i].x, polygon[i].y, polygon[i + 1].x, polygon[i + 1].y);
}
if (!polygon.empty())
{
unsigned int last_index = polygon.size() - 1;
// we also need to close the polygon by going from the last point to the first
raytraceLine(cell_gatherer, polygon[last_index].x, polygon[last_index].y, polygon[0].x, polygon[0].y);
}
}

raytraceLine

找到两点连线上的cell。对于离散的平面点,指定两个点,这个函数可以找到两个点之间的其他点,使得这些中间组成一个尽可能趋近直线的点集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
template<class ActionType>  inline void raytraceLine(ActionType at, 
unsigned int x0, unsigned int y0,
unsigned int x1, unsigned int y1,
unsigned int max_length = UINT_MAX )
{
int dx = x1 - x0;
int dy = y1 - y0;

unsigned int abs_dx = abs(dx);
unsigned int abs_dy = abs(dy);

int offset_dx = sign(dx);
int offset_dy = sign(dy) * size_x_;

unsigned int offset = y0 * size_x_ + x0;

// we need to chose how much to scale our dominant dimension, based on the maximum length of the line
double dist = hypot(dx, dy);
double scale = (dist == 0.0) ? 1.0 : std::min(1.0, max_length / dist);

// if x is dominant
if (abs_dx >= abs_dy)
{
int error_y = abs_dx / 2;
bresenham2D(at, abs_dx, abs_dy, error_y, offset_dx, offset_dy, offset, (unsigned int)(scale * abs_dx));
return;
}
// otherwise y is dominant
int error_x = abs_dy / 2;
bresenham2D(at, abs_dy, abs_dx, error_x, offset_dy, offset_dx, offset, (unsigned int)(scale * abs_dy));
}


Gazebo行人作为动态障碍

Gazebo自带的一些模型,比如人行走和奔跑的模型,在/usr/share/gazebo-11/media/modelsGazebo的行人模型不是障碍,机器人开过去撞不上,Rviz里也不会作为障碍,只能找插件让它变成障碍。

Gazebo模型碰撞的插件actor_collisions,按照说明进行编译后,如果执行gazebo ../actor_collisions.world正常,那么说明安装成功了。然后将libActorCollisionsPlugin.so文件复制到/opt/ros/noetic/lib

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<actor name="actor">
<!-- 插件 -->
<plugin name="actor_collisions_plugin" filename="libActorCollisionsPlugin.so">
<!-- 行人全身关节,不需修改 -->
<scaling collision="LHipJoint_LeftUpLeg_collision" scale="
0.01
0.001
0.001
"/>
......
<scaling collision="RightFingerBase_RightHandIndex1_collision" scale="
4.0
4.0
3.0
"/>
</plugin>
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 行人的初始位姿 -->
<pose>3 -3 0.9 1.57 0 0</pose>
<!-- 使用哪个模型 -->
<skin>
<filename>walk.dae</filename>
<scale>1.0</scale>
</skin>

<animation name="walking">
<filename>walk.dae</filename>
<scale>1.000000</scale>
<interpolate_x>true</interpolate_x>
</animation>

循环行走的逻辑,使用script,手动确定每个时间点的位姿,尽量保证循环行走的平滑性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script>
<loop>true</loop>
<delay_start>0.000000</delay_start>
<auto_start>true</auto_start>

<trajectory id="0" type="walking">
<waypoint>
<time>0.000000</time>
<pose>0.000000 1.000000 0.000000 0.000000 0.000000 0.000000</pose>
</waypoint>
<waypoint>
<time>0.500000</time>
<pose>0.195090 0.980785 0.000000 0.000000 0.000000 -0.196350</pose>
</waypoint>
......
<waypoint>
<time>18.000000</time>
<pose>-0.195090 0.980785 0.000000 0.000000 0.000000 0.196345</pose>
</waypoint>
</trajectory>
</script>

插件的缺点:

  1. 插件的collision参数难以准确设置
  2. 行走的时间和位姿难以保证平滑性


第1个缺点不好解决,不同的模型需要单独设置参数。针对第2个缺点,我发现了插件gazebo_ros_actor_plugin,这个插件可以对行人进行速度控制。

两种模式(follow_mode): velocity模式,使用teleop_twist_keyboard进行手动控制; path模式,按照path_publisher.py指定路径行走。

对于可控制actor移动的模式,world文件加入下面内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<actor name="actor">
<pose>3 -3 0.9 1.57 0 0</pose>

<skin>
<filename>walk.dae</filename>
<scale>1.0</scale>
</skin>

<animation name="walking">
<filename>walk.dae</filename>
<scale>1.000000</scale>
<interpolate_x>true</interpolate_x>
</animation>

<plugin name="actor_plugin" filename="libgazebo_ros_actor_command.so">
<!-- <follow_mode>path</follow_mode> -->
<follow_mode>velocity</follow_mode>
<vel_topic>/gazebo_cmd_vel</vel_topic>
<path_topic>/cmd_path</path_topic>
<animation_factor>4.0</animation_factor>
<linear_tolerance>0.1</linear_tolerance>
<linear_velocity>1</linear_velocity>
<angular_tolerance>0.0872</angular_tolerance>
<angular_velocity>2.5</angular_velocity>
<default_rotation>1.57</default_rotation>
</plugin>
</actor>

但问题在于,这个插件的行人和默认的一样,不是障碍,如果能将两个插件结合就好了。

但是同时加载两个插件后,Gazebo里的行人不能正常显示,要么是频繁在站立和躺着之间切换,要么就是原地走,但没有往前动。如果旋转,行人没反应。显然是两个插件冲突了。

修改

将第1个插件的Load函数加入第2个插件,再把SetWorldPose函数的后两个参数从false改为true,即 this->actor_->SetWorldPose(pose, true, true);,编译之后把生成的libgazebo_actor_cmd_collision.so放入/usr/lib/x86_64-linux-gnu/gazebo-11/plugins

这样把两个插件合并,可以让actor作为障碍被用户控制。

如果配置Gazebo有多个actor,可以都设置为手动控制模式,只要话题名不同,也能分别控制。

缺点: 不同actor不会相撞,遇到时会穿过。

也可以设置有的actor为手动控制模式,有的为路径模式。
最终效果.gif


tf常用函数和geometry_msgs的互相转换

来自transform_datatypes.h,这个头文件真是太有用了

1
2
typedef tf::Vector3 	tf::Point
typedef tf::Transform tf::Pose

  • static tf::Quaternion tf::createQuaternionFromRPY (double roll, double pitch, double yaw)

  • static geometry_msgs::Quaternion tf::createQuaternionMsgFromRollPitchYaw (double roll, double pitch, double yaw)

  • static geometry_msgs::Quaternion tf::createQuaternionMsgFromYaw (double yaw)

  • static double tf::getYaw (const geometry_msgs::Quaternion &orientation)

  • static void tf::transformTFToMsg (const Transform &bt, geometry_msgs::Transform &msg)

  • static void tf::transformStampedMsgToTF (const geometry_msgs::TransformStamped &msg, StampedTransform &bt)

  • static void tf::transformMsgToTF (const geometry_msgs::Transform &msg, Transform &bt)

  • static void tf::quaternionTFToMsg (const Quaternion &bt, geometry_msgs::Quaternion &msg)

  • static void tf::poseTFToMsg (const Pose &bt, geometry_msgs::Pose &msg)

  • static void tf::poseStampedMsgToTF (const geometry_msgs::PoseStamped &msg, Stamped< Pose > &bt)

1
2
3
4
5
6
#include "tf2_geometry_msgs/tf2_geometry_msgs.h"

tf2::Quaternion quat_tf;
quat_tf.setRPY(1.0, 0.0, 0.0);
geometry_msgs::Quaternion quat_msg;
tf2::convert(quat_tf, quat_msg);
1
2
3
4
5
geometry_msgs::PoseStamped  p;
// 对 p 赋值
// ......
tf2::Quaternion q;
tf2::convert(p.pose.orientation, q);
  • geometry_msgs::TransformStampedtf::StampedTransform, tf2::Transform
1
2
3
4
5
6
geometry_msgs::TransformStamped   transform;
tf::StampedTransform tfTransform;
transformStampedTFToMsg(tfTransform , transform);

tf2::Transform tf2_transform;
tf2::convert(transform.transform, tf2_transform);

tf::StampedTransformgeometry_msgs/TransformStamped的区别:前者是C++中的一个类,只能用于C++中,而后者则是ROS的消息,只依赖于ROS,与语言无关,都可以使用。


costmap和OpenCV的坐标关系

costmap和OpenCV的坐标系关系
costmap原点在左下角,OpenCV图片的原点在左上。

cv::Mat map_mat = cv::Mat(m_y, m_x, CV_8U);type经常设置为CV_8U,无符号整数 0~255,Mat代表矩阵,该类声明在头文件opencv2/core/core.hpp。对(行,列)元素访问、赋值常使用m.at<类型>(行,列);

OpenCV的黑白图像是二维数组,255是白色,0是黑色. 数组中每个元素就是图像对应位置的像素值,数组索引、像素行列、像素坐标关系如下:

  • 数组行索引 = 像素所在行数 - 1 = 像素纵坐标
  • 数组列索引 = 像素所在列数 - 1 = 像素横坐标

在ROS中的地图像素扫描方向是从左下角向右上角扫描的。resolution表示地图上一个像素宽度代表实际的距离(通常为0.05,代表一个像素代表5cm*5cm),costmap的本质是栅格地图,其示意图如下:

OpenCV的图相当于costmap先逆时针转180°,再对y轴取对称。

1
2
3
4
5
6
7
8
9
10
11
// pgm的像素值,或者说保存方式和OpenCV是一样的
costmap_->saveMap("/home/user/costmap.pgm");

// 范围 0~255
cv::Mat map_mat = cv::Mat(m_y, m_x, CV_8U);

for (int x = 0; x < map_mat.cols; x++)
for (int y = 0; y < map_mat.rows; y++)
{
map_mat.at<uint8_t>(map_mat.rows -1 - y, x) = 255 - costmap_->getCharMap()[ y * map_mat.cols + x];
}


Action通信(二) MoveBase客户端和服务端的常用代码

MoveBase.action

1
2
3
4
geometry_msgs/PoseStamped target_pose
---
---
geometry_msgs/PoseStamped base_position

target_pose是目标位姿。base_position作为feedback,是base_link在world坐标系的位姿。


MoveBase的Action实际就是另一种形式的话题,会生成7个msg文件:

1
2
3
4
5
6
7
MoveBaseActionFeedback.msg  
MoveBaseActionResult.msg
MoveBaseResult.msg
MoveBaseActionGoal.msg
MoveBaseFeedback.msg
MoveBaseAction.msg
MoveBaseGoal.msg

另外有个RecoveryStatus.msg,目前没有使用过。

1
2
3
4
geometry_msgs/PoseStamped pose_stamped
uint16 current_recovery_number
uint16 total_number_of_recoveries
string recovery_behavior_name

客户端

1
2
template<class ActionSpec>
bool actionlib::SimpleActionClient< ActionSpec >::waitForServer (const ros::Duration & timeout = ros::Duration(0,0) ) [inline]

一般花点时间,比如几百毫秒实现二者的连接,在此时间内的goal会发送失败。

参数:

  • timeout Max time to block before returning. 参数为 0 is interpreted as an infinite timeout.
  • 返回 True if the server connected in the allocated time. False on timeout

所以这样写代码比较优雅:

1
2
3
4
5
while( ! ac->isServerConnected() )
{
ROS_INFO("Waiting for move_base action server to start");
ac->waitForServer(ros::Duration(1) );
}

发送goal的常用程序

1
2
3
4
5
6
7
8
9
10
11
12
13
ac = new actionlib::SimpleActionClient<move_base_msgs::MoveBaseAction>("move_base", true);
// 注意是 MoveBaseGoal, 成员只有 geometry_msgs/PoseStamped target_pose
move_base_msgs::MoveBaseGoal goal;
// 一系列赋值 ......
ac->sendGoal(goal);
// ac->sendGoal(goal, &doneCb, &activeCb, &feedbackCb);

bool finished_before_timeout = ac->waitForResult(ros::Duration(30.0));
if (finished_before_timeout)
{
actionlib::SimpleClientGoalState state = ac->getState();
ROS_INFO("MoveBase Action finished: %s", state.toString().c_str());
}

三个回调函数 :当action完成后,调用回调函数doneCb一次。 当action激活后,调用回调函数activeCb一次,此时还没有获得路径。 收到feedback后,不断调用回调函数feedbackCb,一直到action结束

  • bool waitForResult (const ros::Duration &timeout = ros::Duration(0, 0))

Blocks until this goal finishes. 可以指定阻塞的时间,如果是0,则无线阻塞。

  • SimpleClientGoalState getState() const

Get the state information for this goal. Possible States Are: PENDING, ACTIVE, RECALLED, REJECTED, PREEMPTED, ABORTED, SUCCEEDED, LOST

如果在服务端的回调函数里没有对goal的状态进行设置,会有下面报警

Your executeCallback did not set the goal to a terminal status. This is a bug in your ActionServer implementation. Fix your code!
For now, the ActionServer will set this goal to aborted
因为没有设置goal的最终状态,比如使用setSucceededsetAborted设置状态.

  • cancelGoal

cancelGoal()之后,getState()的状态还是ACTIVE

如果没有goal处于ACTIVE,不要使用cancelGoal,会报错但是不会让进程退出:

两个回调函数

actionlib::SimpleActionServer的成员函数

1
2
3
4
5
// Allows users to register a callback to be invoked when a new goal is available.
void registerGoalCallback (boost::function< void()> cb)

// Allows users to register a callback to be invoked when a new preempt request is available.
void registerPreemptCallback (boost::function< void()> cb)

客户端向SimpleActionServer发送新目标时,isNewGoalAvailable()为true,isPreemptRequested()也为true。 如果客户端调用cancelGoal()isNewGoalAvailable()为false,但isPreemptRequested()还是true.

问题

有时候我在move_base设置了setAborted之后,会出现报警 To transition to an aborted state, the goal must be in a preempting or active state, it is currently in state: 4 。 这是因为多次调用了 setAborted,state 4 其实就是状态 Aborted

此时如果再发送goal,就会出现报警
Your executeCallback did not set the goal to a terminalstatus. This is a bug in your ActionServer implementation. Fix your code! For now, the ActionServer will set this goal to aborted
,这打乱了 Action 的状态机,必须避免这个报警。


我使用了一段时间后,感觉缺resetGoalgetGoal函数,不太方便。

参考:
ROS Action动作通讯编程C++
Writing a Callback Based SimpleActionClientTutorials(2f)Writing(20)a(20)Callback(20)Based(20)Simple(20)Action(20)Client.html)


使用GDB解决算法切换导致的多线程问题 (一)
abstract Welcome to my blog, enter password to read.
Read more
Gazebo使用心得

Gazebo不好上手,费了一番功夫终于熟悉了,内容太多,只记录注意事项。

Gazebo里用的是 前x 左y 上z的右手坐标系 ,gazebo的栅格范围是20mX20m

启动simple_world.launch,然后在Gazebo里添加一个圆柱体,保存覆盖simple.world文件。再次启动时,报错 SpawnModel: Failure - model name sunny already exist. [Spawn service failed. Exiting. [spawn_urdf-6] process has died

网上说的方法没能解决,也不能重命名model,修改simple.world无效。 最后删除了<node pkg="gazebo_ros" type="spawn_model" name="spawn_urdf" args="-urdf -model sunny -x $(arg x_pos) -y $(arg y_pos) -z $(arg z_pos) -param robot_description" /> 解决问题。

urdf 和 xacro

urdf.xacro文件里,<link name="base_link">代表机器人的主体。<geometry> <box size="0.4 0.39 0.5" />是尺寸。

gazebo插件,可以简单理解为就是连接gazebo和ros的一个桥梁,一边插件通过gazebo的api来控制仿真环境的机器人关节等数据,一边通过关节数据通过运动学模型生成里程计、雷达的scan、image等类似的信息

solidworks生成urdf

solidworks to urdf安装后,如果在solidworks里找不到,就安装最新版的,不用管版本是否对应。

solidworks只生成的URDF文件,没有xacro文件. URDF文件做一些简单修改可保存为xacro,注意:

  1. jointtype="fixed",应当为continuous
  2. joint的某些参数值错误,导致车加载差速模型后,运动错误

xacro

xacro里面的模型仍然是urdf模型,有如下变化:

  1. 创建宏定义
  2. 文件包含
  3. 提供可编程接口:常量, 变量, 数学计算, 条件语句

启动Gazebo时,机器人模型的tf关系不完整,即使xacro文件正确。还需要添加joint_state_publisherrobot_state_publisher

设置雷达时,看情况决定是否需要可视化

1
2
3
<sensor type="ray" name="lds_lfcd_sensor">
<pose>0 0 0 0 0 0</pose>
<visualize>false</visualize>

Gazebo默认的障碍物质量轻,车撞上之后会把障碍物推开。改变density选项为铁,则不会推动障碍物。


在Gazebo world中用以下方式加载模型时:

1
2
3
<include>
<uri>model://one_building</uri>
</include>

要让Gazebo能够找到uri中的自建model,需要将自己的model路径加入环境变量, export GAZEBO_MODEL_PATH=~/<path>/my_package/models:${GAZEBO_MODEL_PATH}