diff --git a/examples/somersault.pdx b/examples/somersault.pdx index 61e12398..9badca44 100644 Binary files a/examples/somersault.pdx and b/examples/somersault.pdx differ diff --git a/examples/somersaultecu.py b/examples/somersaultecu.py index 2440f0ae..d80020cb 100755 --- a/examples/somersaultecu.py +++ b/examples/somersaultecu.py @@ -32,6 +32,7 @@ from odxtools.docrevision import DocRevision from odxtools.environmentdata import EnvironmentData from odxtools.environmentdatadescription import EnvironmentDataDescription +from odxtools.exceptions import odxrequire from odxtools.functionalclass import FunctionalClass from odxtools.modification import Modification from odxtools.multiplexer import Multiplexer @@ -669,21 +670,22 @@ class SomersaultSID(IntEnum): long_name=None, semantic=None, description=None, - diag_coded_type=somersault_diagcodedtypes["uint16"], + diag_coded_type=somersault_diagcodedtypes["uint8"], byte_position=0, coded_value=uds.positive_response_id( SID.TesterPresent.value), # type: ignore[attr-defined] bit_position=None, sdgs=[], ), - CodedConstParameter( + ValueParameter( short_name="status", long_name=None, semantic=None, description=None, - diag_coded_type=somersault_diagcodedtypes["uint8"], + dop_ref=OdxLinkRef("somersault.DOP.uint8", doc_frags), + dop_snref=None, + physical_default_value_raw="0", byte_position=1, - coded_value=0x00, bit_position=None, sdgs=[], ), @@ -941,6 +943,17 @@ class SomersaultSID(IntEnum): ), } +# this is a hack to get around a catch-22: we need to specify the +# value of a positive response to the tester present parameter to +# specify ISO_15765_3.CP_TesterPresentMessage communication parameter, +# but we need the comparam for the raw diaglayer which we need for +# retrieving the DOP of the "status" parameter in order to convert the +# raw physical default value. +param = somersault_positive_responses["tester_ok"].parameters.status +assert isinstance(param, ValueParameter) +param._dop = somersault_dops["uint8"] +param._physical_default_value = int(odxrequire(param.physical_default_value_raw)) + # negative responses somersault_negative_responses = { "general": diff --git a/odxtools/basicstructure.py b/odxtools/basicstructure.py index f558a00d..335ddbb7 100644 --- a/odxtools/basicstructure.py +++ b/odxtools/basicstructure.py @@ -208,7 +208,9 @@ def encode_into_pdu(self, physical_value: Optional[ParameterValue], # position directly after the structure and let # EncodeState add the padding as needed. encode_state.cursor_byte_position = encode_state.origin_byte_position + self.byte_size - encode_state.emplace_bytes(b'', "") + # Padding bytes needed. these count as "used". + encode_state.coded_message += b"\x00" * (self.byte_size - actual_len) + encode_state.used_mask += b"\xff" * (self.byte_size - actual_len) # encode the length- and table keys. This cannot be done above # because we allow these to be defined implicitly (i.e. they diff --git a/odxtools/dataobjectproperty.py b/odxtools/dataobjectproperty.py index 3dceaa76..608f39e2 100644 --- a/odxtools/dataobjectproperty.py +++ b/odxtools/dataobjectproperty.py @@ -129,8 +129,8 @@ def encode_into_pdu(self, physical_value: ParameterValue, encode_state: EncodeSt f"The value {repr(physical_value)} of type {type(physical_value).__name__}" f" is not a valid.") - internal_val = self.convert_physical_to_internal(physical_value) - self.diag_coded_type.encode_into_pdu(internal_val, encode_state) + internal_value = self.convert_physical_to_internal(physical_value) + self.diag_coded_type.encode_into_pdu(internal_value, encode_state) def decode_from_pdu(self, decode_state: DecodeState) -> ParameterValue: """ diff --git a/odxtools/encodestate.py b/odxtools/encodestate.py index 1ba58d21..dba83d54 100644 --- a/odxtools/encodestate.py +++ b/odxtools/encodestate.py @@ -18,7 +18,10 @@ class EncodeState: """ #: payload that has been constructed so far - coded_message: bytearray + coded_message: bytearray = field(default_factory=bytearray) + + #: the bits of the payload that are used + used_mask: bytearray = field(default_factory=bytearray) #: The absolute position in bytes from the beginning of the PDU to #: which relative positions refer to, e.g., the beginning of the @@ -53,6 +56,18 @@ class EncodeState: #: (needed for MinMaxLengthType, EndOfPduField, etc.) is_end_of_pdu: bool = True + def __post_init__(self) -> None: + # if a coded message has been specified, but no used_mask, we + # assume that all of the bits of the coded message are + # currently used. + if len(self.coded_message) > len(self.used_mask): + self.used_mask += b'\xff' * (len(self.coded_message) - len(self.used_mask)) + if len(self.coded_message) < len(self.used_mask): + odxraise(f"The specified bit mask 0x{self.used_mask.hex()} for used bits " + f"is not suitable for representing the coded_message " + f"0x{self.coded_message.hex()}") + self.used_mask = self.used_mask[:len(self.coded_message)] + def emplace_atomic_value( self, *, @@ -60,19 +75,22 @@ def emplace_atomic_value( bit_length: int, base_data_type: DataType, is_highlow_byte_order: bool, + used_mask: Optional[bytes], ) -> None: """Convert the internal_value to bytes and emplace this into the PDU""" + + raw_value: AtomicOdxType + # Check that bytes and strings actually fit into the bit length if base_data_type == DataType.A_BYTEFIELD: - if isinstance(internal_value, bytearray): - internal_value = bytes(internal_value) if not isinstance(internal_value, bytes): odxraise() if 8 * len(internal_value) > bit_length: raise EncodeError(f"The bytefield {internal_value.hex()} is too large " f"({len(internal_value)} bytes)." f" The maximum length is {bit_length//8}.") - if base_data_type == DataType.A_ASCIISTRING: + raw_value = internal_value + elif base_data_type == DataType.A_ASCIISTRING: if not isinstance(internal_value, str): odxraise() @@ -80,18 +98,18 @@ def emplace_atomic_value( # But in practice, vendors use iso-8859-1, aka latin-1 # reason being iso-8859-1 never fails since it has a valid # character mapping for every possible byte sequence. - internal_value = internal_value.encode("iso-8859-1") + raw_value = internal_value.encode("iso-8859-1") - if 8 * len(internal_value) > bit_length: + if 8 * len(raw_value) > bit_length: raise EncodeError(f"The string {repr(internal_value)} is too large." f" The maximum number of characters is {bit_length//8}.") elif base_data_type == DataType.A_UTF8STRING: if not isinstance(internal_value, str): odxraise() - internal_value = internal_value.encode("utf-8") + raw_value = internal_value.encode("utf-8") - if 8 * len(internal_value) > bit_length: + if 8 * len(raw_value) > bit_length: raise EncodeError(f"The string {repr(internal_value)} is too large." f" The maximum number of bytes is {bit_length//8}.") @@ -100,11 +118,13 @@ def emplace_atomic_value( odxraise() text_encoding = "utf-16-be" if is_highlow_byte_order else "utf-16-le" - internal_value = internal_value.encode(text_encoding) + raw_value = internal_value.encode(text_encoding) - if 8 * len(internal_value) > bit_length: + if 8 * len(raw_value) > bit_length: raise EncodeError(f"The string {repr(internal_value)} is too large." f" The maximum number of characters is {bit_length//16}.") + else: + raw_value = internal_value # If the bit length is zero, return empty bytes if bit_length == 0: @@ -125,46 +145,79 @@ def emplace_atomic_value( left_pad = f"p{padding}" if padding > 0 else "" # actually encode the value - coded = bitstruct.pack(f"{left_pad}{char}{bit_length}", internal_value) - - # apply byte order for numeric objects + coded = bitstruct.pack(f"{left_pad}{char}{bit_length}", raw_value) + + # create the raw mask of used bits for numeric objects + used_mask_raw = used_mask + if base_data_type in [DataType.A_INT32, DataType.A_UINT32 + ] and (self.cursor_bit_position != 0 or + (self.cursor_bit_position + bit_length) % 8 != 0): + if used_mask is None: + tmp = (1 << bit_length) - 1 + else: + tmp = int.from_bytes(used_mask, "big") + tmp <<= self.cursor_bit_position + + used_mask_raw = tmp.to_bytes((self.cursor_bit_position + bit_length + 7) // 8, "big") + + # apply byte order to numeric objects if not is_highlow_byte_order and base_data_type in [ - DataType.A_INT32, - DataType.A_UINT32, - DataType.A_FLOAT32, - DataType.A_FLOAT64, + DataType.A_INT32, DataType.A_UINT32, DataType.A_FLOAT32, DataType.A_FLOAT64 ]: coded = coded[::-1] - self.emplace_bytes(coded) + if used_mask_raw is not None: + used_mask_raw = used_mask_raw[::-1] + + self.cursor_bit_position = 0 + self.emplace_bytes(coded, obj_used_mask=used_mask_raw) + + def emplace_bytes(self, + new_data: bytes, + obj_name: Optional[str] = None, + obj_used_mask: Optional[bytes] = None) -> None: + if self.cursor_bit_position != 0: + odxraise("EncodeState.emplace_bytes can only be called " + "for a bit position of 0!", RuntimeError) - def emplace_bytes(self, new_data: bytes, obj_name: Optional[str] = None) -> None: pos = self.cursor_byte_position # Make blob longer if necessary min_length = pos + len(new_data) if len(self.coded_message) < min_length: - self.coded_message.extend([0] * (min_length - len(self.coded_message))) - - for i in range(len(new_data)): - # insert new byte. this is pretty hacky: it will fail if - # the value to be inserted is bitwise "disjoint" from the - # value which is already in the PDU... - if self.coded_message[pos + i] & new_data[i] != 0: - if obj_name is not None: - warnings.warn( - f"'{obj_name}' overlaps with another object (bits to be set are already set)", - OdxWarning, - stacklevel=1, - ) - else: + pad = b'\x00' * (min_length - len(self.coded_message)) + self.coded_message += pad + self.used_mask += pad + + if obj_used_mask is None: + # Happy path for when no obj_used_mask has been + # specified. In this case we assume that all bits of the + # new data to be emplaced are used. + n = len(new_data) + + if self.used_mask[pos:pos + n] != b'\x00' * n: + warnings.warn( + f"Overlapping objects detected in between bytes {pos} and " + f"{pos+n}", + OdxWarning, + stacklevel=1, + ) + self.coded_message[pos:pos + n] = new_data + self.used_mask[pos:pos + n] = b'\xff' * n + else: + # insert data the hard way, i.e. we have to look at each + # individual byte to determine if it has already been used + # somewhere else (it would be nice if bytearrays supported + # bitwise operations!) + for i in range(len(new_data)): + if self.used_mask[pos + i] & obj_used_mask[i] != 0: warnings.warn( - "Object overlap (bits to be set are already set)", + f"Overlapping objects detected at position {pos + i}", OdxWarning, stacklevel=1, ) - - self.coded_message[pos + i] |= new_data[i] + self.coded_message[pos + i] &= ~obj_used_mask[i] + self.coded_message[pos + i] |= new_data[i] & obj_used_mask[i] + self.used_mask[pos + i] |= obj_used_mask[i] self.cursor_byte_position += len(new_data) - self.cursor_bit_position = 0 diff --git a/odxtools/leadinglengthinfotype.py b/odxtools/leadinglengthinfotype.py index 70d1b3ec..a1e7ca01 100644 --- a/odxtools/leadinglengthinfotype.py +++ b/odxtools/leadinglengthinfotype.py @@ -55,8 +55,15 @@ def encode_into_pdu(self, internal_value: AtomicOdxType, encode_state: EncodeSta byte_length = self._minimal_byte_length_of(internal_value) + used_mask = None + bit_pos = encode_state.cursor_bit_position + if encode_state.cursor_bit_position != 0 or (bit_pos + self.bit_length) % 8 != 0: + used_mask = (1 << self.bit_length) - 1 + used_mask <<= bit_pos + encode_state.emplace_atomic_value( internal_value=byte_length, + used_mask=None, bit_length=self.bit_length, base_data_type=DataType.A_UINT32, is_highlow_byte_order=self.is_highlow_byte_order, @@ -64,6 +71,7 @@ def encode_into_pdu(self, internal_value: AtomicOdxType, encode_state: EncodeSta encode_state.emplace_atomic_value( internal_value=internal_value, + used_mask=None, bit_length=8 * byte_length, base_data_type=self.base_data_type, is_highlow_byte_order=self.is_highlow_byte_order, diff --git a/odxtools/minmaxlengthtype.py b/odxtools/minmaxlengthtype.py index 39027c9e..83688c3d 100644 --- a/odxtools/minmaxlengthtype.py +++ b/odxtools/minmaxlengthtype.py @@ -68,6 +68,7 @@ def encode_into_pdu(self, internal_value: AtomicOdxType, encode_state: EncodeSta orig_cursor = encode_state.cursor_byte_position encode_state.emplace_atomic_value( internal_value=internal_value, + used_mask=None, bit_length=8 * data_length, base_data_type=self.base_data_type, is_highlow_byte_order=self.is_highlow_byte_order, diff --git a/odxtools/parameterinfo.py b/odxtools/parameterinfo.py index 5135c93c..d22bc2be 100644 --- a/odxtools/parameterinfo.py +++ b/odxtools/parameterinfo.py @@ -37,7 +37,7 @@ def parameter_info(param_list: Iterable[Parameter], quoted_names: bool = False) of.write(f"{q}{param.short_name}{q}: \n") continue elif isinstance(param, NrcConstParameter): - of.write(f"{q}{param.short_name}{q}: NRC_const; choices = {param.coded_values}\n") + of.write(f"{q}{param.short_name}{q}: const; choices = {param.coded_values}\n") continue elif isinstance(param, ReservedParameter): of.write(f"{q}{param.short_name}{q}: \n") diff --git a/odxtools/parameters/lengthkeyparameter.py b/odxtools/parameters/lengthkeyparameter.py index 7a3127b3..9410f6a9 100644 --- a/odxtools/parameters/lengthkeyparameter.py +++ b/odxtools/parameters/lengthkeyparameter.py @@ -114,7 +114,10 @@ def encode_placeholder_into_pdu(self, physical_value: Optional[ParameterValue], encode_state.cursor_byte_position = pos encode_state.cursor_bit_position = self.bit_position or 0 - self.dop.encode_into_pdu(encode_state=encode_state, physical_value=0) + # emplace a value of zero into the encode state, but pretend the bits not to be used + n = odxrequire(self.dop.get_static_bit_length()) + encode_state.cursor_bit_position + tmp_val = b'\x00' * ((n + 7) // 8) + encode_state.emplace_bytes(tmp_val, obj_used_mask=tmp_val) encode_state.cursor_byte_position = max(encode_state.cursor_byte_position, orig_cursor) encode_state.cursor_bit_position = 0 diff --git a/odxtools/parameters/tablekeyparameter.py b/odxtools/parameters/tablekeyparameter.py index 253dd2d4..564bdc33 100644 --- a/odxtools/parameters/tablekeyparameter.py +++ b/odxtools/parameters/tablekeyparameter.py @@ -178,14 +178,16 @@ def encode_placeholder_into_pdu(self, physical_value: Optional[ParameterValue], odxraise(f"No KEY-DOP specified for table {self.table.short_name}") return - size = key_dop.get_static_bit_length() - - if size is None: + sz = key_dop.get_static_bit_length() + if sz is None: odxraise("The DOP of table key {self.short_name} must exhibit a fixed size.", EncodeError) return - encode_state.emplace_bytes(bytes([0] * (size // 8)), self.short_name) + # emplace a value of zero into the encode state, but pretend the bits not to be used + n = sz + encode_state.cursor_bit_position + tmp_val = b'\x00' * ((n + 7) // 8) + encode_state.emplace_bytes(tmp_val, obj_used_mask=tmp_val) encode_state.cursor_byte_position = max(orig_pos, encode_state.cursor_byte_position) encode_state.cursor_bit_position = 0 diff --git a/odxtools/paramlengthinfotype.py b/odxtools/paramlengthinfotype.py index c7e4c1cd..1056249e 100644 --- a/odxtools/paramlengthinfotype.py +++ b/odxtools/paramlengthinfotype.py @@ -81,6 +81,7 @@ def encode_into_pdu(self, internal_value: AtomicOdxType, encode_state: EncodeSta encode_state.emplace_atomic_value( internal_value=internal_value, + used_mask=None, bit_length=bit_length, base_data_type=self.base_data_type, is_highlow_byte_order=self.is_highlow_byte_order, diff --git a/odxtools/request.py b/odxtools/request.py index 4fba2fe7..d7f6a924 100644 --- a/odxtools/request.py +++ b/odxtools/request.py @@ -24,8 +24,7 @@ def from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment]) -> return Request(**kwargs) def encode(self, **kwargs: ParameterValue) -> bytes: - encode_state = EncodeState( - coded_message=bytearray(), triggering_request=None, is_end_of_pdu=True) + encode_state = EncodeState(is_end_of_pdu=True) self.encode_into_pdu(physical_value=kwargs, encode_state=encode_state) diff --git a/odxtools/response.py b/odxtools/response.py index 3876d377..a80e5519 100644 --- a/odxtools/response.py +++ b/odxtools/response.py @@ -39,8 +39,7 @@ def from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment]) -> return Response(response_type=response_type, **kwargs) def encode(self, coded_request: Optional[bytes] = None, **kwargs: ParameterValue) -> bytes: - encode_state = EncodeState( - coded_message=bytearray(), triggering_request=coded_request, is_end_of_pdu=True) + encode_state = EncodeState(triggering_request=coded_request, is_end_of_pdu=True) self.encode_into_pdu(physical_value=kwargs, encode_state=encode_state) diff --git a/odxtools/standardlengthtype.py b/odxtools/standardlengthtype.py index 273b1820..8b7373a0 100644 --- a/odxtools/standardlengthtype.py +++ b/odxtools/standardlengthtype.py @@ -1,13 +1,13 @@ # SPDX-License-Identifier: MIT from dataclasses import dataclass -from typing import Optional +from typing import Literal, Optional from typing_extensions import override from .decodestate import DecodeState from .diagcodedtype import DctType, DiagCodedType from .encodestate import EncodeState -from .exceptions import odxassert, odxraise +from .exceptions import odxassert, odxraise, odxrequire from .odxtypes import AtomicOdxType, DataType @@ -22,6 +22,10 @@ class StandardLengthType(DiagCodedType): def dct_type(self) -> DctType: return "STANDARD-LENGTH-TYPE" + @property + def is_condensed(self) -> bool: + return self.is_condensed_raw is True + def __post_init__(self) -> None: if self.bit_mask is not None: maskable_types = (DataType.A_UINT32, DataType.A_INT32, DataType.A_BYTEFIELD) @@ -30,10 +34,41 @@ def __post_init__(self) -> None: 'Can not apply a bit_mask on a value of type {self.base_data_type}', ) + def __get_raw_mask(self, internal_value: AtomicOdxType) -> Optional[bytes]: + """Returns a byte field where all bits that are used by the + DiagCoded type are set and all unused ones are not set. + + If `None` is returned, all bits are used. + """ + if self.bit_mask is None: + return None + + if self.is_condensed: + odxraise("Condensed bit masks are not yet supported", NotImplementedError) + return + + endianness: Literal["little", "big"] = "big" + if not self.is_highlow_byte_order and self.base_data_type in [ + DataType.A_INT32, DataType.A_UINT32, DataType.A_FLOAT32, DataType.A_FLOAT64 + ]: + # TODO (?): Technically, little endian A_UNICODE2STRING + # objects require a byte swap for each 16 bit letter, and + # thus also for the mask. I somehow doubt that this has + # been anticipated by the standard, though... + endianness = "little" + + sz: int + if isinstance(internal_value, (bytes, bytearray)): + sz = len(internal_value) + else: + sz = (odxrequire(self.get_static_bit_length()) + 7) // 8 + + return self.bit_mask.to_bytes(sz, endianness) + def __apply_mask(self, internal_value: AtomicOdxType) -> AtomicOdxType: if self.bit_mask is None: return internal_value - if self.is_condensed_raw is True: + if self.is_condensed: odxraise("Serialization of condensed bit mask is not supported", NotImplementedError) return if isinstance(internal_value, int): @@ -53,10 +88,10 @@ def get_static_bit_length(self) -> Optional[int]: def encode_into_pdu(self, internal_value: AtomicOdxType, encode_state: EncodeState) -> None: encode_state.emplace_atomic_value( internal_value=self.__apply_mask(internal_value), + used_mask=self.__get_raw_mask(internal_value), bit_length=self.bit_length, base_data_type=self.base_data_type, - is_highlow_byte_order=self.is_highlow_byte_order, - ) + is_highlow_byte_order=self.is_highlow_byte_order) @override def decode_from_pdu(self, decode_state: DecodeState) -> AtomicOdxType: diff --git a/tests/test_diag_coded_types.py b/tests/test_diag_coded_types.py index ed5123cb..edd9cb80 100644 --- a/tests/test_diag_coded_types.py +++ b/tests/test_diag_coded_types.py @@ -88,7 +88,10 @@ def test_decode_leading_length_info_type_bytefield2(self) -> None: is_highlow_byte_order_raw=None, ) - state = EncodeState(bytearray.fromhex("0000ff00"), cursor_bit_position=3) + state = EncodeState( + coded_message=bytearray.fromhex("0000ff00"), + used_mask=bytearray.fromhex("0700ffff"), + cursor_bit_position=3) dct.encode_into_pdu(bytes([0xcc]), state) self.assertEqual(state.coded_message.hex(), "08ccff00") self.assertEqual(state.cursor_byte_position, 2) @@ -687,7 +690,7 @@ def test_encode_min_max_length_type_hex_ff(self) -> None: termination="HEX-FF", is_highlow_byte_order_raw=None, ) - state = EncodeState(coded_message=bytearray([0x00]), is_end_of_pdu=False) + state = EncodeState(is_end_of_pdu=False) dct.encode_into_pdu(bytes([0x34, 0x56]), state) self.assertEqual(state.coded_message, bytes([0x34, 0x56, 0xFF])) @@ -700,7 +703,7 @@ def test_encode_min_max_length_type_zero(self) -> None: termination="ZERO", is_highlow_byte_order_raw=None, ) - state = EncodeState(coded_message=bytearray([0x00]), is_end_of_pdu=False) + state = EncodeState(is_end_of_pdu=False) dct.encode_into_pdu("Hi", state) self.assertEqual(state.coded_message, bytes([0x48, 0x69, 0x0])) @@ -739,7 +742,7 @@ def test_encode_min_max_length_type_min_length(self) -> None: termination=termination, is_highlow_byte_order_raw=None, ) - state = EncodeState(coded_message=bytearray([0x00]), is_end_of_pdu=True) + state = EncodeState(is_end_of_pdu=True) dct.encode_into_pdu(bytes([0x34, 0x56]), state) self.assertTrue(state.coded_message.hex().startswith("3456")) self.assertRaises( @@ -760,7 +763,7 @@ def test_encode_min_max_length_type_max_length(self) -> None: termination=termination, is_highlow_byte_order_raw=None, ) - state = EncodeState(coded_message=bytearray([0x00]), is_end_of_pdu=True) + state = EncodeState(is_end_of_pdu=True) dct.encode_into_pdu(bytes([0x34, 0x56, 0x78]), state) self.assertEqual(state.coded_message, bytes([0x34, 0x56, 0x78])) self.assertRaises( diff --git a/tests/test_encoding.py b/tests/test_encoding.py index 8eebe829..6a4c217e 100644 --- a/tests/test_encoding.py +++ b/tests/test_encoding.py @@ -319,7 +319,7 @@ def test_encode_overlapping(self) -> None: parameters=NamedItemList([param1, param2, param3]), byte_size=None, ) - self.assertEqual(req.encode(), bytearray([0x12, 0x34, 0x56])) + self.assertEqual(req.encode().hex(), "123456") self.assertEqual(req.get_static_bit_length(), 24) def _create_request(self, parameters: List[Parameter]) -> Request: @@ -336,12 +336,12 @@ def _create_request(self, parameters: List[Parameter]) -> Request: def test_bit_mask(self) -> None: inner_dct = StandardLengthType( - bit_mask=0x0ff0, + bit_mask=0x3fc, base_data_type=DataType.A_UINT32, base_type_encoding=None, is_highlow_byte_order_raw=None, is_condensed_raw=None, - bit_length=16) + bit_length=14) outer_dct = StandardLengthType( bit_mask=0xf00f, base_data_type=DataType.A_UINT32, @@ -395,7 +395,7 @@ def test_bit_mask(self) -> None: long_name=None, description=None, byte_position=0, - bit_position=None, + bit_position=2, dop_ref=OdxLinkRef.from_id(inner_dop.odx_id), dop_snref=None, semantic=None, @@ -421,11 +421,13 @@ def test_bit_mask(self) -> None: req = self._create_request([inner_param, outer_param]) - self.assertEqual(req.encode(inner_param=0x1111, outer_param=0x2222).hex(), "2112") + # the bit shifts here stem from the fact that we placed the + # inner parameter at bit position 2... + self.assertEqual(req.encode(inner_param=0x1234 >> 2, outer_param=0x4568).hex(), "4238") self.assertEqual( - req.decode(bytes.fromhex('1234')), { - "inner_param": 0x0230, - "outer_param": 0x1004 + req.decode(bytes.fromhex('abcd')), { + "inner_param": (0xbc << 2), + "outer_param": 0xa00d })