From 074be34e42aaf7c943253fe05427c3cf1402de1b Mon Sep 17 00:00:00 2001 From: Samuel Lee Date: Sun, 25 Dec 2022 17:47:29 +0100 Subject: [PATCH 1/8] Adding propagation of dictionary changes to PDO and vice versa. Also synchronous RPDOs should now be transmitted on sync reception Need to add more tests for this last part --- canopen/node/local.py | 32 ++++++++++++++++++++++++++++++-- canopen/pdo/base.py | 8 +++++++- test/sample.eds | 2 +- test/test_local.py | 27 ++++++++++++++++++++++++++- 4 files changed, 64 insertions(+), 5 deletions(-) diff --git a/canopen/node/local.py b/canopen/node/local.py index 9e0a80b3..98ffeb76 100644 --- a/canopen/node/local.py +++ b/canopen/node/local.py @@ -3,16 +3,16 @@ from .base import BaseNode from ..sdo import SdoServer, SdoAbortedError -from ..pdo import PDO, TPDO, RPDO +from ..pdo import PDO, TPDO, RPDO, Map from ..nmt import NmtSlave from ..emcy import EmcyProducer +from ..sync import SyncProducer from .. import objectdictionary logger = logging.getLogger(__name__) class LocalNode(BaseNode): - def __init__( self, node_id: int, @@ -31,6 +31,7 @@ def __init__( self.nmt = NmtSlave(self.id, self) # Let self.nmt handle writes for 0x1017 self.add_write_callback(self.nmt.on_write) + self.add_write_callback(self._pdo_update_callback) self.emcy = EmcyProducer(0x80 + self.id) def associate_network(self, network): @@ -42,6 +43,7 @@ def associate_network(self, network): self.emcy.network = network network.subscribe(self.sdo.rx_cobid, self.sdo.on_request) network.subscribe(0, self.nmt.on_command) + network.subscribe(SyncProducer.cob_id,self._on_sync) def remove_network(self): self.network.unsubscribe(self.sdo.rx_cobid, self.sdo.on_request) @@ -126,3 +128,29 @@ def _find_object(self, index, subindex): raise SdoAbortedError(0x06090011) obj = obj[subindex] return obj + + def _pdo_update_callback(self, index: int, subindex: int, od, data): + """Update internal PDO data if the variable is mapped""" + try: + self.pdo[index].raw = data + except KeyError: + try: + self.pdo[index][subindex].raw = data + except KeyError: + pass + + + def _on_sync(self, can_id, data, timestamp) -> None: + """Send TPDOs on sync, node should be in OPERATIONAL state""" + if not self.nmt.state == "OPERATIONAL": + logger.debug("Sync received but nothing will be sent because not in OPERATIONAL") + return + for tpdo in self.tpdo.map.values(): + tpdo : Map + if tpdo.enabled: + tpdo._internal_sync_count += 1 + if tpdo.trans_type <= tpdo._internal_sync_count and tpdo.trans_type <= 0xF0: + # Transmit the PDO once + tpdo.transmit() + # Reset internal sync count + tpdo._internal_sync_count = 0 diff --git a/canopen/pdo/base.py b/canopen/pdo/base.py index 1685de62..958c2dd1 100644 --- a/canopen/pdo/base.py +++ b/canopen/pdo/base.py @@ -7,7 +7,7 @@ from collections import Mapping import logging import binascii - +from .. import node from ..sdo import SdoAbortedError from .. import objectdictionary from .. import variable @@ -193,6 +193,8 @@ def __init__(self, pdo_node, com_record, map_array): self.receive_condition = threading.Condition() self.is_received: bool = False self._task = None + #: Internal sync count for synchronous PDOs (used as a prescaler) + self._internal_sync_count = 0 def __getitem_by_index(self, value): valid_values = [] @@ -297,6 +299,10 @@ def on_message(self, can_id, data, timestamp): with self.receive_condition: self.is_received = True self.data = data + # Also update object dictionary in case of local node + if(isinstance(self.pdo_node.node,node.LocalNode)): + for var in self: + self.pdo_node.node.set_data(var.index,var.subindex,data=var.data) if self.timestamp is not None: self.period = timestamp - self.timestamp self.timestamp = timestamp diff --git a/test/sample.eds b/test/sample.eds index bea6b9c3..1c74e977 100644 --- a/test/sample.eds +++ b/test/sample.eds @@ -298,7 +298,7 @@ ObjectType=7 DataType=7 AccessType=RW PDOMapping=0 -DefaultValue=1614807056 +DefaultValue=0x20010032 [1600sub2] ParameterName=RPDO 1 mapping information 2 diff --git a/test/test_local.py b/test/test_local.py index f4119d44..033dbde0 100644 --- a/test/test_local.py +++ b/test/test_local.py @@ -4,7 +4,9 @@ import logging import time -# logging.basicConfig(level=logging.DEBUG) +logging.basicConfig(level=logging.DEBUG) + +logger = logging.getLogger(__name__) EDS_PATH = os.path.join(os.path.dirname(__file__), 'sample.eds') @@ -251,6 +253,29 @@ def setUpClass(cls): def tearDownClass(cls): cls.network1.disconnect() cls.network2.disconnect() + + def test_od_propagation_to_tpdo(self): + """Test that writing via SDO to local node propagates the data to PDO""" + # data of pdo points to the updated data + self.local_node.pdo.read() + self.local_node.pdo.save() + # Updata the stored data via SDO + self.local_node.sdo["INTEGER16 value"].raw = 11 + # Check propagated correctly in PDO + self.assertEqual(self.local_node.pdo["INTEGER16 value"].raw,11) + + def test_rpdo_propagation_to_od(self): + """Test that received PDO gets propagated to internal OD""" + self.remote_node.pdo.read() + self.remote_node.pdo.save() + # Update remote value in PDO to 25, transmit the RPDO + self.remote_node.pdo["INTEGER16 value"].raw = 25 + # Before sending value should be different from 25 + self.assertNotEqual(self.local_node.pdo["INTEGER16 value"].raw,25) + self.remote_node.rpdo[1].transmit() + # Local node should receive RPDO + self.local_node.rpdo[1].wait_for_reception() + self.assertEqual(self.local_node.pdo["INTEGER16 value"].raw,25) def test_read(self): # TODO: Do some more checks here. Currently it only tests that they From 5e673bb8640e2f69eff85071f1446cafd5e89ca1 Mon Sep 17 00:00:00 2001 From: samsam Date: Sun, 15 Jan 2023 20:25:54 +0100 Subject: [PATCH 2/8] - Added possibility to use an external format handler in case EDS is not stored as pure ascii - Added block_transfer bool to use block transfer when reading 0x1021 - Added some documentation example of using upload_eds --- canopen/network.py | 10 +++++++++- canopen/objectdictionary/eds.py | 17 ++++++++++++++--- doc/network.rst | 16 ++++++++++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/canopen/network.py b/canopen/network.py index 00c21cf0..075fcdec 100644 --- a/canopen/network.py +++ b/canopen/network.py @@ -140,6 +140,8 @@ def add_node( node: Union[int, RemoteNode, LocalNode], object_dictionary: Union[str, ObjectDictionary, None] = None, upload_eds: bool = False, + block_transfer: bool = False, + eds_format_handler : Callable = None ) -> RemoteNode: """Add a remote node to the network. @@ -152,6 +154,12 @@ def add_node( :class:`canopen.ObjectDictionary` object. :param upload_eds: Set ``True`` if EDS file should be uploaded from 0x1021. + :param block_transfer: + Set ``True`` if EDS file should be uploaded using block transfer mechanism + This can increase speed if supported + :param eds_format_handler: + Handler for generating .eds in case a custom format is used (object 0x1022) + This is manufacturer specific and can be used to extract a compressed EDS :return: The Node object that was added. @@ -159,7 +167,7 @@ def add_node( if isinstance(node, int): if upload_eds: logger.info("Trying to read EDS from node %d", node) - object_dictionary = import_from_node(node, self) + object_dictionary = import_from_node(node,self,block_transfer,eds_format_handler) node = RemoteNode(node, object_dictionary) self[node.id] = node return node diff --git a/canopen/objectdictionary/eds.py b/canopen/objectdictionary/eds.py index 872df234..933b441e 100644 --- a/canopen/objectdictionary/eds.py +++ b/canopen/objectdictionary/eds.py @@ -167,10 +167,12 @@ def import_eds(source, node_id): return od -def import_from_node(node_id, network): +def import_from_node(node_id, network,block_transfer =False,eds_format_handler = None): """ Download the configuration from the remote node :param int node_id: Identifier of the node :param network: network object + :param block_transfer: use block transfer + :param eds_format_handler: Callable with custom logic that should retunr the EDS """ # Create temporary SDO client sdo_client = SdoClient(0x600 + node_id, 0x580 + node_id, objectdictionary.ObjectDictionary()) @@ -179,9 +181,18 @@ def import_from_node(node_id, network): network.subscribe(0x580 + node_id, sdo_client.on_response) # Create file like object for Store EDS variable try: - eds_fp = sdo_client.open(0x1021, 0, "rt") - od = import_eds(eds_fp, node_id) + if eds_format_handler is not None: + eds_raw_fp = sdo_client.open(0x1021, 0, "rb",block_transfer=block_transfer) + # Do custom handling (extracting,etc) and return an fp to EDS + eds_fp = eds_format_handler(eds_raw_fp) + # Custom format handler must return a fp or string to extracted eds file + + else: + eds_fp = sdo_client.open(0x1021, 0, "rt", block_transfer=block_transfer) + od = import_eds(eds_fp,node_id) + except Exception as e: + print(e) logger.error("No object dictionary could be loaded for node %d: %s", node_id, e) od = None diff --git a/doc/network.rst b/doc/network.rst index 4fad9cd4..6d6cdad0 100644 --- a/doc/network.rst +++ b/doc/network.rst @@ -38,6 +38,22 @@ Add nodes to the network using the :meth:`~canopen.Network.add_node` method:: local_node = canopen.LocalNode(1, '/path/to/master_dictionary.eds') network.add_node(local_node) +Some nodes store their EDS, using object 0x1021.The node can be added using +it's internal EDS in the following way : + + node = network.add_node(6,upload_eds=True) + # Optional block_transfer enabled + node = network.add_node(6,upload_eds = True, block_transfer = True) + # optionally add a handler if the file uses a compressed format. + import zipfile + from io import BytesIO, TextIOWrapper + def def unzip_eds_handler(fp): + zip = zipfile.ZipFile(BytesIO(fp.read())) + io = TextIOWrapper(zip.open(name="device.eds"),encoding="ascii") + return io + node = network.add_node(6,upload_eds = True, block_transfer = True, eds_format_handler=unzip_eds_handler) + + Nodes can also be accessed using the ``Network`` object as a Python dictionary:: for node_id in network: From af804dff6ee3876e2b34c951d23f7fd68b9db81a Mon Sep 17 00:00:00 2001 From: samsam Date: Sun, 15 Jan 2023 21:32:13 +0100 Subject: [PATCH 3/8] - Fixed formating on doc, should be good now --- doc/network.rst | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/doc/network.rst b/doc/network.rst index 6d6cdad0..95302f03 100644 --- a/doc/network.rst +++ b/doc/network.rst @@ -39,19 +39,28 @@ Add nodes to the network using the :meth:`~canopen.Network.add_node` method:: network.add_node(local_node) Some nodes store their EDS, using object 0x1021.The node can be added using -it's internal EDS in the following way : - +it's internal EDS in the following way:: + + # Add node and import EDS from node node = network.add_node(6,upload_eds=True) + # Optional block_transfer enabled node = network.add_node(6,upload_eds = True, block_transfer = True) - # optionally add a handler if the file uses a compressed format. + + # Optionally add a handler if the file uses a specific format in order to + # create an EDS. This is an example with a zip compressed EDS. import zipfile from io import BytesIO, TextIOWrapper + def def unzip_eds_handler(fp): zip = zipfile.ZipFile(BytesIO(fp.read())) io = TextIOWrapper(zip.open(name="device.eds"),encoding="ascii") return io - node = network.add_node(6,upload_eds = True, block_transfer = True, eds_format_handler=unzip_eds_handler) + + node = network.add_node(6, + upload_eds = True, + block_transfer = True, + eds_format_handler = unzip_eds_handler) Nodes can also be accessed using the ``Network`` object as a Python dictionary:: From 368ce5970e5ee9665e8f3e911b3cadc42b42077a Mon Sep 17 00:00:00 2001 From: samsam Date: Sun, 15 Jan 2023 22:07:45 +0100 Subject: [PATCH 4/8] Revert "Adding propagation of dictionary changes to PDO and vice versa." This reverts commit 074be34e42aaf7c943253fe05427c3cf1402de1b. --- canopen/node/local.py | 32 ++------------------------------ canopen/pdo/base.py | 8 +------- test/sample.eds | 2 +- test/test_local.py | 27 +-------------------------- 4 files changed, 5 insertions(+), 64 deletions(-) diff --git a/canopen/node/local.py b/canopen/node/local.py index 98ffeb76..9e0a80b3 100644 --- a/canopen/node/local.py +++ b/canopen/node/local.py @@ -3,16 +3,16 @@ from .base import BaseNode from ..sdo import SdoServer, SdoAbortedError -from ..pdo import PDO, TPDO, RPDO, Map +from ..pdo import PDO, TPDO, RPDO from ..nmt import NmtSlave from ..emcy import EmcyProducer -from ..sync import SyncProducer from .. import objectdictionary logger = logging.getLogger(__name__) class LocalNode(BaseNode): + def __init__( self, node_id: int, @@ -31,7 +31,6 @@ def __init__( self.nmt = NmtSlave(self.id, self) # Let self.nmt handle writes for 0x1017 self.add_write_callback(self.nmt.on_write) - self.add_write_callback(self._pdo_update_callback) self.emcy = EmcyProducer(0x80 + self.id) def associate_network(self, network): @@ -43,7 +42,6 @@ def associate_network(self, network): self.emcy.network = network network.subscribe(self.sdo.rx_cobid, self.sdo.on_request) network.subscribe(0, self.nmt.on_command) - network.subscribe(SyncProducer.cob_id,self._on_sync) def remove_network(self): self.network.unsubscribe(self.sdo.rx_cobid, self.sdo.on_request) @@ -128,29 +126,3 @@ def _find_object(self, index, subindex): raise SdoAbortedError(0x06090011) obj = obj[subindex] return obj - - def _pdo_update_callback(self, index: int, subindex: int, od, data): - """Update internal PDO data if the variable is mapped""" - try: - self.pdo[index].raw = data - except KeyError: - try: - self.pdo[index][subindex].raw = data - except KeyError: - pass - - - def _on_sync(self, can_id, data, timestamp) -> None: - """Send TPDOs on sync, node should be in OPERATIONAL state""" - if not self.nmt.state == "OPERATIONAL": - logger.debug("Sync received but nothing will be sent because not in OPERATIONAL") - return - for tpdo in self.tpdo.map.values(): - tpdo : Map - if tpdo.enabled: - tpdo._internal_sync_count += 1 - if tpdo.trans_type <= tpdo._internal_sync_count and tpdo.trans_type <= 0xF0: - # Transmit the PDO once - tpdo.transmit() - # Reset internal sync count - tpdo._internal_sync_count = 0 diff --git a/canopen/pdo/base.py b/canopen/pdo/base.py index 958c2dd1..1685de62 100644 --- a/canopen/pdo/base.py +++ b/canopen/pdo/base.py @@ -7,7 +7,7 @@ from collections import Mapping import logging import binascii -from .. import node + from ..sdo import SdoAbortedError from .. import objectdictionary from .. import variable @@ -193,8 +193,6 @@ def __init__(self, pdo_node, com_record, map_array): self.receive_condition = threading.Condition() self.is_received: bool = False self._task = None - #: Internal sync count for synchronous PDOs (used as a prescaler) - self._internal_sync_count = 0 def __getitem_by_index(self, value): valid_values = [] @@ -299,10 +297,6 @@ def on_message(self, can_id, data, timestamp): with self.receive_condition: self.is_received = True self.data = data - # Also update object dictionary in case of local node - if(isinstance(self.pdo_node.node,node.LocalNode)): - for var in self: - self.pdo_node.node.set_data(var.index,var.subindex,data=var.data) if self.timestamp is not None: self.period = timestamp - self.timestamp self.timestamp = timestamp diff --git a/test/sample.eds b/test/sample.eds index 1c74e977..bea6b9c3 100644 --- a/test/sample.eds +++ b/test/sample.eds @@ -298,7 +298,7 @@ ObjectType=7 DataType=7 AccessType=RW PDOMapping=0 -DefaultValue=0x20010032 +DefaultValue=1614807056 [1600sub2] ParameterName=RPDO 1 mapping information 2 diff --git a/test/test_local.py b/test/test_local.py index 033dbde0..f4119d44 100644 --- a/test/test_local.py +++ b/test/test_local.py @@ -4,9 +4,7 @@ import logging import time -logging.basicConfig(level=logging.DEBUG) - -logger = logging.getLogger(__name__) +# logging.basicConfig(level=logging.DEBUG) EDS_PATH = os.path.join(os.path.dirname(__file__), 'sample.eds') @@ -253,29 +251,6 @@ def setUpClass(cls): def tearDownClass(cls): cls.network1.disconnect() cls.network2.disconnect() - - def test_od_propagation_to_tpdo(self): - """Test that writing via SDO to local node propagates the data to PDO""" - # data of pdo points to the updated data - self.local_node.pdo.read() - self.local_node.pdo.save() - # Updata the stored data via SDO - self.local_node.sdo["INTEGER16 value"].raw = 11 - # Check propagated correctly in PDO - self.assertEqual(self.local_node.pdo["INTEGER16 value"].raw,11) - - def test_rpdo_propagation_to_od(self): - """Test that received PDO gets propagated to internal OD""" - self.remote_node.pdo.read() - self.remote_node.pdo.save() - # Update remote value in PDO to 25, transmit the RPDO - self.remote_node.pdo["INTEGER16 value"].raw = 25 - # Before sending value should be different from 25 - self.assertNotEqual(self.local_node.pdo["INTEGER16 value"].raw,25) - self.remote_node.rpdo[1].transmit() - # Local node should receive RPDO - self.local_node.rpdo[1].wait_for_reception() - self.assertEqual(self.local_node.pdo["INTEGER16 value"].raw,25) def test_read(self): # TODO: Do some more checks here. Currently it only tests that they From 9480440b5506e3e02256c053c1aeb1804bccf008 Mon Sep 17 00:00:00 2001 From: samsam Date: Sun, 15 Jan 2023 22:09:42 +0100 Subject: [PATCH 5/8] typo --- canopen/objectdictionary/eds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/canopen/objectdictionary/eds.py b/canopen/objectdictionary/eds.py index 933b441e..a66895d9 100644 --- a/canopen/objectdictionary/eds.py +++ b/canopen/objectdictionary/eds.py @@ -172,7 +172,7 @@ def import_from_node(node_id, network,block_transfer =False,eds_format_handler = :param int node_id: Identifier of the node :param network: network object :param block_transfer: use block transfer - :param eds_format_handler: Callable with custom logic that should retunr the EDS + :param eds_format_handler: Callable with custom logic that should return the EDS """ # Create temporary SDO client sdo_client = SdoClient(0x600 + node_id, 0x580 + node_id, objectdictionary.ObjectDictionary()) From 67db91bf4f2a4ef0850a1407ddb17b461378bfc2 Mon Sep 17 00:00:00 2001 From: samsam Date: Sun, 15 Jan 2023 22:10:41 +0100 Subject: [PATCH 6/8] reove empty lines --- canopen/objectdictionary/eds.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/canopen/objectdictionary/eds.py b/canopen/objectdictionary/eds.py index a66895d9..a42035e4 100644 --- a/canopen/objectdictionary/eds.py +++ b/canopen/objectdictionary/eds.py @@ -184,9 +184,8 @@ def import_from_node(node_id, network,block_transfer =False,eds_format_handler = if eds_format_handler is not None: eds_raw_fp = sdo_client.open(0x1021, 0, "rb",block_transfer=block_transfer) # Do custom handling (extracting,etc) and return an fp to EDS - eds_fp = eds_format_handler(eds_raw_fp) # Custom format handler must return a fp or string to extracted eds file - + eds_fp = eds_format_handler(eds_raw_fp) else: eds_fp = sdo_client.open(0x1021, 0, "rt", block_transfer=block_transfer) od = import_eds(eds_fp,node_id) From 6ca7105e62d1c8ee2e3f3c0932d6a88c61a7c039 Mon Sep 17 00:00:00 2001 From: Samuel Lee Date: Mon, 16 Jan 2023 14:41:52 +0100 Subject: [PATCH 7/8] - Fixed typo and added storage_format in network.rst - Added storage_format to handler function & added some logging info --- doc/network.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/network.rst b/doc/network.rst index 95302f03..fffd562d 100644 --- a/doc/network.rst +++ b/doc/network.rst @@ -52,7 +52,7 @@ it's internal EDS in the following way:: import zipfile from io import BytesIO, TextIOWrapper - def def unzip_eds_handler(fp): + def unzip_eds_handler(fp,storage_format): zip = zipfile.ZipFile(BytesIO(fp.read())) io = TextIOWrapper(zip.open(name="device.eds"),encoding="ascii") return io From 3847c92a3790f021823c5510c6c6f43c9de0bc40 Mon Sep 17 00:00:00 2001 From: Samuel Lee Date: Mon, 16 Jan 2023 14:43:21 +0100 Subject: [PATCH 8/8] - Forgot to add 0x1022 part --- canopen/objectdictionary/eds.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/canopen/objectdictionary/eds.py b/canopen/objectdictionary/eds.py index a42035e4..269cfa48 100644 --- a/canopen/objectdictionary/eds.py +++ b/canopen/objectdictionary/eds.py @@ -7,7 +7,7 @@ except ImportError: from ConfigParser import RawConfigParser, NoOptionError, NoSectionError from canopen import objectdictionary -from canopen.sdo import SdoClient +from canopen.sdo import SdoClient, SdoAbortedError logger = logging.getLogger(__name__) @@ -179,14 +179,22 @@ def import_from_node(node_id, network,block_transfer =False,eds_format_handler = sdo_client.network = network # Subscribe to SDO responses network.subscribe(0x580 + node_id, sdo_client.on_response) + # Get Storage format (0x1022) + try: + storage_format = int.from_bytes(sdo_client.upload(0x1022,0),byteorder="little") + except SdoAbortedError as e: + # Default to 0 if not present + storage_format = 0 + logger.debug("Failed to read object 0x1022, for EDS storage format, default to ASCII (0)") # Create file like object for Store EDS variable try: if eds_format_handler is not None: eds_raw_fp = sdo_client.open(0x1021, 0, "rb",block_transfer=block_transfer) - # Do custom handling (extracting,etc) and return an fp to EDS - # Custom format handler must return a fp or string to extracted eds file - eds_fp = eds_format_handler(eds_raw_fp) + # Custom format handler must return a fp or string to extracted EDS file + eds_fp = eds_format_handler(eds_raw_fp,storage_format) else: + if storage_format != 0: + logger.warning("Trying to read EDS with ASCII format (0) even though storage format is not 0") eds_fp = sdo_client.open(0x1021, 0, "rt", block_transfer=block_transfer) od = import_eds(eds_fp,node_id)