Skip to content

Commit b3b4879

Browse files
committed
fix(pdo): avoid PdoMaps(0,0) aliasing; delegate combined PDO to rx/tx
- Add PdoMaps map_offset/com_offset and __getitem__ fallbacks for record indices - Replace dict merge with _CombinedPdoMaps; route 0x1400-0x17FF/0x1800-0x1BFF in PDO - Expand PdoBase fast path to 0x1400-0x1BFF; fix CiA 301 mapping tests (0x1A00 TPDO) - Add test_pdo_iterate Fixes flawed combined-PDO approach from PR canopen-python#613 (dummy zero offsets).
1 parent 593f062 commit b3b4879

3 files changed

Lines changed: 84 additions & 16 deletions

File tree

canopen/pdo/__init__.py

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import itertools
12
import logging
3+
from collections.abc import Iterator, Mapping
4+
from typing import Union
25

36
from canopen import node
47
from canopen.pdo.base import PdoBase, PdoMap, PdoMaps, PdoVariable
@@ -17,6 +20,35 @@
1720
logger = logging.getLogger(__name__)
1821

1922

23+
class _CombinedPdoMaps(Mapping[int, PdoMap]):
24+
"""Combine RPDO and TPDO :class:`PdoMaps` without dummy zero offsets.
25+
26+
Avoids ``PdoMaps(0, 0, …)`` where ``__getitem__`` fallbacks would alias
27+
``key`` to ``key + 1`` (see discussion on PR #613).
28+
"""
29+
30+
def __init__(self, rx: PdoMaps, tx: PdoMaps):
31+
self.rx = rx
32+
self.tx = tx
33+
34+
def __getitem__(self, key: int) -> PdoMap:
35+
for maps in (self.rx, self.tx):
36+
try:
37+
return maps[key]
38+
except KeyError:
39+
continue
40+
raise KeyError(key)
41+
42+
def __iter__(self) -> Iterator[int]:
43+
return itertools.chain(
44+
(self.rx.map_offset + i - 1 for i in self.rx),
45+
(self.tx.map_offset + i - 1 for i in self.tx),
46+
)
47+
48+
def __len__(self) -> int:
49+
return len(self.rx) + len(self.tx)
50+
51+
2052
class PDO(PdoBase):
2153
"""PDO Class for backwards compatibility.
2254
@@ -28,19 +60,30 @@ def __init__(self, node, rpdo, tpdo):
2860
super(PDO, self).__init__(node)
2961
self.rx = rpdo.map
3062
self.tx = tpdo.map
31-
32-
self.map = {}
33-
# the object 0x1A00 equals to key '1' so we remove 1 from the key
34-
for key, value in self.rx.items():
35-
self.map[0x1A00 + (key - 1)] = value
36-
for key, value in self.tx.items():
37-
self.map[0x1600 + (key - 1)] = value
63+
self.map = _CombinedPdoMaps(self.rx, self.tx)
64+
65+
def __getitem__(self, key: Union[int, str]):
66+
if isinstance(key, int):
67+
if key == 0:
68+
raise KeyError("PDO index zero requested for 1-based sequence")
69+
if 0 < key <= 512:
70+
return self.map[key]
71+
if 0x1400 <= key <= 0x17FF:
72+
return self.rx[key]
73+
if 0x1800 <= key <= 0x1BFF:
74+
return self.tx[key]
75+
for pdo_map in self.map.values():
76+
try:
77+
return pdo_map[key]
78+
except KeyError:
79+
continue
80+
raise KeyError(f"PDO: {key} was not found in any map")
3881

3982

4083
class RPDO(PdoBase):
4184
"""Receive PDO to transfer data from somewhere to the represented node.
4285
43-
Properties 0x1400 to 0x1403 | Mapping 0x1600 to 0x1603.
86+
Properties 0x1400 to 0x15FF | Mapping 0x1600 to 0x17FF.
4487
:param object node: Parent node for this object.
4588
"""
4689

@@ -65,7 +108,7 @@ def stop(self):
65108
class TPDO(PdoBase):
66109
"""Transmit PDO to broadcast data from the represented node to the network.
67110
68-
Properties 0x1800 to 0x1803 | Mapping 0x1A00 to 0x1A03.
111+
Properties 0x1800 to 0x19FF | Mapping 0x1A00 to 0x1BFF.
69112
:param object node: Parent node for this object.
70113
"""
71114

canopen/pdo/base.py

Lines changed: 13 additions & 4 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: Optional[Union[PdoMaps, Mapping[int, PdoMap]]] = None
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 0x1400 <= key <= 0x1BFF # RPDO/TPDO communication or mapping record
5050
):
5151
return self.map[key]
5252
for pdo_map in self.map.values():
@@ -155,6 +155,8 @@ def __init__(self, com_offset, map_offset, pdo_node: PdoBase, cob_base=None):
155155
:param cob_base:
156156
"""
157157
self.maps: dict[int, PdoMap] = {}
158+
self.com_offset = com_offset
159+
self.map_offset = map_offset
158160
for map_no in range(512):
159161
if com_offset + map_no in pdo_node.node.object_dictionary:
160162
new_map = PdoMap(
@@ -167,7 +169,14 @@ def __init__(self, com_offset, map_offset, pdo_node: PdoBase, cob_base=None):
167169
self.maps[map_no + 1] = new_map
168170

169171
def __getitem__(self, key: int) -> PdoMap:
170-
return self.maps[key]
172+
try:
173+
return self.maps[key]
174+
except KeyError:
175+
with contextlib.suppress(KeyError):
176+
return self.maps[key + 1 - self.map_offset]
177+
with contextlib.suppress(KeyError):
178+
return self.maps[key + 1 - self.com_offset]
179+
raise KeyError(key)
171180

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

test/test_pdo.py

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

52-
# Test different types of access
53-
by_mapping_record = node.pdo[0x1600]
52+
# Test different types of access (0x1A00 = TPDO mapping per CiA 301)
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,27 @@ 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
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+
for tpdo, (index, pdo) in zip(node.tpdo.values(), pdo_iter):
91+
self.assertIs(tpdo, pdo)
92+
self.assertGreater(index, prev)
93+
prev = index
94+
7995
def test_pdo_maps_iterate(self):
8096
node = self.node
8197
self.assertEqual(len(node.pdo), sum(1 for _ in node.pdo))

0 commit comments

Comments
 (0)