@@ -40,6 +40,7 @@ def __init__(self, bus: Optional[can.BusABC] = None, notifier: Optional[can.Noti
40
40
#: :meth:`canopen.Network.connect` is called
41
41
self .bus : Optional [BusABC ] = bus
42
42
self .loop : Optional [asyncio .AbstractEventLoop ] = loop
43
+ self ._tasks : set [asyncio .Task ] = set ()
43
44
#: A :class:`~canopen.network.NodeScanner` for detecting nodes
44
45
self .scanner = NodeScanner (self )
45
46
#: List of :class:`can.Listener` objects.
@@ -119,6 +120,12 @@ def connect(self, *args, **kwargs) -> Network:
119
120
self .bus = can .Bus (* args , ** kwargs )
120
121
logger .info ("Connected to '%s'" , self .bus .channel_info )
121
122
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.
122
129
self .notifier = can .Notifier (self .bus , [], self .NOTIFIER_CYCLE )
123
130
for listener in self .listeners :
124
131
self .notifier .add_listener (listener )
@@ -148,6 +155,15 @@ def __enter__(self):
148
155
def __exit__ (self , type , value , traceback ):
149
156
self .disconnect ()
150
157
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
+
151
167
# FIXME: Implement async "aadd_node"
152
168
153
169
def add_node (
@@ -264,11 +280,44 @@ def notify(self, can_id: int, data: bytearray, timestamp: float) -> None:
264
280
Timestamp of the message, preferably as a Unix timestamp
265
281
"""
266
282
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 )
270
284
self .scanner .on_message_received (can_id )
271
285
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
+
272
321
def check (self ) -> None :
273
322
"""Check that no fatal error has occurred in the receiving thread.
274
323
@@ -397,7 +446,7 @@ def on_message_received(self, msg):
397
446
self .network .notify (msg .arbitration_id , msg .data , msg .timestamp )
398
447
except Exception as e :
399
448
# Exceptions in any callbaks should not affect CAN processing
400
- logger . error ( str ( e ) )
449
+ self . network . on_error ( e )
401
450
402
451
def stop (self ) -> None :
403
452
"""Override abstract base method to release any resources."""
0 commit comments