发布和订阅

话题通信的媒介是消息,消息的产生和消费是解偶的,二者之间没有强行绑定的关系,ROS节点不在乎是谁在发布Topic,哪个节点在订阅Topic,它只关心topic的名字以及当前的消息类型时候和要求的匹配。所以说话题通信是多对多的方式

publisher注册时的消息类型,如果和发布时的消息类型不一致,编译不报错,运行报错

消息是以Boost共享指针的形式传输,我们可以存储它而又不需要复制数据。回调函数最常用的原型是这样的:

1
void callback(const std_msgs::StringConstPtr& str)

其实它就是void callback(const boost::shared_ptr<std_msgs::String const>&);,还有以下几种:
1
2
3
4
5
6
void callback(boost::shared_ptr<std_msgs::String const>);
void callback(std_msgs::StringConstPtr);
void callback(std_msgs::String::ConstPtr);
void callback(const std_msgs::String&);
void callback(std_msgs::String);
void callback(const ros::MessageEvent<std_msgs::String const>&);

MessageEvent类允许你在订阅的回调函数内获取信息的元数据。

其他常用的ros::Subscriber函数

1
2
3
4
5
6
// 注意: 只能判断话题是否发布,不能判断有没有发布消息
uint32_t getNumPublishers () const //获得连接订阅者的发布者个数

std::string getTopic () const //获得所订阅的话题

uint32_t getNumSubscribers () const  //获得连接发布者的订阅者个数

订阅话题后,回调函数的参数类型必须是对应话题的消息类型,否则能订阅成功但不运行回调。

发布者的回调函数

用于检测是否有订阅者

1
2
3
4
5
6
7
8
9
10
11
12
ros::Publisher pub = nh.advertise<std_msgs::Int8>("topic", 50, connectCb,disconnectCb);

// 回调函数如下
void connectCb(const ros::SingleSubscriberPublisher& pub)
{
ROS_INFO("topic Name: %s", pub.getTopic().c_str());
ROS_INFO("Subscriber Name: %s", pub.getSubscriberName().c_str());
}
void disconnectCb(const ros::SingleSubscriberPublisher& pub)
{
ROS_INFO("disconnectCb");
}

ROS的回调函数里定义count变量,居然报错ambiguous,看来以后最好不用count变量
image-20211227101811232

特殊需求

有时需要在订阅者的回调函数中发布消息,而且只发布一次,这时没必要用service混合topic,用static变量就可以做到:

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
static ros::Publisher *pubPtr;
static bool published = false;

void callback(const std_msgs::Int8ConstPtr& msg)
{
int num = msg->data;
ROS_INFO("msg: %d", num);
std_msgs::String str;
str.data = std::string(to_string(num) );
if(!published && num>8) // 在num>8时发布一次,以后再也不发布
{
pubPtr->publish(str);
published = true;
}
sleep(1);
}

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

ros::Subscriber sub = nh.subscribe<std_msgs::Int8>("topic",20,callback);
ros::Publisher pub = nh.advertise<std_msgs::String>("new",50,false);
pubPtr = &pub;
ros::spin();
return 0;
}

让订阅者停止订阅

ROS官方有这么一段话:

1
2
3
4
5
function   subscribe()  returns a Subscriber object that you must hold on to until you want to unsubscribe. When all copies of the Subscriber object go out of scope, this callback will automatically be unsubscribed from this topic.

function shutdown() Unsubscribe the callback associated with this Subscriber.
This method usually does not need to be explicitly called, as automatic shutdown happens when all copies of this Subscriber go out of scope
This method overrides the automatic reference counted unsubscribe, and immediately unsubscribes the callback associated with this Subscriber

根据说明,有以下三种方法:

  1. 一般让订阅者停止订阅的方法是等subscribe()生成的订阅者对象出作用域.

  2. 另一种方法就是explicitly调用shutdown(),典型方法是声明订阅者对象为全局变量,在回调函数中满足某条件时,对此对象调用shutdown(),但这样会直接退出程序,并不好

  3. 第三种方法其实与第二种类似,声明一个bool全局变量ok,订阅时执行

1
2
3
4
while(!ok)
{
ros::spinOnce();
}

在回调函数中满足某条件时,让ok=true

queue_size和buff_size

发布者的原型是这样的

1
2
3
4
Publisher ros::NodeHandle::advertise(const std::string & topic,
uint32_t queue_size,
bool latch = false
)

  • queue_size Maximum number of outgoing messages to be queued for delivery to subscribers。 消息队列是为了缓存发布节点发布的消息,一旦队列中消息的数量超过了queue_size,那么最先进入队列的(最老的)消息被舍弃。
  • latch (optional) If true, the last message published on this topic will be saved and sent to new subscribers when they connect

订阅者Subscriber ros::NodeHandle::subscribe函数里也有一个queue_size: Number of incoming messages to queue up for processing,消息队列是为了缓存节点接收到的信息,一旦自己处理的速度过慢,接收到的消息数量超过了queue_size,那么最先进入队列的(最老的)消息会被舍弃。

机器人应用中难免会遇到运算起来很费时间的操作,比如图像的特征提取、点云的匹配等等。有时候,我们需要在ROS的Subscriber的Callback回调函数中进行这些费时的操作。如果我们要是没有取舍的对于每个消息都调用一次回调函数,那么势必会导致计算越来越不实时,很有可能当下在处理的还是几秒以前的数据,之前的消息被舍弃了。所以,如果我们想只处理最新的消息,实际上只需要把两个queue_size都设置成1,那么系统不会缓存数据,自然处理的就是最新的消息。

rospy的订阅者还有个buff_size参数,要设置一个很大很大的数,这个缓冲区的大小是指消息队列使用的缓冲区物理内存空间大小。如果这个空间小于一个消息所需要的空间,比如消息是一副图片或者一帧点云,数据量超过了缓冲区的大小。这个时候为了保证通信不发生错误,就会触发网络通信的保护机制,TCP的Buffer会为你缓存消息。

但是roscpp里面没有buff_size参数,它获得 incoming message的大小,分配所需的缓冲区大小,然后一次读取整个incoming message

参考:
queue_size和buff_size的理解
ROS anwser的解释


ROS定时器

roscpp的定时器会计划在某一频率下执行一次回调操作,在订阅或服务中的回调函数队列机制中使用。

创建定时器

通过ros::NodeHandle::createTimer()创建: ros::Timer timer = nh.createTimer(ros::Duration(0.1), timerCallback); 其实有多个重载,这是用的比较多的一种。

回调函数只能是这种形式: void callback(const ros::TimerEvent&);
ros::TimerEvent结构体作为参数传入,它提供时间的相关信息,对于调试和配置非常有用。ros::TimerEvent结构体说明:

1
2
3
4
5
6
7
8
9
10
// 上次回调期望发生的时间
ros::Time last_expected
// 上次回调实际发生的时间
ros::Time last_real
// 本次回调期待发生的时间
ros::Time current_expected
// 本次回调实际发生的时间
ros::Time current_real
// 上次回调的时间间隔(结束时间-开始时间),是wall-clock时间。
ros::WallTime profile.last_duration

常用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void callback(const ros::TimerEvent&)
{
ROS_INFO("Callback triggered");
}

int main(int argc, char** argv)
{
ros::init(argc, argv, "test_timer_node");
signal(SIGINT, SigintHandler);

ros::NodeHandle nh;
ros::Timer timer = nh.createTimer(ros::Duration(0.5), callback);

ros::spin();

return(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
void odomTimerCb(const ros::TimerEvent& e);
void odomSubCb(const nav_msgs::OdometryConstPtr& m);
ros::Timer odomTimer ;
riki_msgs::HealthStatus msg;

int main(int argc, char** argv)
{
ros::init(argc,argv,"healthStatus");
ros::NodeHandle nh;
odomTimer = nh.createTimer(ros::Duration(5), odomTimerCb);
ros::Subscriber odomSub = nh.subscribe("odom", 1000, odomSubCb);

ros::Publisher pub = nh.advertise<riki_msgs::HealthStatus>("healthStatus", 1000);
ros::Rate loop_rate(1);
while(ros::ok())
{
pub.publish(msg);
ros::spinOnce();
loop_rate.sleep();
}
}

void odomTimerCb(const ros::TimerEvent& e)
{
msg.odomData = false;
}
void odomSubCb(const nav_msgs::OdometryConstPtr& m)
{
msg.odomData = true;
odomTimer.stop();
odomTimer.start();
}

未发布odom主题时,不会进入订阅者的回调函数odomSubCb,只进入定时器的回调odomTimerCb,此时消息成员odomData一直是false。
发布odom后,进入订阅者回调,此时odomData成为true,此时需要先停止计时器再打开。如果不停止,还会进入计时器回调,让消息成员赋值为false,实际造成赋值混乱。但是还得打开,因为一旦odom话题关闭了,就不会再进入订阅者回调,需要设法调用计时器回调,赋值false。订阅odom话题成功再打开计时器后,不会进入计时器回调。

参考: ROS与C++入门教程-Timers


ROS只发布和订阅一次消息

只发布一次消息

1
2
3
4
5
6
ros::init(argc,argv,"op_motor");
ros::NodeHandle nh;
ros::Publisher pub = nh.advertise<std_msgs::Bool>("Switch",1000);
//不必判断订阅者,直接等待1秒钟,只发布一次
sleep(1);
pub.publish(msg);

只发布一次消息就简单了,不必用while循环,直接发布就可以,但是最好也延时一次,防止订阅者的网络状况不好,当然如果不考虑有没有订阅者就不必延时了,不过这样没什么意义。


ROS只订阅一次消息

1
2
3
4
5
6
7
8
9
10
11
12
ros::init(argc,argv,"node");
ros::NodeHandle nh;
boost::shared_ptr<riki_msgs::Battery const> Edge; // 必须有const
Edge = ros::topic::waitForMessage<riki_msgs::Battery>("BMS",ros::Duration(0.1));
if(Edge != NULL)
{
riki_msgs::Battery bat = *Edge;
cout<<"battery:"<< bat.Ah <<endl;
cout<<"battery:"<< Edge->Ah <<endl;
}
else
cout<<"no message from topic BMS"<<endl;

waitForMessage是等待一次消息,造成的网络传输会出现延时。如果是获得消息后再发布,新话题的发布频率会明显下降 为了保证发布频率,还是尽量使用spin()

这里用到的函数是ros::topic::waitForMessage,没有涉及回调函数,它有4个重载,常用的是两个:

1
2
3
template<class M> boost::shared_ptr< M const >   waitForMessage (const std::string &topic, ros::Duration timeout)

template<class M> boost::shared_ptr< M const > waitForMessage (const std::string &topic, ros::NodeHandle &nh)

第一个是订阅话题topic,假如没有发布话题,那么等待时间timeout,超时后程序继续执行;第二个是如果没有发现话题topic,会一直阻塞,不能向下运行,除非执行rosnode kill。前一个用的比较多,注意时间不要设置太短,否则可能因为网络状况不好而订阅失败。

返回类型一看就知道是模板类的Boost共享指针,从这里就可以看出, 消息是以const Boost共享指针的形式传输,我们可以存储它而又不需要复制数据waitForMessage就是返回了订阅到的消息,取指向就可以获得消息成员,也可以直接用指针获得消息成员。
注意模板类型有const,否则不符合重载声明

回调函数中常见的MsgType::ConstPtr参数类型其实就是boost::shared_ptr<const MsgType>

MsgType::Ptr就是boost::shared_ptr<MsgType>

参考:

How do I publish exactly one message
ros::topic Namespace


ROS安装的常见问题

新的电脑安装ubuntu16和18很容易出毛病,无线网卡不能用,驱动也装不成功。显卡驱动有问题,不能改变屏幕亮度和外接显示器等等问题。所以但是装新系统就不会有这些问题,因此ROS最好也用noetic,当然更新的是ROS2

安装ROS报错

Ubuntu 16.04 安装ROS kinetic报错Depends: ros-kinetic-desktop but it is not going to be installed

  1. 删除ros-latest.list
  2. 修改源: vim /etc/apt/sources.list,改为下面内容,也就是中科大的安装源:
1
2
3
4
5
6
7
8
9
10
deb http://mirrors.ustc.edu.cn/ubuntu/ xenial main restricted universe multiverse
deb http://mirrors.ustc.edu.cn/ubuntu/ xenial-security main restricted universe multiverse
deb http://mirrors.ustc.edu.cn/ubuntu/ xenial-updates main restricted universe multiverse
deb http://mirrors.ustc.edu.cn/ubuntu/ xenial-proposed main restricted universe multiverse
deb http://mirrors.ustc.edu.cn/ubuntu/ xenial-backports main restricted universe multiverse
deb-src http://mirrors.ustc.edu.cn/ubuntu/ xenial main restricted universe multiverse
deb-src http://mirrors.ustc.edu.cn/ubuntu/ xenial-security main restricted universe multiverse
deb-src http://mirrors.ustc.edu.cn/ubuntu/ xenial-updates main restricted universe multiverse
deb-src http://mirrors.ustc.edu.cn/ubuntu/ xenial-proposed main restricted universe multiverse
deb-src http://mirrors.ustc.edu.cn/ubuntu/ xenial-backports main restricted universe multiverse

结果未变,再运行sudo apt-get -f install后安装了一个软件包,再安装ros-kinetic-desktop-full正常了

Failed to connect to raw.githubusercontent.com port 443

终端中输入vim /etc/hosts,加入185.199.110.133 raw.githubusercontent.com,或者从raw.Githubusercontent.com中选一个IP

退出后重新执行

安装时找不到软件包


从报错的内容找到安装包的地址,直接下载,有时一个软件包还有其他依赖包,都要下载

GPG error, signature were invalid


根据官方的文档,重新设置秘钥
sudo apt-key adv --keyserver 'hkp://keyserver.ubuntu.com:80' --recv-key C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654

编译缺少 libvtkproj4

编译时报错: No rule to make target '/usr/lib/x86_64-linux-gnu/libvtkproj4-6.2.so.6.2.0'

解决方法: sudo ln -s /usr/lib/x86_64-linux-gnu/libvtkCommonCore-6.2.so /usr/lib/x86_64-linux-gnu/libvtkproj4-6.2.so.6.2.0

E: 无法定位软件包 ros-kinetic-map-server

软件和更新其他软件里添加两项:

1
2
deb http://packages.ros.org/ros/ubuntu xenial main
deb http://packages.ros.org/ros-shadow-fixed/ubuntu xenial main

然后执行sudo apt-get update,应该就能安装了。
/etc/apt/sources.list里添加也是同样效果,但两个地方不能都添加,否则update时会报错

without authentic

有时安装某个包出现下面情况,
without authentic.png
在命令最后添加--allow-unauthenticated即可

安装ROS包出现404 Not Found

1
2
Err:1 http://packages.ros.org/ros/ubuntu <YOUR_UBUNTU_VERSION>/main amd64 <SOME_ROS_PKG> amd64 0.13.3-0xenial-20190320-132757-0800
404 Not Found [IP: 64.50.236.52 80]

运行sudo apt update也无效,打开上面的网页后,发现安装包的版本是更新的,看来我们的安装没有更新。
最后发现在2019年夏天,ROS官方更改了安装的Key,也就是第一次安装ROS时设置的Key。需要换成新的Key,步骤如下:

  • sudo apt-key del 421C365BD9FF1F717815A3895523BAEEB01FA116 (Kinetic及以后的版本)

  • sudo -E apt-key adv —keyserver ‘hkp://keyserver.ubuntu.com:80’ —recv-key C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654

  • sudo apt clean

  • sudo apt update

现在可以正常安装了,如果以后安装ROS,需要用这个新的Key,目前网上很多旧资料还是以前那个Key

参考:
apt update fails / cannot install pkgs
ROS报错“An error occurred during the signature verification”的解决办法

找不到***.deb

有时候我们解决了上面的问题,还是会出现下载ros包出现找不到的错误,这是因为ROS的包大部分都是在更新的,比如robot_state_publisher在2020年1月就更新了,如果没有apt-get update,是下载不到的

rqt_tf_tree 报警

执行rqt_tf_tree出现报警:

1
/opt/ros/melodic/lib/python2.7/dist-packages/rqt_tf_tree/dotcode_tf.py: 96: YAMLLoadWarning: calling yaml.load() without Loader=...is deprecated as the default Loader is unsafe. Please read https://msg.pyyaml.org/load for full details.

不理会也无影响,如果要解决,在dotcode_tf.py96行改为 yaml.load(input, Loader=yaml.FullLoader)

参考: yaml.load 问题

ImportError: No module named rospkg pyyaml

sudo pip3 install pyyaml 或者 sudo apt-get install python3-yaml

如果出现 Requirement already satisfied:,尝试下面命令

1
2
3
4
sudo apt-get install python3-catkin-pkg
sudo apt-get install python3-rospkg
sudo apt-get install python3-rospkg-modules
sudo apt-get install python3-catkin-pkg-modules

如果还有问题,那就是python版本问题,而不是安装的问题了,使用python3一般能解决,不过要explicitly指定,所以执行

1
sudo apt install python-is-python3

Could NOT find SDL (missing: SDL_LIBRARY SDL_INCLUDE_DIR)

在编译map_server时出错,是缺少了库文件:

1
2
sudo apt-get install libsdl-image1.2-dev
sudo apt-get install libsdl-dev

缺少某个包

使用catkin_make时,如果缺某个包,会提示缺*Config.cmake文件,平时都会使用apt install直接安装这个包,这并不是只添加了*Config.cmake文件,而是有一系列文件,所以不能直接从其他电脑复制。

liborocos-kdl_1.3.0

liborocos-kdl1.3_1.3.0的下载地址


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.


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的时候发生了什么