代价地图(一) 基础配置

地图

代价地图的参数定义在三个文件里,分别用于全局代价地图,局部代价地图,通用代价地图(用于两个代价地图的参数)

movebase建立了两个costmap,其中`planner_costmap_ros是用于全局导航的地图,controllercostmap_ros`是用于局部导航用的地图。

costmap原点在左下角,OpenCV图片的原点在左上

通用代价地图

参数文件common_costmap_parameters.yaml,这些参数会影响两个代价地图:

1
2
3
4
5
6
7
8
9
10
11
12
13
footprint: [[-0.24, -0.22], [-0.24, 0.22], [0.24, 0.22], [0.24, -0.22]]
inflation_radius: 0.05 #before: 1.8 0.3 1.2
obstacle_range: 2.5
raytrace_range: 3.0
cost_scaling_factor: 0.18
observation_sources: scan
scan:
data_type: LaserScan
topic: scan
marking: true
clearing: true

map_type: costmap

  • footprint: 机器人底盘的轮廓,由一系列二维数据表示,单位为米,每个表示机器人边界的点,逆时针或顺时针方向都可以,轮廓一般是个矩形,机器人中心相当于[0,0]。本参数其实用于表示内切圆和外接圆,它们以一定方式膨胀障碍物以适应机器人。一般为安全起见,本参数比真实轮廓稍微大一些。

  • robot_radius: 与footprint并列,用于圆形底盘,这个设置就简单多了,就是个半径值,单位米

  • min_obstacle_height: 传感器读数的最小高度(以米为单位)视为有效。通常设置为地面高度。

  • obstacle_range: 设置机器人检测障碍物的最大范围,意思是说超过该范围的障碍物,并不进行检测,只有靠近到该范围内才把该障碍物当作影响路径规划和移动的障碍物

  • raytrace_range: 在机器人移动过程中,实时清除代价地图上的障碍物的最大范围,更新可自由移动的空间数据。假如设置该值为3米,那么就意味着在3米内的障碍物,本来开始时是有的,但是本次检测却没有了,那么就需要在代价地图上来更新,将旧障碍物的空间标记为可以自由移动的空间

全局代价地图

全局地图是从静态地图创建的,使用了后者的数据,只是全局地图将静态地图中的障碍物膨胀化, 依据机器人的足迹半径.

千万注意 pgm 地图文件,边界必须是黑边,而且只给一圈黑色像素不够黑,至少4个,否则不能产生代价地图。

pgm文件.png
生成代价地图.png

相应的参数文件global_costmap_params.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
global_costmap:
global_frame: map
robot_base_frame: base_link # 或者 base_footprint
update_frequency: 2.0 #before: 5.0
publish_frequency: 2.0 #before 0.5
static_map: true
rolling_window: false # 与static_map相反
transform_tolerance: 0.5 # tf变换允许的最大时间

plugins:
- {name: static, type: "costmap_2d::StaticLayer"}
- {name: obstacles, type: "costmap_2d::VoxelLayer"}
- {name: inflation, type: "costmap_2d::InflationLayer"}

plugins表示你要在costmap加载哪几层,static,obstacles,inflation是不可缺少的。 这些层在common_costmap_parameters.yaml中定义,然后添加到全局和局部代价地图的yaml文件里。如果没有定义,就不能添加到后两者。

全局代价地图对应的话题是/move_base/global_costmap/costmap,注意类型是nav_msgs/OccupancyGrid
全局代价地图

加载代价地图后,可以在Rviz里看到基本参数信息

记录一个bug

全局代价地图如果正常生成,理应和地图的大小一样,是2000 x 2000

Costmap2D::worldToMap在刚启动时的打印结果
刚启动move_base

发导航命令后,代价地图的大小很奇怪地变为200, 原点变为(0,0)

1
2
3
4
5
6
7
8
9
10
11
Costmap2DR0S transform timeout. Current time: 1687663324.0445, global_pose stamp 321.6492, tolerance: 0.3000
[1687663324.0756730651]: Could not get robot pose, cancelling reconfiguration
[1687663324.6495552631]: MoveBase received move_base goal (48.000000, 58.000000), type: 3
[1687663324]: wx: 46.032079, wy: 46.095481, origin.x: 0.000000, origin_y_: 0.000000

[1687663324]: mx: 920, my: 921 , size_x_: 200, size_y_: 200, resolution.: 0.050000

[1687663324]: wx: 46.032079, wy: 46.095481, start_x_i: 920, start_y_i: 921
[1687663324]: The robot's start position is off the global costmap. Planning will always fail, you sure the robot has been properly localized ?
[1687663324.]: move_base failed to find a plan to point (48.00, 58.00) !
[1687663324.]: wx: 46.032090, wy: 46.095525, start_x_i: 920, start_y_i: 921

局部代价地图

局部规划器用局部代价地图计算出局部路径,局部代价地图是依据机器人的传感器数据创建的,与静态地图无关,它由用户给出宽度和高度。不管机器人怎么运动,它总在局部代价地图的中心,不再这个动态窗口内的障碍信息会drop

相应的参数文件local_costmap_params.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
local_costmap:
global_frame: odom # 局部代价地图运行的坐标系,也可以是map
robot_base_frame: base_link # 也可以是map
update_frequency: 5.0 # 局部代价地图的更新频率,太大会影响消耗CPU
publish_frequency: 5.0 # 局部代价地图更新的发布频率,如果机器人运动很快,需要增大
static_map: false # 只能是false
rolling_window: true # 只能是true,代价地图在一个rolling window内更新
width: 4 # rolling window的宽度,在rviz显示时会四舍五入
height: 4 # rolling window的高度,在rviz显示时会四舍五入
resolution: 0.02 # rolling window的分辨率
transform_tolerance: 0.5 # tf变换的最大延迟

plugins:
- {name: obstacles_layer, type: "costmap_2d::ObstacleLayer"}
- {name: obstacles, type: "costmap_2d::ObstacleLayer"}
- {name: inflation_layer, type: "costmap_2d::InflationLayer"}

加载局部代价地图成功后

局部代价地图的origin_x_origin_y_(在costmap_2d.cpp中的成员变量)如图解释,显然随着机器人的运动会变化。

局部代价地图有一个更新的过程:

  1. 订阅传感器相关主题,根据接受的传感器数据更新代价地图
  2. 执行标记和清除动作,前者是向代价地图insert障碍信息,后者是从代价地图remove障碍信息。两种动作在障碍层中定义
  3. 计算每个网格的代价值
  4. 对每个包含障碍的cell进行障碍膨胀,从每个占据的cell按一定的膨胀半径向外扩张代价值

两个代价地图的设置中,膨胀层要放到所有层的最后,否则全局路径不是贴着膨胀层,而是障碍层。

Rviz的map包含3个Color Scheme,costmap, map, raw。若要增加主题,只能写一个Rviz插件或者修改Rviz的源码

参考:代价地图全面分析总结


cmake教程(四)ROS中的catkin_make

使用catkin_make编译时,有时编译内容太长,而错误在终端的上面,使用滑条仍有可能看不到。这时可以使用catkin_make > path/file.log将编译内容输出到某个文件,这样就容易查看了。 catkin本身不会存储编译过程的日志

catkin_package()

这个函数十分重要,观察CMakeLists就可以发现,它是唯一的非cmake函数,显然ROS的catkin_make就是靠它了,它的作用是指定功能包构建信息导出,在使用add_library()或者add_executable() 之前,必须调用此函数。

该函数有5个可选参数:

1
2
3
4
5
6
7
8
9
INCLUDE_DIRS  导出包含功能包的路径(即cflags)

LIBRARIES 项目中导出的库

CATKIN_DEPENDS 该项目所依赖的其他catkin项目

DEPENDS 此项目依赖的非catkin CMake项目

CFG_EXTRAS 其他配置选项

比如:
1
2
3
4
5
catkin_package(
INCLUDE_DIRS include
LIBRARIES ${PROJECT_NAME}
CATKIN_DEPENDS roscpp nodelet
DEPENDS eigen opencv)

这表示功能包文件夹中的“include”文件夹是导出头文件所在的位置。roscpp+nodelet是构建/运行此功能包需要存在的功能包,eigen+opencv是构建/运行此功能包时需要存在的系统依赖性。

catkin_package在/opt/ros/kinetic/share/catkin/cmake/catkin_package.cmake文件中定义。develbuild文件夹内的内容基本都是由其生成的。

这个函数解析package.xml文件,提取出里面的参数,执行catkin_make时,会在workspace/devel/share里生成pkgConfig.cmake文件的,在cmake文件中find_package就会用到这些文件,这样其他的对这个软件包有依赖的程序就可以方便的使用了。

比如现在有sub这个包,编译之后会在devel/share/sub/cmake生成两个文件:pubConfig.cmakepubConfig-version.cmake,然后到另一个包pub的CMakeLists里调用sub:find_package(sub),这样编译pub就通过了.假如把那两个文件删掉,然后使用catkin_make --pkg pub编译,就会报错:

对于build文件夹,生成是一些编译中的中间文件,比如用来存储一些环境变量之类的文件。这个文件夹意义不大。

在工程中显示其他源文件

新建CMake工程后,只有main.cpp,向目录添加文件foo.h和foo.cpp,但是它们不会在工程目录里显示,编辑CMakeLists如下

1
2
3
4
5
6
7
8
cmake_minimum_required(VERSION 2.8)

project(testStream)

include_directories(.)

# 或者add_library(Foo "foo.cpp")
add_executable(${PROJECT_NAME} "main.cpp" "foo.cpp")

然后执行cmake后就会显示

报错Policy CMP0046 is not set

Policy CMP0046 is not set
可以不用管,也可以按提示修改,最好先安装:sudo apt-get install libproj-dev

参考:Policy CMP0046问题


Valgrind(一) valgrind的基本使用

Memcheck

默认的工具,用来检测程序中出现的内存问题,所有对内存的读写都会被检测到,一切对malloc、free、new、delete的调用都会被捕获。所以,它能检测以下问题:

  1. 对未初始化内存的使用;

  2. 读/写释放后的内存块;

  3. 读/写超出malloc分配的内存块;

  4. 读/写不适当的栈中内存块;

  5. 内存泄漏,指向一块内存的指针永远丢失;

  6. 不正确的malloc/free或new/delete匹配;

  7. memcpy()相关函数中的dst和src指针重叠

valgrind只报告内存泄漏,但没有显示具体代码中泄漏的地方。因此需要使用--leak-check=full选项启动valgrind。你的程序将比正常的运行慢得多(如20〜30倍),并使用更多的内存。Memcheck会报告关于内存错误,和检测到的泄漏的消息。

Memcheck也报告未初始化值的使用,最常见的消息是Conditional jump or move depends on uninitialised value(s)。可以难以确定这些错误的根源。尝试使用--track-origins=yes来获得额外的信息。这使得Memcheck运行得更慢,

Invalid write of size 1 //堆内存越界被查出来

Invalid free() / delete / delete[] //重复释放

Process terminating with default action of signal 11 (SIGSEGV): dumping core //非法指针,导致coredump

massif

堆栈分析器,指示程序中使用了多少堆内存等信息。它能测量程序在堆栈中使用了多少内存,告诉我们堆块,堆管理块和栈的大小。Massif能帮助我们减少内存的使用,在带有虚拟内存的现代系统中,它还能够加速我们程序的运行,减少程序停留在交换区中的几率。

Massif对内存的分配和释放做profile。程序开发者通过它可以深入了解程序的内存使用行为,从而对内存使用进行优化。这个功能对C++尤其有用,因为C++有很多隐藏的内存分配和释放

memcheck的常用选项

—tool= [default: memcheck]

最常用的选项。运行valgrind中名为toolname的工具。如果省略工具名,默认运行memcheck。

—leak-check= [default: summary]

用于控制内存泄漏检测力度。

  • no,不检测内存泄漏;

  • summary,仅报告总共泄漏的数量,不报告具体泄漏位置;

  • yes/full,报告泄漏总数、泄漏的具体位置

—show-reachable= [default: no]

用于控制是否检测控制范围之外的泄漏,比如全局指针、static指针等。

—undef-value-errors= [default: yes]

用于控制是否检测代码中使用未初始化变量的情况。

—log-file=filename # 将结果输出到文件,支持相对路径

—track-fds= [default: no] # 跟踪打开的文件描述符

五种泄露内存的类型

  • definitely lost:确认丢失。程序中存在内存泄露,应尽快修复。当程序结束时如果一块动态分配的内存没有被释放且通过程序内的指针变量均无法访问这块内存则会报这个错误。
  • indirectly lost:间接丢失。当使用了含有指针成员的类或结构时可能会报这个错误。这类错误无需直接修复,他们总是与”definitely lost”一起出现,只要修复”definitely lost”即可。例子可参考我的例程。

  • possibly lost:可能丢失。大多数情况下应视为与”definitely lost”一样需要尽快修复,除非你的程序让一个指针指向一块动态分配的内存(但不是这块内存起始地址),然后通过运算得到这块内存起始地址,再释放它。例子可参考我的例程。当程序结束时如果一块动态分配的内存没有被释放且通过程序内的指针变量均无法访问这块内存的起始地址,但可以访问其中的某一部分数据,则会报这个错误。

  • still reachable:可以访问,未丢失但也未释放。如果程序是正常结束的,那么它可能不会造成程序崩溃,但长时间运行有可能耗尽系统资源,因此笔者建议修复它。如果程序是崩溃(如访问非法的地址而崩溃)而非正常结束的,则应当暂时忽略它,先修复导致程序崩溃的错误,然后重新检测。

  • suppressed:已被解决。出现了内存泄露但系统自动处理了。可以无视这类错误。这类错误我没能用例程触发,看官方的解释也不太清楚是操作系统处理的还是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

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

ros::NodeHandle的内存泄露

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


string和char*

判断char* 是否为NULL

1
2
3
4
char* foo;
foo=NULL;
if(foo)
printf("foo: %s",foo); // 先判断是否为NULL

比较典型的一个应用是获取环境变量时,如果没有设置环境变量,直接使用变量会报错,所以要判断是否为空:

1
2
3
char* path = getenv("CONFIG");
if(path)
cout<< string(path)<<endl;

使用getenv,不需要delete path释放内存

与字符串比较

C++11 does not allow conversion from string literal to char* 所以最好不要把字符串直接赋值给 char*

1
2
3
const char* path = "abcdef";	// 常用
std::string ss = "abcdef"; // 常用,因为源码的 operator= 定义如此
char* s = "abcdef"; // 避免这样使用

const char*与字符串比较,可以用函数strcmp,若相等则返回0

1
2
if( 0==strcmp(path, "abcdef") )
cout<<"yes"<<endl;

或者先转换成string,再进行比较:

1
2
if(string(path) == ss)
cout<<"yes"<<endl;


ROS中使用GDB和Valgrind调试

我们可以使用GDB和Valgrind对ROS程序进行调试,关键就是roslaunch中的launch-prefix参数,但是这两个工具不能同时在roslaunch中使用

GDB

可以在使用catkin_make时,添加编译类型为Debug:catkin_make -DCMAKE_BUILD_TYPE=Debug --pkg pub,另一种方法是在CMakeLists中添加下列信息,这同CMake是一样的:

1
2
3
SET(CMAKE_BUILD_TYPE "Debug")
SET(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -O0 -Wall -g -ggdb")
SET(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall")

编译后,运行命令要加--prefix参数,格式如下
1
rosrun --prefix 'gdb -ex run --args' [package_name] [node_name] 

然后会进入GDB环境运行节点。

运行结果如下,在代码第20行有一句ROS_BREAK

不过更常用的是roslaunch,除了修改CMakeLists外,还需要修改launch文件,在type之后加一句:launch-prefix="xterm -e gdb --args",也就是添加launch-prefix属性来启动node,但是注意不能通过SSH隧道启动GDB,也就是不能从本机SSH到远程机启动GDB,结果会报错,因为本机要弹出GDB的窗口

1
2
3
4
5
6
7
launch-prefix="xterm -e gdb --args" #在一个单独的xterm窗口运行节点,需要手动输入run来运行

launch-prefix="gdb -ex run --args" : 在launch程序相同的窗口运行GDB,不需要手动输入run

launch-prefix="nice" : 优化进程以减少CPU占用

launch-prefix="screen -d -m gdb --args" : 这个就是专门用于节点在另一个机器上运行,SSH登录到对方那里,使用 screen -D -R 观察GDB过程

剩下的就是GDB的使用了

Valgrind

配置很简单,除了上面修改CMakeLists,把launch中的参数修改如下:

1
launch-prefix="valgrind --leak-check=full --show-reachable=yes --undef-value-errors=yes --track-fds=yes"   		#后面的参数写在同一行

使用launch文件时,不要设置respawn为true,那样会没完没了地执行valgrind。在启动launch之后就会启动valgrind工具检测内存泄露,并执行性能分析,剩下的就是valgrind的使用了。

下面是一个简单的节点,只发布一个std_msgs消息,用valgrind测试的结果:

检查一下这个简单的程序:

1
2
3
4
5
6
7
int main(int argc, char** argv)
{
ros::init(argc,argv,"node");
ros::NodeHandle nh;
ROS_INFO("------ waiting for client's request ------");
return 0;
}

结果竟然有内存泄露:

上网一查,原来真是kinetic的bug,分析在这里,是一个指针没有delete就置为NULL,有人解决之后,在melodic没有了这个bug

nemiver

nemiver是基于GNOME的gdb前端,非常强大,但是速度有点慢。配置:launch-prefix="nemiver"

在UBUNTU下直接使用apt安装: sudo apt-get install nemiver

参考:GDB and Valgrind in ROS


Linux网络相关

系统网卡设置

/etc/network/interfaces内容如下:

1
2
3
4
5
auto lo
iface lo inet loopback

auto enp3s0
iface enp3s0 inet dhcp

自动加载网卡loenp3s0,将lo设置为回环,enp3s0设置为DHCP自动获取IP

ifconfig enp3s0 down可以使对应网卡无效,up可以使之有效。另外:

1
2
ifup 网卡名称    //启动某网卡,可能需要用sudo
ifdown 网卡名称 //禁用某网卡

以服务的形式开关网卡(需要root)

  • service network start
  • service network stop
  • service network restart

rfkill list列出网卡是否被屏蔽的情况
sudo rfkill unblock all 解除所有无线网卡的屏蔽

查看局域网的所有主机的IP

使用nmap对局域网扫描,查看arp缓存表就可以知道局域内主机ip和mac。使用nmap -sP 192.168.1.0/24进行ping扫描,打印出对扫描做出响应的主机,就能获知此网段的所有IP

nmap -sS -O 192.168.0.127 探测某主机的端口和操作系统情况

ifconfig

ifconfig可以看到每个网卡对应的IP地址

  • 第一行:连接类型:Ethernet(封装类型-以太网) HWaddr(硬件mac地址)。
  • 第二行:网卡的IP地址、广播、掩码。
  • 第三行:IPV6不用管
  • 第四行:UP(代表网卡开启状态)BROADCAST(广播) RUNNING(代表网卡的网线被接上)MULTICAST(支持组播)MTU:1500(最大传输单元):1500字节。
  • 第四行:接收的包
  • 第五行:发送的包

查看网关

route -n

查看网卡

台式机一般有两个网卡,用两根网线连接装置。

  • lspci | grep Eth  查看物理网卡

  • 查看两个网卡的连接情况:

lspci | grep -i net

sudo lshw -C network 查看网卡详细情况

命令行连接 wifi

查看可以使用的无线网

1
nmcli dev wifi

出现这样的结果:

或者nmcli device wifi list

连接无线网

1
nmcli dev wifi connect 网络名称 password 密码

iwconfig

用于系统配置无线网络设备,或显示无线网络设备信息。 仅和无线网络相关,用来设置参数等。

Ubuntu安装无线网卡驱动

lspci | grep net找到对应网卡名称,再用lsusb找出对应的ID号,比如

1
Bus 001 Device 009: ID 148f:3070 Ralink Technology, Corp. RT2870/RT3070 Wireless Adapter

就是148f:3070,然后据此上网找驱动。

依据安装Linux Kernel 4.15先升级内核版本到4.15,再按下面步骤进行

  1. Download driver directory from this repo: https://github.com/endlessm/linux/tree/master/drivers/net/wireless/rtl8821ce

You can do it by this link: https://minhaskamal.github.io/DownGit/#/home?url=https://github.com/endlessm/linux/tree/master/drivers/net/wireless/rtl8821ce

  1. Unpack zip archive.
  2. Change the Makefile. Line export TopDIR ?= ... to export TopDIR ?= PATH TO EXTRACTED DIRECTORY. 再根据make的报错信息将报错的那行去掉
  3. make
  4. sudo make install
  5. sudo modprobe -a 8821ce

network-scripts

路径 /etc/sysconfig/network-scripts。文件ifcfg-eth0ifcfg-eth1是两个网卡的信息,如果缺少,说明网卡配置不正确。其中的HWADDR是MAC地址;ONBOOT如果是yes,代表启动时激活;BOOTPROTO是协议类型,一般是NONE;
在修改文件ifcfg-eth0后还需要重新导入文件才能生效: /etc/init.d/network reload,其他命令还有start, stop, restart.

问题

Ubuntu无法无线上网

电脑装了Ubuntu16.04后,系统配置里无线上网的开关都打不开,自己加一个无线网卡也不行。

这个问题困扰了我几天,在网上查了很久,但网上提供的方法都没用,最后严重怀疑是网卡和Ubuntu版本不匹配的问题。最后,我用U盘做了一个Ubuntu系统,与工控机的一样,用try without install Ubuntu的方式启动,发现工控机的Ubuntu版本比我自己的电脑内核版本低。这时插上外置无线网卡,等了一会找到信号了。因此,我重装为旧内核版本的Ubuntu,结果发现插上外置无线网卡后,能发现几个无线信号,但显示是在范围之外,偶尔能连接上一次。

最后决定拆掉笔记本的内置无线网卡。果然,拆掉后插上外置无线立刻就正常了。

也可以不拆网卡,ifconfig enp3s0 down可以使对应网卡无效,up可以使之有效。将自带网卡down掉,然后用外接网卡,最好将auto行删掉

修复双网卡同IP

两个网卡,但网络编辑里只有一个自动,两个网卡会用同一个IP,导致双机通信不正常。
解决:删除后重新添加两个有线网络。分别命名为Eth0、Eth1, 然后用ifconfig查看两个网络的MAC地址,分别填写,然后再编辑IP等参数,恢复正常。

SSH

执行ssh root@192.168.20.232 报错 Unable to negotiate with 192.168.20.232 port 22: no matching host key type found. Their offer: ssh-rsa

解决方法: ssh -o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedKeyTypes=+ssh-rsa root@192.168.20.232 -p 22

配置8812无线网卡

以前保存了编译成功的8812au.ko,但是那是在ubuntu20.04上编译的,放到ubuntu18.04上不能用,估计是内核版本冲突,执行modprobe时报错,最后从头编译安装。

1
2
3
4
5
6
7
git clone https://github.com/gnab/rtl8812au.git
cd rtl8812au
make
sudo make install

sudo modprobe 8812au
sudo lsmod | egrep 8812au

顺利的话,不需要重插无线网卡,应该直接可以连接wifi了。


使用Marker在rviz上标记机器人的轨迹

在rviz中标记机器人的轨迹需要用到类visualization_msgs::Marker,其重要成员如下:

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
//各种标志物类型的定义,也是Marker的枚举值,所有的具体介绍和形状:http://wiki.ros.org/rviz/DisplayTypes/Marker
uint8 ARROW=0//箭头
uint8 CUBE=1//立方体
uint8 SPHERE=2//球
uint8 CYLINDER=3//圆柱体
uint8 LINE_STRIP=4//线条(点的连线)
uint8 LINE_LIST=5//线条序列
uint8 CUBE_LIST=6//立方体序列
uint8 SPHERE_LIST=7//球序列
uint8 POINTS=8//点集
uint8 TEXT_VIEW_FACING=9//显示3D的文字
uint8 MESH_RESOURCE=10//网格?
uint8 TRIANGLE_LIST=11//三角形序列


//对标记的操作,也是枚举值
uint8 ADD=0
uint8 MODIFY=0
uint8 DELETE=2
uint8 DELETEALL=3

// 重要成员变量
Header header
string ns //命名空间namespace
int32 id //与命名空间联合起来,形成唯一的id,这个唯一的id可以将各个标志物区分开来,使得程序可以对指定的标志物进行操作,如果再次执行相同id,会删除上一次的标记
int32 type //类型,指哪种形状
int32 action //操作,是添加还是修改还是删除
geometry_msgs/Pose pose # 位姿
geometry_msgs/Vector3 scale # 默认1,指1米大小。一般要小于1
std_msgs/ColorRGBA color # Color [0.0-1.0],其实是普通颜色值除以255
duration lifetime # 在自动删除前维持多久,0表示永久。注意类型为duration,所以要用ros::Duration(1)初始化
bool frame_locked # If this marker should be frame-locked, i.e. retransformed into its frame every timestep

geometry_msgs/Point[] points//这个是在序列、点集中才会用到,指明序列中每个点的位置

# 数量必须是0或与点数相同,alpha还不能用
std_msgs/ColorRGBA[] colors

#只用于文本标识
string text

string mesh_resource
bool mesh_use_embedded_materials

ns和id变量每次不能同时一样,否则后面的操作会覆盖前面的操作,也就一直只能看到最新的了。建议每次让id+=1或者从命令行输入,

对Marker的pose和scale必须赋值, 否则发布会报警,不能正常显示。

1
2
3
4
5
6
7
8
9
10
11
marker.pose.position.x = 0;
marker.pose.position.y = 0;
marker.pose.position.z = 0;
marker.pose.orientation.x = 0.0;
marker.pose.orientation.y = 0.0;
marker.pose.orientation.z = 0.0;
marker.pose.orientation.w = 1.0;
// 缺一不可,不能有0
marker.scale.x = 0.3;
marker.scale.y = 0.3;
marker.scale.z = 0.3;

如果报警Non-empty points array is ignored.可以忽略

代码

我写了代码如下,在rviz上标记机器人当前位置,形状为球体,id从命令行输入,id为0时清除所有标识

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
74
75
76
77
78
79
80

#include <ros/ros.h>
#include <visualization_msgs/Marker.h>
#include <geometry_msgs/Pose.h>

int main( int argc, char** argv )
{
ros::init(argc, argv, "basic_shapes");
ros::NodeHandle nh;
if(argc !=2)
{
ROS_ERROR("no marker id !");
return -1;
}

visualization_msgs::Marker marker;

ros::Publisher marker_pub = nh.advertise<visualization_msgs::Marker>("visualization_marker", 1);

uint32_t shape = visualization_msgs::Marker::SPHERE;

// 获取当前位姿,阻塞至获取成功
boost::shared_ptr<geometry_msgs::Pose const> edge = ros::topic::waitForMessage<geometry_msgs::Pose>("robot_pose",nh);

if( atoi(argv[1])==0 )
{
marker.action = visualization_msgs::Marker::DELETEALL;
ROS_WARN("All marker in rviz will be removed !");
}
else
marker.action = visualization_msgs::Marker::ADD;

// Set the frame ID and timestamp. See the TF tutorials for information on these.
marker.header.frame_id = "/map";
marker.header.stamp = ros::Time::now();

// Set the namespace and id for this marker. This serves to create a unique ID
// Any marker sent with the same namespace and id will overwrite the old one
marker.ns = "basic_shapes";
marker.id = atoi(argv[1]);

marker.type = shape;

// Set the pose of the marker. This is a full 6DOF pose relative to the frame/time specified in the header
marker.pose.position.x = edge->position.x;
marker.pose.position.y = edge->position.y;
marker.pose.position.z = edge->position.z;
marker.pose.orientation.x = edge->orientation.x;
marker.pose.orientation.y = edge->orientation.y;
marker.pose.orientation.z = edge->orientation.z;
marker.pose.orientation.w = edge->orientation.w;

// Set the scale of the marker -- 1x1x1 here means 1m on a side
marker.scale.x = 0.1;
marker.scale.y = 0.1;
marker.scale.z = 0.1;

// Set the color -- set alpha to something non-zero!
marker.color.r = 0.0f;
marker.color.g = 0.0f;
marker.color.b = 1.0f;
// 不要是0,否则报警 Marker is fully transparent (color.a is 0.0).
marker.color.a = 1.0;
// 不加参数就是0,标识生命期为永久
marker.lifetime = ros::Duration();

// 如果rviz里没有订阅话题visualization_marker,等待2秒
while (marker_pub.getNumSubscribers() < 1)
{
if (!ros::ok())
{
return 0;
}
ROS_WARN_ONCE("Please create a subscriber to the marker");
sleep(2);
}
// 这样比较好,有了订阅者再发布
marker_pub.publish(marker);
return 0;
}

打开rviz,后添加Marker,设置话题:

机器人在导航状态,所以Fixed Frame/map

在终端运行rosrun add_marker marker 1,会添加标识成功

运行rosrun map_server map_saver不会保存标识,因为它和地图无关,只是rviz里的显示,否则就成了地图上的障碍物了。

因为发布了Marker消息,在rviz里订阅后观测到,所以重启rviz后就没有标识了

参考:
官方marker教程
ROS之rviz显示历史运动轨迹、路径的各种方法
github: rviz_visual_tools


激光雷达的配置和ROS消息

配置雷达

我用的雷达是sick LMS511,配置步骤可以参考SICK激光雷达LMS511笔记

连接直流电源24V,将雷达网线连接到无线路由器。准备一个windows电脑,连接到雷达的同一个无线网,开启雷达配送的调试工具Sick sensor intellingence,搜索设备获得雷达,记住IP:192.168.10.100,双击设备图标进入配置页面。

在另一台Ubuntu电脑上进行驱动配置,也是连到同一个无线网,先测试能否ping通,如不能则按上面步骤配置。

使用驱动lms511_laser_node,下载后放到某工作空间中,先在lms5xx_node.cpp中,在ip_add修改雷达的IP,然后使用catkin_make --pkg laser_node编译。

数据表示

激光雷达获取的数据是以以雷达为中心的极坐标系表示, 激光雷达以数据帧的方式返还数据
2020-05-31_105134.png
2020-05-31_105250.png

雷达的消息和参数

消息成员

启动roscore后,用rosrun laser_node laser_node启动驱动程序,会有很多输出信息。
使用命令rosnode list查看运行的节点列表,发现了sicklms5xx

使用rostopic list查看主题,发现了主题scan,信息如下:

其消息类型是sensor_msgs/LaserScan,发布者是sicklidar,其消息成员如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Header header            # 时间戳是收到第一束激光的时间
# in frame frame_id, angles are measured around
# the positive Z axis (counterclockwise, if Z is up)
# with zero angle being forward along the x axis
float32 angle_min # 扫描起始角度[rad]
float32 angle_max # 扫描结束角度[rad]
float32 angle_increment # 角分辨率,相邻扫描点的角度增量,[弧度]
float32 time_increment # 每个数据点的时间间隔 [秒]
float32 scan_time # 当前帧数据与下一帧数据的时间间隔 [秒]
float32 range_min # 最小的扫描距离 [m]
float32 range_max # 最大的扫描距离 [m]
float32[] ranges # 各个方向的扫描数据 [m]
float32[] intensities # 雷达的强度数据,思岚有,有些SICK雷达没有

使用命令rostopic echo scan可查看雷达扫描到的数据:

扫描角度

1
2
angle_min: -1.65806281567
angle_max: 1.65806281567

经过换算,其实就是-95°~95°,而雷达的扫描范围是190°,显然扫描范围是个对称的扇形。雷达角度范围是-1.658~1.658, 角分辨率是0.00872, 这样算出来大概是380, 也就是380个激光束射出。

雷达有旋转方向的问题,大部分雷达都是逆时针旋转,这和ROS中的习惯一致,少数雷达是顺时针,这就会对建图和定位产生问题。比如EAI YDLIDAR G4既可正转也可反转。

判断angle_minangle_max的大小,就能知道雷达扫描的方向,因为雷达的扫描角度都是从angle_min到angle_max变化的。如果angle_min<angle_max,就是逆时针。这样雷达数据从极坐标系转换到笛卡尔右手系时,才能使用公式angle = angle_min + (i * angle_increment)。某些雷达已经在驱动里处理过了,即使顺时针旋转,也可以直接用此公式。

换句话说,angle_min应该叫开始角度angle_max应该叫结束角度

扫描距离

激光传感器在每次扫描都会返回几百个数据,这就是ranges数组。每秒钟还要扫描很多次,这是由scan_time描述,也就是每秒扫出一个扇形(一帧),对每个扫描束都必须处理一个ray-tracing操作.

需要保证range数组中元素的存储顺序与扫描到顺序一致:

  • ranges数组中第一个元素存的scan中第一个扫描到点
  • ranges数组中最后一个元素存的scan中最后一个扫描到的点

如果我们有个rosbag文件,使用rosbag info可以看到这样的结果:
scan消息.png
这里面有360个msg,也就是雷达扫描了360次,注意这不是ranges数组的大小

ranges是个float32类型数组,但不同雷达的ranges大小不同,获得数组大小的方法是 rostopic echo --noarr /scan/ranges,会不断显示<array type: float32, length: 381>,可以看到ranges的大小是381,和激光束的数量基本对应。

A2的量程是0.15-12/18,还是基于白色高反光物体测得。 使用A2时,不能离障碍物太近,否则会出现ranges数组很多个0的情况,这一点在导航时要注意

LMS511最大检测81m,所以说适合室外。这个参数一般是在良好的反射率情况下得到的,对黑色物体的检测量程没这么大。


有时的ranges会出现很多inf和nan的情况,这个对不同雷达的处理可能不同,对于A2,可以看一下源码是这样的:

1
2
if (read_value == 0.0)
scan_msg.ranges[node_count-1-i] = std::numeric_limits<float>::infinity();

也就是读取距离为0时,输出inf

对于北洋雷达,距离超过测量范围时候的两种情况是INF,其余由于强度或其他原因导致的数据错误会输出。

强度

雷达数据强度是反映生成某点的激光雷达脉冲回波强度的一种测量指标(针对每个点而采集)。 雷达强度数据跟激光的反射情况有关,如果射向黑色物体,可能不会有反馈 数组intensities与ranges中的数据是一一对应的,描述的是每个测量数据的强度。

雷达在室外跟室内区别, 基本的点就是光照条件的区别

问题

  • 启动地图后,移动机器人,雷达轨迹跟随移动

先看雷达安装朝上还是朝下,程序中要保持一致。如果没问题,把所有程序关闭再启动,一般会恢复正常

  • 启动雷达,雷达未分配到IP,导致无法使用;运行雷达节点时,突然报错,雷达无法扫描到数据,然后雷达IP丢失

  1. 雷达扫描的结果比实际偏一定角度

偏移的scan.png
正常scan.png
再三试验,应当是rviz视角问题

参考:
Turtlebot3中级教程-应用-自动泊车
EAI的YDLIDAR-X4激光雷达在ROS下使用方法


用freopen函数操作输入输出流和读写文件
此方法非常不好,容易造成输入输出流混乱,最好用fopen,fread,fwrite



freopen函数用于重定向输入输出流,freopen(file, "w",stdout)用于将输出流定向到文件file,freopen(file, "r",stdin)是从文件file读文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
FILE* fp = freopen("/home/user/Documents/test.txt","w",stdout);
srand((int)time(NULL));
for(int i=0;i<8;i++)
{
int num = rand()%100;
string s = boost::lexical_cast<string>(num);
cout << s << endl;
}
// fclose(stdout);
freopen("/dev/tty", "w", stdout);

// 在终端运行会输出,在IDE里不会输出
cout<<"**************"<<endl;

代码先将输出流定向到文件test.txt,后面的cout<<s<<endl会将几个随机数写入到文件。
如果之后直接fclose(stdout),之后的cout输出肯定不管用,此时把输出流关了,对终端和文件都无法输出了。

在使用完freopen之后,如果还需要使用标准输入输出,不能把它们直接fclose,不妨再次重定向,把stdin、stdout重定向到控制台,就能从键盘接受输入、从屏幕输出。在linux中,控制台设备是/dev/tty,所以再次调用freopen。最后的星号只能在终端执行可执行文件时,才会输出。

以上函数对log4cpp的日志和ROS_INFO也有影响,显然log4cpp的本质也是操作输出流,与freopen相同。

读文件

比如文件内容如下:

1
2
3
4
5
6
31
96
5
86
98
20

读其中数字的代码:

1
2
3
4
5
6
7
8
freopen(file, "r", stdin);
vector<int> data(6,0);

for(int i = 0; i < 6; ++i)
{
cin >> data[i];
cout << "data: " << data[i] ;
}

但是文件中有其他内容时,会读取失败,例如:

第4个元素赋值会成为0,值之后的数据都不正常


传递数组给函数

数组不能赋值给数组

1
2
3
int a[3] = {1,2,3};
int b[3];
b=a;

编译器报错, 数组不能直接赋值, 可以使用std::copy()或手工循环赋值, 但是不可以直接把一个数组赋值给另外一个数组.

但是std::array和std::vector是可以的
1
2
std::array<int, 5> a = {1,2,3,4,5};
std::array<int, 5> b = a;


形式参数是一个已定义大小的数组:

1
2
void myFunction(int param[10])
{}

形式参数是一个未定义大小的数组:

1
2
void myFunction(int param[])
{}

函数而言,数组的长度是无关紧要的,因为 C++ 不会对形式参数执行边界检查。

二维数组做形参

1
2
3
4
5
6
7
8
9
10
void func1(int  iArray[][10])
{

}

int main()
{
int array[10][10];
func1(array);
}

形参声明一定要给出第二个维度的大小,否则编译不过。