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