(二) slam_gmapping节点和所有参数

slam_gmapping节点在gmapping包里面,它的节点信息如下:

它获取sensor_msgs/LaserScan的消息,然后用来建图。/slam_gmapping一边建图一边将地图数据发布到map话题,如果用rviz订阅map话题,我们就能看到建图的过程了.

话题和服务

entropy话题实际是simple_gmapping/entropy,发布机器人姿态分布熵的估计

服务dynamic_map(nav_msgs/GetMap),调用该服务可以获取地图数据,可以用rosservice call试验

需要的tf变换:

  • laser → base_link, 一般是固定的变换,使用static_transform_publisher节点实现
  • base_link → odom, 由里程计系统提供

运行导航程序后,获得tf变换关系:

slam_gmapping的参数

gmapping包生成三个节点,分别为slam_gmappingslam_gmapping_replay及一个测试用的节点。我们主要看slam_gmapping节点,它用到了很多参数,蓝色的是影响CPU性能的

小强目前所用参数只有map_update_interval改为1,粒子数为50,激光评分为140,其他都是默认的

gmapping wrapper参数

  • throttle_scans : [int,默认1] 当接收到n个(默认1个)激光扫描数据,就进行一次更新
  • base_frame: [string, default:”base_link”] 机器人上用的坐标系frame_id
  • map_frame: [string,default:”map”] 机器人地图所用的frame_id
  • odom_frame: [string,default:”odom”] 里程计坐标系的frame_id
  • map_update_interval : [float,default:5.0] 两次更新地图的间隔,降低参数会更新occupancy grid更频繁,但增大CPU占用

Laser Parameters

  • maxRange [double] 雷达扫描的最大有效距离,超过这个数值的扫描会忽略。

  • maxUrange [double,default: maxRange] 用于建图的最大可用扫描距离,因为激光
    雷达的测量距离越远,测量数据的误差越大, 考虑扫地机器人为室内机器人, 因
    maxUrange尽量设置小一些。

  • sigma [float, default: 0.05] The sigma used by the greedy endpoint matching。endpoint匹配标准差,计算score时的方差

  • kernelSize [int,default: 1] The kernel in which to look for a correspondence, search window for the scan matching process. 用于查找对应的kernel size, 主要用在计算score时搜索框的大小

  • lstep [float,default: 0.05] initial search step for scan matching (linear).平移优化步长

  • astep [float,default: 0.05] initial search step for scan matching (angular).旋转优化步长

  • iterations [int, default: 5] 扫描匹配的迭代步长的细分数量,最终距离和角度的匹配精度分别为lstep * 2 ^ (-iterations)astep * 2 ^ (-iterations),这个不是ICP迭代次数,而是用在ScanMatcher::optimize里的搜索步长范围

  • lsigma [float, default: 0.075] The sigma of a beam used for likelihood computation. 用于扫描匹配概率的激光标准差

  • ogain [float, default: 3.0] Gain to be used while evaluating the likelihood, for smoothing the resampling effects. 似然估计为平滑重采样影响使用的gain. 对应函数normalize()中的m_obsSigmaGain,用于粒子权重归一化

  • lskip [int, default: 0]计算似然时,跳过的激光束。0表示所有的激光都处理,如果计算压力过大,可以改成1。源码中的逻辑相当于这样:

1
2
3
4
5
6
7
8
9
10
// 令 lskip 为5, 50为激光束的个数
unsigned int skip=0;
for (unsigned int i=0; i<50; i++)
{
skip++;
skip=skip > 5 ? 0 : skip;
if (skip)
continue;
cout << i << " ";
}

结果为 5 11 17 23 29 35 41 47

  • minimumScore [float, 默认 0.0] 最小匹配得分,一定要自定义,它决定了你对激光的一个置信度,越高说明你对激光匹配算法的要求越高,激光的匹配也越容易失败而转去使用里程计数据,而设的太低又会使地图中出现大量噪声。为获得好的扫描匹配输出结果,用于避免在大空间范围使用有限距离的激光扫描仪(如5m)出现的jumping pose estimates问题。当Scores高达600+,如果出现了该问题可以考虑设定值50。
它取决于激光雷达的测量范围 、角度分辨率、测量精度以及环境特征。

Others:

  • linearUpdate: [float,默认1] 机器人走过多少米,gmapping才处理雷达scan数据

  • angularUpdate: [float,默认0.5] 机器人转过多少弧度,gmapping才处理雷达scan数据

  • temporalUpdate: [float,默认-1] 两次雷达scan数据之前的等待时间,小于0则不启用这个功能

  • resampleThreshold [float,default: 0.5] 粒子重新采样的门槛值,越大表示重新采样越频繁

  • particles : [int,默认30] 决定gmapping算法中的粒子数,每个粒子代表了机器人走过的可能的trajectory,因为gmapping使用的是粒子滤波算法,粒子在不断地迭代更新,所以选取一个合适的粒子数可以让算法在保证比较准确的同时有较高的速度。 看gmapping论文,一般设置为30,高难度地图用80,我一般用50个,大概对应CPU占比25%

运动模型参数 (高斯噪声模型的所有标准偏差)

  • srr [float, default: 0.1] linear noise component (x and y),Odometry error in translation as a function of translation (rho/rho) 线性运动造成的线性误差的方差

  • srt [float, default: 0.2] linear -> angular noise component,Odometry error in translation as a function of rotation (rho/theta). 线性运动造成的角度误差的方差 (rho/theta)

  • str [float, default: 0.1] angular -> linear noise component,Odometry error in rotation as a function of translation (theta/rho). 旋转运动造成的线性误差的方差 (theta/rho)

  • stt [float, default: 0.2] angular noise component (theta),Odometry error in rotation as a function of rotation (theta/theta). 旋转运动造成的角度误差的方差 (theta/theta)

Likelihood sampling (used in scan matching)

  • llsamplerange [float, default: 0.01] Translational sampling range for the likelihood. 用于似然计算的平移采样距离
  • llsamplestep [float, default: 0.005] Translational sampling step for the likelihood. 用于似然计算的平移采样步长
  • lasamplerange [float, default: 0.01] Angular sampling range for the likelihood. 用于似然计算的角度采样距离
  • lasamplestep [float, default: 0.005] Angular sampling step for the likelihood. 用于似然计算的角度采样步长

地图初始维度和分辨率

  • xmin [double] minimum x position in the map [m]
  • ymin [double] minimum y position in the map [m]
  • xmax [double] maximum x position in the map [m]
  • ymax [double] maximum y position in the map [m]
  • delta [double] 地图分辨率,就是话题map_metadata中的resolution
  • occ_thresh (float, default: 0.25),栅格地图栅格值 (i.e., set to 100 in the resultingsensor_msgs/LaserScan).

参考: ROS Wiki-gmapping


vector

vector的底层(存储)机制

vector就是一个动态数组,底层有一个指针指向一片连续的内存空间,当空间不够装下数据时,会自动申请另一片更大的空间(一般是当前容量的2倍),然后把原来的数据拷贝过去,接着释放原来的那片空间;如果元素数量比较大(比如上百个),重新分配内存的耗费会非常大,可能会出错。

当删除里面的数据时,其存储空间不释放,仅仅是清空了里面的数据。

vector中begin和end函数返回的是什么?

begin返回的是第一个元素的迭代器,end返回的是最后一个元素后面位置的迭代器。

为什么vector的插入操作可能会导致迭代器失效?

vector动态增加大小时,并不是在原空间后增加新的空间,而是以原大小的两倍在另外配置一片较大的新空间,然后将内容拷贝过来,并释放原来的空间。由于操作改变了空间,所以迭代器失效。

vector怎么实现动态空间分布?

vector容器基于数组实现,其元素在内存中连续存放,vector容器除了容器尾部之外,在其他任意位置插入或删除元素时,都需要移动该元素后面的所有元素

capacity和size

capacity是容器不用重新分配内存就可容纳的元素个数,容器的大小一旦超过capacity的大小,vector会重新配置内部的存储器,导致和vector元素相关的所有reference、pointers、iterator都会失效。内存的重新配置会很耗时间,
size()是容器中现有的元素个数,max_size()返回Vector所能容纳元素数量的最大值(包括可重新分配内存),一般是一个很大的数

reserve

vector频繁的销毁新建内存会导致效率很低,所以正确的做法是新建vector的时候初始化一个合适的大小,就像使用普通数组一样,这就用到了reserve。 如果reserve的值大于当前的capacity,那么会重新分配内存

reserve是容器预留空间,但并不真正创建元素对象,在创建对象之前,不能引用容器内的元素。否则会报错terminate called after throwing an instance of 'std::out_of_range'

保证vector的capacity最少达到它的参数所指定的大小n

resize

resize跟reserve不同了,它是改变容器的大小,并且可以创建对象,调用这个函数之后,就可以引用容器内的对象了.

1
2
void resize(size_type _Newsize)
void resize(size_type _Newsize, _Ty _Val)

对于第1个版本:

  • 若_Newsize小于oldsize,则剩余元素值不变
  • 若_Newsize大于oldsize,则新添加的元素值用元素的默认构造函数初始化(特别的,int型的将被初始化为0)

对于第2个版本:

  • _Newsize小于oldsize,则剩余元素值不变,全部调用erase(begin() + _Newsize,end())擦除掉多余元素
  • _Newsize大于oldsize,则新添加的元素值用提供的第2个参数初始化。
1
2
3
4
5
6
7
8
9
10
vector<int> vec;
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
vec.push_back(4);
vec.push_back(5);

cout<< vec.size() <<" "<<vec.capacity()<<" "<<endl; // 5 8
vec.resize(10); // 改变大小
cout<< vec.size() <<" "<<vec.capacity()<<" "<<endl; // 10 10

如果_Newsize小于之前的capacity,那么capacity不变。如果大于,那么capacity也要改变.也就是说 resize也涉及了内存重新分配,同时改变了capacity和size

data

返回指向vector中第一个数据的指针或空vector之后的位置

1
2
vector<int> v{1,2,3,4,5};
qDebug()<< *(v.data() ) ; // 1

assign

1
2
3
4
// 先清除vector之前的内容,再将区间[first, last)的元素赋到当前vector
void assign(const_iterator first, const_iterator last);
// 先清除vector之前的内容,再将Count个Val插入到vector
void assign( size_type _Count, const Type& _Val );

使用assign赋值给容器:

1
2
3
4
vector<int> vec;
vec.assign(5,9);

cout<< vec.size() <<" "<<vec.capacity()<<" "<<endl;

容器的capacitysize都是5

push_back

但是如果使用push_back:

1
2
3
4
5
6
7
vector<int> vec;
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
vec.push_back(4);
vec.push_back(5);
cout<< vec.size() <<" "<<vec.capacity()<<" "<<endl;

此时容器的capacity为8

每次执行push_back操作,相当于底层的数组实现要重新分配大小,如果capacity不够插入新元素,需要开辟新的内存空间,把原来的元素复制到新内存,释放原来内存。

频繁的vector扩容会使效率很低,所以 不要使用for循环和push_back给vector赋值,这样会多次重新分配内存;如果先reserve分配需要的内存空间,再push_back会显著提高效率。

vector的几种清空容器办法

1
2
3
4
iterator erase(iterator it); // 删除指定元素,并返回删除元素后一个元素的位置(如果无元素,返回end())
iterator erase(iterator first, iterator last); // 注意:删除元素后,删除点之后的元素对应的迭代器不再有效。

void clear() const; // 函数删除当前vector中的所有元素,相当于调用erase( begin(), end())

释放内存

vector的内存空间是只增加不减少的,erase和clear只删除元素,不能释放内存。如果是C++11环境,直接使用std::vector::shrink_to_fit(),它会将capacity缩减到size大小,所以如果要释放内存,可以先clear,再shrink_to_fit,这跟Qt里的qDeleteAll非常相似

元素是指针时,如何释放内存

如果vector中存放的是指针,而这些指针指向的对象没有销毁,那么当vector销毁时,内存不会被释放。

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
int v[6] = {1,2,3,4,5,6};
std::vector<int*> ps;
int* p1 = v;
int* p2 = v+1;
int* p3 = v+2;
int* p4 = v+3;

ps.push_back( p1 );
ps.push_back( p2 );
ps.push_back( p3 );
ps.push_back( p4 );

// delete ps.back(), ps.pop_back();

for (auto it = ps.begin(); it != ps.end(); it++)
{
if (*it != NULL)
{
delete *it;
*it = NULL;
}
}

ps.clear();
ps.shrink_to_fit();

vector赋值速度

vector所有赋值方式的速度排名:

  1. assign
  2. resize以后用copy算法赋值
  3. 赋值操作符,也就是 destinationVector = sourceVector;
  4. 采用插入迭代器再用copy的方式
  5. insert
  6. push_back

记住最快的是assign,最慢的是push_back

vector遍历元素的速度

一般的速度排名: iterator > [] > at

但是根据编译器和平台的不同,iterator和[]速度有快有慢,但基本上at都是最慢的

尽量避免在vector前部插入元素

一般也没人这么干,任何在vetor前部的插入操作其复杂度都是O(n)的,所以十分低效,如果实在需要在前部插入元素,可以用list,它的效率远远超过vector

这样的代码是valid

1
2
3
using Type = std::vector<int>;
int f = 12;
Type vec={f};

参考:
vector/list的几种赋值方法的速度比较


SVN问题积累

svn的用户名,密码都是明文的,都在~/.subversion/auth/svn.simple下面的文件里.

SVN 无法登入

经查,原因是没有获得权限

某文件处于冲突状态

这说明文件没有处于某个版本,导致其他文件无法成功提交。所以先解决冲突:
svn revert indoor.yaml或者是svn resolve --accept working indoor.yaml,一般用前者

节点冲突


节点冲突和文件冲突不同,这次应该到上级目录执行svn resolve .,然后就可以升级和提交了

找不到节点


原因是这个文件是新创建的,但没有add和commit,所以找不到节点


SVN常用命令

基本操作

  • 更新代码: svn up

  • 提交代码: 先执行add再commit,就是同git类似:svn add file, 之后svn commit -m "LogMessage" file

  • 删除文件:先delete再commit,svn delete test.php 然后再svn ci -m 'delete test file'。这样就解除了SVN对这个文件的管理,但是svn 删除某文件前,应当保证该文件的修改已经提交,否则会删除失败

  • 未提交代码时,取消对代码修改:如果在当前目录下,要撤销某文件的修改:svn revert file.cpp 如果撤销本目录所有文件修改:svn revert -R .,撤销成功会有提示,否则就是失败

  • 查看提交历史:svn log file.cpp,查看目录的历史也是这个,但版本可能是不连续的,因为某些版本只改一个文件,文件夹的版本没有改变

  • 查看工作目录的SVN信息:Win或Linux系统下,在工作目录下,打开命令行执行svn info,可用于relocate或update前后确认目录是否更新。

更改SVN默认编辑器

一般在Linux上,SVN用的是nano编辑器,非常不方便,修改使用命令svn update-alternatives --config editor

星号标出了当前使用的编辑器,选择4就改成了VIM

还原到某个版本

先用svn log查看文件的提交历史,找到需要的版本,比如我们打算把test.cpp从966版本退回到955

目前有三个版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
 svn log test.cpp 
-------------------------------
r966 | user | 2019-03-25 09:31:29 +0800 (一, 2019-03-25) | 1

3
-------------------------------
r965 | user | 2019-03-25 09:31:16 +0800 (一, 2019-03-25) | 1

2
-------------------------------
r950 | user | 2019-03-22 11:19:08 +0800 (五, 2019-03-22) | 1

------------------------------------------------------------------------

先看一个常见的 错误用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
svn up test.cpp -r 950
#正在升级 'test.cpp':
U test.cpp
#更新到版本 950。

cat test.cpp // 950版本的内容
#111111111

// 这样返回到旧版本,但修改后再提交会失败
vim test.cpp // 做修改
svn ci test.cpp -m "4" //再提交
# ---------结果会报错---------
# 正在发送 test.cpp
# 传输文件数据 .svn: E155011: 提交失败(细节如下):
# svn: E155011: 文件“/home/user/work/workspace/src/test.cpp” 已经过时
# svn: E170004: File '/workspace/src/test.cpp' is out of date

正确做法

  1. 先更新到最新版本, 版本966
  2. 反向合并到旧版本

    现在内容是950,但版本是966

  3. 现在修改文件再提交,版本就是967了

同样地,反向合并目录下所有改动过的文件: svn merge -r 966:950 .

查看文件和目录的状态

svn status path? 不在svn的控制中;M 内容被修改;C 发生冲突;A 预定加入到版本库;K 被锁定

最好除了?外不要有其他标志

删除分支

右击已经存在的SVN项目->repo browser,弹出框的左边选择需要删除的分支右击->delete

锁定服务器文件

有的特殊文件不适合修改,所以锁定,防止各个客户端的提交修改,使用命令svn lock file

命令行实现relocate

命令:

1
svn sw --relocate Old_Path New_Path

会要求输入用户名和密码

参考:命令行重定位SVN地址

查看目录下所有状态异常的文件

在目录下使用svn status查看即可,如果只看冲突标志,使用 svn status | grep -P '^(?=.{0,6}C)'


C++中的时间类型和函数,高精度计时

time_t在Linux中的类型其实是long int

1
2
3
struct timeval tv;
gettimeofday(&tv,nullptr);
time_t curTime = tv.tv_sec; // 获取当前UTC时间戳,单位为秒

高精度计时

c++11的高精度计时方法,要先配置C++11环境:

1
2
3
4
5
6
7
8
9
10
11
#include <chrono>
using namespace chrono;


auto start = system_clock::now();
/* do something */
auto end = system_clock::now();
auto duration = duration_cast<microseconds>(end - start);
cout << "花费了"
<< double(duration.count()) * microseconds::period::num / microseconds::period::den
<< "秒" << endl;

有时候可能报错:error: ‘chrono’ in namespace ‘std’ does not name a type

解决方法: 改为using namespace std::chrono; in addition to #include <chrono>

或者用steady_clock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <chrono>

using std::chrono::steady_clock;
// 这个类型是 steady_clock::time_point
auto t1 = std::chrono::steady_clock::now();

// your code

auto t2 = std::chrono::steady_clock::now();

//秒
double dr_s=std::chrono::duration<double>(t2-t1).count();
//毫秒
double dr_ms=std::chrono::duration<double,std::milli>(t2-t1).count();
//微秒
double dr_us=std::chrono::duration<double,std::micro>(t2-t1).count();
//纳秒
double dr_us=std::chrono::duration<double,std::nano>(t2-t1).count();

ALOAM中的TicToc类其实就是std::chrono::steady_clock::now()

拥有从毫秒到小时的计时

1
2
3
4
std::chrono::milliseconds 
std::chrono::seconds
std::chrono::minutes
std::chrono::hours

可以用于休眠函数

1
2
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::this_thread::sleep_for(std::chrono::seconds(1));


完全退出ROS的方法

执行rosnode kill -a,这会杀死rosout之外的所有节点,但是这样还差很远。如果有的节点是以python文件启动,它对于的进程并没有杀死。另外rosout是可再生的,必须连roscore也杀掉.

现在执行ps aux | grep ros,会看到剩余的4个进程:

按照它们的pid, 使用kill pid的方式将它们杀死,这样才彻底退出所有ROS进程

但是注意如果是在远程机启动roscore,在本地机是不能终止它的。


Valgrind(二) 几种内存泄露的情况

操作MySQL结果集后释放内存

代码中获取MySQL结果常用由mysql_store_result,结果用valgrind检查时发现了内存泄露问题:

由mysql_store_result()、mysql_use_result()、mysql_list_dbs()获得结果集,在完成对结果集的操作后,必须调用mysql_free_result()释放结果集使用的内存。每次查询返回的结果的地址是不一样的,所以每次都要释放,否则会造成内存泄露。

mysql_free_result 的危害太大,目前造成了很多问题,在将获取的SQL结果插入到容器时,出现了很多乱码和不正常的字符串.如果没有明显内存泄露,不再加这句命令.

mysql_init内存泄露

再次用valgrind检查操作MySQL的代码,发现还有一个泄露的情况:

一般在使用MySQL结束后,会调用mysql_close,但是这样解决不了这个泄露情况,应当调用mysql_library_end()释放 剩余的内存空间。所以MySQL的最后经常是:

1
2
3
mysql_free_result(result);
mysql_close(conn);
mysql_library_end();

如果是在类中使用MySQL,一般是把mysql_closemysql_library_end()放在析构函数里。

跨线程释放内存

我在类中使用了一个指针,打算在析构函数里释放其指向内存,编译运行都正常,但是用valgrind发现了问题:

内存地址在线程1的stack上,看代码发现这个指针的内存确实不是在主线程,所以不要在主线程上释放,否则提示free invalid

boost::thread创建的线程有内存泄露

比如这样创建线程:

1
2
3
4
5
6
7
8
9
10
void call()
{
cout<<"call"<<endl;
}

int main()
{
boost::thread thrd1(&call);
thrd1.join();
}

这种情况下不会有泄露,假如去掉join()一行,虽然还能正常运行,但就会有内存泄露

log4cpp::PropertyConfigurator::configure的内存泄露

ros::NodeHandle的内存泄露

在Kinetic的ros::NodeHandle源码里,一个指针没有delete就置为NULL,目前的melodic没有了这个bug,不必自己解决了。


OccupancyGrid地图详解

栅格地图是将环境栅格化, 然后根据测量值对每个栅格的占有率进行计算, 通过概率计算判断给各栅格可能被障碍物占的几率, 并赋值一个0到1之间的值。通过激光雷达获取的大量数据帮助对每个栅格点进行准确判断。栅格地图中栅格大小决定了地图的精度, 也将影响机器人的定位精度,因此追求更小更多的栅格,但是栅格变小后,其数据量将会显著增加, 因此栅格地图不适合用于大型地图的构建, 适用于室内定位这种小型环境地图构建

地图在ROS中是以Topic的形式维护和呈现的,这个Topic名称就叫做/map,它的消息类型是nav_msgs/OccupancyGrid。由于/map中实际上存储的是一张图片,为了减少不必要的开销,这个Topic往往采用锁存(latched)的方式来发布。

锁存器的作用就是,将发布者最后一次发布的消息保存下来,然后把它自动发送给后来的订阅者。这种方式非常适合变动较慢、相对固定的数据(例如地图),然后只发布一次,相比于同样的消息不定的发布,锁存的方式既可以减少通信中对带宽的占用,也可以减少消息资源维护的开销。

map话题的消息类型是OccupancyGrid,通过rosmsg show nav_msgs/OccupancyGrid来查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
std_msgs/Header header       #消息的报头
uint32 seq
time stamp
string frame_id #地图消息绑定在TF的哪个frame上,一般为map

nav_msgs/MapMetaData info #地图相关信息
time map_load_time #加载时间
float32 resolution #分辨率 单位:m/pixel
uint32 width #宽 单位:pixel
uint32 height #高 单位:pixel
geometry_msgs/Pose origin #原点
geometry_msgs/Point position
float64 x
float64 y
float64 z
geometry_msgs/Quaternion orientation
float64 x
float64 y
float64 z
float64 w
int8[] data #地图具体信息

info是地图的配置信息,它反映了地图的属性。设置地图原点
1
2
3
// 将栅格地图原点设置在整张图的(-20, -20)位置
grid_map.info.origin.x = -20;
grid_map.info.origin.y = -20;

整张地图的障碍物信息存放在data数据成员中,data是一个int8类型的vector,它存储的内容有width * height个int8型的数据,也就是这张地图上每个像素。map中data存储的格式如下

1
2
3
4
0      空白区域
100 障碍物
-1 未知
1-99 根据像素的灰度值转换后的值,代表有障碍物的概率、

左下角像素点在世界坐标系下的位置为(origin_x,origin_y), 单位米。那么世界坐标系下的点(x,y),转换到地图坐标系,那么该点对应的data中的索引index为:
1
index = (int)( (x - origin_x) / resolution) + ((int)((y - origin_y) / resolution)) * width;

该点在地图中的信息即为data[index]

一个宽和高的栅格地图,索引保存方式是这样的:

1
2
3
4
5
6
7
4        8        12     16

3 7 11 15

2 6 10 14

1 5 9 13

1相当于地图的原点,6就对应坐标(1, 1)。那么点坐标(2, 3),对应的栅格索引是x * map.info.width + y = 12

pgm和yaml

运行rosrun map_server map_saver -f fileName可以保存地图,保存成功后,会有文件名对应的pgm和yaml文件。pgm文件就是地图的文件,标准的地图是用inkscape打开时看到的模样。

地图就是一张普通的灰度图像,通常为pgm格式。在地图中,黑色的网格是障碍区域,白色的网格是无障碍区域,灰色的网格是未知区域。map_server从图片中读取信息然后将每个网格都赋值[-1, 100],灰色是-1,代表未知区域;白色是0,空闲区域;黑色是100,障碍区域。

yaml描述地图元数据,内容如下:

1
2
3
4
5
6
7
8
9
image: Software_Museum.pgm      #指定地图文件,可以是相对路径
resolution: 0.020000 #地图的分辨率 单位为 m/pixel
# 地图的原点,即地图左下像素的2D姿态(x,y,yaw),偏航为逆时针旋转,忽略偏航时为0
# 这个坐标体系用于让ros在图片里找到原点而已
origin: [-15.720000, -12.520000, 0.000000]
negate: 0 # 0代表白色空闲,黑色占据。一般不用修改
occupied_thresh: 0.65 # 当占据的概率大于0.65认为被占据
free_thresh: 0.196 # 当占据的概率小于0.196认为无障碍
# 介于 occupied_thresh 和 free_thresh 的栅格unknown

其中占据的概率occ = (255-color_avg) / 255.0, color_avg为RGB三个通道的平均值。

  • occupied_thresh:大于这个阀值的占用率的像素被认为occupied
  • free_thresh:小于这个阀值的占用率的像素被认为是free

如果occupied_thresh设置的比较大,有些障碍物会没有障碍层和膨胀层。
occupied_thresh=0.85.png
如果建图时,障碍的边缘不够明显,也可能出现此问题,此时可以降低occupied_thresh。 其实还可以降低参数cost_scaling_factor,不过对膨胀层的影响较大。

  • 栅格地图的分辨率不必太好,常用0.05
栅格地图的坐标系原点,是在栅格地图左下角,这里的origin是左下角在map坐标系的坐标,不是(0,0,0)

实际举例,先获得机器人的当前姿态:

然后在rviz上测量出机器人与左下角两个方向的距离

用机器人的坐标减去origin的坐标:

  • x = -2.394 - (-15.72) = 13.326
  • y = 2.37 - (-12.52) = 14.89

显然和测量结果大致相同,计算都是在map坐标系下。

map_server和map_saver

map_server是一个和地图相关的功能包,它可以将已知地图发布出来,供导航和其他功能使用,也可以保存SLAM建立的地图。它还提供了map_saver命令行实用程序,它允许动态生成的映射保存到文件。

要让map_server发布/map,需要yaml和pgm两个文件,然后可以通过指令来加载这张地图,map_server相关命令如下:

1
2
rosrun map_server map_server    test.yaml 	加载自定义的地图,通常使用roslaunch
rosrun map_server map_saver -f mymap 保存当前地图为mymap.pgm和mymap.yaml

或者使用roslaunch加载地图:
1
2
<arg name="map_file" default="/home/hlhp/robot_ws/workspace/maps/test.yaml"/>
<node name="map_server" type="map_server" args="$(arg map_file)" pkg="map_server"/>

map_server节点提供的服务:

  • static_map (nav_msgs/GetMap):提供检索地图服务

map_server节点读取pgm文件,通过服务static_map将地图数据提供给其他节点。比如move_base节点从中获取地图数据,用来进行路径规划;amcl节点用地图来进行定位,代码在AmclNode::requestMap()

map_server发布的话题包括:

1
2
/map_metadata    (nav_msgs/MapMetaData): 发布地图的描述信息,通过此锁存话题接收地图元数据。
/map (nav_msgs/OccupancyGrid): 通过此锁存话题接收地图

加载yaml文件后,出现下面的结果:

运行rostopic echo map_metadata的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
map_load_time: 
secs: 1569549222
nsecs: 440982758
resolution: 0.019999999553
width: 1504
height: 2112
origin:
position:
x: -15.72
y: -12.52
z: 0.0
orientation:
x: 0.0
y: 0.0
z: 0.0
w: 1.0

echo一下map话题,结果是一大堆-1和0,这就是上面所说的occupancy grid,-1代表未知,0代表无障碍,100代表障碍。

参数:

  • ~frame_id (string, default: “map”):地图坐标系的名称,要在已发布地图的标题中设置的框架, 绑定发布的地图与tf中的哪个frame,通常就是map

问题 1

有时运行rosrun map_server map_saver,结果显示 Wating for the map, 无法保存成功,进程阻塞了

问题 2

有几次,map_server打开pgm时一直阻塞,只能重启ROS

1
2
3
4
5
map_server::loadMapFromFile(&map_resp_,mapfname.c_str(),res,negate,occ_th,free_th, origin, mode);
ROS_INFO("map_server after loadMapFromFile");

// To make sure get a consistent time in simulation
ros::Time::waitForValid();

经过加日志,发现是 ros::Time::waitForValid();没有执行完,问题出在这里。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bool Time::waitForValid(const ros::WallDuration& timeout)
{
ros::WallTime start = ros::WallTime::now();
while (!isValid() && !g_stopped)
{
ros::WallDuration(0.01).sleep();

if (timeout > ros::WallDuration(0, 0) && (ros::WallTime::now() - start > timeout))
{
return false;
}
}
if (g_stopped)
{
return false;
}
return true;
}


使launch文件开机自启动

robot_upstart

机器人开发中,一般都要求工控机开机后要启动某些节点或launch,ROS已经考虑到了这个需求。专门开发了一个package叫做robot_upstart,它提供脚本来安装和卸载随机启动的节点,但是经过很长时间的研究,发现这个包并不好用,需要额外很多设置。

安装: sudo apt-get install ros-kinetic-robot-upstart,现在有一个package,名称是nav,在它的launch文件夹里有个launch文件叫做all.launch,下面是第一个命令:

1
rosrun robot_upstart install nav/launch/all.launch

这实际是把nav做成了一个service,就跟MySQL服务器一样,然后按照系统自动提示执行:

1
sudo systemctl daemon-reload && sudo systemctl start foo

如果是普通节点,重新开机就能启动all.launch所有节点了,但是我的程序用到了很多自定义的环境变量,结果自启动总是失败。

生成的文件

按上面官方流程执行产生了几个文件,到/etc/ros/kinetic查看是否自启动的包有对应文件夹,里面是设定的launch文件,文件名就是启动名称

跟自启动服务相关的脚本是/usr/sbin/nav-start,停止脚本是同目录的nav-stop,内容比较复杂,也没有分析的必要了。

官方说自启动的日志文件在/var/log/upstart,但是我试了试发现从来都没有,后来看了看nav-start脚本,虽然没有完全看懂,但是在一定条件下会在/tmp下创建日志.不过我看了看日志,在自启动节点失败时,也没有什么有用的信息.

两个service文件其实完全一样,而且只修改一个就可以,/lib/systemd/system/nav.service的内容:

1
2
3
4
5
6
7
8
9
10
11
[Unit]
Description="bringup nav"
After=network.target

[Service]
Type=simple
EnvironmentFile=/etc/robotenv
ExecStart=/usr/sbin/nav-start

[Install]
WantedBy=multi-user.target

这里就是复杂的地方了,如果有自定义的环境变量,需要在执行rosrun robot_upstart install后加到这里,再执行后面的启动服务命令。

我新建了一个文件/etc/robotenv,里面定义环境变量:

1
2
MAP_FILE=/home/user/maps
BAG_FILE=/home/user/bags

不要加export,否则无法生效。然后用EnvironmentFile=/etc/robotenv调用,而且这句应当在ExecStart之前

但是在启动跟串口有关的节点时发现失败了,虽然我之前已经对串口进行了永久使能,但在开机过程中可能需要重新使能,问题在于串口使能前先启动了节点,结果失败。所以在rc.local中添加:

1
2
chmod 777 /dev/ttyS0
chmod 777 /dev/ttyS1

停止开机启动:rosrun robot_upstart uninstall nav,会删除除了/tmp里几个文件的其他所有文件
robot_upstart uninstall的注意使用包名

如果之前设置开机启动失败了,必须先用 rosrun robot_upstart uninstall 解除之前的设置,然后重新设置 ,如果是别人设置的开机启动,这个名称nav可以去到`/etc/ros/kinetic`里寻找 设置成功后,修改launch文件或更新节点文件不影响开机启动的设置

问题

在小强机器人上对startup.launch开机启动,在另一台电脑上执行rosnode listrostopic list都是正常的,但rostopic echo却没有结果,rosnode ping也不行。把所有ROS关闭,重启startup.launch,又正常了。

rc.local,只用于无界面节点

如果启动的节点比较简单,可以用rc.local的方式,在文件里添加roslaunch命令和环境变量即可,需要注意的是下面的失败的情况

环境变量

第一次开机启动后,节点都启动了,但发现一些节点不能正常运行,后来发现是一些节点中还使用了自定义的环境变量,比如某些目录的地址,所以把这些也要加上:

1
2
3
export CONFIG_PATH=/home/`hostname`/workspace/config
export MAP_FILES_PATH=/home/`hostname`/data
export LAUNCH_PATH=/home/`hostname`/workspace/src

mini-httpd用到的所有环境变量也要加入到rc.local当中,因为开机时加载的环境变量不是bashrc的,而是rc.local

脚本出错

编辑/etc/rc.local进行程序自启动时没有生效。可能是脚本出错,脚本第一行为#!/bin/sh -e,-e标志脚本如果有错误,就不再向下执行。加入下面内容,可以检查错误的原因:

1
2
3
exec 2> /tmp/rc.local.log    # send stderr from rc.local to a log file
exec 1>&2 # send stdout to the same log file
set -x # tell sh to display commands before execution

错误日志都在log文件里,第二行是把一般输出也放了进去,有时会很多,这行可以不加。
打开错误日志发现source not found,不识别source,这是shell的原因。把第一行改为#!/bin/bash,也就是换成bash,最终生效了。

权限和服务

有时按上面步骤还是失败,那么进行下面操作:

1
2
3
4
sudo chmod +x /etc/rc.local
# rc.local被认为是服务,默认是关闭的,需要打开
sudo systemctl enable rc-local.service
重启

完整脚本如下:

1
2
3
4
5
6
7
8
9
10
11
exec 2> /tmp/rc.local.log  # send stderr from rc.local to a log file
#exec 1>&2 # send stdout to the same log file
set -x

source /opt/ros/kinetic/setup.bash
source /home/user/name/workspace/devel/setup.bash
export ROS_IP=`hostname -I`
export ROS_HOSTNAME=`hostname -I`
export ROS_MASTER_URI=http://192.168.73.14:11311
roslaunch /home/user/name/workspace/src/package/launch/start.launch & mini_httpd -C /etc/mini_httpd.conf # 同一行
exit 0

只启动了一个launch文件

rc.local设置开机启动,最好只有一个launch,否则常常只启动一个,我们可以把所有launch放到一个里面.

启动roslaunch失败

rc.local中启动launch文件用的是roslaunch ~/Robot/workspace/package/bringup.launch,结果失败,查看日志发现实际路径是/root/Robot/workspace/package/bring.launch,可见开机时Linux不能把~识别为/home/user

gnome-session-properties,只用于有界面(带显示器)节点

以上方法用于普通ROS程序没问题,但带界面的程序就失灵了。这种情况下先新建一个脚本,比如test.sh:

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
echo -----------准备开机自启动程序 robot-----------
sleep 10
source /opt/ros/kinetic/setup.sh
source /home/user/work/workspace/devel/setup.bash
export ROS_IP=`hostname -I`
export ROS_HOSTNAME=`hostname -I`
export ROS_MASTER_URI=http://192.168.31.14:11311

rosrun robot robot

注意文件要给权限 sleep 10是让脚本阻塞10秒钟,因为程序需要等待与roscore的通信,如果不阻塞,可能会先于roscore启动节点,导致失败而退出,其他就是加载需要的环境和执行程序。

然后在终端输入gnome-session-properties,打开应用程序首选项设置开机启动程序。

命令中填写gnome-terminal -x /path/test.sh。保存后重启,会出现一个终端执行脚本中的命令

这种方法只适用于有界面的情况,假如机器不带显示器,就无法启动成功。 最好每个节点就要对应一个sh文件,逐个添加。因为都放在一起会阻塞在第一个节点,退出以后会接着执行下一个,不能同时执行

参考:
ROS程序开机自启动的四种方法


Action通信机制(一) 概念和原理

尽管在ROS中已经提供了service机制来满足请求—响应式的使用场景, 但是假如某个请求执行时间很长,在此期间用户想查看执行的进度或者取消这个请求的话,service机制就不能满足了,但是action通信可满足用户这种需求。例如,控制机器人运动到地图中某一目标位置,这个过程可能复杂而漫长,执行过程中还可能强制中断或反馈信息,这时需要的就是 actionlib 机制

service是阻塞的,action是非阻塞的

通信机制

ROS的Action机制本质上仍然是一种Topic机制,只是通过actionlib进行了封装。ActionClientActionServer 通过ROS Action Protocol进行通信,本质上还是消息的方式,action protocol就是预定义的一组 ROS message。

在服务器和客户端之间通过goal, cancel, status, feedback, result五个主题实现Action机制。

  • goal: 代表一个任务,可以被ActionClient发送到ActionServer。比如在MoveBase中,它的类型是PoseStamped,包含了机器人运动目标位置的信息。

  • cancel: client发送取消请求到server

  • status: server通知client每个goal的当前状态

  • feedback: 向client发送周期性的goall执行过程中的辅助信息。在Move Base中,它表示机器人当前姿态。

  • result: 等goal执行结束,才向client发送一次性辅助信息

类似msg文件,在程序的action文件(文件名后缀为.action)中定义上述goal、result、feedback等消息,依次用---分隔

action通信时,如果双方约定的action名称为do_dishes,那么客户端会发布话题 /do_dishes/goal/do_dishes/cancel;服务端发布话题/do_dishes/feedback, /do_dishes/result, /do_dishes/status

client要么发送goal,要么cancel; result和feedback的取值由用户决定,status的取值是固定的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
uint8 PENDING   = 0   # The goal has yet to be processed by the action server
uint8 ACTIVE = 1 # The goal is currently being processed by the action server

uint8 PREEMPTED = 2 # The goal received a cancel request after it started executing
# and has since completed its execution (Terminal State)
uint8 SUCCEEDED = 3 # The goal was achieved successfully by the action server (Terminal State)
uint8 ABORTED = 4 # The goal was aborted during execution by the action server due
# to some failure (Terminal State)
uint8 REJECTED = 5 # The goal was rejected by the action server without being processed,
# because the goal was unattainable or invalid (Terminal State)

uint8 PREEMPTING = 6 # The goal received a cancel request after it started executing
# and has not yet completed execution
uint8 RECALLING = 7 # The goal received a cancel request before it started executing,
# but the action server has not yet confirmed that the goal is canceled
uint8 RECALLED = 8 # The goal received a cancel request before it started executing
# and was successfully cancelled (Terminal State)
uint8 LOST = 9 # An action client can determine that a goal is LOST. This should not be
# sent over the wire by an action server

server状态机

Goals请求由ActionClient发出,ActionServer接收后会创建一个有限状态机来跟踪goal的状态,而不是server的状态,每个goal都有一个状态机。

server状态机的命令(actionlib::SimpleActionServer的函数)

  • setAccepted: 收到goal后,准备处理
  • setRejected: 收到goal后,拒绝处理
  • setSucceeded: 标志goal已经成功处理
  • setAborted: 处理goal的过程中发现错误,终止处理
  • setCanceled: server取消对goal的处理,与setRejected不同之处在于发出取消请求的是client

从客户端也可以异步地触发状态变换,比如cancelGoal函数

client状态机

server状态机用于追踪goal的状态,client状态机作为补充,追踪server的状态。一般根据goal的情况分成 PENDING,ACTIVE,DONE三部分。

由于可以有多个client连接到一个Action server,有可能第二个client会取消第一个client发的goal

Goal的状态转变

两张图片表示得太复杂了。一般这样记忆:对于MoveBase的client,最开始的状态是LOST。 当前sendGoal后,状态为ACTIVE. 调用cancelGoal后,变为 PREEMPTED,而不是Aborted。客户端实际的操作goal的函数只有sendcancel

车成功到达目标后,状态为SUCCEEDED。彻底失败,为Aborted

对于多个goal的处理策略

高层的client和server只考虑goal的处理情况,所以叫SimpleActionServerSimpleActionClient

Simple Action Client一次只追踪一个goal,它发出goal之后,会disables之前的goal的所有回调函数,也不再追踪之前goal的状态,但是并不cancel之前的 goal

SimpleActionServer在ActionServer类上实现了单一目标策略,就是在某一时刻只能有一个goal是处于active状态,并且新的goal可以抢占先前的goal:

  • 每次只能有一个goal处于active状态
  • 新的goal抢占以前的goal是根据GoalID中的时间戳
  • 新的goal抢占成功后,旧的goal状态会改变


如图,goal A正在被处理,后面等待的是B,此时client又发了C,那么SimpleActionServer会把C放到队列中,B进入RECALLING状态,然后server又调用setCanceled使B进入RECALLED状态,C成了PENDING

actionlib 为每个 goal 设置了一个状态跟踪器,每个目标有一个独有的id,这是状态发布与取消goal的基础。

actionlib 是通过多个话题的形式实现控制,由于它支持多个客户端同时对服务器发指令,内部必须维护一个list来控制多个目标点的实施。

工具

actionlib包自带了一个客户端的工具,可以用来测试与server端的通信:

1
rosrun actionlib axclient.py /do_dishes    // do_dishes是action名称

参考:

actionlibDetailedDescription
actionlib库的介绍
ROS actionlib学习(三)