본문 바로가기

ROS

[ROS] plugin 사용법

본 포스팅은 ubuntu 20.04 , ros noetic 기준으로 설명 되어 있습니다!!

ROS Plugin이란?

  • ros에서 프로그램 런타임에 특정 패키지를 load하여 사용할 수 있게 해주는 기능이다.
  • 사전에 정의된 base interface를 상속받은 플러그인이면 어떤 것이든 사용 할 수 있게 해주며, 특정 기능에 대해 추상화를 잘 해준다.

ROS Plugin을 사용하면 좋은 때

  • 특정 기능을 개발해야 하는데 기능의 인터페이스는 변하지 않고 내부 기능만 계속 변화할 때
  • 특정 기능이 개발이 안돼있는 경우 mocking하여 임시로 개발할 때
  • 상황에 따라 사용해야하는 기능이 달라질 때

Interface, BaseClass

plugin을 설계 할때, 먼저 만들고자 하는 기능의 interface를 정의 한다.

java, typescript 같은 언어는 interface라는 형식지 존재하지만 C++에서는 그런게 존재하지 않기 때문에 가장 기본되는 BaseClass를 생성하고 형식은 일반적으로 virtual function을 지정해 인터페이스를 정의한다.

 

Plugin Tutorial

모든 코드는 아래에 있습니다

https://github.com/ladianchad/plugin_study

1. Plugin package 생성

  • package를 생성할때 pluginlib에 대한 dependency를 넣어준다
cd ~/{ros_workspace}/src
catkin_create_pkg plugin_study roscpp pluginlib

2. CMakeLists.txt , package.xml 확인

  • CMakeLists.txt
cmake_minimum_required(VERSION 3.0.2)
project(plugin_study)

find_package(catkin REQUIRED COMPONENTS
  pluginlib
  roscpp
)

catkin_package(
 INCLUDE_DIRS include
 LIBRARIES plugin_study
 CATKIN_DEPENDS pluginlib roscpp
 DEPENDS system_lib
)

include_directories(
  ${catkin_INCLUDE_DIRS}
)
  • package.xml
<?xml version="1.0"?>
<package format="2">
  <name>plugin_study</name>
  <version>0.0.0</version>
  <description>The plugin_study package</description>
  <maintainer email="ladianchad@todo.todo">ladianchad</maintainer>
  <license>TODO</license>

  <buildtool_depend>catkin</buildtool_depend>

  <build_depend>pluginlib</build_depend>
  <build_depend>roscpp</build_depend>

  <build_export_depend>pluginlib</build_export_depend>
  <build_export_depend>roscpp</build_export_depend>

  <exec_depend>pluginlib</exec_depend>
  <exec_depend>roscpp</exec_depend>

  <export>
  </export>
</package>

3. BaseClass 작성

  • 기본적인 plugin을 소개하는 포스팅
  • interface
    • getName() : plugin이름을 반환하는 인터페이스
    • action(): plugin이 특정 기능(여기선 그냥 ROS_INFO하는 걸로) 인터페이스
  • include/plugin_study/base_study_plugin.hpp
#ifndef BASE_STUDY_PLUGIN__
#define BASE_STUDY_PLUGIN__
#include <string>
namespace study_plugin
{
  class BaseStudyPlugin
  {
  protected:
    std::string name_;

  public:
    BaseStudyPlugin(std::string name = "") : name_(name) {}

    virtual std::string getName()
    {
      return name_;
    };

    virtual void action() = 0;
  };

} // namespace study_plugin

#endif

4. Plugin 작성

cd ~/{ros_workspace}/src/plugin_study
mkdir plugins
mkdir src/plugin_study
  • ROS_INFO , ROS_WARN, ROS_ERROR를 하는 플러그인을 각각 작성
  • include/plugin_study/info_plugin.hpp
#ifndef INFO_STUDY_PLUGIN__
#define INFO_STUDY_PLUGIN__

#include <plugin_study/base_study_plugin.hpp>

namespace study_plugin
{
  class InfoPlugin : public BaseStudyPlugin
  {
  public:
    InfoPlugin();
    virtual void action() override;
  };
} // namespace study_plugin

#endif
  • plugins/info_plugin.cpp
#include <plugin_study/info_plugin.hpp>
#include <ros/ros.h>
#include <pluginlib/class_list_macros.h>

PLUGINLIB_EXPORT_CLASS(study_plugin::InfoPlugin, study_plugin::BaseStudyPlugin);

namespace study_plugin
{
  InfoPlugin::InfoPlugin() : BaseStudyPlugin("info plugin"){};

  void InfoPlugin::action()
  {
    ROS_INFO("action : logging info plugin");
  }
} // namespace study_plugin

 

  • include/plugin_study/warn_plugin.hpp
#ifndef WARN_STUDY_PLUGIN__
#define WARN_STUDY_PLUGIN__

#include <plugin_study/base_study_plugin.hpp>

namespace study_plugin
{
  class WarnPlugin : public BaseStudyPlugin
  {
  public:
    WarnPlugin();
    virtual void action() override;
  };
} // namespace study_plugin

#endif
  • plugins/warn_plugin.cpp
#include <plugin_study/warn_plugin.hpp>
#include <ros/ros.h>
#include <pluginlib/class_list_macros.h>

PLUGINLIB_EXPORT_CLASS(study_plugin::WarnPlugin, study_plugin::BaseStudyPlugin);

namespace study_plugin
{
  WarnPlugin::WarnPlugin() : BaseStudyPlugin("warn plugin"){};

  void WarnPlugin::action()
  {
    ROS_WARN("action : logging warn plugin");
  }
} // namespace study_plugin

 

  • include/plugin_study/error_plugin.hpp
#ifndef ERROR_STUDY_PLUGIN__
#define ERROR_STUDY_PLUGIN__

#include <plugin_study/base_study_plugin.hpp>

namespace study_plugin
{
  class ErrorPlugin : public BaseStudyPlugin
  {
  public:
    ErrorPlugin();
    virtual void action() override;
  };
} // namespace study_plugin

#endif
  • plugins/error_plugin.cpp
#include <plugin_study/error_plugin.hpp>
#include <ros/ros.h>
#include <pluginlib/class_list_macros.h>

PLUGINLIB_EXPORT_CLASS(study_plugin::ErrorPlugin, study_plugin::BaseStudyPlugin);

namespace study_plugin
{
  ErrorPlugin::ErrorPlugin() : BaseStudyPlugin("error plugin"){};

  void ErrorPlugin::action()
  {
    ROS_WARN("action : logging error plugin");
  }
} // namespace study_plugin
  • 코드 설명
    • 각각의 plugin들은 BaseClass인 BaseStudyPlugin을 상속받아 virtual fuction인 action을 구현하고 있다.
    • pluginlib/class_list_macros.h에는 plugin을 export하게 해주는 매크로 함수인 PLUGINLIB_EXPORT_CLASS가 있다.
    • PLUGINLIB_EXPORT_CLASS( target class , base class) 는 target class가 base class에 대한 plugin이라는 걸 export해준다.
    • 모든 plugin은 인자를 받지 않는(default 값으로 해도 됨) 생성자를 필요로 한다. 이를 지키지 않으면 후에 plugin loader가 작동하지 않는다.

5. CMakeLists.txt , package.xml 수정 및 plugins.xml 작성

  • CMakeLists.txt
cmake_minimum_required(VERSION 3.0.2)
project(plugin_study)

find_package(catkin REQUIRED COMPONENTS
  pluginlib
  roscpp
)

catkin_package(
 INCLUDE_DIRS include
 LIBRARIES plugin_study
 CATKIN_DEPENDS pluginlib roscpp
 DEPENDS system_lib
)

include_directories(
  include ##추가
  ${catkin_INCLUDE_DIRS}
)

add_library(my_plugins
  plugins/info_plugin.cpp
  plugins/warn_plugin.cpp
  plugins/error_plugin.cpp
)
add_dependencies(my_plugins ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
target_link_libraries(my_plugins ${catkin_LIBRARIES})

install(TARGETS
    my_plugins
    ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
    LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
    RUNTIME DESTINATION ${CATKIN_GLOBAL_BIN_DESTINATION}
)

install(FILES plugins.xml
  DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
)
  • 설명
    • add_library : plugin library 생성
    • add_dependencies: library dependency 정의
    • target_link_libraries : library link
    • install: library install
  • package.xml
<?xml version="1.0"?>
<package format="2">
  <name>plugin_study</name>
  <version>0.0.0</version>
  <description>The plugin_study package</description>
  <maintainer email="ladianchad@todo.todo">ladianchad</maintainer>
  <license>TODO</license>

  <buildtool_depend>catkin</buildtool_depend>

  <build_depend>pluginlib</build_depend>
  <build_depend>roscpp</build_depend>

  <build_export_depend>pluginlib</build_export_depend>
  <build_export_depend>roscpp</build_export_depend>

  <exec_depend>pluginlib</exec_depend>
  <exec_depend>roscpp</exec_depend>

  <export>
    <plugin_study plugin="${prefix}/plugins.xml"/>
  </export>
</package>
  • 설명
    • plugin_study / plugin : plugin export를 위한 xml file 정의
  • plugins.xml
<class_libraries>
  <library path="libmy_plugins">
    <class type="study_plugin::InfoPlugin"  base_class_type="study_plugin::BasePlugin">
      <description>action is info warn</description>
    </class>

    <class type="study_plugin::WarnPlugin"  base_class_type="study_plugin::BasePlugin">
      <description>action is loggin warn</description>
    </class>

    <class type="study_plugin::ErrorPlugin"  base_class_type="study_plugin::BasePlugin">
      <description>action is info error</description>
    </class>
  </library>
</class_libraries>
  • 설명
    • library / path : CMakeLists.txt에서 “lib” + “add_library로 했던 library이름”으로 path가 자동 생성된다.
    • class / type : plugin의 type을 적어준다. namespace안에 있는 class면 namespace부터 적어준다.
    • class / base_class_type : plugin의 base class를 명시한다.
    • class / description : plugin의 간단한 설명을 적어 놓는다.

6. plugin load & test

  • 만든 plugin을 test하기 위해 node 한개를 추가하기 위해 다음과 같이 한다.
  • src/main.cpp
#include <ros/ros.h>
#include <pluginlib/class_loader.h>
#include <plugin_study/base_study_plugin.hpp>

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

  // plugin loader 생성
  pluginlib::ClassLoader<study_plugin::BaseStudyPlugin> plugin_loader("plugin_study", "study_plugin::BasePlugin");

  // plugin load
  boost::shared_ptr<study_plugin::BaseStudyPlugin> plugin = plugin_loader.createInstance("study_plugin::InfoPlugin");
  ROS_INFO("plugin name : %s", plugin->getName().c_str());

  while (ros::ok())
  {
    plugin->action();
    ros::Duration(1).sleep();
  }

  return 0;
}
  • 설명
    • pluginlib::ClassLoader<{BaseClasee}> : plugin을 동적으로 load해주는 class이다 (기본적으로 factory pattern이다). 생성자에는 plugin이 들어있는 package와 plugin의 base class type을 적어주면 된다.
    • plugin_loader.createInstance({target plugin type}): load하고자 하는 plugin의 type을 적어주면 해당하는 plugin을 load해준다. 여기에 해당하는 type을 launch prameter로 받게 되면 소스코드는 다시 빌드하지 않고 plugin만 새로 빌드 해서 넣으면 전혀 다른 기능이 작동하게 만들 수 있다.
  • CMakeLists.txt
cmake_minimum_required(VERSION 3.0.2)
project(plugin_study)

find_package(catkin REQUIRED COMPONENTS
  pluginlib
  roscpp
)

catkin_package(
 INCLUDE_DIRS include
 LIBRARIES plugin_study
 CATKIN_DEPENDS pluginlib roscpp
 DEPENDS system_lib
)

include_directories(
  include ##추가
  ${catkin_INCLUDE_DIRS}
)

add_library(my_plugins
  plugins/info_plugin.cpp
  plugins/warn_plugin.cpp
  plugins/error_plugin.cpp
)
add_dependencies(my_plugins ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
target_link_libraries(my_plugins ${catkin_LIBRARIES})

##추가
add_executable(plugin_test src/main.cpp)
target_link_libraries(plugin_test ${catkin_LIBRARIES})
##추가

install(TARGETS
    my_plugins
    ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
    LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
    RUNTIME DESTINATION ${CATKIN_GLOBAL_BIN_DESTINATION}
)

install(FILES plugins.xml
  DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
)

이제 빌드하고 plugin_study plugin_test를 실행하면 정상작동하는 걸 볼 수 있다.

cd ~/{ros_workspace}
catkin_make
source devel/setup.bash

 

실행 화면