From fd226e3b8c7fe09f933ed10cc42bec8c085366aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frieder=20Sch=C3=BCler?= <frieder.schueler@gmail.com> Date: Fri, 24 Feb 2023 16:52:51 +0100 Subject: [PATCH 01/15] Fixed HighLimit and LowLimit for SIGNED values in EDS (#345) * Fixed incorrect min (LowLImit) and max (HighLimit) values when using signed integer types. * Fixed min/max for non-signed datatypes and added tests * Removed type hints --- canopen/objectdictionary/eds.py | 42 +++++++++++++-- test/sample.eds | 36 +++++++++++++ test/test_eds.py | 92 ++++++++++++++++++++------------- 3 files changed, 128 insertions(+), 42 deletions(-) diff --git a/canopen/objectdictionary/eds.py b/canopen/objectdictionary/eds.py index 872df234..c1c54d78 100644 --- a/canopen/objectdictionary/eds.py +++ b/canopen/objectdictionary/eds.py @@ -1,7 +1,9 @@ -import re -import io -import logging import copy +import logging +import re + +from canopen.objectdictionary import datatypes + try: from configparser import RawConfigParser, NoOptionError, NoSectionError except ImportError: @@ -190,6 +192,28 @@ def import_from_node(node_id, network): return od +def _calc_bit_length(data_type): + if data_type == datatypes.INTEGER8: + return 8 + elif data_type == datatypes.INTEGER16: + return 16 + elif data_type == datatypes.INTEGER32: + return 32 + elif data_type == datatypes.INTEGER64: + return 64 + else: + raise ValueError(f"Invalid data_type '{data_type}', expecting a signed integer data_type.") + + +def _signed_int_from_hex(hex_str, bit_length): + number = int(hex_str, 0) + limit = ((1 << bit_length - 1) - 1) + if number > limit: + return limit - number + else: + return number + + def _convert_variable(node_id, var_type, value): if var_type in (objectdictionary.OCTET_STRING, objectdictionary.DOMAIN): return bytes.fromhex(value) @@ -251,12 +275,20 @@ def build_variable(eds, section, node_id, index, subindex=0): if eds.has_option(section, "LowLimit"): try: - var.min = int(eds.get(section, "LowLimit"), 0) + min_string = eds.get(section, "LowLimit") + if var.data_type in objectdictionary.SIGNED_TYPES: + var.min = _signed_int_from_hex(min_string, _calc_bit_length(var.data_type)) + else: + var.min = int(min_string, 0) except ValueError: pass if eds.has_option(section, "HighLimit"): try: - var.max = int(eds.get(section, "HighLimit"), 0) + max_string = eds.get(section, "HighLimit") + if var.data_type in objectdictionary.SIGNED_TYPES: + var.max = _signed_int_from_hex(max_string, _calc_bit_length(var.data_type)) + else: + var.max = int(max_string, 0) except ValueError: pass if eds.has_option(section, "DefaultValue"): diff --git a/test/sample.eds b/test/sample.eds index bea6b9c3..671a559e 100644 --- a/test/sample.eds +++ b/test/sample.eds @@ -902,3 +902,39 @@ DataType=0x0008 AccessType=ro DefaultValue=0 PDOMapping=1 + +[3020] +ParameterName=INTEGER8 only positive values +ObjectType=0x7 +DataType=0x02 +AccessType=rw +HighLimit=0x7F +LowLimit=0x00 +PDOMapping=0 + +[3021] +ParameterName=UNSIGNED8 value range +2 to +10 +ObjectType=0x7 +DataType=0x05 +AccessType=rw +HighLimit=0x0A +LowLimit=0x02 +PDOMapping=0 + +[3030] +ParameterName=INTEGER32 only negative values +ObjectType=0x7 +DataType=0x04 +AccessType=rw +HighLimit=0x00000000 +LowLimit=0xFFFFFFFF +PDOMapping=0 + +[3040] +ParameterName=INTEGER64 value range -10 to +10 +ObjectType=0x7 +DataType=0x15 +AccessType=rw +HighLimit=0x000000000000000A +LowLimit=0x8000000000000009 +PDOMapping=0 diff --git a/test/test_eds.py b/test/test_eds.py index e5f6c89e..2a6d5098 100644 --- a/test/test_eds.py +++ b/test/test_eds.py @@ -4,6 +4,7 @@ EDS_PATH = os.path.join(os.path.dirname(__file__), 'sample.eds') + class TestEDS(unittest.TestCase): def setUp(self): @@ -47,6 +48,20 @@ def test_record(self): self.assertEqual(var.data_type, canopen.objectdictionary.UNSIGNED32) self.assertEqual(var.access_type, 'ro') + def test_record_with_limits(self): + int8 = self.od[0x3020] + self.assertEqual(int8.min, 0) + self.assertEqual(int8.max, 127) + uint8 = self.od[0x3021] + self.assertEqual(uint8.min, 2) + self.assertEqual(uint8.max, 10) + int32 = self.od[0x3030] + self.assertEqual(int32.min, -2147483648) + self.assertEqual(int32.max, 0) + int64 = self.od[0x3040] + self.assertEqual(int64.min, -10) + self.assertEqual(int64.max, +10) + def test_array_compact_subobj(self): array = self.od[0x1003] self.assertIsInstance(array, canopen.objectdictionary.Array) @@ -98,18 +113,16 @@ def test_dummy_variable_undefined(self): def test_comments(self): self.assertEqual(self.od.comments, -""" + """ |-------------| | Don't panic | |-------------| -""".strip() - ) - +""".strip()) def test_export_eds(self): import tempfile for doctype in {"eds", "dcf"}: - with tempfile.NamedTemporaryFile(suffix="."+doctype, mode="w+") as tempeds: + with tempfile.NamedTemporaryFile(suffix="." + doctype, mode="w+") as tempeds: print("exporting %s to " % doctype + tempeds.name) canopen.export_od(self.od, tempeds, doc_type=doctype) tempeds.flush() @@ -117,54 +130,59 @@ def test_export_eds(self): for index in exported_od: self.assertIn(exported_od[index].name, self.od) - self.assertIn(index , self.od) + self.assertIn(index, self.od) for index in self.od: if index < 0x0008: # ignore dummies continue self.assertIn(self.od[index].name, exported_od) - self.assertIn(index , exported_od) + self.assertIn(index, exported_od) - actual_object = exported_od[index] - expected_object = self.od[index] + actual_object = exported_od[index] + expected_object = self.od[index] self.assertEqual(type(actual_object), type(expected_object)) self.assertEqual(actual_object.name, expected_object.name) if type(actual_object) is canopen.objectdictionary.Variable: expected_vars = [expected_object] - actual_vars = [actual_object ] - else : + actual_vars = [actual_object] + else: expected_vars = [expected_object[idx] for idx in expected_object] - actual_vars = [actual_object [idx] for idx in actual_object] + actual_vars = [actual_object[idx] for idx in actual_object] for prop in [ - "allowed_baudrates", - "vendor_name", - "vendor_number", - "product_name", - "product_number", - "revision_number", - "order_code", - "simple_boot_up_master", - "simple_boot_up_slave", - "granularity", - "dynamic_channels_supported", - "group_messaging", - "nr_of_RXPDO", - "nr_of_TXPDO", - "LSS_supported", + "allowed_baudrates", + "vendor_name", + "vendor_number", + "product_name", + "product_number", + "revision_number", + "order_code", + "simple_boot_up_master", + "simple_boot_up_slave", + "granularity", + "dynamic_channels_supported", + "group_messaging", + "nr_of_RXPDO", + "nr_of_TXPDO", + "LSS_supported", ]: - self.assertEqual(getattr(self.od.device_information, prop), getattr(exported_od.device_information, prop), f"prop {prop!r} mismatch on DeviceInfo") - - - for evar,avar in zip(expected_vars,actual_vars): - self. assertEqual(getattr(avar, "data_type" , None) , getattr(evar,"data_type" ,None) , " mismatch on %04X:%X"%(evar.index, evar.subindex)) - self. assertEqual(getattr(avar, "default_raw", None) , getattr(evar,"default_raw",None) , " mismatch on %04X:%X"%(evar.index, evar.subindex)) - self. assertEqual(getattr(avar, "min" , None) , getattr(evar,"min" ,None) , " mismatch on %04X:%X"%(evar.index, evar.subindex)) - self. assertEqual(getattr(avar, "max" , None) , getattr(evar,"max" ,None) , " mismatch on %04X:%X"%(evar.index, evar.subindex)) + self.assertEqual(getattr(self.od.device_information, prop), + getattr(exported_od.device_information, prop), + f"prop {prop!r} mismatch on DeviceInfo") + + for evar, avar in zip(expected_vars, actual_vars): + self.assertEqual(getattr(avar, "data_type", None), getattr(evar, "data_type", None), + " mismatch on %04X:%X" % (evar.index, evar.subindex)) + self.assertEqual(getattr(avar, "default_raw", None), getattr(evar, "default_raw", None), + " mismatch on %04X:%X" % (evar.index, evar.subindex)) + self.assertEqual(getattr(avar, "min", None), getattr(evar, "min", None), + " mismatch on %04X:%X" % (evar.index, evar.subindex)) + self.assertEqual(getattr(avar, "max", None), getattr(evar, "max", None), + " mismatch on %04X:%X" % (evar.index, evar.subindex)) if doctype == "dcf": - self.assertEqual(getattr(avar, "value" , None) , getattr(evar,"value" ,None) , " mismatch on %04X:%X"%(evar.index, evar.subindex)) + self.assertEqual(getattr(avar, "value", None), getattr(evar, "value", None), + " mismatch on %04X:%X" % (evar.index, evar.subindex)) self.assertEqual(self.od.comments, exported_od.comments) - From 64772dc45cfdfd924f6de0bb6d58b66a5d0c1fba Mon Sep 17 00:00:00 2001 From: Alflanker94 <61652913+Alflanker94@users.noreply.github.com> Date: Thu, 2 Mar 2023 06:51:22 +0100 Subject: [PATCH 02/15] Update profiles.rst (#348) --- doc/profiles.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/profiles.rst b/doc/profiles.rst index 1ef5ab58..9fdc1d29 100644 --- a/doc/profiles.rst +++ b/doc/profiles.rst @@ -66,7 +66,7 @@ class :attr:`.state` attribute can be read and set (command) by a string:: # command a state (an SDO message will be called) some_node.state = 'SWITCHED ON' # read the current state - some_node.state = 'SWITCHED ON' + some_node.state Available states: From 5efd42181f01daa94af80540027d038938251699 Mon Sep 17 00:00:00 2001 From: Svein Seldal <sveinse@users.noreply.github.com> Date: Mon, 3 Apr 2023 08:11:38 +0200 Subject: [PATCH 03/15] Change type() into isinstance() (#357) --- canopen/objectdictionary/__init__.py | 2 +- canopen/objectdictionary/eds.py | 16 ++++++++-------- test/test_eds.py | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/canopen/objectdictionary/__init__.py b/canopen/objectdictionary/__init__.py index f900f71c..225c3f25 100644 --- a/canopen/objectdictionary/__init__.py +++ b/canopen/objectdictionary/__init__.py @@ -29,7 +29,7 @@ def export_od(od, dest:Union[str,TextIO,None]=None, doc_type:Optional[str]=None) """ doctypes = {"eds", "dcf"} - if type(dest) is str: + if isinstance(dest, str): if doc_type is None: for t in doctypes: if dest.endswith(f".{t}"): diff --git a/canopen/objectdictionary/eds.py b/canopen/objectdictionary/eds.py index c1c54d78..f6fdd2b8 100644 --- a/canopen/objectdictionary/eds.py +++ b/canopen/objectdictionary/eds.py @@ -323,11 +323,11 @@ def export_dcf(od, dest=None, fileInfo={}): def export_eds(od, dest=None, file_info={}, device_commisioning=False): def export_object(obj, eds): - if type(obj) is objectdictionary.Variable: + if isinstance(obj, objectdictionary.Variable): return export_variable(obj, eds) - if type(obj) is objectdictionary.Record: + if isinstance(obj, objectdictionary.Record): return export_record(obj, eds) - if type(obj) is objectdictionary.Array: + if isinstance(obj, objectdictionary.Array): return export_array(obj, eds) def export_common(var, eds, section): @@ -337,7 +337,7 @@ def export_common(var, eds, section): eds.set(section, "StorageLocation", var.storage_location) def export_variable(var, eds): - if type(var.parent) is objectdictionary.ObjectDictionary: + if isinstance(var.parent, objectdictionary.ObjectDictionary): # top level variable section = "%04X" % var.index else: @@ -376,7 +376,7 @@ def export_record(var, eds): section = "%04X" % var.index export_common(var, eds, section) eds.set(section, "SubNumber", "0x%X" % len(var.subindices)) - ot = RECORD if type(var) is objectdictionary.Record else ARR + ot = RECORD if isinstance(var, objectdictionary.Record) else ARR eds.set(section, "ObjectType", "0x%X" % ot) for i in var: export_variable(var[i], eds) @@ -428,11 +428,11 @@ def export_record(var, eds): ("LSS_Supported", "LSS_supported"), ]: val = getattr(od.device_information, odprop, None) - if type(val) is None: + if val is None: continue - elif type(val) is str: + elif isinstance(val, str): eds.set("DeviceInfo", eprop, val) - elif type(val) in (int, bool): + elif isinstance(val, (int, bool)): eds.set("DeviceInfo", eprop, int(val)) # we are also adding out of spec baudrates here. diff --git a/test/test_eds.py b/test/test_eds.py index 2a6d5098..c34df381 100644 --- a/test/test_eds.py +++ b/test/test_eds.py @@ -144,7 +144,7 @@ def test_export_eds(self): self.assertEqual(type(actual_object), type(expected_object)) self.assertEqual(actual_object.name, expected_object.name) - if type(actual_object) is canopen.objectdictionary.Variable: + if isinstance(actual_object, canopen.objectdictionary.Variable): expected_vars = [expected_object] actual_vars = [actual_object] else: From 61294f9b5e8b085490df138cfef0472c47398e77 Mon Sep 17 00:00:00 2001 From: Svein Seldal <sveinse@users.noreply.github.com> Date: Wed, 5 Apr 2023 08:09:15 +0200 Subject: [PATCH 04/15] Remove use of object (#361) --- canopen/emcy.py | 4 ++-- canopen/lss.py | 2 +- canopen/network.py | 7 ++++--- canopen/nmt.py | 2 +- canopen/node/base.py | 2 +- canopen/objectdictionary/__init__.py | 2 +- canopen/pdo/base.py | 2 +- canopen/profiles/p402.py | 6 +++--- canopen/sdo/base.py | 2 +- canopen/sync.py | 2 +- canopen/timestamp.py | 2 +- canopen/variable.py | 2 +- 12 files changed, 18 insertions(+), 17 deletions(-) diff --git a/canopen/emcy.py b/canopen/emcy.py index 8964262e..118ede4f 100644 --- a/canopen/emcy.py +++ b/canopen/emcy.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) -class EmcyConsumer(object): +class EmcyConsumer: def __init__(self): #: Log of all received EMCYs for this node @@ -79,7 +79,7 @@ def wait( return emcy -class EmcyProducer(object): +class EmcyProducer: def __init__(self, cob_id: int): self.network = None diff --git a/canopen/lss.py b/canopen/lss.py index d375e852..9dee3147 100644 --- a/canopen/lss.py +++ b/canopen/lss.py @@ -65,7 +65,7 @@ ] -class LssMaster(object): +class LssMaster: """The Master of Layer Setting Services""" LSS_TX_COBID = 0x7E5 diff --git a/canopen/network.py b/canopen/network.py index 00c21cf0..51137b22 100644 --- a/canopen/network.py +++ b/canopen/network.py @@ -13,8 +13,9 @@ except ImportError: # Do not fail if python-can is not installed can = None - Listener = object CanError = Exception + class Listener: + """ Dummy listener """ from .node import RemoteNode, LocalNode from .sync import SyncProducer @@ -282,7 +283,7 @@ def __len__(self) -> int: return len(self.nodes) -class PeriodicMessageTask(object): +class PeriodicMessageTask: """ Task object to transmit a message periodically using python-can's CyclicSendTask @@ -359,7 +360,7 @@ def on_message_received(self, msg): logger.error(str(e)) -class NodeScanner(object): +class NodeScanner: """Observes which nodes are present on the bus. Listens for the following messages: diff --git a/canopen/nmt.py b/canopen/nmt.py index 09963de0..7a718d5d 100644 --- a/canopen/nmt.py +++ b/canopen/nmt.py @@ -39,7 +39,7 @@ } -class NmtBase(object): +class NmtBase: """ Can set the state of the node it controls using NMT commands and monitor the current state using the heartbeat protocol. diff --git a/canopen/node/base.py b/canopen/node/base.py index d87e5517..29d298e6 100644 --- a/canopen/node/base.py +++ b/canopen/node/base.py @@ -2,7 +2,7 @@ from .. import objectdictionary -class BaseNode(object): +class BaseNode: """A CANopen node. :param node_id: diff --git a/canopen/objectdictionary/__init__.py b/canopen/objectdictionary/__init__.py index 225c3f25..793b8c4c 100644 --- a/canopen/objectdictionary/__init__.py +++ b/canopen/objectdictionary/__init__.py @@ -266,7 +266,7 @@ def add_member(self, variable: "Variable") -> None: self.names[variable.name] = variable -class Variable(object): +class Variable: """Simple variable.""" STRUCT_TYPES = { diff --git a/canopen/pdo/base.py b/canopen/pdo/base.py index 1685de62..d7160738 100644 --- a/canopen/pdo/base.py +++ b/canopen/pdo/base.py @@ -156,7 +156,7 @@ def __len__(self) -> int: return len(self.maps) -class Map(object): +class Map: """One message which can have up to 8 bytes of variables mapped.""" def __init__(self, pdo_node, com_record, map_array): diff --git a/canopen/profiles/p402.py b/canopen/profiles/p402.py index 12ccdd3b..915621d7 100644 --- a/canopen/profiles/p402.py +++ b/canopen/profiles/p402.py @@ -7,7 +7,7 @@ logger = logging.getLogger(__name__) -class State402(object): +class State402: # Controlword (0x6040) commands CW_OPERATION_ENABLED = 0x000F CW_SHUTDOWN = 0x0006 @@ -101,7 +101,7 @@ def next_state_indirect(_from): return next_state -class OperationMode(object): +class OperationMode: NO_MODE = 0 PROFILED_POSITION = 1 VELOCITY = 2 @@ -155,7 +155,7 @@ class OperationMode(object): } -class Homing(object): +class Homing: CW_START = 0x10 CW_HALT = 0x100 diff --git a/canopen/sdo/base.py b/canopen/sdo/base.py index 3c3d0bbe..622eed14 100644 --- a/canopen/sdo/base.py +++ b/canopen/sdo/base.py @@ -9,7 +9,7 @@ from .. import variable -class CrcXmodem(object): +class CrcXmodem: """Mimics CrcXmodem from crccheck.""" def __init__(self): diff --git a/canopen/sync.py b/canopen/sync.py index 32248279..6e583b04 100644 --- a/canopen/sync.py +++ b/canopen/sync.py @@ -3,7 +3,7 @@ from typing import Optional -class SyncProducer(object): +class SyncProducer: """Transmits a SYNC message periodically.""" #: COB-ID of the SYNC message diff --git a/canopen/timestamp.py b/canopen/timestamp.py index e96f7576..f3004da2 100644 --- a/canopen/timestamp.py +++ b/canopen/timestamp.py @@ -10,7 +10,7 @@ TIME_OF_DAY_STRUCT = struct.Struct("<LH") -class TimeProducer(object): +class TimeProducer: """Produces timestamp objects.""" #: COB-ID of the SYNC message diff --git a/canopen/variable.py b/canopen/variable.py index 2357d162..2abfa2ab 100644 --- a/canopen/variable.py +++ b/canopen/variable.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) -class Variable(object): +class Variable: def __init__(self, od: objectdictionary.Variable): self.od = od From f687e2da2945f5f05d732f433d30fa0a48c95a06 Mon Sep 17 00:00:00 2001 From: Svein Seldal <sveinse@users.noreply.github.com> Date: Thu, 6 Apr 2023 19:25:50 +0200 Subject: [PATCH 05/15] Use with contexts for opens (#356) --- canopen/objectdictionary/__init__.py | 4 ++++ canopen/objectdictionary/eds.py | 10 +++++--- canopen/sdo/client.py | 13 +++++------ doc/sdo.rst | 34 ++++++++++++---------------- test/test_eds.py | 3 ++- test/test_local.py | 10 ++++---- test/test_sdo.py | 19 +++++++--------- 7 files changed, 48 insertions(+), 45 deletions(-) diff --git a/canopen/objectdictionary/__init__.py b/canopen/objectdictionary/__init__.py index 793b8c4c..70816515 100644 --- a/canopen/objectdictionary/__init__.py +++ b/canopen/objectdictionary/__init__.py @@ -48,6 +48,10 @@ def export_od(od, dest:Union[str,TextIO,None]=None, doc_type:Optional[str]=None) from . import eds return eds.export_dcf(od, dest) + # If dest is opened in this fn, it should be closed + if type(dest) is str: + dest.close() + def import_od( source: Union[str, TextIO, None], diff --git a/canopen/objectdictionary/eds.py b/canopen/objectdictionary/eds.py index f6fdd2b8..c595ed66 100644 --- a/canopen/objectdictionary/eds.py +++ b/canopen/objectdictionary/eds.py @@ -33,7 +33,11 @@ def import_eds(source, node_id): except AttributeError: # Python 2 eds.readfp(fp) - fp.close() + finally: + # Only close object if opened in this fn + if not hasattr(source, "read"): + fp.close() + od = objectdictionary.ObjectDictionary() if eds.has_section("FileInfo"): @@ -181,8 +185,8 @@ 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) + with sdo_client.open(0x1021, 0, "rt") as eds_fp: + od = import_eds(eds_fp, node_id) except Exception as e: logger.error("No object dictionary could be loaded for node %d: %s", node_id, e) diff --git a/canopen/sdo/client.py b/canopen/sdo/client.py index 7e0f58bf..7faa103a 100644 --- a/canopen/sdo/client.py +++ b/canopen/sdo/client.py @@ -114,9 +114,9 @@ def upload(self, index: int, subindex: int) -> bytes: :raises canopen.SdoAbortedError: When node responds with an error. """ - fp = self.open(index, subindex, buffering=0) - size = fp.size - data = fp.read() + with self.open(index, subindex, buffering=0) as fp: + size = fp.size + data = fp.read() if size is None: # Node did not specify how many bytes to use # Try to find out using Object Dictionary @@ -155,10 +155,9 @@ def download( :raises canopen.SdoAbortedError: When node responds with an error. """ - fp = self.open(index, subindex, "wb", buffering=7, size=len(data), - force_segment=force_segment) - fp.write(data) - fp.close() + with self.open(index, subindex, "wb", buffering=7, size=len(data), + force_segment=force_segment) as fp: + fp.write(data) def open(self, index, subindex=0, mode="rb", encoding="ascii", buffering=1024, size=None, block_transfer=False, force_segment=False, request_crc_support=True): diff --git a/doc/sdo.rst b/doc/sdo.rst index bfc78c25..c0db4ca5 100644 --- a/doc/sdo.rst +++ b/doc/sdo.rst @@ -72,14 +72,11 @@ Variables can be opened as readable or writable file objects which can be useful when dealing with large amounts of data:: # Open the Store EDS variable as a file like object - infile = node.sdo[0x1021].open('r', encoding='ascii') - # Open a file for writing to - outfile = open('out.eds', 'w', encoding='ascii') - # Iteratively read lines from node and write to file - outfile.writelines(infile) - # Clean-up - infile.close() - outfile.close() + with node.sdo[0x1021].open('r', encoding='ascii') as infile, + open('out.eds', 'w', encoding='ascii') as outfile: + + # Iteratively read lines from node and write to file + outfile.writelines(infile) Most APIs accepting file objects should also be able to accept this. @@ -88,17 +85,16 @@ server supports it. This is done through the file object interface:: FIRMWARE_PATH = '/path/to/firmware.bin' FILESIZE = os.path.getsize(FIRMWARE_PATH) - infile = open(FIRMWARE_PATH, 'rb') - outfile = node.sdo['Firmware'].open('wb', size=FILESIZE, block_transfer=True) - - # Iteratively transfer data without having to read all into memory - while True: - data = infile.read(1024) - if not data: - break - outfile.write(data) - infile.close() - outfile.close() + + with open(FIRMWARE_PATH, 'rb') as infile, + node.sdo['Firmware'].open('wb', size=FILESIZE, block_transfer=True) as outfile: + + # Iteratively transfer data without having to read all into memory + while True: + data = infile.read(1024) + if not data: + break + outfile.write(data) .. warning:: Block transfer is still in experimental stage! diff --git a/test/test_eds.py b/test/test_eds.py index c34df381..4977dc10 100644 --- a/test/test_eds.py +++ b/test/test_eds.py @@ -15,7 +15,8 @@ def test_load_nonexisting_file(self): canopen.import_od('/path/to/wrong_file.eds') def test_load_file_object(self): - od = canopen.import_od(open(EDS_PATH)) + with open(EDS_PATH) as fp: + od = canopen.import_od(fp) self.assertTrue(len(od) > 0) def test_variable(self): diff --git a/test/test_local.py b/test/test_local.py index f4119d44..493cef1b 100644 --- a/test/test_local.py +++ b/test/test_local.py @@ -40,7 +40,8 @@ def test_expedited_upload(self): def test_block_upload_switch_to_expedite_upload(self): with self.assertRaises(canopen.SdoCommunicationError) as context: - self.remote_node.sdo[0x1008].open('r', block_transfer=True) + with self.remote_node.sdo[0x1008].open('r', block_transfer=True) as fp: + pass # We get this since the sdo client don't support the switch # from block upload to expedite upload self.assertEqual("Unexpected response 0x41", str(context.exception)) @@ -48,9 +49,10 @@ def test_block_upload_switch_to_expedite_upload(self): def test_block_download_not_supported(self): data = b"TEST DEVICE" with self.assertRaises(canopen.SdoAbortedError) as context: - self.remote_node.sdo[0x1008].open('wb', - size=len(data), - block_transfer=True) + with self.remote_node.sdo[0x1008].open('wb', + size=len(data), + block_transfer=True) as fp: + pass self.assertEqual(context.exception.code, 0x05040001) def test_expedited_upload_default_value_visible_string(self): diff --git a/test/test_sdo.py b/test/test_sdo.py index 67115499..c0ba086b 100644 --- a/test/test_sdo.py +++ b/test/test_sdo.py @@ -110,10 +110,9 @@ def test_block_download(self): (RX, b'\xa1\x00\x00\x00\x00\x00\x00\x00') ] data = b'A really really long string...' - fp = self.network[2].sdo['Writable string'].open( - 'wb', size=len(data), block_transfer=True) - fp.write(data) - fp.close() + with self.network[2].sdo['Writable string'].open( + 'wb', size=len(data), block_transfer=True) as fp: + fp.write(data) def test_block_upload(self): self.data = [ @@ -128,9 +127,8 @@ def test_block_upload(self): (RX, b'\xc9\x40\xe1\x00\x00\x00\x00\x00'), (TX, b'\xa1\x00\x00\x00\x00\x00\x00\x00') ] - fp = self.network[2].sdo[0x1008].open('r', block_transfer=True) - data = fp.read() - fp.close() + with self.network[2].sdo[0x1008].open('r', block_transfer=True) as fp: + data = fp.read() self.assertEqual(data, 'Tiny Node - Mega Domains !') def test_writable_file(self): @@ -144,10 +142,9 @@ def test_writable_file(self): (TX, b'\x0f\x00\x00\x00\x00\x00\x00\x00'), (RX, b'\x20\x00\x20\x00\x00\x00\x00\x00') ] - fp = self.network[2].sdo['Writable string'].open('wb') - fp.write(b'1234') - fp.write(b'56789') - fp.close() + with self.network[2].sdo['Writable string'].open('wb') as fp: + fp.write(b'1234') + fp.write(b'56789') self.assertTrue(fp.closed) # Write on closed file with self.assertRaises(ValueError): From 9cdfe9b476f3b265d08b9fbb4a1fd4537c19f54a Mon Sep 17 00:00:00 2001 From: Samuel Lee <54152208+samsamfire@users.noreply.github.com> Date: Sat, 8 Apr 2023 19:24:08 +0100 Subject: [PATCH 06/15] - Remove old callbacks if re-adding a node with the same id (#342) --- canopen/network.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/canopen/network.py b/canopen/network.py index 51137b22..e348324c 100644 --- a/canopen/network.py +++ b/canopen/network.py @@ -269,6 +269,9 @@ def __getitem__(self, node_id: int) -> Union[RemoteNode, LocalNode]: def __setitem__(self, node_id: int, node: Union[RemoteNode, LocalNode]): assert node_id == node.id + if node_id in self.nodes: + # Remove old callbacks + self.nodes[node_id].remove_network() self.nodes[node_id] = node node.associate_network(self) From 9e303f22967efc1dac34fcaa91864681e6b0a0a6 Mon Sep 17 00:00:00 2001 From: Svein Seldal <sveinse@users.noreply.github.com> Date: Sat, 8 Apr 2023 20:43:25 +0200 Subject: [PATCH 07/15] Import cleanups (#362) * Use absolute `canopen` instead of relative imports * Ensure canopen imports after system imports * Fix isinstance bug in TPDO.stop() * Change const from objectdictionary.* to datatypes.* in objectdictionary.eds.py for consistency * Save typing by using ObjectDictionary instead of objectdictionary.ObjectDictionary --- canopen/__init__.py | 12 +++++------ canopen/network.py | 14 ++++++------- canopen/nmt.py | 2 -- canopen/node/__init__.py | 4 ++-- canopen/node/base.py | 10 ++++------ canopen/node/local.py | 15 +++++++------- canopen/node/remote.py | 22 +++++++++----------- canopen/objectdictionary/__init__.py | 10 +++++----- canopen/objectdictionary/eds.py | 30 ++++++++++++++-------------- canopen/objectdictionary/epf.py | 4 +++- canopen/pdo/__init__.py | 11 +++++----- canopen/pdo/base.py | 6 +++--- canopen/profiles/p402.py | 5 +++-- canopen/sdo/__init__.py | 8 ++++---- canopen/sdo/base.py | 13 ++++++------ canopen/sdo/client.py | 13 ++++++------ canopen/sdo/server.py | 6 +++--- canopen/sync.py | 2 -- canopen/variable.py | 2 +- 19 files changed, 91 insertions(+), 98 deletions(-) diff --git a/canopen/__init__.py b/canopen/__init__.py index a63ecb83..ee5ff7b5 100644 --- a/canopen/__init__.py +++ b/canopen/__init__.py @@ -1,10 +1,10 @@ -from .network import Network, NodeScanner -from .node import RemoteNode, LocalNode -from .sdo import SdoCommunicationError, SdoAbortedError -from .objectdictionary import import_od, export_od, ObjectDictionary, ObjectDictionaryError -from .profiles.p402 import BaseNode402 +from canopen.network import Network, NodeScanner +from canopen.node import RemoteNode, LocalNode +from canopen.sdo import SdoCommunicationError, SdoAbortedError +from canopen.objectdictionary import import_od, export_od, ObjectDictionary, ObjectDictionaryError +from canopen.profiles.p402 import BaseNode402 try: - from ._version import version as __version__ + from canopen._version import version as __version__ except ImportError: # package is not installed __version__ = "unknown" diff --git a/canopen/network.py b/canopen/network.py index e348324c..7768795d 100644 --- a/canopen/network.py +++ b/canopen/network.py @@ -17,13 +17,13 @@ class Listener: """ Dummy listener """ -from .node import RemoteNode, LocalNode -from .sync import SyncProducer -from .timestamp import TimeProducer -from .nmt import NmtMaster -from .lss import LssMaster -from .objectdictionary.eds import import_from_node -from .objectdictionary import ObjectDictionary +from canopen.node import RemoteNode, LocalNode +from canopen.sync import SyncProducer +from canopen.timestamp import TimeProducer +from canopen.nmt import NmtMaster +from canopen.lss import LssMaster +from canopen.objectdictionary.eds import import_from_node +from canopen.objectdictionary import ObjectDictionary logger = logging.getLogger(__name__) diff --git a/canopen/nmt.py b/canopen/nmt.py index 7a718d5d..98d8ea25 100644 --- a/canopen/nmt.py +++ b/canopen/nmt.py @@ -4,8 +4,6 @@ import time from typing import Callable, Optional -from .network import CanError - logger = logging.getLogger(__name__) NMT_STATES = { diff --git a/canopen/node/__init__.py b/canopen/node/__init__.py index 98bc707b..31fed19e 100644 --- a/canopen/node/__init__.py +++ b/canopen/node/__init__.py @@ -1,2 +1,2 @@ -from .remote import RemoteNode -from .local import LocalNode +from canopen.node.remote import RemoteNode +from canopen.node.local import LocalNode diff --git a/canopen/node/base.py b/canopen/node/base.py index 29d298e6..bf72d959 100644 --- a/canopen/node/base.py +++ b/canopen/node/base.py @@ -1,5 +1,5 @@ from typing import TextIO, Union -from .. import objectdictionary +from canopen.objectdictionary import ObjectDictionary, import_od class BaseNode: @@ -15,14 +15,12 @@ class BaseNode: def __init__( self, node_id: int, - object_dictionary: Union[objectdictionary.ObjectDictionary, str, TextIO], + object_dictionary: Union[ObjectDictionary, str, TextIO], ): self.network = None - if not isinstance(object_dictionary, - objectdictionary.ObjectDictionary): - object_dictionary = objectdictionary.import_od( - object_dictionary, node_id) + if not isinstance(object_dictionary, ObjectDictionary): + object_dictionary = import_od(object_dictionary, node_id) self.object_dictionary = object_dictionary self.id = node_id or self.object_dictionary.node_id diff --git a/canopen/node/local.py b/canopen/node/local.py index 9e0a80b3..b36cf3eb 100644 --- a/canopen/node/local.py +++ b/canopen/node/local.py @@ -1,12 +1,13 @@ import logging from typing import Dict, Union -from .base import BaseNode -from ..sdo import SdoServer, SdoAbortedError -from ..pdo import PDO, TPDO, RPDO -from ..nmt import NmtSlave -from ..emcy import EmcyProducer -from .. import objectdictionary +from canopen.node.base import BaseNode +from canopen.sdo import SdoServer, SdoAbortedError +from canopen.pdo import PDO, TPDO, RPDO +from canopen.nmt import NmtSlave +from canopen.emcy import EmcyProducer +from canopen.objectdictionary import ObjectDictionary +from canopen import objectdictionary logger = logging.getLogger(__name__) @@ -16,7 +17,7 @@ class LocalNode(BaseNode): def __init__( self, node_id: int, - object_dictionary: Union[objectdictionary.ObjectDictionary, str], + object_dictionary: Union[ObjectDictionary, str], ): super(LocalNode, self).__init__(node_id, object_dictionary) diff --git a/canopen/node/remote.py b/canopen/node/remote.py index 5aca17ff..8e4025d7 100644 --- a/canopen/node/remote.py +++ b/canopen/node/remote.py @@ -1,16 +1,12 @@ import logging from typing import Union, TextIO -from ..sdo import SdoClient -from ..nmt import NmtMaster -from ..emcy import EmcyConsumer -from ..pdo import TPDO, RPDO, PDO -from ..objectdictionary import Record, Array, Variable -from .base import BaseNode - -import canopen - -from canopen import objectdictionary +from canopen.sdo import SdoClient, SdoCommunicationError, SdoAbortedError +from canopen.nmt import NmtMaster +from canopen.emcy import EmcyConsumer +from canopen.pdo import TPDO, RPDO, PDO +from canopen.objectdictionary import Record, Array, Variable, ObjectDictionary +from canopen.node.base import BaseNode logger = logging.getLogger(__name__) @@ -31,7 +27,7 @@ class RemoteNode(BaseNode): def __init__( self, node_id: int, - object_dictionary: Union[objectdictionary.ObjectDictionary, str, TextIO], + object_dictionary: Union[ObjectDictionary, str, TextIO], load_od: bool = False, ): super(RemoteNode, self).__init__(node_id, object_dictionary) @@ -138,9 +134,9 @@ def __load_configuration_helper(self, index, subindex, name, value): index=index, name=name, value=value))) - except canopen.SdoCommunicationError as e: + except SdoCommunicationError as e: logger.warning(str(e)) - except canopen.SdoAbortedError as e: + except SdoAbortedError as e: # WORKAROUND for broken implementations: the SDO is set but the error # "Attempt to write a read-only object" is raised any way. if e.code != 0x06010002: diff --git a/canopen/objectdictionary/__init__.py b/canopen/objectdictionary/__init__.py index 70816515..3c608126 100644 --- a/canopen/objectdictionary/__init__.py +++ b/canopen/objectdictionary/__init__.py @@ -9,7 +9,7 @@ from collections import MutableMapping, Mapping import logging -from .datatypes import * +from canopen.objectdictionary.datatypes import * logger = logging.getLogger(__name__) @@ -42,10 +42,10 @@ def export_od(od, dest:Union[str,TextIO,None]=None, doc_type:Optional[str]=None) assert doc_type in doctypes if doc_type == "eds": - from . import eds + from canopen.objectdictionary import eds return eds.export_eds(od, dest) elif doc_type == "dcf": - from . import eds + from canopen.objectdictionary import eds return eds.export_dcf(od, dest) # If dest is opened in this fn, it should be closed @@ -78,10 +78,10 @@ def import_od( filename = source suffix = filename[filename.rfind("."):].lower() if suffix in (".eds", ".dcf"): - from . import eds + from canopen.objectdictionary import eds return eds.import_eds(source, node_id) elif suffix == ".epf": - from . import epf + from canopen.objectdictionary import epf return epf.import_epf(source) else: raise NotImplementedError("No support for this format") diff --git a/canopen/objectdictionary/eds.py b/canopen/objectdictionary/eds.py index c595ed66..aa83db0c 100644 --- a/canopen/objectdictionary/eds.py +++ b/canopen/objectdictionary/eds.py @@ -2,13 +2,13 @@ import logging import re -from canopen.objectdictionary import datatypes - try: from configparser import RawConfigParser, NoOptionError, NoSectionError except ImportError: from ConfigParser import RawConfigParser, NoOptionError, NoSectionError + from canopen import objectdictionary +from canopen.objectdictionary import ObjectDictionary, datatypes from canopen.sdo import SdoClient logger = logging.getLogger(__name__) @@ -38,7 +38,7 @@ def import_eds(source, node_id): if not hasattr(source, "read"): fp.close() - od = objectdictionary.ObjectDictionary() + od = ObjectDictionary() if eds.has_section("FileInfo"): od.__edsFileInfo = { @@ -130,7 +130,7 @@ def import_eds(source, node_id): arr = objectdictionary.Array(name, index) last_subindex = objectdictionary.Variable( "Number of entries", index, 0) - last_subindex.data_type = objectdictionary.UNSIGNED8 + last_subindex.data_type = datatypes.UNSIGNED8 arr.add_member(last_subindex) arr.add_member(build_variable(eds, section, node_id, index, 1)) arr.storage_location = storage_location @@ -179,7 +179,7 @@ def import_from_node(node_id, network): :param network: network object """ # Create temporary SDO client - sdo_client = SdoClient(0x600 + node_id, 0x580 + node_id, objectdictionary.ObjectDictionary()) + sdo_client = SdoClient(0x600 + node_id, 0x580 + node_id, ObjectDictionary()) sdo_client.network = network # Subscribe to SDO responses network.subscribe(0x580 + node_id, sdo_client.on_response) @@ -219,11 +219,11 @@ def _signed_int_from_hex(hex_str, bit_length): def _convert_variable(node_id, var_type, value): - if var_type in (objectdictionary.OCTET_STRING, objectdictionary.DOMAIN): + if var_type in (datatypes.OCTET_STRING, datatypes.DOMAIN): return bytes.fromhex(value) - elif var_type in (objectdictionary.VISIBLE_STRING, objectdictionary.UNICODE_STRING): + elif var_type in (datatypes.VISIBLE_STRING, datatypes.UNICODE_STRING): return value - elif var_type in objectdictionary.FLOAT_TYPES: + elif var_type in datatypes.FLOAT_TYPES: return float(value) else: # COB-ID can contain '$NODEID+' so replace this with node_id before converting @@ -237,11 +237,11 @@ def _convert_variable(node_id, var_type, value): def _revert_variable(var_type, value): if value is None: return None - if var_type in (objectdictionary.OCTET_STRING, objectdictionary.DOMAIN): + if var_type in (datatypes.OCTET_STRING, datatypes.DOMAIN): return bytes.hex(value) - elif var_type in (objectdictionary.VISIBLE_STRING, objectdictionary.UNICODE_STRING): + elif var_type in (datatypes.VISIBLE_STRING, datatypes.UNICODE_STRING): return value - elif var_type in objectdictionary.FLOAT_TYPES: + elif var_type in datatypes.FLOAT_TYPES: return value else: return "0x%02X" % value @@ -273,14 +273,14 @@ def build_variable(eds, section, node_id, index, subindex=0): except NoSectionError: logger.warning("%s has an unknown or unsupported data type (%X)", name, var.data_type) # Assume DOMAIN to force application to interpret the byte data - var.data_type = objectdictionary.DOMAIN + var.data_type = datatypes.DOMAIN var.pdo_mappable = bool(int(eds.get(section, "PDOMapping", fallback="0"), 0)) if eds.has_option(section, "LowLimit"): try: min_string = eds.get(section, "LowLimit") - if var.data_type in objectdictionary.SIGNED_TYPES: + if var.data_type in datatypes.SIGNED_TYPES: var.min = _signed_int_from_hex(min_string, _calc_bit_length(var.data_type)) else: var.min = int(min_string, 0) @@ -289,7 +289,7 @@ def build_variable(eds, section, node_id, index, subindex=0): if eds.has_option(section, "HighLimit"): try: max_string = eds.get(section, "HighLimit") - if var.data_type in objectdictionary.SIGNED_TYPES: + if var.data_type in datatypes.SIGNED_TYPES: var.max = _signed_int_from_hex(max_string, _calc_bit_length(var.data_type)) else: var.max = int(max_string, 0) @@ -341,7 +341,7 @@ def export_common(var, eds, section): eds.set(section, "StorageLocation", var.storage_location) def export_variable(var, eds): - if isinstance(var.parent, objectdictionary.ObjectDictionary): + if isinstance(var.parent, ObjectDictionary): # top level variable section = "%04X" % var.index else: diff --git a/canopen/objectdictionary/epf.py b/canopen/objectdictionary/epf.py index 5cef4058..8bfc513a 100644 --- a/canopen/objectdictionary/epf.py +++ b/canopen/objectdictionary/epf.py @@ -3,7 +3,9 @@ except ImportError: import xml.etree.ElementTree as etree import logging + from canopen import objectdictionary +from canopen.objectdictionary import ObjectDictionary logger = logging.getLogger(__name__) @@ -32,7 +34,7 @@ def import_epf(epf): The Object Dictionary. :rtype: canopen.ObjectDictionary """ - od = objectdictionary.ObjectDictionary() + od = ObjectDictionary() if etree.iselement(epf): tree = epf else: diff --git a/canopen/pdo/__init__.py b/canopen/pdo/__init__.py index a08b3ccc..d47ec693 100644 --- a/canopen/pdo/__init__.py +++ b/canopen/pdo/__init__.py @@ -1,8 +1,7 @@ -from .base import PdoBase, Maps, Map, Variable - import logging -import itertools -import canopen + +from canopen import node +from canopen.pdo.base import PdoBase, Maps, Map, Variable logger = logging.getLogger(__name__) @@ -45,7 +44,7 @@ def stop(self): :raise TypeError: Exception is thrown if the node associated with the PDO does not support this function. """ - if isinstance(self.node, canopen.RemoteNode): + if isinstance(self.node, node.RemoteNode): for pdo in self.map.values(): pdo.stop() else: @@ -70,7 +69,7 @@ def stop(self): :raise TypeError: Exception is thrown if the node associated with the PDO does not support this function. """ - if isinstance(canopen.LocalNode, self.node): + if isinstance(self.node, node.LocalNode): for pdo in self.map.values(): pdo.stop() else: diff --git a/canopen/pdo/base.py b/canopen/pdo/base.py index d7160738..086a0774 100644 --- a/canopen/pdo/base.py +++ b/canopen/pdo/base.py @@ -8,9 +8,9 @@ import logging import binascii -from ..sdo import SdoAbortedError -from .. import objectdictionary -from .. import variable +from canopen.sdo import SdoAbortedError +from canopen import objectdictionary +from canopen import variable PDO_NOT_VALID = 1 << 31 RTR_NOT_ALLOWED = 1 << 30 diff --git a/canopen/profiles/p402.py b/canopen/profiles/p402.py index 915621d7..2e5fb133 100644 --- a/canopen/profiles/p402.py +++ b/canopen/profiles/p402.py @@ -1,8 +1,9 @@ # inspired by the NmtMaster code import logging import time -from ..node import RemoteNode -from ..sdo import SdoCommunicationError + +from canopen.node import RemoteNode +from canopen.sdo import SdoCommunicationError logger = logging.getLogger(__name__) diff --git a/canopen/sdo/__init__.py b/canopen/sdo/__init__.py index b395d0fd..160affd3 100644 --- a/canopen/sdo/__init__.py +++ b/canopen/sdo/__init__.py @@ -1,4 +1,4 @@ -from .base import Variable, Record, Array -from .client import SdoClient -from .server import SdoServer -from .exceptions import SdoAbortedError, SdoCommunicationError +from canopen.sdo.base import Variable, Record, Array +from canopen.sdo.client import SdoClient +from canopen.sdo.server import SdoServer +from canopen.sdo.exceptions import SdoAbortedError, SdoCommunicationError diff --git a/canopen/sdo/base.py b/canopen/sdo/base.py index 622eed14..25d3d60c 100644 --- a/canopen/sdo/base.py +++ b/canopen/sdo/base.py @@ -5,8 +5,9 @@ except ImportError: from collections import Mapping -from .. import objectdictionary -from .. import variable +from canopen import objectdictionary +from canopen.objectdictionary import ObjectDictionary +from canopen import variable class CrcXmodem: @@ -31,7 +32,7 @@ def __init__( self, rx_cobid: int, tx_cobid: int, - od: objectdictionary.ObjectDictionary, + od: ObjectDictionary, ): """ :param rx_cobid: @@ -81,7 +82,7 @@ def download( class Record(Mapping): - def __init__(self, sdo_node: SdoBase, od: objectdictionary.ObjectDictionary): + def __init__(self, sdo_node: SdoBase, od: ObjectDictionary): self.sdo_node = sdo_node self.od = od @@ -100,7 +101,7 @@ def __contains__(self, subindex: Union[int, str]) -> bool: class Array(Mapping): - def __init__(self, sdo_node: SdoBase, od: objectdictionary.ObjectDictionary): + def __init__(self, sdo_node: SdoBase, od: ObjectDictionary): self.sdo_node = sdo_node self.od = od @@ -120,7 +121,7 @@ def __contains__(self, subindex: int) -> bool: class Variable(variable.Variable): """Access object dictionary variable values using SDO protocol.""" - def __init__(self, sdo_node: SdoBase, od: objectdictionary.ObjectDictionary): + def __init__(self, sdo_node: SdoBase, od: ObjectDictionary): self.sdo_node = sdo_node variable.Variable.__init__(self, od) diff --git a/canopen/sdo/client.py b/canopen/sdo/client.py index 7faa103a..21517717 100644 --- a/canopen/sdo/client.py +++ b/canopen/sdo/client.py @@ -7,12 +7,11 @@ except ImportError: import Queue as queue -from ..network import CanError -from .. import objectdictionary - -from .base import SdoBase -from .constants import * -from .exceptions import * +from canopen.network import CanError +from canopen import objectdictionary +from canopen.sdo.base import SdoBase +from canopen.sdo.constants import * +from canopen.sdo.exceptions import * logger = logging.getLogger(__name__) @@ -192,7 +191,7 @@ def open(self, index, subindex=0, mode="rb", encoding="ascii", Force use of segmented download regardless of data size. :param bool request_crc_support: If crc calculation should be requested when using block transfer - + :returns: A file like object. """ diff --git a/canopen/sdo/server.py b/canopen/sdo/server.py index 7986e1fa..e9574feb 100644 --- a/canopen/sdo/server.py +++ b/canopen/sdo/server.py @@ -1,8 +1,8 @@ import logging -from .base import SdoBase -from .constants import * -from .exceptions import * +from canopen.sdo.base import SdoBase +from canopen.sdo.constants import * +from canopen.sdo.exceptions import * logger = logging.getLogger(__name__) diff --git a/canopen/sync.py b/canopen/sync.py index 6e583b04..d3734512 100644 --- a/canopen/sync.py +++ b/canopen/sync.py @@ -1,5 +1,3 @@ - - from typing import Optional diff --git a/canopen/variable.py b/canopen/variable.py index 2abfa2ab..c7924e51 100644 --- a/canopen/variable.py +++ b/canopen/variable.py @@ -5,7 +5,7 @@ except ImportError: from collections import Mapping -from . import objectdictionary +from canopen import objectdictionary logger = logging.getLogger(__name__) From e8807b87e69902840f459279ce38e330b069f53d Mon Sep 17 00:00:00 2001 From: Svein Seldal <sveinse@users.noreply.github.com> Date: Sat, 8 Apr 2023 20:45:19 +0200 Subject: [PATCH 08/15] Fix pytest on windows (#364) (#365) * Fix pytest on windows (#364) * Slack test even more for WIndows --- setup.cfg | 6 ++++++ test/test_eds.py | 15 +++++++++------ test/test_network.py | 5 ++++- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/setup.cfg b/setup.cfg index ca6253b5..7d33e805 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,3 +21,9 @@ python_requires = >=3.6 install_requires = python-can >= 3.0.0 include_package_data = True + +[tool:pytest] +testpaths = + test +filterwarnings = + ignore::DeprecationWarning diff --git a/test/test_eds.py b/test/test_eds.py index 4977dc10..5edd8d86 100644 --- a/test/test_eds.py +++ b/test/test_eds.py @@ -122,12 +122,15 @@ def test_comments(self): def test_export_eds(self): import tempfile - for doctype in {"eds", "dcf"}: - with tempfile.NamedTemporaryFile(suffix="." + doctype, mode="w+") as tempeds: - print("exporting %s to " % doctype + tempeds.name) - canopen.export_od(self.od, tempeds, doc_type=doctype) - tempeds.flush() - exported_od = canopen.import_od(tempeds.name) + from pathlib import Path + with tempfile.TemporaryDirectory() as tempdir: + for doctype in {"eds", "dcf"}: + tempfile = str(Path(tempdir, "test." + doctype)) + with open(tempfile, "w+") as tempeds: + print("exporting %s to " % doctype + tempeds.name) + canopen.export_od(self.od, tempeds, doc_type=doctype) + + exported_od = canopen.import_od(tempfile) for index in exported_od: self.assertIn(exported_od[index].name, self.od) diff --git a/test/test_network.py b/test/test_network.py index e89ae4dd..04129271 100644 --- a/test/test_network.py +++ b/test/test_network.py @@ -59,7 +59,10 @@ def test_send_perodic(self): task = self.network.send_periodic(0x123, [1, 2, 3], 0.01) time.sleep(0.1) - self.assertTrue(9 <= bus.queue.qsize() <= 11) + # FIXME: This test is a little fragile, as the number of elements + # depends on the timing of the machine. + print("Queue size: %s" % (bus.queue.qsize(),)) + self.assertTrue(9 <= bus.queue.qsize() <= 13) msg = bus.recv(0) self.assertIsNotNone(msg) self.assertSequenceEqual(msg.data, [1, 2, 3]) From 2f42ec69e71f1183f67367433d69ed4091d4ffc7 Mon Sep 17 00:00:00 2001 From: Joep <xorjoep@gmail.com> Date: Mon, 29 May 2023 22:17:05 +0200 Subject: [PATCH 09/15] Enable inline comments when reading files (#380) --- 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 aa83db0c..d9f06e97 100644 --- a/canopen/objectdictionary/eds.py +++ b/canopen/objectdictionary/eds.py @@ -21,7 +21,7 @@ def import_eds(source, node_id): - eds = RawConfigParser() + eds = RawConfigParser(inline_comment_prefixes=(';',)) eds.optionxform = str if hasattr(source, "read"): fp = source From 2d5d6d369efe1f1cdbd2a4e6ae4fd997d2845c62 Mon Sep 17 00:00:00 2001 From: Marco <marcovannoord@users.noreply.github.com> Date: Wed, 30 Aug 2023 07:36:13 +0100 Subject: [PATCH 10/15] Feature/eds parse factor and description (#378) --- canopen/objectdictionary/eds.py | 23 ++++++++++++++++++++++ test/sample.eds | 34 +++++++++++++++++++++++++++++++++ test/test_eds.py | 12 ++++++++++++ 3 files changed, 69 insertions(+) diff --git a/canopen/objectdictionary/eds.py b/canopen/objectdictionary/eds.py index d9f06e97..187b5135 100644 --- a/canopen/objectdictionary/eds.py +++ b/canopen/objectdictionary/eds.py @@ -309,6 +309,22 @@ def build_variable(eds, section, node_id, index, subindex=0): var.value = _convert_variable(node_id, var.data_type, eds.get(section, "ParameterValue")) except ValueError: pass + # 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 + if eds.has_option(section, "Factor"): + try: + var.factor = float(eds.get(section, "Factor")) + except ValueError: + pass + if eds.has_option(section, "Description"): + try: + var.description = eds.get(section, "Description") + except ValueError: + pass + if eds.has_option(section, "Unit"): + try: + var.unit = eds.get(section, "Unit") + except ValueError: + pass return var @@ -376,6 +392,13 @@ def export_variable(var, eds): if getattr(var, 'max', None) is not None: eds.set(section, "HighLimit", var.max) + if getattr(var, 'description', '') != '': + eds.set(section, "Description", var.description) + if getattr(var, 'factor', 1) != 1: + eds.set(section, "Factor", var.factor) + if getattr(var, 'unit', '') != '': + eds.set(section, "Unit", var.unit) + def export_record(var, eds): section = "%04X" % var.index export_common(var, eds, section) diff --git a/test/sample.eds b/test/sample.eds index 671a559e..2267ff55 100644 --- a/test/sample.eds +++ b/test/sample.eds @@ -938,3 +938,37 @@ AccessType=rw HighLimit=0x000000000000000A LowLimit=0x8000000000000009 PDOMapping=0 + + +[3050] +ParameterName=EDS file extensions +SubNumber=0x7 +ObjectType=0x9 + +[3050sub0] +ParameterName=Highest subindex +ObjectType=0x7 +DataType=0x0005 +AccessType=ro +DefaultValue=0x02 +PDOMapping=0x0 + +[3050sub1] +ParameterName=FactorAndDescription +ObjectType=0x7 +DataType=0x0004 +AccessType=ro +PDOMapping=0x0 +Factor=0.1 +Description=This is the a test description +Unit=mV + +[3050sub2] +ParameterName=Error Factor and No Description +ObjectType=0x7 +DataType=0x0004 +AccessType=ro +PDOMapping=0x0 +Factor=ERROR +Description= +Unit= diff --git a/test/test_eds.py b/test/test_eds.py index 5edd8d86..17674d45 100644 --- a/test/test_eds.py +++ b/test/test_eds.py @@ -111,6 +111,18 @@ def test_dummy_variable(self): def test_dummy_variable_undefined(self): with self.assertRaises(KeyError): var_undef = self.od['Dummy0001'] + + def test_reading_factor(self): + var = self.od['EDS file extensions']['FactorAndDescription'] + self.assertEqual(var.factor, 0.1) + self.assertEqual(var.description, "This is the a test description") + self.assertEqual(var.unit,'mV') + var2 = self.od['EDS file extensions']['Error Factor and No Description'] + self.assertEqual(var2.description, '') + self.assertEqual(var2.factor, 1) + self.assertEqual(var2.unit, '') + + def test_comments(self): self.assertEqual(self.od.comments, From 3585837c482bae2dc0aadd7726fe5608c03163e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frieder=20Sch=C3=BCler?= <frieder.schueler@gmail.com> Date: Fri, 1 Sep 2023 21:36:42 +0200 Subject: [PATCH 11/15] Correct signed_int_from_hex implementation (#394) * fixed signed_int_from_hex method and eds tests. added full unit tests for method. * Fixed testdata, missing import and hex-prefix in string conversion. --- canopen/objectdictionary/eds.py | 7 +++--- test/sample.eds | 7 +++--- test/test_eds.py | 41 ++++++++++++++++++++++++++++++++- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/canopen/objectdictionary/eds.py b/canopen/objectdictionary/eds.py index 187b5135..16ed22c1 100644 --- a/canopen/objectdictionary/eds.py +++ b/canopen/objectdictionary/eds.py @@ -211,9 +211,10 @@ def _calc_bit_length(data_type): def _signed_int_from_hex(hex_str, bit_length): number = int(hex_str, 0) - limit = ((1 << bit_length - 1) - 1) - if number > limit: - return limit - number + max_value = (1 << (bit_length - 1)) - 1 + + if number > max_value: + return number - (1 << bit_length) else: return number diff --git a/test/sample.eds b/test/sample.eds index 2267ff55..b88ff75b 100644 --- a/test/sample.eds +++ b/test/sample.eds @@ -926,8 +926,8 @@ ParameterName=INTEGER32 only negative values ObjectType=0x7 DataType=0x04 AccessType=rw -HighLimit=0x00000000 -LowLimit=0xFFFFFFFF +HighLimit=0xFFFFFFFF +LowLimit=0x80000000 PDOMapping=0 [3040] @@ -936,10 +936,9 @@ ObjectType=0x7 DataType=0x15 AccessType=rw HighLimit=0x000000000000000A -LowLimit=0x8000000000000009 +LowLimit=0xFFFFFFFFFFFFFFF6 PDOMapping=0 - [3050] ParameterName=EDS file extensions SubNumber=0x7 diff --git a/test/test_eds.py b/test/test_eds.py index 17674d45..50629234 100644 --- a/test/test_eds.py +++ b/test/test_eds.py @@ -1,12 +1,44 @@ import os import unittest import canopen +from canopen.objectdictionary.eds import _signed_int_from_hex EDS_PATH = os.path.join(os.path.dirname(__file__), 'sample.eds') class TestEDS(unittest.TestCase): + test_data = { + "int8": [ + {"hex_str": "7F", "bit_length": 8, "expected": 127}, + {"hex_str": "80", "bit_length": 8, "expected": -128}, + {"hex_str": "FF", "bit_length": 8, "expected": -1}, + {"hex_str": "00", "bit_length": 8, "expected": 0}, + {"hex_str": "01", "bit_length": 8, "expected": 1} + ], + "int16": [ + {"hex_str": "7FFF", "bit_length": 16, "expected": 32767}, + {"hex_str": "8000", "bit_length": 16, "expected": -32768}, + {"hex_str": "FFFF", "bit_length": 16, "expected": -1}, + {"hex_str": "0000", "bit_length": 16, "expected": 0}, + {"hex_str": "0001", "bit_length": 16, "expected": 1} + ], + "int32": [ + {"hex_str": "7FFFFFFF", "bit_length": 32, "expected": 2147483647}, + {"hex_str": "80000000", "bit_length": 32, "expected": -2147483648}, + {"hex_str": "FFFFFFFF", "bit_length": 32, "expected": -1}, + {"hex_str": "00000000", "bit_length": 32, "expected": 0}, + {"hex_str": "00000001", "bit_length": 32, "expected": 1} + ], + "int64": [ + {"hex_str": "7FFFFFFFFFFFFFFF", "bit_length": 64, "expected": 9223372036854775807}, + {"hex_str": "8000000000000000", "bit_length": 64, "expected": -9223372036854775808}, + {"hex_str": "FFFFFFFFFFFFFFFF", "bit_length": 64, "expected": -1}, + {"hex_str": "0000000000000000", "bit_length": 64, "expected": 0}, + {"hex_str": "0000000000000001", "bit_length": 64, "expected": 1} + ] + } + def setUp(self): self.od = canopen.import_od(EDS_PATH, 2) @@ -58,11 +90,18 @@ def test_record_with_limits(self): self.assertEqual(uint8.max, 10) int32 = self.od[0x3030] self.assertEqual(int32.min, -2147483648) - self.assertEqual(int32.max, 0) + self.assertEqual(int32.max, -1) int64 = self.od[0x3040] self.assertEqual(int64.min, -10) self.assertEqual(int64.max, +10) + def test_signed_int_from_hex(self): + for data_type, test_cases in self.test_data.items(): + for test_case in test_cases: + with self.subTest(data_type=data_type, test_case=test_case): + result = _signed_int_from_hex('0x' + test_case["hex_str"], test_case["bit_length"]) + self.assertEqual(result, test_case["expected"]) + def test_array_compact_subobj(self): array = self.od[0x1003] self.assertIsInstance(array, canopen.objectdictionary.Array) From 0285f582beb344de708ce8308274a99bc04988a1 Mon Sep 17 00:00:00 2001 From: Svein Seldal <sveinse@users.noreply.github.com> Date: Fri, 1 Sep 2023 21:49:49 +0200 Subject: [PATCH 12/15] Rename Variable, Record and Array (#363) (#368) To create better distinction between the different Variable, Record and Array types used for different purposes. Helps development, as IDE/linters often only display base name of class. --- canopen/node/local.py | 2 +- canopen/node/remote.py | 8 ++-- canopen/objectdictionary/__init__.py | 62 +++++++++++++++------------- canopen/objectdictionary/eds.py | 24 +++++------ canopen/objectdictionary/epf.py | 6 +-- canopen/pdo/__init__.py | 5 ++- canopen/pdo/base.py | 30 ++++++++------ canopen/sdo/__init__.py | 5 ++- canopen/sdo/base.py | 34 ++++++++------- canopen/variable.py | 6 +-- doc/od.rst | 14 +++---- doc/pdo.rst | 8 ++-- doc/sdo.rst | 20 ++++----- test/test_eds.py | 14 +++---- test/test_od.py | 40 +++++++++--------- 15 files changed, 150 insertions(+), 128 deletions(-) diff --git a/canopen/node/local.py b/canopen/node/local.py index b36cf3eb..de3e23b9 100644 --- a/canopen/node/local.py +++ b/canopen/node/local.py @@ -120,7 +120,7 @@ def _find_object(self, index, subindex): # Index does not exist raise SdoAbortedError(0x06020000) obj = self.object_dictionary[index] - if not isinstance(obj, objectdictionary.Variable): + if not isinstance(obj, objectdictionary.ODVariable): # Group or array if subindex not in obj: # Subindex does not exist diff --git a/canopen/node/remote.py b/canopen/node/remote.py index 8e4025d7..07462422 100644 --- a/canopen/node/remote.py +++ b/canopen/node/remote.py @@ -5,7 +5,7 @@ from canopen.nmt import NmtMaster from canopen.emcy import EmcyConsumer from canopen.pdo import TPDO, RPDO, PDO -from canopen.objectdictionary import Record, Array, Variable, ObjectDictionary +from canopen.objectdictionary import ODRecord, ODArray, ODVariable, ObjectDictionary from canopen.node.base import BaseNode logger = logging.getLogger(__name__) @@ -148,10 +148,10 @@ def __load_configuration_helper(self, index, subindex, name, value): def load_configuration(self): ''' Load the configuration of the node from the object dictionary.''' for obj in self.object_dictionary.values(): - if isinstance(obj, Record) or isinstance(obj, Array): + if isinstance(obj, ODRecord) or isinstance(obj, ODArray): for subobj in obj.values(): - if isinstance(subobj, Variable) and subobj.writable and (subobj.value is not None): + if isinstance(subobj, ODVariable) and subobj.writable and (subobj.value is not None): self.__load_configuration_helper(subobj.index, subobj.subindex, subobj.name, subobj.value) - elif isinstance(obj, Variable) and obj.writable and (obj.value is not None): + elif isinstance(obj, ODVariable) and obj.writable and (obj.value is not None): self.__load_configuration_helper(obj.index, None, obj.name, obj.value) self.pdo.read() # reads the new configuration from the driver diff --git a/canopen/objectdictionary/__init__.py b/canopen/objectdictionary/__init__.py index 3c608126..066a069c 100644 --- a/canopen/objectdictionary/__init__.py +++ b/canopen/objectdictionary/__init__.py @@ -103,7 +103,7 @@ def __init__(self): def __getitem__( self, index: Union[int, str] - ) -> Union["Array", "Record", "Variable"]: + ) -> Union["ODArray", "ODRecord", "ODVariable"]: """Get object from object dictionary by name or index.""" item = self.names.get(index) or self.indices.get(index) if item is None: @@ -112,7 +112,7 @@ def __getitem__( return item def __setitem__( - self, index: Union[int, str], obj: Union["Array", "Record", "Variable"] + self, index: Union[int, str], obj: Union["ODArray", "ODRecord", "ODVariable"] ): assert index == obj.index or index == obj.name self.add_object(obj) @@ -131,14 +131,14 @@ def __len__(self) -> int: def __contains__(self, index: Union[int, str]): return index in self.names or index in self.indices - def add_object(self, obj: Union["Array", "Record", "Variable"]) -> None: + def add_object(self, obj: Union["ODArray", "ODRecord", "ODVariable"]) -> None: """Add object to the object dictionary. :param obj: Should be either one of - :class:`~canopen.objectdictionary.Variable`, - :class:`~canopen.objectdictionary.Record`, or - :class:`~canopen.objectdictionary.Array`. + :class:`~canopen.objectdictionary.ODVariable`, + :class:`~canopen.objectdictionary.ODRecord`, or + :class:`~canopen.objectdictionary.ODArray`. """ obj.parent = self self.indices[obj.index] = obj @@ -146,20 +146,20 @@ def add_object(self, obj: Union["Array", "Record", "Variable"]) -> None: def get_variable( self, index: Union[int, str], subindex: int = 0 - ) -> Optional["Variable"]: + ) -> Optional["ODVariable"]: """Get the variable object at specified index (and subindex if applicable). - :return: Variable if found, else `None` + :return: ODVariable if found, else `None` """ obj = self.get(index) - if isinstance(obj, Variable): + if isinstance(obj, ODVariable): return obj - elif isinstance(obj, (Record, Array)): + elif isinstance(obj, (ODRecord, ODArray)): return obj.get(subindex) -class Record(MutableMapping): - """Groups multiple :class:`~canopen.objectdictionary.Variable` objects using +class ODRecord(MutableMapping): + """Groups multiple :class:`~canopen.objectdictionary.ODVariable` objects using subindices. """ @@ -178,13 +178,13 @@ def __init__(self, name: str, index: int): self.subindices = {} self.names = {} - def __getitem__(self, subindex: Union[int, str]) -> "Variable": + def __getitem__(self, subindex: Union[int, str]) -> "ODVariable": item = self.names.get(subindex) or self.subindices.get(subindex) if item is None: raise KeyError("Subindex %s was not found" % subindex) return item - def __setitem__(self, subindex: Union[int, str], var: "Variable"): + def __setitem__(self, subindex: Union[int, str], var: "ODVariable"): assert subindex == var.subindex self.add_member(var) @@ -202,18 +202,18 @@ def __iter__(self) -> Iterable[int]: def __contains__(self, subindex: Union[int, str]) -> bool: return subindex in self.names or subindex in self.subindices - def __eq__(self, other: "Record") -> bool: + def __eq__(self, other: "ODRecord") -> bool: return self.index == other.index - def add_member(self, variable: "Variable") -> None: - """Adds a :class:`~canopen.objectdictionary.Variable` to the record.""" + def add_member(self, variable: "ODVariable") -> None: + """Adds a :class:`~canopen.objectdictionary.ODVariable` to the record.""" variable.parent = self self.subindices[variable.subindex] = variable self.names[variable.name] = variable -class Array(Mapping): - """An array of :class:`~canopen.objectdictionary.Variable` objects using +class ODArray(Mapping): + """An array of :class:`~canopen.objectdictionary.ODVariable` objects using subindices. Actual length of array must be read from the node using SDO. @@ -234,7 +234,7 @@ def __init__(self, name: str, index: int): self.subindices = {} self.names = {} - def __getitem__(self, subindex: Union[int, str]) -> "Variable": + def __getitem__(self, subindex: Union[int, str]) -> "ODVariable": var = self.names.get(subindex) or self.subindices.get(subindex) if var is not None: # This subindex is defined @@ -243,7 +243,7 @@ def __getitem__(self, subindex: Union[int, str]) -> "Variable": # Create a new variable based on first array item template = self.subindices[1] name = "%s_%x" % (template.name, subindex) - var = Variable(name, self.index, subindex) + var = ODVariable(name, self.index, subindex) var.parent = self for attr in ("data_type", "unit", "factor", "min", "max", "default", "access_type", "description", "value_descriptions", @@ -260,17 +260,17 @@ def __len__(self) -> int: def __iter__(self) -> Iterable[int]: return iter(sorted(self.subindices)) - def __eq__(self, other: "Array") -> bool: + def __eq__(self, other: "ODArray") -> bool: return self.index == other.index - def add_member(self, variable: "Variable") -> None: - """Adds a :class:`~canopen.objectdictionary.Variable` to the record.""" + def add_member(self, variable: "ODVariable") -> None: + """Adds a :class:`~canopen.objectdictionary.ODVariable` to the record.""" variable.parent = self self.subindices[variable.subindex] = variable self.names[variable.name] = variable -class Variable: +class ODVariable: """Simple variable.""" STRUCT_TYPES = { @@ -289,8 +289,8 @@ class Variable: def __init__(self, name: str, index: int, subindex: int = 0): #: The :class:`~canopen.ObjectDictionary`, - #: :class:`~canopen.objectdictionary.Record` or - #: :class:`~canopen.objectdictionary.Array` owning the variable + #: :class:`~canopen.objectdictionary.ODRecord` or + #: :class:`~canopen.objectdictionary.ODArray` owning the variable self.parent = None #: 16-bit address of the object in the dictionary self.index = index @@ -328,7 +328,7 @@ def __init__(self, name: str, index: int, subindex: int = 0): self.pdo_mappable = False - def __eq__(self, other: "Variable") -> bool: + def __eq__(self, other: "ODVariable") -> bool: return (self.index == other.index and self.subindex == other.subindex) @@ -486,3 +486,9 @@ def __init__(self): class ObjectDictionaryError(Exception): """Unsupported operation with the current Object Dictionary.""" + + +# Compatibility for old names +Record = ODRecord +Array = ODArray +Variable = ODVariable diff --git a/canopen/objectdictionary/eds.py b/canopen/objectdictionary/eds.py index 16ed22c1..73a7eaeb 100644 --- a/canopen/objectdictionary/eds.py +++ b/canopen/objectdictionary/eds.py @@ -101,7 +101,7 @@ def import_eds(source, node_id): for i in range(1, 8): key = "Dummy%04d" % i if eds.getint(section, key) == 1: - var = objectdictionary.Variable(key, i, 0) + var = objectdictionary.ODVariable(key, i, 0) var.data_type = i var.access_type = "const" od.add_object(var) @@ -127,8 +127,8 @@ def import_eds(source, node_id): var = build_variable(eds, section, node_id, index) od.add_object(var) elif object_type == ARR and eds.has_option(section, "CompactSubObj"): - arr = objectdictionary.Array(name, index) - last_subindex = objectdictionary.Variable( + arr = objectdictionary.ODArray(name, index) + last_subindex = objectdictionary.ODVariable( "Number of entries", index, 0) last_subindex.data_type = datatypes.UNSIGNED8 arr.add_member(last_subindex) @@ -136,11 +136,11 @@ def import_eds(source, node_id): arr.storage_location = storage_location od.add_object(arr) elif object_type == ARR: - arr = objectdictionary.Array(name, index) + arr = objectdictionary.ODArray(name, index) arr.storage_location = storage_location od.add_object(arr) elif object_type == RECORD: - record = objectdictionary.Record(name, index) + record = objectdictionary.ODRecord(name, index) record.storage_location = storage_location od.add_object(record) @@ -152,8 +152,8 @@ def import_eds(source, node_id): index = int(match.group(1), 16) subindex = int(match.group(2), 16) entry = od[index] - if isinstance(entry, (objectdictionary.Record, - objectdictionary.Array)): + if isinstance(entry, (objectdictionary.ODRecord, + objectdictionary.ODArray)): var = build_variable(eds, section, node_id, index, subindex) entry.add_member(var) @@ -257,7 +257,7 @@ def build_variable(eds, section, node_id, index, subindex=0): :param subindex: Subindex of the CANOpen object (if presente, else 0) """ name = eds.get(section, "ParameterName") - var = objectdictionary.Variable(name, index, subindex) + var = objectdictionary.ODVariable(name, index, subindex) try: var.storage_location = eds.get(section, "StorageLocation") except NoOptionError: @@ -344,11 +344,11 @@ def export_dcf(od, dest=None, fileInfo={}): def export_eds(od, dest=None, file_info={}, device_commisioning=False): def export_object(obj, eds): - if isinstance(obj, objectdictionary.Variable): + if isinstance(obj, objectdictionary.ODVariable): return export_variable(obj, eds) - if isinstance(obj, objectdictionary.Record): + if isinstance(obj, objectdictionary.ODRecord): return export_record(obj, eds) - if isinstance(obj, objectdictionary.Array): + if isinstance(obj, objectdictionary.ODArray): return export_array(obj, eds) def export_common(var, eds, section): @@ -404,7 +404,7 @@ def export_record(var, eds): section = "%04X" % var.index export_common(var, eds, section) eds.set(section, "SubNumber", "0x%X" % len(var.subindices)) - ot = RECORD if isinstance(var, objectdictionary.Record) else ARR + ot = RECORD if isinstance(var, objectdictionary.ODRecord) else ARR eds.set(section, "ObjectType", "0x%X" % ot) for i in var: export_variable(var[i], eds) diff --git a/canopen/objectdictionary/epf.py b/canopen/objectdictionary/epf.py index 8bfc513a..f884b659 100644 --- a/canopen/objectdictionary/epf.py +++ b/canopen/objectdictionary/epf.py @@ -61,7 +61,7 @@ def import_epf(epf): od.add_object(var) elif len(parameters) == 2 and parameters[1].get("ObjectType") == "ARRAY": # Array - arr = objectdictionary.Array(name, index) + arr = objectdictionary.ODArray(name, index) for par_tree in parameters: var = build_variable(par_tree) arr.add_member(var) @@ -71,7 +71,7 @@ def import_epf(epf): od.add_object(arr) else: # Complex record - record = objectdictionary.Record(name, index) + record = objectdictionary.ODRecord(name, index) for par_tree in parameters: var = build_variable(par_tree) record.add_member(var) @@ -89,7 +89,7 @@ def build_variable(par_tree): name = par_tree.get("SymbolName") data_type = par_tree.get("DataType") - par = objectdictionary.Variable(name, index, subindex) + par = objectdictionary.ODVariable(name, index, subindex) factor = par_tree.get("Factor", "1") par.factor = int(factor) if factor.isdigit() else float(factor) unit = par_tree.get("Unit") diff --git a/canopen/pdo/__init__.py b/canopen/pdo/__init__.py index d47ec693..46396e30 100644 --- a/canopen/pdo/__init__.py +++ b/canopen/pdo/__init__.py @@ -1,7 +1,10 @@ import logging from canopen import node -from canopen.pdo.base import PdoBase, Maps, Map, Variable +from canopen.pdo.base import PdoBase, Maps + +# Compatibility +from .base import Variable logger = logging.getLogger(__name__) diff --git a/canopen/pdo/base.py b/canopen/pdo/base.py index 086a0774..bb166e7a 100644 --- a/canopen/pdo/base.py +++ b/canopen/pdo/base.py @@ -180,7 +180,7 @@ def __init__(self, pdo_node, com_record, map_array): #: Ignores SYNC objects up to this SYNC counter value (optional) self.sync_start_value: Optional[int] = None #: List of variables mapped to this PDO - self.map: List["Variable"] = [] + self.map: List["PdoVariable"] = [] self.length: int = 0 #: Current message data self.data = bytearray() @@ -214,7 +214,7 @@ def __getitem_by_name(self, value): raise KeyError('{0} not found in map. Valid entries are {1}'.format( value, ', '.join(valid_values))) - def __getitem__(self, key: Union[int, str]) -> "Variable": + def __getitem__(self, key: Union[int, str]) -> "PdoVariable": var = None if isinstance(key, int): # there is a maximum available of 8 slots per PDO map @@ -229,7 +229,7 @@ def __getitem__(self, key: Union[int, str]) -> "Variable": var = self.__getitem_by_name(key) return var - def __iter__(self) -> Iterable["Variable"]: + def __iter__(self) -> Iterable["PdoVariable"]: return iter(self.map) def __len__(self) -> int: @@ -237,9 +237,9 @@ def __len__(self) -> int: def _get_variable(self, index, subindex): obj = self.pdo_node.node.object_dictionary[index] - if isinstance(obj, (objectdictionary.Record, objectdictionary.Array)): + if isinstance(obj, (objectdictionary.ODRecord, objectdictionary.ODArray)): obj = obj[subindex] - var = Variable(obj) + var = PdoVariable(obj) var.pdo_parent = self return var @@ -248,8 +248,8 @@ def _fill_map(self, needed): logger.info("Filling up fixed-length mapping array") while len(self.map) < needed: # Generate a dummy mapping for an invalid object with zero length. - obj = objectdictionary.Variable('Dummy', 0, 0) - var = Variable(obj) + obj = objectdictionary.ODVariable('Dummy', 0, 0) + var = PdoVariable(obj) var.length = 0 self.map.append(var) @@ -440,13 +440,13 @@ def add_variable( index: Union[str, int], subindex: Union[str, int] = 0, length: Optional[int] = None, - ) -> "Variable": + ) -> "PdoVariable": """Add a variable from object dictionary as the next entry. :param index: Index of variable as name or number :param subindex: Sub-index of variable as name or number :param length: Size of data in number of bits - :return: Variable that was added + :return: PdoVariable that was added """ try: var = self._get_variable(index, subindex) @@ -528,11 +528,11 @@ def wait_for_reception(self, timeout: float = 10) -> float: return self.timestamp if self.is_received else None -class Variable(variable.Variable): +class PdoVariable(variable.Variable): """One object dictionary variable mapped to a PDO.""" - def __init__(self, od: objectdictionary.Variable): - #: PDO object that is associated with this Variable Object + def __init__(self, od: objectdictionary.ODVariable): + #: PDO object that is associated with this ODVariable Object self.pdo_parent = None #: Location of variable in the message in bits self.offset = None @@ -542,7 +542,7 @@ def __init__(self, od: objectdictionary.Variable): def get_data(self) -> bytes: """Reads the PDO variable from the last received message. - :return: Variable value as :class:`bytes`. + :return: PdoVariable value as :class:`bytes`. """ byte_offset, bit_offset = divmod(self.offset, 8) @@ -598,3 +598,7 @@ def set_data(self, data: bytes): self.pdo_parent.data[byte_offset:byte_offset + len(data)] = data self.pdo_parent.update() + + +# For compatibility +Variable = PdoVariable diff --git a/canopen/sdo/__init__.py b/canopen/sdo/__init__.py index 160affd3..775a8c4e 100644 --- a/canopen/sdo/__init__.py +++ b/canopen/sdo/__init__.py @@ -1,4 +1,7 @@ -from canopen.sdo.base import Variable, Record, Array +from canopen.sdo.base import SdoVariable, SdoRecord, SdoArray from canopen.sdo.client import SdoClient from canopen.sdo.server import SdoServer from canopen.sdo.exceptions import SdoAbortedError, SdoCommunicationError + +# Compatibility +from .base import Variable, Record, Array diff --git a/canopen/sdo/base.py b/canopen/sdo/base.py index 25d3d60c..85cfe4e9 100644 --- a/canopen/sdo/base.py +++ b/canopen/sdo/base.py @@ -49,14 +49,14 @@ def __init__( def __getitem__( self, index: Union[str, int] - ) -> Union["Variable", "Array", "Record"]: + ) -> Union["SdoVariable", "SdoArray", "SdoRecord"]: entry = self.od[index] - if isinstance(entry, objectdictionary.Variable): - return Variable(self, entry) - elif isinstance(entry, objectdictionary.Array): - return Array(self, entry) - elif isinstance(entry, objectdictionary.Record): - return Record(self, entry) + if isinstance(entry, objectdictionary.ODVariable): + return SdoVariable(self, entry) + elif isinstance(entry, objectdictionary.ODArray): + return SdoArray(self, entry) + elif isinstance(entry, objectdictionary.ODRecord): + return SdoRecord(self, entry) def __iter__(self) -> Iterable[int]: return iter(self.od) @@ -80,14 +80,14 @@ def download( raise NotImplementedError() -class Record(Mapping): +class SdoRecord(Mapping): def __init__(self, sdo_node: SdoBase, od: ObjectDictionary): self.sdo_node = sdo_node self.od = od - def __getitem__(self, subindex: Union[int, str]) -> "Variable": - return Variable(self.sdo_node, self.od[subindex]) + def __getitem__(self, subindex: Union[int, str]) -> "SdoVariable": + return SdoVariable(self.sdo_node, self.od[subindex]) def __iter__(self) -> Iterable[int]: return iter(self.od) @@ -99,14 +99,14 @@ def __contains__(self, subindex: Union[int, str]) -> bool: return subindex in self.od -class Array(Mapping): +class SdoArray(Mapping): def __init__(self, sdo_node: SdoBase, od: ObjectDictionary): self.sdo_node = sdo_node self.od = od - def __getitem__(self, subindex: Union[int, str]) -> "Variable": - return Variable(self.sdo_node, self.od[subindex]) + def __getitem__(self, subindex: Union[int, str]) -> "SdoVariable": + return SdoVariable(self.sdo_node, self.od[subindex]) def __iter__(self) -> Iterable[int]: return iter(range(1, len(self) + 1)) @@ -118,7 +118,7 @@ def __contains__(self, subindex: int) -> bool: return 0 <= subindex <= len(self) -class Variable(variable.Variable): +class SdoVariable(variable.Variable): """Access object dictionary variable values using SDO protocol.""" def __init__(self, sdo_node: SdoBase, od: ObjectDictionary): @@ -165,3 +165,9 @@ def open(self, mode="rb", encoding="ascii", buffering=1024, size=None, """ return self.sdo_node.open(self.od.index, self.od.subindex, mode, encoding, buffering, size, block_transfer, request_crc_support=request_crc_support) + + +# For compatibility +Record = SdoRecord +Array = SdoArray +Variable = SdoVariable diff --git a/canopen/variable.py b/canopen/variable.py index c7924e51..e8687660 100644 --- a/canopen/variable.py +++ b/canopen/variable.py @@ -12,12 +12,12 @@ class Variable: - def __init__(self, od: objectdictionary.Variable): + def __init__(self, od: objectdictionary.ODVariable): self.od = od #: Description of this variable from Object Dictionary, overridable self.name = od.name - if isinstance(od.parent, (objectdictionary.Record, - objectdictionary.Array)): + if isinstance(od.parent, (objectdictionary.ODRecord, + objectdictionary.ODArray)): # Include the parent object's name for subentries self.name = od.parent.name + "." + od.name #: Holds a local, overridable copy of the Object Index diff --git a/doc/od.rst b/doc/od.rst index 043dce01..6e7eb3b5 100644 --- a/doc/od.rst +++ b/doc/od.rst @@ -40,7 +40,7 @@ Here is an example where the entire object dictionary gets printed out:: node = network.add_node(6, 'od.eds') for obj in node.object_dictionary.values(): print('0x%X: %s' % (obj.index, obj.name)) - if isinstance(obj, canopen.objectdictionary.Record): + if isinstance(obj, canopen.objectdictionary.ODRecord): for subobj in obj.values(): print(' %d: %s' % (subobj.subindex, subobj.name)) @@ -79,7 +79,7 @@ API Return a list of objects (records, arrays and variables). -.. autoclass:: canopen.objectdictionary.Variable +.. autoclass:: canopen.objectdictionary.ODVariable :members: .. describe:: len(var) @@ -91,12 +91,12 @@ API Return ``True`` if the variables have the same index and subindex. -.. autoclass:: canopen.objectdictionary.Record +.. autoclass:: canopen.objectdictionary.ODRecord :members: .. describe:: record[subindex] - Return the :class:`~canopen.objectdictionary.Variable` for the specified + Return the :class:`~canopen.objectdictionary.ODVariable` for the specified subindex (as int) or name (as string). .. describe:: iter(record) @@ -118,15 +118,15 @@ API .. method:: values() - Return a list of :class:`~canopen.objectdictionary.Variable` in the record. + Return a list of :class:`~canopen.objectdictionary.ODVariable` in the record. -.. autoclass:: canopen.objectdictionary.Array +.. autoclass:: canopen.objectdictionary.ODArray :members: .. describe:: array[subindex] - Return the :class:`~canopen.objectdictionary.Variable` for the specified + Return the :class:`~canopen.objectdictionary.ODVariable` for the specified subindex (as int) or name (as string). This will work for all subindexes between 1 and 255. If the requested subindex has not been specified in the object dictionary, it will be diff --git a/doc/pdo.rst b/doc/pdo.rst index 9a7c027b..05e1e94d 100644 --- a/doc/pdo.rst +++ b/doc/pdo.rst @@ -106,22 +106,22 @@ API .. describe:: map[name] - Return the :class:`canopen.pdo.Variable` for the variable specified as + Return the :class:`canopen.pdo.PdoVariable` for the variable specified as ``"Group.Variable"`` or ``"Variable"`` or as a position starting at 0. .. describe:: iter(map) - Return an iterator of the :class:`canopen.pdo.Variable` entries in the map. + Return an iterator of the :class:`canopen.pdo.PdoVariable` entries in the map. .. describe:: len(map) Return the number of variables in the map. -.. autoclass:: canopen.pdo.Variable +.. autoclass:: canopen.pdo.PdoVariable :members: :inherited-members: .. py:attribute:: od - The :class:`canopen.objectdictionary.Variable` associated with this object. + The :class:`canopen.objectdictionary.ODVariable` associated with this object. diff --git a/doc/sdo.rst b/doc/sdo.rst index c0db4ca5..7b06118e 100644 --- a/doc/sdo.rst +++ b/doc/sdo.rst @@ -163,25 +163,25 @@ API Return a list of objects (records, arrays and variables). -.. autoclass:: canopen.sdo.Variable +.. autoclass:: canopen.sdo.SdoVariable :members: :inherited-members: .. py:attribute:: od - The :class:`canopen.objectdictionary.Variable` associated with this object. + The :class:`canopen.objectdictionary.ODVariable` associated with this object. -.. autoclass:: canopen.sdo.Record +.. autoclass:: canopen.sdo.SdoRecord :members: .. py:attribute:: od - The :class:`canopen.objectdictionary.Record` associated with this object. + The :class:`canopen.objectdictionary.ODRecord` associated with this object. .. describe:: record[subindex] - Return the :class:`canopen.sdo.Variable` for the specified subindex + Return the :class:`canopen.sdo.SdoVariable` for the specified subindex (as int) or name (as string). .. describe:: iter(record) @@ -199,19 +199,19 @@ API .. method:: values() - Return a list of :class:`canopen.sdo.Variable` in the record. + Return a list of :class:`canopen.sdo.SdoVariable` in the record. -.. autoclass:: canopen.sdo.Array +.. autoclass:: canopen.sdo.SdoArray :members: .. py:attribute:: od - The :class:`canopen.objectdictionary.Array` associated with this object. + The :class:`canopen.objectdictionary.ODArray` associated with this object. .. describe:: array[subindex] - Return the :class:`canopen.sdo.Variable` for the specified subindex + Return the :class:`canopen.sdo.SdoVariable` for the specified subindex (as int) or name (as string). .. describe:: iter(array) @@ -234,7 +234,7 @@ API .. method:: values() - Return a list of :class:`canopen.sdo.Variable` in the array. + Return a list of :class:`canopen.sdo.SdoVariable` in the array. This will make a SDO read operation on subindex 0 in order to get the actual length of the array. diff --git a/test/test_eds.py b/test/test_eds.py index 50629234..a3923709 100644 --- a/test/test_eds.py +++ b/test/test_eds.py @@ -53,7 +53,7 @@ def test_load_file_object(self): def test_variable(self): var = self.od['Producer heartbeat time'] - self.assertIsInstance(var, canopen.objectdictionary.Variable) + self.assertIsInstance(var, canopen.objectdictionary.ODVariable) self.assertEqual(var.index, 0x1017) self.assertEqual(var.subindex, 0) self.assertEqual(var.name, 'Producer heartbeat time') @@ -69,12 +69,12 @@ def test_relative_variable(self): def test_record(self): record = self.od['Identity object'] - self.assertIsInstance(record, canopen.objectdictionary.Record) + self.assertIsInstance(record, canopen.objectdictionary.ODRecord) self.assertEqual(len(record), 5) self.assertEqual(record.index, 0x1018) self.assertEqual(record.name, 'Identity object') var = record['Vendor-ID'] - self.assertIsInstance(var, canopen.objectdictionary.Variable) + self.assertIsInstance(var, canopen.objectdictionary.ODVariable) self.assertEqual(var.name, 'Vendor-ID') self.assertEqual(var.index, 0x1018) self.assertEqual(var.subindex, 1) @@ -104,11 +104,11 @@ def test_signed_int_from_hex(self): def test_array_compact_subobj(self): array = self.od[0x1003] - self.assertIsInstance(array, canopen.objectdictionary.Array) + self.assertIsInstance(array, canopen.objectdictionary.ODArray) self.assertEqual(array.index, 0x1003) self.assertEqual(array.name, 'Pre-defined error field') var = array[5] - self.assertIsInstance(var, canopen.objectdictionary.Variable) + self.assertIsInstance(var, canopen.objectdictionary.ODVariable) self.assertEqual(var.name, 'Pre-defined error field_5') self.assertEqual(var.index, 0x1003) self.assertEqual(var.subindex, 5) @@ -139,7 +139,7 @@ def test_sub_index_w_capital_s(self): def test_dummy_variable(self): var = self.od['Dummy0003'] - self.assertIsInstance(var, canopen.objectdictionary.Variable) + self.assertIsInstance(var, canopen.objectdictionary.ODVariable) self.assertEqual(var.index, 0x0003) self.assertEqual(var.subindex, 0) self.assertEqual(var.name, 'Dummy0003') @@ -199,7 +199,7 @@ def test_export_eds(self): self.assertEqual(type(actual_object), type(expected_object)) self.assertEqual(actual_object.name, expected_object.name) - if isinstance(actual_object, canopen.objectdictionary.Variable): + if isinstance(actual_object, canopen.objectdictionary.ODVariable): expected_vars = [expected_object] actual_vars = [actual_object] else: diff --git a/test/test_od.py b/test/test_od.py index 794df05c..fe90dc13 100644 --- a/test/test_od.py +++ b/test/test_od.py @@ -5,7 +5,7 @@ class TestDataConversions(unittest.TestCase): def test_boolean(self): - var = od.Variable("Test BOOLEAN", 0x1000) + var = od.ODVariable("Test BOOLEAN", 0x1000) var.data_type = od.BOOLEAN self.assertEqual(var.decode_raw(b"\x01"), True) self.assertEqual(var.decode_raw(b"\x00"), False) @@ -13,25 +13,25 @@ def test_boolean(self): self.assertEqual(var.encode_raw(False), b"\x00") def test_unsigned8(self): - var = od.Variable("Test UNSIGNED8", 0x1000) + var = od.ODVariable("Test UNSIGNED8", 0x1000) var.data_type = od.UNSIGNED8 self.assertEqual(var.decode_raw(b"\xff"), 255) self.assertEqual(var.encode_raw(254), b"\xfe") def test_unsigned16(self): - var = od.Variable("Test UNSIGNED16", 0x1000) + var = od.ODVariable("Test UNSIGNED16", 0x1000) var.data_type = od.UNSIGNED16 self.assertEqual(var.decode_raw(b"\xfe\xff"), 65534) self.assertEqual(var.encode_raw(65534), b"\xfe\xff") def test_unsigned32(self): - var = od.Variable("Test UNSIGNED32", 0x1000) + var = od.ODVariable("Test UNSIGNED32", 0x1000) var.data_type = od.UNSIGNED32 self.assertEqual(var.decode_raw(b"\xfc\xfd\xfe\xff"), 4294901244) self.assertEqual(var.encode_raw(4294901244), b"\xfc\xfd\xfe\xff") def test_integer8(self): - var = od.Variable("Test INTEGER8", 0x1000) + var = od.ODVariable("Test INTEGER8", 0x1000) var.data_type = od.INTEGER8 self.assertEqual(var.decode_raw(b"\xff"), -1) self.assertEqual(var.decode_raw(b"\x7f"), 127) @@ -39,7 +39,7 @@ def test_integer8(self): self.assertEqual(var.encode_raw(127), b"\x7f") def test_integer16(self): - var = od.Variable("Test INTEGER16", 0x1000) + var = od.ODVariable("Test INTEGER16", 0x1000) var.data_type = od.INTEGER16 self.assertEqual(var.decode_raw(b"\xfe\xff"), -2) self.assertEqual(var.decode_raw(b"\x01\x00"), 1) @@ -47,13 +47,13 @@ def test_integer16(self): self.assertEqual(var.encode_raw(1), b"\x01\x00") def test_integer32(self): - var = od.Variable("Test INTEGER32", 0x1000) + var = od.ODVariable("Test INTEGER32", 0x1000) var.data_type = od.INTEGER32 self.assertEqual(var.decode_raw(b"\xfe\xff\xff\xff"), -2) self.assertEqual(var.encode_raw(-2), b"\xfe\xff\xff\xff") def test_visible_string(self): - var = od.Variable("Test VISIBLE_STRING", 0x1000) + var = od.ODVariable("Test VISIBLE_STRING", 0x1000) var.data_type = od.VISIBLE_STRING self.assertEqual(var.decode_raw(b"abcdefg"), "abcdefg") self.assertEqual(var.decode_raw(b"zero terminated\x00"), "zero terminated") @@ -63,7 +63,7 @@ def test_visible_string(self): class TestAlternativeRepresentations(unittest.TestCase): def test_phys(self): - var = od.Variable("Test INTEGER16", 0x1000) + var = od.ODVariable("Test INTEGER16", 0x1000) var.data_type = od.INTEGER16 var.factor = 0.1 @@ -71,7 +71,7 @@ def test_phys(self): self.assertEqual(var.encode_phys(-0.1), -1) def test_desc(self): - var = od.Variable("Test UNSIGNED8", 0x1000) + var = od.ODVariable("Test UNSIGNED8", 0x1000) var.data_type = od.UNSIGNED8 var.add_value_description(0, "Value 0") var.add_value_description(1, "Value 1") @@ -82,7 +82,7 @@ def test_desc(self): self.assertEqual(var.encode_desc("Value 1"), 1) def test_bits(self): - var = od.Variable("Test UNSIGNED8", 0x1000) + var = od.ODVariable("Test UNSIGNED8", 0x1000) var.data_type = od.UNSIGNED8 var.add_bit_definition("BIT 0", [0]) var.add_bit_definition("BIT 2 and 3", [2, 3]) @@ -99,15 +99,15 @@ class TestObjectDictionary(unittest.TestCase): def test_add_variable(self): test_od = od.ObjectDictionary() - var = od.Variable("Test Variable", 0x1000) + var = od.ODVariable("Test Variable", 0x1000) test_od.add_object(var) self.assertEqual(test_od["Test Variable"], var) self.assertEqual(test_od[0x1000], var) def test_add_record(self): test_od = od.ObjectDictionary() - record = od.Record("Test Record", 0x1001) - var = od.Variable("Test Subindex", 0x1001, 1) + record = od.ODRecord("Test Record", 0x1001) + var = od.ODVariable("Test Subindex", 0x1001, 1) record.add_member(var) test_od.add_object(record) self.assertEqual(test_od["Test Record"], record) @@ -116,8 +116,8 @@ def test_add_record(self): def test_add_array(self): test_od = od.ObjectDictionary() - array = od.Array("Test Array", 0x1002) - array.add_member(od.Variable("Last subindex", 0x1002, 0)) + array = od.ODArray("Test Array", 0x1002) + array.add_member(od.ODVariable("Last subindex", 0x1002, 0)) test_od.add_object(array) self.assertEqual(test_od["Test Array"], array) self.assertEqual(test_od[0x1002], array) @@ -126,12 +126,12 @@ def test_add_array(self): class TestArray(unittest.TestCase): def test_subindexes(self): - array = od.Array("Test Array", 0x1000) - last_subindex = od.Variable("Last subindex", 0x1000, 0) + array = od.ODArray("Test Array", 0x1000) + last_subindex = od.ODVariable("Last subindex", 0x1000, 0) last_subindex.data_type = od.UNSIGNED8 array.add_member(last_subindex) - array.add_member(od.Variable("Test Variable", 0x1000, 1)) - array.add_member(od.Variable("Test Variable 2", 0x1000, 2)) + array.add_member(od.ODVariable("Test Variable", 0x1000, 1)) + array.add_member(od.ODVariable("Test Variable 2", 0x1000, 2)) self.assertEqual(array[0].name, "Last subindex") self.assertEqual(array[1].name, "Test Variable") self.assertEqual(array[2].name, "Test Variable 2") From 54180545d06d8173a0589d95d01bfc75415658dd Mon Sep 17 00:00:00 2001 From: Svein Seldal <sveinse@users.noreply.github.com> Date: Fri, 1 Sep 2023 21:52:43 +0200 Subject: [PATCH 13/15] Implement reading PDO from OD (#273) (#370) --- canopen/pdo/base.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/canopen/pdo/base.py b/canopen/pdo/base.py index bb166e7a..ffe6dc32 100644 --- a/canopen/pdo/base.py +++ b/canopen/pdo/base.py @@ -50,10 +50,10 @@ def __getitem__(self, key): def __len__(self): return len(self.map) - def read(self): + def read(self, from_od=False): """Read PDO configuration from node using SDO.""" for pdo_map in self.map.values(): - pdo_map.read() + pdo_map.read(from_od=from_od) def save(self): """Save PDO configuration to node using SDO.""" @@ -313,43 +313,49 @@ def add_callback(self, callback: Callable[["Map"], None]) -> None: """ self.callbacks.append(callback) - def read(self) -> None: + def read(self, from_od=False) -> None: """Read PDO configuration for this map using SDO.""" - cob_id = self.com_record[1].raw + + def _raw_from(param): + if from_od: + return param.od.default + return param.raw + + cob_id = _raw_from(self.com_record[1]) self.cob_id = cob_id & 0x1FFFFFFF logger.info("COB-ID is 0x%X", self.cob_id) self.enabled = cob_id & PDO_NOT_VALID == 0 logger.info("PDO is %s", "enabled" if self.enabled else "disabled") self.rtr_allowed = cob_id & RTR_NOT_ALLOWED == 0 logger.info("RTR is %s", "allowed" if self.rtr_allowed else "not allowed") - self.trans_type = self.com_record[2].raw + self.trans_type = _raw_from(self.com_record[2]) logger.info("Transmission type is %d", self.trans_type) if self.trans_type >= 254: try: - self.inhibit_time = self.com_record[3].raw + self.inhibit_time = _raw_from(self.com_record[3]) except (KeyError, SdoAbortedError) as e: logger.info("Could not read inhibit time (%s)", e) else: logger.info("Inhibit time is set to %d ms", self.inhibit_time) try: - self.event_timer = self.com_record[5].raw + self.event_timer = _raw_from(self.com_record[5]) except (KeyError, SdoAbortedError) as e: logger.info("Could not read event timer (%s)", e) else: logger.info("Event timer is set to %d ms", self.event_timer) try: - self.sync_start_value = self.com_record[6].raw + self.sync_start_value = _raw_from(self.com_record[6]) except (KeyError, SdoAbortedError) as e: logger.info("Could not read SYNC start value (%s)", e) else: logger.info("SYNC start value is set to %d ms", self.sync_start_value) self.clear() - nof_entries = self.map_array[0].raw + nof_entries = _raw_from(self.map_array[0]) for subindex in range(1, nof_entries + 1): - value = self.map_array[subindex].raw + value = _raw_from(self.map_array[subindex]) index = value >> 16 subindex = (value >> 8) & 0xFF size = value & 0xFF From 6f9f06eb4a2d8989583e35ec0d4b34d7705726d0 Mon Sep 17 00:00:00 2001 From: Carl Ljungholm <ljungholm@gmail.com> Date: Sun, 17 Sep 2023 10:26:42 +0200 Subject: [PATCH 14/15] Use smaller of response size and size in OD (#395) --- canopen/sdo/client.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/canopen/sdo/client.py b/canopen/sdo/client.py index 21517717..370aab72 100644 --- a/canopen/sdo/client.py +++ b/canopen/sdo/client.py @@ -114,21 +114,22 @@ def upload(self, index: int, subindex: int) -> bytes: When node responds with an error. """ with self.open(index, subindex, buffering=0) as fp: - size = fp.size + response_size = fp.size data = fp.read() - if size is None: - # Node did not specify how many bytes to use - # Try to find out using Object Dictionary - var = self.od.get_variable(index, subindex) - if var is not None: - # Found a matching variable in OD - # If this is a data type (string, domain etc) the size is - # unknown anyway so keep the data as is - if var.data_type not in objectdictionary.DATA_TYPES: - # Get the size in bytes for this variable - size = len(var) // 8 + + # If size is available through variable in OD, then use the smaller of the two sizes. + # Some devices send U32/I32 even if variable is smaller in OD + var = self.od.get_variable(index, subindex) + if var is not None: + # Found a matching variable in OD + # If this is a data type (string, domain etc) the size is + # unknown anyway so keep the data as is + if var.data_type not in objectdictionary.DATA_TYPES: + # Get the size in bytes for this variable + var_size = len(var) // 8 + if response_size is None or var_size < response_size: # Truncate the data to specified size - data = data[0:size] + data = data[0:var_size] return data def download( From ba4fa0c337e1d8df1800788f4a778be162302663 Mon Sep 17 00:00:00 2001 From: Pavel Gostev <73311144+vongostev@users.noreply.github.com> Date: Tue, 14 Nov 2023 23:29:49 +0300 Subject: [PATCH 15/15] Support of custom can.Bus implementations to the Network class (#404) --- canopen/network.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/canopen/network.py b/canopen/network.py index 7768795d..0f9c9327 100644 --- a/canopen/network.py +++ b/canopen/network.py @@ -33,7 +33,7 @@ class Listener: class Network(MutableMapping): """Representation of one CAN bus containing one or more nodes.""" - def __init__(self, bus=None): + def __init__(self, bus: can.BusABC | None = None): """ :param can.BusABC bus: A python-can bus instance to re-use. @@ -110,7 +110,8 @@ def connect(self, *args, **kwargs) -> "Network": if node.object_dictionary.bitrate: kwargs["bitrate"] = node.object_dictionary.bitrate break - self.bus = can.interface.Bus(*args, **kwargs) + if self.bus is None: + self.bus = can.Bus(*args, **kwargs) logger.info("Connected to '%s'", self.bus.channel_info) self.notifier = can.Notifier(self.bus, self.listeners, 1) return self