diff --git a/rise_motion_dev_ws/src/py_sdo_serializer/package.xml b/rise_motion_dev_ws/src/py_sdo_serializer/package.xml new file mode 100644 index 0000000..04bed26 --- /dev/null +++ b/rise_motion_dev_ws/src/py_sdo_serializer/package.xml @@ -0,0 +1,18 @@ + + + + py_sdo_serializer + 0.0.0 + Offers functions to serialize and deserialize EtherCAT base data types. + a + TODO: License declaration + + ament_copyright + ament_flake8 + ament_pep257 + python3-pytest + + + ament_python + + diff --git a/rise_motion_dev_ws/src/py_sdo_serializer/py_sdo_serializer/__init__.py b/rise_motion_dev_ws/src/py_sdo_serializer/py_sdo_serializer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rise_motion_dev_ws/src/py_sdo_serializer/py_sdo_serializer/sdo_serializer.py b/rise_motion_dev_ws/src/py_sdo_serializer/py_sdo_serializer/sdo_serializer.py new file mode 100644 index 0000000..a9d00c5 --- /dev/null +++ b/rise_motion_dev_ws/src/py_sdo_serializer/py_sdo_serializer/sdo_serializer.py @@ -0,0 +1,404 @@ +"""Functions to serialize/deserialize a value according to EtherCAT data type. + +The data type can be identified by its index, name, or base data type +string. Providing one is sufficient. + +For definitions of supported base data types and encoding see see ETG.1000.6 +and ETG.1020 at https://www.ethercat.org/en/downloads.html. +""" +import struct +from dataclasses import dataclass + +# --------------------------------------------------------------------------- +# Record type +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class TypeInfo: + """Stores attributes of EtherCAT base data types from ETG.1020. + + For definitions of supported base data types and encoding + see see ETG.1000.6, ETG.1020 at https://www.ethercat.org/en/downloads.html. + """ + + index: int # EtherCAT object-dictionary type index + name: str # ETG long name (e.g. 'INTEGER16') + base_data_type: str # IEC / short name (e.g. 'INT') + bit_size: int # Canonical bit width + serialize_fn: str # Name of the serialization subfunction to call + deserialize_fn: str # Name of the deserialization subfunction to call + + +# --------------------------------------------------------------------------- +# Master table +# --------------------------------------------------------------------------- + +BASE_DATA_TYPES: dict[int, TypeInfo] = { + + # Boolean / generic word types + 0x0001: TypeInfo(0x0001, 'BOOLEAN', 'BOOL', 1, 'serialize_bool', 'deserialize_bool'), + 0x001E: TypeInfo(0x001E, 'BYTE', 'BYTE', 8, 'serialize_bitn', 'deserialize_bitn'), + 0x001F: TypeInfo(0x001F, 'WORD', 'WORD', 16, 'serialize_bitn', 'deserialize_bitn'), + 0x0020: TypeInfo(0x0020, 'DWORD', 'DWORD', 32, 'serialize_bitn', 'deserialize_bitn'), + + # Time types (48-bit, special structure) + 0x000C: TypeInfo(0x000C, 'TIME_OF_DAY', 'TIME_OF_DAY', 48, 'serialize_time_of_day', 'deserialize_time48'), + 0x000D: TypeInfo(0x000D, 'TIME_DIFFERENCE', 'TIME_DIFFERENCE', 48, 'serialize_time_difference', 'deserialize_time48'), + + # Bit strings BIT1 - BIT16 + 0x0030: TypeInfo(0x0030, 'BIT1', 'BIT1', 1, 'serialize_bitn', 'deserialize_bitn'), + 0x0031: TypeInfo(0x0031, 'BIT2', 'BIT2', 2, 'serialize_bitn', 'deserialize_bitn'), + 0x0032: TypeInfo(0x0032, 'BIT3', 'BIT3', 3, 'serialize_bitn', 'deserialize_bitn'), + 0x0033: TypeInfo(0x0033, 'BIT4', 'BIT4', 4, 'serialize_bitn', 'deserialize_bitn'), + 0x0034: TypeInfo(0x0034, 'BIT5', 'BIT5', 5, 'serialize_bitn', 'deserialize_bitn'), + 0x0035: TypeInfo(0x0035, 'BIT6', 'BIT6', 6, 'serialize_bitn', 'deserialize_bitn'), + 0x0036: TypeInfo(0x0036, 'BIT7', 'BIT7', 7, 'serialize_bitn', 'deserialize_bitn'), + 0x0037: TypeInfo(0x0037, 'BIT8', 'BIT8', 8, 'serialize_bitn', 'deserialize_bitn'), + 0x0038: TypeInfo(0x0038, 'BIT9', 'BIT9', 9, 'serialize_bitn', 'deserialize_bitn'), + 0x0039: TypeInfo(0x0039, 'BIT10', 'BIT10', 10, 'serialize_bitn', 'deserialize_bitn'), + 0x003A: TypeInfo(0x003A, 'BIT11', 'BIT11', 11, 'serialize_bitn', 'deserialize_bitn'), + 0x003B: TypeInfo(0x003B, 'BIT12', 'BIT12', 12, 'serialize_bitn', 'deserialize_bitn'), + 0x003C: TypeInfo(0x003C, 'BIT13', 'BIT13', 13, 'serialize_bitn', 'deserialize_bitn'), + 0x003D: TypeInfo(0x003D, 'BIT14', 'BIT14', 14, 'serialize_bitn', 'deserialize_bitn'), + 0x003E: TypeInfo(0x003E, 'BIT15', 'BIT15', 15, 'serialize_bitn', 'deserialize_bitn'), + 0x003F: TypeInfo(0x003F, 'BIT16', 'BIT16', 16, 'serialize_bitn', 'deserialize_bitn'), + + # Bit arrays + 0x002D: TypeInfo(0x002D, 'BITARR8', 'BITARR8', 8, 'serialize_bitn', 'deserialize_bitn'), + 0x002E: TypeInfo(0x002E, 'BITARR16', 'BITARR16', 16, 'serialize_bitn', 'deserialize_bitn'), + 0x002F: TypeInfo(0x002F, 'BITARR32', 'BITARR32', 32, 'serialize_bitn', 'deserialize_bitn'), + + # Signed integers + 0x0002: TypeInfo(0x0002, 'INTEGER8', 'SINT', 8, 'serialize_int', 'deserialize_int'), + 0x0003: TypeInfo(0x0003, 'INTEGER16', 'INT', 16, 'serialize_int', 'deserialize_int'), + 0x0010: TypeInfo(0x0010, 'INTEGER24', 'INT24', 24, 'serialize_int', 'deserialize_int'), + 0x0004: TypeInfo(0x0004, 'INTEGER32', 'DINT', 32, 'serialize_int', 'deserialize_int'), + 0x0012: TypeInfo(0x0012, 'INTEGER40', 'INT40', 40, 'serialize_int', 'deserialize_int'), + 0x0013: TypeInfo(0x0013, 'INTEGER48', 'INT48', 48, 'serialize_int', 'deserialize_int'), + 0x0014: TypeInfo(0x0014, 'INTEGER56', 'INT56', 56, 'serialize_int', 'deserialize_int'), + 0x0015: TypeInfo(0x0015, 'INTEGER64', 'LINT', 64, 'serialize_int', 'deserialize_int'), + + # Unsigned integers + 0x0005: TypeInfo(0x0005, 'UNSIGNED8', 'USINT', 8, 'serialize_uint', 'deserialize_uint'), + 0x0006: TypeInfo(0x0006, 'UNSIGNED16', 'UINT', 16, 'serialize_uint', 'deserialize_uint'), + 0x0016: TypeInfo(0x0016, 'UNSIGNED24', 'UINT24', 24, 'serialize_uint', 'deserialize_uint'), + 0x0007: TypeInfo(0x0007, 'UNSIGNED32', 'UDINT', 32, 'serialize_uint', 'deserialize_uint'), + 0x0018: TypeInfo(0x0018, 'UNSIGNED40', 'UINT40', 40, 'serialize_uint', 'deserialize_uint'), + 0x0019: TypeInfo(0x0019, 'UNSIGNED48', 'UINT48', 48, 'serialize_uint', 'deserialize_uint'), + 0x001A: TypeInfo(0x001A, 'UNSIGNED56', 'UINT56', 56, 'serialize_uint', 'deserialize_uint'), + 0x001B: TypeInfo(0x001B, 'UNSIGNED64', 'ULINT', 64, 'serialize_uint', 'deserialize_uint'), + + # Floating point + 0x0008: TypeInfo(0x0008, 'REAL32', 'REAL', 32, 'serialize_float', 'deserialize_float'), + 0x0011: TypeInfo(0x0011, 'REAL64', 'LREAL', 64, 'serialize_float', 'deserialize_float'), + + # GUID (according to specifications value is stored as a 128-bit integer) + 0x001D: TypeInfo(0x001D, 'GUID', 'GUID', 128, 'serialize_int', 'deserialize_int'), + + # - Base Data Types with variable length - + # Strings + 0x0009: TypeInfo(0x0009, 'VISIBLE_STRING', 'STRING(n)', 8, 'serialize_visible_string', 'deserialize_visible_string'), # 8*(n)), + 0x0268: TypeInfo(0x0268, 'UNICODE_STRING', 'WSTRING(n)', 16, 'serialize_unicode_string', 'deserialize_unicode_string'), # 16*(n) + + # Octet field + 0x000A: TypeInfo(0x000A, 'OCTET_STRING', 'ARRAY [0..n] OF BYTE', 8, 'serialize_bitn', 'deserialize_bitn'),#8*(n+1) + 0x000B: TypeInfo(0x000B, 'ARRAY_OF_UINT', 'ARRAY [0..n] OF UINT', 16, 'serialize_uint', 'deserialize_uint'),#16*(n+1) + 0x0260: TypeInfo(0x0260, 'ARRAY_OF_INT', 'ARRAY [0..n] OF INT', 16, 'serialize_int', 'deserialize_int'),#16*(n+1) + 0x0261: TypeInfo(0x0261, 'ARRAY_OF_SINT', 'ARRAY [0..n] OF SINT', 8, 'serialize_int', 'deserialize_int'),#8*(n+1) + 0x0262: TypeInfo(0x0262, 'ARRAY_OF_DINT', 'ARRAY [0..n] OF DINT', 32, 'serialize_int', 'deserialize_int'),#32*(n+1) + 0x0263: TypeInfo(0x0263, 'ARRAY_OF_UDINT', 'ARRAY [0..n] OF UDINT', 32, 'serialize_uint', 'deserialize_uint'),#32*(n+1) + 0x0264: TypeInfo(0x0264, 'ARRAY_OF_BITARR8', 'ARRAY [0..n] OF BITARR8', 8, 'serialize_bitn', 'deserialize_bitn'),#8*(n+1) + 0x0265: TypeInfo(0x0265, 'ARRAY_OF_BITARR16', 'ARRAY [0..n] OF BITARR16', 16, 'serialize_bitn', 'deserialize_bitn'),#16*(n+1) + 0x0266: TypeInfo(0x0266, 'ARRAY_OF_BITARR32', 'ARRAY [0..n] OF BITARR32', 32, 'serialize_bitn', 'deserialize_bitn'),#32*(n+1) + 0x0267: TypeInfo(0x0267, 'ARRAY_OF_USINT', 'ARRAY [0..n] OF USINT', 8, 'serialize_uint', 'deserialize_uint'),#8*(n+1) + 0x0269: TypeInfo(0x0269, 'ARRAY_OF_REAL', 'ARRAY [0..n] OF REAL', 32, 'serialize_float', 'deserialize_float'),#32*(n+1) + 0x026A: TypeInfo(0x026A, 'ARRAY_OF_LREAL', 'ARRAY [0..n] OF LREAL', 64, 'serialize_float', 'deserialize_float'),#64*(n+1) +} + + +# --------------------------------------------------------------------------- +# Secondary lookup dicts (built once at import time) +# --------------------------------------------------------------------------- + +BY_NAME: dict[str, TypeInfo] = { + info.name: info for info in BASE_DATA_TYPES.values() +} + +BY_BASE_DATA_TYPE: dict[str, TypeInfo] = { + info.base_data_type: info for info in BASE_DATA_TYPES.values() +} + + +# --------------------------------------------------------------------------- +# Convenience accessor +# --------------------------------------------------------------------------- + +def get_type_info( + *, + index: int | None = None, + name: str | None = None, + base_data_type: str | None = None, +) -> TypeInfo: + """ + Retrieve a TypeInfo record by any one of the three keys. + + Examples + -------- + get_type_info(index=0x0003) + get_type_info(name='INTEGER16') + get_type_info(base_data_type='INT') + """ + if index is not None: + try: + return BASE_DATA_TYPES[index] + except KeyError: + raise KeyError( + f"No EtherCAT type with index {index:#04x}" + ) from None + + if name is not None: + try: + return BY_NAME[name] + except KeyError: + raise KeyError( + f"No EtherCAT type with name '{name}'" + ) from None + + if base_data_type is not None: + try: + return BY_BASE_DATA_TYPE[base_data_type] + except KeyError: + raise KeyError( + f"No EtherCAT type with base_data_type '{base_data_type}'" + ) from None + + raise ValueError('Provide at least one of: index, name, base_data_type') + + +# --------------------------------------------------------------------------- +# Generic functions +# --------------------------------------------------------------------------- + +def serialize( + value, + *, + index: int | None = None, + name: str | None = None, + base_data_type: str | None = None, +) -> list[int]: + """Serialize a value according to an EtherCAT base data type. + + The data type can be identified by its index, name, or base data type + string. Providing one is sufficient. + + For definitions of supported base data types and encoding + see see ETG.1000.6 and ETG.1020 at https://www.ethercat.org/en/downloads.html + + :param value: + Value to serialize. + :param index: + EtherCAT data type index. + :param name: + EtherCAT data type name. + :param base_data_type: + Data type making up this base data type. Called 'Type' in + Somanet Circulo Object Dictionary reference. + :returns: + Serialized value as a list of bytes. + :rtype: + list[int] + """ + try: + object_info = get_type_info(index=index, name=name, base_data_type=base_data_type) + + # serialize a base data type + if object_info.base_data_type[0:5] != 'ARRAY': + return globals()[object_info.serialize_fn]( + value, object_info.bit_size) + # serialize a base data type list (imagine this: base_data_type[]) + else: + out = [] + for item in value: + out.extend(globals()[object_info.serialize_fn]( + item, object_info.bit_size)) + return out + except: + return -1 + +def deserialize( + serialized_value: list[int], + *, + index: int | None = None, + name: str | None = None, + base_data_type: str | None = None, +): + """Deserializes a list of bytes representing an EtherCAT base data type. + + The data type can be identified by its index, name, or base data type + string. Providing one is sufficient. + + For definitions of supported base data types and encoding + see ETG.1000.6 and ETG.1020 at https://www.ethercat.org/en/downloads.html + + :param serialized_value: + Bytes to deserialize. + :param index: + EtherCAT data type index. + :param name: + EtherCAT data type name. + :param base_data_type: + Data type making up this base data type. Called 'Type' in + Somanet Circulo Object Dictionary reference. + :returns: + Deserialized value as a fitting python data type. + """ + try: + if not isinstance(serialized_value, list) or not all(isinstance(b, int) for b in serialized_value): + raise TypeError(f"serialized_value must be a list[int] not {type(serialized_value)} {serialized_value}") + object_info = get_type_info(index=index, name=name, base_data_type=base_data_type) + byte_len = (object_info.bit_size + 7) // 8 + isArray = object_info.base_data_type[0:5] == 'ARRAY' + + # check if size of variably sized serialized object is plausible + if object_info.name[-6:] == 'STRING' or isArray: + if len(serialized_value)%byte_len != 0: + raise ValueError( + f"Number of bytes needed to contain variably sized list of Base Data Types must be" + 'evenly divisible by the byte-size of those Base Data Types.') + # check if size of serialized object is correct + else: + if byte_len != len(serialized_value): + raise ValueError( + f"Number of bytes needed to contain object_info.bit_size must be equal to serialized_value length.") + + # deserialize a serialized base data type + if not isArray: + return globals()[object_info.deserialize_fn](serialized_value, object_info.bit_size) + # deserialize a serialized base data type array (imagine this: base_data_type[]) + else: + out = [] + for i in range(0, len(serialized_value), byte_len): + out.append(globals()[object_info.deserialize_fn](serialized_value[i:i+byte_len], object_info.bit_size)) + return out + except: + return -1 + +# --------------------------------------------------------------------------- +# Specific functions (called by generic functions) +# --------------------------------------------------------------------------- + + +def serialize_bitn(val, bit_s: int) -> list[int]: + """Serialize a bit-string into a list[int].""" + if isinstance(val, str): + val = int(val, 2) + elif not isinstance(val, int): + raise TypeError(f"Input value must be either int or str, not {type(val)} {val}.") + + byte_len = (bit_s + 7) // 8 + return list(val.to_bytes(byte_len, byteorder='little')) + + +def deserialize_bitn(ser_val: list[int], bit_s: int) -> str: + """Deserialize a list[int] into a bit-string.""" + value = int.from_bytes(bytes(ser_val), byteorder='little') + return bin(value)[2:].zfill(bit_s) + + +def serialize_int(val: int, bit_s: int) -> list[int]: + """Serialize a signed int into a list[int].""" + if not isinstance(val, int): + raise TypeError(f"Input value must be an int, not {type(val)} {val}.") + byte_len = (bit_s + 7) // 8 + return list(val.to_bytes(byte_len, byteorder='little', signed=True)) + + +def deserialize_int(ser_val: list[int], bit_s: int) -> int: + """Deserialize a list[int] into a signed int.""" + return int.from_bytes(bytes(ser_val), byteorder='little', signed=True) + + +def serialize_uint(val: int, bit_s: int) -> list[int]: + """Serialize an unsigned int into a list[int].""" + if not isinstance(val, int): + raise TypeError(f"Input value must be an int, not {type(val)} {val}.") + byte_len = (bit_s + 7) // 8 + return list(val.to_bytes(byte_len, byteorder='little')) + + +def deserialize_uint(ser_val: list[int], bit_s: int) -> int: + """Deserialize a list[int] into an unsigned int.""" + return int.from_bytes(bytes(ser_val), byteorder='little') + + +def serialize_bool(val, bit_s: int) -> list[int]: + """Serialize a bool into a list[int].""" + if (not isinstance(val, bool)) and (val not in [1, 0]): + raise TypeError(f"Input value must be a bool or the int 1 or 0, not {type(val)} {val}.") + if val: + return serialize_bitn(0xff, bit_s) + else: + return serialize_bitn(0x00, bit_s) + + +def deserialize_bool(ser_val: list[int], bit_s: int) -> bool: + """Deserialize a list[int] into a bool.""" + return 0 != ser_val[0] + + +def serialize_float(val, bit_s: int) -> list[int]: + """Serialize a float into a list[int].""" + if not isinstance(val, float): + raise TypeError(f"Input value must be a float, not {type(val)} {val}.") + return list(struct.pack(f"<{'f' if bit_s == 32 else 'd'}", val)) + + +def deserialize_float(ser_val: list[int], bit_s: int) -> float: + """Deserialize a list[int] into a float.""" + return struct.unpack(f"<{'f' if bit_s == 32 else 'd'}", bytes(ser_val))[0] + + +def serialize_time_of_day(val, bit_s: int) -> list[int]: + """Serialize tuple of ints (ms since midnight, days since 01.01.1984).""" + if (len(val) != 2) or (not isinstance(val[0], int)) or (not isinstance(val[1], int)): + raise TypeError(f"There needs to be a ms and a day value. They must be ints in a tuple, not {type(val)} {val}") + if val[0] > (1<<28)-1: + raise OverflowError('the 4 most significant bits of number of ms since midnight need to be 0') + return list(val[0].to_bytes(4, byteorder='big')) + list(val[1].to_bytes(2, byteorder='big')) + + +def serialize_time_difference(val, bit_s: int) -> list[int]: + """Serialize tuple of ints (ms, days).""" + if (len(val) != 2) or (not isinstance(val[0], int)) or (not isinstance(val[1], int)): + raise TypeError(f"There needs to be a ms and a day value. They must be ints in a tuple, not {type(val)} {val}") + return list(val[0].to_bytes(4, byteorder='big')) + list(val[1].to_bytes(2, byteorder='big')) + + +def deserialize_time48(ser_val: list[int], bit_s: int) -> tuple[int]: + """Deserialize list[int] to tuple of ints (ms, days).""" + return (int.from_bytes(ser_val[:4], byteorder='big'), int.from_bytes(ser_val[4:], byteorder='big')) + + +def serialize_visible_string(val: str, bit_s: int) -> list[int]: + """Serialize an UTF-8 encoded string into a list[int].""" + if not isinstance(val, str): + raise TypeError(f"Input value must be str, not {type(val)} {val}") + return list(val.encode('UTF-8')) + + +def deserialize_visible_string(ser_val: list[int], bit_s: int) -> str: + """Deserialize a list[int] into an UTF-8 encoded string.""" + return bytes(ser_val).decode('UTF-8') + + +def serialize_unicode_string(val: str, bit_s: int) -> list[int]: + """Serialize a utf_16_le encoded string into a list[int].""" + if not isinstance(val, str): + raise TypeError(f"Input value must be str, not {type(val)} {val}") + return list(val.encode('utf_16_le')) + + +def deserialize_unicode_string(ser_val: list[int], bit_s: int) -> str: + """Deerialize a list[int] into a utf_16_le encoded string.""" + return bytes(ser_val).decode('utf_16_le') diff --git a/rise_motion_dev_ws/src/py_sdo_serializer/py_sdo_serializer/temp_test_node.py b/rise_motion_dev_ws/src/py_sdo_serializer/py_sdo_serializer/temp_test_node.py new file mode 100644 index 0000000..d80bbbe --- /dev/null +++ b/rise_motion_dev_ws/src/py_sdo_serializer/py_sdo_serializer/temp_test_node.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 + +# execute this using +# python3 path/to/this/file + +import sys +import rclpy +from rclpy.node import Node +from rise_motion_messages.msg import MotorPositions +from rise_motion_messages.srv import EnableEthercatSrv, SDOReadSrv +from rise_motion_dev_ws.src.py_sdo_serialization.sdo_serializer import deserialize, serialize + + +class TestNode(Node): + def __init__(self, increment: int): + super().__init__("python_sdo_test_node") + self.increment = increment + self.valid_positions = False + self.motor_pos: list[int] = [] + + self.get_logger().info("Starting TestNode") + + self.input_sub = self.create_subscription( + MotorPositions, + "motor_feedback", + self._feedback_cb, + 10, + ) + + self.output_pub = self.create_publisher(MotorPositions, "motor_commands", 10) + + self.publish_timer = self.create_timer(0.001, self._publish_cb) # 1 ms + + self.enable_client = self.create_client(EnableEthercatSrv, "enable_ethercat") + + def __del__(self): + self.get_logger().info("Bye :)") + + # --------------------------------------------------------------------------- + # Callbacks + # --------------------------------------------------------------------------- + + def _feedback_cb(self, msg: MotorPositions): + if not self.valid_positions: + self.get_logger().info("Got feedback") + self.valid_positions = True + self.motor_pos = list(msg.positions) + + def _publish_cb(self): + if not self.valid_positions: + return + response = MotorPositions() + response.positions = [p + self.increment for p in self.motor_pos] + self.output_pub.publish(response) + + # --------------------------------------------------------------------------- + # Service calls + # --------------------------------------------------------------------------- + + def request_enable_ethercat(self) -> bool: + """Returns True on success (mirrors the C++ int == 0 success check).""" + self.get_logger().info(f"Incrementing motor position with {self.increment}") + self.get_logger().info("Requesting Enable Ethercat") + + while not self.enable_client.wait_for_service(timeout_sec=1.0): + if not rclpy.ok(): + self.get_logger().error("Interrupted while waiting for enable_ethercat service.") + return False + self.get_logger().info("Waiting for enable_ethercat service...") + + request = EnableEthercatSrv.Request() + request.enable = True + + future = self.enable_client.call_async(request) + rclpy.spin_until_future_complete(self, future) + + if future.result() is None: + self.get_logger().error("enable_ethercat service call failed.") + return False + + self.get_logger().info("Done requesting") + return bool(future.result().status_enable) + + def sdo_read( + self, + device_id: int, + index: int, + subindex: int, + value_type: int = 0, + *, + type_name: str | None = None, + base_data_type: str | None = None, + ): + """ + Call the sdo_read service and deserialize the result. + + Returns the deserialized value, or None on error. + """ + client = self.create_client(SDOReadSrv, "sdo_read") + + while not client.wait_for_service(timeout_sec=1.0): + if not rclpy.ok(): + self.get_logger().error("Interrupted while waiting for sdo_read service.") + return None + self.get_logger().info("Waiting for sdo_read service...") + + request = SDOReadSrv.Request() + request.device_id = device_id + request.index = index + request.subindex = subindex + request.value_type = value_type + + future = client.call_async(request) + rclpy.spin_until_future_complete(self, future) + + if future.result() is None: + self.get_logger().error("sdo_read service call failed.") + return None + + raw: list[int] = list(future.result().value) + result = deserialize(raw, name=type_name, base_data_type=base_data_type) + + if result == -1: + self.get_logger().error("sdo deserialization failed for index 0x%04X sub %d", index, subindex) + return None + + return result + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + +def main(): + increment = int(sys.argv[1]) if len(sys.argv) >= 2 else 10 + + rclpy.init() + node = TestNode(increment) + + # Keep calling until EtherCAT is enabled + while not node.request_enable_ethercat(): + pass + + for p in [ + [1,0x1008,0,"VISIBLE_STRING"], [1,0x1000,0,"UDINT"], + [1,0x1005,0,"DINT"], [1,0x1018,1,"UDINT"]]: + # Read device name (object 0x1008, sub 0) as a VISIBLE_STRING + result = node.sdo_read( + device_id=p[0], + index=p[1], + subindex=p[2], + type_name=p[3], + ) + + if result == -1: + rclpy.get_logger("rclcpp").error(f"SDO read {p[1]} {p[3]} failed") + else: + node.get_logger().info(f"sdo read value {p[1]} {p[3]}: %s", str(result)) + + rclpy.spin(node) + rclpy.shutdown() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/rise_motion_dev_ws/src/py_sdo_serializer/resource/py_sdo_serializer b/rise_motion_dev_ws/src/py_sdo_serializer/resource/py_sdo_serializer new file mode 100644 index 0000000..e69de29 diff --git a/rise_motion_dev_ws/src/py_sdo_serializer/setup.cfg b/rise_motion_dev_ws/src/py_sdo_serializer/setup.cfg new file mode 100644 index 0000000..aa20d50 --- /dev/null +++ b/rise_motion_dev_ws/src/py_sdo_serializer/setup.cfg @@ -0,0 +1,4 @@ +[develop] +script_dir=$base/lib/py_sdo_serializer +[install] +install_scripts=$base/lib/py_sdo_serializer diff --git a/rise_motion_dev_ws/src/py_sdo_serializer/setup.py b/rise_motion_dev_ws/src/py_sdo_serializer/setup.py new file mode 100644 index 0000000..f568759 --- /dev/null +++ b/rise_motion_dev_ws/src/py_sdo_serializer/setup.py @@ -0,0 +1,30 @@ +from setuptools import find_packages, setup + +package_name = 'py_sdo_serializer' + +setup( + name=package_name, + version='0.0.0', + packages=find_packages(exclude=['test']), + data_files=[ + ('share/ament_index/resource_index/packages', + ['resource/' + package_name]), + ('share/' + package_name, ['package.xml']), + ], + install_requires=['setuptools'], + zip_safe=True, + maintainer='a', + maintainer_email='anton.ohler@gmail.com', + description='Offers functions to serialize and deserialize EtherCAT base data types.', + license='TODO: License declaration', + extras_require={ + 'test': [ + 'pytest', + ], + }, + entry_points={ + 'console_scripts': [ + ], + }, + tests_require=['pytest'], +) diff --git a/rise_motion_dev_ws/src/py_sdo_serializer/test/test_copyright.py b/rise_motion_dev_ws/src/py_sdo_serializer/test/test_copyright.py new file mode 100644 index 0000000..97a3919 --- /dev/null +++ b/rise_motion_dev_ws/src/py_sdo_serializer/test/test_copyright.py @@ -0,0 +1,25 @@ +# Copyright 2015 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ament_copyright.main import main +import pytest + + +# Remove the `skip` decorator once the source file(s) have a copyright header +@pytest.mark.skip(reason='No copyright header has been placed in the generated source file.') +@pytest.mark.copyright +@pytest.mark.linter +def test_copyright(): + rc = main(argv=['.', 'test']) + assert rc == 0, 'Found errors' diff --git a/rise_motion_dev_ws/src/py_sdo_serializer/test/test_flake8.py b/rise_motion_dev_ws/src/py_sdo_serializer/test/test_flake8.py new file mode 100644 index 0000000..27ee107 --- /dev/null +++ b/rise_motion_dev_ws/src/py_sdo_serializer/test/test_flake8.py @@ -0,0 +1,25 @@ +# Copyright 2017 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ament_flake8.main import main_with_errors +import pytest + + +@pytest.mark.flake8 +@pytest.mark.linter +def test_flake8(): + rc, errors = main_with_errors(argv=[]) + assert rc == 0, \ + 'Found %d code style errors / warnings:\n' % len(errors) + \ + '\n'.join(errors) diff --git a/rise_motion_dev_ws/src/py_sdo_serializer/test/test_pep257.py b/rise_motion_dev_ws/src/py_sdo_serializer/test/test_pep257.py new file mode 100644 index 0000000..b234a38 --- /dev/null +++ b/rise_motion_dev_ws/src/py_sdo_serializer/test/test_pep257.py @@ -0,0 +1,23 @@ +# Copyright 2015 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ament_pep257.main import main +import pytest + + +@pytest.mark.linter +@pytest.mark.pep257 +def test_pep257(): + rc = main(argv=['.', 'test']) + assert rc == 0, 'Found code style errors / warnings' diff --git a/rise_motion_dev_ws/src/py_sdo_serializer/test/test_sdo_serializer.py b/rise_motion_dev_ws/src/py_sdo_serializer/test/test_sdo_serializer.py new file mode 100644 index 0000000..061f8aa --- /dev/null +++ b/rise_motion_dev_ws/src/py_sdo_serializer/test/test_sdo_serializer.py @@ -0,0 +1,900 @@ +"""Tests for sdo_serializer.py module.""" +import pytest +import random +from math import isnan +from py_sdo_serializer.sdo_serializer import serialize, deserialize + +# --------------------------------------------------------------------------- +# Bit strings BIT1 - BIT16 tests +# --------------------------------------------------------------------------- + +# --- round-trip tests --- + +@pytest.mark.parametrize("bit_width", range(1, 17)) +def test_roundtrip_random_bitstrings(bit_width: int): + """Serializing then deserializing a random valid value returns the original.""" + max_val = (1 << bit_width) - 1 + value = format(random.randint(0, max_val), f"0{bit_width}b") + dtype = f"BIT{bit_width}" + assert value == deserialize(serialize(value, base_data_type=dtype), base_data_type=dtype) + + +@pytest.mark.parametrize("bit_width", range(1, 17)) +def test_roundtrip_min_bitstrings(bit_width: int): + """Zero survives a round-trip for every bit width.""" + value = "0".zfill(bit_width) + dtype = f"BIT{bit_width}" + assert value == deserialize(serialize(value, base_data_type=dtype), base_data_type=dtype) + + +@pytest.mark.parametrize("bit_width", range(1, 17)) +def test_roundtrip_max_bitstrings(bit_width: int): + """All-ones value survives a round-trip for every bit width.""" + value = "1" * bit_width + dtype = f"BIT{bit_width}" + assert value == deserialize(serialize(value, base_data_type=dtype), base_data_type=dtype) + + +# --- Explicit serialization checks (known input -> known list[int]) --- + +@pytest.mark.parametrize("value, dtype, expected", [ + ("0", "BIT1", [0x00]), + ("1", "BIT1", [0x01]), + ("11", "BIT2", [0x03]), + ("10", "BIT2", [0x02]), + ("1111111111111111", "BIT16", [0xff, 0xff]), + ("0000000000000000", "BIT16", [0x00, 0x00]), + ("1000000000000000", "BIT16", [0x00, 0x80]), # MSB only +]) +def test_serialize_known_values_bitstrings(value: str, dtype: str, expected: list[int]): + assert expected == serialize(value, base_data_type=dtype) + + +# --- Explicit deserialization checks (known list[int] -> known output) --- + +@pytest.mark.parametrize("serialized, dtype, expected_value", [ + ([0x00], "BIT1", "0"), + ([0x01], "BIT1", "1"), + ([0x03], "BIT2", "11"), + ([0x02], "BIT2", "10"), + ([0xff, 0xff], "BIT16", "1111111111111111"), + ([0x00, 0x00], "BIT16", "0000000000000000"), +]) +def test_deserialize_known_values_bitstrings(serialized: list[int], dtype: str, expected_value: str): + assert expected_value == deserialize(serialized, base_data_type=dtype) + + +# --------------------------------------------------------------------------- +# BITARR8, BITARR16, BITARR32, BYTE, WORD, DWORD tests +# --------------------------------------------------------------------------- + +# --- round-trip tests --- + +@pytest.mark.parametrize("bit_width, dtype", [ + (8, "BITARR8"),(16, "BITARR16"),(32, "BITARR32"), + (8, "BYTE"),(16, "WORD"),(32, "DWORD")]) +def test_roundtrip_random_bitarr_word(bit_width: int, dtype: str): + """Serializing then deserializing a random valid value returns the original.""" + max_val = (1 << bit_width) - 1 + value = format(random.randint(0, max_val), f"0{bit_width}b") + assert value == deserialize(serialize(value, base_data_type=dtype), base_data_type=dtype) + + +@pytest.mark.parametrize("bit_width, dtype", [ + (8, "BITARR8"),(16, "BITARR16"),(32, "BITARR32"), + (8, "BYTE"),(16, "WORD"),(32, "DWORD")]) +def test_roundtrip_min_bitarr_word(bit_width: int, dtype: str): + """Zero survives a round-trip for every bit width.""" + value = "0".zfill(bit_width) + assert value == deserialize(serialize(value, base_data_type=dtype), base_data_type=dtype) + + +@pytest.mark.parametrize("bit_width, dtype", [ + (8, "BITARR8"),(16, "BITARR16"),(32, "BITARR32"), + (8, "BYTE"),(16, "WORD"),(32, "DWORD")]) +def test_roundtrip_max_bitarr_word(bit_width: int, dtype: str): + """All-ones value survives a round-trip for every bit width.""" + value = "1" * bit_width + assert value == deserialize(serialize(value, base_data_type=dtype), base_data_type=dtype) + + +# --- Explicit serialization checks (known input -> known list[int]) --- + +@pytest.mark.parametrize("value, dtype, expected", [ + ("10101100", "BITARR8", [0xac]), + ("01010101", "BITARR8", [0x55]), + ("1100101001110001", "BITARR16", [0x71, 0xca]), + ("0011110000101110", "BITARR16", [0x2e, 0x3c]), + ("10010000111100001010101001101101", "BITARR32", [0x6d, 0xaa, 0xf0, 0x90]), + ("01101111000101011000001111110000", "BITARR32", [0xf0, 0x83, 0x15, 0x6f]), + ("10101100", "BYTE", [0xac]), + ("01010101", "BYTE", [0x55]), + ("1100101001110001", "WORD", [0x71, 0xca]), + ("0011110000101110", "WORD", [0x2e, 0x3c]), + ("10010000111100001010101001101101", "DWORD", [0x6d, 0xaa, 0xf0, 0x90]), + ("01101111000101011000001111110000", "DWORD", [0xf0, 0x83, 0x15, 0x6f]), +]) +def test_serialize_known_values_bitarr_word(value: str, dtype: str, expected: list[int]): + assert expected == serialize(value, base_data_type=dtype) + + +# --- Explicit deserialization checks (known list[int] -> known output) --- + +@pytest.mark.parametrize("serialized, dtype, expected_value", [ + ([0xac], "BITARR8", "10101100"), + ([0x55], "BITARR8", "01010101"), + ([0x71, 0xca], "BITARR16", "1100101001110001"), + ([0x2e, 0x3c], "BITARR16", "0011110000101110"), + ([0x6d, 0xaa, 0xf0, 0x90], "BITARR32", "10010000111100001010101001101101"), + ([0xf0, 0x83, 0x15, 0x6f], "BITARR32", "01101111000101011000001111110000"), + ([0xac], "BYTE", "10101100"), + ([0x55], "BYTE", "01010101"), + ([0x71, 0xca], "WORD", "1100101001110001"), + ([0x2e, 0x3c], "WORD", "0011110000101110"), + ([0x6d, 0xaa, 0xf0, 0x90], "DWORD", "10010000111100001010101001101101"), + ([0xf0, 0x83, 0x15, 0x6f], "DWORD", "01101111000101011000001111110000"), +]) +def test_deserialize_known_values_bitarr_word(serialized: list[int], dtype: str, expected_value: str): + assert expected_value == deserialize(serialized, base_data_type=dtype) + + +# --------------------------------------------------------------------------- +# Signed integers 8, 16, 24, 32, 40, 48, 56, 64 tests +# --------------------------------------------------------------------------- + +# --- round-trip tests --- + +@pytest.mark.parametrize("bit_width", [8, 16, 24, 32, 40, 48, 56, 64]) +def test_roundtrip_random_sint(bit_width: int): + """Serializing then deserializing a random valid value returns the original.""" + max_val = 1 << (bit_width-1) + value = random.randint(-max_val, (max_val-1)) + dtype = f"INTEGER{bit_width}" + assert value == deserialize(serialize(value, name=dtype), name=dtype) + + +@pytest.mark.parametrize("bit_width", [8, 16, 24, 32, 40, 48, 56, 64]) +def test_roundtrip_zero_sint(bit_width: int): + """Zero survives a round-trip for every integer size.""" + value = 0 + dtype = f"INTEGER{bit_width}" + assert value == deserialize(serialize(value, name=dtype), name=dtype) + + +@pytest.mark.parametrize("bit_width", [8, 16, 24, 32, 40, 48, 56, 64]) +def test_roundtrip_max_sint(bit_width: int): + """max-value survives a round-trip for every integer size.""" + max_val = 1 << (bit_width-1) + value = max_val-1 + dtype = f"INTEGER{bit_width}" + assert value == deserialize(serialize(value, name=dtype), name=dtype) + + +@pytest.mark.parametrize("bit_width", [8, 16, 24, 32, 40, 48, 56, 64]) +def test_roundtrip_min_sint(bit_width: int): + """min-value survives a round-trip for every integer size.""" + max_val = 1 << (bit_width-1) + value = -max_val + dtype = f"INTEGER{bit_width}" + assert value == deserialize(serialize(value, name=dtype), name=dtype) + + +# --- Explicit serialization checks (known input -> known list[int]) --- + +@pytest.mark.parametrize("value, dtype, expected", [ + (-(1 << 3), "INTEGER8", [0xf8]), + ((1 << 12), "INTEGER16", [0x00, 0x10]), + (-(1 << 22), "INTEGER24", [0x00, 0x00, 0xc0]), + ((1 << 2), "INTEGER32", [0x04, 0x00, 0x00, 0x00]), + (-(1 << 20), "INTEGER40", [0x00, 0x00, 0xf0, 0xff, 0xff]), + ((1 << 40), "INTEGER48", [0x00, 0x00, 0x00, 0x00, 0x00, 0x01]), + (-(1 << 40), "INTEGER56", [0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff]), + ((1 << 62), "INTEGER64", [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40]), +]) +def test_serialize_known_values_sint(value: int, dtype: str, expected: list[int]): + assert expected == serialize(value, name=dtype) + + +# --- Explicit deserialization checks (known list[int] -> known output) --- + +@pytest.mark.parametrize("serialized, dtype, expected_value", [ + ([0xf8], "INTEGER8", -(1 << 3)), + ([0x00, 0x10], "INTEGER16", (1 << 12)), + ([0x00, 0x00, 0xc0], "INTEGER24", -(1 << 22)), + ([0x04, 0x00, 0x00, 0x00], "INTEGER32", (1 << 2)), + ([0x00, 0x00, 0xf0, 0xff, 0xff], "INTEGER40", -(1 << 20)), + ([0x00, 0x00, 0x00, 0x00, 0x00, 0x01], "INTEGER48", (1 << 40)), + ([0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff], "INTEGER56", -(1 << 40)), + ([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40], "INTEGER64", (1 << 62)), +]) +def test_deserialize_known_values_sint(serialized: list[int], dtype: str, expected_value: int): + assert expected_value == deserialize(serialized, name=dtype) + + +# --------------------------------------------------------------------------- +# Unsigned integers 8, 16, 24, 32, 40, 48, 56, 64 tests +# --------------------------------------------------------------------------- + +# --- round-trip tests --- + +@pytest.mark.parametrize("bit_width", [8, 16, 24, 32, 40, 48, 56, 64]) +def test_roundtrip_random_uint(bit_width: int): + """Serializing then deserializing a random valid value returns the original.""" + max_val = (1 << bit_width)-1 + value = random.randint(0, max_val) + dtype = f"UNSIGNED{bit_width}" + assert value == deserialize(serialize(value, name=dtype), name=dtype) + + +@pytest.mark.parametrize("bit_width", [8, 16, 24, 32, 40, 48, 56, 64]) +def test_roundtrip_min_uint(bit_width: int): + """Zero survives a round-trip for every unsigned integer size.""" + value = 0 + dtype = f"UNSIGNED{bit_width}" + assert value == deserialize(serialize(value, name=dtype), name=dtype) + + +@pytest.mark.parametrize("bit_width", [8, 16, 24, 32, 40, 48, 56, 64]) +def test_roundtrip_max_uint(bit_width: int): + """max-value survives a round-trip for every unsigned integer size.""" + max_val = (1 << bit_width)-1 + value = max_val + dtype = f"UNSIGNED{bit_width}" + assert value == deserialize(serialize(value, name=dtype), name=dtype) + + +# --------------------------------------------------------------------------- +# Bool tests +# --------------------------------------------------------------------------- + +# --- round-trip test --- + +def test_roundtrip_random_bool(): + """Serializing then deserializing a random valid value returns the original.""" + value = random.choice([True, False, 1, 0]) + assert value == deserialize(serialize(value, base_data_type="BOOL"), base_data_type="BOOL") + + +# --- Explicit serialization checks (known input -> known list[int]) --- + +@pytest.mark.parametrize("value, dtype, expected", [ + (False, "BOOL", [0x00]), + (True, "BOOL", [0xff]), + (0, "BOOL", [0x00]), + (1, "BOOL", [0xff]), +]) +def test_serialize_known_values_bool(value, dtype: str, expected: list[int]): + assert expected == serialize(value, base_data_type=dtype) + + +# --- Explicit deserialization checks (known list[int] -> known output) --- + +@pytest.mark.parametrize("serialized, dtype, expected_value", [ + ([0x00], "BOOL", False), + ([0x01], "BOOL", True), + ([0x00], "BOOL", 0), + ([0x01], "BOOL", 1), +]) +def test_deserialize_known_values_bool(serialized: list[int], dtype: str, expected_value): + assert expected_value == deserialize(serialized, base_data_type=dtype) + + +# --------------------------------------------------------------------------- +# Floating-point REAL32, REAL64 tests +# --------------------------------------------------------------------------- + +# --- round-trip tests --- + +@pytest.mark.parametrize(("dtype", "value"), [ + ("REAL32", random.uniform(-3.4e38, 3.4e38)), + ("REAL64", random.uniform(-1.7e308, 1.7e308)), +]) +def test_roundtrip_random_real(dtype: str, value: float): + """Serializing then deserializing a random valid float returns the original.""" + result = deserialize(serialize(value, name=dtype), name=dtype) + if dtype == "REAL32": + assert result == pytest.approx(value, rel=1e-6) # Python uses 64 bit so we lose some precision on the roundtrip + else: + assert result == value + + +@pytest.mark.parametrize("dtype", ["REAL32", "REAL64"]) +def test_roundtrip_zero_real(dtype: str): + """Zero survives a round-trip for every floating-point size.""" + value = 0.0 + assert value == deserialize(serialize(value, name=dtype), name=dtype) + + +@pytest.mark.parametrize(("dtype", "value"), [ + ("REAL32", float("inf")), + ("REAL32", float("-inf")), + ("REAL64", float("inf")), + ("REAL64", float("-inf")), +]) +def test_roundtrip_infinity_real(dtype: str, value: float): + """Infinity values survive a round-trip.""" + assert value == deserialize(serialize(value, name=dtype), name=dtype) + + +@pytest.mark.parametrize("dtype", ["REAL32", "REAL64"]) +def test_roundtrip_nan_real(dtype: str): + """NaN survives a round-trip.""" + value = float("nan") + result = deserialize(serialize(value, name=dtype), name=dtype) + assert isnan(result) + + +# --------------------------------------------------------------------------- +# TIME_OF_DAY, TIME_DIFFERENCE tests +# --------------------------------------------------------------------------- + +# --- round-trip tests --- + +@pytest.mark.parametrize("dtype", ["TIME_OF_DAY", "TIME_DIFFERENCE"]) +def test_roundtrip_random_time48(dtype: str): + """Serializing then deserializing a random valid value returns the original.""" + ms = random.randint(0, (1 << 28) - 1) + days = random.randint(0, (1 << 16) - 1) + value = (ms, days) + assert value == deserialize(serialize(value, base_data_type=dtype), base_data_type=dtype) + + +@pytest.mark.parametrize("dtype", ["TIME_OF_DAY", "TIME_DIFFERENCE"]) +def test_roundtrip_min_time48(dtype: str): + """Zero ms and days survive a round-trip for both data types.""" + assert (0, 0) == deserialize(serialize((0, 0), base_data_type=dtype), base_data_type=dtype) + + +@pytest.mark.parametrize("dtype", ["TIME_OF_DAY", "TIME_DIFFERENCE"]) +def test_roundtrip_max_time48(dtype: str): + """Max-value ms and days survive a round-trip for both data types.""" + ms = (1 << 28) - 1 + days = (1 << 16) - 1 + value = (ms, days) + assert value == deserialize(serialize(value, base_data_type=dtype), base_data_type=dtype) + + +# --- Explicit serialization checks (known input -> known list[int]) --- + +@pytest.mark.parametrize("value, dtype, expected", [ + ((5000, 5000), "TIME_OF_DAY", [0x00, 0x00, 0x13, 0x88, 0x13, 0x88]), + ((1000, 1000), "TIME_DIFFERENCE", [0x00, 0x00, 0x03, 0xE8, 0x03, 0xE8]), +]) +def test_serialize_known_values_time48(value: int, dtype: str, expected: list[int]): + assert expected == serialize(value, name=dtype) + + +# --- Explicit deserialization checks (known list[int] -> known output) --- + +@pytest.mark.parametrize("serialized, dtype, expected_value", [ + ([0x00, 0x00, 0x13, 0x88, 0x13, 0x88], "TIME_OF_DAY", (5000, 5000)), + ([0x00, 0x00, 0x03, 0xE8, 0x03, 0xE8], "TIME_DIFFERENCE", (1000, 1000)), +]) +def test_deserialize_known_values_time48(serialized: list[int], dtype: str, expected_value: int): + assert expected_value == deserialize(serialized, name=dtype) + + +# --------------------------------------------------------------------------- +# GUID tests +# --------------------------------------------------------------------------- + +# --- round-trip test --- + +def test_roundtrip_random_guid(): + """Serializing then deserializing a random valid value returns the original.""" + max_val = 1 << (128-1) + value = random.randint(-max_val, (max_val-1)) + assert value == deserialize(serialize(value, base_data_type="GUID"), base_data_type="GUID") + + +# --------------------------------------------------------------------------- +# VISIBLE_STRING (ASCII, 1 byte per char) tests +# --------------------------------------------------------------------------- + +# --- round-trip tests --- + +@pytest.mark.parametrize("value", [ + "hello", + "Hello, World!", + "EtherCAT", + "test 123", + "", # empty string + "A", # single char + " ", # space + "!@#$%^&*()", # special ASCII chars + "a" * 100, # long string +]) +def test_roundtrip_visible_string(value: str): + """Serializing then deserializing a string returns the original.""" + assert value == deserialize(serialize(value, name="VISIBLE_STRING"), name="VISIBLE_STRING") + + +# --- Explicit serialization checks (known input -> known list[int]) --- + +@pytest.mark.parametrize("value, expected", [ + ("A", [0x41]), + ("AB", [0x41, 0x42]), + ("hi", [0x68, 0x69]), + ("", []), + ("\x00", [0x00]), # null byte + (" ", [0x20]), # space + ("ABC", [0x41, 0x42, 0x43]), +]) +def test_serialize_known_values_visible_string(value: str, expected: list[int]): + assert expected == serialize(value, name="VISIBLE_STRING") + + +# --- Explicit deserialization checks (known list[int] -> known output) --- + +@pytest.mark.parametrize("serialized, expected_value", [ + ([0x41], "A"), + ([0x41, 0x42], "AB"), + ([0x68, 0x69], "hi"), + ([], ""), + ([0x00], "\x00"), + ([0x20], " "), + ([0x41, 0x42, 0x43], "ABC"), + ([0x63, 0x61, 0x66, 0xC3, 0xA9], "café"), # 2-byte sequence: é = 0xC3 0xA9 + ([0xE6, 0x97, 0xA5, 0xE6, 0x9C, 0xAC, 0xE8, 0xAA, 0x9E], "日本語"), # 3-byte sequences + ([0xF0, 0x9F, 0x98, 0x80], "😀"), # 4-byte sequence +]) +def test_deserialize_known_values_visible_string(serialized: list[int], expected_value: str): + assert expected_value == deserialize(serialized, name="VISIBLE_STRING") + + +# --- Invalid input --- + +@pytest.mark.parametrize("value", [ + ["h", "b"], + 5 +]) +def test_serialize_visible_string_rejects_invalid(value: str): + """Byte sequences that are not valid UTF-8 should raise.""" + assert -1 == serialize(value, name="VISIBLE_STRING") + + +# --------------------------------------------------------------------------- +# UNICODE_STRING (UTF-16-LE, 2 bytes per char) tests +# --------------------------------------------------------------------------- + +# --- round-trip tests --- + +@pytest.mark.parametrize("value", [ + "hello", + "Hello, World!", + "", # empty string + "A", # single char + "café", # accented characters + "日本語", # CJK characters + "EtherCAT 🚀", # emoji (surrogate pair in UTF-16) + "a" * 100, # long string +]) +def test_roundtrip_unicode_string(value: str): + """Serializing then deserializing a unicode string returns the original.""" + assert value == deserialize(serialize(value, name="UNICODE_STRING"), name="UNICODE_STRING") + + +# --- Explicit serialization checks (known input -> known list[int]) --- + +@pytest.mark.parametrize("value, expected", [ + ("A", [0x41, 0x00]), # U+0041, LE + ("AB", [0x41, 0x00, 0x42, 0x00]), # two ASCII chars + ("", []), # empty + (" ", [0x20, 0x00]), # space + ("é", [0xe9, 0x00]), # U+00E9 + ("中", [0x2d, 0x4e]), # U+4E2D +]) +def test_serialize_known_values_unicode_string(value: str, expected: list[int]): + assert expected == serialize(value, name="UNICODE_STRING") + + +# --- Explicit deserialization checks (known list[int] -> known output) --- + +@pytest.mark.parametrize("serialized, expected_value", [ + ([0x41, 0x00], "A"), + ([0x41, 0x00, 0x42, 0x00], "AB"), + ([], ""), + ([0x20, 0x00], " "), + ([0xe9, 0x00], "é"), + ([0x2d, 0x4e], "中"), +]) +def test_deserialize_known_values_unicode_string(serialized: list[int], expected_value: str): + assert expected_value == deserialize(serialized, name="UNICODE_STRING") + + +# --- Byte count is always even --- + +@pytest.mark.parametrize("value", ["A", "AB", "ABC", "日本語"]) +def test_unicode_string_serialized_length_is_even(value: str): + """UTF-16-LE encoding always produces an even number of bytes.""" + assert len(serialize(value, name="UNICODE_STRING")) % 2 == 0 + + +# --- Odd-length input is rejected --- + +def test_deserialize_unicode_string_rejects_odd_length(): + """An odd number of bytes cannot be valid UTF-16-LE.""" + assert -1 == deserialize([0x41], name="UNICODE_STRING") + + +# --------------------------------------------------------------------------- +# OCTET_STRING and ARRAY_OF_BITARRn tests (arrays of bit strings) +# --------------------------------------------------------------------------- + +# --- round-trip tests --- + +@pytest.mark.parametrize("dtype, bit_width", [ + ("OCTET_STRING", 8), + ("ARRAY_OF_BITARR8", 8), + ("ARRAY_OF_BITARR16", 16), + ("ARRAY_OF_BITARR32", 32), +]) +def test_roundtrip_random_bitarr_array(dtype: str, bit_width: int): + """Serializing then deserializing a random array of bit strings returns the original.""" + max_val = (1 << bit_width) - 1 + value = [format(random.randint(0, max_val), f"0{bit_width}b") for _ in range(random.randint(1, 8))] + assert value == deserialize(serialize(value, name=dtype), name=dtype) + + +@pytest.mark.parametrize("dtype, bit_width", [ + ("OCTET_STRING", 8), + ("ARRAY_OF_BITARR8", 8), + ("ARRAY_OF_BITARR16", 16), + ("ARRAY_OF_BITARR32", 32), +]) +def test_roundtrip_single_element_bitarr_array(dtype: str, bit_width: int): + """Single-element array survives a round-trip.""" + value = ["0" * bit_width] + assert value == deserialize(serialize(value, name=dtype), name=dtype) + + +@pytest.mark.parametrize("dtype, bit_width", [ + ("OCTET_STRING", 8), + ("ARRAY_OF_BITARR8", 8), + ("ARRAY_OF_BITARR16", 16), + ("ARRAY_OF_BITARR32", 32), +]) +def test_roundtrip_all_ones_bitarr_array(dtype: str, bit_width: int): + """All-ones array survives a round-trip.""" + value = ["1" * bit_width] * 4 + assert value == deserialize(serialize(value, name=dtype), name=dtype) + + +# --- Explicit serialization checks --- + +@pytest.mark.parametrize("value, dtype, expected", [ + # OCTET_STRING / ARRAY_OF_BITARR8: 1 byte per element + (["10101100"], "OCTET_STRING", [0xac]), + (["10101100", "01010101"], "OCTET_STRING", [0xac, 0x55]), + (["00000000", "11111111"], "OCTET_STRING", [0x00, 0xff]), + (["10101100"], "ARRAY_OF_BITARR8", [0xac]), + (["10101100", "01010101"], "ARRAY_OF_BITARR8", [0xac, 0x55]), + # ARRAY_OF_BITARR16: 2 bytes per element + (["1100101001110001"], "ARRAY_OF_BITARR16", [0x71, 0xca]), + (["1100101001110001", "0011110000101110"], "ARRAY_OF_BITARR16", [0x71, 0xca, 0x2e, 0x3c]), + # ARRAY_OF_BITARR32: 4 bytes per element + (["10010000111100001010101001101101"], "ARRAY_OF_BITARR32", [0x6d, 0xaa, 0xf0, 0x90]), + (["10010000111100001010101001101101", + "01101111000101011000001111110000"], "ARRAY_OF_BITARR32", [0x6d, 0xaa, 0xf0, 0x90, + 0xf0, 0x83, 0x15, 0x6f]), +]) +def test_serialize_known_values_bitarr_array(value: list, dtype: str, expected: list[int]): + assert expected == serialize(value, name=dtype) + + +# --- Explicit deserialization checks --- + +@pytest.mark.parametrize("serialized, dtype, expected_value", [ + ([0xac], "OCTET_STRING", ["10101100"]), + ([0xac, 0x55], "OCTET_STRING", ["10101100", "01010101"]), + ([0x00, 0xff], "OCTET_STRING", ["00000000", "11111111"]), + ([0xac], "ARRAY_OF_BITARR8", ["10101100"]), + ([0xac, 0x55], "ARRAY_OF_BITARR8", ["10101100", "01010101"]), + ([0x71, 0xca], "ARRAY_OF_BITARR16", ["1100101001110001"]), + ([0x71, 0xca, 0x2e, 0x3c], "ARRAY_OF_BITARR16", ["1100101001110001", "0011110000101110"]), + ([0x6d, 0xaa, 0xf0, 0x90], "ARRAY_OF_BITARR32", ["10010000111100001010101001101101"]), + ([0x6d, 0xaa, 0xf0, 0x90, + 0xf0, 0x83, 0x15, 0x6f], "ARRAY_OF_BITARR32", ["10010000111100001010101001101101", + "01101111000101011000001111110000"]), +]) +def test_deserialize_known_values_bitarr_array(serialized: list[int], dtype: str, expected_value: list): + assert expected_value == deserialize(serialized, name=dtype) + + +# --------------------------------------------------------------------------- +# ARRAY_OF_UINT, ARRAY_OF_UDINT, ARRAY_OF_USINT tests (unsigned int arrays) +# --------------------------------------------------------------------------- + +# --- round-trip tests --- + +@pytest.mark.parametrize("dtype, bit_width", [ + ("ARRAY_OF_USINT", 8), + ("ARRAY_OF_UINT", 16), + ("ARRAY_OF_UDINT", 32), +]) +def test_roundtrip_random_uint_array(dtype: str, bit_width: int): + """Serializing then deserializing a random array of uints returns the original.""" + max_val = (1 << bit_width) - 1 + value = [random.randint(0, max_val) for _ in range(random.randint(1, 8))] + assert value == deserialize(serialize(value, name=dtype), name=dtype) + + +@pytest.mark.parametrize("dtype", ["ARRAY_OF_USINT", "ARRAY_OF_UINT", "ARRAY_OF_UDINT"]) +def test_roundtrip_zeros_uint_array(dtype: str): + """Array of zeros survives a round-trip.""" + value = [0, 0, 0, 0] + assert value == deserialize(serialize(value, name=dtype), name=dtype) + + +@pytest.mark.parametrize("dtype, bit_width", [ + ("ARRAY_OF_USINT", 8), + ("ARRAY_OF_UINT", 16), + ("ARRAY_OF_UDINT", 32), +]) +def test_roundtrip_max_uint_array(dtype: str, bit_width: int): + """Array of max values survives a round-trip.""" + max_val = (1 << bit_width) - 1 + value = [max_val] * 4 + assert value == deserialize(serialize(value, name=dtype), name=dtype) + + +# --- Explicit serialization checks --- + +@pytest.mark.parametrize("value, dtype, expected", [ + # ARRAY_OF_USINT: 1 byte per element + ([0], "ARRAY_OF_USINT", [0x00]), + ([255], "ARRAY_OF_USINT", [0xff]), + ([1, 2, 3], "ARRAY_OF_USINT", [0x01, 0x02, 0x03]), + ([0, 128, 255],"ARRAY_OF_USINT", [0x00, 0x80, 0xff]), + # ARRAY_OF_UINT: 2 bytes per element (little-endian) + ([0], "ARRAY_OF_UINT", [0x00, 0x00]), + ([1], "ARRAY_OF_UINT", [0x01, 0x00]), + ([256], "ARRAY_OF_UINT", [0x00, 0x01]), + ([1, 2], "ARRAY_OF_UINT", [0x01, 0x00, 0x02, 0x00]), + ([0x1234], "ARRAY_OF_UINT", [0x34, 0x12]), + # ARRAY_OF_UDINT: 4 bytes per element (little-endian) + ([0], "ARRAY_OF_UDINT", [0x00, 0x00, 0x00, 0x00]), + ([1], "ARRAY_OF_UDINT", [0x01, 0x00, 0x00, 0x00]), + ([0x12345678], "ARRAY_OF_UDINT", [0x78, 0x56, 0x34, 0x12]), + ([1, 2], "ARRAY_OF_UDINT", [0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00]), +]) +def test_serialize_known_values_uint_array(value: list, dtype: str, expected: list[int]): + assert expected == serialize(value, name=dtype) + + +# --- Explicit deserialization checks --- + +@pytest.mark.parametrize("serialized, dtype, expected_value", [ + ([0x00], "ARRAY_OF_USINT", [0]), + ([0xff], "ARRAY_OF_USINT", [255]), + ([0x01, 0x02, 0x03], "ARRAY_OF_USINT", [1, 2, 3]), + ([0x00, 0x80, 0xff], "ARRAY_OF_USINT", [0, 128, 255]), + ([0x00, 0x00], "ARRAY_OF_UINT", [0]), + ([0x01, 0x00], "ARRAY_OF_UINT", [1]), + ([0x34, 0x12], "ARRAY_OF_UINT", [0x1234]), + ([0x01, 0x00, 0x02, 0x00], "ARRAY_OF_UINT", [1, 2]), + ([0x00, 0x00, 0x00, 0x00], "ARRAY_OF_UDINT", [0]), + ([0x78, 0x56, 0x34, 0x12], "ARRAY_OF_UDINT", [0x12345678]), + ([0x01, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00], "ARRAY_OF_UDINT", [1, 2]), +]) +def test_deserialize_known_values_uint_array(serialized: list[int], dtype: str, expected_value: list): + assert expected_value == deserialize(serialized, name=dtype) + + +# --------------------------------------------------------------------------- +# ARRAY_OF_INT, ARRAY_OF_SINT, ARRAY_OF_DINT tests (signed int arrays) +# --------------------------------------------------------------------------- + +# --- round-trip tests --- + +@pytest.mark.parametrize("dtype, bit_width", [ + ("ARRAY_OF_SINT", 8), + ("ARRAY_OF_INT", 16), + ("ARRAY_OF_DINT", 32), +]) +def test_roundtrip_random_sint_array(dtype: str, bit_width: int): + """Serializing then deserializing a random array of signed ints returns the original.""" + half = 1 << (bit_width - 1) + value = [random.randint(-half, half - 1) for _ in range(random.randint(1, 8))] + assert value == deserialize(serialize(value, name=dtype), name=dtype) + + +@pytest.mark.parametrize("dtype", ["ARRAY_OF_SINT", "ARRAY_OF_INT", "ARRAY_OF_DINT"]) +def test_roundtrip_zeros_sint_array(dtype: str): + """Array of zeros survives a round-trip.""" + value = [0, 0, 0, 0] + assert value == deserialize(serialize(value, name=dtype), name=dtype) + + +@pytest.mark.parametrize("dtype, bit_width", [ + ("ARRAY_OF_SINT", 8), + ("ARRAY_OF_INT", 16), + ("ARRAY_OF_DINT", 32), +]) +def test_roundtrip_min_max_sint_array(dtype: str, bit_width: int): + """Array of min and max values survives a round-trip.""" + half = 1 << (bit_width - 1) + value = [-half, half - 1] + assert value == deserialize(serialize(value, name=dtype), name=dtype) + + +# --- Explicit serialization checks --- + +@pytest.mark.parametrize("value, dtype, expected", [ + # ARRAY_OF_SINT: 1 byte per element (signed) + ([0], "ARRAY_OF_SINT", [0x00]), + ([-1], "ARRAY_OF_SINT", [0xff]), + ([127], "ARRAY_OF_SINT", [0x7f]), + ([-128], "ARRAY_OF_SINT", [0x80]), + ([-1, 1], "ARRAY_OF_SINT", [0xff, 0x01]), + # ARRAY_OF_INT: 2 bytes per element (signed, little-endian) + ([0], "ARRAY_OF_INT", [0x00, 0x00]), + ([-1], "ARRAY_OF_INT", [0xff, 0xff]), + ([1], "ARRAY_OF_INT", [0x01, 0x00]), + ([-1, 1], "ARRAY_OF_INT", [0xff, 0xff, 0x01, 0x00]), + ([(1 << 12)],"ARRAY_OF_INT", [0x00, 0x10]), + # ARRAY_OF_DINT: 4 bytes per element (signed, little-endian) + ([0], "ARRAY_OF_DINT", [0x00, 0x00, 0x00, 0x00]), + ([-1], "ARRAY_OF_DINT", [0xff, 0xff, 0xff, 0xff]), + ([1], "ARRAY_OF_DINT", [0x01, 0x00, 0x00, 0x00]), + ([-1, 1], "ARRAY_OF_DINT", [0xff, 0xff, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00]), +]) +def test_serialize_known_values_sint_array(value: list, dtype: str, expected: list[int]): + assert expected == serialize(value, name=dtype) + + +# --- Explicit deserialization checks --- + +@pytest.mark.parametrize("serialized, dtype, expected_value", [ + ([0x00], "ARRAY_OF_SINT", [0]), + ([0xff], "ARRAY_OF_SINT", [-1]), + ([0x7f], "ARRAY_OF_SINT", [127]), + ([0x80], "ARRAY_OF_SINT", [-128]), + ([0xff, 0x01], "ARRAY_OF_SINT", [-1, 1]), + ([0x00, 0x00], "ARRAY_OF_INT", [0]), + ([0xff, 0xff], "ARRAY_OF_INT", [-1]), + ([0xff, 0xff, 0x01, 0x00], "ARRAY_OF_INT", [-1, 1]), + ([0x00, 0x10], "ARRAY_OF_INT", [1 << 12]), + ([0x00, 0x00, 0x00, 0x00], "ARRAY_OF_DINT", [0]), + ([0xff, 0xff, 0xff, 0xff], "ARRAY_OF_DINT", [-1]), + ([0xff, 0xff, 0xff, 0xff, + 0x01, 0x00, 0x00, 0x00], "ARRAY_OF_DINT", [-1, 1]), +]) +def test_deserialize_known_values_sint_array(serialized: list[int], dtype: str, expected_value: list): + assert expected_value == deserialize(serialized, name=dtype) + + +# --------------------------------------------------------------------------- +# ARRAY_OF_REAL, ARRAY_OF_LREAL tests (float arrays) +# --------------------------------------------------------------------------- + +# --- round-trip tests --- + +@pytest.mark.parametrize("dtype, gen", [ + ("ARRAY_OF_REAL", lambda: random.uniform(-3.4e38, 3.4e38)), + ("ARRAY_OF_LREAL", lambda: random.uniform(-1.7e308, 1.7e308)), +]) +def test_roundtrip_random_float_array(dtype: str, gen): + """Serializing then deserializing a random float array returns the original.""" + value = [gen() for _ in range(random.randint(1, 8))] + result = deserialize(serialize(value, name=dtype), name=dtype) + if dtype == "ARRAY_OF_REAL": + assert result == pytest.approx(value, rel=1e-6) + else: + assert result == value + + +@pytest.mark.parametrize("dtype", ["ARRAY_OF_REAL", "ARRAY_OF_LREAL"]) +def test_roundtrip_zeros_float_array(dtype: str): + """Array of zeros survives a round-trip.""" + value = [0.0, 0.0, 0.0] + assert deserialize(serialize(value, name=dtype), name=dtype) == value + + +@pytest.mark.parametrize("dtype", ["ARRAY_OF_REAL", "ARRAY_OF_LREAL"]) +def test_roundtrip_inf_float_array(dtype: str): + """Infinity values survive a round-trip.""" + value = [float("inf"), float("-inf")] + assert deserialize(serialize(value, name=dtype), name=dtype) == value + + +@pytest.mark.parametrize("dtype", ["ARRAY_OF_REAL", "ARRAY_OF_LREAL"]) +def test_roundtrip_nan_float_array(dtype: str): + """NaN values survive a round-trip.""" + value = [float("nan"), float("nan")] + result = deserialize(serialize(value, name=dtype), name=dtype) + assert all(isnan(r) for r in result) + + +# --- Explicit serialization checks --- + +@pytest.mark.parametrize("value, dtype, expected", [ + # ARRAY_OF_REAL: 4 bytes per element + ([0.0], "ARRAY_OF_REAL", [0x00, 0x00, 0x00, 0x00]), + ([1.0], "ARRAY_OF_REAL", [0x00, 0x00, 0x80, 0x3f]), + ([-1.0], "ARRAY_OF_REAL", [0x00, 0x00, 0x80, 0xbf]), + ([0.0, 1.0], "ARRAY_OF_REAL", [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3f]), + # ARRAY_OF_LREAL: 8 bytes per element + ([0.0], "ARRAY_OF_LREAL", [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + ([1.0], "ARRAY_OF_LREAL", [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f]), + ([-1.0], "ARRAY_OF_LREAL", [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0xbf]), +]) +def test_serialize_known_values_float_array(value: list, dtype: str, expected: list[int]): + assert expected == serialize(value, name=dtype) + + +# --- Explicit deserialization checks --- + +@pytest.mark.parametrize("serialized, dtype, expected_value", [ + ([0x00, 0x00, 0x00, 0x00], "ARRAY_OF_REAL", [0.0]), + ([0x00, 0x00, 0x80, 0x3f], "ARRAY_OF_REAL", [1.0]), + ([0x00, 0x00, 0x80, 0xbf], "ARRAY_OF_REAL", [-1.0]), + ([0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x80, 0x3f], "ARRAY_OF_REAL", [0.0, 1.0]), + ([0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00], "ARRAY_OF_LREAL", [0.0]), + ([0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xf0, 0x3f], "ARRAY_OF_LREAL", [1.0]), + ([0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xf0, 0xbf], "ARRAY_OF_LREAL", [-1.0]), +]) +def test_deserialize_known_values_float_array(serialized: list[int], dtype: str, expected_value: list): + assert expected_value == pytest.approx(deserialize(serialized, name=dtype)) + + +# --------------------------------------------------------------------------- +# General array property tests (all array types) +# --------------------------------------------------------------------------- + +ALL_ARRAY_TYPES = [ + "OCTET_STRING", + "ARRAY_OF_UINT", "ARRAY_OF_INT", "ARRAY_OF_SINT", "ARRAY_OF_DINT", "ARRAY_OF_UDINT", + "ARRAY_OF_BITARR8", "ARRAY_OF_BITARR16", "ARRAY_OF_BITARR32", + "ARRAY_OF_USINT", "ARRAY_OF_REAL", "ARRAY_OF_LREAL", +] + +ELEMENT_BYTE_WIDTH = { + "OCTET_STRING": 1, + "ARRAY_OF_USINT": 1, "ARRAY_OF_SINT": 1, "ARRAY_OF_BITARR8": 1, + "ARRAY_OF_UINT": 2, "ARRAY_OF_INT": 2, "ARRAY_OF_BITARR16": 2, + "ARRAY_OF_UDINT": 4, "ARRAY_OF_DINT": 4, "ARRAY_OF_BITARR32": 4, "ARRAY_OF_REAL": 4, + "ARRAY_OF_LREAL": 8, +} + +ELEMENT_GENERATORS = { + "OCTET_STRING": lambda: format(random.randint(0, 0xff), "08b"), + "ARRAY_OF_USINT": lambda: random.randint(0, 0xff), + "ARRAY_OF_UINT": lambda: random.randint(0, 0xffff), + "ARRAY_OF_UDINT": lambda: random.randint(0, 0xffffffff), + "ARRAY_OF_SINT": lambda: random.randint(-128, 127), + "ARRAY_OF_INT": lambda: random.randint(-32768, 32767), + "ARRAY_OF_DINT": lambda: random.randint(-(1 << 31), (1 << 31) - 1), + "ARRAY_OF_BITARR8": lambda: format(random.randint(0, 0xff), "08b"), + "ARRAY_OF_BITARR16": lambda: format(random.randint(0, 0xffff), "016b"), + "ARRAY_OF_BITARR32": lambda: format(random.randint(0, 0xffffffff), "032b"), + "ARRAY_OF_REAL": lambda: random.uniform(-1e10, 1e10), + "ARRAY_OF_LREAL": lambda: random.uniform(-1e100, 1e100), +} + + +@pytest.mark.parametrize("dtype", ALL_ARRAY_TYPES) +def test_serialized_length_matches_element_count(dtype: str): + """Serialized byte count equals number of elements × bytes per element.""" + n = random.randint(1, 8) + gen = ELEMENT_GENERATORS[dtype] + value = [gen() for _ in range(n)] + result = serialize(value, name=dtype) + assert len(result) == n * ELEMENT_BYTE_WIDTH[dtype] + + +@pytest.mark.parametrize("dtype", ALL_ARRAY_TYPES) +def test_empty_array_serializes_to_empty(dtype: str): + """An empty array serializes to an empty list.""" + assert [] == serialize([], name=dtype) + + +@pytest.mark.parametrize("dtype", ALL_ARRAY_TYPES) +def test_empty_array_deserializes_to_empty(dtype: str): + """An empty list deserializes to an empty array.""" + assert [] == deserialize([], name=dtype) \ No newline at end of file diff --git a/rise_motion_dev_ws/src/rise_motion/CMakeLists.txt b/rise_motion_dev_ws/src/rise_motion/CMakeLists.txt index ce8a2e3..36fd103 100644 --- a/rise_motion_dev_ws/src/rise_motion/CMakeLists.txt +++ b/rise_motion_dev_ws/src/rise_motion/CMakeLists.txt @@ -14,6 +14,13 @@ include_directories(include) add_subdirectory(SOEM) +# --- sdo_serializer library --- +add_library(sdo_serializer_lib INTERFACE) +target_include_directories(sdo_serializer_lib INTERFACE + $ + $ +) + add_executable( rise_motion_main src/main.cpp @@ -50,8 +57,11 @@ install(TARGETS testing_node DESTINATION lib/${PROJECT_NAME} ) + + if(BUILD_TESTING) find_package(ament_lint_auto REQUIRED) + find_package(ament_cmake_gtest REQUIRED) # the following line skips the linter which checks for copyrights # comment the line when a copyright and license is added to all source files set(ament_cmake_copyright_FOUND TRUE) @@ -62,4 +72,18 @@ if(BUILD_TESTING) ament_lint_auto_find_test_dependencies() endif() +install( + DIRECTORY include/ + DESTINATION include +) + +install( + TARGETS sdo_serializer_lib + EXPORT export_sdo_serializer_lib + INCLUDES DESTINATION include +) + +ament_export_targets(export_sdo_serializer_lib) +ament_export_include_directories(include) + ament_package() diff --git a/rise_motion_dev_ws/src/rise_motion/include/rise_motion/ec_manager.hpp b/rise_motion_dev_ws/src/rise_motion/include/rise_motion/ec_manager.hpp index 279b07b..3175940 100755 --- a/rise_motion_dev_ws/src/rise_motion/include/rise_motion/ec_manager.hpp +++ b/rise_motion_dev_ws/src/rise_motion/include/rise_motion/ec_manager.hpp @@ -26,7 +26,7 @@ class ECManager { bool set_motor_values_apsa(const std::vector& motor_values); // Wrappers for SOEM ecx_SDOwrite, ecx_SDOread - bool sdo_read(uint16 device_id, uint16 index, uint8 subindex, std::vector& value); + bool sdo_read(uint16 device_id, uint16 index, uint8 subindex, std::vector& value, uint8 value_size); bool sdo_write(uint16 device_id, uint16 index, uint8 subindex, std::vector& value); private: diff --git a/rise_motion_dev_ws/src/rise_motion/include/rise_motion/result.hpp b/rise_motion_dev_ws/src/rise_motion/include/rise_motion/result.hpp new file mode 100644 index 0000000..f7c1584 --- /dev/null +++ b/rise_motion_dev_ws/src/rise_motion/include/rise_motion/result.hpp @@ -0,0 +1,30 @@ +namespace rise +{ + template struct Result + { + T value{}; + E error{}; + bool hasValue = false; + + static Result ok(T v) + { + Result result; + result.value = std::move(v); + result.hasValue = true; + return result; + } + + static Result err(E e) + { + Result result; + result.error = std::move(e); + result.hasValue = false; + return result; + } + + explicit operator bool() const + { + return hasValue; + } + }; +} \ No newline at end of file diff --git a/rise_motion_dev_ws/src/rise_motion/include/rise_motion/sdo_serializer.hpp b/rise_motion_dev_ws/src/rise_motion/include/rise_motion/sdo_serializer.hpp new file mode 100644 index 0000000..ca6d80a --- /dev/null +++ b/rise_motion_dev_ws/src/rise_motion/include/rise_motion/sdo_serializer.hpp @@ -0,0 +1,1273 @@ +// library assumes little-endian + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sdo +{ + enum class ErrorCode + { + None, + InvalidSize, + OutOfRange, + UnsupportedType, + CallFailed, + ServiceUnavailable + }; + + struct Error + { + ErrorCode code = ErrorCode::None; + std::string message = ""; + }; + + + // as equivalent of the EtherCAT TIME_OF_DAY data type + struct TimeOfDay + { + std::uint32_t ms_since_midnight; + std::uint16_t d_since_1984_01_01; + + TimeOfDay() = default; + }; + + // as equivalent of the EtherCAT TIME_DIFFERENCE data type + struct TimeDifference + { + std::uint32_t ms; + std::uint16_t d; + + TimeDifference() = default; + }; + + struct Int24 + { + std::int32_t value; + + Int24() = default; + + Int24(std::int32_t v) : value(v) + {} + + operator std::int32_t() const + { + return value; + } + }; + + struct Int40 + { + std::int64_t value; + + Int40() = default; + + Int40(std::int64_t v) : value(v) + {} + + operator std::int64_t() const + { + return value; + } + }; + + struct Int48 + { + std::int64_t value; + + Int48() = default; + + Int48(std::int64_t v) : value(v) + {} + + operator std::int64_t() const + { + return value; + } + }; + + struct Int56 + { + std::int64_t value; + + Int56() = default; + + Int56(std::int64_t v) : value(v) + {} + + operator std::int64_t() const + { + return value; + } + }; + + struct UInt24 + { + std::uint32_t value; + + UInt24() = default; + + UInt24(std::uint32_t v) : value(v) + {} + + operator std::uint32_t() const + { + return value; + } + }; + + struct UInt40 + { + std::uint64_t value; + + UInt40() = default; + + UInt40(std::uint64_t v) : value(v) + {} + + operator std::uint64_t() const + { + return value; + } + }; + + struct UInt48 + { + std::uint64_t value; + + UInt48() = default; + + UInt48(std::uint64_t v) : value(v) + {} + + operator std::uint64_t() const + { + return value; + } + }; + + struct UInt56 + { + std::uint64_t value; + + UInt56() = default; + + UInt56(std::uint64_t v) : value(v) + {} + + operator std::uint64_t() const + { + return value; + } + }; + + struct Guid + { + std::array bytes; + + Guid(std::array b) : bytes(b){}; + Guid() = default; + + operator std::array() const + { + return bytes; + } + }; + + template struct STRING + { + std::string value; + + STRING() = default; + + STRING(std::string v) : value(std::move(v)) + {} + + operator std::string() const + { + return value; + } + }; + + template struct WSTRING + { + std::u16string value; + + WSTRING() = default; + + WSTRING(std::u16string v) : value(std::move(v)) + {} + + operator std::u16string() const + { + return value; + } + }; + + template struct ARRAY + { + std::vector value; + + ARRAY() = default; + + ARRAY(std::vector v) : value(std::move(v)) + {} + + operator std::vector() const + { + return value; + } + }; + + + template using DeserializeResult = rise::Result; + using SerializeResult = rise::Result, sdo::Error>; + + // IEC 61131-3 data types + using BOOL = bool; + + using SINT = std::int8_t; + using INT = std::int16_t; + using DINT = std::int32_t; + using LINT = std::int64_t; + + using USINT = std::uint8_t; + using UINT = std::uint16_t; + using UDINT = std::uint32_t; + using ULINT = std::uint64_t; + + using REAL = float; + using LREAL = double; + + using BYTE = std::uint8_t; + using WORD = std::uint16_t; + using DWORD = std::uint32_t; + using LWORD = std::uint64_t; + + using DATE = sdo::TimeOfDay; + using TIME = sdo::TimeDifference; + + // EtherCAT data types + using BOOLEAN = bool; + + using INTEGER8 = std::int8_t; + using INTEGER16 = std::int16_t; + using INTEGER24 = sdo::Int24; + using INTEGER32 = std::int32_t; + using INTEGER40 = sdo::Int40; + using INTEGER48 = sdo::Int48; + using INTEGER56 = sdo::Int56; + using INTEGER64 = std::int64_t; + + using UNSIGNED8 = std::uint8_t; + using UNSIGNED16 = std::uint16_t; + using UNSIGNED24 = sdo::UInt24; + using UNSIGNED32 = std::uint32_t; + using UNSIGNED40 = sdo::UInt40; + using UNSIGNED48 = sdo::UInt48; + using UNSIGNED56 = sdo::UInt56; + using UNSIGNED64 = std::uint64_t; + + using BITARR8 = std::uint8_t; + using BITARR16 = std::uint16_t; + using BITARR32 = std::uint32_t; + + using BIT1 = std::uint8_t; + using BIT2 = std::uint8_t; + using BIT3 = std::uint8_t; + using BIT4 = std::uint8_t; + using BIT5 = std::uint8_t; + using BIT6 = std::uint8_t; + using BIT7 = std::uint8_t; + using BIT8 = std::uint8_t; + using BIT9 = std::uint16_t; + using BIT10 = std::uint16_t; + using BIT11 = std::uint16_t; + using BIT12 = std::uint16_t; + using BIT13 = std::uint16_t; + using BIT14 = std::uint16_t; + using BIT15 = std::uint16_t; + using BIT16 = std::uint16_t; + + using REAL32 = float; + using REAL64 = double; + + using TIME_OF_DAY = sdo::TimeOfDay; + using TIME_DIFFERENCE = sdo::TimeDifference; + + using GUID = sdo::Guid; + using DOMAIN = std::vector; + + template using VISIBLE_STRING = STRING; + template using UNICODE_STRING = WSTRING; + template using OCTET_STRING = ARRAY; + template using ARRAY_OF_USINT = ARRAY; + template using ARRAY_OF_UINT = ARRAY; + template using ARRAY_OF_INT = ARRAY; + template using ARRAY_OF_SINT = ARRAY; + template using ARRAY_OF_DINT = ARRAY; + template using ARRAY_OF_UDINT = ARRAY; + template using ARRAY_OF_BITARR8 = ARRAY; + template using ARRAY_OF_BITARR16 = ARRAY; + template using ARRAY_OF_BITARR32 = ARRAY; +} + + +namespace sdo::helpers +{ + template inline constexpr bool always_false = false; + + template struct is_string_type : std::false_type {}; + template struct is_string_type> : std::true_type {}; + + template struct string_size; + template struct string_size> + { + static constexpr std::size_t size = T; + }; + + template struct is_wstring_type : std::false_type {}; + template struct is_wstring_type> : std::true_type {}; + + template struct wstring_size; + template struct wstring_size> + { + static constexpr std::size_t size = T; + }; + + template struct is_array_type : std::false_type {}; + template + struct is_array_type> : std::true_type {}; + + template struct array_size; + template struct array_size> + { + static constexpr std::size_t size = N; + }; + + template struct elementType; + template struct elementType> + { + using type = T; + }; + + + inline std::optional check_size( + const std::vector& blob, std::size_t expectedSize, const char* type) + { + if (blob.size() != expectedSize){ + return Error{ + ErrorCode::InvalidSize, + std::string("deserialize<") + type + ">: expected " + std::to_string(expectedSize) + + " bytes, got " + std::to_string(blob.size())}; + } + + return std::nullopt; + } + + template [[nodiscard]] inline DeserializeResult to_raw( + const std::vector& blob, std::size_t numBytes, std::size_t offset = 0) + { + static_assert(std::is_unsigned_v, "to_raw: T must be unsigned"); + static_assert(sizeof(T) <= sizeof(std::uint64_t), "to_raw: T can be max uint64_t"); + + if (numBytes > sizeof(T)) + { + return DeserializeResult::err({ + ErrorCode::InvalidSize, "to_raw: numBytes does not fit T"}); + } + + if (offset + numBytes > blob.size()) + { + return DeserializeResult::err({ + ErrorCode::InvalidSize, "to_raw: blob has less bytes than numBytes"}); + } + + T raw = 0; + + for (std::size_t i = 0; i < numBytes; ++i) + { + raw |= static_cast(blob[offset + i]) << (8 * i); + } + + return DeserializeResult::ok(raw); + } + + template [[nodiscard]] inline SerializeResult from_raw( + const T& raw, std::size_t numBytes) + { + static_assert(std::is_unsigned_v, "from_raw: T must be unsigned"); + + if (numBytes > sizeof(T)) + { + SerializeResult::err({ErrorCode::InvalidSize, "from_raw: numBytes does not fit T"}); + } + + std::vector blob; + blob.reserve(numBytes); + + for (std::size_t i = 0; i < numBytes; ++i) + { + blob.push_back(static_cast((raw >> (8 * i)) & 0xFF)); + } + + return SerializeResult::ok(blob); + } +} + +namespace sdo +{ + // ---DESERIALIZATION--- + + template [[nodiscard]] inline DeserializeResult deserialize( + const std::vector& blob) + { + using CleanT = std::remove_cv_t>; + + if constexpr (sdo::helpers::is_array_type::value) + { + constexpr std::size_t numElements = sdo::helpers::array_size::size; + using ElementT = typename sdo::helpers::elementType::type; + constexpr std::size_t elementSize = sizeof(ElementT); + constexpr std::size_t byteSize = numElements * elementSize; + + if (blob.size() > byteSize || blob.size() % elementSize != 0){ + return DeserializeResult::err({ + ErrorCode::InvalidSize, "deserialize>: invalid blob size"}); + } + + T result{}; + result.value.reserve(blob.size() / elementSize); + + for (std::size_t i = 0; i < blob.size(); i += elementSize){ + if constexpr (elementSize == 1){ + result.value.push_back(static_cast(blob[i])); + } + else if constexpr (elementSize == 2){ + auto raw = sdo::helpers::to_raw(blob, 2, i); + if (!raw){ + return DeserializeResult::err(raw.error); + } + + result.value.push_back(static_cast(raw.value)); + } + else if constexpr (elementSize == 4){ + auto raw = sdo::helpers::to_raw(blob, 4, i); + if (!raw){ + return DeserializeResult::err(raw.error); + } + + if constexpr (std::is_same_v){ + float element; + std::memcpy(&element, &raw.value, sizeof(element)); + result.value.push_back(element); + } + else{ + result.value.push_back(static_cast(raw.value)); + } + } + else if constexpr (elementSize == 8){ + auto raw = sdo::helpers::to_raw(blob, 8, i); + if (!raw){ + return DeserializeResult::err(raw.error); + } + + if constexpr (std::is_same_v){ + double element; + std::memcpy(&element, &raw.value, sizeof(element)); + result.value.push_back(element); + } + else{ + result.value.push_back(static_cast(raw.value)); + } + } + else{ + return DeserializeResult::err({ + ErrorCode::UnsupportedType, + "deserialize>: unsupported array type"}); + } + } + + return DeserializeResult::ok(std::move(result)); + } + else if constexpr (sdo::helpers::is_wstring_type::value) + { + constexpr std::size_t size = sdo::helpers::wstring_size::size; + + if (blob.size() > size * 2 || blob.size() % 2 != 0){ + return DeserializeResult::err({ + ErrorCode::InvalidSize, + "deserialize>: blob is larger than the expected size T"}); + } + + T str{}; + + for (std::size_t i = 0; i < blob.size(); i += 2){ + std::uint16_t raw = + static_cast(blob[i]) | + static_cast(blob[i + 1]) << 8; + + str.value.push_back(static_cast(raw)); + } + + // strip padding zeros + while (!str.value.empty() && str.value.back() == u'\0'){ + str.value.pop_back(); + } + + return DeserializeResult::ok(str); + } + else if constexpr (sdo::helpers::is_string_type::value) + { + + constexpr std::size_t size = sdo::helpers::string_size::size; + + if (blob.size() > size){ + return DeserializeResult::err({ + ErrorCode::InvalidSize, + std::string("deserialize>: blob is larger than the expected size T. Expected: ") + + std::to_string(size) + ", got: " + std::to_string(blob.size())}); + } + + T str{}; + + str.value.assign(blob.begin(), blob.end()); + + // strip padding zeros + while (!str.value.empty() && str.value.back() == '\0'){ + str.value.pop_back(); + } + + return DeserializeResult::ok(str); + } + else{ + static_assert(sdo::helpers::always_false, "deserialize: unsupported type"); + } + } + + // -Boolean- + + template <> [[nodiscard]] inline DeserializeResult deserialize( + const std::vector& blob) + { + if (auto error = sdo::helpers::check_size(blob, 1, "bool")){ + return sdo::DeserializeResult::err(*error); + } + + return DeserializeResult::ok(blob[0] != 0); + } + + // -Signed Integer- + + template <> [[nodiscard]] inline DeserializeResult deserialize( + const std::vector& blob) + { + if (auto error = sdo::helpers::check_size(blob, 1, "int8_t")){ + return sdo::DeserializeResult::err(*error); + } + + return DeserializeResult::ok(static_cast(blob[0])); + } + + template <> [[nodiscard]] inline DeserializeResult deserialize( + const std::vector& blob) + { + if (auto error = sdo::helpers::check_size(blob, 2, "int16_t")){ + return sdo::DeserializeResult::err(*error); + } + + auto raw = sdo::helpers::to_raw(blob, 2); + if(!raw){ + return DeserializeResult::err(raw.error); + } + + auto result = static_cast(raw.value); + + return DeserializeResult::ok(result); + } + + template <> [[nodiscard]] inline DeserializeResult deserialize( + const std::vector& blob) + { + if (auto error = sdo::helpers::check_size(blob, 4, "int32_t")){ + return sdo::DeserializeResult::err(*error); + } + + auto raw = sdo::helpers::to_raw(blob, 4); + if(!raw){ + return DeserializeResult::err(raw.error); + } + + auto result = static_cast(raw.value); + + return DeserializeResult::ok(result); + } + + // -Unsigned Integer / raw data / bit arrays / bit strings- + + // use for UNSIGNED8, BYTE, BITARR8, BIT1-BIT8 + template <> [[nodiscard]] inline DeserializeResult deserialize( + const std::vector& blob) + { + if (auto error = sdo::helpers::check_size(blob, 1, "uint8_t")){ + return sdo::DeserializeResult::err(*error); + } + + return DeserializeResult::ok(static_cast(blob[0])); + } + + // use for UNSIGNED16, WORD, BITARR16, BIT9-BIT16 + template <> [[nodiscard]] inline DeserializeResult deserialize( + const std::vector& blob) + { + if (auto error = sdo::helpers::check_size(blob, 2, "uint16_t")){ + return sdo::DeserializeResult::err(*error); + } + + return sdo::helpers::to_raw(blob, 2); + } + + // use for UNSIGNED32, DWORD, BITARR32 + template <> [[nodiscard]] inline DeserializeResult deserialize( + const std::vector& blob) + { + if (auto error = sdo::helpers::check_size(blob, 4, "uint32_t")){ + return sdo::DeserializeResult::err(*error); + } + + return sdo::helpers::to_raw(blob, 4); + } + + // -Floating Point- + + template <> [[nodiscard]] inline DeserializeResult deserialize( + const std::vector& blob) + { + if (auto error = sdo::helpers::check_size(blob, 4, "float")){ + return sdo::DeserializeResult::err(*error); + } + + auto raw = sdo::helpers::to_raw(blob, 4); + if(!raw){ + return DeserializeResult::err(raw.error); + } + + float value; + std::memcpy(&value, &raw.value, sizeof(value)); + + return DeserializeResult::ok(value); + } + + template <> [[nodiscard]] inline DeserializeResult deserialize( + const std::vector& blob) + { + if (auto error = sdo::helpers::check_size(blob, 8, "double")){ + return sdo::DeserializeResult::err(*error); + } + + auto raw = sdo::helpers::to_raw(blob, 8); + if(!raw){ + return DeserializeResult::err(raw.error); + } + + double value; + std::memcpy(&value, &raw.value, sizeof(value)); + + return DeserializeResult::ok(value); + } + + // -Time- + + template <> [[nodiscard]] inline DeserializeResult deserialize( + const std::vector& blob) + { + if (auto error = sdo::helpers::check_size(blob, 6, "TimeOfDay")){ + return sdo::DeserializeResult::err(*error); + } + + TimeOfDay value{}; + + auto ms_raw = sdo::helpers::to_raw(blob, 4); + if(!ms_raw){ + return DeserializeResult::err(ms_raw.error); + } + value.ms_since_midnight = ms_raw.value; + + auto d_raw = sdo::helpers::to_raw(blob, 2, 4); + if(!d_raw){ + return DeserializeResult::err(d_raw.error); + } + value.d_since_1984_01_01 = d_raw.value; + + return DeserializeResult::ok(value); + } + + template <> [[nodiscard]] inline DeserializeResult deserialize( + const std::vector& blob) + { + if (auto error = sdo::helpers::check_size(blob, 6, "TimeDifference")){ + return sdo::DeserializeResult::err(*error); + } + + TimeDifference value{}; + + auto ms_raw = sdo::helpers::to_raw(blob, 4); + if(!ms_raw){ + return DeserializeResult::err(ms_raw.error); + } + // the upper 4 bits of ms are reserved + value.ms = ms_raw.value & 0x0FFFFFFF; + + auto d_raw = sdo::helpers::to_raw(blob, 2, 4); + if(!d_raw){ + return DeserializeResult::err(d_raw.error); + } + value.d = d_raw.value; + + return DeserializeResult::ok(value); + } + + // -Domain- + // (returns raw blob as equivalent to the EtherCAT Domain data type) + + template <> [[nodiscard]] inline DeserializeResult> deserialize>( + const std::vector& blob) + { + return DeserializeResult>::ok(blob); + } + + // -Extended Signed Integer- + + template <> [[nodiscard]] inline DeserializeResult deserialize( + const std::vector& blob) + { + if (auto error = sdo::helpers::check_size(blob, 3, "Int24")){ + return sdo::DeserializeResult::err(*error); + } + + auto raw = sdo::helpers::to_raw(blob, 3); + if(!raw){ + return DeserializeResult::err(raw.error); + } + + if (raw.value & 0x00800000){ + raw.value |= 0xFF000000; + } + + return DeserializeResult::ok(Int24{static_cast(raw.value)}); + } + + template <> [[nodiscard]] inline DeserializeResult deserialize( + const std::vector& blob) + { + if (auto error = sdo::helpers::check_size(blob, 5, "Int40")){ + return sdo::DeserializeResult::err(*error); + } + + auto raw = sdo::helpers::to_raw(blob, 5); + if(!raw){ + return DeserializeResult::err(raw.error); + } + + if (raw.value & 0x0000008000000000ULL) + { + raw.value |= 0xFFFFFF0000000000ULL; + } + + return DeserializeResult::ok(Int40{static_cast(raw.value)}); + } + + template <> [[nodiscard]] inline DeserializeResult deserialize( + const std::vector& blob) + { + if (auto error = sdo::helpers::check_size(blob, 6, "Int48")){ + return sdo::DeserializeResult::err(*error); + } + + auto raw = sdo::helpers::to_raw(blob, 6); + if(!raw){ + return DeserializeResult::err(raw.error); + } + + if (raw.value & 0x0000800000000000ULL) + { + raw.value |= 0xFFFF000000000000ULL; + } + + return DeserializeResult::ok(Int48{static_cast(raw.value)}); + } + + template <> [[nodiscard]] inline DeserializeResult deserialize( + const std::vector& blob) + { + if (auto error = sdo::helpers::check_size(blob, 7, "Int56")){ + return sdo::DeserializeResult::err(*error); + } + + auto raw = sdo::helpers::to_raw(blob, 7); + if(!raw){ + return DeserializeResult::err(raw.error); + } + + if (raw.value & 0x0080000000000000ULL) + { + raw.value |= 0xFF00000000000000ULL; + } + + return DeserializeResult::ok(Int56{static_cast(raw.value)}); + } + + template <> [[nodiscard]] inline DeserializeResult deserialize( + const std::vector& blob) + { + if (auto error = sdo::helpers::check_size(blob, 8, "int64_t")){ + return sdo::DeserializeResult::err(*error); + } + + auto raw = sdo::helpers::to_raw(blob, 8); + if(!raw){ + return DeserializeResult::err(raw.error); + } + + return DeserializeResult::ok(static_cast(raw.value)); + } + + // -Extended Unsigned Integer- + + template <> [[nodiscard]] inline DeserializeResult deserialize( + const std::vector& blob) + { + if (auto error = sdo::helpers::check_size(blob, 3, "UInt24")){ + return sdo::DeserializeResult::err(*error); + } + + auto raw = sdo::helpers::to_raw(blob, 3); + if(!raw){ + return DeserializeResult::err(raw.error); + } + + return DeserializeResult::ok(UInt24{raw.value}); + } + + template <> [[nodiscard]] inline DeserializeResult deserialize( + const std::vector& blob) + { + if (auto error = sdo::helpers::check_size(blob, 5, "UInt40")){ + return sdo::DeserializeResult::err(*error); + } + + auto raw = sdo::helpers::to_raw(blob, 5); + if(!raw){ + return DeserializeResult::err(raw.error); + } + + return DeserializeResult::ok(UInt40{raw.value}); + } + + template <> [[nodiscard]] inline DeserializeResult deserialize( + const std::vector& blob) + { + if (auto error = sdo::helpers::check_size(blob, 6, "UInt48")){ + return sdo::DeserializeResult::err(*error); + } + + auto raw = sdo::helpers::to_raw(blob, 6); + if(!raw){ + return DeserializeResult::err(raw.error); + } + + return DeserializeResult::ok(UInt48{raw.value}); + } + + template <> [[nodiscard]] inline DeserializeResult deserialize( + const std::vector& blob) + { + if (auto error = sdo::helpers::check_size(blob, 7, "UInt56")){ + return sdo::DeserializeResult::err(*error); + } + + auto raw = sdo::helpers::to_raw(blob, 7); + if(!raw){ + return DeserializeResult::err(raw.error); + } + + return DeserializeResult::ok(UInt56{raw.value}); + } + + template <> [[nodiscard]] inline DeserializeResult deserialize( + const std::vector& blob) + { + if (auto error = sdo::helpers::check_size(blob, 8, "uint64_t")){ + return sdo::DeserializeResult::err(*error); + } + + return sdo::helpers::to_raw(blob, 8); + } + + // -GUID- + + template <> [[nodiscard]] inline DeserializeResult deserialize( + const std::vector& blob) + { + if (auto error = sdo::helpers::check_size(blob, 16, "Guid")){ + return sdo::DeserializeResult::err(*error); + } + + Guid value{}; + + for (std::size_t i = 0; i < value.bytes.size(); ++i) + { + value.bytes[i] = blob[i]; + } + + return DeserializeResult::ok(value); + } + + + // ---SERIALIZATION--- + + template SerializeResult serialize(const T& value) + { + using CleanT = std::remove_cv_t>; + + if constexpr (sdo::helpers::is_array_type::value) + { + constexpr std::size_t numElements = sdo::helpers::array_size::size; + using ElementT = typename sdo::helpers::elementType::type; + constexpr std::size_t elementSize = sizeof(ElementT); + constexpr std::size_t byteSize = numElements * elementSize; + + if (value.value.size() > numElements){ + return SerializeResult::err({ + ErrorCode::OutOfRange, + "serialize>: array is longer than size N" + }); + } + + std::vector blob; + blob.reserve(byteSize); + + for (const auto& element : value.value){ + if constexpr (elementSize == 1){ + const auto raw = static_cast(element); + + blob.push_back(raw); + } + else if constexpr (elementSize == 2){ + const auto raw = static_cast(element); + + blob.push_back(static_cast(raw & 0xFF)); + blob.push_back(static_cast((raw >> 8) & 0xFF)); + } + else if constexpr (elementSize == 4){ + std::uint32_t raw; + + if constexpr (std::is_same_v){ + std::memcpy(&raw, &element, sizeof(raw)); + } + else{ + raw = static_cast(element); + } + + blob.push_back(static_cast(raw & 0xFF)); + blob.push_back(static_cast((raw >> 8) & 0xFF)); + blob.push_back(static_cast((raw >> 16) & 0xFF)); + blob.push_back(static_cast((raw >> 24) & 0xFF)); + } + else if constexpr (elementSize == 8){ + std::uint64_t raw; + + if constexpr (std::is_same_v){ + std::memcpy(&raw, &element, sizeof(raw)); + } + else{ + raw = static_cast(element); + } + + blob.push_back(static_cast(raw & 0xFF)); + blob.push_back(static_cast((raw >> 8) & 0xFF)); + blob.push_back(static_cast((raw >> 16) & 0xFF)); + blob.push_back(static_cast((raw >> 24) & 0xFF)); + blob.push_back(static_cast((raw >> 32) & 0xFF)); + blob.push_back(static_cast((raw >> 40) & 0xFF)); + blob.push_back(static_cast((raw >> 48) & 0xFF)); + blob.push_back(static_cast((raw >> 56) & 0xFF)); + } + else{ + return SerializeResult::err({ + ErrorCode::UnsupportedType, + "serialize>: unsupported array type"}); + } + } + + blob.resize(byteSize, 0); + + return SerializeResult::ok(std::move(blob)); + } + else if constexpr (sdo::helpers::is_wstring_type::value) + { + constexpr std::size_t size = sdo::helpers::wstring_size::size; + + if (value.value.size() > size) + { + return SerializeResult::err({ + ErrorCode::InvalidSize, "serialize>: string is longer than size T"}); + } + + std::vector blob; + blob.reserve(size * 2); + + for (char16_t c : value.value) + { + std::uint16_t raw = static_cast(c); + + blob.push_back(static_cast(raw & 0xFF)); + blob.push_back(static_cast((raw >> 8) & 0xFF)); + } + + blob.resize(size * 2, '\0'); + + return SerializeResult::ok(std::move(blob)); + } + else if constexpr (sdo::helpers::is_string_type::value) + { + + constexpr std::size_t size = sdo::helpers::string_size::size; + + if (value.value.size() > size){ + return SerializeResult::err({ + ErrorCode::InvalidSize, std::string("serialize>: string is longer than size T. Expected: ") + + std::to_string(size) + ", got: " + std::to_string(value.value.size())}); + } + + std::vector blob(value.value.begin(), value.value.end()); + blob.resize(size, '\0'); + + return SerializeResult::ok(blob); + } + else{ + static_assert(sdo::helpers::always_false, "serialize: unsupported type"); + } + return{}; + } + + // -Boolean- + + template <> [[nodiscard]] inline SerializeResult serialize(const bool& value) + { + return SerializeResult::ok({static_cast(value ? 0xFF : 0)}); + } + + // -Signed Integer- + + template <> [[nodiscard]] inline SerializeResult serialize(const std::int8_t& value) + { + return SerializeResult::ok({static_cast(value)}); + } + + template <> [[nodiscard]] inline SerializeResult serialize(const std::int16_t& value) + { + return sdo::helpers::from_raw(static_cast(value), 2); + } + + template <> [[nodiscard]] inline SerializeResult serialize(const std::int32_t& value) + { + return sdo::helpers::from_raw(static_cast(value), 4); + } + + // -Unsigned Integer / raw data / bit arrays / bit strings- + + // use for UNSIGNED8, BYTE, BITARR8, BIT1-BIT8 + template <> [[nodiscard]] inline SerializeResult serialize(const std::uint8_t& value) + { + return SerializeResult::ok({value}); + } + + // use for UNSIGNED16, WORD, BITARR16, BIT9-BIT16 + template <> [[nodiscard]] inline SerializeResult serialize(const std::uint16_t& value) + { + return sdo::helpers::from_raw(value, 2); + } + + // use for UNSIGNED32, DWORD, BITARR32 + + template <> [[nodiscard]] inline SerializeResult serialize(const std::uint32_t& value) + { + return sdo::helpers::from_raw(value, 4); + } + + // -Floating Point- + + template <> [[nodiscard]] inline SerializeResult serialize(const float& value) + { + std::uint32_t raw; + std::memcpy(&raw, &value, sizeof(raw)); + + return sdo::helpers::from_raw(raw, 4); + } + + template <> [[nodiscard]] inline SerializeResult serialize(const double& value) + { + std::uint64_t raw; + std::memcpy(&raw, &value, sizeof(raw)); + + return sdo::helpers::from_raw(raw, 8); + } + + // -Time- + + template <> [[nodiscard]] inline SerializeResult serialize(const TimeOfDay& value) + { + return SerializeResult::ok({ + static_cast(value.ms_since_midnight & 0xFF), + static_cast((value.ms_since_midnight >> 8) & 0xFF), + static_cast((value.ms_since_midnight >> 16) & 0xFF), + static_cast((value.ms_since_midnight >> 24) & 0xFF), + + static_cast(value.d_since_1984_01_01 & 0xFF), + static_cast((value.d_since_1984_01_01 >> 8) & 0xFF) + }); + } + + template <> [[nodiscard]] inline SerializeResult serialize(const TimeDifference& value) + { + if (value.ms > 0x0FFFFFFF) + { + SerializeResult::err({ + ErrorCode::OutOfRange, + "serialize: TimeDifference.ms exceeds 28 bit range"}); + } + + return SerializeResult::ok({ + static_cast(value.ms & 0xFF), + static_cast((value.ms >> 8) & 0xFF), + static_cast((value.ms >> 16) & 0xFF), + static_cast((value.ms >> 24) & 0xFF), + + static_cast(value.d & 0xFF), + static_cast((value.d >> 8) & 0xFF) + }); + } + + // -Domain- + // (returns raw blob as equivalent to the EtherCAT Domain data type) + + template <> [[nodiscard]] inline SerializeResult serialize>( + const std::vector& value) + { + return SerializeResult::ok(value); + } + + // -Extended Signed Integer- + + template <> [[nodiscard]] inline SerializeResult serialize(const Int24& value) + { + if (value.value < -pow(2, 23) || value.value > pow(2, 23)-1){ + SerializeResult::err({ErrorCode::OutOfRange, "Int24 value exceeds 24 bit signed range"}); + } + + const auto raw = static_cast(value.value); + + return sdo::helpers::from_raw(raw, 3); + } + + template <> [[nodiscard]] inline SerializeResult serialize(const Int40& value) + { + if (value.value < -pow(2, 39) || value.value > pow(2, 39)-1){ + SerializeResult::err({ErrorCode::OutOfRange, "Int40 value exceeds 40 bit signed range"}); + } + + const auto raw = static_cast(value.value); + + return sdo::helpers::from_raw(raw, 5); + } + + template <> [[nodiscard]] inline SerializeResult serialize(const Int48& value) + { + if (value.value < -pow(2, 47) || value.value > pow(2, 47)-1){ + SerializeResult::err({ErrorCode::OutOfRange, "Int48 value exceeds 48 bit signed range"}); + } + + const auto raw = static_cast(value.value); + + return sdo::helpers::from_raw(raw, 6); + } + + template <> [[nodiscard]] inline SerializeResult serialize(const Int56& value) + { + if (value.value < -pow(2, 55) || value.value > pow(2, 55)-1){ + SerializeResult::err({ErrorCode::OutOfRange, "Int56 value exceeds 56 bit signed range"}); + } + + const auto raw = static_cast(value.value); + + return sdo::helpers::from_raw(raw, 7); + } + + template <> [[nodiscard]] inline SerializeResult serialize(const int64_t& value) + { + return sdo::helpers::from_raw(static_cast(value), 8); + } + + // -Extended unsigned Integer- + + template <> [[nodiscard]] inline SerializeResult serialize(const UInt24& value) + { + if (value.value > pow(2, 24)-1){ + SerializeResult::err({ + ErrorCode::OutOfRange, "UInt24 value exceeds 24 bit unsigned range"}); + } + + return sdo::helpers::from_raw(value.value, 3); + } + + template <> [[nodiscard]] inline SerializeResult serialize(const UInt40& value) + { + if (value.value > pow(2, 40)-1){ + SerializeResult::err({ + ErrorCode::OutOfRange, "UInt40 value exceeds 40 bit unsigned range"}); + } + + return sdo::helpers::from_raw(value.value, 5); + } + + template <> [[nodiscard]] inline SerializeResult serialize(const UInt48& value) + { + if (value.value > pow(2, 48)-1){ + SerializeResult::err({ + ErrorCode::OutOfRange, "UInt48 value exceeds 48 bit unsigned range"}); + } + + return sdo::helpers::from_raw(value.value, 6); + } + + template <> [[nodiscard]] inline SerializeResult serialize(const UInt56& value) + { + if (value.value > pow(2, 56)-1){ + SerializeResult::err({ + ErrorCode::OutOfRange, "UInt56 value exceeds 56 bit unsigned range"}); + } + + return sdo::helpers::from_raw(value.value, 7); + } + + template <> [[nodiscard]] inline SerializeResult serialize(const uint64_t& value) + { + return sdo::helpers::from_raw(value, 8); + } + + // -GUID- + + template <> [[nodiscard]] inline SerializeResult serialize(const Guid& value) + { + return SerializeResult::ok(std::vector(value.bytes.begin(), value.bytes.end())); + } +} diff --git a/rise_motion_dev_ws/src/rise_motion/package.xml b/rise_motion_dev_ws/src/rise_motion/package.xml index 9c5ac6c..e7034d0 100644 --- a/rise_motion_dev_ws/src/rise_motion/package.xml +++ b/rise_motion_dev_ws/src/rise_motion/package.xml @@ -8,6 +8,9 @@ TODO: License declaration ament_cmake + + ament_cmake_python + ament_cmake_pytest rclcpp rise_motion_messages diff --git a/rise_motion_dev_ws/src/rise_motion/src/ec_manager.cpp b/rise_motion_dev_ws/src/rise_motion/src/ec_manager.cpp index 8b8cfd6..6a63faa 100755 --- a/rise_motion_dev_ws/src/rise_motion/src/ec_manager.cpp +++ b/rise_motion_dev_ws/src/rise_motion/src/ec_manager.cpp @@ -309,8 +309,8 @@ uint16 ECManager::transition_ec(uint16 state) { } bool ECManager::sdo_read(uint16 device_id, uint16 index, uint8 subindex, - std::vector &value) { - int psize = 64; + std::vector &value, uint8 value_size) { + int psize = value_size; uint8 *buf = new uint8[psize]; boolean CA = FALSE; @@ -329,7 +329,7 @@ bool ECManager::sdo_read(uint16 device_id, uint16 index, uint8 subindex, bool ECManager::sdo_write(uint16 device_id, uint16 index, uint8 subindex, std::vector &value) { int psize = value.size(); - uint8 *buf = new uint8[psize]; + uint8 *buf = &value[0]; boolean CA = FALSE; int wkc = ecx_SDOwrite(&ctx, device_id, index, subindex, CA, psize, diff --git a/rise_motion_dev_ws/src/rise_motion/src/ethercat_node.cpp b/rise_motion_dev_ws/src/rise_motion/src/ethercat_node.cpp index 89bad70..b67968c 100755 --- a/rise_motion_dev_ws/src/rise_motion/src/ethercat_node.cpp +++ b/rise_motion_dev_ws/src/rise_motion/src/ethercat_node.cpp @@ -118,7 +118,7 @@ void EthercatNode::sdoReadServiceCallback( std::vector value; bool success = ec_manager_.sdo_read(request->device_id, request->index, - request->subindex, value); + request->subindex, value, request->value_size); if (!success) { RCLCPP_WARN(get_logger(), "Read failed"); @@ -131,7 +131,7 @@ void EthercatNode::sdoReadServiceCallback( response->index = request->index; response->subindex = request->subindex; response->value = value; - response->value_type = 0; + response->value_type = request->value_type; } void EthercatNode::sdoWriteServiceCallback( const std::shared_ptr @@ -156,5 +156,5 @@ void EthercatNode::sdoWriteServiceCallback( response->device_id = request->device_id; response->index = request->index; response->subindex = request->subindex; - response->value_type = 0; + response->value_type = request->value_type; } diff --git a/rise_motion_dev_ws/src/rise_motion/src/testing_node.cpp b/rise_motion_dev_ws/src/rise_motion/src/testing_node.cpp index d7c51e7..4059f98 100755 --- a/rise_motion_dev_ws/src/rise_motion/src/testing_node.cpp +++ b/rise_motion_dev_ws/src/rise_motion/src/testing_node.cpp @@ -2,11 +2,15 @@ #include #include #include +#include #include #include #include #include #include +#include +#include +#include class TestNode : public rclcpp::Node { @@ -73,6 +77,77 @@ class TestNode : public rclcpp::Node { return result->status_enable; } + template auto sdo_read( + uint16_t device_id, uint16_t index, uint8_t subindex, uint8_t value_size, uint8_t value_type = 0) + { + auto client = this->create_client("sdo_read"); + + while (!client->wait_for_service(std::chrono::seconds(1))) { + if (!rclcpp::ok()) { + return rise::Result::err({ + sdo::ErrorCode::ServiceUnavailable, "Interrupted while waiting for sdo_read service"}); + } + } + + auto request = std::make_shared(); + + request->device_id = device_id; + request->index = index; + request->subindex = subindex; + request->value_size = value_size; + request->value_type = value_type; + + auto future = client->async_send_request(request); + + if (rclcpp::spin_until_future_complete( + this->get_node_base_interface(), future) != rclcpp::FutureReturnCode::SUCCESS){ + client->remove_pending_request(future); + return rise::Result::err({ + sdo::ErrorCode::CallFailed, "Failed to call sdo_read service"}); + } + + return sdo::deserialize(future.get()->value); + } + + template auto sdo_write( + uint16_t device_id, uint16_t index, uint8_t subindex, const T &value, uint8_t value_type = 0) +{ + auto serialized = sdo::serialize(value); + + if (!serialized){ + return rise::Result::err(serialized.error); + } + + auto client = this->create_client("sdo_write"); + + while (!client->wait_for_service(std::chrono::seconds(1))){ + if (!rclcpp::ok()){ + return rise::Result::err({ + sdo::ErrorCode::ServiceUnavailable, "Interrupted while waiting for sdo_write service"}); + } + } + + auto request = std::make_shared(); + + request->device_id = device_id; + request->index = index; + request->subindex = subindex; + request->value_type = value_type; + request->value = serialized.value; + + auto future = client->async_send_request(request); + + if (rclcpp::spin_until_future_complete(this->get_node_base_interface(), future) != rclcpp::FutureReturnCode::SUCCESS){ + client->remove_pending_request(future); + + return rise::Result::err({ + sdo::ErrorCode::CallFailed, "Failed to call sdo_write service"}); + } + + return rise::Result::ok(true); +} + + private: std::vector motor_pos; void print_motor_positions(std::vector const &motor_pos, @@ -113,6 +188,16 @@ int main(int argc, char **argv) { while (!node->request_enable_ethercat()) { } + + // example sdo read + auto result = node->sdo_read>(1, 0x1008, 0, 0, 50); + if(!result){ + RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), result.error.message.std::string::c_str()); + } + else{ + RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "sdo_value: %s", (std::string(result.value).std::string::c_str())); + } + rclcpp::spin(node); rclcpp::shutdown(); return 0; diff --git a/rise_motion_dev_ws/src/rise_motion/test/test_sdo_serializer.cpp b/rise_motion_dev_ws/src/rise_motion/test/test_sdo_serializer.cpp new file mode 100644 index 0000000..922d238 --- /dev/null +++ b/rise_motion_dev_ws/src/rise_motion/test/test_sdo_serializer.cpp @@ -0,0 +1,562 @@ +#include + +#include "rise_motion/sdo_serializer.hpp" + +TEST(SDOSerializationTest, Bool) +{ + bool value = true; + + auto blob = sdo::serialize(value); + ASSERT_TRUE(blob); + auto result = sdo::deserialize(blob.value); + ASSERT_TRUE(result); + + ASSERT_EQ(result.value, value); +} + +TEST(SDOSerializationTest, Int8) +{ + std::int8_t value_zero = 0; + std::int8_t value_max = std::numeric_limits::max(); + std::int8_t value_min = std::numeric_limits::min(); + + auto blob = sdo::serialize(value_zero); + ASSERT_TRUE(blob); + auto result_zero = sdo::deserialize(blob.value); + ASSERT_TRUE(result_zero); + ASSERT_EQ(result_zero.value, value_zero); + + blob = sdo::serialize(value_max); + ASSERT_TRUE(blob); + auto result_max = sdo::deserialize(blob.value); + ASSERT_TRUE(result_max); + ASSERT_EQ(result_max.value, value_max); + + blob = sdo::serialize(value_min); + ASSERT_TRUE(blob); + auto result_min = sdo::deserialize(blob.value); + ASSERT_TRUE(result_min); + ASSERT_EQ(result_min.value, value_min); +} + +TEST(SDOSerializationTest, Int16) +{ + std::int16_t value_zero = 0; + std::int16_t value_max = std::numeric_limits::max(); + std::int16_t value_min = std::numeric_limits::min(); + + auto blob = sdo::serialize(value_zero); + ASSERT_TRUE(blob); + auto result_zero = sdo::deserialize(blob.value); + ASSERT_TRUE(result_zero); + ASSERT_EQ(result_zero.value, value_zero); + + blob = sdo::serialize(value_max); + ASSERT_TRUE(blob); + auto result_max = sdo::deserialize(blob.value); + ASSERT_TRUE(result_max); + ASSERT_EQ(result_max.value, value_max); + + blob = sdo::serialize(value_min); + ASSERT_TRUE(blob); + auto result_min = sdo::deserialize(blob.value); + ASSERT_TRUE(result_min); + ASSERT_EQ(result_min.value, value_min); +} + +TEST(SDOSerializationTest, Int32) +{ + std::int32_t value_zero = 0; + std::int32_t value_max = std::numeric_limits::max(); + std::int32_t value_min = std::numeric_limits::min(); + + auto blob = sdo::serialize(value_zero); + ASSERT_TRUE(blob); + auto result_zero = sdo::deserialize(blob.value); + ASSERT_TRUE(result_zero); + ASSERT_EQ(result_zero.value, value_zero); + + blob = sdo::serialize(value_max); + ASSERT_TRUE(blob); + auto result_max = sdo::deserialize(blob.value); + ASSERT_TRUE(result_max); + ASSERT_EQ(result_max.value, value_max); + + blob = sdo::serialize(value_min); + ASSERT_TRUE(blob); + auto result_min = sdo::deserialize(blob.value); + ASSERT_TRUE(result_min); + ASSERT_EQ(result_min.value, value_min); +} + +TEST(SDOSerializationTest, uInt8) +{ + std::uint8_t value_zero = 0; + std::uint8_t value_max = std::numeric_limits::max(); + + auto blob = sdo::serialize(value_zero); + ASSERT_TRUE(blob); + auto result_zero = sdo::deserialize(blob.value); + ASSERT_TRUE(result_zero); + ASSERT_EQ(result_zero.value, value_zero); + + blob = sdo::serialize(value_max); + ASSERT_TRUE(blob); + auto result_max = sdo::deserialize(blob.value); + ASSERT_TRUE(result_max); + ASSERT_EQ(result_max.value, value_max); +} + +TEST(SDOSerializationTest, uInt16) +{ + std::uint16_t value_zero = 0; + std::uint16_t value_max = std::numeric_limits::max(); + + auto blob = sdo::serialize(value_zero); + ASSERT_TRUE(blob); + auto result_zero = sdo::deserialize(blob.value); + ASSERT_TRUE(result_zero); + ASSERT_EQ(result_zero.value, value_zero); + + blob = sdo::serialize(value_max); + ASSERT_TRUE(blob); + auto result_max = sdo::deserialize(blob.value); + ASSERT_TRUE(result_max); + ASSERT_EQ(result_max.value, value_max); +} + +TEST(SDOSerializationTest, uInt32) +{ + std::uint32_t value_zero = 0; + std::uint32_t value_max = std::numeric_limits::max(); + + auto blob = sdo::serialize(value_zero); + ASSERT_TRUE(blob); + auto result_zero = sdo::deserialize(blob.value); + ASSERT_TRUE(result_zero); + ASSERT_EQ(result_zero.value, value_zero); + + blob = sdo::serialize(value_max); + ASSERT_TRUE(blob); + auto result_max = sdo::deserialize(blob.value); + ASSERT_TRUE(result_max); + ASSERT_EQ(result_max.value, value_max); +} + +TEST(SDOSerializationTest, Float) +{ + float value_zero = 0.0f; + float value_pos = 123.456f; + float value_neg = -123.456f; + + auto blob = sdo::serialize(value_zero); + ASSERT_TRUE(blob); + auto result_zero = sdo::deserialize(blob.value); + ASSERT_TRUE(result_zero); + ASSERT_EQ(result_zero.value, value_zero); + + blob = sdo::serialize(value_pos); + ASSERT_TRUE(blob); + auto result_pos = sdo::deserialize(blob.value); + ASSERT_TRUE(result_pos); + ASSERT_EQ(result_pos.value, value_pos); + + blob = sdo::serialize(value_neg); + ASSERT_TRUE(blob); + auto result_neg = sdo::deserialize(blob.value); + ASSERT_TRUE(result_neg); + ASSERT_EQ(result_neg.value, value_neg); +} + +TEST(SDOSerializationTest, Double) +{ + double value_zero = 0.0; + double value_pos = 123.456; + double value_neg = -123.456; + + auto blob = sdo::serialize(value_zero); + ASSERT_TRUE(blob); + auto result_zero = sdo::deserialize(blob.value); + ASSERT_TRUE(result_zero); + ASSERT_EQ(result_zero.value, value_zero); + + blob = sdo::serialize(value_pos); + ASSERT_TRUE(blob); + auto result_pos = sdo::deserialize(blob.value); + ASSERT_TRUE(result_pos); + ASSERT_EQ(result_pos.value, value_pos); + + blob = sdo::serialize(value_neg); + ASSERT_TRUE(blob); + auto result_neg = sdo::deserialize(blob.value); + ASSERT_TRUE(result_neg); + ASSERT_EQ(result_neg.value, value_neg); +} + +TEST(SDOSerializationTest, TimeOfDay) +{ + sdo::TimeOfDay value{12345678, 42}; + + auto blob = sdo::serialize(value); + ASSERT_TRUE(blob); + auto result = sdo::deserialize(blob.value); + ASSERT_TRUE(result); + ASSERT_EQ(result.value.ms_since_midnight, value.ms_since_midnight); + ASSERT_EQ(result.value.d_since_1984_01_01, value.d_since_1984_01_01); +} + +TEST(SDOSerializationTest, TimeDifference) +{ + sdo::TimeDifference value{12345678, 42}; + + auto blob = sdo::serialize(value); + ASSERT_TRUE(blob); + auto result = sdo::deserialize(blob.value); + ASSERT_TRUE(result); + ASSERT_EQ(result.value.ms, value.ms); + ASSERT_EQ(result.value.d, value.d); +} + +TEST(SDOSerializationTest, Domain) +{ + std::vector value = {0, 1, 2, 3}; + + auto blob = sdo::serialize>(value); + ASSERT_TRUE(blob); + auto result = sdo::deserialize>(blob.value); + ASSERT_TRUE(result); + ASSERT_EQ(result.value, value); +} + +TEST(SDOSerializationTest, Int24) +{ + std::int32_t value_zero = 0; + sdo::Int24 value_max{(std::int64_t{1} << 23) - 1}; + sdo::Int24 value_min{-(std::int64_t{1} << 23)}; + + auto blob = sdo::serialize(value_zero); + ASSERT_TRUE(blob); + auto result_zero = sdo::deserialize(blob.value); + ASSERT_TRUE(result_zero); + ASSERT_EQ(result_zero.value, value_zero); + + blob = sdo::serialize(value_max); + ASSERT_TRUE(blob); + auto result_max = sdo::deserialize(blob.value); + ASSERT_TRUE(result_max); + ASSERT_EQ(result_max.value.value, value_max.value); + + blob = sdo::serialize(value_min); + ASSERT_TRUE(blob); + auto result_min = sdo::deserialize(blob.value); + ASSERT_TRUE(result_min); + ASSERT_EQ(result_min.value.value, value_min.value); +} + +TEST(SDOSerializationTest, Int40) +{ + sdo::Int40 value_zero{0}; + sdo::Int40 value_max{(std::int64_t{1} << 39) - 1}; + sdo::Int40 value_min{-(std::int64_t{1} << 39)}; + + auto blob = sdo::serialize(value_zero); + ASSERT_TRUE(blob); + auto result_zero = sdo::deserialize(blob.value); + ASSERT_TRUE(result_zero); + ASSERT_EQ(result_zero.value.value, value_zero.value); + + blob = sdo::serialize(value_max); + ASSERT_TRUE(blob); + auto result_max = sdo::deserialize(blob.value); + ASSERT_TRUE(result_max); + ASSERT_EQ(result_max.value.value, value_max.value); + + blob = sdo::serialize(value_min); + ASSERT_TRUE(blob); + auto result_min = sdo::deserialize(blob.value); + ASSERT_TRUE(result_min); + ASSERT_EQ(result_min.value.value, value_min.value); +} + +TEST(SDOSerializationTest, Int48) +{ + sdo::Int48 value_zero{0}; + sdo::Int48 value_max{(std::int64_t{1} << 47) - 1}; + sdo::Int48 value_min{-(std::int64_t{1} << 47)}; + + auto blob = sdo::serialize(value_zero); + ASSERT_TRUE(blob); + auto result_zero = sdo::deserialize(blob.value); + ASSERT_TRUE(result_zero); + ASSERT_EQ(result_zero.value.value, value_zero.value); + + blob = sdo::serialize(value_max); + ASSERT_TRUE(blob); + auto result_max = sdo::deserialize(blob.value); + ASSERT_TRUE(result_max); + ASSERT_EQ(result_max.value.value, value_max.value); + + blob = sdo::serialize(value_min); + ASSERT_TRUE(blob); + auto result_min = sdo::deserialize(blob.value); + ASSERT_TRUE(result_min); + ASSERT_EQ(result_min.value.value, value_min.value); +} + +TEST(SDOSerializationTest, Int56) +{ + sdo::Int56 value_zero{0}; + sdo::Int56 value_max{(std::int64_t{1} << 55) - 1}; + sdo::Int56 value_min{-(std::int64_t{1} << 55)}; + + auto blob = sdo::serialize(value_zero); + ASSERT_TRUE(blob); + auto result_zero = sdo::deserialize(blob.value); + ASSERT_TRUE(result_zero); + ASSERT_EQ(result_zero.value.value, value_zero.value); + + blob = sdo::serialize(value_max); + ASSERT_TRUE(blob); + auto result_max = sdo::deserialize(blob.value); + ASSERT_TRUE(result_max); + ASSERT_EQ(result_max.value.value, value_max.value); + + blob = sdo::serialize(value_min); + ASSERT_TRUE(blob); + auto result_min = sdo::deserialize(blob.value); + ASSERT_TRUE(result_min); + ASSERT_EQ(result_min.value.value, value_min.value); +} + +TEST(SDOSerializationTest, Int64) +{ + std::int64_t value_zero = 0; + std::int64_t value_max = std::numeric_limits::max(); + std::int64_t value_min = std::numeric_limits::min(); + + auto blob = sdo::serialize(value_zero); + ASSERT_TRUE(blob); + auto result_zero = sdo::deserialize(blob.value); + ASSERT_TRUE(result_zero); + ASSERT_EQ(result_zero.value, value_zero); + + blob = sdo::serialize(value_max); + ASSERT_TRUE(blob); + auto result_max = sdo::deserialize(blob.value); + ASSERT_TRUE(result_max); + ASSERT_EQ(result_max.value, value_max); + + blob = sdo::serialize(value_min); + ASSERT_TRUE(blob); + auto result_min = sdo::deserialize(blob.value); + ASSERT_TRUE(result_min); + ASSERT_EQ(result_min.value, value_min); +} + +TEST(SDOSerializationTest, uInt24) +{ + sdo::UInt24 value_zero{0}; + sdo::UInt24 value_max{(std::uint64_t{1} << 23) - 1}; + + auto blob = sdo::serialize(value_zero); + ASSERT_TRUE(blob); + auto result_zero = sdo::deserialize(blob.value); + ASSERT_TRUE(result_zero); + ASSERT_EQ(result_zero.value.value, value_zero.value); + + blob = sdo::serialize(value_max); + ASSERT_TRUE(blob); + auto result_max = sdo::deserialize(blob.value); + ASSERT_TRUE(result_max); + ASSERT_EQ(result_max.value.value, value_max.value); +} + +TEST(SDOSerializationTest, uInt40) +{ + sdo::UInt40 value_zero{0}; + sdo::UInt40 value_max{(std::uint64_t{1} << 40) - 1}; + + auto blob = sdo::serialize(value_zero); + ASSERT_TRUE(blob); + auto result_zero = sdo::deserialize(blob.value); + ASSERT_TRUE(result_zero); + ASSERT_EQ(result_zero.value.value, value_zero.value); + + blob = sdo::serialize(value_max); + ASSERT_TRUE(blob); + auto result_max = sdo::deserialize(blob.value); + ASSERT_TRUE(result_max); + ASSERT_EQ(result_max.value.value, value_max.value); +} + +TEST(SDOSerializationTest, uInt48) +{ + sdo::UInt48 value_zero{0}; + sdo::UInt48 value_max{(std::uint64_t{1} << 48) - 1}; + + auto blob = sdo::serialize(value_zero); + ASSERT_TRUE(blob); + auto result_zero = sdo::deserialize(blob.value); + ASSERT_TRUE(result_zero); + ASSERT_EQ(result_zero.value.value, value_zero.value); + + blob = sdo::serialize(value_max); + ASSERT_TRUE(blob); + auto result_max = sdo::deserialize(blob.value); + ASSERT_TRUE(result_max); + ASSERT_EQ(result_max.value.value, value_max.value); +} + +TEST(SDOSerializationTest, uInt56) +{ + sdo::UInt56 value_zero{0}; + sdo::UInt56 value_max{(std::uint64_t{1} << 56) - 1}; + + auto blob = sdo::serialize(value_zero); + ASSERT_TRUE(blob); + auto result_zero = sdo::deserialize(blob.value); + ASSERT_TRUE(result_zero); + ASSERT_EQ(result_zero.value.value, value_zero.value); + + blob = sdo::serialize(value_max); + ASSERT_TRUE(blob); + auto result_max = sdo::deserialize(blob.value); + ASSERT_TRUE(result_max); + ASSERT_EQ(result_max.value.value, value_max.value); +} + +TEST(SDOSerializationTest, uInt64) +{ + std::uint64_t value_zero = 0; + std::uint64_t value_max = std::numeric_limits::max(); + + auto blob = sdo::serialize(value_zero); + ASSERT_TRUE(blob); + auto result_zero = sdo::deserialize(blob.value); + ASSERT_TRUE(result_zero); + ASSERT_EQ(result_zero.value, value_zero); + + blob = sdo::serialize(value_max); + ASSERT_TRUE(blob); + auto result_max = sdo::deserialize(blob.value); + ASSERT_TRUE(result_max); + ASSERT_EQ(result_max.value, value_max); +} + +TEST(SDOSerializationTest, Guid) +{ + sdo::Guid value{{ + 1, 2, 3, 4, + 5, 6, 7, 8, + 9, 10, 11, 12, + 13, 14, 15, 16 + }}; + + auto blob = sdo::serialize(value); + ASSERT_TRUE(blob); + auto result = sdo::deserialize(blob.value); + ASSERT_TRUE(result); + ASSERT_EQ(result.value.bytes, value.bytes); +} + +TEST(SDOSerializationTest, String50) +{ + std::string str = "RISE"; + + auto blob = sdo::serialize>(str); + ASSERT_TRUE(blob); + + ASSERT_EQ(blob.value.size(), 50); + + ASSERT_EQ(blob.value[0], static_cast('R')); + ASSERT_EQ(blob.value[1], static_cast('I')); + ASSERT_EQ(blob.value[2], static_cast('S')); + ASSERT_EQ(blob.value[3], static_cast('E')); + + for (std::size_t i = str.size(); i < blob.value.size(); ++i) + { + ASSERT_EQ(blob.value[i], 0); + } + + auto result = sdo::deserialize>(blob.value); + ASSERT_TRUE(result); + + ASSERT_EQ(std::string(result.value), str); +} + +TEST(SDOSerializationTest, WString50) +{ + std::u16string str = u"RISE"; + + auto blob = sdo::serialize>(str); + ASSERT_TRUE(blob); + + ASSERT_EQ(blob.value.size(), 100); + + ASSERT_EQ(blob.value[0], static_cast('R')); + ASSERT_EQ(blob.value[1], 0); + ASSERT_EQ(blob.value[2], static_cast('I')); + ASSERT_EQ(blob.value[3], 0); + ASSERT_EQ(blob.value[4], static_cast('S')); + ASSERT_EQ(blob.value[5], 0); + ASSERT_EQ(blob.value[6], static_cast('E')); + ASSERT_EQ(blob.value[7], 0); + + for (std::size_t i = str.size() * 2; i < blob.value.size(); ++i) + { + ASSERT_EQ(blob.value[i], 0); + } + + auto result = sdo::deserialize>(blob.value); + ASSERT_TRUE(result); + + ASSERT_EQ(std::u16string(result.value), str); +} + +TEST(SDOSerializationTest, ArrayUInt16) +{ + sdo::ARRAY_OF_BITARR16<5> value{{1, 2, 3, 4, 5}}; + + auto blob = sdo::serialize>(value); + ASSERT_TRUE(blob); + + ASSERT_EQ(blob.value.size(), 10); + + ASSERT_EQ(blob.value[0], 1); + ASSERT_EQ(blob.value[1], 0); + ASSERT_EQ(blob.value[2], 2); + ASSERT_EQ(blob.value[3], 0); + ASSERT_EQ(blob.value[4], 3); + ASSERT_EQ(blob.value[5], 0); + ASSERT_EQ(blob.value[6], 4); + ASSERT_EQ(blob.value[7], 0); + ASSERT_EQ(blob.value[8], 5); + ASSERT_EQ(blob.value[9], 0); + + auto result = sdo::deserialize>(blob.value); + ASSERT_TRUE(result); + + ASSERT_EQ(result.value.value, value.value); +} + +TEST(SDOSerializationTest, ArrayInt16) +{ + sdo::ARRAY_OF_INT<3> value{{0, 123, -1}}; + + auto blob = sdo::serialize>(value); + ASSERT_TRUE(blob); + + ASSERT_EQ(blob.value.size(), 6); + + ASSERT_EQ(blob.value[0], 0); + ASSERT_EQ(blob.value[1], 0); + ASSERT_EQ(blob.value[2], 123); + ASSERT_EQ(blob.value[3], 0); + ASSERT_EQ(blob.value[4], 0xFF); + ASSERT_EQ(blob.value[5], 0xFF); + + auto result = sdo::deserialize>(blob.value); + ASSERT_TRUE(result); + + ASSERT_EQ(result.value.value, value.value); +} diff --git a/rise_motion_dev_ws/src/rise_motion_messages/srv/SDOReadSrv.srv b/rise_motion_dev_ws/src/rise_motion_messages/srv/SDOReadSrv.srv index e36b472..e3b9138 100644 --- a/rise_motion_dev_ws/src/rise_motion_messages/srv/SDOReadSrv.srv +++ b/rise_motion_dev_ws/src/rise_motion_messages/srv/SDOReadSrv.srv @@ -2,6 +2,7 @@ uint16 device_id uint16 index uint8 subindex uint8 value_type +uint8 value_size --- int8 status_code uint16 device_id