Using Apollo Algorithms in Your Normal ROS Package

6 minute read

This blog will introduce how the port_apollo repository converts an apollo module to a normal ROS package and uses Apollo algorithms in your own ros package in a native linux system (i.e. ubuntu 16.04 LTS) instead of docker environments.

The tested codes are based on the release apollo-v3.0.0.

The goal of this project is to convert a bazel project to a cmake project while without modifying the algorithm part of source codes in apollo and keeping the existing directory structure unchanged. But the #include directories in the source files somehow need to be adapted according to the structure of cmake projects. I am not going the split the header files and source files of apollo like typical cmake projects. I prefer keeping codes as it was to avoid copying files and moving directories. The rational behind this is that in this way I can easily checkout the contents of the module folder of new apollo release version and throw the existing CMakeLists.txt and package.xml to the updated codes and get a new working version with minimum effort. So the include directory won’t exist in every ROS package folder. But you can still include project header files like other normal ROS packages assuming the headers are there.

Structure of the port_apollo Repo

port_apollo
├── LICENSE
├── README.md
├── scripts
│   ├── build_pkg.sh  # handy build script for ros workspace
│   ├── docker_build_image.sh # docker image for CI, you can use it locally as well
│   ├── docker_compile_pkgs.sh # repo-build script in docker container
│   ├── Dockerfile   # base docker file to build a image: ubuntu 16.04 + ros kinetics
│   ├── docker_install_dependencies.sh # install dependencies in the docker container
│   ├── docker_run.sh  # run the docker for the CI test
│   ├── installer  # dependencies install scripts go here
│   └── tools  # tools scripts
└── src
    ├── catkin_simple  # dependencies, ros package
    ├── glog_catkin  # dependencies, ros package
    ├── cmake  # cmake module folder to hold FindProtobuf.cmake
    ├── common  # apollo module, ros package 
    ├── planning # apollo module, to be done
    ...
    └── other_module_name # apollo module, to be done

Converting Steps

Let’s create a new branch named feature_catkinizing_module to hold the all the changes we are going to make. Pick a module named module_name from planning,control, routing, perception,…, etc. to convert.

1. Create a working branch based on the master branch

git checkout master && git pull # pull the latest codes
git checkout -b feature_catkinizing_module # create a new branch locally
git push --set-upstream origin feature_catkinizing_module # push to the remote

2. Get the source codes of a module:

  • Checkout a module
    This repo holds a copy of the source codes of apollo-v3.0.0 in branch apollo_3_0_0. You can use the git command to checkout a folder from the branch apollo_3_0_0.
    cd src && git checkout apollo_3_0_0 -- module_name # copy the module_name folder from apollo_3_0_0 to current working branch
    
  • Modify the #include directories in the header and source files in module_name directory
    All the original files include the modules/module_name in the #include lines. But in cmake projects, the prefix modules/module_name is not needed. My solution is searching all the .h and .cc files for modules/module_name/ and replacing it with an empty string "". In port_apollo repo, I provide a python script scripts/tools/content_hunter.py to do the work automatically. The usage is as below:
    python scripts/tools/content_hunter.py [module1_name] [module2_name]
    

    The module names here can be common, planning, control, perception etc.

    # for example 
    python scripts/tools/content_hunter.py common
    

    Note: The script only substitutes the string modules/module_name/. If the file include modules/other_module_name, you may need to remove it manually. Or improve the python script.

  • Create a test main file called module_name_tests.cc in the module_name folder.
    This file will call the test cases you are going to add in the CMakeLists.txt file.
      #include <gtest/gtest.h>
      int main(int argc, char **argv) {
        ::testing::InitGoogleTest(&argc, argv);
        return RUN_ALL_TESTS();
      }
    
  • Add CMakeLists.txt

      cmake_minimum_required(VERSION 3.0)
      project(module_name VERSION 0.0.1 LANGUAGES CXX)
      # enable c++11 feature
      set(CMAKE_CXX_STANDARD 11)
      #  enable tests
      set(CATKIN_ENABLE_TESTING ON)
      # for protobuf to generated interfacing files
      list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../cmake")
      ###############
      #### set the catkin dependend packages
      set(PKG_DEPS
          glog_catkin
          roscpp)
      find_package(catkin REQUIRED COMPONENTS ${PKG_DEPS})
      find_package(Protobuf REQUIRED)
      # set the .proto definition files  
      set(PROTOS
            path/to/file.proto # relative to module_name folder   
      )
      # call the protobuf command to generate the header and source files
      PROTOBUF_GENERATE_CPP(PROTO_SRCS PROTO_HDRS ${PROTOS})
    

    Note: I write a custom cmake module called FindProtobuf.cmake to find the protobuf library and related variables. It depends on pkg_check_modules of the pkgconfig and protobuf.pc to find the right version and return below variables

      PROTOBUF_INCLUDE_DIRS
      PROTOBUF_LIBRARIES
      RPOTOBUF_FOUND
    

    It also provides a cutom cmake command PROTOBUF_GENERATE_CPP to generate the header and source files for the .proto definition files before compiling the library. Currently, the script does not install .proto files to the shared folder like catkin_ws/devel/share or catkin_ws/install/share. The generated files will mirror the .proto directory structure in module_name folder. So if you need to use the generated header file of some .proto, just include the directory_to_proto file but replace the extension .proto with the .pb.h. For example, #include "common/configs/proto/vehicle_config.pb.h" for common/configs/proto/vehicle_config.proto. Of course, in the same package, you don’t need the module name common.

      # global include directories
      include_directories(
          ${catkin_INCLUDE_DIRS}
          ${PROTOBUF_INCLUDE_DIRS}
      )
      # use catkin_package command to expose the header files and the same directory structure to other catkin packages
      catkin_package(
          ## export these folder sturcture in the package root folder to other catkin packages
          INCLUDE_DIRS ${CATKIN_DEVEL_PREFIX}/include
          LIBRARIES ${PROJECT_NAME}  # we are going to wrap all the algorithms to one library named by the module_name
          CATKIN_DEPENDS ${PKG_DEPS})
      # set the source files for the library, you can select the source files you want to test in the module. Be careful with the dependencies. You can check the bazel build files for the dependency tree. You may need other module to be a catkin package first. In that case, convert other module first. Then add other modules as catkin dependencies to PKG_DEPS
      set(SOURCES
          path/to/source1.cc
          path/to/source2.cc)
      # add library target
      add_library(${PROJECT_NAME}
          ${SOURCES}
          ${PROTO_HDRS}
          ${PROTO_SRCS})
      # set the include directory only for the target
      target_include_directories(${PROJECT_NAME}
          PUBLIC
            $<BUILD_INTERFACE:${CATKIN_DEVEL_PREFIX}/include>
            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
          PRIVATE
            $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}>
          )
      # link the library
      target_link_libraries(${PROJECT_NAME}
          ${catkin_LIBRARIES}
          ${PROTOBUF_LIBRARIES}
          )
      if (CATKIN_ENABLE_TESTING)
          catkin_add_gtest(${PROJECT_NAME}_tests
              module_name_tests.cc
              path/to/test1.cc
              path/to/test2.cc)
    
          target_include_directories(${PROJECT_NAME}_tests
            PUBLIC
              $<BUILD_INTERFACE:${CATKIN_DEVEL_PREFIX}/include${PROJECT_NAME}>
              $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
            PRIVATE
              $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}>
              )
          target_link_libraries(${PROJECT_NAME}_tests
            ${catkin_LIBRARIES}
            ${PROJECT_NAME})
    
      endif (CATKIN_ENABLE_TESTING)
        
      #############
      ## Install ##
      #############
      ## install the headers and directories
      ## don't miss any subfolders of the root, or you can't include the headers
      ## from other ros packages
      # take the `common` module for example
      set(DIRS proto math util time vehicle_state data configs adapters status transform_listener monitor_log kv_db filters)
      # install all the headers to devel space since we export ${CATKIN_DEVEL_PREFIX}/include/
      # to catkin package instead of the include folder in the root
      install(DIRECTORY ${DIRS}
                DESTINATION ${CATKIN_DEVEL_PREFIX}/include/${PROJECT_NAME}
                FILES_MATCHING PATTERN "*.h"
                PATTERN ".svn" EXCLUDE
        )
      # install the header files in the module_name root folder
      install(FILES log.h macro.h
                DESTINATION ${CATKIN_DEVEL_PREFIX}/include/${PROJECT_NAME}
      )
      # install all the headers to the install space, mirror the structure
      # install the generated headers
      install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}
                DESTINATION ${CATKIN_GLOBAL_INCLUDE_DESTINATION}
                FILES_MATCHING PATTERN "*.h"
                PATTERN ".svn" EXCLUDE
      )
      # install all the headers to install space
      install(DIRECTORY ${DIRS}
                DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION}
                FILES_MATCHING PATTERN "*.h"
                PATTERN ".svn" EXCLUDE
      )
      # install header in the module_name root folder to install space
      install(FILES log.h macro.h
                DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION}
      )
    
  • Add package.xml, should match the ros packages defined by PKG_DEPS in CMakeLists.txt.

      <package format="2">
        <name>module_name</name>
        <description>apollo module_name module</description>
        <maintainer email="abc@abc.com">abc</maintainer>
        <license>Apache 2.0</license>
        <version>3.0.0</version>
        <buildtool_depend>catkin</buildtool_depend>
        <depend>glog_catkin</depend>
        <depend>roscpp</depend>
      </package>
    

4. Compile and Test, in port_apollo

  • Build and run tests

      catkin build
      catkin build --make-args tests
      ./devel/lib/module_name/module_name
    

5. Call the algorithms from the module_name you just converted in your catkin package my_package

  • CMakeLists.txt

      find_package(catkin REQUIRED COMPONENTS module_name)
      catkin_package(
          CATKIN_DEPENDS module_name
      )
    
  • package.xml

      <package format="2">
        <name>my_package</name>
        <description>my_package</description>
        <maintainer email="yu.zhang.bit@gmail.com">Yu Zhang</maintainer>
        <license>Apache 2.0</license>
        <version>0.0.1</version>
        <buildtool_depend>catkin</buildtool_depend>
        <depend>module_name</depend>
      </package>
    
  • Include headers from module_name package in the source files of your own package. Just follow the directory structure in module_name package. You can also use clion IDE to help you find the headers automatically.

      #include <module_name/subfolder/some_header.h>
    

A working example could be found in package common.

Pull requests for port_apollo are welcome!

Leave a Comment