Skip to content

Commit 3aa08e4

Browse files
committed
Merge remote-tracking branch 'origin/master' into pr/bizfsc/654
2 parents bccbd5c + 7f8347f commit 3aa08e4

19 files changed

Lines changed: 371 additions & 55 deletions

canopen/lss.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def __init__(self) -> None:
8585
self.network: canopen.network.Network = canopen.network._UNINITIALIZED_NETWORK
8686
self._node_id = 0
8787
self._data = None
88-
self.responses = queue.Queue()
88+
self.responses: queue.Queue[bytes] = queue.Queue()
8989

9090
def send_switch_state_global(self, mode):
9191
"""switch mode to CONFIGURATION_STATE or WAITING_STATE

canopen/network.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ def connect(self, *args, **kwargs) -> Network:
107107
if self.bus is None:
108108
self.bus = can.Bus(*args, **kwargs)
109109
logger.info("Connected to '%s'", self.bus.channel_info)
110-
self.notifier = can.Notifier(self.bus, self.listeners, self.NOTIFIER_CYCLE)
110+
if self.notifier is None:
111+
self.notifier = can.Notifier(self.bus, self.listeners, self.NOTIFIER_CYCLE)
111112
return self
112113

113114
def disconnect(self) -> None:
@@ -123,7 +124,11 @@ def disconnect(self) -> None:
123124
if self.bus is not None:
124125
self.bus.shutdown()
125126
self.bus = None
126-
self.check()
127+
try:
128+
self.check()
129+
finally:
130+
# Release notifier after check
131+
self.notifier = None
127132

128133
def __enter__(self):
129134
return self

canopen/node/base.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class BaseNode:
88
"""A CANopen node.
99
1010
:param node_id:
11-
Node ID (set to None or 0 if specified by object dictionary)
11+
Node ID (set to 0 if specified by object dictionary)
1212
:param object_dictionary:
1313
Object dictionary as either a path to a file, an ``ObjectDictionary``
1414
or a file like object.
@@ -25,7 +25,9 @@ def __init__(
2525
object_dictionary = import_od(object_dictionary, node_id)
2626
self.object_dictionary = object_dictionary
2727

28-
self.id = node_id or self.object_dictionary.node_id
28+
self.id = node_id or object_dictionary.node_id or 0
29+
if not 1 <= self.id <= 127:
30+
raise ValueError(f"No valid Node ID provided, {self.id} not in range 1..127")
2931

3032
def has_network(self) -> bool:
3133
"""Check whether the node has been associated to a network."""

canopen/node/local.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import logging
4+
from collections.abc import Callable
45
from typing import Union
56

67
import canopen.network
@@ -17,6 +18,21 @@
1718

1819

1920
class LocalNode(BaseNode):
21+
"""Local CANopen node implementing essential communication services.
22+
23+
This does not provide a full-fledged communication logic stack, but needs
24+
additional application logic to wire up the various services, such as
25+
triggering PDO transmissions according to their communication parameters.
26+
27+
Notable exceptions are a local data store for SDO server access, and using
28+
the Heartbeat Producer Time parameter to control Heartbeat transmission.
29+
30+
:param node_id:
31+
Node ID (set to 0 if specified by object dictionary)
32+
:param object_dictionary:
33+
Object dictionary as either a path to a file, an ``ObjectDictionary``
34+
or a file like object.
35+
"""
2036

2137
def __init__(
2238
self,
@@ -26,8 +42,8 @@ def __init__(
2642
super(LocalNode, self).__init__(node_id, object_dictionary)
2743

2844
self.data_store: dict[int, dict[int, bytes]] = {}
29-
self._read_callbacks = []
30-
self._write_callbacks = []
45+
self._read_callbacks: list[Callable] = []
46+
self._write_callbacks: list[Callable] = []
3147

3248
self.sdo = SdoServer(0x600 + self.id, 0x580 + self.id, self)
3349
self.tpdo = TPDO(self)
@@ -62,10 +78,10 @@ def remove_network(self) -> None:
6278
self.nmt.network = canopen.network._UNINITIALIZED_NETWORK
6379
self.emcy.network = canopen.network._UNINITIALIZED_NETWORK
6480

65-
def add_read_callback(self, callback):
81+
def add_read_callback(self, callback: Callable):
6682
self._read_callbacks.append(callback)
6783

68-
def add_write_callback(self, callback):
84+
def add_write_callback(self, callback: Callable):
6985
self._write_callbacks.append(callback)
7086

7187
def get_data(

canopen/node/remote.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class RemoteNode(BaseNode):
1919
"""A CANopen remote node.
2020
2121
:param node_id:
22-
Node ID (set to None or 0 if specified by object dictionary)
22+
Node ID (set to 0 if specified by object dictionary)
2323
:param object_dictionary:
2424
Object dictionary as either a path to a file, an ``ObjectDictionary``
2525
or a file like object.
@@ -39,7 +39,7 @@ def __init__(
3939
#: Enable WORKAROUND for reversed PDO mapping entries
4040
self.curtis_hack = False
4141

42-
self.sdo_channels = []
42+
self.sdo_channels: list[SdoClient] = []
4343
self.sdo = self.add_sdo(0x600 + self.id, 0x580 + self.id)
4444
self.tpdo = TPDO(self)
4545
self.rpdo = RPDO(self)

canopen/objectdictionary/__init__.py

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def __iter__(self) -> Iterator[int]:
160160
def __len__(self) -> int:
161161
return len(self.indices)
162162

163-
def __contains__(self, index: Union[int, str]):
163+
def __contains__(self, index: object) -> bool:
164164
return index in self.names or index in self.indices
165165

166166
def add_object(self, obj: Union[ODArray, ODRecord, ODVariable]) -> None:
@@ -206,11 +206,11 @@ def __init__(self, name: str, index: int):
206206
#: Name of record
207207
self.name = name
208208
#: Storage location of index
209-
self.storage_location = None
209+
self.storage_location: Optional[str] = None
210210
#: CiA 306 ObjFlags bitfield
211211
self.obj_flags: int = 0
212-
self.subindices = {}
213-
self.names = {}
212+
self.subindices: dict[int, ODVariable] = {}
213+
self.names: dict[str, ODVariable] = {}
214214

215215
def __repr__(self) -> str:
216216
return f"<{type(self).__qualname__} {self.name!r} at {pretty_index(self.index)}>"
@@ -236,10 +236,12 @@ def __len__(self) -> int:
236236
def __iter__(self) -> Iterator[int]:
237237
return iter(sorted(self.subindices))
238238

239-
def __contains__(self, subindex: Union[int, str]) -> bool:
239+
def __contains__(self, subindex: object) -> bool:
240240
return subindex in self.names or subindex in self.subindices
241241

242-
def __eq__(self, other: ODRecord) -> bool:
242+
def __eq__(self, other: object) -> bool:
243+
if not isinstance(other, ODRecord):
244+
return NotImplemented
243245
return self.index == other.index
244246

245247
def add_member(self, variable: ODVariable) -> None:
@@ -267,11 +269,11 @@ def __init__(self, name: str, index: int):
267269
#: Name of array
268270
self.name = name
269271
#: Storage location of index
270-
self.storage_location = None
272+
self.storage_location: Optional[str] = None
271273
#: CiA 306 ObjFlags bitfield
272274
self.obj_flags: int = 0
273-
self.subindices = {}
274-
self.names = {}
275+
self.subindices: dict[int, ODVariable] = {}
276+
self.names: dict[str, ODVariable] = {}
275277

276278
def __repr__(self) -> str:
277279
return f"<{type(self).__qualname__} {self.name!r} at {pretty_index(self.index)}>"
@@ -302,7 +304,9 @@ def __len__(self) -> int:
302304
def __iter__(self) -> Iterator[int]:
303305
return iter(sorted(self.subindices))
304306

305-
def __eq__(self, other: ODArray) -> bool:
307+
def __eq__(self, other: object) -> bool:
308+
if not isinstance(other, ODArray):
309+
return NotImplemented
306310
return self.index == other.index
307311

308312
def add_member(self, variable: ODVariable) -> None:
@@ -368,14 +372,16 @@ def __init__(self, name: str, index: int, subindex: int = 0):
368372
self.data_type: Optional[int] = None
369373
#: Access type, should be "rw", "ro", "wo", or "const"
370374
self.access_type: str = "rw"
375+
#: The variable represents a DOMAIN ObjectType
376+
self.is_domain: bool = False
371377
#: Description of variable
372378
self.description: str = ""
373379
#: Dictionary of value descriptions
374380
self.value_descriptions: dict[int, str] = {}
375381
#: Dictionary of bitfield definitions
376382
self.bit_definitions: dict[str, list[int]] = {}
377383
#: Storage location of index
378-
self.storage_location = None
384+
self.storage_location: Optional[str] = None
379385
#: CiA 306 ObjFlags bitfield
380386
self.obj_flags: int = 0
381387
#: CiA 306 Denotation string (DCF only)
@@ -395,7 +401,9 @@ def qualname(self) -> str:
395401
return f"{self.parent.name}.{self.name}"
396402
return self.name
397403

398-
def __eq__(self, other: ODVariable) -> bool:
404+
def __eq__(self, other: object) -> bool:
405+
if not isinstance(other, ODVariable):
406+
return NotImplemented
399407
return (self.index == other.index and
400408
self.subindex == other.subindex)
401409

@@ -421,7 +429,7 @@ def add_value_description(self, value: int, descr: str) -> None:
421429
"""
422430
self.value_descriptions[value] = descr
423431

424-
def add_bit_definition(self, name: str, bits: List[int]) -> None:
432+
def add_bit_definition(self, name: str, bits: list[int]) -> None:
425433
"""Associate bit(s) with a string description.
426434
427435
:param name: Name of bit(s)
@@ -492,8 +500,8 @@ def decode_phys(self, value: int) -> Union[int, bool, float, str, bytes]:
492500

493501
def encode_phys(self, value: Union[int, bool, float, str, bytes]) -> int:
494502
if self.data_type in INTEGER_TYPES:
495-
value /= self.factor
496-
value = int(round(value))
503+
if self.factor != 1:
504+
value = round(value / self.factor)
497505
return value
498506

499507
def decode_desc(self, value: int) -> str:
@@ -516,7 +524,7 @@ def encode_desc(self, desc: str) -> int:
516524
raise ValueError(
517525
f"No value corresponds to '{desc}'. Valid values are: {valid_values}")
518526

519-
def decode_bits(self, value: int, bits: List[int]) -> int:
527+
def decode_bits(self, value: int, bits: list[int]) -> int:
520528
try:
521529
bits = self.bit_definitions[bits]
522530
except (TypeError, KeyError):
@@ -526,7 +534,7 @@ def decode_bits(self, value: int, bits: List[int]) -> int:
526534
mask |= 1 << bit
527535
return (value & mask) >> min(bits)
528536

529-
def encode_bits(self, original_value: int, bits: List[int], bit_value: int):
537+
def encode_bits(self, original_value: int, bits: list[int], bit_value: int):
530538
try:
531539
bits = self.bit_definitions[bits]
532540
except (TypeError, KeyError):

canopen/objectdictionary/eds.py

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def import_eds(source, node_id):
4141
od = ObjectDictionary()
4242

4343
if eds.has_section("FileInfo"):
44-
od.__edsFileInfo = {
44+
od.__edsFileInfo = { # type: ignore[attr-defined] # custom addition
4545
opt: eds.get("FileInfo", opt)
4646
for opt in eds.options("FileInfo")
4747
}
@@ -50,7 +50,7 @@ def import_eds(source, node_id):
5050
linecount = int(eds.get("Comments", "Lines"), 0)
5151
od.comments = '\n'.join([
5252
eds.get("Comments", f"Line{line}")
53-
for line in range(1, linecount+1)
53+
for line in range(1, linecount + 1)
5454
])
5555

5656
if not eds.has_section("DeviceInfo"):
@@ -129,15 +129,15 @@ def import_eds(source, node_id):
129129
storage_location = None
130130

131131
if object_type in (objectcodes.VAR, objectcodes.DOMAIN):
132-
var = build_variable(eds, section, node_id, index)
132+
var = build_variable(eds, section, node_id, object_type, index)
133133
od.add_object(var)
134134
elif object_type == objectcodes.ARRAY and eds.has_option(section, "CompactSubObj"):
135135
arr = ODArray(name, index)
136136
last_subindex = ODVariable(
137137
"Number of entries", index, 0)
138138
last_subindex.data_type = datatypes.UNSIGNED8
139139
arr.add_member(last_subindex)
140-
arr.add_member(build_variable(eds, section, node_id, index, 1))
140+
arr.add_member(build_variable(eds, section, node_id, object_type, index, 1))
141141
arr.storage_location = storage_location
142142
arr.obj_flags = _get_obj_flags(eds, section)
143143
od.add_object(arr)
@@ -161,7 +161,11 @@ def import_eds(source, node_id):
161161
subindex = int(match.group(2), 16)
162162
entry = od[index]
163163
if isinstance(entry, (ODRecord, ODArray)):
164-
var = build_variable(eds, section, node_id, index, subindex)
164+
try:
165+
object_type = int(eds.get(section, "ObjectType"), 0)
166+
except NoOptionError:
167+
object_type = objectcodes.VAR
168+
var = build_variable(eds, section, node_id, object_type, index, subindex)
165169
entry.add_member(var)
166170

167171
# Match [index]Name
@@ -213,7 +217,9 @@ def _calc_bit_length(data_type):
213217
elif data_type == datatypes.INTEGER64:
214218
return 64
215219
else:
216-
raise ValueError(f"Invalid data_type '{data_type}', expecting a signed integer data_type.")
220+
raise ValueError(
221+
f"Invalid data_type '{data_type}', expecting a signed integer data_type."
222+
)
217223

218224

219225
def _signed_int_from_hex(hex_str, bit_length):
@@ -264,13 +270,23 @@ def _get_obj_flags(eds, section):
264270
return 0
265271

266272

267-
def build_variable(eds, section, node_id, index, subindex=0):
268-
"""Creates a object dictionary entry.
273+
def build_variable(
274+
eds: RawConfigParser,
275+
section: str,
276+
node_id: int,
277+
object_type: int,
278+
index: int,
279+
subindex: int = 0
280+
) -> ODVariable:
281+
"""Create a object dictionary entry.
282+
283+
269284
:param eds: String stream of the eds file
270285
:param section:
271286
:param node_id: Node ID
272287
:param index: Index of the CANOpen object
273-
:param subindex: Subindex of the CANOpen object (if presente, else 0)
288+
:param subindex: Subindex of the CANOpen object (if present, else 0)
289+
:param is_domain: variable represents a DOMAIN ObjectType (if present, else False)
274290
"""
275291
name = eds.get(section, "ParameterName")
276292
var = ODVariable(name, index, subindex)
@@ -280,15 +296,19 @@ def build_variable(eds, section, node_id, index, subindex=0):
280296
var.storage_location = None
281297
var.data_type = int(eds.get(section, "DataType"), 0)
282298
var.access_type = eds.get(section, "AccessType").lower()
299+
var.is_domain = object_type == objectcodes.DOMAIN
283300
if var.data_type > 0x1B:
284-
# The object dictionary editor from CANFestival creates an optional object if min max values are used
285-
# This optional object is then placed in the eds under the section [A0] (start point, iterates for more)
286-
# The eds.get function gives us 0x00A0 now convert to String without hex representation and upper case
287-
# The sub2 part is then the section where the type parameter stands
301+
# The object dictionary editor from CANFestival creates an optional object if min max
302+
# values are used. This optional object is then placed in the eds under the section
303+
# [A0] (start point, iterates for more). The eds.get function gives us 0x00A0 now
304+
# convert to String without hex representation and upper case. The sub2 part is then
305+
# the section where the type parameter stands.
288306
try:
289307
var.data_type = int(eds.get(f"{var.data_type:X}sub1", "DefaultValue"), 0)
290308
except NoSectionError:
291-
logger.warning("%s has an unknown or unsupported data type (0x%X)", name, var.data_type)
309+
logger.warning(
310+
"%s has an unknown or unsupported data type (0x%X)", name, var.data_type
311+
)
292312
# Assume DOMAIN to force application to interpret the byte data
293313
var.data_type = datatypes.DOMAIN
294314

@@ -317,16 +337,17 @@ def build_variable(eds, section, node_id, index, subindex=0):
317337
var.default_raw = eds.get(section, "DefaultValue")
318338
if '$NODEID' in var.default_raw:
319339
var.relative = True
320-
var.default = _convert_variable(node_id, var.data_type, eds.get(section, "DefaultValue"))
340+
var.default = _convert_variable(node_id, var.data_type, var.default_raw)
321341
except ValueError:
322342
pass
323343
if eds.has_option(section, "ParameterValue"):
324344
try:
325345
var.value_raw = eds.get(section, "ParameterValue")
326-
var.value = _convert_variable(node_id, var.data_type, eds.get(section, "ParameterValue"))
346+
var.value = _convert_variable(node_id, var.data_type, var.value_raw)
327347
except ValueError:
328348
pass
329-
# Factor, Description and Unit are not standard according to the CANopen specifications, but they are implemented in the python canopen package, so we can at least try to use them
349+
# Factor, Description and Unit are not standard according to the CANopen specifications, but
350+
# they are implemented in the python canopen package, so we can at least try to use them
330351
if eds.has_option(section, "Factor"):
331352
try:
332353
var.factor = float(eds.get(section, "Factor"))
@@ -385,7 +406,8 @@ def export_variable(var, eds):
385406
section = f"{var.index:04X}sub{var.subindex:X}"
386407

387408
export_common(var, eds, section)
388-
eds.set(section, "ObjectType", f"0x{objectcodes.VAR:X}")
409+
object_type = objectcodes.DOMAIN if var.is_domain else objectcodes.VAR
410+
eds.set(section, "ObjectType", f"0x{object_type:X}")
389411
if var.data_type:
390412
eds.set(section, "DataType", f"0x{var.data_type:04X}")
391413
if var.access_type:

0 commit comments

Comments
 (0)