ROS Resources: Documentation | Support | Discussion Forum | Index | Service Status | ros @ Robotics Stack Exchange
Ask Your Question
0

Dynamically create subcription callback functions (Python)

asked 2022-04-24 23:27:42 -0500

somebody un gravatar image

Hello, I am trying to generalize one of my nodes to subscribe to not just a different set of topics, but a different number of topics, depending on the situation. Specifically I want it to republish the data it receives on a given topic, to a topic with a similar name and the incoming data modified in some way.

Given a dictionary of topic names, I figured I could do this in a loop like so:

class DynamicSubscriptions(Node):

def __init__(self):
    super().__init__("dynamic_subscriptions")

    list_of_subs_to_make = [  # Could come from a parameter, be added as the node runs, ... 
        "one",
        "two",
        "three",
    ]

    self.subscribers = {}
    self.sub_callbacks = {}
    self.pubs = {}

    for topic in list_of_subs_to_make:

        self.pubs[topic] = self.create_publisher(
            Float64,
            topic + '/republished',
            QoSPresetProfiles.SENSOR_DATA.value
        )

        new_cb = self.make_callback(topic)
        self.sub_callbacks[topic] = new_cb

        self.subscribers[topic] = self.create_subscription(
            Float64,  # Leaving these the same for all topics, for simplicity of the example
            topic, 
            new_cb,
            QoSPresetProfiles.SENSOR_DATA.value
        )

    for topic, sub in self.subscribers.items():
        self.get_logger().info(f"topic {topic} subscription {sub}")

def make_callback(self, topic_name):
    def callback(self, msg):
        self.get_logger().info("Inside {topic_name} callback, got {msg}")
    return callback

If I run the node, and try publishing to the topics, I get this error:

root@thomas-Precision-5530-big:/workdir# ros2 run rloc_subsystems dynamic_subscriptions 
[INFO] [1650860469.579535172] [dynamic_subscriptions]: topic one subscription <rclpy.subscription.Subscription object at 0x7fb30ce415b0>
[INFO] [1650860469.580131489] [dynamic_subscriptions]: topic two subscription <rclpy.subscription.Subscription object at 0x7fb30ce41760>
[INFO] [1650860469.580512294] [dynamic_subscriptions]: topic three subscription <rclpy.subscription.Subscription object at 0x7fb30ce41910>
Traceback (most recent call last):
  File "/workdir/install/rloc_subsystems/lib/rloc_subsystems/dynamic_subscriptions", line 9, in <module>
    main()
  File "/workdir/install/rloc_subsystems/lib/python3.8/site-packages/rloc_subsystems/wheel_odometry/dynamic_minimal.py", line 65, in main
    rclpy.spin(node)
  File "/opt/ros/foxy/lib/python3.8/site-packages/rclpy/__init__.py", line 191, in spin
    executor.spin_once()
  File "/opt/ros/foxy/lib/python3.8/site-packages/rclpy/executors.py", line 711, in spin_once
    raise handler.exception()
  File "/opt/ros/foxy/lib/python3.8/site-packages/rclpy/task.py", line 239, in __call__
    self._handler.send(None)
  File "/opt/ros/foxy/lib/python3.8/site-packages/rclpy/executors.py", line 426, in handler
    await call_coroutine(entity, arg)
  File "/opt/ros/foxy/lib/python3.8/site-packages/rclpy/executors.py", line 351, in _execute_subscription
    await await_or_execute(sub.callback, msg)
  File "/opt/ros/foxy/lib/python3.8/site-packages/rclpy/executors.py", line 118, in await_or_execute
    return callback(*args)
TypeError: callback() missing 1 required positional argument: 'msg'
root@thomas-Precision-5530-big:/workdir#

What am I missing? Is there a better way to achieve my goal? I understand this may be a problem with how I am using python, and not a ROS problem.

I am using ROS2 Foxy on Docker using osrf/ros:foxy-desktop image (described here), Python 3.8.5

edit retag flag offensive close merge delete

1 Answer

Sort by ยป oldest newest most voted
1

answered 2022-04-25 03:41:14 -0500

aprotyas gravatar image

updated 2022-04-25 11:42:22 -0500

What's happening is that you're assigning new_cb a local scope function of DynamicSubscriptions.make_callback(). As such, new_cb does not represent a bound method of the DynamicSubscriptions class, i.e. it isn't a method that is bound to an instance of DynamicSubscriptions.

This is a problem because when the executor calls the callback with the arguments (just the published message instance), it populates the self parameter (since it isn't bound to an instance) rather than the msg parameter.

What you can do is return the bound method callback() as such:

def make_callback(self, topic_name):
        def callback(self, msg):
            self.get_logger().info("Inside {topic_name} callback, got {msg}")
        setattr(self, 'callback', callback.__get__(self, self.__class__))
        return self.callback

If you don't need the callback to be a class method, you could also just remove the self parameter and get a free-standing callable. Maybe something like:

def make_callback(self, topic_name):
    def callback(msg):
        print(f'In callback, got {msg}') # can't use rclpy.Node methods now
    return callback
edit flag offensive delete link more

Comments

Thank you for your answer. I will need the callback to be a class method, since I want to publish something using the msg. So I don't think a free-standing callable will work for me.

Your idea of returning the bound method sounds like it should work. However as you have written it, self.callback does not exist, I get:

AttributeError: 'DynamicSubscriptions' object has no attribute 'callback' # <- from the `return self.callback` line

If I add it to the class like so:

def make_callback(self, topic_name):
    def callback(self, msg):
        self.get_logger().info("Inside {topic_name} callback, got {msg}")
    self.cb = callback
    return self.cb

I get the same TypeError as before:

TypeError: callback() missing 1 required positional argument: 'msg'
somebody un gravatar image somebody un  ( 2022-04-25 09:18:50 -0500 )edit

Ah, my bad. I forgot to actually bind the unbound callback method to the class. This updated snippet should work:

def make_callback(self, topic_name):
    def callback(self, msg):
        self.get_logger().info(f'Inside {topic_name} callback, got {msg}')
    setattr(self, 'callback', callback.__get__(self, self.__class__)) # bind
    return self.callback

For implementation reference: https://gist.github.com/hangtwenty/a9...

I've updated my original answer to that effect. Thanks for pointing out the problem!

aprotyas gravatar image aprotyas  ( 2022-04-25 11:41:46 -0500 )edit
1

This works, amazing! Thank you! Today I learned about binding :)

somebody un gravatar image somebody un  ( 2022-04-25 11:47:30 -0500 )edit

Question Tools

1 follower

Stats

Asked: 2022-04-24 23:27:42 -0500

Seen: 813 times

Last updated: Apr 25 '22