工作空间和package.xml

创建package后,package.xml不需要再修改

创建package命令是catkin_create_pkg nodeName

package名称

package的名称对应CMakeLists中的project(package)package.xml中的<name>package</name>

package的命名是有官方规范的,我意外地发现test这个名字往往会带来问题,可能是因为带test的package都是用于测试的,所以要避免使用。基本规则如下:

  1. 只能使用字母数字下划线
  2. 字母只能用小写,开头必须是字母
  3. 至少有两个字符长度
  4. 只做消息的package以_msg结尾,只含launch的以_launch结尾,以此类推

不要轻易用msg, launch, test这样的字符串做名称,容易出问题。如果命名不规范,编译时会告警,影响编译时间。查看官方package列表,发现除了tf这样一个单词做名称的例子外,其他基本都是a_ba_b_c这样的名称,例如tuw_geometry_rviz,所以这样命名比较稳妥。

package.xml

每个package.xml文件必须包含的标签包括:

  • <package> : 最高级tag,属性:format,用于指定格式
  • <name> :package名称
  • <version> :当前版本
  • <description>:package的基本描述
  • <maintainer>(至少一个):维护者
  • <license>(至少一个):协议
  • <buildtool_depend>(至少一个): 一般情况下只需要指定catkin作为编译工具, 在需要交叉编译的情况下需要增加目标机器的编译工具

可选标签包括:

  • <build_depend> (多个):编译时需要依赖的其它package,适用于静态库
  • <exec_depend> (多个):运行时需要依赖的其它package,适用于静态库
  • build_export_depend: 编译时需要链接(build against)的package
  • <export>:用于添加额外的信息,比如需要嵌入的其它package的插件,或者一些说明信息。

使用depend可以表示同时包括buildexec依赖,也就是说<depend>roscpp</depend>相当于

1
2
<build_depend>roscpp</build_depend>
<exec_depend>roscpp</exec_depend>

一般都包括的几个是

1
2
3
4
5
6
<buildtool_depend>catkin</buildtool_depend>
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
<!-- 可能还有其他msgs类型 -->
<depend>std_msgs</depend>
<build_export_depend>std_msgs</build_export_depend>

几个重要路径

4个ROS的重要路径,可在CMakeLists中用message函数查看:

  • CATKIN_PACKAGE_LIB_DESTINATION: lib库的路径

  • CATKIN_PACKAGE_BIN_DESTINATION: 可执行文件的路径

  • CATKIN_PACKAGE_SHARE_DESTINATION:共享文件的路径,就是CMakeLists中的INSTALL生成的两个cmake文件的路径

  • catkin_INCLUDE_DIRS: 跟catkin有关文件的路径,不仅仅是ROS相关的

在代码中包含另一个package的头文件时,经常写成#include<robot_api/robot_api.h>,注意程序目录结构必须是这样:

多工作空间的问题

尽量只用一个工作空间,如果用到两个以上,不要出现同名package,也不要出现不同工作空间的包有依赖的情况

如果在/opt/ros/kinetic/之外,如果我们创建了多个工作空间且需要在多个工作空间中来回地工作,比如/home/user/catkin_ws1/home/user/catkin_ws2

ROS_PACKAGE_PATH环境变量中的多个目录是有先后顺序的。如果工作空间1和2中的所有包之间没有依赖关系,那么将下面两句话放在bashrc文件中合适的位置(至少应该放在source /opt/ros/kinetic/setup.*sh后)。

1
2
source /home/user/catkin_ws1/devel/setup.*sh --extended
source /home/user/catkin_ws2/devel/setup.*sh --extended

后面source的工作空间会遮蔽之前工作空间的同名包,比如opt/ros/indigo/share/中安装了move_base包,又在/home/user/catkin_ws1中通过下载源代码的方式安装了move_base,由于ROS_PACKAGE_PATH环境变量的遮蔽效果,我实际用的是/home/user/catkin_ws1/中的move_base,这正是我们常常的目的

环境变量中这样设置:

1
2
source /opt/ros/kinetic/setup.zsh
source /home/xiaoqiang/catkin_ws/devel/setup.zsh

ROS_PACKAGE_PATH结果是/home/xiaoqiang/catkin_ws/src:/opt/ros/kinetic/share,也就是说后source的工作空间会排在ROS_PACKAGE_PATH的靠前部分,也就是优先级高

source工作空间的顺序会影响链接的so文件

source devel/setup.bash,再source /opt/ros/melodic/setup.bash,没有source install/setup.bash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# libmove_base.so 所链接的 libcostmap_2d.so 是deve/lib里的
user@user:~/catkin_ws/install/lib$ ldd libmove_base.so | grep user
libcostmap_2d.so => /home/user/my_ws/devel/lib/libcostmap_2d.so (0x00007f1d87062000)

user@user:~/catkin_ws/install/lib$ locate libcostmap_2d
# 有5个 libcostmap_2d.so
/home/user/catkin_ws/devel/lib/libcostmap_2d.so
/home/user/catkin_ws/install/lib/libcostmap_2d.so

/home/user/my_ws/devel/lib/libcostmap_2d.so
/home/user/my_ws/install/lib/libcostmap_2d.so

/opt/ros/melodic/lib/libcostmap_2d.so
# 删除 devel/lib/libcostmap_2d.so
user@user:~/catkin_ws/install/lib$ rm /home/user/my_ws/devel/lib/libcostmap_2d.so
# 此时就会链接到 ros 自带的 libcostmap_2d.so
# 如果ros自带的也被删除,就会链接失败,不去链接其他的 libcostmap_2d.so
user@user:~/catkin_ws/install/lib$ ldd libmove_base.so | grep cost
libcostmap_2d.so => /opt/ros/melodic/lib/libcostmap_2d.so (0x00007f15a177a000)

编译时寻找其他工作空间的包


很奇怪,我编译的工作空间已经有voxel_grid,结果会去另一个工作空间找同名的包,但那个包已经被我删了,所以找不到,结果报错。

看catkin_make开头的信息,发现这里就涉及到了xju_ws这个包了


发现build文件夹里的几个文件涉及了xju_ws,但是如果删除再编译,还是会生成一模一样的文件。

最后只有重建一个工作空间,执行catkin_init_workspace,再编译,问题解决。


环境变量

必须有设置的环境变量

  • ROS_ROOT

这个是ROS核心包安装路径,也就是/opt/ros/kinetic/share/ros,不须自己设置

  • ROS_PACKAGE_PATH

所有ros package所在位置,一般为/home/name/catkin_ws_test/src:/opt/ros/kinetic/share

这几个环境变量一般不需要显式地添加,而是通过运行source /opt/ros/kinetic/setup.bashsource %个人工作空间目录%/devel/setup.bash来进行设置。一般需要将上面这两行命令添加到系统的~/.bashrc文件中,来避免每次开启终端时都要重新手动执行设置命令。

  • ROS_MASTER_URI

这个表示主机的IP信息,比如http://192.168.134.123:11311。这个必须要手动设置

  • ROS_IP/ROS_HOSTNAME

ROS主从多机协作时使用,用来设置设置不同机器上ROS的网络地址。这个必须要手动设置


执行env | grep ROS会得到如下结果,有些环境变量不设置也会有默认值

1
2
3
4
5
6
7
8
9
10
11
ROSCONSOLE_FORMAT=[${message}
ROS_ETC_DIR=/opt/ros/melodic/etc/ros
ROS_ROOT=/opt/ros/melodic/share/ros
ROS_MASTER_URI=http://192.168.8.177:11311/
ROS_VERSION=1
ROS_PYTHON_VERSION=2
ROS_IP=192.168.8.177
ROS_PACKAGE_PATH=/home/user/test_nodes/src:/home/user/catkin_ws/src:/opt/ros/melodic/share
ROSLISP_PACKAGE_DIRECTORIES=/home/user/test_nodes/devel/share/common-lisp:/home/user/catkin_ws/devel/share/common-lisp
ROS_HOSTNAME=192.168.8.177
ROS_DISTRO=melodic

可选的环境变量

  • ROS_VERSION, 比如 1

  • ROS_DISTRO, 比如 noetic

  • ROS_ETC_DIR

比如 /opt/ros/noetic/etc/ros

  • ROS_HOME

ROS默认的home路径是~/.ros,默认情况下该目录下保存log文件和test的结果文件。通过修改这个环境变量可以改变上面两个文件的保存路径。

  • ROS_LOG_DIR

该变量指向ros输出的log文件保存的目录。该变量为空时,log文件默认保存在ROS_HOME指向的目录中;设置该变量后,输出的log文件保存在ROS_LOG_DIR指向的目录中,做日志工具时很有用。一般在~/.bashrc文件中设置该变量。

  • ROSCONSOLE_CONFIG_FILE

默认情况下,c++接口使用的log配置文件路径为$ROS_ROOT/config/rosconsole.config, 通过设置该变量可以配置c++接口的log信息的输出级别。

  • ROSCONSOLE_FORMAT

修改log输出的格式,默认情况下的输出格式 export ROSCONSOLE_FORMAT= [${severity}] [${time}]: ${message}

  • ROS_BOOST_ROOT

这个不用研究。 设置搜索boost库的地址,如果没有设置,那么默认使用ROS_BINDEPS_PATH

  • ROS_PARALLEL_JOBS

这个和cmake 的-j8是类似的。

export ROS_PARALLEL_JOBS='-j8 -l8'
-j flag with an argument to run up to 8 jobs in parallel, independent of system load:

export ROS_PARALLEL_JOBS=-j8

using the -l flag to set a system load-dependent limit on parallelism. Excessive parallelism in a large build can exhaust system memory.


roslaunch详解

在ros中,我们有时需要一次性启动多个节点,这种情况下再用rosrun就力不从心了,于是出现了roslaunch命令和launch文件,launch文件使用XML语法launch文件使用XML语法。roslaunch不保证节点开始的顺序。因为没有办法从外部知道节点何时被完全初始化,所以所有被启动的节点必须是robust,以便以任何顺序启动。

node

node的形式为:

1
<node name="node-name" pkg="pkg-name" type="executable-name" output="screen" />

node的三个属性分别为节点名字(name)、程序包名字(pkg)和可执行文件(type)的名字。尤其注意:type是CMakeLists中的add_executable中的名称,而不是cpp文件的名称。 否则会报错:

不要把pkg写成 pkg="$(find package)",可能会报错:

1
2
ERROR: cannot launch node of type [path_to_file] package_path
ROS path [0]=/opt/ros/...

name属性给节点指派了名称,它将覆盖任何通过调用ros::init来赋予节点的名称。另外node标签内也可以用过arg设置节点参数值。如果node标签有children标签,就需要显式标签来定义。即末尾为/node>

当roslaunch开启所有nodes后,roslaunch会监视每个node,记录那些仍然活动的nodes。对于每个node,当其终止后,我们可以要求roslaunch重启该node,通过使用respawn属性。通过使用respawn属性: respawn="true"

respawn_delay="10"会让节点终止后,隔一段时间重新运行,和respawn="true"同时使用,单位秒

多个同样的节点有不同的topic

在launch文件中使用node_1和node_2,它们的type是一样的,但是还发布了话题。这样启动launch就会出现两个节点同一个话题的情况,这不是我们想要的。 此时要在代码中对NodeHandle的构造函数加~,这时param也加上了节点名做前缀,可能还要在launch中用remap修改话题名。

param

param是ROS系统运行中的参数,存储在参数服务器中。在launch文件中通过元素加载参数;launch文件执行后,参数就加载到ROS的参数服务器上了。每个活跃的节点都可以通过 ros::param::get()getParam接口来获取parameter的值,用户也可以在终端中通过rosparam命令或获得参数的值。

<param>的使用方法如下:

1
<param name="frame" type="string" value="odom"/>    <!-- type可以没有 -->

1
2
string f;
nh.getParam("frame",f)

argument

argument是另外一个概念,类似于launch文件内部的局部变量,仅限于launch文件使用,和ROS节点内部的实现没有关系。argument只在启动文件内才有意义,不能被节点直接获取的

设置argument使用标签元素,语法如下:

1
2
<arg name="arg-name" default="arg-value" />
<arg name="arg-name" default="arg-unchanged-value" />

launch文件中需要使用到argument时,可以使用如下方式调用:

1
2
3
<arg name="arg-name" default="arg-value" />
<param name = "foo" value="$(arg arg-name)" />
<node name="node" pkg="package" type="type "args="$(arg arg-name)" />

也就是不能直接在程序里获得arg的值,只能通过param来间接获得。不过在读move_base.cpp时发现,如果main函数带有argc和argv,那么arg参数值实际就是argv[1],也就是ros运行时是以node_name arg-value的形式进行的

1
2
3
4
5
int main(int argc, char **argv)
{
ros::init(argc, argv, "map_server", ros::init_options::AnonymousName);
std::string s(argv[1]); // launch文件中的arg赋值给了s
}

虽然在launch文件中规定了参数的值,但我们在终端使用roslaunch命令时,还可以修改这个值,但前提是arg的赋值是用default,不是用value,命令如下:

1
roslaunch package-name launch-file-name arg-name:=arg-value

注意是 arg-name ,不是name

标签

使用这个标签可以读取bashrc里的环境变量没有关系,也就是说 在roslaunch里获取 bashrc 里的环境变量 。 除此之外看不出跟arg参数还有什么区别。

我们常常遇到一种情况:多个launch都用到一个参数,比如某个路径,如果用arg,只能在每个launch里设置它,或者在一个单独launch里设置好,然后所有launch都去include这个launch。 如果有了环境变量,就可以在每个launch里直接使用这个环境变量代表的路径了。

1
2
<param name="path1" value="$(env PATH1)"  />
<param name="path2" value="$(env PATH2)" />

eval

$(eval <expression>)可以实现复杂的python表达式,不太常用

例如:

1
2
3
<param name="circumference" value="$(eval 2.* 3.1415 * arg('radius'))"/>
<!-- 当前launch所在的路径 -->
<arg name="abspath" value="$(eval eval('_' + '_import_' + '_(\'os\').path.abspath(\'.\')') )" />

launch中加条件判断

launch文件部分如下:

1
2
3
4
5
6
7
<arg name="driver_enable" default="true"/>

<group if="$(arg driver_enable)">

<include file= "$(find openni2_launch)/launch/openni2.launch"/>

</group>

其实还是从上一节引申而来,在终端使用如下命令,根据arg的值是否执行,:
1
roslaunch [pkg] [node] driver_enable:=false

如果是false,就不加载openni2.launch

include另一个launch

在launch文件中可以include另一个launch文件,扩展名也可以是xml

1
<include file="$(find package)/launch/amcl.launch"/>

一般这样写,package的路径用shell命令 $(find package)获得

其他

roslaunch中的可选标签:

  • ns = foo(可选) 在”foo”命名空间中启动节点
  • clear_params = true 或 false 在启动前删除节点的私有命名空间中的所有参数。

  • master 目前已经淘汰, master 用以配置 ROS Master

还可选 restartno. defines the behavior for starting a new master with the roslaunch session.

  • 如果是start, then a master will be started if none is running.
  • 如果是restart, then a fresh master will be started for the demo — any existing master will be shutdown first.
  • 默认是no, which is to do nothing.

launch-prefix

  • launch-prefix=”nice” : nice your process to lower its CPU usage

  • launch-prefix=”screen -d -m gdb —args” : useful if the node is being run on another machine; you can then ssh to that machine and do screen -D -R to see the gdb session

required


这个可加可不加,加了之后在关闭时会有这样的提示

参考: 如何多次启动相同名称的节点


cmake教程(二) 库的使用

include_directories

参数应该是find_package调用生成的* _INCLUDE_DIRS变量以及需要包含的任何其他目录。如果您使用catkin和Boost,则include_directories调用应如下所示:

1
2
# 先调用find_package(Boost catkin),才能使用下面两个宏
include_directories(include ${Boost_INCLUDE_DIRS} ${catkin_INCLUDE_DIRS})

include是目录名,不是命令,一般放头文件。使用message函数发现catkin_INCLUDE_DIRS/opt/ros/kinetic/include/usr/include

不能直接include当前文件夹的头文件,这倒是奇怪。需要加一行include_directories(.)。另一种解决方法就是在add_executable里添加用到的头文件

添加库路径

1
LINK_DIRECTORIES(/usr/lib/) 

并不是必须的,如果能直接链接就不需要。

ADD_LIBRARY

生成库文件是ADD_LIBRARY,它的功能是用来指定库来构建。默认情况下,catkin构建共享库。

构建库如下PointManage:

1
2
3
4
5
6
7
add_library(PointManage src/point_manage.cpp)
target_link_libraries(PointManage
${catkin_LIBRARIES} -pthread -llog4cpp -lcgicc -lmysqlclient jsoncpp Qt5::Core Qt5::Xml
)

add_executable(point_node src/point_manage_node.cpp src/point_manage.cpp)
target_link_libraries(point_node ${catkin_LIBRARIES} -llog4cpp -lmysqlclient -pthread Qt5::Core Qt5::Xml)

库PointManage用到了Qt库,此时也需要加上target_link_libraries命令,编译才能成功。

add_library(lib a.cpp),如果a.cpp中使用了类Test,对应头文件test.h和源文件test.cpp。这种情况下,光include "test.h"不够,应当改成add_library(lib a.cpp test.cpp)

链接库

target_link_libraries必须放在add_executable(或者add_library)的后面。一般要先写好后者,然后再链接库:

1
2
3
4
5
add_executable(test "main.c")

target_link_libraries(test
${catkin_LIBRARIES}
) //必须跟上面的test同名,后者是很多共享库,大部分是Boost,显然ROS底层用的是Boost

但是注意,如果是生成动态库文件,最好不要再让它链接其他库文件,有时会失败。对于静态库,禁止链接其他库。

查看是否链接动态库成功,可以使用Linux的ldd命令

链接库时报警 缺函数.png
从报错内容可以看出是缺了distance函数,也就是要链接的so文件里没有distance函数

ALL_TARGET_LIBRARIES 变量

为了避免target_link_libraries后面跟很长一串库的名字,而且库增减的时候它也得跟着增减,我们在CMakeLists一开始定义一个变量: set(ALL_TARGET_LIBRARIES "")

然后在每个库对应的XX.cmake文件中,把库的名字合并到这个变量中去,list(APPEND ALL_TARGET_LIBRARIES ${XX_LIBRARIES}),这样在target_link_libraries就只使用ALL_TARGET_LIBRARIES这一个变量就好了。

install命令

安装网上下载的库时常常最后一步是make install,这是使用CMake的install函数完成的,该函数的参数:

1
2
3
4
TARGETS   目标安装
ARCHIVE DESTINATION 静态库和DLL(Windows).lib存根
LIBRARY DESTINATION 非DLL共享库和模块
RUNTIME DESTINATION 可执行目标和DLL(Windows)样式共享库

ROS的launch文件可以安装到${CATKIN_PACKAGE_SHARE_DESTINATION}:

1
2
3
install(DIRECTORY launch/
DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}/launch
PATTERN ".svn" EXCLUDE)

参考:
ROS CMakeLists Posted by G.J.先生
catkin_make的时候发生了什么


自定义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中生成可执行文件