내 로봇의 에너지를 좀 더 효율적으로! 기능 껐다 켰다 하는 Lifecycle Node구현하기(C++)

IROBOU·2024년 3월 12일
2

인공지능_로봇개발

목록 보기
17/17
post-thumbnail

동영상 튜토리얼 part1

해당 tutorial code


배경지식 & 왜 필요할까?

  • 우리는 방이 어두울 때 조명을 킨다. 조명은 우리의 시야를 밝혀주는 기능을 하고 있는 셈이다.

  • 그런데 우리가 외출을 한다고해보자. 우리는 밖에 있는 동안엔 조명의 기능이 필요 없기 때문에 불을 끄고 외출 할 것이다.

  • 그리고 다시 저녁에 돌아오면 조명을 다시 켠다.

  • 이렇듯 로봇에도 수많은 기능들(주행, 주변 지도 그리기, 위치 찾기, 주변 인식하기, 장애물 피하기 등등) 들이 있지만 이 모든 기능들이 항상 필요한 건 아니다.

  • 예를 들어 삼성 로봇 청소기를 생각해보자.

  • 영상에서 와 같이 로봇은 주변의 파악하는 기능이 필요하다.

  • 이 기능은 지도를 그리는 것인데 어떻게 지도가 그려지는지 참고하고 싶은 사람은 이 영상을 보기바란다.

  • 이렇게 로봇이 지도를 그리는 과정은 청소를 하기 전에 한 번만 필요하다.

  • 그렇기 때문에 한 번 지도를 작성하고 나선 그 기능을 담당하는 node를 꺼줘야한다.

  • 그래야 로봇의 에너지를 절약할 수 있기 때문이다.

  • 오늘은 이렇듯 특정 기능을 담당하는 node의 상태(꺼졌다, 켜졌다 등)를 정의하고 그 상태를 바꿀 수(껐다, 켰다 등) 있는 lifecycle node에 대해서 배우고 구현해보도록 하자.


<용어 설명>

  • State machine

    • 소프트웨어 공학 등에서 시스템이나 프로그램을 묘사하는데 사용하는 개념이다.
    • 한 시스템을 state machine으로 나타낼 때는 그 시스템이 몇 가지 상태가 있다고 가정한다.
    • 이 상태들은 서로간 전이(변화, transition)이 가능하다.
    • 가장 간단한 예시로 지도그리는 기능을 담당하는 node(system)을 생각해보자.
    • 그리고 이 시스템이 1. 꺼진 상태, 2. 켜진 상태를 갖는다고 하자.
    • 그럼 이시스템은 1.꺼진 상태에서 켜진 상태로의 변화, 2.켜진 상태에서 꺼진 상태로의 변화가 일어 날 수 있다.
    • 이렇게 한 시스템의 가능한 상태와 변화를 정의해 묘사한 것을 state machine이라고 한다.
  • Lifecycle

    • state machine으로 묘사한 시스템의 상태를 관리하고 제어하는 기능을 제공하는 ROS2의 프레임워크(기능과 도구)이다.

    • 위에서 들었던 지도 그리는 node를 생각하면 상태가 1.꺼짐 2. 켜짐이 있었다.

    • ROS2의 lifecycle을 이용하면 해당 node가 꺼져있는지, 켜져있는지 상태를 확인 할 수 있다.

    • 또한 현재 상태를 확인한 후 원하는 상태(꺼져있다면 켜고, 켜져있다면 끄는 등)로 변경시킬 수 있다.

    • 하단의 그림은 ROS2에서 lifecycle 기능으로 관리 및 제어 가능한 node의 state machine이다. 뭐가 많아 보이는데 끄고 켜는걸 좀 더 세세하게 나눴을 뿐 본질은 어렵지 않다. 하나하나 살펴보자. 참고로 안외워도 되고 구현해야되는데 생각 안날 때 밑에 그림 다시 찾아서 천천히 구현하면 된다.

    • node 상태를 나타내는 용어

      • Unconfigured : node가 객체화(instantiated)된 직후의 상태이다. node class의 생성자가 호출됐다고 생각하면된다. 이 상태에서는 변수등이 선언만 형식적으로 되어 있을 뿐 의미있는 데이터를 저장하지 않는 것이 일반적이다. 이 상태의 특징을 생각하면서 구현하도록 하자.
        • Unconfigured에서 변경 가능한 상태
          • "configure"를 통해서 "Inactive" 상태로 변경 가능
          • "shutdown"을 통해서 "Finalized" 상태로 변경 가능
      • Inactive : node내 parameter를 변경하거나, 특정 topic의 publication/subscription을 생성/삭제 하거나, service를 생성/삭제 하는 등 system구성이 이루어진 상태이다. 다만 생성/삭제만 됐을 뿐 실제로 데이터를 구독하거나 service를 처리하진 않는 상태이다. 이 상태의 특징을 생각하면서 구현하도록 하자.
        • Inactive에서 변경 가능한 상태
          • "shutdown"을 통해서 "Finalized" 상태로 변경 가능
          • "cleanup"을 통해서 "Unconfigured" 상태로 변경 가능
          • "activate"를 통해서 "Active" 상태로 변경 가능
      • Active : 실제로 node가 기능을 수행할 때 상태이다. 기능을 수행하기 위해 데이터를 읽고 처리하거나, service 요청을 처리, output을 만들어낸다. 다시 말하면 Inactive 상태에서 생성되어 있던 publisher, subscription, service등이 제 역할을 하기 시작한다는 말이다.
        • Active에서 변경 가능한 상태
          • "deactivate"를 통해서 "Inactive" 상태로 변경 가능
          • "shutdown"을 통해서 "Finalized" 상태로 변경 가능
      • Finalized : node가 완전히 없어지기 직전 상태이다. node를 바로 없애면 되지 왜 있나 생각할 수 있다. 이 상태는 "shutdown"을 통해서 도달 할 수도 있지만, error가 발생해서도 도달 할 수 있는 상태로써 error등을 잡는 debugging용도로 사용되기 위한 상태이다. 일단 이런 상태가 있구나하고 참고하도록 하자.
        • Finalized에서 변경 가능한 상태
          • "destroy"를 통해서 node를 완전히 없앨 수 있다.(node에 할당된 모든 메모리가 해제된다)
    • 상태를 변화시키는 행위를 나타내는 용어

      • create : 단지 node의 생성자(constructor)만 실행한다.
      • configure : parameter, publication/subscription, service server/client등을 생성한다. 실제 데이터 처리나 서비스 요청에 대한 응답은 처리하지 않는다. 관련 구현 함수: on_configure()
      • cleanup : configure에서 생성했던 변수 및 데이터를 지운다. 보통 re configure하기 전에 호출한다. 관련 구현 함수: on_cleanup
      • activate : configure에서 생성했던 publisher, subscriber, service server등이 실제로 기능을 수행한다. 관련 구현 함수: on_activate
      • deactivate : activate에서 수행되던 기능을 멈춘다. node가 없어지는 것은 아니다. 관련 구현 함수: on_deactivate
      • shutdown : node의 메모리가 해지되기 전(파괴되기 전) 필요한 메모리 해제를 추가로 진행하는 과정이다. 구현할 때 cleanup과 다소 유사한 성격을 지니는 경우가 있다. 다만 cleanup과 다르게 unconfigured, inactive, active상태에서 사용자가 node를 종료 시키고 싶을 때 호출 될 수 있다.
      • destroy : node 자체의 메모리를 해지하는 행위이다.
    • 상태 변화동안을 묘사하는 용어

      • Configuring : Unconfigured 상태에서 Inactive 상태로 전환 중인 상태를 나타내는 용어이다.
      • CleaningUp : Inactive 상태에서 Unconfigured 상태로 전환 중인 상태를 나타내는 용어이다.
      • ShuttingDown : node가 finalized 상태로 전환 중인 상태를 나타내는 용어이다.
      • Activating : Inactive 상태에서 Active 상태로 전환 중인 상태를 나타내는 용어이다.
      • Deactivating : Active 상태에서 Inactive 상태로 전환 중인 상태를 나타내는 용어이다.
      • ErrorProcessing : Configuring, CleaningUp, Activating, Deactivating, ShuttingDown이 진행 중일 때 의도치않은 에러가 발생되면 Finalized상태로 넘기는 과정을 나타내는 용어이다. 이 상태가 진행되면 node는 finalized상태가 되고 메모리가 해제되면서 종료된다.
  • Lifecycle node

    • lifecycle기능을 갖춰 상태 관리 및 제어가 가능한 node를 의미한다.

준비물


실습

실습1. 패키지 만들기

이번엔 simple_lifecycle_pkg라는 이름으로 패키지를 만들어보자.

mkdir -p simple_lifecycle_ws/src
cd simple_lifecycle_ws/src
ros2 pkg create --build-type ament_cmake --license Apache-2.0 simple_lifecycle_pkg
cd simple_lifecycle_pkg
code . 

패키지 내부 경로로 이동해서 visual code까지 열어줬다.


실습2. talker lifecycle node작성하기

자 그 다음 우리 talker lifecycle node를 작성해야한다.
이 node는 우리가 일전에 pub/sub 구현 실습
에서 작성한 것과 매우 유사한 기능을 가지고 있다.(살짝 출력하는 log는 지도 작성 기능 예시에 맞게 변경했다.)

먼저 소스코드를 작성할 파일, lifecycle_talker.cpp을 다음과 같이 생성해주자.

그다음 내부에 다음 코드를 먼저 복사 붙여넣기 하자. 항상 말하지만 나중에 스스로 구현해야되면 이 코드를 reference삼아 구성요소 및 흐름을 이해하고 있다고 써먹으면 된다.

#include <chrono>
#include <functional>
#include <memory>
#include <string>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

#include "rclcpp_lifecycle/lifecycle_node.hpp"

using namespace std::chrono_literals;

// class MinimalPublisher : public rclcpp::Node
class MinimalPublisher : public rclcpp_lifecycle::LifecycleNode
{
public:
    MinimalPublisher()
        // : Node("minimal_publisher"), count_(0)
        : rclcpp_lifecycle::LifecycleNode("minimal_publisher"), count_(0)
    {
        // publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
        // timer_ = this->create_wall_timer(
        //     500ms, std::bind(&MinimalPublisher::timer_callback, this));
    }

    rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn on_configure(const rclcpp_lifecycle::State &state)
    {
        publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
        timer_ = this->create_wall_timer(1s, std::bind(&MinimalPublisher::timer_callback, this));

        RCUTILS_LOG_INFO_NAMED(get_name(), "on_configure() is called from state %s.", state.label().c_str());

        return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
    }

    rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn on_activate(const rclcpp_lifecycle::State &state)
    {

        publisher_->on_activate();

        RCUTILS_LOG_INFO_NAMED(get_name(), "on_activate() is called from state %s.", state.label().c_str());

        return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
    }

    rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn on_deactivate(const rclcpp_lifecycle::State &state)
    {
        publisher_->on_deactivate();

        RCUTILS_LOG_INFO_NAMED(get_name(), "on_deactivate() is called from state %s.", state.label().c_str());

        return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
    }

    rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn on_cleanup(const rclcpp_lifecycle::State &state)
    {

        timer_.reset();
        publisher_.reset();

        RCUTILS_LOG_INFO_NAMED(get_name(), "on cleanup is called from state %s.", state.label().c_str());

        return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
    }

    rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn on_shutdown(const rclcpp_lifecycle::State &state)
    {

        timer_.reset();
        publisher_.reset();

        RCUTILS_LOG_INFO_NAMED(get_name(), "on shutdown is called from state %s.", state.label().c_str());

        return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
    }

private:
    void timer_callback()
    {
        auto message = std_msgs::msg::String();
        message.data = "Creating a map " + std::to_string(count_++);
        // Print the current state for demo purposes
        if (!publisher_->is_activated())
        {
            RCLCPP_INFO(
                get_logger(), "Lifecycle publisher is currently inactive. Messages are not published.");
        }
        else
        {
            RCLCPP_INFO(get_logger(), "Lifecycle publisher is active. Publishing: [%s]", message.data.c_str());
        }

        // We independently from the current state call publish on the lifecycle
        // publisher.
        // Only if the publisher is in an active state, the message transfer is
        // enabled and the message actually published.
        publisher_->publish(message);
    }
    rclcpp::TimerBase::SharedPtr timer_;
    std::shared_ptr<rclcpp_lifecycle::LifecyclePublisher<std_msgs::msg::String>> publisher_;
    size_t count_;
};

int main(int argc, char *argv[])
{
    rclcpp::init(argc, argv);
    std::shared_ptr<MinimalPublisher> minimal_publisher = std::make_shared<MinimalPublisher>();
    rclcpp::spin(minimal_publisher->get_node_base_interface());
    // rclcpp::spin(std::make_shared<MinimalPublisher>()); //origianl
    rclcpp::shutdown();
    return 0;
}

실습 2.1. 코드 분석

자 이제 코드 분석을 해보자.

실습 2.1.1 헤더파일 포함 및 namespace선언

먼저 필요한 헤더파일들은 표준 라이브러리와 ROS2에서 include하고 있다.

나머지는 전부다 일반 talker node를 만들 때 배웠던 거라 익숙하지만

#include "rclcpp_lifecycle/lifecycle_node.hpp"

는 처음본다.
해당 헤더파일은 일반 node가 아닌 lifecycle node라는 것을 만들 때 필요한 헤더파일이다.

추후 어디서 어떻게 사용하기 위해 include된 것 인지 나머지 코드를 보면서 직접 살펴보자.

실습 2.1.2. lifecycle class 생성자 생성

위의 코드를 보면 기존의 Node를 상속 받던 부분, publisher와 timer를 생성하는 부분이 class 생성자(constructor) 부분에서 빠진 것을 볼 수 있다.

...
// : Node("minimal_publisher"), count_(0)
...
        // publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
        // timer_ = this->create_wall_timer(
        //     500ms, std::bind(&MinimalPublisher::timer_callback, this));  

대신 rclcpp_lifecycle::LifecycleNode를 상속받고 있는데 lifecycle node는 일반 node와 다르게 해당 node의 상태를 확인할 수 있고 제어할 수 있는 기능을 갖는다.

Publisher와 timer 생성은 해당 class의 method인 on_configure에서 생성될 예정이다.

실습 2.1.3. on_configure() 작성하기

앞서 용어 설명에서 on_configure는 unconfigure 상태에서 inactive 상태로 변환 시켜주는 기능을 한다고 배웠다.
즉, publisher/subscriber, timer, service 등을 생성하는 역할을 담당한다.

그렇기 때문에 MinimalPublisher class의 onconfigure() method는 publisher와 timer를 만들고 있다.
만드는 방식은 일반 node와 다르지 않지만 여기서 "publisher
"는 그 타입이 일반 publisher가 아닌데 밑에서 해당 타입을 정의하는 부분에서 살펴볼 에정이다.

그리고 on_configure()가 성공적으로 동작했으면 성공했다는

rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS

를 반환한다.

실습 2.1.4. on_activate() 작성하기

on_activate()의 경우 inactive에서 active로 lifecycle node의 상태를 변환한다. active상태에서는 실제로 publisher/subscriber 등이 데이터를 생성 및 처리하고 service 호출에 응답하는 등의 실질 적인 기능이 동작해야한다.

이 lifecycle node의 경우 publihser를 on_active()라는 method를 통해서 실질적으로 publisher가 메세지를 발행할 수 있는 상태가 될 수 있도록 활성화 시키고 있다.
이 on_active()가 어떤 영향을 끼치는지는 timer callback을 살펴볼 때 살펴볼 예정이다.

그리고 on_activate()가 성공적으로 동작했으면 성공했다는

rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS

를 반환한다.

실습 2.1.5. on_deactivate() 작성하기

on_deactivate()의 경우 active에서 inactive로 node의 상태를 변화 시킨다.

inactive상태에서는 publisher/subscriber, service등이 동작하지 않아야 하기 때문에 publisher_를 on_deactive()를 통해서 비활성화한다.

이렇게 비활성화 되면 timer callback에서 publisher_를 통해 message 발생 명령을 내려도 실제로 그 기능을 하지 못 한다.

그리고 on_deactivate()가 성공적으로 동작했으면 성공했다는

rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS

를 반환한다.

실습 2.1.6 on_cleanup() 작성하기

on_cleanup()은 re-configure를 하기 전에 생성되었던 publisher/subscriber, service, 필요없는 변수들을 청소하는 작업을 한다. 또한 node 상태를 inactive에서 unconfigured로 변경한다.

따라서 코드에서는 이전에 생성했던 timer와 publisher의 메모리를 해제하고 있다.

그리고 on_cleanup()가 성공적으로 동작했으면 성공했다는

rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS

를 반환한다.

실습 2.1.7 on_shutdown() 작성하기

on_shutdown()은 unconfigured, inactive, active 상태에서 호출 될 수 있으므로 node가 기능을 잃기전 안전하게 필요없는 메모리를 해제해줘야하는 역할을 수행해야한다.

그렇기 때문에 다소 on_cleanup()과 유사해보일 수 있다.

해당 코드에서는 일전에 생성되어있을 수 있었던 timer, publisher의 메모리를 해제한다.

그리고 on_shutdown()가 성공적으로 동작했으면 성공했다는

rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS

를 반환한다.

실습 2.1.8. timer_callback작성하기

publsher_의 publisher를 생성할 때 등록해줘야하는 timer_callback이 정상적으로 동작하기 위해서 private 접근 지정자에서 정의해줘야한다.

해당 timer_callback 코드에서는

  • 발행할 메세지를 만들어 놓는다. 우리는 지도 그리는 예시를 사용하므로 "Creating a map"이라는 메세지를 작성했다.
  • publisher_가 active상 태인지 확인하고 그에 맞는 log를 출력한다.
  • 일전에 만들어 놓은 메세지를 publisher로 발행하는데 이 코드는 publisher가 on_activate()에 의해 active 상태에 있을 때만 동작한다.

실습 2.1.9. 클래스내 멤버 변수 선언

메세지를 발행할 때 필요한 publisher, timer, count 변수들을 선언한다.

한가지 특이한건 publisher_의 타입이

std::shared_ptr<rclcpp_lifecycle::LifecyclePublisher<std_msgs::msg::String>>

라는 것이다.

이렇게 on_active, on_deactive으로 활성화/비활성화 할 수 있는 publisher를 만들기 위해서는 rclcpp_lifecycle 헤더로부터 LifecyclePublisher라는 타입을 사용해야한다.

실습 2.1.10. main 함수 작성하기

이제 node가 executable을 실행했을 때 동작할 수 있도록 main함수를 작성해야 한다.

여기서 독특한 점은 rclcpp spin을 사용할 때 이전처럼 class 자체를 넣어주는게 아니라 node의 base interface를 획득하여 넣어준다.

이렇게 하는 이유는 lifecycle node 특성상 노드의 상태를 관찰 및 제어하는데 그 제어권등을 get_node_base_interface로 획득해서 spin으로 넘겨줘야 하기 때문이다.

실습 2.2. CMakeLists.txt 작성

소스코드 작성이 끝났으면 다음 함수들을 CMakeLists.txt에 추가해주자.

find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
find_package(rclcpp_lifecycle REQUIRED)

add_executable(lifecycle_talker src/lifecycle_talker.cpp)
ament_target_dependencies(lifecycle_talker rclcpp std_msgs rclcpp_lifecycle)

install(TARGETS
  lifecycle_talker
  DESTINATION lib/${PROJECT_NAME})

(예시)

여기서 주목할 점은 우리는 lifecycle node와 lifecycle publisher를 사용했기 때문에 해당 타입을 갖고 있는 "rclcpp_lifecycle" 패키지를 찾고 executable을 생성할 때 dependency로 넣어줬다는 것이다.

dependency 이야기를 하니 생각나는게 있는가?

그렇다 package.xml도 수정해주자!

실습 2.3. package.xml 작성

다음 코드를 추가해주자.

  <depend>rclcpp</depend>
  <depend>std_msgs</depend>

  <build_depend>rclcpp_lifecycle</build_depend>

(예시)

rclcpp와 std_msgs는 일전에 봤던 것과 같다.
그런데 dependency로 rclcpp_lifecycle을 추가해줬다.


실습3. 빌드 및 실행

대망의 빌드 시간이다.

다음과 같이 colcon build해주자.

cd ~/simple_lifecycle_ws
colcon build

빌드가 완료되면 실행하자.

cd ~/simple_lifecycle_ws
source install/setup.bash
ros2 run simple_lifecycle_pkg lifecycle_talker

오잉?

메세지가 나오지 않는다.

뭐가 잘 못 된 것일까?


실습4. rqt로 껐다 켰다 하는 service call 보여주기

일반 node와 lifecycle node가 다른 점은 node를 껐다 켰다 할 수 있다는 점이다.

그렇기 때문에 앞서 실습3에서 executable을 실행해줬지만 node 상태가 unconfigured라서 아무 메세지도 출력되지 않는 것이다.

lifecycle node를 만들 때 on_configure, on_active, on_deactive, on_cleanup, on_shutdown method를 구현해주면 이에 상응하는 service들을 이용할 수 있게 된다.

오늘은 두가지 서비스에 대해서 배워볼 예정이다.

실습4.1. rqt service caller 켜기

rqt service caller가 익숙하지 않은 사람은 이 글 에서 5.rqt 실습을 참고 바란다.

다음과 같이 두개의 service caller를 열어준다. 두개의 service caller를 구성하려면 service caller를 구성하는 것을 단순히 2번 반복하면 된다.

  • 하나는 "/minimal_publisher/change_state" 서비스에 대한 caller이다.
  • 나머지 하나는 "/minimal_publisher/get_state" 서비스에 대한 caller이다.

(2개의 rqt service caller를 구성한 예시)

실습 4.2. /minimal_publisher/change_state 서비스 이용하기

  • /minimal_publisher/change_state 서비스는 minimal_publisher lifecycle node상태를 변환하는데 사용한다.
    변환하기 위한 int type Expression은 다음과 같다.
  • 1: on_configure()을 호출
  • 2: on_cleanup()을 호출
  • 3: on_activate()를 호출
  • 4: on_deactivate()를 호출
  • 5: unconfigured상태에서 on_shutdown()을 호출
  • 6: inactive상태에서 on_shutdown()을 호출
  • 7: active 상태에서 on_shutdown()을 호출

여기서는 다음과 같은 시나리오를 실습해보자

  • on_configure() 호출하기
    (예시)
  • 그 후, on_activate() 호출하기
    (예시)

  • 그 후, on_deactivate() 호출하기
    (예시)

  • 그 후, on_cleanup()호출하기
    (예시)

  • 마지막으로 unconfigured 상태일 때 on_shutdown() 호출하기
    (예시)

실습 4.3. /minimal_publisher/get_state 서비스 이용하기

해당 서비스는 이용하는 방법이 간단하다.

그냥 우측 상단에 call을 누르면 lifecycle node의 현재 상태를 알려준다.

위 실습 4.2를 진행하면서 다음과 같은 상태들을 확인해보자.

  • on_configure() 호출하기 전 상태 : "unconfigured"
    (예시)
  • on_configure() 호출한 후 상태 : "inactive"
    (예시)
  • 그 후, on_activate() 호출 한 후 상태: "active"
    (예시)
  • 그 후, on_deactivate() 호출한 후 상태 : "inactive"
    (예시)
  • 그 후, on_cleanup()호출한 후 상태: "unconfigured"
    (예시)
  • 마지막으로 unconfigured 상태일 때 on_shutdown() 호출한 후 상태 : "finalized"
    (예시)

실습5. lifecycle listener code 구현하기

  • lifecycle talker가 발행하는 메세지를 구독하는 lifecycle node
  • 아쉽게도 rclcpp_lifecycle::LifecyclePublisher와 같이 LifecycleSubscription이 없어서 on_activate(), on_deactivate() 같은 함수를 사용할 수 없다. 그렇지만 lifecycle의 state machine 기본 구조를 충분히 이해하고 약간의 c++프로그래밍적 요소를 더하면 충분히 lifecycle node를 만들기 위해 필요한 on_configure, on_activate, on_deactivate, on_cleanup, on_shutdown 함수들을 구현할 수 있다.
  • 자 그렇다면 본격적으로 lifecycle talker가 발행하는 메세지를 구독하는 lifecycle node를 만들어보자.
  • 먼저 src폴더에 다음과 같이 lifecycle_listener.cpp라는 파일을 만들어준다.
  • 그 다음 이 파일에 다음의 코드를 작성하도록 하자.
#include <memory>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using std::placeholders::_1;

#include "rclcpp_lifecycle/lifecycle_node.hpp"

class MinimalSubscriber : public rclcpp_lifecycle::LifecycleNode
{
public:
    MinimalSubscriber()
        : rclcpp_lifecycle::LifecycleNode("minimal_subscriber")
    {
        // subscription_ = this->create_subscription<std_msgs::msg::String>(
        //     "topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
    }

    rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
    on_configure(const rclcpp_lifecycle::State &state)
    {
        subscription_ = this->create_subscription<std_msgs::msg::String>(
            "topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
        m_isActivated = false;
        RCUTILS_LOG_INFO_NAMED(get_name(), "on_configure() is called from state %s.", state.label().c_str());

        return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
    }

    rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
    on_activate(const rclcpp_lifecycle::State &state)
    {
        m_isActivated = true;
        RCUTILS_LOG_INFO_NAMED(get_name(), "on_activate() is called from state %s.", state.label().c_str());

        return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
    }

    rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
    on_deactivate(const rclcpp_lifecycle::State &state)
    {
        m_isActivated = false;
        RCUTILS_LOG_INFO_NAMED(get_name(), "on_deactivate() is called from state %s.", state.label().c_str());

        return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
    }

    rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
    on_cleanup(const rclcpp_lifecycle::State &state)
    {
        subscription_.reset();
        RCUTILS_LOG_INFO_NAMED(get_name(), "on cleanup is called from state %s.", state.label().c_str());

        return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
    }

    rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
    on_shutdown(const rclcpp_lifecycle::State &state)
    {
        subscription_.reset();
        RCUTILS_LOG_INFO_NAMED(get_name(), "on shutdown is called from state %s.", state.label().c_str());

        return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
    }

private:
    void topic_callback(const std_msgs::msg::String &msg) const
    {
        if (m_isActivated)
        {
            RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg.data.c_str());
        }
        else
        {
            RCLCPP_INFO(
                get_logger(), "Subscriber is currently inactive. Callback function is not working .");
        }
    }
    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
    bool m_isActivated = false;
    // std::shared_ptr<rclcpp_lifecycle::LifecycleSusbcription<std_msgs::msg::String>> subscription_; -> unfortunately no such thing
};

int main(int argc, char *argv[])
{
    rclcpp::init(argc, argv);
    std::shared_ptr<MinimalSubscriber> minimal_subscriber = std::make_shared<MinimalSubscriber>();
    rclcpp::spin(minimal_subscriber->get_node_base_interface());
    // rclcpp::spin(std::make_shared<MinimalSubscriber>());
    rclcpp::shutdown();
    return 0;
}

실습 5.1. 코드 분석

그럼 lifecycle 용 subscription이 없는데 어떻게 lifecycle node 처럼 동작시킬 수 있는지 위의 코드를 하나하나 분석해보자!

실습 5.1.1. 필수 헤더파일 추가

제일 먼저 살펴볼 부분은 필수 헤더파일을 추가하는 것이다.

  • ROS2 관련 코드를 위한 rclcpp.hpp
  • string type의 message를 사용하기 위한 string.hpp
  • lifecycle node를 상속받기 위한 lifecycle_node.hpp
  • std::shared_ptr를 사용하기 위한 memory
    • (참고: 표준 라이브러리인 memory는 헤더파일이지만 .hpp나 .h라는 확장자를 갖지 않는다고 한다.)
  • 등의 헤더파일을 include하는 것을 볼 수 있다.

실습 5.1.2. lifecycle node 상속 및 constructor 정의

다음 과정은 node를 만들려면 class가 필요하기 때문에 class 생성자(constructor)를 정의해준다!

  • MinimalSubsriber 클래스는 LifecycleNode를 상속받는다.
  • lifecycle node를 상속 받았기 때문에 on_configure, on_activate, on_deactivate, on_cleanup, on_shutdown 함수를 override할 수 있다.
  • MinimalSubscriber() 생성자에서 lifecycle node 이름은 "minal_subscriber"로 초기화하고 있다.
  • 예전에 배웠던 기본 listener에서와는 다르게 생성자에서 subscription을 생성하고 callback을 묶어주는 작업을 하지 않는다.
  • lifecycle 정의상 subscription이 생성되는 것은 on_configure가 호출될 때 행해지는 것이 적절하기 때문이다.

실습 5.1.3. on_configure 정의

lifecycle node의 구색을 갖추기 위해서는 on_configure(), on_activate(), on_deactivate(), on_cleanup(), on_shutdown()이라는 함수가 필요했었다.
on_configure()부터 정의하자.

  • 외부 사용자(client등)가 해당 lifecycle node를 configure할 수 있도록 도와주는 함수 on_configure를 정의한다.
  • configure에서는 node가 주요기능을 하기 위해 필요한 변수, 객체등을 생성하는 구간이라고 생각하면 된다.
  • 여기서는 subscription이 필요하므로 "topic" 이름을 갖는 topic message를 구독하는 subscription을 생성하고 이 subscription을 topic_callback이라는 함수를 등록해준다.
  • topic_callback은 "topic"에서 message를 받을 때마다 실행된다. i.e. message가 들어오지 않으면 topic_callback 함수 호출 안됨.
  • m_isACtivated라는 멤버변수를 false로 지정한다.
  • 이는 아직 해당 lifecycle node가 active한 상태는 아니라는 것을 의미한다.
  • on_configure가 호출 될 때 어떤 상태였는지 state.label()을 통해 log를 출력한다.
  • on_configure내 코드들이 정상적으로 동작했다면 마지막에 함수 호출이 성공했다는 의미에서 CallbackReturn::SUCCESS라는 값을 반환한다.

실습 5.1.4. on_activate 정의

좋아 on_configure()를 정의했다. 이제 on_activate로 이동하자!

  • m_isActivated라는 멤버 변수가 true로 설정된다.
  • 이 멤버 변수 값이 true이면 topic_callback 호출 시 특정 동작을 취하게 된다.
  • 이는 lifecycle node 정의인 activated상태일 때 주요 기능을 node가 수행한다는 것에 부합한다.
  • 그 후 어떤 상태에서 on_activate가 호출 되었는지 log를 출력한다.
  • on_activate 함수 호출이 성공했다는 의미에서 CallbackReturn::SUCESS를 반환한다.

실습 5.1.5. on_deactivate 정의

on_activate도 별거 아니였다. on_deactivate는 어떨까? 한 번 살펴보자.

  • lifecycle node의 deativated 상태의 정의에 의하면 해당 node에서 주요 기능인 subscription기능이 실행되지 않으면 된다.
  • 그렇게 하기 위해 m_isActivated를 false로 설정한다.
  • 이렇게 설정하는게 subscription 동작을 멈추게 하는 것과 무슨 연관이 있을까?
  • 밑에 topic_callback에서 살펴보겠지만 m_isActivated 값이 false인 경우 topic_callback에서는 아무 동작을 하지 않고 함수를 종료한다.
  • 그렇기 때문에 m_isActivated를 false로 해놓으면 deactivated 상태를 만들 수 있다!

실습 5.1.6. on_cleanup 정의

lifecycle node 정의를 알고 있으니 on_deactivate도 너무 쉬웠다.
on_cleanup에서는 무엇을 해야할까?
그렇다. on_configure에서 생성해줬던 기능 수행시 필요한 변수, 객체등을 제거해줘야한다.
여기서는 subscription이 그 변수, 객체등에 해당한다.
이를 제거하기 위해 다음과 같이 코드를 작성할 수 있다.

  • cpp에서는 shared pointer 변수를 제거하기 위해 reset()을 사용할 수 있다. reset()으로 subscription을 제거해준다.
  • state.label()로 어떤 상태에서 on_cleanup()이 호출 되었는지 log를 출력한다.
  • on_cleanup()이 성공적으로 수행되었다면 CallbackReturn::SUCCESS를 반환한다.

실습 5.1.7. on_shutdown 정의

거의 다왔다. 이제 on_shutdown()만 정의하면된다. shutdown은 무슨 역할을 했던가? 그렇다. node가 없어지기전 node에 있던 변수들의 메모리등을 안전하게 해제해주면된다.
그렇기 때문에 cleanup에서 하는 작업과 다소 겹치는 부분이 있다.
어떻게 하는지 코드로 살펴보자.

  • subscription pointer를 reset()해준다.
  • 이 node는 워낙 간단해서 pointer의 메모리만 해제해주면 shutdown의 역할이 끝났다고 볼 수 있다.
  • 그렇기 때문에 on_cleanup()과 코드가 거의 유사하다.
  • 추가로 어떤 state에서 on_shutdown()이 호출 되었는지 출력한다.
  • 성공적으로 on_shutdown()호출이 마무리 되면 CallbackReturn::SUCCESS를 반환한다.

실습 5.1.8. topic_callback 정의

좋아! 이제 lifecycle node 기본 기능(안전하게 끄고, 켜기)을 위한 함수들을 모두 구현했다.

이제 실제 node가 수행해야하는 기능(listenr: 메세지를 듣고 적절한 행동을 취하는 것)을 위한 디테일들을 신경써보자.

먼저 topic message를 구독할 때 수행하고 싶은 동작을 정의해야한다.

  • topic_callback함수는 String type의 msg를 인자로 받는다.
  • on_configure, on_activate, on_deactivate등에 의해 결정되는 m_isActivated 변수가 true인 경우 listener의 역할인 msg의 data를 출력하는 코드를 실행한다.
  • 그렇지 않은 경우 node가 inactive상태(deacitvated)이므로 inactive상태라는 log를 출력한다.

실습 5.1.9. 멤버 변수 정의

이제 우리가 사용했던 멤버 변수들을 초기화해주는 과정을 살펴보자.

  • subscription_을 type에 맞게 선언한다. 이 때 이 때 변수 이름뒤에 붙어있는 underbar는 해당 변수가 멤버변수임을 가리킨다.
  • m_isActivated 변수를 false로 초기화한다. 이 변수에 따라 topic_callback의 동작 여부가 결정된다.

실습 5.1.10. main함수에서 node 생성하기

드이어 대망의 node 생성하는 부분이다.

실제로 node가 생성되고 동작하는 코드이므로 매우 중요한 부분이라고 볼 수 있다.

  • rclcpp::init으로 ROS2 node등을 사용할 것을 알린다. init을 해줘서 node의 run time등을 초기화해준다고 간단히 생각할 수 있다.
  • shared_ptr을 이용해서 MinimalSubscriber 객체를 생성한다.
  • 객체가 생성되면서 앞서 구현했던 lifecycle node가 생성된다.
  • 생성한 node의 base interface를 get_node_base_interface()로 획득하여 rclcpp::spin에 넘겨준다.
  • 기본적인 node interface를(node를 통제할 수 있는 수단 같은 것) 넘겨준다고 이해하면 쉽다.
  • rclcpp::spin은 callback들이 호출 될 수 있게 해주므로 꼭 코드에 포함되어야한다.
  • 가끔 node 객체를 생성하고 spin을 빼먹는 경우가 있는데 이러면 ROS2는 아무 동작을 하지않으므로 주의하자!
  • 프로그램을 종료시키는 명령어등(ctrl+C) 등이 들어오면 rclcpp::shutdown()을 통해 해당 ROS2 코드를 종료한다.
  • 성공적으로 프로그램이 끝났음을 알리는 0을 반환하고 main 함수를 종료한다.

하나하나 설명하느라 길었지만 결국에는 lifecycle node에 subscription이 있을 때도 on_configure, on_activate, on_deactive, on_cleanup, on_shutdown이 lifecyce node의 현재 상태를 어떤 상태로 만들어야하는지만 기억하면 구현하는 것 자체는 간단하다!

실습 5.2. CMakeLists.txt와 package.xml 작성

source code를 작성했으니 이제 CMakeLists.txt와 package.xml을 수정해주자.

다음 코드를 적절히 추가해준다.

full code는 github repository에 업로드해놨으니 참고하자!

  • executable 추가하기 위한 함수들을 작성한다.
add_executable(lifecycle_listener src/lifecycle_listener.cpp)
ament_target_dependencies(lifecycle_listener rclcpp std_msgs rclcpp_lifecycle)

  • executable을 install하기 위한 함수를 작성한다.
install(TARGETS
  lifecycle_talker
  lifecycle_listener
  #lifecycle_service_client
  DESTINATION lib/${PROJECT_NAME})
  • 참고 lifecycle_Service_client라는 executable을 아직 작성하지 않았으므로 실습시 제거하도록하자.(실습 6에서 작성할 예정)

-package.xml 같은 경우는 lifecycle_listener가 lifecycle_talker와 의존 패키지들(rclcpp, std_msgs, rclcpp_lifecycle)이 동일하므로 따로 추가할 것이 없다.

실습 5.2.1. build 및 실행하기

이제 build하면 된다.
다음 명령어들을 각각 실행해주자.

cd ~/simple_lifecycle_ws
colcon build

빌드가 완료되면 실행하자.

cd ~/simple_lifecycle_ws
source install/setup.bash
ros2 run simple_lifecycle_pkg lifecycle_listener

아마 아무 메세지도 출력되지 않을 것이다.

당연하다. rqt를 열어서 lifecycle_talker 실습에서 처럼 service call을 해보자.

이는 실습 5.3의 개인 연습으로 남겨두도록 하겠다.

힌트: change_state 서비스를 호출 할 때 현재 lifecycle node상태와 호출하려는 상태의 순서를 주의하자.
잘 모르겠을 시 lifecycle node state machine 그림을 참고하면서 천천히 해보자.

실습 5.3 rqt 통한 lifecycle node 제어 service 호출 (개인연습)


실습6. 껐다 켰다 하는 service 코드 만들기

  • rqt의 service caller를 통해 lifecycle node를 껐다 켰다 했는데, 이는 service client code를 통해서도 같은 동작이 가능하다는 의미이다.

  • service client code에 대해 개념이 가물가물 한 사람들은 예전 내용을 가볍게 읽어보도록 하자.

  • Service Server 와 Client 코드 작성하기 링크

실습 6.1. 코드 작성

  • 먼저 src 폴더에 "lifecycle_service_client.cpp" 라는 파일을 만들자.

  • 일단 다음의 코드를 복사 붙여넣자. 해당 코드는 여기 에서 발췌 및 수정했음을 참고하자.

// Copyright 2016 Open Source Robotics Foundation, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include <chrono>
#include <memory>
#include <string>
#include <thread>

#include "lifecycle_msgs/msg/state.hpp"
#include "lifecycle_msgs/msg/transition.hpp"
#include "lifecycle_msgs/srv/change_state.hpp"
#include "lifecycle_msgs/srv/get_state.hpp"

#include "rclcpp/rclcpp.hpp"

#include "rcutils/logging_macros.h"

using namespace std::chrono_literals;

// which node to handle
static constexpr char const *lifecycle_node = "lc_talker";

// Every lifecycle node has various services
// attached to it. By convention, we use the format of
// <node name>/<service name>.
// In this demo, we use get_state and change_state
// and thus the two service topics are:
// lc_talker/get_state
// lc_talker/change_state
static constexpr char const *node_get_state_topic = "minimal_publisher/get_state";
static constexpr char const *node_change_state_topic = "minimal_publisher/change_state";

template <typename FutureT, typename WaitTimeT>
std::future_status
wait_for_result(
    FutureT &future,
    WaitTimeT time_to_wait)
{
  auto end = std::chrono::steady_clock::now() + time_to_wait;
  std::chrono::milliseconds wait_period(100);
  std::future_status status = std::future_status::timeout;
  do
  {
    auto now = std::chrono::steady_clock::now();
    auto time_left = end - now;
    if (time_left <= std::chrono::seconds(0))
    {
      break;
    }
    status = future.wait_for((time_left < wait_period) ? time_left : wait_period);
  } while (rclcpp::ok() && status != std::future_status::ready);
  return status;
}

class LifecycleServiceClient : public rclcpp::Node
{
public:
  explicit LifecycleServiceClient(const std::string &node_name)
      : Node(node_name)
  {
  }

  void
  init()
  {
    // Every lifecycle node spawns automatically a couple
    // of services which allow an external interaction with
    // these nodes.
    // The two main important ones are GetState and ChangeState.
    client_get_state_ = this->create_client<lifecycle_msgs::srv::GetState>(
        node_get_state_topic);
    client_change_state_ = this->create_client<lifecycle_msgs::srv::ChangeState>(
        node_change_state_topic);
  }

  /// Requests the current state of the node
  /**
   * In this function, we send a service request
   * asking for the current state of the node
   * lc_talker.
   * If it does return within the given time_out,
   * we return the current state of the node, if
   * not, we return an unknown state.
   * \param time_out Duration in seconds specifying
   * how long we wait for a response before returning
   * unknown state
   */
  unsigned int
  get_state(std::chrono::seconds time_out = 3s)
  {
    auto request = std::make_shared<lifecycle_msgs::srv::GetState::Request>();

    if (!client_get_state_->wait_for_service(time_out))
    {
      RCLCPP_ERROR(
          get_logger(),
          "Service %s is not available.",
          client_get_state_->get_service_name());
      return lifecycle_msgs::msg::State::PRIMARY_STATE_UNKNOWN;
    }

    // We send the service request for asking the current
    // state of the lc_talker node.
    auto future_result = client_get_state_->async_send_request(request).future.share();

    // Let's wait until we have the answer from the node.
    // If the request times out, we return an unknown state.
    auto future_status = wait_for_result(future_result, time_out);

    if (future_status != std::future_status::ready)
    {
      RCLCPP_ERROR(
          get_logger(), "Server time out while getting current state for node %s", lifecycle_node);
      return lifecycle_msgs::msg::State::PRIMARY_STATE_UNKNOWN;
    }

    // We have an succesful answer. So let's print the current state.
    if (future_result.get())
    {
      RCLCPP_INFO(
          get_logger(), "Node %s has current state %s.",
          lifecycle_node, future_result.get()->current_state.label.c_str());
      return future_result.get()->current_state.id;
    }
    else
    {
      RCLCPP_ERROR(
          get_logger(), "Failed to get current state for node %s", lifecycle_node);
      return lifecycle_msgs::msg::State::PRIMARY_STATE_UNKNOWN;
    }
  }

  /// Invokes a transition
  /**
   * We send a Service request and indicate
   * that we want to invoke transition with
   * the id "transition".
   * By default, these transitions are
   * - configure
   * - activate
   * - cleanup
   * - shutdown
   * \param transition id specifying which
   * transition to invoke
   * \param time_out Duration in seconds specifying
   * how long we wait for a response before returning
   * unknown state
   */
  bool
  change_state(std::uint8_t transition, std::chrono::seconds time_out = 3s)
  {
    auto request = std::make_shared<lifecycle_msgs::srv::ChangeState::Request>();
    request->transition.id = transition;

    if (!client_change_state_->wait_for_service(time_out))
    {
      RCLCPP_ERROR(
          get_logger(),
          "Service %s is not available.",
          client_change_state_->get_service_name());
      return false;
    }

    // We send the request with the transition we want to invoke.
    auto future_result = client_change_state_->async_send_request(request).future.share();

    // Let's wait until we have the answer from the node.
    // If the request times out, we return an unknown state.
    auto future_status = wait_for_result(future_result, time_out);

    if (future_status != std::future_status::ready)
    {
      RCLCPP_ERROR(
          get_logger(), "Server time out while getting current state for node %s", lifecycle_node);
      return false;
    }

    // We have an answer, let's print our success.
    if (future_result.get()->success)
    {
      RCLCPP_INFO(
          get_logger(), "Transition %d successfully triggered.", static_cast<int>(transition));
      return true;
    }
    else
    {
      RCLCPP_WARN(
          get_logger(), "Failed to trigger transition %u", static_cast<unsigned int>(transition));
      return false;
    }
  }

private:
  std::shared_ptr<rclcpp::Client<lifecycle_msgs::srv::GetState>> client_get_state_;
  std::shared_ptr<rclcpp::Client<lifecycle_msgs::srv::ChangeState>> client_change_state_;
};

/**
 * This is a little independent
 * script which triggers the
 * default lifecycle of a node.
 * It starts with configure, activate,
 * deactivate, activate, deactivate,
 * cleanup and finally shutdown
 */
void callee_script(std::shared_ptr<LifecycleServiceClient> lc_client)
{
  rclcpp::WallRate time_between_state_changes(0.1); // 10s

  // configure
  {
    if (!lc_client->change_state(lifecycle_msgs::msg::Transition::TRANSITION_CONFIGURE))
    {
      return;
    }
    if (!lc_client->get_state())
    {
      return;
    }
  }

  // activate
  {
    time_between_state_changes.sleep();
    if (!rclcpp::ok())
    {
      return;
    }
    if (!lc_client->change_state(lifecycle_msgs::msg::Transition::TRANSITION_ACTIVATE))
    {
      return;
    }
    if (!lc_client->get_state())
    {
      return;
    }
  }

  // deactivate
  {
    time_between_state_changes.sleep();
    if (!rclcpp::ok())
    {
      return;
    }
    if (!lc_client->change_state(lifecycle_msgs::msg::Transition::TRANSITION_DEACTIVATE))
    {
      return;
    }
    if (!lc_client->get_state())
    {
      return;
    }
  }

  // activate it again
  {
    time_between_state_changes.sleep();
    if (!rclcpp::ok())
    {
      return;
    }
    if (!lc_client->change_state(lifecycle_msgs::msg::Transition::TRANSITION_ACTIVATE))
    {
      return;
    }
    if (!lc_client->get_state())
    {
      return;
    }
  }

  // and deactivate it again
  {
    time_between_state_changes.sleep();
    if (!rclcpp::ok())
    {
      return;
    }
    if (!lc_client->change_state(lifecycle_msgs::msg::Transition::TRANSITION_DEACTIVATE))
    {
      return;
    }
    if (!lc_client->get_state())
    {
      return;
    }
  }

  // we cleanup
  {
    time_between_state_changes.sleep();
    if (!rclcpp::ok())
    {
      return;
    }
    if (!lc_client->change_state(lifecycle_msgs::msg::Transition::TRANSITION_CLEANUP))
    {
      return;
    }
    if (!lc_client->get_state())
    {
      return;
    }
  }

  // and finally shutdown
  // Note: We have to be precise here on which shutdown transition id to call
  // We are currently in the unconfigured state and thus have to call
  // TRANSITION_UNCONFIGURED_SHUTDOWN
  {
    time_between_state_changes.sleep();
    if (!rclcpp::ok())
    {
      return;
    }
    if (!lc_client->change_state(lifecycle_msgs::msg::Transition::TRANSITION_UNCONFIGURED_SHUTDOWN))
    {
      return;
    }
    if (!lc_client->get_state())
    {
      return;
    }
  }
}

void wake_executor(std::shared_future<void> future, rclcpp::executors::SingleThreadedExecutor &exec)
{
  future.wait();
  // Wake the executor when the script is done
  // https://github.com/ros2/rclcpp/issues/1916
  exec.cancel();
}

int main(int argc, char **argv)
{
  // force flush of the stdout buffer.
  // this ensures a correct sync of all prints
  // even when executed simultaneously within the launch file.
  setvbuf(stdout, NULL, _IONBF, BUFSIZ);

  rclcpp::init(argc, argv);

  auto lc_client = std::make_shared<LifecycleServiceClient>("lc_client");
  lc_client->init();

  rclcpp::executors::SingleThreadedExecutor exe;
  exe.add_node(lc_client);

  std::shared_future<void> script = std::async(
      std::launch::async,
      std::bind(callee_script, lc_client));
  auto wake_exec = std::async(
      std::launch::async,
      std::bind(wake_executor, script, std::ref(exe)));

  exe.spin_until_future_complete(script);

  rclcpp::shutdown();

  return 0;
}

뭐가 굉장히 길다..

포기할까? 당연히 아니다.

너무 겁먹을게 없는게 반복되는 작업이 많아서 그렇지 원리는 간단한다

  • configure 호출한다.
  • activate 호출한다.
  • deactivate 호출한다.
  • cleanup 호출한다.
  • shutdown 호출한다.

이걸 code로 작성하기 위해 부가적인 코드들이 많이 추가됐을 뿐이다.

자 이제 그 무서워보이는 코드. 파헤쳐보자!!

6.2. 코드 분석

6.2.1. 필요 헤더파일 추가하기

역시 코드 작성 제일 처음에는 헤더파일들을 추가해줘야한다.
그래야 원하는 함수등을 쓸 수 있을테니 말이다.

  • chrono, memory, string은 많이 소개했으니 넘어가자.
  • thread 헤더
    • 이 헤더는 std::async와 같은 함수를 제공한다. thread란 정말 간단하게 생각해서 일하는 사람이라고 생각할 수 있다.
      따라서 std::async같은 함수를 사용하게되면 사람A가 업무1을 하고 있는데 동시에 사람B가 업무2를 사람A가 일을 하고 있던 말던 할 수 있게 해준다.
  • state.hpp
    • lifecycle_msgs라는 package에서 온 이 헤더파일은 lifecycle node 관련 state를 나타내는 (e.g. lifecycle_msgs::msg::State::PRIMARY_STATE_UNKNOWN) 데이터를 사용하기 위해 include한다.
  • transition.hpp
    • 이 헤더는 lifecycle node의 state간 변화(변이) 과정을 나타내는 lifecycle_msgs::msg::Transition::TRANSITION_CLEANUP 과 같은 데이터를 사용하기 위해 include한다.
  • change_state.hpp
    • 이 헤더파일을 lifecycle node의 상태를 변경할 때 필요한 service message를 사용하기 위해 추가한다.
  • get_state.hpp
    • 이 헤더파일을 lifecycle node의 상태를 확인할 때 필요한 service message를 사용하기 위해 추가한다.
  • rclcpp.hpp 많이 나왔으므로 생략
  • logging_macros.h
    • 이 헤더파일을 RCUTILS_LOG_INFO와 같은 log 출력 매크로를 사용하기 위해 추가한다.
  • using namespace std::chrono_literals
    • 이 namespace는 많이 설명했으므로 생략

6.2.2. lifecycle node 및 service 이름(service topic) 변수 설정

  • 위와 같이 lifecycle node 및 service 이름을 나타내는 변수를 설정한다.
  • 여기서 lifecycle node 이름인 "lc_talker"는 우리가 앞서 개발했던 lifecycle_talker를 가리킨다.
  • 여기서 "node_get_state_topic"와 "node_change_state_topic"는 우리가 service client로 호출할 service topic 이름을 설정하는 변수이다.
  • lifecycle_talker executable에서 제공되는 service의 이름은 각각 "minimal_publisher/get_state"와 "minimal_publisher/change_state"이다.
  • 그 밖에도 몇가지 낯선 키워드들이 눈에 보인다. 간단히 살펴보자
  • static: class를 객체로 만들지 않아도 외부에서 접근할 수 있는 변수를 만든다.
  • constexpr: 컴파일 시간에 변수가 아닌 상수 취급을 하여 처리할 수 있다. 프로그램 속도등을 최적화 할 때 사용할 수 있는 프로그래밍 키워드이다. 낯설다면 안써도 된다.
  • const: 해당 변수는 한번 정해지면 변경할 수 없다는 뜻이다. node이름, service topic 이름등을 변경할 수 있는 시스템을 만들지 않겠다는 뜻에서 이 변수들을 const 처리한다.
  • char *: string의 시작점의 주소를 나타낼 때 사용하는 키워드이다. *를 사용하면 pointer(주소)를 사용하게 된다고 생각하면 기억하기 수월하다.

6.2.3. client node를 위한 class 만들기

service를 호출할 client는 node형태로 만들어진다.
이미 알다시피 node는 class 형태로 만들 수 있다.

  • Node를 상속 받아서 LifecycleServiceClient라는 클래스를 선언한다.
  • class의 생성자는 node_name을 인자로 받는다.
  • 이 node_name으로 Node의 이름을 초기화 시킨다.
  • 해당 class를 생성할 때 인자인 node_name을 명시적으로 사용하게 하기 위해서 explicit이라는 키워드를 사용했다.
  • 이 class의 생성자에서는 node_name을 초기화하는 것 이외에는 딱히 다른 기능을 하지않는다.

6.2.4. client 생성하는 함수 구현하기

이 node의 주요 기능은 lifecycle_talker의 service를 request하는 것이다. 따라서 client가 필요하므로 client들을 만들어주자.

먼저 client 변수들을 private으로 만들어준다.

  • client들은 rclcpp::Client의 shared pointer로 만들어지고, lifecycle_msgs의 service message인 GetState, ChangeState를 각각 사용한다.

이 client들은 이제 init()함수가 호출될 때 다음과 같이 생성된다.

  • this(node class를 가리킴)->create_client를 이용해서 앞서 정의했던 node_get_state_topic과 node_change_State_topic 이름에 해당하는 service를 request하는 client를 생성한다.
  • 이 client는 service message를 구성하여 request할 때 두고두고 사용될 예정이다.

6.2.5. get_state 함수 구현(part1)

그럼 이제 이 client 들을 사용하는 함수를 구현해보자.

lifecycle node의 상태를 바꾸고 나서 유효한 상태인지 체크하는 것이 디버깅할 때 유리하다.

그러면 node 상태를 획득할 수 있는 get_state함수를 살펴보자.

client_get_state를 사용해서 lifecycle node의 현재 상태를 획득할 것이다.

먼저 아래 코드를 살펴보자.

  • 먼저 state를 get하기 위해 필요한 request 변수를 만들어준다.
  • 그 다음 앞서 만들어 놨던 client인 clientget_state의 wait_for_service 함수를 이용해서 이 client가 이용하고자 하는 service가 현재 유효할 때까지 기다린다.
  • 그런데 가게가 열지 않을 때 계속 기다리면 의미가 없듯이, 여기서도 time_out 만큼만 기다린다.
  • time_out값은 get_state함수의 인자로 들어오는데 기본값은 3초로 설정했다.
  • 3초동안 client가 이용하고자하는 service인 "minimal_publisher/get_state"가 없으면 현재 사용불가능하다는 메세지를 출력하고, 현재 상태가 불분명하다는 의미인 PRIMARY_STATE_UNKNOWN이라는 값을 반환한다.
  • 만약 service가 이용가능하면 앞서 만들었던 request를 async_send_request 함수를 사용해서 service server에 보내고 그 결과를 future_result로 받는다.
  • 이렇게 획득한 future_result는 해당 작업이 끝났는지를 체크할 때 사용된다.
  • 즉, 어떤 이유에 의해서 service request가 즉각적으로 처리되서 lifecycle node의 state를 바로 획득할 수도 있고 그렇지 못 할 수 도있다.
  • 참고: future라는 이름이 붙은 이유는 코드가 불린 시점(request 한 이후)에 결과가 나오기 때문에 미래라는 의미에서 future를 붙인 것이다.
  • 그래서 이 future_result값을 time_out과 함께 wait_for_result라는 함수에 전달한다.
  • 이 함수는 service request 작업이 time_out 이전에 끝났는지를 판단하는 결과를 반환한다. (자세한 설명은 하단에서 wait_for_reuslt함수 설명을 참고)

6.2.6 get_state 함수 구현(part2)

이어서 함수의 나머지 부분을 살펴보자.

  • 먼저 wait_for_result로 future_result 가 time_out이내에 들어왔는지 확인한다.
  • time_out 이내에 result가 준비가 됐다면 future_status 값은 std::future_status::ready가 되어야 한다.
  • 그렇지 않은 경우 현재 service server에서 request처리가 무슨 이유에서인지 시간초과 됐다고 알리고 현재 lifecycle node 상태는 불분명 하다는 값을 반환한다.
  • future_status가 준비된 경우 future_result.get()을 통해서 future_result가 유효한 값을 갖고 있는지 확인한다.
  • future_result.get()이 nullptr이면 제대로 상태를 받지 못한 것인데 그 이유로는 service server가 어떤 이유에서인지 유효한 값을 반환하지 못했거나, 응답이 없거나 할 수 있다.
  • 그럴 경우 에러 메세지와 함께 현 상태 알수 없다는 값을 반환한다.
  • future_result.get()이 nullptr가 아닌 유효한 경우, current_state.label(active, inactive, configured등) 을 log로 출력하고 label에 대응되는 current_state.id를 반환한다.

이렇게 lifecycle의 node 상태를 획득하는 client 코드를 구현했다.
복잡한거 같지만 service server가 응답하지 않을 수 있는 상태를 여러 if문에 거쳐 확인하고 node 상태를 유효하게 획득한 경우에만 처리하는 것을 길게 써놓았을 뿐이다.

목적은 간단하다. node의 상태 획득하는거!

6.2.7 wait_for_result 함수 구현

그렇다면 get_state 함수에서 사용된 wait_for_result는 어떻게 구현될까?

실제 숫자를 넣어 생각하면 수월하니 예시를 들어 살펴보자.

  • time_to_wait 가 10초라고 가정하자
  • steady_clock::now()에서 얻은 현재 시간이 50초라고 하면 end 값은 60초가된다.
  • 즉 현재 시간 50초로부터 60초까지만 기다릴 것이다.
  • 기다리는 주기(wait_period)는 100 milisecond(0.1초) 이다.
  • 기본적으로 status는 시간초과라고 설정한다.
  • do-while 구문에 진입하면 현재 시간 now를 획득한다. now를 51초라고 가정하자
  • end 60초에서 now 51초를 뺀 9초를 time_left라고 하자.
  • time_left가 0 이하가 되면 do-while문을 break하고 현재 status를 반환한다.
  • 그렇지만 아직 time_left가 9초이므로 break되지 않는다.
  • time_left와 wait_period 중 작은 시간을 선택해 future는 그 시간만큼 기다린다.
  • 여기선 wait_period 0.1초가 더 적으므로 0.1초동안 기다려서 status를 획득한다.
  • 아직 아직 status가 ready상태가 아니면 do-while 구문을 반복한다.
  • now는 계속 시간이 흐름에 따라 값이 증가하므로 언젠가 이 do-while 구문은 status가 ready가되지 않아도 break된다.
  • status 가 ready 된 순간에도 do-while 구문은 동작을 멈춘다.

이러한 절차를 거쳐 service로 요청한 작업이 준비가 됐는지 안됐는지를 판단한다.

아직 많이 낯설다면 "아 이렇게 service 요청을 기다리는 코드를 짤 수 있구나" 하고 넘어가고 나중에 직접 짜야할 때 해당 코드를 참고하면된다. 걱정은 No!

6.2.8 change_state 함수 구현

앞서 node 상태 확인하는 함수를 구현했는데, 이제는 어떤 함수를 구현해야할까?

그렇다! 본격적으로 node의 상태를 바꾸는 함수가 필요하다.

이 코드에서는 change_state함수를 통해 해당 기능을 수행하고 있다.

어떻게 바꾸는지 하나하나 살펴보자.

  • transition과 time_out을 인자로 받는다.
  • transition은 바꾸고자 하는 node의 상태를 위한 행동이다. 예를 들어 node를 active로 바꾸기위해서는 activate를해야 하므로 이때 transition은 "lifecycle_msgs::msg::Transition::TRANSITION_ACTIVATE" 이 된다.
  • time_out은 어떤 행동의 시간초과를 판단할 때 사용하는 인자이다. 고정값은 3초이다.
  • 먼저 ChangeState::Request type의 service request를 생성한다.
  • request의 transition.id 값을 transition으로 설정한다. 이 값은 node를 어떤 상태로 바꿀 것인지를 결정한다.
  • clientchange_state의 wait_for_service 함수를 통해서 time_out에 설정되어 있는 시간동안 기다리면서(e.g. 3초) 이 client가 이용할 service가 준비되어 있는지 확인한다.
  • 준비되어 있지 않다면 false를 반환하고 함수를 종료한다. false는 node 상태 변경을 실패했다는 의미이다.
  • service가 준비됐으면 앞서 만든 상태 변화 request를 async_send_request를 통해 보내고 future_result를 획득한다.
  • 참고로 여기서 share()를 사용하면 이 future_result를 다른 thread에서도 사용할 수 있다. 참고만 하자.
  • 그 후 get_state에서와 마찬가지로 wait_for_result함수에 인자로 넘겨주어 future_status가 ready 인지 timeout인지 판단한다.
  • future_status가 ready가 아니면 service server에서 request를 처리하는 동안 시간초과를 해버린 것이므로 변경 실패라고 판단하고 함수를 종료한다.
  • 반면에 timeout 시간내에 future_state가 ready됐으면 future_result.get()->sucess 또한 nullptr이 아닌 유효한 값인지 확인하여 성공했으면 성공한 상태를, 실패했으면 실패했다는 메세지를 각각 출력한다.
  • node 상태 변화를 성공했으면 true, 아니면 false를 반환한다.

복잡해보이지만 change_state 절차를 정리하자면 다음과 같다. 괄호 안에 있는 것은 실생활에 비유한 예시이니 참고하도록하자.
1. request 메세지를 만든다. (e.g. 먹고 싶은 음식 생각)
2. service가 준비되었는지 확인한다.(e.g. 식당 열렸는지 확인)
3. request를 보낸다. (e.g. 키오스크로 음식주문)
4. request가 시간내 처리됐는지 확인 (e.g. 음식이 너무 늦게 나오지 않는지 확인)
5. request가 제대로 처리됐는지 확인 (e.g. 내가 시킨 음식이 알맞게 나왔는지 확인)

코드는 긴거 같지만 논리는 별거아닌 것 같다. ㅎㅎ

LifecycleServiceClient class내에서 구현해야할 것은 다했다.

이제 뭐가 더 필요할까?

6.2.9. callee_script 함수 구현하기

LifecycleServiceClient class내 멤버 method를 상태를 바꿀때마다 호출하면 되지만 이 과정을 좀 더 자동화할 순 없을까?

이를 callee_script라는 함수를 추가로 구현해서 처리할 수 있다.

lifecycle node 상태를 제어하는 시나리오는 무수히 많겠지만 이 함수에서는 다음과 같은 시나리오로 node 상태를 변화시킬 것이다.

  • configure -> activate ->deactivate -> activate -> deactivate -> cleanup -> shutdown

자 그럼 어떻게 코드를 구성할 수 있는지 파헤쳐보자!

  • LifecycleServiceClient의 shared_ptr을 인자로 받는다.
  • 이 lc_client는 node 상태를 변경하고 획득하는데 사용될 것이다.
  • WallRate를 이용해서 time_between_state_changes라는 것을 선언한다. 상태변화 시도간 얼마나 쉴지를 의미하며 0.1은 0.1 Hz 즉 1초당 0.1번 즉 10초의 간격을 준다는 의미이다.
  • 앞서 구현해놓은 change_state 함수를 TRANSITION_CONFIGURE 인자로 호출한다.
  • congifure 동작을 수행하기 위한 인자이다.
  • 상태 변경을 정상적으로 성공했을 때 다음 if 구문으로 넘어간다.
  • get_state method를 호출하여 유효한 상태로 변경됐는지 확인한다.
  • 유효한 상태일 때만 다음 구문으로 넘어간다.
  • 10초동안 동작이 완료될 때 까지 대기한다.
  • configure를 해서 inactive상태이다. 이 node를 activate하자. lifecycle_msgs::msg::Transition::TRANSITION_ACTIVATE 를 change_state method의 인자로 넘겨주면 된다.
  • 역시 잘 변경됐는지 상태를 체크한다.

  • 10초동안 동작이 완료될 때 까지 대기한다.
  • deactivete 과정을 진행한다. transition을 위한 인자는 lifecycle_msgs::msg::Transition::TRANSITION_DEACTIVATE를 사용한다.
  • 다시 activate한다.

  • 다시 deactivate한다.
  • cleanup 과정도 유사하다. transition을 위한 인자는 lifecycle_msgs::msg::Transition::TRANSITION_CLEANUP를 사용한다.

  • 마지막으로 unconfigured 상태일 때 shutdown을 진행한다. 이렇게 shutdown의 종류를 구분 짓는 이유는 shutdown은 unconfigured, inactive, active 모든 상태에서 호출 될 수 있기 때문이다.

6.2.10. main 함수 작성

자 그럼 이제 본격적으로 callee_script를 호출할 main 함수를 작성해보자.

아직 안배운 executor가 등장하지만 겁먹지 말자!

  • servbuf로 출력한 메세지를 위한 버퍼를 세팅하는 구문을 작성한다. 이 글에서 학습하고자하는 주제와 크게 연관되지 않으니 넘어가도록 하자.
  • rclcpp::init으로 ROS를 사용할 준비하자.
  • LifecycleServiceClient class의 객체를 share_ptr 로 만든 lc_client를 생성한다.
  • 이 객체의 init method로 client들을 생성한다.
  • rclcpp::executors::SingleThreadedExecutor 타입의 exe를 선언한다.
  • 나중에 배우겠지만 executor는 node의 기능을 수행하는 일꾼들이라 생각하면 된다. 이 일꾼들이 어떻게 일하는가에 따라 그 타입이 결정된다.
  • 일꾼들에게 lc_client라는 class(node)를 add_node라는 함수를 통해서 추가해준다. 일꾼들이 무슨 일을 해야하는지 알려주기 위함이다.
  • 여기서부터 조금 헷갈릴 수 있는데 그냥 읽어보고 "아 lifecycle node 바꾸는 작업을 이렇게 요청했구나~" 정도로 생각하고 넘어가도 좋다.
  • 먼저 std::bind를 사용해서 callee_script가 호출될 때 방금 생성한 lc_client를 인자로 받도록 한다.
  • 그리고 std::aync에서 std::launch::async 정책을 이용하여 callee_script를 호출하는데 이 정책은 "지금 새로운 thread(일 할수 있는 사람)으로 callee_script 처리해줘" 라는 의미이다.
  • 그리고 그 결과는 shared_future 타입으로 script에 반환되게 된다.
  • 그럼 이 shared_future 타입 script를 wake_executor라는 함수의 인자로 exe와 같이 std::bind로 묶어서 전달해주게 된다. 이 과정 역시 새로운 thread를 만들어서 실행하는 정책을 사용한다.
  • 그 결과는 wake_exec에 저장한다.
  • wake_executor 함수에서 어떤 일이 벌어지는 지는 밑에서 설명하겠다.
  • 그럼 callee_script는 지금 실행되고 있는 중인데 시간이 꽤 걸린다. 왜냐면 앞서 봤듯이 우리는 node 상태를 10초에 한번씩 변경하고 있기 때문이다.
  • 그렇기 때문에 exe.spin_until_future_complete(script) 함수를 통해서 shared_future script가 완료되길 기다린다.
  • callee_script가 완료 되면 wake_executor내에서 또 다른 작업이 이뤄진다.
  • 바로 살펴보자!

  • 코드는 간단하다.
  • 먼저 인자로 들어온 future(여기선 script에 해당)의 wait()를 호출한다.
  • 이렇게되면 이 future를 위한 작업이 다 진행될 때까지(여기선 callee_script) 기다리는 효과를 얻을 수 있다.
  • 그 후 call_script가 다 끝났다면 wait()가 막고 있던 코드 다음으로 넘어가 exec.cancel()이 호출된다.
  • 이 exec.cancel()은 우리가 만들었던 executor에서 하고 있는 동작들을 정지시키고 메모리를 정리한다.
  • 우리 프로그램에서 이제 executor를 사용하지 않을 것이니까 치우는 과정이라고 보면된다.
  • 그러면 main 함수에서 rclcpp::shutdown()이 호출되고 프로그램이 0을 반환하고 종료된다.

자. 뭔가 호출하고 기다리는 과정이 많았다.
특히 async라는 표현과 future, wait 등 낯선 용어들이 있었는데 간단히 요약하면

  • async는 다른일도 하면서 새로운일도 동시에 하고 싶을 때 (비동기적) 사용되는 경우가 많다.
  • future는 async로 수행되는 업무는 시간이 좀 걸릴 수 있으므로 업무 수행 시간에 대한 미래 결과를 관리할 수 있게 도와준다.
  • wait는 async로 수행되는 업무가 끝날 때까지 기다리는 역할을 한다.

전체적으로 다시 main 함수를 정리하자면

  • node 상태 변경 client를 만든다.
  • client로 상태 변경하는 코드를 async로 호출한다.
  • 결과를 기다린다.
  • 안전하게 일한 사람(thread)를 정리한다.
  • 프로그램을 종료한다.

자 이렇게 대망의 lifecycle_service_client.cpp 분석이 끝났다. 기지개 쭉 펴고 빌드하기 위해 CMakeLists.txt와 package.xml을 수정하러 가자!

6.3 CMakeLists.txt 와 package.xml 수정 및 빌드

  • CMakeLists.txt 수정

CMakeLists.txt에 다음과 같이 executable 이름과 dependencies를 추가한다. 해당 executable을 lifecycle_msgs에 의존성을 갖는다는 것을 상기하자. 전체 완성본은 github코드를 참고하기!

add_executable(lifecycle_service_client src/lifecycle_service_client.cpp)
ament_target_dependencies(lifecycle_service_client
  "lifecycle_msgs"
  "rclcpp_lifecycle"
)

추가로 lifecycle_service_client를 설치하는 코드를 다음과 같이 추가한다.

install(TARGETS
  lifecycle_talker
  lifecycle_listener
  lifecycle_service_client
  DESTINATION lib/${PROJECT_NAME})
  • package.xml 수정
    lifecycle_msgs에 의존하므로 다음을 추가한다.
<depend>lifecycle_msgs</depend>

변경사항들을 저장하고 새로운 터미널을 열어 빌드한다.

cd ~/simple_lifecycle_ws
colcon build

6.4 lifecycle_service_client.cpp 실행하기

자 그럼 lifecycle_service_client를 실행해보자!

이때 lifecycle_talker를 먼저 실행해주는 것을 잊지말자.

  • lifecycle_talker 실행하기
cd ~/simple_lifecycle_ws
source install/setup.bash
ros2 run simple_lifecycle_pkg lifecycle_talker
  • lifecycle_service_client 실행하기
cd ~/simple_lifecycle_ws
source install/setup.bash
ros2 run simple_lifecycle_pkg lifecycle_service_client

그러면 client가 lifecycle_talker의 상태를 변화시키면서 다음과 같은 메세지들이 각각의 터미널에서 출력될 것이다.

  • client 실행한 터미널

  • talker 실행한 터미널

자세히 살펴보면 inactive -> active -> inactive -> active -> inactive -> unconfigured -> finalized 가 된 것을 확인할 수 있다.

야호!


요약

이번시간에는 이렇게 lifecycle node가 왜 필요하며 어떻게 구현할 수 있는지 알아봤다.

수많은 용어가 등장하긴 했지만 사실 본질은 사용하는 node는 켜주고 사용하지 않는 node는 꺼주고 이상이 있는 node는 어떤 상태에 있는지 등을 파악하기 위한 것으로 간단한다.

그러니 너무 어렵게 생각하지말자!

state machine을 보면서 오늘 배운 용어를 다시 머릿속으로 정리해보자.

  • lifecycle만드는 순서 요약

      1. lifecycle node를 상속받아 클래스를 정의한다.
      1. on_configure, on_activate, on_deactive, on_cleanup, on_shutdown이 각 상태에서 적절히 동작하도록 구현한다.
      1. main함수에서 node의 base interface를 획득하여 spin에 넘겨준다.
      1. CMakeLists.txt를 lifecycle을 사용할 수 있도록 수정한다.
      1. package.xml에서 lifecycle을 사용할 수 있도록 수정한다.

      이렇게 정리하고 보니 흐름을 5줄로 요약이 가능하다.

      별거 없네 하면서 자신감을 갖도록 하자.

      다음시간엔 우리 로봇이 동시에 여러가지 일을 처리할 수 있도록 도와주는 executor와 callback group에 대해 다뤄보도록 하겠다. 많은 관심 바란다.

  • 다음번 주제 executor & callback groups
    관련 원문 링크:

개념

https://docs.ros.org/en/humble/Concepts/Intermediate/About-Executors.html#executors

예제

https://docs.ros.org/en/humble/How-To-Guides/Using-callback-groups.html#using-callback-groups


질문하고 싶거나 인공지능 & 로봇 개발에 대해 다뤄줬으면 하는 주제를 댓글로 남겨주기 바란다~!


참고 문헌


profile
지식이 현실이 되는 공간

5개의 댓글

comment-user-thumbnail
2024년 3월 15일

오웃 라이클사이클 내용이 기네요. 잘 볼게요!

1개의 답글