rosparam与参数服务器

机器人的参数一般会提前设置好,这些参数都会放在yaml文件里,但有些参数是需要动态改变的,调试导航时尤其如此。于是ROS提供了rosparam命令和参数服务器这两个工具。rosparam命令的方便之处在于它可以直接读取yaml文件中的参数,而不必自己找第三方库去文件中读取。

如果参数服务器不加载yaml文件,就没有相应的参数。`nh.param`函数只是对相应的变量赋值,不会加载参数到服务器。

一般在package的文件夹里会建一个param文件夹,也就是与launchmsg同一级,yaml文件就放在这个param文件夹里。

使用rosparam load file.yaml命令将多个参数加载到参数服务器,在用到时会从服务器中获取。但实际上,这个命令直接用得不多。要将一个yaml文件中的参数在节点启动时都加载到参数服务器,一般是在launch文件中使用rosparam标签:

1
2
<rosparam file="$(find turtlebot_navigation)/param/move_base_params.yaml" command="load" />
<rosparam file="$(arg custom_param_file)" command="load" />

加载完成后,可以使用一些调试性的命令了。用rosparam list查看所有参数。可以用rosparam get param查看某参数的值,用rosparam set param value设定参数的值。另外还可以用rosparam delete param删除某个参数。

在launch文件里,如果参数paramA是在某个node之内的,那么它的全名是/node/paramA。如果希望全名是paramA,那么不应在任何node内,一般写在launch开头。

使用rosparam get时要注意,参数名称不只是yaml文件中的名称,而是一个完整名称,比如.yaml中是这样的:

1
2
3
TrajectoryPlannerROS:
int_param: 2
max_vel_x: 0.15

要获得max_vel_x的值,命令是rosparam get /move_base/TrajectoryPlannerROS/max_vel_x,也就是/node/tag/param

启动ROS节点后, 用rosparam get param可获知各个参数值,而且发现在关掉所有节点后,仍然能用rosparam get获得参数值,可见它被存到参数服务器里了

程序中的param

加载参数的方法其实有三种,除了上面说的直接用rosparam load命令和launch文件中使用load外,还可以在程序中使用paramsetParam,getParam函数,这里和yaml文件就没关系了。

1
2
3
4
5
6
7
8
9
10
std::string s;
int num;
n.getParam("string_param", s);
pn.getParam("int_param", num);
n.setParam("string_param", "hehe");
pn.setParam("int_param", 222);


n.param<std::string>("string_param", s, "haha");
pn.param<int>("int_param", num, 666)

getParam()函数可以从参数服务器获取参数值。如果成功,变量s和num的值将会被修改为参数值,函数返回true;如果不成功(譬如参数服务器没有设置这个参数),变量s和num将保持原值,函数会返回false。setParam()就是设置参数的值了。param()函数从参数服务器取参数值给变量。如果无法获取,则将默认值赋给变量,和param()函数的区别是还提供了一个默认值。

还有ros::NodeHandle::hasParam()ros::NodeHandle::deleteParam()函数,后者可以在析构函数中使用。


如果在yaml文件中定义如下

1
2
3
waypoints:
X: [38, 38, 38, 38, 38, 38, 38 , 38.21, 39.25, 42.56, 48.97, 61]
Y: [-57, -45, -32.0, -18.5, -12.0, 0.0, 12, 35, 42.89, 80.41, 131.48, 139.47]

可以直接以vector形式获取数据
1
2
vector<double> X;
nh.getParam("/frenet_planner/waypoints/X", X);

注意

使用yaml文件时,一定要注意格式的正确,但是最好不要用-符号,也就是不要用数组项,否则可能出问题,之前尝试使用一个第三方库读取yaml文件时,总是读取失败,发现只有加上-符号,也就是将参数作为数组项后才能正常读取。但再用rosparam get读取这个文件中的参数就会出错,结果获得的全是默认值,不是当前值。用rosparam list会发现加载的参数不全,原因是yaml文件中这样编辑:

1
2
3
4
TrajectoryPlannerROS:
-name: value
int_param: 2
max_vel_x: 0.15

不要加-name一行,也就是反而不能使用标准的yaml格式

参考:Using Parameters in roscpp


解析yaml

YAML最基本,最常用的一些使用格式:
首先YAML中允许表示三种格式,分别是常量值,对象和数组

1
2
3
4
5
6
7
8
9
10
11
12
13
url: http://www.wolfcode.cn 

server:
host: http://www.wolfcode.cn

server:
- 120.168.117.21
- 120.168.117.22
- 120.168.117.23
#常量
pi: 3.14 #定义一个数值3.14
hasChild: true #定义一个boolean值
name: 'Hello YAML' #定义一个字符串

yaml-cpp 读 yaml

ROS默认的yaml-cpp版本一般在0.5以上,CMakeLists如下配置

1
2
3
4
5
6
7
8
9
10
include_directories(
/usr/include
${catkin_INCLUDE_DIRS}
)

add_executable(${PROJECT_NAME} src/test_yaml.cpp)
target_link_libraries(${PROJECT_NAME}
${catkin_LIBRARIES}
yaml-cpp # 这里必须有
)

map_server中学习一段代码:

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
#include <ros/ros.h>
#include "yaml-cpp/yaml.h"
#include <iostream>
#include <fstream>
#include <string>

// 必须自定义,不明白为什么网上的资料都没有提到
template<typename T> void operator >> (const YAML::Node& node, T& i )
{
i = node.as<T>();
}

int main(int argc, char** argv)
{
ros::init(argc, argv, "test_yaml" );
ros::NodeHandle nh;

std::string file = "/home/user/map.yaml";
std::ifstream fin(file.c_str());
if (fin.fail() )
{
ROS_ERROR("could not open %s.", file.c_str());
return false;
}
// 适用于 0.5 以上版本
YAML::Node doc = YAML::Load(fin);
try {
double res, negate;
std::string image;
doc["resolution"] >> res;
doc["image"] >> image;

ROS_INFO("res: %f", res );
ROS_INFO("negate: %f", negate);
ROS_INFO("image: %s", image.c_str() );
}
catch (YAML::InvalidScalar &e) {
ROS_ERROR("The map yaml file error, reason: %s.", e.what());
return false;
}
}

Python 读 Yaml

读YAML的最好方法是使用Python的yaml库PyYAML,到官网下载安装后,可以先运行测试程序看是否成功。
读取程序如下:

1
2
3
4
5
6
import yaml,os

f = open("test.yaml")
y = yaml.load(f,Loader=yaml.FullLoader)

print y

运行后,结果:


工作空间和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.


切换机器人的地图
abstract Welcome to my blog, enter password to read.
Read more
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. 运行

rosbridge实战(三) 使用roslibjs订阅消息
abstract Welcome to my blog, enter password to read.
Read more
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