ROS 編譯系統(tǒng) catkin 詳解

引言

最近項(xiàng)目中遇到一個(gè)需求:將 C++ 程序 (不是 ROS node,只是普通的 C++ 程序)中的變量發(fā)布到 ROS topic 上,以便 ROS 中的其他 node 進(jìn)行后續(xù)處理。

原 C++ 程序比較復(fù)雜,我們希望盡量少修改原程序,只要輸出其中某些變量的值即可,不要大規(guī)模改寫成 ROS node 的形式,不要新建 ROS package。

在以往使用 ROS 的過程中,我們一般是借助 catkin 來編譯 ROS node C++ 程序。這可以看成是將 C++ 程序放入 ROS 框架中,以 ROS 的標(biāo)準(zhǔn)形式來編譯?,F(xiàn)在這個(gè)項(xiàng)目需求正好相反,我們要將 ROS 的相關(guān)庫(library)嵌入到普通 C++ 程序中,采用 C++ 標(biāo)準(zhǔn)的 cmake 方式來編譯。這就要求我們對 cmake 和 catkin 的關(guān)系有比較深入的了解。

在查找資料的過程中,我們發(fā)現(xiàn)了一篇講解 catkin 編譯系統(tǒng)的文章,從最基本的命令行編譯方式,到 makefile 文件編譯,再到 catkin 編譯,每一步發(fā)展的必要性都講解的很清楚,看完之后,我們對 catkin 有了更深入的認(rèn)識。

本文是一篇學(xué)習(xí)筆記,也可以看成是對原文的意譯。

對原文感興趣的讀者可以移步這里 http://jbohren.com/tutorials/2014-02-12-gentle-catkin-intro/

預(yù)安裝

原文例子中使用了 hydro 版本的 ROS,現(xiàn)在看來比較古老了,這里替換為 kinetic 版本。

  • Ubuntu 16.04
  • ROS kinetic (base 即可)

從最簡單的例子開始

首先創(chuàng)建一個(gè)文件夾 hello_world_tutorial,存放我們的程序

mkdir hello_world_tutorial
cd hello_world_tutorial

創(chuàng)建 C++ 源文件,名為 hello_world_node.cpp

// 為了與 ROS 交互,需要調(diào)用 ROS C++ APIs
#include <ros/ros.h>

// 標(biāo)準(zhǔn)的 C++ main 函數(shù)
int main(int argc, char** argv) {

  // 該命令告訴 ROS 初始化了一個(gè) node,名為 hello_world_node 
  ros::init(argc, argv, "hello_world_node");

  // 在一般的 ROS node 程序中,我們會(huì)用 ros::NodeHandle nh 來啟動(dòng) node 程序,
  // ros::NodeHandle nh 默認(rèn)會(huì)調(diào)用 ros::start() 函數(shù),程序關(guān)閉時(shí)也會(huì)自動(dòng)調(diào)用 ros::shutdown() 函數(shù)。
  // 我們也可以直接通過 ros::start() 和 ros::shutdown() 來手動(dòng)控制 node 的開啟和關(guān)閉
  ros::start();

  // 顯示 hello, world! 信息
  ROS_INFO_STREAM("Hello, world!");

  // 用 ros::spin() 保持該程序運(yùn)行,一直等待處理 subscribe 的數(shù)據(jù)
  // 由于該程序并沒有 sub,所以就是簡單的保持程序不退出而已, 直到接受到終止信號 SIGINT (ctrl-c)
  ros::spin();

  // 關(guān)閉 node 程序
  ros::shutdown();

  // 結(jié)束主程序
  return 0;
}

下邊將 C++ 源文件編譯成可執(zhí)行文件

g++ hello_world_node.cpp -o hello_world_node -I/opt/ros/kinetic/include -L/opt/ros/kinetic/lib -Wl,-rpath,/opt/ros/kinetic/lib -lroscpp -lrosconsole

各參數(shù)含義

  • -I<dir> 指定頭文件的搜索路徑
  • -L<dir> 指定靜態(tài)庫的搜索路徑
  • -Wl,-rpath,/opt/ros/kinetic/lib 指定共享庫的搜索路徑
  • -lroscpp -lrosconsole 指定需要鏈接的具體的庫文件

編譯之后,生成 hello_world_node 可執(zhí)行文件。由于程序中生成了 ROS node,而 ROS node 需要與 ROS master 進(jìn)行通訊注冊,否則會(huì)報(bào)錯(cuò)。因此為了正常運(yùn)行程序,需要先開啟 ROS master

roscore

然后運(yùn)行 hello_world_node

./hello_world_node

如果一切順利,應(yīng)該顯示類似如下信息:

[ INFO] [1561908777.116073864]: Hello, world!

上述編譯方式擴(kuò)展性很差,對于如此簡單的 hello_world 程序,需要設(shè)置的參數(shù)已經(jīng)這么多了。而且在 terminal 中書寫比較麻煩,修改也不方便。

改進(jìn):使用 Makefile 文件進(jìn)行編譯

Makefile 編譯方式是將上述編譯命令和參數(shù)設(shè)置放入一個(gè)文件中,然后基于該文件,完成編譯過程。Makefile 文件有自己的一套語法規(guī)則,可以實(shí)現(xiàn)批量、相對自動(dòng)化的編譯。

與前述 hello world 程序?qū)?yīng)的 Makefile 文件內(nèi)容如下:

# 聲明要使用的編譯器
CC=g++
# 聲明一些變量,實(shí)際上就是對應(yīng)上述搜索路徑設(shè)置
CFLAGS=-I/opt/ros/kinetic/include
LDFLAGS=-L/opt/ros/kinetic/lib -Wl,-rpath,/opt/ros/kinetic/lib -lroscpp -lrosconsole

# % 作為通配符,代表對一類滿足條件的文件進(jìn)行操作
# 這是由源文件 *.cpp 編譯成目標(biāo)文件 *.o 的操作
%.o: %.cpp
  $(CC) -c -o $@ $< $(CFLAGS)  

# 也可以不用通配符,具體寫出要編譯的文件
# 這是由目標(biāo)文件 *.o 通過鏈接 (linking) 操作生成最終的可執(zhí)行文件
hello_world_node: hello_world_node.o 
  $(CC) -o hello_world_node hello_world_node.o $(LDFLAGS)

對于 Makefile 的介紹,可以參考這里。

Makefile 文件的基本格式是

target: pre-req
    command

即,希望生成 target 文件,依賴 pre-req 文件,通過 command 命令實(shí)現(xiàn)。

需要注意的是,Makefile 要求 command 那一行開頭用 TAB 鍵縮進(jìn),不能用空格,如果出現(xiàn)如下報(bào)錯(cuò):

makefile:...: *** missing separator. Stop

說明誤用了空格鍵。如果你跟我一樣用的是 vs code 編輯器,可以在右下角選擇 Indent Using Tabs。

將上述 Makefile 文件放在與 hello_world_node.cpp 同一路徑下,然后編譯

make  # 或者指明某個(gè) target 編譯任務(wù),如:  make hello_world_node

Makefile 編譯方式相比于剛才的命令行編譯方式有如下優(yōu)點(diǎn):

  • 在設(shè)置好 Makefile 的前提下,編譯命令更簡單,只需要 make,不必每次都輸入一長串命令
  • Makefile 中將編譯和鏈接分開進(jìn)行,如果項(xiàng)目中包含多個(gè) c++ 源文件,改動(dòng)了其中的一個(gè),只需要重新生成改動(dòng)文件的目標(biāo)文件 (*.o) 即可,其他源文件不需要重新編譯,然后基于更新之后的目標(biāo)文件,生成新的可執(zhí)行文件。也就是說,如果源文件沒有改變,就不會(huì)浪費(fèi)時(shí)間更新目標(biāo)文件。

在書寫上邊的 Makefile 文件時(shí),我們依然要明確設(shè)定頭文件和 library 的搜索路徑。為了進(jìn)一步簡化這個(gè)過程,我們可以在 Makefile 中使用 pkg-config 設(shè)置搜索路徑。

改進(jìn):在 Makefile 中使用 pkg-config 設(shè)置搜索路徑

實(shí)際上,library 對應(yīng)的搜索路徑包含在與該 library 對應(yīng)的.pc 文件中,例如
roscpp library 對應(yīng)的 .pc 文件為 /opt/ros/kinetic/lib/pkgconfig/roscpp.pc,里面內(nèi)容如下

prefix=/opt/ros/kinetic

Name: roscpp
Description: Description of roscpp
Version: 1.12.14
Cflags: -I/opt/ros/kinetic/include -I/usr/include
Libs: -L/opt/ros/kinetic/lib -lroscpp -lpthread /usr/lib/x86_64-linux-gnu/libboost_chrono.so  ...
Requires: cpp_common message_runtime rosconsole roscpp_serialization ...

可以看出,這個(gè) .pc 文件里面的 CflagsLibs 條目就是調(diào)用 roscpp 時(shí)要設(shè)置的路徑信息。我們可以通過 pkg-config 這個(gè)工具查找 roscpp.pc 文件,然后提取其中的路徑信息,放入 Makefile 中,這樣就避免了手動(dòng)輸入。例如

$ pkg-config --cflags roscpp

-I/opt/ros/kinetic/include

$ pkg-config --libs roscpp

-L/opt/ros/kinetic/lib -lroscpp -lpthread /usr/lib/x86_64-linux-gnu/libboost_chrono.so ...

因此,我們可以改寫 Makefile 文件如下:

CC=g++

# 通過 pkg-config 設(shè)置相應(yīng)的路徑信息
CFLAGS=$(shell pkg-config --cflags roscpp)
LDFLAGS=$(shell pkg-config --libs roscpp)

%.o:  %.cpp
    $(CC) -c  -o  $@  $<  $(CFLAGS) 
hello_world_node: hello_world_node.o 
    $(CC) -o hello_world_node hello_world_node.o $(LDFLAGS)

然后依然用 make 命令編譯文件,與前邊編譯方式相同,最終也是生成 hello_world_node 可執(zhí)行文件。

在使用 pkg-config 時(shí)需要確保它能夠找到相應(yīng)的 library。pkg-config 有自己的搜索 library 的路徑,存放在環(huán)境變量 PKG_CONFIG_PATH中,可以通過 echo 命令查看

echo $PKG_CONFIG_PATH

如果我們安裝完 ROS,并且運(yùn)行了source /opt/ros/kinetic/setup.bash, ROS 相關(guān)的 library 對應(yīng)的 .pc 文件就被加入了 pkg-config 的搜索路徑。通過 pkg-config <library> 就可以搜到相應(yīng)的信息。

盡管 pkg-config 簡化了 Makefile 中設(shè)置頭文件和 library 路徑的過程,但是 Makefile 文件中后續(xù)的編譯過程依然需要手動(dòng)設(shè)置。另外這里手動(dòng)書寫的編譯命令是與操作系統(tǒng)平臺(tái)相關(guān)的,Linux 中的編譯命令不能在 Windows 中使用,這就導(dǎo)致 Makefile 不能跨平臺(tái)使用。

改進(jìn): CMake 跨平臺(tái)編譯方式

CMake 的一個(gè)功能是自動(dòng)生成 Makefile 文件。另外,CMake 可以在 Linux 、Windows 和 Mac OS 上使用。

要使用 CMake,首先要?jiǎng)?chuàng)建一個(gè) CMakeLists.txt 文件,包含必要的編譯設(shè)置。
與上述 hello_world_node 例子對應(yīng)的 CMakeLists.txt 內(nèi)容如下:

# 聲明 CMake API 版本
cmake_minimum_required(VERSION 2.8)

# 聲明項(xiàng)目名稱
project(hello_world_tutorial)

# 搜索依賴 library (即 roscpp) 的信息
# 與 pkg-config 功能類似,但可以跨平臺(tái)使用
# pkg-config 查找 .pc 配置文件,而 find_package 查找 .cmake 配置文件
find_package(roscpp REQUIRED)

# 搜索 roscpp 中調(diào)用的頭文件
include_directories(${roscpp_INCLUDE_DIRS})

# 設(shè)置待生成的可執(zhí)行文件名字 
add_executable(hello_world_node hello_world_node.cpp)

# 設(shè)置編譯過程中 linking library 
target_link_libraries(hello_world_node ${roscpp_LIBRARIES})

其中 find_package(roscpp REQUIRED) 會(huì)自動(dòng)定義幾個(gè)變量,包括 roscpp_INCLUDE_DIRS,roscpp_LIBRARY_DIRS,roscpp_LIBRARIES。在 CMakeLists.txt 中可以直接使用這些變量。REQUIRED 參數(shù)的作用是在找不到相應(yīng) library 時(shí)停止并報(bào)錯(cuò),提示

-- Configuring incomplete, errors occurred!
See also ".../CMakeFiles/CMakeOutput.log".

如果不加 REQUIRED,則只會(huì)提示找不到 library,整個(gè)過程并不會(huì)停止,顯示信息如下:

-- Configuring done
-- Generating done
-- Build files have been written to: ...

盡管顯示各種 done,由于沒有找到必要的 library ,后續(xù)的編譯肯定會(huì)不成功。

通過 CMakeLists.txt 進(jìn)行編譯時(shí)會(huì)產(chǎn)生一些中間文件,如果都放在 .cpp 源文件目錄下,會(huì)顯得很雜亂。最好單獨(dú)建一個(gè)文件夾,存放這些編譯文件。例如在 .cpp 源文件和 CMakeLists.txt 同一路徑下新建 build 文件夾。新的路徑結(jié)構(gòu)如下:

├── build
├── CMakeLists.txt
└── hello_world_node.cpp

CMakeLists.txt 中的 find_package 之所以能找到相應(yīng)的 library,是因?yàn)橐呀?jīng)設(shè)置了搜索路徑,存放在環(huán)境變量 CMAKE_PREFIX_PATH 中,通過 echo $CMAKE_PREFIX_PATH 可以顯示當(dāng)前 find_package 使用的搜索路徑。在安裝完 ROS 之后,source 命令會(huì)自動(dòng)將 ROS 相關(guān)的 library 加入上述搜索路徑中。

通過 cmakeCMakeLists.txt 自動(dòng)生成編譯文件 Makefile:

cd build       # 進(jìn)入剛才創(chuàng)建的 build 文件夾
cmake ..      # 運(yùn)行 cmake,它會(huì)調(diào)用上一層路徑中的 CMakeLists.txt 文件 

運(yùn)行完上述命令以后,產(chǎn)生了一些新文件,路徑結(jié)構(gòu)如下:

├── build
│   ├── CMakeCache.txt
│   ├── CMakeFiles
│   ├── cmake_install.cmake
│   └── Makefile
├── CMakeLists.txt
└── hello_world_node.cpp

可以看到自動(dòng)產(chǎn)生了 Makefile ,此時(shí)就可以用 make 命令編譯文件了。

這里借張圖展示一下 CMake 編譯方式跨平臺(tái)的能力
( From: https://cgold.readthedocs.io/en/latest/overview/cmake-can.html)

cmake.png

到了這里,我們就已經(jīng)解決了最初的項(xiàng)目需求:讓 C++ 程序?qū)?nèi)部變量以 ros topic 的形式發(fā)布出來?;静襟E:
1 . 改寫 C++ 程序,加入 ROS 元素,如 ros 頭文件,msg 頭文件等,設(shè)置 ros::init,
ros::NodeHandle ,pub msg 等,這些 ROS 元素可以使 C++ 程序在 ROS master 中以 ROS node 的形式注冊。
2. 我們原來的 C++ 程序有自己的 CMakeLists.txt 文件,在其中添加依賴的 ROS library。
3. 用基本的 cmake 方式編譯即可。

改進(jìn):針對 ROS 系統(tǒng)的 Catkin 編譯方式

ROS 的 Catkin 編譯系統(tǒng)的一個(gè)特點(diǎn)是將程序做成 package (稱為 catkin package 或者 ROS package) 的形式,可以理解成模塊化。典型的 ROS workspace 中包含 src, build, devel 三個(gè)文件夾,在分享時(shí)只需要分享 src 中的某個(gè) package 即可,所有的編譯信息都在此 package 中。一個(gè) package 在編譯時(shí)可以指定依賴于另一個(gè) package。
另外,由于 ROS 中程序以及 library 變動(dòng)比較頻繁,不太適合在整個(gè)系統(tǒng)層面安裝編譯之后的文件,通過 source devel 文件中的 setup.bash 文件可以告知系統(tǒng)去哪里查找相應(yīng)的文件,避免了系統(tǒng)級的安裝 。

要構(gòu)造 ROS package,我們首先要修改 CMakeLists.txt 文件如下:

cmake_minimum_required(VERSION 2.8)
project(hello_world_tutorial)

#  要用到 catkin
find_package(catkin REQUIRED)

# 聲明該項(xiàng)目為一個(gè) catkin package
catkin_package()

find_package(roscpp REQUIRED)
include_directories(${roscpp_INCLUDE_DIRS})
add_executable(hello_world_node hello_world_node.cpp)
target_link_libraries(hello_world_node ${roscpp_LIBRARIES})

另外,還需要添加一個(gè) package.xml 文件,指明該 package 在編譯和運(yùn)行時(shí)依賴于哪些其他 package,同時(shí)也包含該 package 的一些描述信息,如作者、版本等。內(nèi)容如下:

<package>
  <name>hello_world_tutorial</name>
  <maintainer email="you@example.com">Your Name</maintainer>
  <description>
    A ROS tutorial.
  </description>
  <version>0.0.0</version>
  <license>BSD</license>

  <!-- Required by Catkin -->
  <buildtool_depend>catkin</buildtool_depend>

  <!-- Package Dependencies -->
  <build_depend>roscpp</build_depend>
  <run_depend>roscpp</run_depend>
</package>

現(xiàn)在路徑結(jié)構(gòu)如下:

├── build
├── CMakeLists.txt
├── hello_world_node.cpp
└── package.xml

跟之前一樣,進(jìn)入 build 文件夾中,用 cmake + make 方式編譯

cd build
cmake ..
make

編譯結(jié)束之后會(huì)發(fā)現(xiàn),并沒有在 build 根目錄下生成可執(zhí)行文件。與普通的 cmake 編譯不同,catkin 編譯會(huì)生成一個(gè) devel 文件夾,這里包含了生成的可執(zhí)行文件,以及作為 library 使用的配置文件 .pc.cmake。

對于我們的 hello_world_node package 來說,上述文件路徑如下:

  • 可執(zhí)行文件:devel/lib/hello_world_tutorial/hello_world_node
  • .pc 配置文件:devel/lib/pkgconfig/hello_world_tutorial.pc
  • .cmake 配置文件:devel/share/hello_world_tutorial/cmake/hello_world_tutorialConfig.cmake

當(dāng)作為 library 使用時(shí),只需要將路徑 .../devel/lib/pkgconfig 添加到 PKG_CONFIG_PATH 環(huán)境變量中,或者將 .../devel 添加到 CMAKE_PREFIX_PATH 變量中。實(shí)際上,我們不需要手動(dòng)設(shè)置這些環(huán)境變量,只需要通過 source devel 文件夾下的 setup.bash 文件即可,source setup.bash 不僅添加了以上兩個(gè)環(huán)境變量,還有諸如 ROS_PACKAGE_PATH,PYTHONPATH等。

source 之后,由于該 package 加入了 ROS_PACKAGE_PATH,此時(shí)可以通過 ROS 相關(guān)的命令對該 package 進(jìn)行操作,如 rospack find ..., rosrun <package> <exe>, roscd <package> 等。

為了更有條例地存放不同類型的文件,可以建立三個(gè)文件夾 src, build, devel,其中 src 存放源文件,源文件又以 package 為單位分別存放,build 存放編譯過程中的中間文件,devel 存放最終生成的可執(zhí)行文件和配置文件。這就是所謂的 out-of-source 編譯方式。在分享、發(fā)布程序時(shí),我們可以很清楚的知道哪些是必要的源文件,哪些是最終生成的可執(zhí)行文件和 library,哪些是作為副產(chǎn)品存在的中間文件。

路徑結(jié)構(gòu)如下:

├── build
├── devel
└── src
    └── hello_world_tutorial
        ├── CMakeLists.txt
        ├── hello_world_node.cpp
        └── package.xml

在做了以上路徑設(shè)置之后,在編譯時(shí),我們就需要特別指定各類文件對應(yīng)的路徑:

cd build
cmake ../src/hello_world_tutorial  -DCATKIN_DEVEL_PREFIX=../devel
make

catkin 的特點(diǎn)還體現(xiàn)在編譯多個(gè) package 中。

我們可以在 src 文件夾中再添加一個(gè) catkin package,這里我們就直接從網(wǎng)上下載一個(gè)簡單的 package:

git clone https://github.com/ros/robot_state_publisher.git -b kinetic-devel

現(xiàn)在路徑結(jié)構(gòu)如下:

├── build
├── devel
└── src
    ├── hello_world_tutorial
    │   ├── CMakeLists.txt
    │   ├── hello_world_node.cpp
    │   └── package.xml
    └── robot_state_publisher
        ├── CHANGELOG.rst
        ├── CMakeLists.txt
        ├── doc.dox
        ├── include
        ├── package.xml
        ├── src
        └── test

上述兩個(gè) package 各有一個(gè) CMakeLists.txt 文件,按照普通的 cmake 方法,我們不能同時(shí)編譯它們。catkin 為我們提供了一個(gè)更高層的 CMakeLists.txt 文件,可以從 ROS 安裝文件夾中以超鏈接的形式復(fù)制過來,放在更高層的 src 目錄下:

cd src
ln  -s  /opt/ros/kinetic/share/catkin/cmake/toplevel.cmake  CMakeLists.txt

實(shí)際上,ROS 為我們提供了專門的命令,實(shí)現(xiàn)上述操作:

cd src
catkin_init_workspace src

此時(shí),路徑結(jié)構(gòu)如下:

├── build
├── devel
└── src
    ├── CMakeLists.txt -> /opt/ros/kinetic/share/catkin/cmake/toplevel.cmake
    ├── hello_world_tutorial
    └── robot_state_publisher

這就是典型的 ROS workspace 的結(jié)構(gòu)。
此時(shí)就可以使用 cmake 同時(shí)編譯 src 中的所有 package 了,命令如下:

cd build
cmake ../src  -DCATKIN_DEVEL_PREFIX=../devel
make

將以上三個(gè)命令合并在一起就是 ROS 中的 catkin_make 命令。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容