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()
There is a small issue with the following code:
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):