I have been playing with the old and new (lyrical) executor and things at a lower level.
- The wait_set is very messy, so I totally support getting rid of it.
- The new Python implementation in lyrical is far better than jazzy (I guess it comes from the
EventsCBGExecutormodel), let me just explain so we are on the same page:- Actually the
set_on_new_message_callbackandtake_messagemethods were added in lyrical that’s why there has never been any docs or discussion about them. entity.set_on_new_message_callback(my_callback)bears no data and signals that an event happened on the entity (sub/srv…). This is (I guess) executed by the RMW thread for minimal overhead, so it must not be blocked, otherwise RMW will stall.msg = entity.take_message()is what gets the data (from any thread you want), by taking it from the RMW queue.- That’s very good to separate the event from the data . Let’s say I just want to know that there’s a message, but I want to consume it later (typically for: batching messages and topics together, or avoiding deserialization, or building backpressure). It is now possible with this structure and was not possible before with data-bearing callbacks.
- With this I can create a whole ROS application in
rclpywithout an executor and with executor back-pressure in 50 lines.
- Actually the
So set_on_new_message_callback and take_message are very very powerful and I think they should be exposed. However, I don’t think that counts as a " unified, canonical reference executor" because the user is in charge of scheduling and execution. If we want an executor to spin and normal callbacks, from this, it should be fairly easy to wake-up one (or many) thread to execute msg = entity.take_message() and the user callback. I however don’t know what’s happening on the C side of things.
Example of using rclpy with no executor using set_on_new_message_callback and take_message:
import asyncio
from contextlib import ExitStack, suppress
import rclpy
from rclpy.impl.implementation_singleton import rclpy_implementation as _rclpy
from rclpy.node import Node
from rclpy.qos import qos_profile_default
from rclpy.type_support import check_is_valid_msg_type
from std_msgs.msg import String
async def consume(ros_sub: _rclpy.Subscription, queue: asyncio.Queue):
msg, info = ros_sub.take_message(String, False)
while msg is not None:
await queue.put(msg)
msg, info = ros_sub.take_message(String, False)
async def main():
with ExitStack() as es:
rclpy.init()
es.callback(rclpy.shutdown)
n = Node("afor_dbg")
es.callback(n.destroy_node)
check_is_valid_msg_type(String)
ros_sub = _rclpy.Subscription(
n.handle,
String,
n.resolve_topic_name("example/talker"),
qos_profile_default.get_c_qos_profile(),
)
es.callback(n.destroy_subscription, ros_sub)
queue: asyncio.Queue[String] = asyncio.Queue()
loop = asyncio.get_event_loop()
def react(*_):
asyncio.run_coroutine_threadsafe(consume(ros_sub, queue), loop)
ros_sub.set_on_new_message_callback(react)
es.callback(ros_sub.clear_on_new_message_callback)
while 1:
msg = await queue.get()
print(msg)
if __name__ == "__main__":
with suppress(KeyboardInterrupt):
asyncio.run(main())