ROS入门 - 如何新建功能包 & 如何将现有程序作为功能包加入ROS

FunnyWii
FunnyWii
发布于 2023-05-06 / 31 阅读
0
0

ROS入门 - 如何新建功能包 & 如何将现有程序作为功能包加入ROS

写在前面:Ubuntu20.04 对应ROS版本为 Noetic。
每日一问:亲爱的领导什么时候才能让我用PCL和ROS2 并给我配一张显卡捏?

新建ROS工作空间和功能包

创建新的工作空间

# home目录递归创建工作空间,也可以是任何你想创建ws的地方
mkdir -p ~/catkin_ws/src
cd ~/catkin_ws/src

# 注:std_msgs 等为创建功能时指定的依赖
# 新功能包可以在创建时指定
# 新加入的功能包如果需新的依赖,可以在Cmakelist.txt和package.xml中手动添加
# pkg的名称需要注意全小写
catkin_create_pkg testpkg roscpp rospy std_msgs message_generation message_runtime

# 查看一下在 ~/catkin_ws/src 目录下自动生成了那些内容
# 工作空间的目录结构很关键,尤其是在后续添加新的package时
tree

└── testpkg
    ├── CMakeLists.txt
    ├── include
    │   └── testpkg
    ├── package.xml
    └── src

修改 cmakelist.txt和package.xml

cmakelist.txt

后面再改

package.xml

按照大部分教程,在新建package的时候,一般添加 roscpp rospy std_msgs 这三个依赖。要使用更多的功能,则需要手动在 package.xml 里添加。

比如上面的 message_generation message_runtime 可以以如下形式写入 package.xml:

<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>

<build_depend></build_depend> 标签定义了功能包中代码编译时所依赖的其他功能包。<exec_depend></exec_depend> 标签定义了功能包中可执行程序运行时所依赖的其他功能包自定义数据类型:话题消息 msg、服务数据 srv。

创建Publisher和Subscriber

发布和订阅消息,必须先知道要传递的消息类型。

std_msgs

是 ros 内置的标准消息类型,是最基础的消息类型。

单类型包括:

bool unsigned 8-bit int uint8_t bool
int8 signed 8-bit int int8_t int
uint8 unsigned 8-bit int uint8_t int
int16 signed 16-bit int int16_t int
uint16 unsigned 16-bit int uint16_t int
int32 signed 32-bit int int32_t int
uint32 unsigned 32-bit int uint32_t int
int64 signed 64-bit int int64_t long int
uint64 unsigned 64-bit int uint64_t long int
float32 32-bit IEEE float float float
float64 64-bit IEEE float double float
string ascii string std::string str bytes
time secs/nsecs unsigned 32-bit ints ros::Time rospy.Time
duration secs/nsecs signed 32-bit ints ros::Duration rospy.Duration

还包括了数组类型,上述单类型可以按照 ×××[] 的方式改为不定长数组类型

Primitive Type Serialization C++ Python2 / Python3
fixed-length no extra serialization boost::array/std::vector tuple
variable-length uint32 length prefix std::vector tuple
uint8[] see above as above str bytes
bool[] see above std::vector list of bool

Head类型,表示头,是一个结构体,内置了

uint32 seq # 表示数据流的 sequenceID
time stamp # 表示时间戳
string frame_id # 表示当前帧数据的帧头(帧序号)

Head 类型常用于记录每帧数据的时间和序列信息,用于记录历史数据的情形。

以上消息类型是其他各种类型的基础,其他各种消息类型的嵌套定义归根结底都依赖以上几种类型。

这里顺便推荐一个markdown的表格生成工具:https://tableconvert.com/zh-cn/markdown-generator 不然自己手敲表格实在是太痛苦了。

comm_msg

该类型是ros常用数据类型的集合,包括以下几种:actionlib_msgs、diagnostic_msgs、geometry_msgs、nav_msgs、sensor_msgs等。

geometry_msgs 是最常用的几何消息类型,定义了描述机器人状态的各种类型,比如点、速度、加速度、位姿等。

创建msg文件夹和文件

继续上面 tree之后的步骤。
此时需要使用 catkin_make对当前工作空间(workspace, ws)进行编译。编译完成后,会出现两个新文件夹 builddevel,此时这个 catkin_ws 文件夹才算的上是一个真正的工作空间。

tree
├── build
│   ├── atomic_configure
│   ├── bin
│   ├── catkin
│   ├── catkin_generated
│   ├── CATKIN_IGNORE
│   ├── catkin_make.cache
│   ├── CMakeCache.txt
│   ├── CMakeFiles
│   ├── cmake_install.cmake
│   ├── CTestConfiguration.ini
│   ├── CTestCustom.cmake
│   ├── CTestTestfile.cmake
│   ├── gtest
│   ├── Makefile
│   ├── testpkg
│   └── test_results
├── devel
│   ├── cmake.lock
│   ├── env.sh
│   ├── lib
│   ├── local_setup.bash
│   ├── local_setup.sh
│   ├── local_setup.zsh
│   ├── setup.bash
│   ├── setup.sh
│   ├── _setup_util.py
│   ├── setup.zsh
│   └── share
└── src
    ├── CMakeLists.txt -> /opt/ros/noetic/share/catkin/cmake/toplevel.cmake
    └── testpkg

在生成的package testpkg中新建一个 msg文件夹用来存放msg文件,这个文件夹名称不要改成其他的,然后再新建一个 pkg_msg.msg文件用来存放消息列表。在 .msg中写入以下测试消息。消息是可以嵌套的,比如你可以使用ROS中的消息类型,具体的可以参考官方wiki:
http://wiki.ros.org/std_msgs
http://wiki.ros.org/common_msgs

bool bool_msg
int8 int8_msg
uint8 uint8_msg
int16 int16_msg
uint16 uint16_msg
int32 int32_msg
uint32 uint32_msg
int64 int64_msg
uint64 uint64_msg
float32 float32_msg
float64 float64_msg
string string_msg
time time_msg
duration duration_msg

随后在功能包 testpkgsrc文件夹下新建两个.cpp文件 publisher.cpp subscriber.cpp,均写入一个空main函数(为了编译通过)。

#include <cstdlib>
#include "ros/ros.h"
#include <iostream>
#include <string>
#include <cstring>

int main(void)
{
    return 0;
}

CMakeLists.txt 需要修改如下几个地方(从上到下):

# 1. 调用find_package查找依赖的功能包
find_package(catkin REQUIRED COMPONENTS
  message_generation
  message_runtime
  roscpp
  rospy
  std_msgs
)

如果没有调用 find_package编译也通过了,是因为catkin把所有的package都整合在了一起,也就是说ws中的其他package调用了 find_package,本package也会调用同样的依赖,。但是如果单独编译,没有加 find_package编译就会出错。

# 2. 添加消息文件,指定.msg文件。
add_message_files(
  FILES
  pkg_msg.msg
)
# 3. 指定生成消息文件时的依赖项,其实如果消息文件中有依赖的话,就需要在这里设置
# 当前不需要std_msgs以外的其他依赖
generate_messages(
  DEPENDENCIES
  std_msgs
)
# 4. catkin_package声明相关的依赖
# 通过catkin_create_pkg命令设置依赖的话,这里不需要设置
catkin_package(
#  INCLUDE_DIRS include
#  LIBRARIES my_topic002
#  CATKIN_DEPENDS message_generation message_runtime roscpp rospy std_msgs
#  DEPENDS system_lib
)
# 5. 编写编译规则,生成的可执行文件名字、源码、链接库、依赖等
add_executable(publisher src/publisher.cpp)
target_link_libraries(publisher ${catkin_LIBRARIES})
add_dependencies(publisher ${PROJECT_NAME}_gencpp) 

add_executable(subscriber src/subscriber.cpp)
target_link_libraries(subscriber ${catkin_LIBRARIES})
add_dependencies(subscriber ${PROJECT_NAME}_gencpp)

上述提到需要修改的地方,均可以在 catkin_make自动生成的CMakeLists.txt找到建议写法,当然是以注释的方式出现在CMakeLists.txt中,你可以自己写新的,也可以取消注释并在原有的位置上修改。

再编译一次

$ catkin_make
……
[ 63%] Building CXX object testpkg/CMakeFiles/publisher.dir/src/publisher.cpp.o
[ 72%] Building CXX object testpkg/CMakeFiles/subscriber.dir/src/subscriber.cpp.o
[ 81%] Generating Python msg __init__.py for testpkg
[ 81%] Built target testpkg_generate_messages_eus
[ 81%] Built target testpkg_generate_messages_py
Scanning dependencies of target testpkg_generate_messages
[ 81%] Built target testpkg_generate_messages
[100%] Linking CXX executable /home/funnywii/Documents/catkin_ws/devel/lib/testpkg/subscriber
[100%] Linking CXX executable /home/funnywii/Documents/catkin_ws/devel/lib/testpkg/publisher
[100%] Built target subscriber
[100%] Built target publisher

此时可以在 devel/include中找到生成的 pkg_msg.h头文件,头文件名称和之前创建的 .msg一样。里面会生成我们之前在 .msg中声明的那些消息作为变量。

Publisher 和 Subscriber 节点代码

publisher.cpp中写入下面测试代码,代码来自 https://www.guyuehome.com/34142

/*
 * @Author: jiejie
 * @Github: https://github.com/jiejieTop
 * @Date: 2020-04-11 23:16:46
 * @LastEditTime: 2020-04-12 12:17:00
 * @Description: the code belongs to jiejie, please keep the author information and source code according to the license.
 */
#include <ros/ros.h> //ros/ros.h包括了使用ROS系统中最常见的公共部分所需的全部头文件。 
#include "testpkg/pkg_msg.h" //调用testpkg包下自定义的pkg_msg文件
#include "std_msgs/String.h" //引用位于std_msgs包里的std_msgs/String消息。这是从std_msgs包里的String.msg文件中自动生成的头文件。有关消息定义的更多信息,请参见msg页面。 

int main(int argc, char **argv)
{
	// ROS节点初始化
    // 这使得ROS可以通过命令行进行名称重映射——不过目前不重要。
    // 这也是我们给节点指定名称的地方。节点名在运行的系统中必须是唯一的。
    // 注意:名称必须是基本名称,例如不能包含任何斜杠/
	ros::init(argc, argv, "publisher_topic002");

	// 创建节点句柄
    // 创建的第一个NodeHandle实际上将执行节点的初始化
    // 最后一个被销毁的NodeHandle将清除节点所使用的任何资源。 
	ros::NodeHandle n;

	// 创建一个Publisher,发布名为 my_topic 的 topic ,消息类型为 testpkg::pkg_msg,队列长度100
    // 也就是说告诉主节点我们将要在 my_topic 话题上发布一个类型为 testpkg::pkg_msg 的消息。
    // 这会让主节点告诉任何正在监听 my_topic 的节点,我们将在这一话题上发布数据。第二个参数是发布队列的大小。
    // 在本例中,如果我们发布得太快,它将最多缓存100条消息,不然就会丢弃旧消息。 
    // NodeHandle::advertise()返回一个ros::Publisher对象,
    // 它有2个目的:其一,它包含一个publish()方法,可以将消息发布到创建它的话题上;其二,当超出范围时,它将自动取消这一宣告操作。
	ros::Publisher pub_topic = n.advertise<testpkg::pkg_msg>("my_topic",100);

	// 设置循环的频率 hz
    // 会记录从上次调用Rate::sleep()到现在已经有多长时间,并按频率休眠
	ros::Rate loop_rate(1);

    // 以下情况ros::ok()会返回false
    // 收到SIGINT信号(Ctrl+C)
    // 被另一个同名的节点踢出了网络
    // ros::shutdown()被程序的另一部分调用
    // 所有的ros::NodeHandles都已被销毁 
	while (ros::ok())   
	{
	    // 初始化std_msgs::String类型的消息
        // 是一个消息自适应的类在ROS上广播消息,该类由.msg文件生成
		testpkg::pkg_msg msg;
		msg.bool_msg = true;
		msg.string_msg = "hello world!";
		msg.float32_msg = 6.66;
        msg.float64_msg = 666.666;
		msg.int8_msg = -66;
		msg.int16_msg = -666;
		msg.int32_msg = -6666;
        msg.int64_msg = -66666;
        msg.uint8_msg = 66;
        msg.uint16_msg = 666;
        msg.uint32_msg = 6666;
        msg.uint64_msg = 66666;
        msg.time_msg = ros::Time::now();
  
	    // 发布消息 把这个信息广播给任何已连接的节点
		pub_topic.publish(msg);

        // 此处调用ros::spinOnce()对于这个简单程序来说没啥必要,因为我们没有接收任何回调。
        // 然而,如果要在这个程序中添加订阅,但此处没有ros::spinOnce()的话,回调函数将永远不会被调用。
        ros::spinOnce();

	    // 按照循环频率延时
	    loop_rate.sleep();
	}

	return 0;
}

subscriber.cpp中写入:

#include <ros/ros.h>
#include "testpkg/pkg_msg.h"
#include <std_msgs/String.h>

// 接收到订阅的消息后,会进入消息回调函数
// 当有新消息到达 my_topic 话题时它就会被调用。
// 该消息是用boost shared_ptr智能指针传递的,这意味着你可以根据需要存储它,即不用担心它在下面被删除,又不必复制底层(underlying)数据。 
void callback(const testpkg::pkg_msg::ConstPtr & msg)
{
    // 将接收到的消息打印出来 !注意占位符
    ROS_INFO("------------------ msg ---------------------");
    ROS_INFO("bool_msg:    [%d]", msg->bool_msg);
    ROS_INFO("string_msg:  [%s]", msg->string_msg.c_str());
    ROS_INFO("float32_msg: [%f]", msg->float32_msg);
    ROS_INFO("float64_msg: [%f]", msg->float64_msg);
    ROS_INFO("int8_msg:    [%d]", msg->int8_msg);
    ROS_INFO("int16_msg:   [%d]", msg->int16_msg);
    ROS_INFO("int32_msg:   [%d]", msg->int32_msg);
    ROS_INFO("int64_msg:   [%ld]", msg->int64_msg);
    ROS_INFO("uint8_msg:   [%d]", msg->uint8_msg);
    ROS_INFO("uint16_msg:  [%d]", msg->uint16_msg);
    ROS_INFO("uint32_msg:  [%d]", msg->uint32_msg);
    ROS_INFO("uint64_msg:  [%ld]", msg->uint64_msg);
    ROS_INFO("time_msg:    [%d.%d]", msg->time_msg.sec, msg->time_msg.nsec);
}

int main(int argc, char **argv)
{
    // 初始化ROS节点
    ros::init(argc, argv, "subscriber");

    // 创建节点句柄
    ros::NodeHandle n;

    // 创建一个Subscriber,订阅名为my_topic的消息,注册回调函数 callback
    // 通过主节点订阅 my_topic 话题。每当有新消息到达时,ROS将调用 callback()函数。
    // NodeHandle::subscribe()返回一个ros::Subscriber对象,你必须保持它,除非想取消订阅。当Subscriber对象被析构,它将自动从chatter话题取消订阅。 
    ros::Subscriber sub_topic = n.subscribe<testpkg::pkg_msg>("my_topic", 100, callback);

    // 循环等待回调函数
    // ros::spin()启动一个自循环,它会尽可能快地调用消息回调函数
    ros::spin();

    return 0;
}

使用 catkin_make重新编译后,在一个terminal中运行 roscore
再打开两个terminal,均输入 source devel/setup.bash,每次打开新的terminal都需要输入这行命令。
分别再输入 rosrun testpkg publisher rosrun testpkg subscriber

roscore.png

Figure1 运行roscore节点管理器

rospublisher.png

Figure2 运行publisher节点

rossubscriber.png

Figure3 运行subscriber节点

传递数组消息

加入现有程序作为ros功能包

比如说我现在有一个已经能正常运行的project,如何作为一个功能包加入现有的ws中呢?比如下面这个我用来测试功能用的project:

funnywii@funnywii-3020S:~/Documents/funnywiitest$ tree -L 1
.
├── build
├── CalibImgs
├── CamCalibPara.yaml
├── cmake-build-debug
├── CMakeLists.txt
├── config_files
├── main.cpp
├── model
├── sample.mp4
├── src
├── test.avi
└── yolov5s.onnx

6 directories, 6 files

先新建一个包 catkin_create_pkg funnywiitest roscpp rospy std_msgs message_generation message_runtime

接下来可以把整个 funnywiitest文件夹,粘贴至 catkin_ws/src下,但是要注意。CMakelist.txt需要改写,将新成生的CMakelist.txt 和 原有项目的CMakelist.txt内容合并一下。

下面是我的CMakelist.txt 删除了无关的内容和注释,可以看到有几个很关键的地方需要修改。

  1. 自己之前需要的库,我这里是opencv
  2. 在之前提到的和订阅相关的依赖
  3. !非常重要: 务必为当前功能包添加 target_link_libraries(funnywiitest ${catkin_LIBRARIES})add_dependencies(funnywiitest ${PROJECT_NAME}_gencpp) 库和依赖。否则ros相关功能都不能使用
cmake_minimum_required(VERSION 3.0.2)
project(funnywiitest)

find_package( OpenCV REQUIRED )
include_directories(/usr/local/include)
include_directories(${OpenCV_INCLUDE_DIRS})

## Compile as C++11, supported in ROS Kinetic and newer
# add_compile_options(-std=c++11)

## Find catkin macros and libraries
## if COMPONENTS list like find_package(catkin REQUIRED COMPONENTS xyz)
## is used, also find other catkin packages
find_package(catkin REQUIRED COMPONENTS
  message_generation
  message_runtime
  roscpp
  rospy
  std_msgs
)

################################################
## Declare ROS messages, services and actions ##
################################################

## Generate messages in the 'msg' folder
add_message_files(
  FILES
  funnywii_msg.msg
)

## Generate added messages and services with any dependencies listed here
generate_messages(
  DEPENDENCIES
  std_msgs
)

################################################
## Declare ROS dynamic reconfigure parameters ##
################################################

###################################
## catkin specific configuration ##
###################################
catkin_package(
#  INCLUDE_DIRS include
#  LIBRARIES funnywiitest
#  CATKIN_DEPENDS message_generation message_runtime roscpp rospy std_msgs
#  DEPENDS system_lib
)

###########
## Build ##
###########

## Specify additional locations of header files
## Your package locations should be listed before other locations
include_directories(
# include
  ${catkin_INCLUDE_DIRS}
)


## Add cmake target dependencies of the library
## as an example, code may need to be generated before libraries
## either from message generation or dynamic reconfigure
# add_dependencies(${PROJECT_NAME} ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})

#############
## Install ##
#############

#############
## Testing ##
#############

add_executable( funnywiitest src/Camera_Calib.cpp src/ReadYaml.cpp main.cpp src/MyHeaders.h
        src/ExtrinsicParaCalib.cpp src/SaveTxt.cpp src/YoloV5.h src/YoloV5.cpp src/CppLearning.cpp src/CppLearning.h)
target_link_libraries( funnywiitest ${OpenCV_LIBS} )
target_link_libraries(funnywiitest ${YAMLCPP_LIBRARIES})
target_link_libraries(funnywiitest ${catkin_LIBRARIES})

add_executable(funnywiisubscriber src/funnywiisubscriber.cpp)
target_link_libraries(funnywiisubscriber ${catkin_LIBRARIES})

add_dependencies(funnywiitest ${PROJECT_NAME}_gencpp) 
add_dependencies(funnywiisubscriber ${PROJECT_NAME}_gencpp)

然后在 catkin_ws文件夹下执行 catkin_make编译。

如要使用订阅等功能,在 funnywiitest文件夹下新建 msg文件夹,接下来的操作和上面新建ws后的操作类似。

参考文章:

[1] https://zhuanlan.zhihu.com/p/94579820
[2] https://blog.csdn.net/qq_30193419/article/details/111867500
[3] http://wiki.ros.org/cn/ROS/Tutorials/WritingPublisherSubscriber%28c%2B%2B%29
[4] https://www.guyuehome.com/34142
[5] http://wiki.ros.org/std_msgs
[6] http://wiki.ros.org/common_msgs


评论