How to set/get parameters from another node | ROS 2 Humble Python Tutorial

How to set/get parameters from another node | ROS 2 Humble Python Tutorial

What you will learn:

Parameters in ROS 2 are associated with individual nodes, it is not possible to work with another node’s parameters simply by using the Node API. For example, a node calling get_parameter() can only get its own parameters.

In this tutorial, you’ll learn how to get and set another node’s parameters by using a service call in code. All other parameter services should be straightforward as they are similar.

If you want to learn more advanced ROS 2 topics, including parameters and more, in a practical, hands-on way, check out the course Intermediate ROS 2: https://app.theconstruct.ai/courses/113

Opening the rosject

In order to follow this tutorial, we need to have ROS 2 installed in our system, and ideally a ros2_ws (ROS2 Workspace). To make your life easier, we have already prepared a rosject for that: https://app.theconstruct.ai/l/639b9a55/

Just by copying the rosject (clicking the link above), you will have a setup already prepared for you.

After the rosject has been successfully copied to your own area, you should see a Run button. Just click that button to launch the rosject.

After pressing the Run button, you should have the rosject loaded. Now, let’s head to the next section to get some real practice.

In order to interact with ROS2, we need a terminal.

Let’s open a terminal by clicking the Open a new terminal button.

Open a new Terminal

Create a ROS2 python package with a simple node that has two parameters

In a terminal, create a python package called python_parameters:

cd ~/ros2_ws/src/
ros2 pkg create --build-type ament_python python_parameters --dependencies rclpy

In the same terminal, create a python node called python_parameters_node.py:

touch python_parameters/python_parameters/python_parameters_node.py

Here is a simple node with two parameters of type string (to paste in python_parameters_node.py):

  • The parameters are:
    • my_parameter with value me and description This parameter is mine!
    • your_parameter with value you and description This parameter is yours!

    You can also have other types as listed in the documentation.

  • The node has a timer callback with a period of 1 second just to log what the current parameters are.
import rclpy
import rclpy.node
from rcl_interfaces.msg import ParameterDescriptor

class MinimalParam(rclpy.node.Node):
    def __init__(self):
        super().__init__('minimal_param_node')
        
        my_parameter_descriptor = ParameterDescriptor(description='This parameter is mine!')
        self.declare_parameter('my_parameter', 'me', my_parameter_descriptor)

        your_parameter_descriptor = ParameterDescriptor(description='This parameter is yours!')
        self.declare_parameter('your_parameter', 'you', your_parameter_descriptor)

        self.timer = self.create_timer(1, self.timer_callback)

    def timer_callback(self):
        my_param = self.get_parameter('my_parameter').get_parameter_value().string_value

        self.get_logger().info('I am %s!' % my_param)

        your_param = self.get_parameter('your_parameter').get_parameter_value().string_value

        self.get_logger().info('You are %s!' % your_param)

def main():
    rclpy.init()
    node = MinimalParam()
    rclpy.spin(node)

if __name__ == '__main__':
    main()

Add an entry point to the setup.py file so that it looks like this:

    entry_points={
        'console_scripts': [
            'minimal_param_node = python_parameters.python_parameters_node:main',
        ],
    },

In a terminal, compile the package:

cd ~/ros2_ws/
colcon build --packages-select python_parameters

Then start the node:

source ~/ros2_ws/install/setup.bash
ros2 run python_parameters minimal_param_node

You should get something like this:

[INFO] [1713695285.349594469] [minimal_param_node]: I am me!
[INFO] [1713695285.349875641] [minimal_param_node]: You are you!
[INFO] [1713695286.337758113] [minimal_param_node]: I am me!
[INFO] [1713695286.338776447] [minimal_param_node]: You are you!
[INFO] [1713695287.337323765] [minimal_param_node]: I am me!
[INFO] [1713695287.338010397] [minimal_param_node]: You are you!

Create a simple client node that retrieves another node’s parameters

The goal is to get the parameter values of the node that we just created in the previous section (python_parameters_node.py) from within a new client node.

Create a new Python file for the client node called get_parameters_client_node.py:

touch ~/ros2_ws/src/python_parameters/python_parameters/get_parameters_client_node.py

Here is a simple asynchronous client (to paste in get_parameters_client_node.py):

  • It creates a client with service type GetParameters and service name /minimal_param_node/get_parameters
  • The request message needs to be a list of names of parameters whose values we want to get. In this case, we chose to retrieve the values of both my_parameter and your_parameter
    • Remember that we can use the terminal command ros2 interface show rcl_interfaces/srv/GetParameters to determine the request (and response) fields
  • The send_request method is passed the request message and handles the service call
import rclpy
from rclpy.node import Node
from rcl_interfaces.srv import GetParameters

class MinimalGetParamClientAsync(Node):

    def __init__(self):
        super().__init__('minimal_get_param_client_async')

        self.cli = self.create_client(GetParameters, '/minimal_param_node/get_parameters')

        while not self.cli.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('service not available, waiting again...')
        
        self.req = GetParameters.Request()

    def send_request(self, params_name_list):
        self.req.names = params_name_list

        self.future = self.cli.call_async(self.req)
        rclpy.spin_until_future_complete(self, self.future)
        return self.future.result()

def main():
    rclpy.init()

    minimal_get_param_client = MinimalGetParamClientAsync()

    list_of_params_to_get = ['my_parameter', 'your_parameter']
    response = minimal_get_param_client.send_request(list_of_params_to_get)
    minimal_get_param_client.get_logger().info('First value: %s, Second value: %s'  % (response.values[0].string_value, response.values[1].string_value))

    minimal_get_param_client.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

Add the entry point to the setup.py file so that it looks like this:

...

    entry_points={
        'console_scripts': [
            'minimal_param_node = python_parameters.python_parameters_node:main',
            'minimal_get_param_client_async = python_parameters.get_parameters_client_node:main',
        ],
    },
)

Compile the package:

cd ~/ros2_ws/
colcon build --packages-select python_parameters

Run the client node in one terminal:

source ~/ros2_ws/install/setup.bash
ros2 run python_parameters minimal_get_param_client_async

And the service node in a different terminal:

source ~/ros2_ws/install/setup.bash
ros2 run python_parameters minimal_param_node

You should get something like this in the terminal of the client:

[INFO] [1719039475.331557220] [minimal_get_param_client_async]: service not available, waiting again...
[INFO] [1719039476.333952497] [minimal_get_param_client_async]: service not available, waiting again...
[INFO] [1719039477.336702012] [minimal_get_param_client_async]: service not available, waiting again...
[INFO] [1719039478.338849982] [minimal_get_param_client_async]: service not available, waiting again...
[INFO] [1719039478.591154332] [minimal_get_param_client_async]: First value: me, Second value: you
            
...

Since the client was started first, it was initially waiting for the GetParameters service to be available. After the service started, the client successfully retrieved the parameter values before shutting down.

End all terminal processes before continuing.

Create a simple client node that modifies another node’s parameters

The process here is similar to the previous section, only this time we are setting the parameter values.

Create a new Python file for the client node called set_parameters_client_node.py:

touch ~/ros2_ws/src/python_parameters/python_parameters/set_parameters_client_node.py

Here is a simple asynchronous client (to paste in set_parameters_client_node.py):

  • It creates a client with service type SetParameters and service name /minimal_param_node/set_parameters
  • The request message needs to be a list of the Parameter msg type which has sub-fields to store the parameter name, data type and value
    • Remember that we can use the terminal command ros2 interface show rcl_interfaces/srv/SetParameters to determine the request (and response) fields
  • The send_request method is passed the new parameter values to set and handles the service call
import rclpy
from rclpy.node import Node
from rcl_interfaces.srv import SetParameters
from rcl_interfaces.msg import Parameter, ParameterType

class MinimalSetParamClientAsync(Node):

    def __init__(self):
        super().__init__('minimal_set_param_client_async')

        self.cli = self.create_client(SetParameters, '/minimal_param_node/set_parameters')

        while not self.cli.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('service not available, waiting again...')
        
        self.req = SetParameters.Request()

    def send_request(self, my_parameter_value, your_parameter_value):
        param = Parameter()
        param.name = "my_parameter"
        param.value.type = ParameterType.PARAMETER_STRING
        param.value.string_value = my_parameter_value
        self.req.parameters.append(param)

        param = Parameter()
        param.name = "your_parameter"
        param.value.type = ParameterType.PARAMETER_STRING
        param.value.string_value = your_parameter_value
        self.req.parameters.append(param)

        self.future = self.cli.call_async(self.req)
        rclpy.spin_until_future_complete(self, self.future)
        return self.future.result()

def main():
    rclpy.init()

    minimal_set_param_client = MinimalSetParamClientAsync()

    my_parameter_value = 'a teacher'
    your_parameter_value = 'a student'
    response = minimal_set_param_client.send_request(my_parameter_value, your_parameter_value)
    minimal_set_param_client.get_logger().info('Results: %s'  % (response))

    minimal_set_param_client.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

Add the entry point to the setup.py file so that it looks like this:

...

    entry_points={
        'console_scripts': [
            'minimal_param_node = python_parameters.python_parameters_node:main',
            'minimal_get_param_client_async = python_parameters.get_parameters_client_node:main',
            'minimal_set_param_client_async = python_parameters.set_parameters_client_node:main',
        ],
    },
)

Compile the package:

cd ~/ros2_ws/
colcon build --packages-select python_parameters

Run the service node first:

source ~/ros2_ws/install/setup.bash
ros2 run python_parameters minimal_param_node

Then the client node in another terminal:

source ~/ros2_ws/install/setup.bash
ros2 run python_parameters minimal_set_param_client_async

You should get something like this in the terminal of the client:

[INFO] [1719045088.071267960] [minimal_set_param_client_async]: Results: rcl_interfaces.srv.SetParameters_Response(results=[rcl_interfaces.msg.SetParametersResult(successful=True, reason=''), rcl_interfaces.msg.SetParametersResult(successful=True, reason='')])

And this in the terminal of the service node:

[INFO] [1719045085.623969364] [minimal_param_node]: You are you!
[INFO] [1719045086.613542065] [minimal_param_node]: I am me!
[INFO] [1719045086.614106714] [minimal_param_node]: You are you!
[INFO] [1719045087.613402451] [minimal_param_node]: I am me!
[INFO] [1719045087.613898409] [minimal_param_node]: You are you!
[INFO] [1719045088.613406942] [minimal_param_node]: I am a teacher!
[INFO] [1719045088.613964772] [minimal_param_node]: You are a student!
[INFO] [1719045088.613406942] [minimal_param_node]: I am a teacher!
[INFO] [1719045088.613964772] [minimal_param_node]: You are a student!
    
...

Just like in my previous tutorial (How to manipulate multiple parameters at once from the command line using parameter services), you managed to set multiple parameters at once- only this time it was from within another node! This opens up the possibility of scripted interaction with external parameters rather than being restricted to just the CLI!

Now you know how to interact with parameter services in code.

Congratulations. You now have a basic understanding of parameters and are familiar with creating and using services.

To learn more advanced topics about ROS 2, have a look at the course below:

We hope this post was really helpful to you.

This tutorial is created by Robotics Ambassador Ernest.

Video Tutorial

 

Parameterの使い方 – Japanese ROS2 Tutorial

Parameterの使い方 – Japanese ROS2 Tutorial

This tutorial is created by Robotics Ambassador Takumi

 

Robotics Ambassador: theconstruct.ai/robotics-ambassador/

何を学ぶか

このチュートリアルでは公式チュートリアルの内容を参考にROS Paramを動的に書き換える方法を試してみます。

なんで?

ROS 2になって、Parameterの動的な書き換えが可能になりました。
Parameterを初期値やファイルパスの設定とかに使ってる方割と多いと思うのですが、動的にいじれるとサーバの接続先変えたり、テスト中に設定間違えたパラメータ書き直したりできます。
かなり便利で多用しているので、紹介したいと思いました。

環境

ROS2 Humble
rosject link https://app.theconstruct.ai/l/61071157/

作業

パッケージ作成

以下のコマンドでパッケージを作ります。

ros2 pkg create --build-type ament_cmake --license Apache-2.0 cpp_param_exp --dependencies rclcpp

ソースコード記述

srcディレクトリに参考にc++のコードを書きましょう。

#include 

#include "rclcpp/rclcpp.hpp"

class SampleNodeWithParameters : public rclcpp::Node {
public:
  SampleNodeWithParameters() : Node("node_with_parameters") {
    this->declare_parameter("an_int_param", 0);

    param_subscriber_ = std::make_shared(this);

    auto cb = [this](const rclcpp::Parameter &p) {
      RCLCPP_INFO(
          this->get_logger(),
          "cb: Received an update to parameter \"%s\" of type %s: \"%ld\"",
          p.get_name().c_str(), p.get_type_name().c_str(), p.as_int());
    };
    cb_handle_ = param_subscriber_->add_parameter_callback("an_int_param", cb);
  }

private:
  std::shared_ptr param_subscriber_;
  std::shared_ptr cb_handle_;
};

int main(int argc, char **argv) {
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared());
  rclcpp::shutdown();

  return 0;
}

コードの動作としては、an_int_paramパラメータの更新を受け取ったら、そのデータをログに吐くといういたって単純なモノです。
特徴的な所があるとしたら、コールバック関数の定義にラムダ式を使っている所でしょうか。

コンパイル

次はコンパイルします。
package.xmlとCMakeList.txtの記述が必要ですが、特に難しいことはやってないので、詳しくはRosjectを参照してください。
(sourceはしている前提とします。)

cd ~/ros2_ws/
colcon buildじ

実行

以下のコマンドで実行していきます。

[端末A]

cd ~/ros2_ws/
user:~/ros2_ws$ . install/setup.bash
user:~/ros2_ws$ ros2 run cpp_param_exp parameter_event_handlerき

起動できたら、別の[端末B]を開いて以下のコマンドを実行します。”Set parameter successful”と出れば成功です。

cd ~/ros2_ws/
user:~$ ros2 param set /node_with_parameters an_int_param 10
Set parameter successful

端末Aを見ると以下のメッセージが表示され、parameterの更新に成功していることが分かります。

[INFO] [1714224724.898754729] [node_with_parameters]: cb: Received an update to parameter "an_int_param" of type integer: "10"

また,rqt_reconfigureを使ってguiでデータ設定をする事も出来ます。試しあれ。
[端末B]

 . install/setup.bash
ros2 run rqt_reconfigure rqt_reconfigure

Video Tutorial

Pin It on Pinterest