自定义msg或srv

类型说明

msg文件存放在package的msg目录下,直接自己创建就可以。msg文件实际上就是每行声明一个数据类型和变量名。可以使用的数据类型如下:

1
2
3
4
5
6
7
int8, int16, int32, int64 (以及uint开头的类型)
float32, float64
string
bool
time, duration
其他msg文件定义的类型 // 例如geometry_msgs/Pose
variable-length array[] and fixed-length array[C]

在ROS中有一个特殊的数据类型:Header,它含有时间戳和坐标系信息。在msg文件的第一行经常可以看到Header header的声明.

比如这样一个消息文件:

1
2
3
4
string name
float32 gridX
float32 gridY
geometry_msgs/Pose worldPos

编译准备

要确保msg文件被转换成为C++,Python和其他语言的源代码: 编辑package下面的package.xml , 确保它包含一下两条语句:

1
2
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>

编辑CMakeLists.txt,利用find_packag函数,增加对message_generation的依赖:

1
find_package(REQUIRED COMPONENTS message_generation)

同时设置运行依赖:

1
2
3
4
catkin_package(
...
CATKIN_DEPENDS message_runtime ...
...)

还要确保添加了msg文件:

1
2
3
4
add_message_files(
FILES
test.msg
)

以及生成消息的依赖,也就是消息文件中的各个类型要依赖的消息库,一般要有std_msgs:
1
2
3
4
5
6
generate_messages(
DEPENDENCIES
nav_msgs
sensor_msgs
std_msgs
)

完成以上步骤之后,进行编译,msg文件都将转换为ROS所支持语言的源代码。生成的C++头文件将会放置在~/catkin_ws/devel/include/packageName/,然后使用rosmsg show packageName/msgName命令测试是否成功 ,其实有头文件就说明成功了。

include/功能包头文件目录:你可以把你的功能包程序包含的*.h头文件放在这里,include下之所以还要加一级路径是为了更好的区分自己定义的头文件和系统标准头文件

专门自定义消息的package

实际中一种消息类型会被多个package使用,这时再一次次定义消息就太麻烦了,多种消息分散在多个package里面,很不方便管理。此时应该创建一个package专门用于定义各类消息。比如定义一个名为myMsgs的package,然后在其msg文件夹中创建各个消息文件,继续按照上面的流程编辑package.xmlCMakeLists,最后编译这个package,才能供其他package使用。

使用

在其他package中调用myMsgs中的消息时,在package.xml中添加以下语句:

1
2
<build_depend>myMsgs</build_depend>
<exec_depend>myMsgs</exec_depend>

然后到CMakeLists中的find_package添加myMsgs不需要进行其他修改

在cpp文件中使用自定义的消息时,需要#include <packageName/msgName.h>。代码中这样使用:

1
2
ros::Publisher pub = n.advertise<packageName::msgName>("topic",1500);
packageName::msgName msg;

无法echo某个话题,报如下错误


此时对应的消息一般是自定义的,如果已经编译,一般执行source devel/setup.bash就能修复,如果不能执行下面操作:

  1. 确保自定义的消息文件加入到了CMakeLists.txt
  2. catkin_make clean
  3. catkin_make
  4. source devel/setup.bash
  5. 运行

ROS安装配置单目摄像头

看本文前,先在Linux上安装cheese,插上摄像头后看能不能正常显示图像,如果不能,就是摄像头有问题,摄像头正常之后再进行下面操作

安装摄像头驱动

先装驱动: sudo apt-get install ros-kinetic-usb-cam,最终把package安装到/opt/ros/kinetic/lib
从Github安装usb_cam驱动
usb_cam包用于和摄像头交互,将图片发布为sensor_msgs::Image主题,使用image_transport库来传递压缩图片。

运行和配置

USB摄像头插到工控机上后,默认是video0,用ls /dev | grep video查看,也就是/dev/video0
在机器人工控机上运行roslaunch usb_cam usb_cam.launch,包含下列参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
~video_device (string, default: "/dev/video0")
~pixel_format (string, default: "mjpeg") //图像格式,还可取mjpeg, yuyv, uyvy
~io_method (string, default: "mmap")  // 还可取mmap, read, userptr
~camera_frame_id (string, default: "head_camera")  // 相机的tf frame

~image_width (integer, default: 640)
~image_height (integer, default: 480)
~framerate (integer, default: 30) // 要求的framerate
~contrast (integer, default: 32) // 图像的对比度(0-255)
~brightness (integer, default: 32) // 图像的亮度(0-255)
~saturation (integer, default: 32) // 图像的饱和度(0-255)
~sharpness (integer, default: 22) // 图像的锐度(0-255)
~autofocus (boolean, default: false) // 自动对焦,默认是false
~focus (integer, default: 51) // 如果关闭自动对焦,相机的焦点,0代表无穷

~camera_info_url (string, default: ) //标定文件的地址,文件将被CameraInfoManager类读取
~camera_name (string, default: head_camera) //相机名称,与相机标定中的名称要一致

注意launch文件中的参数video_device要设置成对应摄像头的接口,比如/dev/video0

奇怪的是,我发现在自己的笔记本上摄像头对应video2video3,但用这两个都没有获得图像,应该是电脑问题。
另外在VMWare的Ubuntu里试了一下,虽然能出现图像,但是卡的要命,估计是虚拟机问题,所以不用考虑虚拟机上配置摄像头了。

摄像头常见分辨率有160X120, 320X240, 640X480以及1280X960。如果CPU和图形处理器处理20帧320X240时已经饱和,那么使用640X480时,实际只有5帧,这就太低了,我们一般取折中的320x240

比如我的launch文件是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
<launch>
<node name="usb_cam" pkg="usb_cam" type="usb_cam_node" output="screen" >
<param name="video_device" value="/dev/video0" />
<param name="image_width" value="640" />
<param name="image_height" value="480" />
<param name="pixel_format" value="yuyv" />
<param name="framerate" value="20" />
<param name="autofocus" value="true" />
<param name="camera_frame_id" value="usb_cam" />
<param name="io_method" value="mmap"/>
</node>
</launch>

注意布尔量也要用value="true"的形式,要设置自动对焦,因为偶尔插上摄像头会模糊。

launch中的节点发布了两个主题:

1
2
/usb_cam/camera_info     // 类型 sensor_msgs/CameraInfo
/usb_cam/image_raw   // 类型 sensor_msgs/Image

然后到上位机运行image_view包:rosrun image_view image_view image:=usb_cam/image_rawimage_view订阅了image_view主题,结果会出现一个窗口显示了摄像头的图像。或者在终端运行rqt,在话题列表里会直接出现/usb_cam/image_raw,选择即可

对于图像消息,如果需要跨设备图传,不要使用原始的sensor_msgs/Image,会非常慢,改为sensor_msgs/CompressedImage

上面的配置只发布了usb_cam/image_raw一个话题,有一个插件compressed-image-transport还可以发布三个话题:usb_cam/image_raw/compressed, usb_cam/image_raw/compressedDepth, usb_cam/image_raw/theora

安装命令如下:

1
sudo apt-get install ros-indigo-compressed-image-transport

发现的问题

问题1:未校正摄像头

运行之后出现一些告警:

1
2
[ WARN ] : Camera calibration file /home/..../.ros/camera_info/head_camera.yaml not found
[ WARN ] : unknown control 'focus_auto'

第一个告警是因为没有校正摄像头模型。第二个告警说明摄像头不支持在自动对焦和手动对焦之间切换。

[校正摄像头的方法](https://rjosodtssp.github.io/2019/02/02/ROS/ROS%E6%9C%BA%E5%99%A8%E4%BA%BA/ROS%E6%A0%A1%E6%AD%A3%E6%91%84%E5%83%8F%E5%A4%B4/)

问题2:分辨率和帧率

launch文件中可以进行图像长宽和帧率的设置。长宽默认是640X480,帧率默认30。如果参数都设置太大,程序所占CPU会很大,可以适当降低。分辨率的比例最好维持在4x3

问题3:输入输出错误

有时重启程序会出现告警信息,启动失败

这种现象偶尔会出现,后来发现在一个虚拟机上测试时每次都如此,原因是视频流本身的问题,用cheese打开后的视频不正常。
在工控机上只有重插摄像头,然后恢复正常。

问题4:v4l2-ctl:not found

启动usb_cam出现告警:warn the information as the title "sh: 1: v4l2-ctl:not found",
解决方法:sudo apt-get install v4l-utils

插入其他 USB也可能导致摄像头的端口改变

比如/dev/video0变成了/dev/video1

某些报错没有规律,只能加respawn解决


压缩图片格式出错

有时启动摄像头程序会出现下面的错误:

这应该是个视频流格式问题,目前没有造成影响,加上是偶尔出现,所以可以不管

源码分析

usb_cam是先找到USB设备,例如/dev/video0,使用Linux的open函数打开,获得文件描述符再操作.

UsbCamNode::take_and_send_image()函数中的image_pub_.publish(img_, *ci);是发布视频流话题,不止usb_cam/image_raw,还有compressed等几个话题。

参考:
ROS官方介绍: usb_cam
ROS官方介绍: camera_calibration
how to install a driver like usb_cam


日志系统和log4cxx

我们都知道ROS有默认的日志工具,用rosclean check检查日志所在路径和占用的大小,一般放在~/.ros/log,可以定义环境变量ROS_LOG_DIR改变日志存放的位置。rosclean purge可以清除日志,日志记录了所有的输出信息

应该就是rosconsole这个东西,它在启动时会加载$ROS_ROOT/config/rosconsole.config作为配置文件。另外可以定义配置文件供log4cxx使用,定义环境变量ROSCONSOLE_CONFIG_FILE为配置文件的路径。

一般做成launch文件,这样使用方便,编辑灵活:

1
2
3
4
5
<launch>
<env name="ROSCONSOLE_CONFIG_FILE"
value="$(find log_test)/setting.conf"/>
<node pkg="log_test" type="log_test" name="log_test" output="screen"/>
</launch>

package和可执行文件的名称都是log_test,配置文件由环境变量指定路径。

日志消息有三个不同的目的地:控制台,话题/rosout,日志文件。相比于控制台输出,rosout话题输出的主要作用是它在一个流中包含了系统中所有节点的日志消息。查看话题/rosout中的消息:

1
rostopic echo /rosout

也可以使用图形界面查看:rqt_consolerqt_console订阅的是话题/rosout_agg,话题由节点rosout收集话题/rosout聚合之后发布。

我写的一个节点订阅了5个话题,这几个话题是单片机发布的,在回调函数里输出对应消息类型的变量信息,日志信息最终在output.txt中,注意输出信息是用逗号隔开,这样用EXCEL打开可以将各部分隔开,实现表格效果:

ROS_INFO中文显示问号

显然这是编码问题,在ROS_INFO之前加入下面代码中的一行:

1
2
3
// 设置区域为中文,或者直接取所有枚举的并集,不必仔细研究了
// setlocale(LC_CTYPE, "zh_CN.utf8");
setlocale(LC_ALL, "");

自定义ROS日志的格式

默认的日志输出是这样的:

1
2
[ INFO] [1556115082.590417546]: msg:91
[ INFO] [1556115083.591389486]: msg:12

其实看源码rosconsole.cpp可知ROS日志就是基于log4cxx。我们可以修改环境变量ROSCONSOLE_FORMAT定制格式,格式包括以下选项:
1
2
3
4
5
6
7
8
9
severity
message
time
thread
logger
file
line
function
node

看一下rosconsole.cpp源码部分:

1
2
3
4
5
6
7
8
9
const char* g_format_string = "[${severity}] [${time}]: ${message}";
Formatter g_formatter;
format_string = getenv("ROSCONSOLE_FORMAT");
if (format_string)
{
g_format_string = format_string;
}

g_formatter.init(g_format_string);

从中可以发现源码里先给了个默认格式,我们也可以针对环境变量自己设置格式.

网上有人问能不能自定义日志信息的颜色,结果发现在函数Formatter::print里,各等级的日志信息的颜色已经写死了,比如levels::Fatal对应COLOR_RED,所以不能自定义了.

根据环境变量,我们可以这样修改:

1
export ROSCONSOLE_FORMAT='[${severity}][${node}] [${function}] line_${line}  ${message}'

只保留等级、节点名、函数、行号、信息,结果是这样的:
1
2
3
[ INFO][/Pub] [main] line_19  msg:13
[ INFO][/Pub] [main] line_19 msg:37
[ INFO][/Pub] [main] line_19 msg:73

界面日志工具

ROS提供两个带界面的日志工具:rqt_console和rqt_logger_level

命令如下:

1
2
rosrun rqt_console rqt_console
rosrun rqt_logger_level rqt_logger_level

使用很简单,就不详细解释了




参考:
rosconsole
log4j.properties配置详解
ROS 程序编写之日志
rosconsole.cpp


ROS中的坐标系

ROS中的坐标系

在ROS中坐标系总是3维的,而且遵循右手法则,Z轴用拇指表示,X轴用食指表示,Y轴用中指。X轴指向前方,Y轴指向左方,Z轴指向上方。研究坐标系绕某轴旋转时,也是用右手法则,右手握住坐标轴,大拇指 的方向朝着坐标轴朝向的正方向,四指环绕的方向定义沿着这个坐标轴旋转的正方向。


在Rviz中,默认X轴是红色、Y轴时绿色、Z轴是蓝色,也就是XYZ对应RGB。

机器人的朝哪走是向前,这是由陀螺仪和码盘决定的。

机器人中的坐标系

机器人自身坐标系一般用base_link表示,所在位置为原点,朝向的方向为X轴正方向,左方为Y轴正方向,头顶是Z轴正方向。

base_footprint坐标系,原点为base_link坐标系原点在地面的投影,只有Z值有变化

earth坐标系适用于多个机器人的环境,先不讨论了

如果想使用其他坐标系,必须使用tf包对其与map坐标系进行转换,比如下面三个坐标系都有直接或间接的转换:

如果没有经过转换,就会报错,例如:

For frame [tag_9]: No transform to fixed frame [/map]. TF error: [Could not find a connection between 'map' and 'tag_9' because they are not part of the same tree.Tf has two or more unconnected trees.]

tag_9没有向固定坐标系map的转化,也就是不在tf树上。


基础知识和常用命令

ROS最主要的构成是节点(node),每一个节点可以看做为一个单独的处理单元.它可以接受外部的消息(这个步骤叫订阅,subscribe),也可以向外部发出自己的消息(这个步骤叫发布,publish);

节点之间通讯的管理需要一个管理员(ros中被称为roscore节点).当节点分布在不同的计算机上时,需要指明master.ROS底层的通信是通过HTTP完成的,因此ROS内核本质上是一个HTTP服务器,它的地址一般是http://localhost:11311/ ,即本机的11311端口。当需要连接到另一台计算机上运行的ROS时,只要连上该机的11311端口即可。

rosbash系列

rosbash是ROS提供的一系列便利的命令,在source /opt/ros/kinetic/setup.bash后可使用。一个setup.*sh会尽可能撤销先前其他所有setup.*sh的影响,然后它再自己施加影响。

source可在最后加上选项--extend,作用是 skips the undoing of changes from a previously sourced setup file

devel/setup.bash的作用是让我们能使用roscd等ROS独有的shell命令,还有识别当前工作空间的package名称

roscd: 直接跳到某个package的地址,这个是用的最多的。不过它的本质是针对环境变量ROS_LOCATIONS中规定的地址,它的格式是这样:

1
export ROS_LOCATIONS="pkgs=~/ros/pkg:openmv=/home/user/qt-ros-ws/src/openmv_cam"

pkgs和openmv是其中规定的两个地址,二者用冒号隔开,现在就可以使用roscd openmv

rosls: 列出某个package中的所有文件

rospd: 这个命令特殊,可以记录和跳到所有package的地址,可以看做roscd的升级版。rospd package会跳到某个package,rospd会列出去过的所有package地址并且编号,以后可以直接rospd num到指定地址

rosnode

  • 查看当前运行的节点用rosnode list命令

  • 优雅关闭每个节点,使用rosnode kill命令。使用ctrl+c或者直接关闭终端也可以关闭节点,但这种方法不免过于简单粗暴,而且并未完全的关闭节点,因为节点管理器那里仍然有记录。

  • rosnode kill -a     关闭除rosout外的所有节点

  • ping节点: rosnode ping node,检查一个节点的连通性

  • 显示ROS节点使用的IP: rosnode machine

rospack系列命令

  • 列出所有有效的package:rospack list
  • 输出指定package的路径: rospack find turtlesim,输出/opt/ros/kinetic/share/turtlesim
  • 查看package的依赖项:rospack depends turtlesim

catkin_make系列命令

使用catkin_make系列命令时,会把工作空间所有的包的CMakeLists先检查一遍,有时会很麻烦。使用catkin_make_isolated命令可以针对单独的包进行编译,但速度会慢很多。

rqt系列命令

rqt_graph包用于显示各节点和主题之间的关系,命令:rosrun rqt_graph rqt_graph

rosrun rqt_plot rqt_plot

rosdep

rosdep是一个安装系统依赖项的命令,用于package的依赖项安装,比如对PACKAGE:

1
rosdep install PACKAGE

也可以安装工作空间中所有package的依赖项,先cd到工作空间的根目录,执行下面命令:

1
rosdep install --from-paths src --ignore-src -r -y


catkin_make及cmakelists分析
  • 只用catkin_make遍历所有包时,运行的是cmake进程。实际编译时,才运行cc1plus进程,一个核一个进程。

  • catkin_make编译时,对工作空间所有packages的编译顺序是按拓扑遍历的,不是按字母也不是按创建时间。修改任意一个CMakeLists.txt或者package.xml后,执行catkin_make会将工作空间的所有package的CMakeLists重新处理一遍。catkin_make遍历所有包的过程,占CPU并不大,占CPU还是在编译阶段

  • catkin_make可能报错内存不足,尤其是move_base,因为需要占用很多资源

  • catkin_make时占CPU太大,导致几乎死机。可以从另一台电脑SSH,执行 pkill cc1plus 停止编译

编译整个工作空间是这样的:
catkin_make编译.png
先检查了slam_gmapping_nodelet的依赖项,然后是编译fake_localization和depth_image_proc,然后又开始检查map_to_image_node,并不是按包的顺序编译slam_gmapping_nodelet,也就是并不是一个一个包按顺序编译,而是万箭齐发式的。因此,要看某一个包的问题时,不要编译整个工作空间,否则很难找。

catkin_make -D是指定编译的配置, 在D后面增加命令 ,比如指定编译类型Release和核数编译: catkin_make -DCMAKE_BUILD_TYPE=Release -j8

不同package的类型

编译时,catkin_make会显示各种包的情况
plain cmake.png
plain cmake是纯cmake的包,不能用catkin_make

metapackage都是ROS官方的,自己不用写这种类型,它不安装任何文件(除了package.xml说明的), 也不包含任何代码文件、test、launch文件,你会发现navigation这个包只有四个文件:CHANGELOG.rst, CMakeLists.txt, package.xml, README.rst

它的package.xml会含有下面内容:

1
2
3
<export>
<metapackage />
</export>

CMakeLists.txt中还有一句catkin_metapackage()

这里涉及到 16和18版本的不同 ,对于16 kinetic,会有如下如下报错
使用kinetic编译会报错,但是melodic不会.png
18 melodic不会有这个报错,此外18版对C++11类型的支持更好,有时无需手动修改cmake

不同版本的package.xml

目前常用的package.xml开头是这样的:

1
2
3
4
<?xml version="1.0"?>
<package format="2">
<name>name</name>
<version>0.0.0</version>

旧版本的是这样的:

1
2
3
<package>
<name>base_local_planner</name>
<version>1.14.4</version>

旧版本的不能使用exec_depend,而是用run_depend,否则会报错

单独编译某个package

以前一直认为单独编译某个package的命令是catkin_make --pkg package1,结果这样仍然会将工作空间中所有package的CMakeLists全检查一遍,花费时间相当长,实际的命令是这个:

1
catkin_make -DCATKIN_WHITELIST_PACKAGES="package1;package2"

可以编译一个或多个package,但是执行这个命令之后再catkin_make回发现它仍然只编译上次的package。恢复成编译所有package去掉引号里面的内容就行:
1
2
3
catkin_make -DCATKIN_WHITELIST_PACKAGES=""
# 对ninja编译的包
catkin_make -DCATKIN_WHITELIST_PACKAGES="package" --use-ninja

build文件夹存放cmakemake相关的文件,devel文件夹存放编译生成的文件和目标,包括setup.sh

clean命令

make clean类似,也有个catkin_make clean命令,它会删除所有编译的可执行文件或库文件,但是不会删除删除头文件,例如msg和srv生成的头文件

INSTALL

现在执行INSTALL命令不再是sudo make install了,而是catkin_make install,它相当于:

1
2
3
4
5
cd ~/catkin_ws/build
# If cmake hasn't already been called
cmake ../src -DCMAKE_INSTALL_PREFIX=../install -DCATKIN_DEVEL_PREFIX=../devel
make
make install

这样执行完以后,工作空间会出现一个install文件夹,里面存放着编译生成的库文件,问题是它会把所有package的install都进行处理,如果想改变这个目录的位置,执行catkin_make -DCMAKE_INSTALL_PREFIX=path install。另外在CMakeLists中指定安装目录用SET(CMAKE_INSTALL_PREFIX < install_path >)

编译出的.so和可执行文件可以直接放到另一台电脑使用,但是如果是跨平台,catkin_make install恐怕不可用,需要交叉编译。

结果会在share/status_panel/cmake中生成两个cmake文件

这两个文件是做依赖包时必需的,比如roscpp就有相应的文件

链接ROS库

如果想使用ROS的头文件,必须在CMakeLists里加入下面内容,也就是链接ROS的头文件和库:

1
2
3
4
5
6
7
8
include_directories(
# include
${catkin_INCLUDE_DIRS}
)
find_package(roscpp)
target_link_libraries(foo
${catkin_LIBRARIES}
)

别忘了target_link_libraries在add_excutable之后。用message函数可以看到include文件夹和libraries文件夹如下:

在CMakeLists中加入catkin_package(), 才能在执行catkin_make后,在devel/lib中生成可执行文件


cmake教程(一)基本使用规则

查看cmake版本: cmake --version,Ubuntu自带的是3.5.1

cmake和qmake都用于产生Makefile,然后执行make命令进行编译,make还有其他参数,叫做make目标

升级 CMake

1
2
3
4
5
6
7
8
9
sudo apt-get install -y build-essential libssl-dev
wget https://github.com/Kitware/CMake/releases/download/v3.19.2/cmake-3.19.2.tar.gz
tar -zxvf cmake-3.19.2.tar.gz
cd cmake-3.19.2/
./bootstrap
make
sudo make install
hash -r
cmake --version

报错: Could not find CMAKE_ROOT !!! CMake has most likely not been installed correctly. 先执行hash -r再使用

还可参考: cmake源码安装 安装CMake和cmake-gui

cmake-gui

where is the source code里选择源代码位置,在where to build the binaries里选择编译出的文件,一般是在源代码目录里新建build文件夹。

先点一次Configure,出现配置对话框,选默认或者交叉编译(最后一个选项)。如果出现红色区域,再点一次。直到没有红色区域之后,点击configure,配置完成后点击generate,会在build文件夹下生成工程文件,然后去build文件夹里执行make

常见的make目标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
make all:编译程序、库、文档等(等同于make)

make install:安装已经编译好的程序,会默认把程序安装至 /usr/local/bin

make unistall:卸载已经安装的程序。

make clean:删除由make命令产生的文件(例如object file,*.o)

make -j N : 同时允许N个任务进行,如果不指定N则进行无限个任务,结果会将CPU性能榨干。N的最大值取决于CPU性能,不要把N设置太大,更不要不指定,否则可能死机。

make check:测试刚刚编译的软件(某些程序可能不支持,有时会测试很长时间,这就没有必要)

# 有时碰到这样的情况,第一次make会出错,再次make却能成功,以下命令用于这种情况
make -i: 忽略指令执行错误,并继续执行,且如果出错的话就会生成目标文件。这个一般用在调试的时候。
make -k: 出错也不停止运行,不仅可以忽略指令错误,而且还能忽略makefile规则错误

注释方法: CMake 3.0以上版本的多行注释:从#[[开始,在块注释的另一端以]]结束。但是并不好用,似乎和Tab对齐有关。



语法规则:

  • 变量使用${}方式取值, 但是在IF控制语句中是直接使用变量名
  • 环境变量使用$ENV{}方式取值, 使用SET(ENV{VAR} VALUE)赋值
  • 指令(参数1 参数2…) 参数使用括弧括起, 参数之间使用空格或分号分开

有时候看到一些工程有一个cmake文件夹,里面放几个cmake文件,打开发现是一些cmake的命令。这是因为工程规模太大,有些库比如PCL在每个CMakeLists里使用,每次都添加太麻烦,所以做成文件形式,在用到的时候在CMakeListsinclude(cmake/PCL.cmake)

基本规则

预定义变量,设置方式是set(VARIABLE value),大小写都是敏感的,可以用message函数查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PROJECT_NAME   #通过PROJECT指令定义的项目名称
PROJECT_SOURCE_DIR #工程的根目录
PROJECT_BINARY_DIR #运行cmake命令的目录,通常是${PROJECT_SOURCE_DIR}/build
CMAKE_CURRENT_SOURCE_DIR #当前处理的CMakeLists.txt所在的路径
EXECUTABLE_OUTPUT_PATH #目标可执行文件的存放位置
LIBRARY_OUTPUT_PATH #目标链接库文件的存放位置

CMAKE_MAJOR_VERSION #cmake主版本号,如2.8.6中的2
CMAKE_MINOR_VERSION #cmake次版本号,如2.8.6中的8
CMAKE_PATCH_VERSION #cmake补丁等级,如2.8.6中的6
CMAKE_SYSTEM #系统名称,包含内核版本,例如Linux-2.6.22
CAMKE_SYSTEM_NAME #不包含版本的系统名,如Linux
UNIX #在所有的类UNIX平台为TRUE,包括OS X和cygwin
WIN32 #在所有的win32平台为TRUE,包括cygwin

BUILD_SHARED_LIBS #控制默认的库编译方式,默认编译生成的库都是静态库
CMAKE_C_COMPILER/CMAKE_CXX_COMPILER #指定C/C++编译器
CMAKE_CXX_FLAGS # 设置C++编译选项
CMAKE_BUILD_TYPE # 常用的编译类型 Debug Release
BUILD_SHARED_LIBS # 编译动态链接库(ON,OFF)

option函数

option(<variable> "<help_text>" [value])

  • variable 选项名
  • help_text 描述、解释、备注
  • value 选项初始化值(除ON而外全为OFF)

add_compile_options

下面的代码里,foo不会按照-Wall编译,但是bar会。

1
2
3
add_library(foo ...)
add_compile_options(-Wall 03)
add_library(bar ...)

-Wall显示所有警告。
O3(字母O)是编译优化的程度选择,有O1 O2 O3。数字越大编译优化越多,程序执行速度越快 但编译时间越长,不过一般程序看不出来。

CMAKE_CXX_STANDARD

CMake 3.1或者更高版本可使用set(CMAKE_CXX_STANDARD 11),不能用17

最好把add_compile_options(-std=c++17) 改为跨平台的写法: set(CMAKE_CXX_STANDARD 17),也就是x86arm平台

./configure命令

一般第三方库里都有个configure文件,它是个Shell脚本,内容很多../configure 是用来检测你的安装平台的目标特征的,比如它会检测你是不是有CC或GCC。主要功能是生成 Makefile,为下一步的编译做准备,你可以通过在./configure后加上参数来对安装进行控制,比如./configure --prefix=/usr是将该软件安装在 /usr 下面,执行文件就会安装在/usr/bin(而不是默认的 /usr/local/bin),资源文件就会安装在 /usr/share(而不是默认的/usr/local/share)

message函数

1
2
3
message(STATUS  "12345")   #如果不加STATUS,不会在前面加--标志
message(STATUS " include dirs: ${catkin_INCLUDE_DIRS}" ) #有一个--开头
message(STATUS " include dirs:" ${catkin_INCLUDE_DIRS} ) # 同上面等价

除了最常用的STATUS还有下面可使用的参数

  • (无) = 重要消息;
  • WARNING = CMake 警告, 会继续执行;
  • AUTHOR_WARNING = CMake 警告 (dev), 会继续执行;
  • SEND_ERROR = CMake 错误, 继续执行,但是会跳过生成的步骤;
  • FATAL_ERROR = CMake 错误, 终止所有处理过程;

比如使用message(WARNING "Compiler not supported C++ 20 standard"),出现下面结果

1
2
CMake Warning at ros/src/lqr_steering/CMakeLists.txt:15 (message):
Compiler not supported C++ 20 standard

换成AUTHOR_WARNING会出现下面结果

1
2
3
CMake Warning (dev) at ros/src/lqr_steering/CMakeLists.txt:15 (message):
Compiler not supported C++ 20 standard
This warning is for project developers. Use -Wno-dev to suppress it.

如果在执行cmake时,遇到一个错误情况需要停止执行,可以用FATAL_ERROR

1
2
3
if( SOME_COND )
message( FATAL_ERROR "You can not do this at all, CMake will exit." )
endif()

如果出现这种情况还要继续编译,那么就换成SEND_ERROR


指令最好全用大写
路径名中不要用\,而是用/,例如 include_directories(F:/Eigen)

常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 这必须是CMakeLists的第一行,限定cmake版本,Catkin需要2.8.3或更高版本
CMAKE_MINIMUM_REQUIRED(VERSION 3.3)

PROJECT #指定工程名称

add_compile_options(-std=c++11) # 支持C++ 11

set(CMAKE_BUILD_TYPE "Release") # 指定cmake版本为Release,或者用Debug
if(CMAKE_BUILD_TYPE STREQUAL "Release")
message(STATUS "build type is Release")
endif()

#SET 定义变量,可定义多个变量,例如: set(SRC_FILES main.cpp mainwindow.cpp mainwindow.h)
#MESSAGE 输出用户定义的信息

include_directories #指定头文件的搜索路径
link_directories #指定动态链接库的搜索路径
ADD_EXECUTABLE(bin_file_name ${SRC_FILES}) #生成可执行文件
add_definitions #添加编译参数 add_definitions("-Wall -g")
ADD_LIBRARY #生成动态库或静态库
link_libraries(lib1 lib2) #所有编译目标链接相同的库

SET_TARGET_PROPERTIES #设置输出的名称,设置动态库的版本和API版本
ADD_SUBDIRECTORY #向当前工程添加存放源文件的子目录

获取环境变量

获取bash.rc中的环境变量,通过ENV前缀来访问环境变量,读取环境变量使用$ENV{JAVA_HOME},查看环境变量:

1
message(STATUS " java home: $ENV{JAVA_HOME}" )

写环境变量如下:

1
set( ENV{PATH} /home/martink )

cmake判断操作系统和架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
message(${CMAKE_HOST_SYSTEM_NAME})
message(${CMAKE_HOST_SYSTEM_PROCESSOR})

if(CMAKE_HOST_SYSTEM_NAME MATCHES "Linux")
message("this is Linux")
elseif(CMAKE_HOST_SYSTEM_NAME MATCHES "Android")
message("this is Android")
endif()

if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "aarch64")
message("this is aarch64 cpu")
elseif(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "x86_64")
message("this is x86_64 cpu")
endif()

获得include的文件路径

1
2
3
4
5
6
include_directories(
include
/usr/include/eigen3
)
get_property(dirs DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY INCLUDE_DIRECTORIES)
MESSAGE("include path: ${dirs}")

指定生成文件的输出路径

使用SET命令重新定义EXECUTABLE_OUTPUT_PATHLIBRARY_OUTPUT_PATH变量来指定最终的二进制文件的位置

1
2
SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)

上面的两条命令通常紧跟ADD_EXECUTABLEADD_LIBRARY,与其写在同一个CMakeLists.txt即可

include_directories 和 target_include_directories

都是添加头文件搜索路径。差别:

  • include_directories的影响范围最大,可以为CMakelists.txt后的所有项目添加头文件目录。一般写在最外层CMakelists.txt中影响全局

  • 影响范围可以自定义,加关键子PRIVATEPUBLIC。一般引用库路径使用这个命令,作为外部依赖项引入进来,target是自己项目生成的lib。

PRIVATE:target对应的源文件使用
PUBLIC:target对应的头文件、源文件都使用

如果有不同目录相同名称的头文件会产生影响,所以这里建议针对特定的 target 使用 target_include_directories

参考:
CMake基本教程
子工程和链接静态库
CMake使用进阶


互斥锁 mutex

互斥锁

在多线程的情况下,当一个变量可以被多个线程修改时,就需要考虑多线程同步问题。线程A修改变量前,先加锁,修改结束再解锁,然后线程B获取同样的锁,修改结束再解锁,如果不是同一把锁,同步是无效的。

在C++中使用pthread的互斥量接口实现数据同步,线程A对互斥量mutex加锁后,其他尝试加锁的线程都会阻塞,等线程A解锁后,其他线程从阻塞变为运行态,第一个抢到CPU的线程加锁成功,其他线程再次阻塞,这样每次只有一个线程能加锁。这里存在规则统一的问题,就是线程可以在不加锁情况下访问变量,此时即使另一个线程加了锁,还是会出现不同步的问题,所以不能有的线程需要加锁,有的线程不需要加锁,必须统一化。

缺点:

  • 重复锁定和解锁,每次都会检查共享数据结构,浪费时间和资源;
  • 繁忙查询的效率非常低;

互斥锁的程序如下,注意两个线程的join是在两个线程定义之后运行

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
#include<ros/ros.h>
#include <boost/thread.hpp>
#include "boost/thread/mutex.hpp"
using namespace std;

boost::mutex myLock;
unsigned sum = 10;

void thread_1(int n)
{
myLock.lock();
sum = sum * n;
sleep(2);
cout<<"thread 1, sum: "<< sum <<endl;
myLock.unlock();
}

void thread_2(int n)
{
sleep(1);
myLock.lock();
sum = sum * 7 * n;
cout<<"thread 2, sum: "<< sum <<endl;
myLock.unlock();
}

int main()
{
unsigned int n = 2;
boost::thread th_1 = boost::thread(boost::bind(&thread_1,n));
boost::thread th_2 = boost::thread(boost::bind(&thread_2,n));

th_1.join();
th_2.join();
return 0;
}

不加互斥锁的情况下,运行结果

调试时,我发现mutex的lockunlock应当包括cout,否则执行顺序还是不确定。

try_lock

解释一下try_lock的特点,它试图取得一个lock,成功就返回true, 失败就返回false. 但是失败也不会阻塞。

If try_lock is called by a thread that already owns the mutex, the behavior is undefined.

程序如下

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
#include<ros/ros.h>
#include <boost/thread.hpp>
#include "boost/thread/mutex.hpp"
using namespace std;

boost::mutex myLock;
unsigned sum = 10;

void thread_1(int n)
{
cout << "start thread 1" << endl;
myLock.lock();
sum = sum * n;
cout<<"thread 1, sum: "<< sum <<endl;
// myLock.unlock();
}

void thread_2(int n)
{
cout << "start thread 2" << endl;
myLock.lock();
// myLock.try_lock();
sum = sum * 7 * n;
cout<<"thread 2, sum: "<< sum <<endl;
myLock.unlock();
}

int main()
{
unsigned int n = 2;
boost::thread th_1 = boost::thread(boost::bind(&thread_1, n) );
boost::thread th_2 = boost::thread(boost::bind(&thread_2, n) );

th_1.join();
th_2.join();
return 0;
}

运行结果是
1
2
3
start thread 1
thread 1, sum: 20
start thread 2

显然线程1的最后没有unlock互斥锁,线程2获取互斥锁失败,会阻塞。

如果在线程2中改用try_lock,就不会阻塞,运行结果是

1
2
3
4
start thread 1
thread 1, sum: 20
start thread 2
thread 2, sum: 280

或者
1
2
3
4
start thread 1start thread 2
thread 1, sum:
thread 2, sum: 280
20

是不会阻塞,但会出现我们不想要的结果。

参考: C++ 多线程互斥锁


Linux系统函数

exit函数

exit中的参数exit_code为0代表进程正常终止,若为其他值表示程序执行过程中有错误发生。出错时退出一般用exit(EXIT_FAILURE);

system()函数

这个函数是调用/bin/sh执行脚本的,有些命令如rosrun不能在/bin/sh下执行

WIFEXITED(status) 这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status)就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7。请注意,如果进程不是正常退出的,也就是说,WIFEXITED返回0,这个值就毫无意义。

所以一个典型的system函数的使用是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pid_t status;
std::string cmd = "rosnode kill /lidar";
status = system(cmd.data());
if (-1 == status) // 语句有错
{
return -1;
}
else
{
ROS_INFO("WIFEXITED return: %d",WIFEXITED(status));
if (WIFEXITED(status)) //返回一个非零值, 正常退出
ROS_INFO("child process exit done: %d", WEXITSTATUS(status) );
else
ROS_INFO("child process exit abnormally");
}

errno

errno表示错误代码。 记录系统的最后一次错误代码。代码是一个int型的值,在errno.h中定义。系统每一次出错都会对应一个出错代码,例如12表示“Cannot allocate memory”。

stderr和fprintf函数

linux中的进程启动时,都会打开三个文件:标准输入、标准输出和标准出错处理。通常这三个文件都与终端联系。这三个文件分别对应文件描述符0、1、2。系队统自定义了三个文件指针stdin、stdout、stderr,分别指向标准输入、标准输出和标准出错输出。stderr是linux标准出错的文件指针,定义为extern struct _IO_FILE *stderr;,对应文件描述符2,通常结合fprintf使用:

1
fprintf(stderr,"error message");	//不必加换行

STDERR_FILENO和write函数

1
2
3
4
/* 文件描述符*/
#define STDIN_FILENO 0 /* Standard input. */
#define STDOUT_FILENO 1 /* Standard output. */
#define STDERR_FILENO 2 /* Standard error output. */

与上面用法类似,但write的首个参数是文件描述符:

1
2
char err[] = "error\n";
write(STDERR_FILENO,err,strlen(err)); //输出error(换行)

perror函数

需要包含头文件stdio.h,perror是错误输出函数,在标准输出设备上输出一个错误信息,是对errno的封装。perror(“fun”),其输出为:fun:后面跟着错误信息(加一个换行符)。

1
perror("status:");	//不报错时,输出 status:Success

strerror函数

stderror是通过参数errno,返回错误信息:printf("strerror: %s\n",strerror(errno));

atexit函数

功 能: 注册终止函数(即main执行结束后调用的函数)

用 法: int atexit(void (*func)(void)),也就是只能注册形参和返回值都为空的函数

exit函数和_exit函数都可以退出程序,但后者是立即进入内核,前者是做一些清理处理再进入内核.atexit函数就是用于执行清理时的一些操作.

exit调用终止处理函数的顺序和atexit登记的顺序相反.程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void  test1()
{
cout<<"test 1"<<endl;
}

void test2()
{
cout<<"test 2"<<endl;
}

void test3()
{
cout<<"test 3"<<endl;
}

int main()
{
atexit(test1);
atexit(test2);
atexit(test3);
return 0;
}

运行结果:
1
2
3
test 3
test 2
test 1

gethostname函数

功能是获得计算机主机名,形参分别是char*和字符长度,成功会返回0

1
2
3
4
char name[40];
memset(name,0,sizeof(name));
if(!gethostname(name,sizeof(name) ) )
printf("%s\n\n",name);


memset和memcpy函数

memset函数

原型:void *memset(void *s, int ch, size_t n);
作用:将s所指向的内存中的前n个字节的内容全部设置为ch指定的ASCII值,这个函数通常为新申请的内存做初始化工作。一般用于结构体和数组的初始化。

  1. memset中的第三个参数一定要使用sizeof操作符,因为每个系统下对类型长度的定义可能不一样。
  2. memset中的第一个参数一定要是一个已知的、已经被分配内存的地址,否则会出错。
  3. 对于单字节数据类型(char)可以初始化为任意支持的值,都没有问题,但是对于非多字节数据类型只能初始化为0,而不能初始化成别的初值,否则容易出错。

memset的效率很高,比手动赋值要高的多,比bzero也要高,尤其大数组的情况。

我是这样实现的:

1
2
3
4
5
6
7
8
9
10
11
void* _memset(void* dst,int val, size_t count)
{
assert(dst!=NULL);
char* tmpdst = (char*)dst;
while(count--)
{
*tmpdst = (char)val;
tmpdst++;
}
return dst;
}

memcpy函数

memcpy函数的使用场合是不需要考虑内存重叠问题的,因为涉及到内存重叠时我们应该调用的是memmove函数。我是这样实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void* _memcpy(void* dst, const void* src, size_t n)
{
assert(dst!=NULL && src!=NULL);
assert(n>=0);
char* temp = (char*)dst;
const char* p = (char*)src;
size_t m=0; //void指针不能自增
while(n--)
{
*temp = *p;
temp++;
p++;
}
return dst;
}

memmove进行了改进,考虑了内存重叠的情况:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void* my_memmove(void* dst, const void* src, size_t n)
{
char* s_dst;
char* s_src;
s_dst = (char*)dst;
s_src = (char*)src;
if(s_dst>s_src && (s_src+n>s_dst)) {
s_dst = s_dst+n-1;
s_src = s_src+n-1;
while(n--) {
*s_dst-- = *s_src--;
}
}else {
while(n--) {
*s_dst++ = *s_src++;
}
}
return dst;
}