2

The problem I am having is that:

  • To start an async function in the background I need an asycio event loop.

  • This event loop usualy exists in the main thread, and when started, blocks the exeuction of that thread (i.e lines of code after starting the event loop aren't run untill the event loop is cancelled).

  • However, ROS2 has it's own event loop (executor) that also usually runs in the main thread and blocks execution. This means it is difficult to have both event loops running

My attempted sollution was to start the asyncio event loop in a seperate thread. This is started in the Node constructor, and stops after the Node is deconstructed.

This looks like this:

class IncrementPercentDoneServiceNode(Node):
    

    def __create_task(self, f: Awaitable):
        self.__task = self.__loop.create_task(f)

    def __init__(self):

        super().__init__('increment_percent_done_service_node')
        
        self.__loop = asyncio.new_event_loop()
        self.__task: Optional[Task] = None
        self.__thread = threading.Thread(target=self.__loop.run_forever)
        self.__thread.start()
        self.done = False

        self.create_service(Trigger, 'start_incrementing',
            callback=lambda request, responce : (
                self.get_logger().info("Starting service"),
                self.__loop.call_soon_threadsafe(self.__create_task, self.__increment_percent_complete()),
                TriggerResponse(success=True, message='')
            )[-1]
        )

    def __del__(self):
        print("stopping loop")
        self.done = True
        if self.__task is not None:
            self.__task.cancel()
            
        self.__loop.stop()
        self.__thread.join()

    async def __increment_percent_complete(self):
        timeout_start = time.time()        
        duration = 5

        while time.time() < (timeout_start + duration):
            time_since_start = time.time() - timeout_start
            percent_complete = (time_since_start / duration) * 100.0
            self.get_logger().info("Percent complete: {}%".format(percent_complete))
            await asyncio.sleep(0.5)

        self.get_logger().info("leaving async function")
        self.done = True

if __name__ == '__main__':

    rclpy.init()
    test = IncrementPercentDoneServiceNode()
    e = MultiThreadedExecutor()
    e.add_node(test)
    e.spin()

Is this a sensible way to do it? Is there a better way? How would I cancel the start_incrementing service with another service? (I know that this is what actions are for, but I cannot use them in this instance).

  • *ROS2 has it's own event loop (executor)* - Is it an event loop or an executor? Those two concepts are quite distinct in Python. If it runs an asyncio event loop, you could hook your coroutines into its loop and thus make use of it. – user4815162342 Nov 26 '20 at 18:13
  • It tunrs out it doesn't use an asyncio even loop: https://github.com/ros2/rclpy/issues/279 Is there any way I can get an async function to work without an asycio event loop? – Craig Hickman Nov 27 '20 at 11:30
  • 1
    That's interesting, so they seem to support coroutines, but not *asyncio* coroutines. In that case I guess your approach is the correct one, although I'd still use _one_ asyncio event loop (created upfront) and use `asyncio.run_coroutine_threadsafe` to submit tasks to it. – user4815162342 Nov 28 '20 at 09:45

0 Answers0