ros2 / rclpy

rclpy (ROS Client Library for Python)
Apache License 2.0
283 stars 224 forks source link

Streamline Sharing Parameter Information Between Nodes #938

Open 808brick opened 2 years ago

808brick commented 2 years ago

Feature Request

Feature description

In ROS1, rospy allowed the retrieval of parameters (from the parameter server) with the one-liner:

rospy.get_param("/param_name")

In ROS2, parameters are associated by Node, and retrieving information about another Node's parameters proves to be a pretty big hassle currently in rclpy since you need to declare a server client and do some message processing to do so. An example of this can be seen in this ROS Answers post.

rclpy also lacks an AsynParametersClient and SyncParametersClient which is available in rclcpp (rclcpp GitHub paramet_client.hpp). So if it was possible to implement a similar functionality in rclpy, I think it would greatly help streamline sharing parameter values between Nodes in Python.

Implementation considerations

For SyncParametersClient the return I'd assume is just a list of Parameter objects. For AsynParametersClient I'd assume it would return a Future object (or similar).

Getting parameter values should just return a list of Parameter objects. To set parameter values, it can just take in a list of Parameter objects.

Example usage could look something like this:

import rclpy 
from rclpy.node import Node, 
from rclpy import SyncParametersClient, Parameter

class MyNode(Node):
    def __init__(self):
        super().__init__('my_node')

    # Example of getting parameter from another Node (sync)
    def get_param_1_value(self):
        return SyncParametersClient(self, remote_node_name='other_node_name').get_parameters(['param_1'])[0].value

    # Example of setting parameter from another Node (sync)
    def set_param_2_value(self, value):
        SyncParametersClient(self, remote_node_name='other_node_name').set_parameters([Parameter("param_2", value)])

def main(args=None):
    rclpy.init(args=args)

    my_node = MyNode()

    while rclpy.ok():
        print(my_node.get_param_1_value())
        my_node.set_param_2_value("test_value")
        break

    my_node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

Or alternatively it could be a feature added directly into the Node class as a method similar to that of the current get_parameter method like so:

import rclpy 
from rclpy.node import Node, 
from rclpy import Parameter

class MyNode(Node):
    def __init__(self):
        super().__init__('my_node')

    # Example of getting parameter from another Node (sync)
    def get_param_1_value(self):
        return self.get_remote_parameter_sync(remote_node_name='other_node_name', 'param_1')

    # Example of setting parameter from another Node (sync)
    def set_param_2_value(self, value):
        self.set_remote_parameter_sync(remote_node_name='other_node_name', Parameter("param_2", value))

def main(args=None):
    rclpy.init(args=args)

    my_node = MyNode()

    while rclpy.ok():
        print(my_node.get_param_1_value())
        my_node.set_param_2_value("test_value")
        break

    my_node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

You could then have get_remote_parameter_sync, get_remote_parameter_async, set_remote_parameter_sync, set_remote_parameter_async. This could also be constructed to instead be plural and take arrays instead of single entries.


Edit 1: Added second possible implementation Edit 2: Fixed typos

808brick commented 2 years ago

For the time being I've added two methods to my Node class which will allow me to easily set/retrieve parameter data of "remote nodes" with the following code:

import rclpy 
import rclpy.node
from rclpy.parameter import Parameter
from rcl_interfaces.srv import SetParameters, GetParameters

class Node(rclpy.node.Node):
    def __init__(self, node_name):
        super().__init__(node_name)
        self.type_arr = ["not_set", "bool_value", "integer_value", "double_value", "string_value", 
                         "byte_array_value", "bool_array_value", "integer_array_value", 
                         "double_array_value", "string_array_value"]

    def get_remote_parameter(self, remote_node_name, param_name):
        cli = self.create_client(GetParameters, remote_node_name + '/get_parameters')
        while not cli.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('service not available, waiting again...')
        req = GetParameters.Request()
        req.names = [param_name]
        future = cli.call_async(req)

        while rclpy.ok():
            rclpy.spin_once(self)
            if future.done():
                try:
                    res = future.result()
                    return getattr(res.values[0], self.type_arr[res.values[0].type])
                except Exception as e:
                    self.get_logger().warn('Service call failed %r' % (e,))
                break

    def set_remote_parameter(self, remote_node_name, parameter_name, new_parameter_value):
        cli = self.create_client(SetParameters, remote_node_name + '/set_parameters')
        while not cli.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('service not available, waiting again...')
        req = SetParameters.Request()
        req.parameters = [Parameter(parameter_name, value=new_parameter_value).to_parameter_msg()]
        future = cli.call_async(req)

        while rclpy.ok():
            rclpy.spin_once(self)
            if future.done():
                try:
                    res = future.result()
                    return res.results[0].successful
                except Exception as e:
                    self.get_logger().warn('Service call failed %r' % (e,))
                break

In this case I created a new Node instance using the code above in a file called ros2_helper.py so that I can use this functionality across all my nodes without having to copy the code. Then in future files I just call the following:

import rclpy
from ros2_helper another_import Node

rclpy.init()

a = Node("node_a")

## Prints the value of the remote node parameter
print(a.get_remote_parameter("node_b", "test_param"))  

## Prints True/False depending on if the value of the remote node parameter was successfully changed to the new value
print(a.set_remote_parameter("node_b", "test_param", "new_value"))

a.destory_node()
rclpy.shutdown()

Assuming node_b was already running prior and declared the parameter test_param.

I'm sure this is not efficient / clean code in the slightest, but it does make my life easier when creating new nodes which need parameter information from other nodes. So maybe this can be used as an inspiration for the feature request, or maybe it can just be used as a template by others who are looking for the same type of functionality in the meantime.

-Raymond

clalancette commented 2 years ago

Thanks for the report. I agree that it would be a good idea to have an AsyncParameterClient in rclpy, as that would get rid of some boilerplate.

I'm less convinced about SyncParameterClient. Because it uses a service under the hood, and can take an arbitrary amount of time to complete, you basically would never want to call SyncParameterClient from inside a ROS 2 callback or a timer. If you did, you would deadlock. So the places it is useful is limited, and the use of it is actually dangerous in some cases.

Anyway, this is useful. We don't have any current plans to work on it, but please do consider opening a pull request with the feature implemented and we can consider it.