diff --git a/CHANGELOG.md b/CHANGELOG.md index c7ba281..71e3a2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.23.0] - 2024-02-05 + +### Changed + +- Padding in a frame can now be changed from zeros to padding with ones + ## [0.22.0] - 2024-01-30 ### Added diff --git a/ldfparser/frame.py b/ldfparser/frame.py index 29ef1a5..ef3419f 100644 --- a/ldfparser/frame.py +++ b/ldfparser/frame.py @@ -30,6 +30,7 @@ def __init__(self, frame_id: int, name: str) -> None: self.frame_id = frame_id self.name = name + class LinUnconditionalFrame(LinFrame): """ LinUnconditionalFrame represents an unconditional frame consisting of signals @@ -42,19 +43,24 @@ class LinUnconditionalFrame(LinFrame): :type length: int :param signals: Signals of the frame :type signals: Dict[int, LinSignal] + :param pad_with_zero: If True, pad with zeros during frame encoding. Otherwise, pad with ones. + Default: True + :type pad_with_zero: Boolean """ - def __init__(self, frame_id: int, name: str, length: int, signals: Dict[int, 'LinSignal']): + def __init__(self, frame_id: int, name: str, length: int, signals: Dict[int, 'LinSignal'], pad_with_zero: bool = True): super().__init__(frame_id, name) self.publisher = None self.length = length self.signal_map = sorted(signals.items(), key=lambda x: x[0]) - self._packer = LinUnconditionalFrame._frame_pattern(self.length, self.signal_map) + self._packer = LinUnconditionalFrame._frame_pattern(self.length, self.signal_map, pad_with_zero) @staticmethod def _frame_pattern( frame_size: int, - signals: List[Tuple[int, 'LinSignal']]) -> bitstruct.CompiledFormat: + signals: List[Tuple[int, 'LinSignal']], + pad_with_zero: bool = True, + ) -> bitstruct.CompiledFormat: """ Converts a frame layout into a bitstructure formatting string @@ -73,7 +79,9 @@ def _frame_pattern( :param signals: List of signals and offsets that represent the frame layout :type signals: List[Tuple[int, LinSignal]] where the tuple's first element is the offset and the second element is the signal object - + :param pad_with_zero: If True, pad with zeros during frame encoding. Otherwise, pad with ones. + Default: True + :type pad_with_zero: Boolean :raises: ValueError if the signals inside the frame would overlap or span outside the frame :returns: Bitstruct packer object :rtype: bitstruct.CompiledFormat @@ -81,12 +89,13 @@ def _frame_pattern( pattern = "<" frame_bits = frame_size * 8 frame_offset = 0 + padding_value = "p" if pad_with_zero else "P" for (offset, signal) in signals: if offset < frame_offset: raise ValueError(f"{signal} is overlapping ") if offset != frame_offset: padding = offset - frame_offset - pattern += f"p{padding}" + pattern += f"{padding_value}{padding}" frame_offset += padding if frame_offset + signal.width > frame_bits: raise ValueError(f"{signal} with offset {offset} spans outside frame!") @@ -96,7 +105,7 @@ def _frame_pattern( pattern += f"u{signal.width}" frame_offset += signal.width if frame_offset < frame_bits: - pattern += f"p{frame_bits - frame_offset}" + pattern += f"{padding_value}{frame_bits - frame_offset}" return bitstruct.compile(pattern) def _get_signal(self, name: str): diff --git a/ldfparser/ldf.py b/ldfparser/ldf.py index 14e97ca..3ebe4d7 100644 --- a/ldfparser/ldf.py +++ b/ldfparser/ldf.py @@ -17,7 +17,12 @@ class LDF(): LDF is a container class that describes a LIN network """ - def __init__(self): + def __init__(self, pad_with_zero: bool = True): + """ + :param pad_with_zero: If True, pad with zeros during frame encoding. Otherwise, pad with ones. + Default: True + :type pad_with_zero: Boolean + """ self._source: Dict = None self._protocol_version: Union[LinVersion, Iso17987Version, J2602Version] = None self._language_version: Union[LinVersion, Iso17987Version, J2602Version] = None @@ -37,6 +42,7 @@ def __init__(self): self._slave_response_frame: LinDiagnosticResponse = None self._schedule_tables: Dict[str, ScheduleTable] = {} self._comments: List[str] = [] + self._pad_with_zero = pad_with_zero def get_protocol_version(self) -> Union[LinVersion, Iso17987Version, J2602Version]: """Returns the protocol version of the LIN network""" diff --git a/ldfparser/parser.py b/ldfparser/parser.py index 5242a8a..9b3f36f 100644 --- a/ldfparser/parser.py +++ b/ldfparser/parser.py @@ -50,7 +50,7 @@ def parseLDFtoDict(path: str, captureComments: bool = False, encoding: str = Non warnings.warn("'parseLDFtoDict' is deprecated, use 'parse_ldf_to_dict' instead", DeprecationWarning) return parse_ldf_to_dict(path, captureComments, encoding) -def parse_ldf(path: str, capture_comments: bool = False, encoding: str = None) -> LDF: +def parse_ldf(path: str, capture_comments: bool = False, encoding: str = None, pad_with_zero: bool = True) -> LDF: """ Parses an LDF file into an object @@ -58,9 +58,12 @@ def parse_ldf(path: str, capture_comments: bool = False, encoding: str = None) - :type path: str :param encoding: File encoding, for example 'UTF-8' :type encoding: str + :param pad_with_zero: If True, pad with zeros during frame encoding. Otherwise, pad with ones. + Default: True + :type pad_with_zero: Boolean """ json = parse_ldf_to_dict(path, capture_comments, encoding) - ldf = LDF() + ldf = LDF(pad_with_zero=pad_with_zero) ldf._source = json _populate_ldf_header(json, ldf) @@ -122,7 +125,7 @@ def _populate_ldf_frames(json: dict, ldf: LDF): elif 48 <= frame['frame_id'] <= 63: length = 8 - frame_obj = LinUnconditionalFrame(frame['frame_id'], frame['name'], length, signals) + frame_obj = LinUnconditionalFrame(frame['frame_id'], frame['name'], length, signals, pad_with_zero=ldf._pad_with_zero) ldf._unconditional_frames[frame['name']] = frame_obj for (_, signal) in signals.items(): diff --git a/setup.cfg b/setup.cfg index 0f41448..17bc7b2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,2 @@ [metadata] -version = 0.22.0 +version = 0.23.0 diff --git a/tests/test_frame.py b/tests/test_frame.py index 54ba6e2..08a6272 100644 --- a/tests/test_frame.py +++ b/tests/test_frame.py @@ -375,3 +375,17 @@ def test_decode_int(self, frame): def test_decode_with_unit(self, frame, range_type): decoded = frame.decode(b'\x20\x3F\x08', {'MotorSpeed': range_type}, keep_unit=True) assert decoded['MotorSpeed'] == '1600.000 rpm' + +@pytest.mark.unit +def test_frame_encoding_with_optional_padding1(): + signal1 = LinSignal('Signal_1', 8, 255) + signal2 = LinSignal('Signal_2', 4, 255) + signal3 = LinSignal('Signal_3', 1, 255) + + frame = LinUnconditionalFrame(1, 'Frame_1', 2, {0: signal1, 8: signal2, 15: signal3}, pad_with_zero=False) + content = frame.encode_raw({ + 'Signal_2': 10, + 'Signal_3': 1 + }) + + assert list(content) == [255, 250] # 10 | ( 1 << 7 | 0x70) = 250 diff --git a/tests/test_parser.py b/tests/test_parser.py index ee22b97..7f71f21 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -246,3 +246,16 @@ def test_j2602_attributes_default( assert list(ldf.slaves)[0].response_tolerance == slave_response_tolerance assert list(ldf.slaves)[0].wakeup_time == slave_wakeup_time assert list(ldf.slaves)[0].poweron_time == slave_poweron_time + +@pytest.mark.unit +@pytest.mark.parametrize( + "pad_with_zero", [True, False, None] +) +def test_padding_option(pad_with_zero): + path = os.path.join(os.path.dirname(__file__), "ldf", "lin20.ldf") + if pad_with_zero is None: + ldf = parse_ldf(path) + assert ldf._pad_with_zero is True + else: + ldf = parse_ldf(path, pad_with_zero=pad_with_zero) + assert ldf._pad_with_zero == pad_with_zero