文章目录
1 说明
本文以 ros 消息的转换为例介绍如何使用 c++ 来:
- 接收 ros 消息并解析
- 发布 ros 消息
本案例运行的环境:
- Ubuntu 16.04LTS
- Qt Creator 4.11.1 (Based on Qt 5.14.1 (GCC 5.3.1 20160406 (Red Hat 5.3.1-6), 64 bit))
- ROS Kinetic
- c++ 11
2 创建 QT 项目
File
- New File or Project ...
(或Ctrl+N);
选择Qt Console Application
,然后点击Choose...
,
设置项目名和路径后点击Next >,设置编译工具为qmake
后一直点击Next >。
然后就自动生成了项目文件(show是我的项目名称,之后的演示是基于重新生成的tran_msg项目):
自动生成的文件如下:
show.pro
:
QT -= gui
CONFIG += c++11 console
CONFIG -= app_bundle
# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
main.cpp
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
main.cpp
:
#include <QCoreApplication>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
return a.exec();
}
2 将消息头文件复制到 QT 项目工作目录
消息的头文件是根据*.msg
生成的 c++ 头文件,一般在 catkin 工作空间下,具体路径为<path to workspace>/devel/include/<package name>/
,如果要创建自己的消息可以参考这篇文章:使用 catkin 的方式创建自定义的 ros 消息。
对于我来说,将以下消息的头文件复制进了工作目录,为了方便理解消息的字段结构,我也将消息文件本体*.msg
复制到了工作目录。
需要注意的是,如果消息中包涵自定义消息类型的字段,需要将这个自定义消息的头文件也拷贝到工作目录中。
例如,我们有两个*.msg
文件:
- A.msg
- B.msg
A 消息的内容为:
int 32 id
...
B b_object
...
此时 A 就嵌套了 B 消息,如果我们的项目中需要用到 A 消息,除了需要将 A 消息的头文件拷贝到项目的工作目录,同时也需要把 B 消息的头文件拷贝到项目的工作目录。
3 修改 .pro 文件
.pro
文件中包涵了编译的一些信息,需要进行修改,修改之后的文件为
QT += core
QT -= gui
CONFIG += c++11 console
CONFIG -= app_bundle
TEMPLATE = app
INCLUDEPATH += /opt/ros/indigo/include \
/opt/ros/kinetic/include \
./planner \
/usr/include/eigen3
DEPENDPATH += /opt/ros/indigo/include \
/opt/ros/kinetic/include
LIBS += -L/opt/ros/indigo/lib -L/opt/ros/kinetic/lib -L/usr/local/lib \
-lroscpp -lrospack -lpthread -lrosconsole \
-lrosconsole_log4cxx -lrosconsole_backend_interface \
-lxmlrpcpp -lroscpp_serialization -lrostime \
-lcpp_common -lroslib -lpthread
SOURCES += main.cpp
HEADERS += \
BodyIMU.h \
CarlaEgoVehicleControl.h \
CarlaEgoVehicleInfo.h \
CarlaImu.h \
ChasisStatus.h \
GPSPoint.h \
insMsg.h
具体来说,添加了复制进来的消息头文件:
HEADERS += \
BodyIMU.h \
CarlaEgoVehicleControl.h \
CarlaEgoVehicleInfo.h \
CarlaImu.h \
ChasisStatus.h \
GPSPoint.h \
insMsg.h
添加了一些依赖项/链接库的路径:
INCLUDEPATH += /opt/ros/indigo/include \
/opt/ros/kinetic/include \
./planner \
/usr/include/eigen3
DEPENDPATH += /opt/ros/indigo/include \
/opt/ros/kinetic/include
LIBS += -L/opt/ros/indigo/lib -L/opt/ros/kinetic/lib -L/usr/local/lib \
-lroscpp -lrospack -lpthread -lrosconsole \
-lrosconsole_log4cxx -lrosconsole_backend_interface \
-lxmlrpcpp -lroscpp_serialization -lrostime \
-lcpp_common -lroslib -lpthread
4 main.cpp 的准备工作
4.1 添加#include
项:
- ros 依赖
#include "ros/ros.h"
#include "ros/init.h"
- 消息头文件
#include "BodyIMU.h"
#include "CarlaImu.h"
(下面的示例就用到这两个消息)
4.2 ros 初始化
参考ros 初始化和关闭 (ros::init(); ros::shutdown())。
在 main 函数的开头进行 ros 的初始化,其命令格式为:
ros::init(argc,argv,"my_node_name");
或
ros::init(argc,argv,"my_node_name",ros::init_options::AnonymousName);
初始化选项有
NoSigintHandler
: 不安装 SIGINT 句柄。这种情况下你需要自己安装SIGINT句柄来保证节点在退出时候会正确的关闭 。 SIGINT的默认操作是终结一个进行,所以如果你想自己处理 SIGTERM 你需要使用这个选项。ros::init_options::NoSigintHandler
AnonymousName
: 匿名节点名称,增加一个随机数在节点名的后面,使得节点名唯一。ros::init_options::AnonymousName
NoRosout
:不向/rosout 话题广播 rosconsoleros::init_options::NoRosout
我这里直接使用第一种方式:
ros::init(argc,argv,"TransTool");
5 我的程序总览
下面是我的main.cpp
函数的内容,接下来的两个部分我将结合这个函数讲解 ros 消息的接收和发送。
#include "ros/ros.h"
#include "ros/init.h"
#include "BodyIMU.h"
#include "CarlaImu.h"
// 因为要在`main`和消息处理函数`CarlaImuInfoCallback`都使用这个变量,所以将其定义为全局变量
static ros::Publisher car_imu_info;
// 处理接收到的消息的函数
void CarlaImuInfoCallback(const trans_msg::CarlaImu &msg)
{
trans_msg::BodyIMU imu;
imu.acc_x = msg.linear_acceleration.x;
imu.acc_y = msg.linear_acceleration.y;
imu.acc_z = msg.linear_acceleration.z;
imu.rot_x = msg.angular_velocity.x;
imu.rot_y = msg.angular_velocity.y;
imu.rot_z = msg.angular_velocity.z;
imu.frame_num = msg.header.seq;
imu.time_stamp = msg.header.stamp.nsec;
// 发布消息
car_imu_info.publish(imu);
// printf("get imu\n");
}
int main(int argc, char *argv[])
{
ros::init(argc,argv,"TransTool");
ros::Subscriber carla_imu_info;
// ros::Publisher car_imu_info;
ros::NodeHandle NodeHandle;
car_imu_info = NodeHandle.advertise<trans_msg::BodyIMU>("carImu",10);
carla_imu_info = NodeHandle.subscribe("/carla/ego_vehicle/imu/imu1",10,CarlaImuInfoCallback);
// 定时接收消息
while(1)
{
ros::spinOnce();
usleep(100000);
}
}
6 订阅 ros 消息并解析
参考ROS自定义消息。
定义订阅实例:
ros::Subscriber carla_imu_info;
实例化NodeHandle
NodeHandle
是roscpp用于创建订阅者、发布者的接口。详情参见http://docs.ros.org/melodic/api/roscpp/html/classros_1_1NodeHandle.html
ros::NodeHandle NodeHandle;
然后就是订阅节点了,一般的用法如下:
void callback(const std_msgs::Empty::ConstPtr& message)
{
}
ros::Subscriber sub = handle.subscribe("my_topic", 1, callback);
对应于我的代码就是:
carla_imu_info = NodeHandle.subscribe("/carla/ego_vehicle/imu/imu1",10,CarlaImuInfoCallback);
// 处理接收到的消息
void CarlaImuInfoCallback(const trans_msg::CarlaImu &msg)
{
// 处理函数
}
说明:下面代码中的
trans_msg
是我自定消息所在package的名称,不太清楚可以打开消息头文件查看
接下来需要写一个 ros 消息回调处理函数,这个函数有两个选项:
ros::spin()
ros::spinOnce()
ros::spin()
自身就是一个循环体,也就是说代码执行到这个语句就进入ros::spin()
的内循环了,不断的触发,如果接收到订阅的节点的消息,就回调声明的消息处理函数;
ros::spinOnce()
的特性如其名,只监听一次消息,只触发一次回调函数,执行完它之后程序还会继续执行。如果要用ros::spinOnce()
持续监听消息,则需要将其放置在循环体中。
在我的代码里就是将其放置在循环体中来实现周期性监听消息的目的的:
while(1)
{
ros::spinOnce();
usleep(100000);
}
7 发布 ros 消息
先实例化一个Publisher
:
ros::Publisher car_imu_info;
然后设置publisher(下面这一句的含义是:发布名为carImu,消息类型为trans_msg::BodyIMU,队列长度10)
car_imu_info = NodeHandle.advertise<trans_msg::BodyIMU>("carImu",10);
下面是初始化一个 trans_msg::BodyIMU 类型的消息
trans_msg::BodyIMU imu;
imu.acc_x = msg.linear_acceleration.x;
imu.acc_y = msg.linear_acceleration.y;
imu.acc_z = msg.linear_acceleration.z;
imu.rot_x = msg.angular_velocity.x;
imu.rot_y = msg.angular_velocity.y;
imu.rot_z = msg.angular_velocity.z;
imu.frame_num = msg.header.seq;
imu.time_stamp = msg.header.stamp.nsec;
最后发布这个消息:
car_imu_info.publish(imu);
如果不是像我这样有一个消息来触发创建当前消息,可以用一个循环来持续发布消息,如下:
// 设置循环的频率
ros::Rate loop_rate(1);
while (ros::ok()) {
// 初始化std_msgs::String类型的消息
my_topic::topic_msg msg;
// 发布消息
pub_topic.publish(msg);
// 按照循环频率延时
loop_rate.sleep();
}
8 代码运行
使用QT运行 ros 项目的问题参见https://blog.csdn.net/amnesiagreen/article/details/108356826
我的程序的功能是将CARLA 仿真环境中车的IMU消息订阅并解析,然后转换为另一种格式的IMU消息。
我先运行CARLA的仿真环境,然后运行我的消息转换工程: