Skip to content

Commit

Permalink
Implement the remaining canopen datatypes (christiansandberg#440)
Browse files Browse the repository at this point in the history
* Implement the remaining canopen datatypes
* Add datatype defs for the canopen standard types
* Move datatypes_24.py classes to datatypes.py
* Replace Unsigned24 and Interger24 by generic UnsignedN and IntegerN respectively
* Add EDS-file containing all datatypes
* Added tests for encoding and decoding all datatypes
* Added tests for SDO uploads of all datatypes
* Annotate type hint for STRUCT_TYPES listing.
* Disable failing tests waiting on a fix for christiansandberg#436.

Co-authored-by: André Colomb <[email protected]>
  • Loading branch information
sveinse and acolomb authored Jun 11, 2024
1 parent 599f4ac commit 9df972c
Show file tree
Hide file tree
Showing 6 changed files with 854 additions and 43 deletions.
27 changes: 19 additions & 8 deletions canopen/objectdictionary/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import logging

from canopen.objectdictionary.datatypes import *
from canopen.objectdictionary.datatypes_24bit import Integer24, Unsigned24
from canopen.utils import pretty_index

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -282,17 +281,25 @@ def add_member(self, variable: ODVariable) -> None:
class ODVariable:
"""Simple variable."""

STRUCT_TYPES = {
STRUCT_TYPES: dict[int, struct.Struct] = {
# Use struct module to pack/unpack data where possible and use the
# custom IntegerN and UnsignedN classes for the special data types.
BOOLEAN: struct.Struct("?"),
INTEGER8: struct.Struct("b"),
INTEGER16: struct.Struct("<h"),
INTEGER24: Integer24(),
INTEGER24: IntegerN(24),
INTEGER32: struct.Struct("<l"),
INTEGER40: IntegerN(40),
INTEGER48: IntegerN(48),
INTEGER56: IntegerN(56),
INTEGER64: struct.Struct("<q"),
UNSIGNED8: struct.Struct("B"),
UNSIGNED16: struct.Struct("<H"),
UNSIGNED24: Unsigned24(),
UNSIGNED24: UnsignedN(24),
UNSIGNED32: struct.Struct("<L"),
UNSIGNED40: UnsignedN(40),
UNSIGNED48: UnsignedN(48),
UNSIGNED56: UnsignedN(56),
UNSIGNED64: struct.Struct("<Q"),
REAL32: struct.Struct("<f"),
REAL64: struct.Struct("<d")
Expand Down Expand Up @@ -386,10 +393,13 @@ def add_bit_definition(self, name: str, bits: List[int]) -> None:

def decode_raw(self, data: bytes) -> Union[int, float, str, bytes, bytearray]:
if self.data_type == VISIBLE_STRING:
return data.rstrip(b"\x00").decode("ascii", errors="ignore")
# Strip any trailing NUL characters from C-based systems
return data.decode("ascii", errors="ignore").rstrip("\x00")
elif self.data_type == UNICODE_STRING:
# Is this correct?
return data.rstrip(b"\x00").decode("utf_16_le", errors="ignore")
# The CANopen standard does not specify the encoding. This
# library assumes UTF-16, being the most common two-byte encoding format.
# Strip any trailing NUL characters from C-based systems
return data.decode("utf_16_le", errors="ignore").rstrip("\x00")
elif self.data_type in self.STRUCT_TYPES:
try:
value, = self.STRUCT_TYPES[self.data_type].unpack(data)
Expand All @@ -407,8 +417,9 @@ def encode_raw(self, value: Union[int, float, str, bytes, bytearray]) -> bytes:
elif self.data_type == VISIBLE_STRING:
return value.encode("ascii")
elif self.data_type == UNICODE_STRING:
# Is this correct?
return value.encode("utf_16_le")
elif self.data_type in (DOMAIN, OCTET_STRING):
return bytes(value)
elif self.data_type in self.STRUCT_TYPES:
if self.data_type in INTEGER_TYPES:
value = int(value)
Expand Down
105 changes: 103 additions & 2 deletions canopen/objectdictionary/datatypes.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import struct

BOOLEAN = 0x1
INTEGER8 = 0x2
Expand All @@ -10,16 +11,116 @@
VISIBLE_STRING = 0x9
OCTET_STRING = 0xA
UNICODE_STRING = 0xB
TIME_OF_DAY = 0xC
TIME_DIFFERENCE = 0xD
DOMAIN = 0xF
INTEGER24 = 0x10
REAL64 = 0x11
INTEGER40 = 0x12
INTEGER48 = 0x13
INTEGER56 = 0x14
INTEGER64 = 0x15
UNSIGNED24 = 0x16
UNSIGNED40 = 0x18
UNSIGNED48 = 0x19
UNSIGNED56 = 0x1A
UNSIGNED64 = 0x1B
PDO_COMMUNICATION_PARAMETER = 0x20
PDO_MAPPING = 0x21
SDO_PARAMETER = 0x22
IDENTITY = 0x23

SIGNED_TYPES = (INTEGER8, INTEGER16, INTEGER24, INTEGER32, INTEGER64)
UNSIGNED_TYPES = (UNSIGNED8, UNSIGNED16, UNSIGNED24, UNSIGNED32, UNSIGNED64)
SIGNED_TYPES = (
INTEGER8,
INTEGER16,
INTEGER24,
INTEGER32,
INTEGER40,
INTEGER48,
INTEGER56,
INTEGER64,
)
UNSIGNED_TYPES = (
UNSIGNED8,
UNSIGNED16,
UNSIGNED24,
UNSIGNED32,
UNSIGNED40,
UNSIGNED48,
UNSIGNED56,
UNSIGNED64,
)
INTEGER_TYPES = SIGNED_TYPES + UNSIGNED_TYPES
FLOAT_TYPES = (REAL32, REAL64)
NUMBER_TYPES = INTEGER_TYPES + FLOAT_TYPES
DATA_TYPES = (VISIBLE_STRING, OCTET_STRING, UNICODE_STRING, DOMAIN)


class UnsignedN(struct.Struct):
"""Packing and unpacking unsigned integers of arbitrary width, like struct.Struct.
The width must be a multiple of 8 and must be between 8 and 64.
"""

def __init__(self, width: int):
self.width = width
if width % 8 != 0:
raise ValueError("Width must be a multiple of 8")
if width <= 0 or width > 64:
raise ValueError("Invalid width for UnsignedN")
elif width <= 8:
fmt = "B"
elif width <= 16:
fmt = "<H"
elif width <= 32:
fmt = "<L"
else:
fmt = "<Q"
super().__init__(fmt)

def unpack(self, buffer):
return super().unpack(buffer + b'\x00' * (super().size - self.size))

def pack(self, *v):
return super().pack(*v)[:self.size]

@property
def size(self) -> int:
return self.width // 8


class IntegerN(struct.Struct):
"""Packing and unpacking integers of arbitrary width, like struct.Struct.
The width must be a multiple of 8 and must be between 8 and 64.
"""

def __init__(self, width: int):
self.width = width
if width % 8 != 0:
raise ValueError("Width must be a multiple of 8")
if width <= 0 or width > 64:
raise ValueError("Invalid width for IntegerN")
elif width <= 8:
fmt = "b"
elif width <= 16:
fmt = "<h"
elif width <= 32:
fmt = "<l"
else:
fmt = "<q"
super().__init__(fmt)

def unpack(self, buffer):
mask = 0x80
neg = (buffer[self.size - 1] & mask) > 0
return super().unpack(
buffer + (b'\xff' if neg else b'\x00') * (super().size - self.size)
)

def pack(self, *v):
return super().pack(*v)[:self.size]

@property
def size(self) -> int:
return self.width // 8
33 changes: 0 additions & 33 deletions canopen/objectdictionary/datatypes_24bit.py

This file was deleted.

Loading

0 comments on commit 9df972c

Please sign in to comment.