Skip to content

running ros on asyncio loop without active polling #1

Description

@ksuszka

There is a small issue with the following code:

async def ros_loop():
    while rclpy.ok():
        rclpy.spin_once(node, timeout_sec=0)
        await asyncio.sleep(1e-4)

it uses some measurable CPU time and introduces additional latency. You need to choose sleep period which balances CPU/latency.

Here is an attempt to solve this issue by introducing an extension to the ROS executor which makes the executor to wait for a ROS event on a separate thread but delegates handling of the event to asyncio loop. Code is based on rclpy executors code (https://github.com/ros2/rclpy/blob/87fbec0d6bbfda968d14689c977e9a6bdaa48886/rclpy/rclpy/__init__.py#L318, https://github.com/ros2/rclpy/blob/87fbec0d6bbfda968d14689c977e9a6bdaa48886/rclpy/rclpy/executors.py#L902):

import asyncio
from typing import Optional

import rclpy
from rclpy.context import Context
from rclpy.executors import ConditionReachedException
from rclpy.executors import ShutdownException
from rclpy.executors import SingleThreadedExecutor
from rclpy.executors import TimeoutException
from rclpy.node import Node

from std_msgs.msg import String

class AsyncioLoopExecutor(SingleThreadedExecutor):

    def __init__(self, *, context: Optional[Context] = None) -> None:
        super().__init__(context=context)

    async def spin_once_async(self) -> None:
        loop = asyncio.get_event_loop()
        def handle(handler):
            handler()
            if handler.exception() is not None:
                raise handler.exception()
            handler.result()  # raise any exceptions

        def spin_once_thread(loop):
            try:
                handler, entity, node = self.wait_for_ready_callbacks()
            except ShutdownException:
                pass
            except TimeoutException:
                pass
            except ConditionReachedException:
                pass
            else:
                loop.call_soon_threadsafe(handle, handler)
        await asyncio.to_thread(spin_once_thread, loop)

class MinimalPublisher(Node):

    def __init__(self):
        super().__init__('minimal_publisher')
        self.publisher_ = self.create_publisher(String, 'topic', 10)
        timer_period = 0.5  # seconds
        self.timer = self.create_timer(timer_period, self.timer_callback)
        self.i = 0

    def timer_callback(self):
        msg = String()
        msg.data = 'Hello World: %d' % self.i
        self.publisher_.publish(msg)
        self.get_logger().info('Publishing: "%s"' % msg.data)
        self.i += 1

async def ros_loop(node):
    executor = AsyncioLoopExecutor()
    try:
        executor.add_node(node)
        while executor.context.ok():
            await executor.spin_once_async()
    finally:
        executor.remove_node(node)

async def asyncio_sample_work_loop():
    while True:
        print("asyncio work")
        await asyncio.sleep(1)

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

    minimal_publisher = MinimalPublisher()

    try:
        future = asyncio.wait([ros_loop(minimal_publisher), asyncio_sample_work_loop()], return_when=asyncio.FIRST_EXCEPTION)
        done, _pending = asyncio.get_event_loop().run_until_complete(future)
        for task in done:
            task.result()  # raises exceptions if any        
    except KeyboardInterrupt:
        pass
    finally:
        # Destroy the node explicitly
        # (optional - otherwise it will be done automatically
        # when the garbage collector destroys the node object)
        minimal_publisher.destroy_node()
        rclpy.try_shutdown()    


if __name__ == '__main__':
    main()

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions