From 6ab6091247ceff2ae0f547a28dc372107872ae5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20D=C4=85browski?= Date: Mon, 4 Nov 2024 15:32:35 +0100 Subject: [PATCH 1/6] implementation for abstract data record --- uds/database/__init__.py | 7 ++ uds/database/abstract_data_record.py | 119 +++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 uds/database/__init__.py create mode 100644 uds/database/abstract_data_record.py diff --git a/uds/database/__init__.py b/uds/database/__init__.py new file mode 100644 index 0000000..7cac16e --- /dev/null +++ b/uds/database/__init__.py @@ -0,0 +1,7 @@ +""" +Implementation for diagnostic messages databases. + +Tools for decoding and encoding information from/to diagnostic messages. +""" + +from .abstract_data_record import AbstractDataRecord, DataRecordType, DecodedDataRecord diff --git a/uds/database/abstract_data_record.py b/uds/database/abstract_data_record.py new file mode 100644 index 0000000..d522adc --- /dev/null +++ b/uds/database/abstract_data_record.py @@ -0,0 +1,119 @@ +""" +Definition of all Data Records types. + +Each Data Record contains mapping (translation) of raw data (sequence of bits in diagnostic message payload) to some +meaningful information (e.g. physical value, text). +""" + +__all__ = ["DataRecordType", "AbstractDataRecord", "DecodedDataRecord"] + +from abc import ABC, abstractmethod +from typing import Tuple, TypedDict, Union + +from uds.utilities import ValidatedEnum + +DataRecordPhysicalValueAlias = Union[int, float, str, Tuple["DecodedDataRecord", ...]] +"""Alias of Data Records' physical value.""" + + +class DecodedDataRecord(TypedDict): + """Structure of decoded Data Record.""" + + name: str + raw_value: int + physical_value: DataRecordPhysicalValueAlias + + +class DataRecordType(ValidatedEnum): + """All Data Record types.""" + + # TODO: fill with following tasks: + # - https://github.com/mdabrowski1990/uds/issues/2 + # - https://github.com/mdabrowski1990/uds/issues/6 + # - https://github.com/mdabrowski1990/uds/issues/8 + # - https://github.com/mdabrowski1990/uds/issues/9 + # - https://github.com/mdabrowski1990/uds/issues/10 + + +class AbstractDataRecord(ABC): + """Common implementation and interface for all Data Records.""" + + def __init__(self, name: str) -> None: + """ + Common part of initialization for all Data Records. + + :param name: Name to assign to this Data Record. + """ + self.__name = name + + @property + def name(self) -> str: + """Name of this Data Record.""" + return self.__name + + @property + @abstractmethod + def data_record_type(self) -> DataRecordType: + """Type of this Data Record.""" + + @property + @abstractmethod + def length(self) -> int: + """Number of bits that this Data Record is carried over.""" + + @property + @abstractmethod + def is_reoccurring(self) -> bool: + """ + Whether this Data Record might occur multiple times. + + Values meaning: + - False - exactly one occurrence in every diagnostic message + - True - number of occurrences might vary + """ + + @property + @abstractmethod + def min_occurrences(self) -> int: + """ + Minimal number of this Data Record occurrences. + + .. note:: Relevant only if :attr:`~uds.database.abstract_data_record.AbstractDataRecord.min_occurrences` + equals True. + """ + + @property + @abstractmethod + def max_occurrences(self) -> int: + """ + Maximal number of this Data Record occurrences. + + .. note:: Relevant only if :attr:`~uds.database.abstract_data_record.AbstractDataRecord.min_occurrences` + equals True. + .. warning:: No maximal number (infinite number of occurrences) is represented by `0` value. + """ + + @property + @abstractmethod + def contains(self) -> Tuple["AbstractDataRecord", ...]: + """Data Records contained by this Data Record.""" + + @abstractmethod + def decode(self, raw_value: int) -> DecodedDataRecord: + """ + Decode physical value for provided raw value. + + :param raw_value: Raw (bit) value of Data Record. + + :return: Dictionary with physical value for this Data Record. + """ + + @abstractmethod + def encode(self, physical_value: DataRecordPhysicalValueAlias) -> int: + """ + Encode raw value for provided physical value. + + :param physical_value: Physical (meaningful e.g. float, str type) value of this Data Record. + + :return: Raw Value of this Data Record. + """ From 06bc62978362f8317934cc9e95a227ff1d567351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20D=C4=85browski?= Date: Mon, 4 Nov 2024 15:43:14 +0100 Subject: [PATCH 2/6] tests for abstract data record --- tests/software_tests/database/__init__.py | 0 .../database/test_abstract_data_record.py | 36 +++++++++++++++++++ uds/database/abstract_data_record.py | 6 +++- 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 tests/software_tests/database/__init__.py create mode 100644 tests/software_tests/database/test_abstract_data_record.py diff --git a/tests/software_tests/database/__init__.py b/tests/software_tests/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/software_tests/database/test_abstract_data_record.py b/tests/software_tests/database/test_abstract_data_record.py new file mode 100644 index 0000000..db60739 --- /dev/null +++ b/tests/software_tests/database/test_abstract_data_record.py @@ -0,0 +1,36 @@ +import pytest +from mock import Mock, patch +from uds.database.abstract_data_record import AbstractDataRecord + +SCRIPT_LOCATION = "uds.database.abstract_data_record" + + +class TestAbstractDataRecord: + + def setup_method(self): + self.mock_data_record = Mock(spec=AbstractDataRecord) + + # __init__ + + @patch(f"{SCRIPT_LOCATION}.isinstance") + def test_init__type_error(self, mock_isinstance): + mock_isinstance.return_value = False + mock_name = Mock() + with pytest.raises(TypeError): + AbstractDataRecord.__init__(self.mock_data_record, mock_name) + mock_isinstance.assert_called_once_with(mock_name, str) + + @patch(f"{SCRIPT_LOCATION}.isinstance") + def test_init__valid(self, mock_isinstance): + mock_isinstance.return_value = True + mock_name = Mock() + assert AbstractDataRecord.__init__(self.mock_data_record, mock_name) is None + assert self.mock_data_record._AbstractDataRecord__name == mock_name.strip.return_value + mock_isinstance.assert_called_once_with(mock_name, str) + mock_name.strip.assert_called_once_with() + + # name + + def test_name(self): + self.mock_data_record._AbstractDataRecord__name = Mock() + assert AbstractDataRecord.name.fget(self.mock_data_record) == self.mock_data_record._AbstractDataRecord__name diff --git a/uds/database/abstract_data_record.py b/uds/database/abstract_data_record.py index d522adc..d1de32b 100644 --- a/uds/database/abstract_data_record.py +++ b/uds/database/abstract_data_record.py @@ -43,8 +43,12 @@ def __init__(self, name: str) -> None: Common part of initialization for all Data Records. :param name: Name to assign to this Data Record. + + :raise TypeError: Provided value of name is not str type. """ - self.__name = name + if not isinstance(name, str): + raise TypeError("Provided name is not str type.") + self.__name = name.strip() @property def name(self) -> str: From 90b0edbeda2d0985d15a0dd62e22b742e176197c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20D=C4=85browski?= Date: Mon, 4 Nov 2024 15:51:19 +0100 Subject: [PATCH 3/6] Fix prospector warnings --- .../database/test_abstract_data_record.py | 1 + uds/can/flow_control.py | 4 ++-- uds/database/abstract_data_record.py | 24 +++++++++---------- .../transmission_direction.py | 4 ++-- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/tests/software_tests/database/test_abstract_data_record.py b/tests/software_tests/database/test_abstract_data_record.py index db60739..87aff10 100644 --- a/tests/software_tests/database/test_abstract_data_record.py +++ b/tests/software_tests/database/test_abstract_data_record.py @@ -1,5 +1,6 @@ import pytest from mock import Mock, patch + from uds.database.abstract_data_record import AbstractDataRecord SCRIPT_LOCATION = "uds.database.abstract_data_record" diff --git a/uds/can/flow_control.py b/uds/can/flow_control.py index d0e196f..100030c 100644 --- a/uds/can/flow_control.py +++ b/uds/can/flow_control.py @@ -57,9 +57,9 @@ class CanFlowStatus(ValidatedEnum, NibbleEnum): ContinueToSend: "CanFlowStatus" = 0x0 # type: ignore """Asks to resume Consecutive Frames transmission.""" - Wait: "CanFlowStatus" = 0x1 # type: ignore # noqa: F841 + Wait: "CanFlowStatus" = 0x1 # type: ignore """Asks to pause Consecutive Frames transmission.""" - Overflow: "CanFlowStatus" = 0x2 # type: ignore # noqa: F841 + Overflow: "CanFlowStatus" = 0x2 # type: ignore """Asks to abort transmission of a diagnostic message.""" diff --git a/uds/database/abstract_data_record.py b/uds/database/abstract_data_record.py index d1de32b..df8670a 100644 --- a/uds/database/abstract_data_record.py +++ b/uds/database/abstract_data_record.py @@ -21,7 +21,7 @@ class DecodedDataRecord(TypedDict): name: str raw_value: int - physical_value: DataRecordPhysicalValueAlias + physical_value: DataRecordPhysicalValueAlias # noqa: F841 class DataRecordType(ValidatedEnum): @@ -40,7 +40,7 @@ class AbstractDataRecord(ABC): def __init__(self, name: str) -> None: """ - Common part of initialization for all Data Records. + Initialize common part for all Data Records. :param name: Name to assign to this Data Record. @@ -55,17 +55,17 @@ def name(self) -> str: """Name of this Data Record.""" return self.__name - @property + @property # noqa: F841 @abstractmethod def data_record_type(self) -> DataRecordType: """Type of this Data Record.""" - @property + @property # noqa: F841 @abstractmethod def length(self) -> int: - """Number of bits that this Data Record is carried over.""" + """Get number of bits that this Data Record is stored over.""" - @property + @property # noqa: F841 @abstractmethod def is_reoccurring(self) -> bool: """ @@ -76,7 +76,7 @@ def is_reoccurring(self) -> bool: - True - number of occurrences might vary """ - @property + @property # noqa: F841 @abstractmethod def min_occurrences(self) -> int: """ @@ -86,7 +86,7 @@ def min_occurrences(self) -> int: equals True. """ - @property + @property # noqa: F841 @abstractmethod def max_occurrences(self) -> int: """ @@ -97,13 +97,13 @@ def max_occurrences(self) -> int: .. warning:: No maximal number (infinite number of occurrences) is represented by `0` value. """ - @property + @property # noqa: F841 @abstractmethod def contains(self) -> Tuple["AbstractDataRecord", ...]: - """Data Records contained by this Data Record.""" + """Get Data Records contained by this Data Record.""" @abstractmethod - def decode(self, raw_value: int) -> DecodedDataRecord: + def decode(self, raw_value: int) -> DecodedDataRecord: # noqa: F841 """ Decode physical value for provided raw value. @@ -113,7 +113,7 @@ def decode(self, raw_value: int) -> DecodedDataRecord: """ @abstractmethod - def encode(self, physical_value: DataRecordPhysicalValueAlias) -> int: + def encode(self, physical_value: DataRecordPhysicalValueAlias) -> int: # noqa: F841 """ Encode raw value for provided physical value. diff --git a/uds/transmission_attributes/transmission_direction.py b/uds/transmission_attributes/transmission_direction.py index cfef445..ee65bc7 100644 --- a/uds/transmission_attributes/transmission_direction.py +++ b/uds/transmission_attributes/transmission_direction.py @@ -11,7 +11,7 @@ class TransmissionDirection(ValidatedEnum, StrEnum): """Direction of a communication.""" - RECEIVED: "TransmissionDirection" = "Rx" # type: ignore # noqa: F841 + RECEIVED: "TransmissionDirection" = "Rx" # type: ignore """Incoming transmission from the perspective of the code.""" - TRANSMITTED: "TransmissionDirection" = "Tx" # type: ignore # noqa: F841 + TRANSMITTED: "TransmissionDirection" = "Tx" # type: ignore """Outgoing transmission from the perspective of the code.""" From 1b5bbb9ded126d83c835ab0513fd9d18598015f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20D=C4=85browski?= Date: Mon, 4 Nov 2024 16:24:24 +0100 Subject: [PATCH 4/6] Update __init__.py --- uds/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/uds/__init__.py b/uds/__init__.py index be182c2..4538ef2 100644 --- a/uds/__init__.py +++ b/uds/__init__.py @@ -28,6 +28,7 @@ import uds.can +import uds.database import uds.message import uds.packet import uds.segmentation From c7dca7c10f9cbe11fb3e132e8bc77e35b8894cbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20D=C4=85browski?= Date: Mon, 4 Nov 2024 21:52:14 +0100 Subject: [PATCH 5/6] Update README.rst --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index c1ef505..5b15084 100644 --- a/README.rst +++ b/README.rst @@ -65,7 +65,7 @@ Link: https://github.com/pylessard/python-udsoncan https://udsoncan.readthedocs.io/en/latest/udsoncan/connection.html#available-connections - CAN bus fully supported with possibility to extension for other buses (requires custom code) - possibility to configure all transmission parameters for CAN using can-isotp package - - https://can-isotp.readthedocs.io/en/latest/isotp/implementation.html# + https://can-isotp.readthedocs.io/en/latest/isotp/implementation.html - handlers for multiple diagnostic services are implemented - https://udsoncan.readthedocs.io/en/latest/udsoncan/services.html - positive and negatives scenarios are handled - https://udsoncan.readthedocs.io/en/latest/udsoncan/exceptions.html @@ -78,7 +78,7 @@ Link: https://github.com/pylessard/python-udsoncan - cons: - no support for full-duplex communication (sending and receiving at the same time) - - only Client side communication is implemented - https://udsoncan.readthedocs.io/en/latest/udsoncan/client.html# + - only Client side communication is implemented - https://udsoncan.readthedocs.io/en/latest/udsoncan/client.html python-uds From f388a29320d0e2aa00ad3aa59d99e38d5fa208c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20D=C4=85browski?= Date: Thu, 7 Nov 2024 17:39:15 +0100 Subject: [PATCH 6/6] Update abstract_data_record.py --- uds/database/abstract_data_record.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/uds/database/abstract_data_record.py b/uds/database/abstract_data_record.py index df8670a..c781118 100644 --- a/uds/database/abstract_data_record.py +++ b/uds/database/abstract_data_record.py @@ -8,7 +8,7 @@ __all__ = ["DataRecordType", "AbstractDataRecord", "DecodedDataRecord"] from abc import ABC, abstractmethod -from typing import Tuple, TypedDict, Union +from typing import Tuple, TypedDict, Union, Optional from uds.utilities import ValidatedEnum @@ -82,19 +82,19 @@ def min_occurrences(self) -> int: """ Minimal number of this Data Record occurrences. - .. note:: Relevant only if :attr:`~uds.database.abstract_data_record.AbstractDataRecord.min_occurrences` + .. note:: Relevant only if :attr:`~uds.database.abstract_data_record.AbstractDataRecord.is_reoccurring` equals True. """ @property # noqa: F841 @abstractmethod - def max_occurrences(self) -> int: + def max_occurrences(self) -> Optional[int]: """ Maximal number of this Data Record occurrences. - .. note:: Relevant only if :attr:`~uds.database.abstract_data_record.AbstractDataRecord.min_occurrences` + .. note:: Relevant only if :attr:`~uds.database.abstract_data_record.AbstractDataRecord.is_reoccurring` equals True. - .. warning:: No maximal number (infinite number of occurrences) is represented by `0` value. + .. warning:: No maximal number (infinite number of occurrences) is represented by None value. """ @property # noqa: F841