Repository: mdabrowski1990/uds (language: python)

Issue:
Introduce UDS Messages
Implement UDS Request and Response 

As a user, I want to have an option to create UDS Request and UDS Response objects so I can easily create and interpret messages.
As a developer, I want to have UDS Message class which contains common implementation for Request and Response, so I do not have to implement and test features twice.
As a user, I want to have timestamp when the message was received (wither by server or client), so I can check it.
As a user, I want to have option to check response type (positive, negative, invalid in terms of protocol), so I do not have to implement it myself.
As a user, I want to have easy access to raw message data, so I can modify however I please. Note: once modified, some other fields might also be changed (e.g. time of frame reception shall be reset).


Discussion / hints:

Fix patch:
diff --git a/requirements.txt b/requirements.txt
index 8b137891..a35b6adc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,1 +1,1 @@
-
+aenum>=3.0.0
\ No newline at end of file
diff --git a/uds/__init__.py b/uds/__init__.py
index e69de29b..e3ae3891 100644
--- a/uds/__init__.py
+++ b/uds/__init__.py
@@ -0,0 +1,19 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+Package for handling Unified Diagnostic Services (UDS) protocol defined by ISO-14229.
+
+The package is meant to provide tools to handle following UDS protocol layers (according to OSI model):
+ - Session (5th) - specified by ISO 14229-2
+ - Presentation (6th) - some part is specified by ISO 27145-2, rest is vehicle manufacturer specific
+ - Application (7th) - specified by ISO 14229-1, ISO 14229-3, ISO 14229-4, ISO 14229-5, ISO 14229-6, ISO 14229-7,
+        ISO 14229-8, ISO 27145-3 and further standards
+Other (bus specific) layers would be supported by extensions (once released) to this package or by custom interfaces
+created by users themselves.
+"""
+
+# __all__ = []  TODO: update when new feature are added
+__version__ = "0.0"
+__author__ = "Maciej Dąbrowski"  # TODO: add other other contributors
+# __credits__ = []  TODO: place for sponsors and other stakeholders
+__email__ = "maciek_dabrowski@o2.pl"
diff --git a/uds/messages/__init__.py b/uds/messages/__init__.py
new file mode 100644
index 00000000..a428cfdd
--- /dev/null
+++ b/uds/messages/__init__.py
@@ -0,0 +1,8 @@
+"""Module with basic tools to handle UDS messages."""
+
+from .pdu import AbstractPDU
+from .base_message import UdsMessage
+from .response import UdsResponse, UdsResponseType
+from .request import UdsRequest
+from .service_identifiers import RequestSID, ResponseSID
+from .addressing import AddressingType
diff --git a/uds/messages/addressing.py b/uds/messages/addressing.py
new file mode 100644
index 00000000..2c829b9b
--- /dev/null
+++ b/uds/messages/addressing.py
@@ -0,0 +1,20 @@
+"""UDS messages addressing implementation."""
+
+__all__ = ["AddressingType"]
+
+from enum import Enum
+
+
+class AddressingType(Enum):
+    """
+    Types of communication (addressing) defined by UDS.
+
+    Options:
+    - PHYSICAL - 1 (client) to 1 (server) communication
+    - FUNCTIONAL - 1 (client) to many (servers) communication
+    - BROADCAST - 1 (client) to many (servers) communication that does not require response
+    """
+
+    PHYSICAL = "Physical"  # noqa: F841
+    FUNCTIONAL = "Functional"  # noqa: F841
+    BROADCAST = "Broadcast"  # noqa: F841
diff --git a/uds/messages/base_message.py b/uds/messages/base_message.py
new file mode 100644
index 00000000..00c8a742
--- /dev/null
+++ b/uds/messages/base_message.py
@@ -0,0 +1,89 @@
+"""Common implementation of all UDS messages (requests and responses)."""
+
+__all__ = ["UdsMessage"]
+
+from typing import Optional, Union, List, Tuple
+
+from .addressing import AddressingType
+from .pdu import AbstractPDU
+
+# TODO: since python 3.9 it can be replaced with Union[list[int], tuple[int]]; keep this way for backward compatibility
+# TODO: prospector supports only pylint version 2.5 that has serious problem with Aliases in Python 3.9.
+#  Remove unsubscriptable-object once prospector supports newer versions of pylint.
+TypingRawMessage = Union[List[int], Tuple[int, ...]]  # pylint: disable=unsubscriptable-object
+PDUs = Union[List[AbstractPDU], Tuple[AbstractPDU, ...]]  # pylint: disable=unsubscriptable-object
+
+
+class UdsMessage:
+    """Common implementation of all UDS messages (requests and responses)."""
+
+    def __init__(self,
+                 raw_message: TypingRawMessage,
+                 pdu_sequence: Optional[PDUs] = None) -> None:  # pylint: disable=unsubscriptable-object
+        """
+        Create storage for a single UDS message.
+
+        :param raw_message: A single message that consists of raw bytes only.
+        :param pdu_sequence: Sequence of PDUs (Protocol Data Units) that were published to a bus to send/receive
+            this messages. It should be None, if message was never transmitted.
+        """
+        # verify arguments
+        self.__validate_raw_message(raw_message=raw_message)
+        if pdu_sequence is not None:
+            self.__validate_pdu_sequence(pdu_sequence=pdu_sequence)
+        # update data
+        self.__raw_message = tuple(raw_message)
+        self.__pdu_sequence = tuple() if pdu_sequence is None else tuple(pdu_sequence)
+
+    @staticmethod
+    def __validate_raw_message(raw_message: TypingRawMessage) -> None:
+        """
+        Verify raw message argument.
+
+        :param raw_message: Raw message value to validate.
+
+        :raise TypeError: Raw message is not list or tuple type.
+        :raise ValueError: Any of raw message elements is not raw byte value.
+        """
+        if not isinstance(raw_message, (tuple, list)):
+            raise TypeError("'raw_message' is not list or tuple type")
+        if not all([isinstance(raw_byte, int) and 0x00 <= raw_byte <= 0xFF for raw_byte in raw_message]):
+            raise ValueError("'raw_message' does not contain raw bytes (int value between 0 and 255) values only")
+
+    @staticmethod
+    def __validate_pdu_sequence(pdu_sequence: PDUs) -> None:
+        """
+        Verify pdu sequence argument.
+
+        :param pdu_sequence: PDUs sequence to validate.
+
+        :raise TypeError: PDUs sequence is not list or tuple type.
+        :raise ValueError: Any of PDUs sequence elements is not PDU.
+        """
+        if not isinstance(pdu_sequence, (tuple, list)):
+            raise TypeError("'pdu_sequence' is not list or tuple type")
+        if not all([isinstance(pdu, AbstractPDU) for pdu in pdu_sequence]):
+            raise ValueError("'pdu_sequence' does not contain AbstractPDU instances only")
+
+    @property
+    def pdu_sequence(self) -> PDUs:
+        """
+        Get PDUs that were sent/received to transmit this message over any bus.
+
+        :return: PDUs list that were sent/received or empty list if this message was never transmitted.
+        """
+        return self.__pdu_sequence
+
+    @property
+    def raw_message(self) -> TypingRawMessage:
+        """Raw message that this message carries."""
+        return self.__raw_message
+
+    @property
+    def addressing(self) -> Optional[AddressingType]:  # pylint: disable=unsubscriptable-object
+        """
+        Addressing type over which this message was transmitted.
+
+        :return: Addressing over which this message was sent/received. None if messages was never transmitted.
+        """
+        return self.pdu_sequence[0].addressing if self.pdu_sequence else None
diff --git a/uds/messages/pdu.py b/uds/messages/pdu.py
new file mode 100644
index 00000000..8890b563
--- /dev/null
+++ b/uds/messages/pdu.py
@@ -0,0 +1,30 @@
+"""Common implementation of UDS PDU (Protocol Data Unit) for all bus types."""
+
+__all__ = ["AbstractPDU"]
+
+from typing import Optional
+from abc import ABC, abstractmethod
+
+from .addressing import AddressingType
+
+
+class AbstractPDU(ABC):
+    """Abstract definition of Protocol Data Unit."""
+
+    @abstractmethod
+    def __init__(self) -> None:  # TODO: update
+        """Create storage related to a single PDU."""
+
+    @property
+    @abstractmethod
+    def addressing(self) -> Optional[AddressingType]:
+        """
+        Get addressing type over which this PDU was transmitted/received.
+
+        :return: Addressing over which the PDU was transmitted/received. None if PDU was not transmitted/received.
+        """
+
+    @property  # noqa
+    @abstractmethod
+    def pdu_type(self):  # TODO: update typing
+        """Getter of this PDU type."""
diff --git a/uds/messages/request.py b/uds/messages/request.py
new file mode 100644
index 00000000..11043206
--- /dev/null
+++ b/uds/messages/request.py
@@ -0,0 +1,13 @@
+"""Module with UDS request messages implementation."""
+
+__all__ = ["UdsRequest"]
+
+from .base_message import UdsMessage
+
+
+class UdsRequest(UdsMessage):
+    """
+    Storage for a single diagnostic request message.
+
+    UDS request is always sent by a client and received by a server.
+    """
diff --git a/uds/messages/response.py b/uds/messages/response.py
new file mode 100644
index 00000000..5bed1673
--- /dev/null
+++ b/uds/messages/response.py
@@ -0,0 +1,45 @@
+"""Module with UDS response messages implementation."""
+
+__all__ = ["UdsResponse", "UdsResponseType"]
+
+from enum import Enum
+
+from .base_message import UdsMessage
+from .service_identifiers import RequestSID, ResponseSID
+
+
+class UdsResponseType(Enum):
+    """
+    Types of UDS response messages.
+
+    Options:
+    - POSITIVE - response messages
+        WARNING! Positive response messages might be incompatible with messaging database
+    - NEGATIVE - negative response messages in format [0x7F, SID, NRC]
+        Where:
+        SID - Service Identifier of request (identifies request to which it is response)
+        NRC - Negative Response Code (provides reason why responded negatively)
+    - INVALID - response messages that is incompatible with UDS standard
+    """
+
+    POSITIVE = "Positive Response Message"
+    NEGATIVE = "Negative Response Message"
+    INVALID = "Invalid"
+
+
+class UdsResponse(UdsMessage):
+    """
+    Storage for a single diagnostic response message.
+
+    UDS response is always sent by a server and received by a client.
+    """
+
+    def get_response_type(self) -> UdsResponseType:
+        """Get type of this response message."""
+        if self.raw_message:
+            if len(self.raw_message) == 3 and self.raw_message[0] == ResponseSID.NegativeResponse and \
+                    RequestSID.is_request_sid(value=self.raw_message[1]):
+                return UdsResponseType.NEGATIVE
+            if ResponseSID.is_response_sid(self.raw_message[0]):
+                return UdsResponseType.POSITIVE
+        return UdsResponseType.INVALID
diff --git a/uds/messages/service_identifiers.py b/uds/messages/service_identifiers.py
new file mode 100644
index 00000000..a2b56954
--- /dev/null
+++ b/uds/messages/service_identifiers.py
@@ -0,0 +1,93 @@
+"""Module with data and implementation related to Service Identifiers (SID)."""
+
+from aenum import IntEnum, unique, extend_enum
+
+__all__ = ["RequestSID", "ResponseSID"]
+
+
+@unique
+class RequestSID(IntEnum):
+    """
+    Storage for all known Service Identifiers (SID).
+
+    SID is always the first byte of the request messages. In this enum, only the valid values are defined.
+    """
+
+    @classmethod
+    def is_request_sid(cls, value: int) -> bool:
+        """
+        Check whether given value is Service Identifier (SID).
+
+        :param value: Value to check.
+
+        :return: True if value is int of known SID, else False.
+        """
+        try:
+            cls(value)
+            return True
+        except ValueError:
+            return False
+
+    # Diagnostic and communication management - more information in ISO 14229-1:2020, chapter 10
+    DiagnosticSessionControl = 0x10  # noqa: F841
+    ECUReset = 0x11  # noqa: F841
+    SecurityAccess = 0x27  # noqa: F841
+    CommunicationControl = 0x28  # noqa: F841
+    Authentication = 0x29  # noqa: F841
+    TesterPresent = 0x3E  # noqa: F841
+    ControlDTCSetting = 0x85  # noqa: F841
+    ResponseOnEvent = 0x86  # noqa: F841
+    LinkControl = 0x87  # noqa: F841
+    # Data transmission - more information in ISO 14229-1:2020, chapter 11
+    ReadDataByIdentifier = 0x22  # noqa: F841
+    ReadMemoryByAddress = 0x23  # noqa: F841
+    ReadScalingDataByIdentifier = 0x24  # noqa: F841
+    ReadDataByPeriodicIdentifier = 0x2A  # noqa: F841
+    DynamicallyDefineDataIdentifier = 0x2C  # noqa: F841
+    WriteDataByIdentifier = 0x2E  # noqa: F841
+    WriteMemoryByAddress = 0x3D  # noqa: F841
+    # Stored data transmission - more information in ISO 14229-1:2020, chapter 12
+    ClearDiagnosticInformation = 0x14  # noqa: F841
+    ReadDTCInformation = 0x19  # noqa: F841
+    # InputOutput control - more information in ISO 14229-1:2020, chapter 13
+    InputOutputControlByIdentifier = 0x2F  # noqa: F841
+    # Routine - more information in ISO 14229-1:2020, chapter 14
+    RoutineControl = 0x31  # noqa: F841
+    # Upload download - more information in ISO 14229-1:2020, chapter 15
+    RequestDownload = 0x34  # noqa: F841
+    RequestUpload = 0x35  # noqa: F841
+    TransferData = 0x36  # noqa: F841
+    RequestTransferExit = 0x37  # noqa: F841
+    RequestFileTransfer = 0x38  # noqa: F841
+    SecuredDataTransmission = 0x84  # noqa: F841
+
+
+@unique
+class ResponseSID(IntEnum):
+    """
+    Storage for all known Response Service Identifiers (RSID).
+
+    RSID is always the first byte of the response messages. In this enum, only the valid values are defined.
+    """
+
+    @classmethod
+    def is_response_sid(cls, value: int) -> bool:
+        """
+        Check whether given value is Response Service Identifier (RSID).
+
+        :param value: Value to check.
+
+        :return: True if value is int of known RSID, else False.
+        """
+        try:
+            cls(value)
+            return True
+        except ValueError:
+            return False
+
+    NegativeResponse = 0x7F
+
+
+# extend 'ResponseSID' with members that were defined in RequestSID
+for request_name, request_enum_member in RequestSID.__members__.items():
+    extend_enum(ResponseSID, request_name, request_enum_member.value + 0x40)


Test patch:
diff --git a/tests/uds/messages/__init__.py b/tests/uds/messages/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/uds/messages/conftest.py b/tests/uds/messages/conftest.py
new file mode 100644
index 00000000..34f7aea8
--- /dev/null
+++ b/tests/uds/messages/conftest.py
@@ -0,0 +1,6 @@
+from pytest import fixture
+
+
+@fixture
+def example_raw_message():
+    return 0x10, 0x01
diff --git a/tests/uds/messages/test_base_message.py b/tests/uds/messages/test_base_message.py
new file mode 100644
index 00000000..ccce080e
--- /dev/null
+++ b/tests/uds/messages/test_base_message.py
@@ -0,0 +1,127 @@
+import pytest
+from mock import Mock
+
+from uds.messages.base_message import UdsMessage, AbstractPDU
+
+
+class TestUdsMessage:
+    """Tests for 'UdsMessage' class."""
+
+    def setup(self):
+        self.mock_uds_message = Mock(spec=UdsMessage)
+
+    # __init__
+
+    @pytest.mark.parametrize("raw_message", [
+        [0x10, 0x01],
+        (0x22, 0x10, 0x01, 0x12, 0x34),
+        [0x51, 0x03],
+        (0x54, ),
+    ])
+    @pytest.mark.parametrize("pdu_sequence", [
+        [Mock(spec=AbstractPDU), Mock(spec=AbstractPDU)],
+        (Mock(spec=AbstractPDU), )
+    ])
+    def test_init__valid_values(self, raw_message, pdu_sequence):
+        UdsMessage.__init__(self=self.mock_uds_message, raw_message=raw_message, pdu_sequence=pdu_sequence)
+        # data validation
+        self.mock_uds_message._UdsMessage__validate_raw_message.assert_called_once_with(raw_message=raw_message)
+        self.mock_uds_message._UdsMessage__validate_pdu_sequence.assert_called_once_with(pdu_sequence=pdu_sequence)
+        # raw_message verification
+        assert isinstance(self.mock_uds_message._UdsMessage__raw_message, tuple), \
+            "Value must be converted to tuple to be immutable"
+        assert len(self.mock_uds_message._UdsMessage__raw_message) == len(raw_message), "Elements must be unchanged"
+        assert all(value_set == value_provided for value_set, value_provided in
+                   zip(self.mock_uds_message._UdsMessage__raw_message, raw_message)), "Elements must be unchanged"
+        # pdu_sequence verification
+        assert isinstance(self.mock_uds_message._UdsMessage__pdu_sequence, tuple), \
+            "Value must be converted to tuple to be immutable"
+        assert len(self.mock_uds_message._UdsMessage__pdu_sequence) == len(pdu_sequence), "Elements must be unchanged"
+        assert all(value_set == value_provided for value_set, value_provided in
+                   zip(self.mock_uds_message._UdsMessage__pdu_sequence, pdu_sequence)), "Elements must be unchanged"
+
+    def test_init__no_pdus(self, example_raw_message):
+        UdsMessage.__init__(self=self.mock_uds_message, raw_message=example_raw_message)
+        assert self.mock_uds_message._UdsMessage__pdu_sequence == ()
+
+    # __validate_raw_message
+
+    @pytest.mark.parametrize("raw_message", [
+        [0x10, 0x01],
+        (0x22, 0x10, 0x01, 0x12, 0x34),
+        [0x51, 0x03],
+        (0x54, ),
+    ])
+    def test_validate_raw_message__valid(self, raw_message):
+        assert UdsMessage._UdsMessage__validate_raw_message(raw_message=raw_message) is None
+
+    @pytest.mark.parametrize("raw_message", [
+        {0x10, 0x01},
+        "abcdef",
+        b"\x10\x01"
+    ])
+    def test_validate_raw_message__wrong_type(self, raw_message):
+        with pytest.raises(TypeError):
+            UdsMessage._UdsMessage__validate_raw_message(raw_message=raw_message)
+
+    @pytest.mark.parametrize("raw_message", [
+        [-1],
+        [0x10, 0x100],
+        [0x22, "10", "00"],
+        [0x11, 1.]
+    ])
+    def test_validate_raw_message__wrong_value(self, raw_message):
+        with pytest.raises(ValueError):
+            UdsMessage._UdsMessage__validate_raw_message(raw_message=raw_message)
+
+    # __validate_pdu_sequence
+
+    @pytest.mark.parametrize("pdu_sequence", [
+        [Mock(spec=AbstractPDU)],
+        (Mock(spec=AbstractPDU), Mock(spec=AbstractPDU))
+    ])
+    def test_validate_pdu_sequence__valid(self, pdu_sequence):
+        assert UdsMessage._UdsMessage__validate_pdu_sequence(pdu_sequence=pdu_sequence) is None
+
+    @pytest.mark.parametrize("pdu_sequence", [
+        {Mock(spec=AbstractPDU), Mock(spec=AbstractPDU)}
+    ])
+    def test_validate_pdu_sequence__wrong_type(self, pdu_sequence):
+        with pytest.raises(TypeError):
+            UdsMessage._UdsMessage__validate_pdu_sequence(pdu_sequence=pdu_sequence)
+
+    @pytest.mark.parametrize("pdu_sequence", [
+        [-1],
+        (Mock(spec=AbstractPDU), "10"),
+    ])
+    def test_validate_pdu_sequence__wrong_value(self, pdu_sequence):
+        with pytest.raises(ValueError):
+            UdsMessage._UdsMessage__validate_pdu_sequence(pdu_sequence=pdu_sequence)
+
+    # addressing
+
+    def test_addressing__undefined(self):
+        self.mock_uds_message.pdu_sequence = []
+        assert UdsMessage.addressing.fget(self=self.mock_uds_message) is None
+
+    @pytest.mark.parametrize("pdu_sequence", [
+        [Mock(spec=AbstractPDU)],
+        (Mock(spec=AbstractPDU), Mock(spec=AbstractPDU))
+    ])
+    def test_addressing__got_from_pdu(self, pdu_sequence):
+        self.mock_uds_message.pdu_sequence = pdu_sequence
+        assert UdsMessage.addressing.fget(self=self.mock_uds_message) is pdu_sequence[0].addressing
+
+    # pdu_sequence
+
+    @pytest.mark.parametrize("pdu_sequence", [0, Mock(), "xyz"])
+    def test_pdu_sequence(self, pdu_sequence):
+        self.mock_uds_message._UdsMessage__pdu_sequence = pdu_sequence
+        assert UdsMessage.pdu_sequence.fget(self=self.mock_uds_message) is pdu_sequence
+
+    # raw_message
+
+    @pytest.mark.parametrize("raw_message", [0, Mock(), "xyz"])
+    def test_raw_message(self, raw_message):
+        self.mock_uds_message._UdsMessage__raw_message = raw_message
+        assert UdsMessage.raw_message.fget(self=self.mock_uds_message) is raw_message
diff --git a/tests/uds/messages/test_response.py b/tests/uds/messages/test_response.py
new file mode 100644
index 00000000..660b6fee
--- /dev/null
+++ b/tests/uds/messages/test_response.py
@@ -0,0 +1,38 @@
+import pytest
+from mock import Mock
+
+from uds.messages.response import UdsResponse, UdsResponseType
+
+
+class TestsUdsResponse:
+
+    def setup(self):
+        self.mock_uds_response = Mock(spec=UdsResponse)
+
+    # get_response_type
+
+    @pytest.mark.parametrize("positive_response_message", [
+        [0x54],
+        [0x51, 0x01],
+    ])
+    def test_get_response_type__positive(self, positive_response_message):
+        self.mock_uds_response.raw_message = positive_response_message
+        assert UdsResponse.get_response_type(self=self.mock_uds_response) == UdsResponseType.POSITIVE
+
+    @pytest.mark.parametrize("negative_response_message", [
+        [0x7F, 0x10, 0x22],
+        [0x7F, 0x31, 0xAA],
+        [0x7F, 0x22, 0x10],
+    ])
+    def test_get_response_type__negative(self, negative_response_message):
+        self.mock_uds_response.raw_message = negative_response_message
+        assert UdsResponse.get_response_type(self=self.mock_uds_response) == UdsResponseType.NEGATIVE
+
+    @pytest.mark.parametrize("request_message", [
+        [0x10, 0x03],
+        [0x11, 0x01],
+        [0x14, 0xFF, 0xFF, 0xFF]
+    ])
+    def test_get_response_type__invalid(self, request_message):
+        self.mock_uds_response.raw_message = request_message
+        assert UdsResponse.get_response_type(self=self.mock_uds_response) == UdsResponseType.INVALID
diff --git a/tests/uds/messages/test_service_identifiers.py b/tests/uds/messages/test_service_identifiers.py
new file mode 100644
index 00000000..43a0335c
--- /dev/null
+++ b/tests/uds/messages/test_service_identifiers.py
@@ -0,0 +1,42 @@
+import pytest
+
+from uds.messages.service_identifiers import RequestSID, ResponseSID
+
+
+class TestRequestSID:
+    """Tests for RequestSID enum"""
+
+    EXAMPLE_REQUEST_SIDS = [0x10, 0x14, 0x22, 0x86]
+    EXAMPLE_RESPONSE_SIDS = [0x50, 0x54, 0x62, 0xC6, 0x7F]
+
+    @pytest.mark.parametrize("valid_sid", EXAMPLE_REQUEST_SIDS)
+    def test_is_request_sid__true(self, valid_sid):
+        assert RequestSID.is_request_sid(value=valid_sid) is True
+
+    @pytest.mark.parametrize("invalid_value", [-1, "something", 2.3] + EXAMPLE_RESPONSE_SIDS)
+    def test_is_request_sid__false(self, invalid_value):
+        assert RequestSID.is_request_sid(value=invalid_value) is False
+
+
+class TestResponseSID:
+    """Tests for ResponseSID Enum"""
+
+    EXAMPLE_REQUEST_SIDS = TestRequestSID.EXAMPLE_REQUEST_SIDS
+    EXAMPLE_RESPONSE_SIDS = TestRequestSID.EXAMPLE_RESPONSE_SIDS
+
+    def test_number_of_members(self):
+        assert len(ResponseSID) == len(RequestSID) + 1, \
+            "ResponseSID shall contain RSID for each SID and one additional element for 'NegativeResponse'."
+
+    @pytest.mark.parametrize("request_sid_member", list(RequestSID))
+    def test_rsid_members(self, request_sid_member):
+        assert ResponseSID[request_sid_member.name] == request_sid_member.value + 0x40, \
+            "Verify each ResponseSID member has correct value (SID + 0x40)."
+
+    @pytest.mark.parametrize("valid_rsid", EXAMPLE_RESPONSE_SIDS)
+    def test_is_response_sid__true(self, valid_rsid):
+        assert ResponseSID.is_response_sid(value=valid_rsid) is True
+
+    @pytest.mark.parametrize("invalid_value", [-1, "something", 2.3] + EXAMPLE_REQUEST_SIDS)
+    def test_is_response_sid__false(self, invalid_value):
+        assert ResponseSID.is_response_sid(value=invalid_value) is False