11from __future__ import annotations
22
3- from collections .abc import MutableMapping
43import logging
54import threading
6- from typing import Callable , Dict , Iterator , List , Optional , Union
5+ from collections .abc import MutableMapping
6+ from typing import Callable , Dict , Final , Iterator , List , Optional , Union
77
88import can
99from can import Listener
10- from can import CanError
1110
12- from canopen .node import RemoteNode , LocalNode
13- from canopen .sync import SyncProducer
14- from canopen .timestamp import TimeProducer
15- from canopen .nmt import NmtMaster
1611from canopen .lss import LssMaster
17- from canopen .objectdictionary .eds import import_from_node
12+ from canopen .nmt import NmtMaster
13+ from canopen .node import LocalNode , RemoteNode
1814from canopen .objectdictionary import ObjectDictionary
15+ from canopen .objectdictionary .eds import import_from_node
16+ from canopen .sync import SyncProducer
17+ from canopen .timestamp import TimeProducer
18+
1919
2020logger = logging .getLogger (__name__ )
2121
2525class Network (MutableMapping ):
2626 """Representation of one CAN bus containing one or more nodes."""
2727
28+ NOTIFIER_CYCLE : float = 1.0 #: Maximum waiting time for one notifier iteration.
29+ NOTIFIER_SHUTDOWN_TIMEOUT : float = 5.0 #: Maximum waiting time to stop notifiers.
30+
2831 def __init__ (self , bus : Optional [can .BusABC ] = None ):
2932 """
3033 :param can.BusABC bus:
@@ -38,7 +41,7 @@ def __init__(self, bus: Optional[can.BusABC] = None):
3841 #: List of :class:`can.Listener` objects.
3942 #: Includes at least MessageListener.
4043 self .listeners = [MessageListener (self )]
41- self .notifier = None
44+ self .notifier : Optional [ can . Notifier ] = None
4245 self .nodes : Dict [int , Union [RemoteNode , LocalNode ]] = {}
4346 self .subscribers : Dict [int , List [Callback ]] = {}
4447 self .send_lock = threading .Lock ()
@@ -72,10 +75,10 @@ def unsubscribe(self, can_id, callback=None) -> None:
7275 If given, remove only this callback. Otherwise all callbacks for
7376 the CAN ID.
7477 """
75- if callback is None :
76- del self .subscribers [can_id ]
77- else :
78+ if callback is not None :
7879 self .subscribers [can_id ].remove (callback )
80+ if not self .subscribers [can_id ] or callback is None :
81+ del self .subscribers [can_id ]
7982
8083 def connect (self , * args , ** kwargs ) -> Network :
8184 """Connect to CAN bus using python-can.
@@ -105,7 +108,7 @@ def connect(self, *args, **kwargs) -> Network:
105108 if self .bus is None :
106109 self .bus = can .Bus (* args , ** kwargs )
107110 logger .info ("Connected to '%s'" , self .bus .channel_info )
108- self .notifier = can .Notifier (self .bus , self .listeners , 1 )
111+ self .notifier = can .Notifier (self .bus , self .listeners , self . NOTIFIER_CYCLE )
109112 return self
110113
111114 def disconnect (self ) -> None :
@@ -117,7 +120,7 @@ def disconnect(self) -> None:
117120 if hasattr (node , "pdo" ):
118121 node .pdo .stop ()
119122 if self .notifier is not None :
120- self .notifier .stop ()
123+ self .notifier .stop (self . NOTIFIER_SHUTDOWN_TIMEOUT )
121124 if self .bus is not None :
122125 self .bus .shutdown ()
123126 self .bus = None
@@ -279,6 +282,21 @@ def __len__(self) -> int:
279282 return len (self .nodes )
280283
281284
285+ class _UninitializedNetwork (Network ):
286+ """Empty network implementation as a placeholder before actual initialization."""
287+
288+ def __init__ (self , bus : Optional [can .BusABC ] = None ):
289+ """Do not initialize attributes, by skipping the parent constructor."""
290+
291+ def __getattribute__ (self , name ):
292+ raise RuntimeError ("No actual Network object was assigned, "
293+ "try associating to a real network first." )
294+
295+
296+ #: Singleton instance
297+ _UNINITIALIZED_NETWORK : Final [Network ] = _UninitializedNetwork ()
298+
299+
282300class PeriodicMessageTask :
283301 """
284302 Task object to transmit a message periodically using python-can's
@@ -308,7 +326,6 @@ def __init__(
308326 self .msg = can .Message (is_extended_id = can_id > 0x7FF ,
309327 arbitration_id = can_id ,
310328 data = data , is_remote_frame = remote )
311- self ._task = None
312329 self ._start ()
313330
314331 def _start (self ):
@@ -375,7 +392,9 @@ class NodeScanner:
375392 SERVICES = (0x700 , 0x580 , 0x180 , 0x280 , 0x380 , 0x480 , 0x80 )
376393
377394 def __init__ (self , network : Optional [Network ] = None ):
378- self .network = network
395+ if network is None :
396+ network = _UNINITIALIZED_NETWORK
397+ self .network : Network = network
379398 #: A :class:`list` of nodes discovered
380399 self .nodes : List [int ] = []
381400
@@ -391,8 +410,6 @@ def reset(self):
391410
392411 def search (self , limit : int = 127 ) -> None :
393412 """Search for nodes by sending SDO requests to all node IDs."""
394- if self .network is None :
395- raise RuntimeError ("A Network is required to do active scanning" )
396413 sdo_req = b"\x40 \x00 \x10 \x00 \x00 \x00 \x00 \x00 "
397414 for node_id in range (1 , limit + 1 ):
398415 self .network .send_message (0x600 + node_id , sdo_req )
0 commit comments