@@ -40,6 +40,7 @@ def __init__(self, bus: Optional[can.BusABC] = None, notifier: Optional[can.Noti
4040 #: :meth:`canopen.Network.connect` is called
4141 self .bus : Optional [BusABC ] = bus
4242 self .loop : Optional [asyncio .AbstractEventLoop ] = loop
43+ self ._tasks : set [asyncio .Task ] = set ()
4344 #: A :class:`~canopen.network.NodeScanner` for detecting nodes
4445 self .scanner = NodeScanner (self )
4546 #: List of :class:`can.Listener` objects.
@@ -119,6 +120,12 @@ def connect(self, *args, **kwargs) -> Network:
119120 self .bus = can .Bus (* args , ** kwargs )
120121 logger .info ("Connected to '%s'" , self .bus .channel_info )
121122 if self .notifier is None :
123+ # Do not start a can notifier with the async loop. It changes the
124+ # behavior of the notifier callbacks. Instead of running the
125+ # callbacks from a separate thread, it runs the callbacks in the
126+ # same thread as the event loop where blocking calls are not allowed.
127+ # This library needs to support both async and sync, so we need to
128+ # use the notifier in a separate thread.
122129 self .notifier = can .Notifier (self .bus , [], self .NOTIFIER_CYCLE )
123130 for listener in self .listeners :
124131 self .notifier .add_listener (listener )
@@ -148,6 +155,15 @@ def __enter__(self):
148155 def __exit__ (self , type , value , traceback ):
149156 self .disconnect ()
150157
158+ async def __aenter__ (self ):
159+ # FIXME: When TaskGroup are available, we should use them to manage the
160+ # tasks. The user must use the `async with` statement with the Network
161+ # to ensure its created.
162+ return self
163+
164+ async def __aexit__ (self , type , value , traceback ):
165+ self .disconnect ()
166+
151167 # FIXME: Implement async "aadd_node"
152168
153169 def add_node (
@@ -264,11 +280,44 @@ def notify(self, can_id: int, data: bytearray, timestamp: float) -> None:
264280 Timestamp of the message, preferably as a Unix timestamp
265281 """
266282 if can_id in self .subscribers :
267- callbacks = self .subscribers [can_id ]
268- for callback in callbacks :
269- callback (can_id , data , timestamp )
283+ self .dispatch_callbacks (self .subscribers [can_id ], can_id , data , timestamp )
270284 self .scanner .on_message_received (can_id )
271285
286+ def on_error (self , exc : BaseException ) -> None :
287+ """This method is called to handle any exception in the callbacks."""
288+
289+ # Exceptions in any callbaks should not affect CAN processing
290+ logger .exception ("Exception in callback: %s" , exc_info = exc )
291+
292+ def dispatch_callbacks (self , callbacks : List [Callback ], * args ) -> None :
293+ """Dispatch a list of callbacks with the given arguments.
294+
295+ :param callbacks:
296+ List of callbacks to call
297+ :param args:
298+ Arguments to pass to the callbacks
299+ """
300+ def task_done (task : asyncio .Task ) -> None :
301+ """Callback to be called when a task is done."""
302+ self ._tasks .discard (task )
303+
304+ # FIXME: This section should probably be migrated to a TaskGroup.
305+ # However, this is not available yet in Python 3.8 - 3.10.
306+ try :
307+ if (exc := task .exception ()) is not None :
308+ self .on_error (exc )
309+ except (asyncio .CancelledError , asyncio .InvalidStateError ) as exc :
310+ # Handle cancelled tasks and unfinished tasks gracefully
311+ self .on_error (exc )
312+
313+ # Run the callbacks
314+ for callback in callbacks :
315+ result = callback (* args )
316+ if result is not None and asyncio .iscoroutine (result ):
317+ task = asyncio .create_task (result )
318+ self ._tasks .add (task )
319+ task .add_done_callback (task_done )
320+
272321 def check (self ) -> None :
273322 """Check that no fatal error has occurred in the receiving thread.
274323
@@ -397,7 +446,7 @@ def on_message_received(self, msg):
397446 self .network .notify (msg .arbitration_id , msg .data , msg .timestamp )
398447 except Exception as e :
399448 # Exceptions in any callbaks should not affect CAN processing
400- logger . error ( str ( e ) )
449+ self . network . on_error ( e )
401450
402451 def stop (self ) -> None :
403452 """Override abstract base method to release any resources."""
0 commit comments