Skip to content

Commit e58a653

Browse files
authored
Fix and extend PDO selection by record IDs (fixes #607) (#613)
Fix mixed up mapping parameter record index numbers in class PDO. The access by this index number mixed up the two number ranges. The CiA 301 standard defines: * Object 1600h to 17FFh: RPDO mapping parameter * Object 1A00h to 1BFFh: TPDO mapping parameter The test was also wrong, because it added the two tested variables to the TPDO1, while testing the __getitem__ lookup with index 0x1600. Support lookup by mapping record and communication parameter record index in TPDO and RPDO classes, by storing record index offsets for each PdoMaps collection. These two PdoBase-derived classes only supported numeric access based on the sequential index, not with the mapping parameter record index like the PDO class. Implement a fallback to check those if the regular index lookup fails. Fix docstrings to cover whole RPDO / TPDO parameter index ranges. Support lookup by communication parameter record index in generic PDO class, by storing each PDO under two different index numbers in the dummy PdoMaps object's maps attribute. That would duplicate them in sequential access while iterating / counting though, so override the PdoBase dunder methods to explicitly chain just the two sub-sequences rx and tx. This class now uses a proper PdoMaps proxy object as well, instead of overwriting it with a custom dictionary. To allow this dummy usage, adjust its constructor to skip generating entries. Add some explanation why relative indices cannot be used here.
1 parent 593f062 commit e58a653

File tree

3 files changed

+59
-15
lines changed

3 files changed

+59
-15
lines changed

canopen/pdo/__init__.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import itertools
12
import logging
3+
from collections.abc import Iterator
24

35
from canopen import node
46
from canopen.pdo.base import PdoBase, PdoMap, PdoMaps, PdoVariable
@@ -24,23 +26,36 @@ class PDO(PdoBase):
2426
:param tpdo: TPDO object holding the Transmit PDO mappings
2527
"""
2628

27-
def __init__(self, node, rpdo, tpdo):
29+
def __init__(self, node, rpdo: PdoBase, tpdo: PdoBase):
2830
super(PDO, self).__init__(node)
2931
self.rx = rpdo.map
3032
self.tx = tpdo.map
3133

32-
self.map = {}
33-
# the object 0x1A00 equals to key '1' so we remove 1 from the key
34+
self.map = PdoMaps(0, 0, self)
35+
# Combine RX and TX entries, but only via mapping parameter index. Relative index
36+
# numbers would be ambiguous.
37+
# The object 0x1A00 equals to key '1' so we remove 1 from the key
3438
for key, value in self.rx.items():
35-
self.map[0x1A00 + (key - 1)] = value
39+
self.map.maps[self.rx.map_offset + (key - 1)] = value
40+
self.map.maps[self.rx.com_offset + (key - 1)] = value
3641
for key, value in self.tx.items():
37-
self.map[0x1600 + (key - 1)] = value
42+
self.map.maps[self.tx.map_offset + (key - 1)] = value
43+
self.map.maps[self.tx.com_offset + (key - 1)] = value
44+
45+
def __iter__(self) -> Iterator[int]:
46+
return itertools.chain(
47+
(self.rx.map_offset + i - 1 for i in self.rx),
48+
(self.tx.map_offset + i - 1 for i in self.tx),
49+
)
50+
51+
def __len__(self) -> int:
52+
return len(self.rx) + len(self.tx)
3853

3954

4055
class RPDO(PdoBase):
4156
"""Receive PDO to transfer data from somewhere to the represented node.
4257
43-
Properties 0x1400 to 0x1403 | Mapping 0x1600 to 0x1603.
58+
Properties 0x1400 to 0x15FF | Mapping 0x1600 to 0x17FF.
4459
:param object node: Parent node for this object.
4560
"""
4661

@@ -65,7 +80,7 @@ def stop(self):
6580
class TPDO(PdoBase):
6681
"""Transmit PDO to broadcast data from the represented node to the network.
6782
68-
Properties 0x1800 to 0x1803 | Mapping 0x1A00 to 0x1A03.
83+
Properties 0x1800 to 0x19FF | Mapping 0x1A00 to 0x1BFF.
6984
:param object node: Parent node for this object.
7085
"""
7186

canopen/pdo/base.py

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

33
import binascii
4+
import contextlib
45
import logging
56
import math
67
import threading
@@ -33,7 +34,7 @@ class PdoBase(Mapping):
3334

3435
def __init__(self, node: Union[LocalNode, RemoteNode]):
3536
self.network: canopen.network.Network = canopen.network._UNINITIALIZED_NETWORK
36-
self.map: Optional[PdoMaps] = None
37+
self.map: PdoMaps # must initialize in derived classes
3738
self.node: Union[LocalNode, RemoteNode] = node
3839

3940
def __iter__(self):
@@ -45,8 +46,7 @@ def __getitem__(self, key: Union[int, str]):
4546
raise KeyError("PDO index zero requested for 1-based sequence")
4647
if (
4748
0 < key <= 512 # By PDO Index
48-
or 0x1600 <= key <= 0x17FF # By RPDO ID (512)
49-
or 0x1A00 <= key <= 0x1BFF # By TPDO ID (512)
49+
or 0x1600 <= key <= 0x1BFF # By RPDO / TPDO mapping or communication record
5050
):
5151
return self.map[key]
5252
for pdo_map in self.map.values():
@@ -144,17 +144,22 @@ def stop(self):
144144
pdo_map.stop()
145145

146146

147-
class PdoMaps(Mapping):
147+
class PdoMaps(Mapping[int, 'PdoMap']):
148148
"""A collection of transmit or receive maps."""
149149

150-
def __init__(self, com_offset, map_offset, pdo_node: PdoBase, cob_base=None):
150+
def __init__(self, com_offset: int, map_offset: int, pdo_node: PdoBase, cob_base=None):
151151
"""
152152
:param com_offset:
153153
:param map_offset:
154154
:param pdo_node:
155155
:param cob_base:
156156
"""
157157
self.maps: dict[int, PdoMap] = {}
158+
self.com_offset = com_offset
159+
self.map_offset = map_offset
160+
if not com_offset and not map_offset:
161+
# Skip generating entries without parameter index offsets
162+
return
158163
for map_no in range(512):
159164
if com_offset + map_no in pdo_node.node.object_dictionary:
160165
new_map = PdoMap(
@@ -167,7 +172,14 @@ def __init__(self, com_offset, map_offset, pdo_node: PdoBase, cob_base=None):
167172
self.maps[map_no + 1] = new_map
168173

169174
def __getitem__(self, key: int) -> PdoMap:
170-
return self.maps[key]
175+
try:
176+
return self.maps[key]
177+
except KeyError:
178+
with contextlib.suppress(KeyError):
179+
return self.maps[key + 1 - self.map_offset]
180+
with contextlib.suppress(KeyError):
181+
return self.maps[key + 1 - self.com_offset]
182+
raise
171183

172184
def __iter__(self) -> Iterator[int]:
173185
return iter(self.maps)

test/test_pdo.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,12 @@ def test_pdo_getitem(self):
5050
self.assertEqual(node.tpdo[1]['BOOLEAN value 2'].raw, True)
5151

5252
# Test different types of access
53-
by_mapping_record = node.pdo[0x1600]
53+
by_mapping_record = node.pdo[0x1A00]
5454
self.assertIsInstance(by_mapping_record, canopen.pdo.PdoMap)
5555
self.assertEqual(by_mapping_record['INTEGER16 value'].raw, -3)
56+
self.assertIs(node.tpdo[0x1A00], by_mapping_record)
57+
self.assertIs(node.tpdo[0x1800], by_mapping_record)
58+
self.assertIs(node.pdo[0x1800], by_mapping_record)
5659
by_object_name = node.pdo['INTEGER16 value']
5760
self.assertIsInstance(by_object_name, canopen.pdo.PdoVariable)
5861
self.assertIs(by_object_name.od, node.object_dictionary['INTEGER16 value'])
@@ -68,14 +71,28 @@ def test_pdo_getitem(self):
6871
self.assertEqual(by_object_index.raw, 0xf)
6972
self.assertIs(node.pdo['0x2002'], by_object_index)
7073
self.assertIs(node.tpdo[0x2002], by_object_index)
71-
self.assertIs(node.pdo[0x1600][0x2002], by_object_index)
74+
self.assertIs(node.pdo[0x1A00][0x2002], by_object_index)
7275

7376
self.assertRaises(KeyError, lambda: node.pdo[0])
7477
self.assertRaises(KeyError, lambda: node.tpdo[0])
7578
self.assertRaises(KeyError, lambda: node.pdo['DOES NOT EXIST'])
7679
self.assertRaises(KeyError, lambda: node.pdo[0x1BFF])
7780
self.assertRaises(KeyError, lambda: node.tpdo[0x1BFF])
7881

82+
def test_pdo_iterate(self):
83+
node = self.node
84+
pdo_iter = iter(node.pdo.items())
85+
prev = 0 # To check strictly increasing record index number
86+
for rpdo, (index, pdo) in zip(node.rpdo.values(), pdo_iter):
87+
self.assertIs(rpdo, pdo)
88+
self.assertGreater(index, prev)
89+
prev = index
90+
# Continue consuming from pdo_iter
91+
for tpdo, (index, pdo) in zip(node.tpdo.values(), pdo_iter):
92+
self.assertIs(tpdo, pdo)
93+
self.assertGreater(index, prev)
94+
prev = index
95+
7996
def test_pdo_maps_iterate(self):
8097
node = self.node
8198
self.assertEqual(len(node.pdo), sum(1 for _ in node.pdo))

0 commit comments

Comments
 (0)