Lifecycle Node Demo

Introduction

In this tutorial, we will demonstrate how to trigger a state transition in a lifecycle node. We'll start with a brief introduction to the lifecycle node's states and transitions. Next, we'll clarify a few potentially confusing concepts. Lastly, we'll guide you through a practical example and demonstrate the state transition via the command line and the code.

State and State Transition

According to ROS2 Design Doc - Managed Node (Lifecycle Node), a lifecycle node has the following states and state transitions:

Lifecycle nodes in ROS2 have two state types: primary states and transition states. Transitioning between primary states requires invoking a specific function. For instance, to move a node from inactive to active, we use the activate function. This transition can be initiated either from the command line or directly in the code. During the execution of the transition function, the node enters the corresponding transition states. For example, if the activate function takes 10 seconds to run and we query the node's state during this window, it will return activating.

Before moving on to our demo, it's important to clarify a few key points.

Where are the states defined?

The state class is implemented in the rclcpp/rclcpp_lifecycle package. However, if you check the code, the State class is simply a wrapper of the id and label pair. The class does not contain any clue about the mapping between the id and the actual state name described in the design document. The mapping is defined in the state message of the rcl_interfaces/lifecycle_msgs package. Therefore, the most robust way to check the current state of the lifecycle node in the code is to include lifecycle_msgs/msg/state.hpp and examine the id field of the state object.

How to transit out of the finalized state?

The design doc ROS2 Design Doc - Managed Node (Lifecycle Node) seems to suggest that transitioning out of the finalized state is possible by invoking the destroy method. Yet, when we set the node to the finalized state and attempt to invoke the destroy method, the following results are observed:

learnros2@7D41:~/ros2-lifecycle-node-demo$ ros2 lifecycle get /my_demo_node
finalized [4]
:learnros2@7D41~/ros2-lifecycle-node-demo$ ros2 lifecycle set /my_demo_node destroy
Unknown transition requested, available ones are:

When the node is in the finalized state, it appears that there are no valid transitions available. Indeed, examining the implementation of transition methods in lifecycle_node.hpp within the rclcpp_lifecycle package reveals no reference to the destroy method. This raises the question: what exactly is happening in this scenario?

There are two plausible explanations. Firstly, like the start state, destroyed is a concept state. This means although we understand the node is destroyed, querying it for its current state is not feasible as the node no longer exists. This explains why the destroyed state is not a primary state.

Secondly, the destruction of a node involves memory deallocation by an owner. However, not all nodes are under the supervision of a node manager or similar system, making it challenging to determine the recipient of the destroy command in advance.

Demo Code Setup

The demo code will illustrate the transition from the unconfigured state to the finalized state. We will manually trigger most transitions via the command line, except for the transition from inactive to active, which happens automatically after a pre-defined delay. Components such as timer, subscriber, and publisher are created in the configuring state and become functional in the inactive state. The node will then automatically move to the active state, where it can receive messages and become ready for operation.

The ready state is a user-define concept in our demo code. As suggested in the design document, complex software-level initialization logic can be implemented as a user-defined state machine that runs when the node is in the active state. In our demonstration, the user-defined state machine is trivial: the node transitions from active to ready upon receiving a message from the ready_signal topic.

Transit from Unconfigured to Inactive

Let's first launch the system:

ros2 launch bringup demo.launch.py

It gives the following results:

[INFO] [launch]: Default logging verbosity is set to INFO
[INFO] [demo-1]: process started with pid [280004]

There aren't many outputs at this step. We can check the state of the node and it should be in the unconfigured state:

learnros2@7D41:~/ros2-lifecycle-node-demo$ ros2 lifecycle get /my_demo_node
unconfigured [1]

To transit from the unconfigured state to the inactive state, we can invoke the configure method using the following command:

ros2 lifecycle set /my_demo_node configure

We will see the following outputs in the terminal where we launched the system:

[demo-1] [INFO] [1704042797.997082968] [my_demo_node]: on_configure() is called. Previous state is unconfigured. Internal state: is_ready=false
[demo-1] [INFO] [1704042797.997865606] [my_demo_node]: on_configure is complete. Thread ID is 140308162622336
[demo-1] [INFO] [1704042797.997879296] [my_demo_node]: thread ID in lambda: 140308090152512
[demo-1] [INFO] [1704042798.997785585] [my_demo_node]: Demo node is not active yet and cannot publish messages.
[demo-1] [INFO] [1704042799.997786979] [my_demo_node]: Demo node is not active yet and cannot publish messages.
[demo-1] [INFO] [1704042800.997802945] [my_demo_node]: Demo node is not active yet and cannot publish messages.

We can see the on_configured method is called and the node executes the publish method at a fixed rate. This shows that the timer is activated when the node is in the inactive state. Check the node state again by running the command below

ros2 lifecycle get /my_demo_node

It confirms that the node is in the inactive state.

inactive [2]

If we publish a message to the read_signal topic, we can see that the node is able to subscirbe to the topic when it's in the inactive state. To publish a message, use the following command:

ros2 topic pub --once /ready_signal std_msgs/msg/String "{data: 'Hello from terminal'}"

The output confirms the node can receive the message:

[demo-1] [INFO] [1704043103.047689958] [my_demo_node]: Demo node is not active yet and cannot publish messages.
[demo-1] [INFO] [1704043104.047725655] [my_demo_node]: Demo node is not active yet and cannot publish messages.
[demo-1] [INFO] [1704043104.744767441] [my_demo_node]: Demo node is ready for work now.
[demo-1] [INFO] [1704043105.047731930] [my_demo_node]: Demo node is not active yet and cannot publish messages.
[demo-1] [INFO] [1704043106.047750263] [my_demo_node]: Demo node is not active yet and cannot publish messages.
[demo-1] [INFO] [1704043107.047752290] [my_demo_node]: Demo node is not active yet and cannot publish messages.

Transit from Inactive to Active

We add a small code in the on_configure method to make the transit from inactive to active automatic:

std::thread async_task(
        [this]()
        {
            std::ostringstream ss;
            ss << std::this_thread::get_id();
            RCLCPP_INFO(this->get_logger(), "thread ID in lambda: %s", ss.str().c_str());
            std::this_thread::sleep_for(std::chrono::seconds(20));
            RCLCPP_INFO(this->get_logger(), "Activate the node by itself");
            this->activate();
            RCLCPP_INFO(this->get_logger(), "[lambda] Activation is completed.");
        }
);
async_task.detach();

Note a few details. We use sleep_for to delay the invocation of the activate function and this code runs in a separate thread. The detach is required because we want to separate the execution from the thread object; otherwise, when the execution exits the on_configure method, the thread object will terminate the program (see c++ documentation).

Once the node reaches the inactive state, no further commands are required. Depending on the delay specified in the code, simply wait a few seconds and you should observe the following outputs:

[demo-1] [INFO] [1704044107.059493913] [my_demo_node]: Demo node is not active yet and cannot publish messages.
[demo-1] [INFO] [1704044108.059540732] [my_demo_node]: Demo node is not active yet and cannot publish messages.
[demo-1] [INFO] [1704044108.059703175] [my_demo_node]: Activate the node by itself
[demo-1] [INFO] [1704044108.059876436] [my_demo_node]: on_activate() is called. Previous state is inactive. Internal state: is_ready=false
[demo-1] [INFO] [1704044108.059975961] [my_demo_node]: [lambda] Activation is completed.
[demo-1] [INFO] [1704044109.059562172] [my_demo_node]: Publish message: internal_state: is_ready=false
[demo-1] [INFO] [1704044110.059572780] [my_demo_node]: Publish message: internal_state: is_ready=false
[demo-1] [INFO] [1704044111.059531749] [my_demo_node]: Publish message: internal_state: is_ready=false
[demo-1] [INFO] [1704044112.059586519] [my_demo_node]: Publish message: internal_state: is_ready=false

If you haven't published a message to the ready_signal topic, the ready flag of the node remains false. To set the node to ready, you can manually publish a message from the command line:

ros2 topic pub --once /ready_signal std_msgs/msg/String "{data: 'Hello from terminal'}"

And you should see the following messages:

[demo-1] [INFO] [1704044121.059626693] [my_demo_node]: Publish message: internal_state: is_ready=false
[demo-1] [INFO] [1704044122.059679773] [my_demo_node]: Publish message: internal_state: is_ready=false
[demo-1] [INFO] [1704044123.059692743] [my_demo_node]: Publish message: internal_state: is_ready=false
[demo-1] [INFO] [1704044123.265610986] [my_demo_node]: Demo node is ready for work now.
[demo-1] [INFO] [1704044124.059697042] [my_demo_node]: Publish message: internal_state: is_ready=true
[demo-1] [INFO] [1704044125.059758017] [my_demo_node]: Publish message: internal_state: is_ready=true
[demo-1] [INFO] [1704044126.059715186] [my_demo_node]: Publish message: internal_state: is_ready=true

Download the code

The code is available for download at this link.

©2023 - 2024 all rights reserved

Last updated