diff --git a/.codecov.yml b/.codecov.yml index 5fbba5e1..b8ee47eb 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -3,7 +3,7 @@ codecov: uploads: false coverage: - range: 80...100 + range: 50...100 round: down precision: 2 status: @@ -23,6 +23,7 @@ coverage: flags: - integration-tests integration-tests-branch: + target: 50% flags: - integration-tests-branch performance-tests: @@ -33,3 +34,4 @@ comment: layout: "reach, diff, flags, files" behavior: default require_changes: false + show_carryforward_flags: true diff --git a/examples/can/python-can/kvaser/config.wcc b/examples/can/python-can/kvaser/config.wcc index 0f58375a..12670ced 100644 --- a/examples/can/python-can/kvaser/config.wcc +++ b/examples/can/python-can/kvaser/config.wcc @@ -86,7 +86,7 @@ StandardText 0 - 0 + 1 0 0 0 @@ -162,7 +162,7 @@ 1 - 1,0,2 + 0,1,2 0,1,0,0 @@ -215,7 +215,7 @@ 0 - 0 + 1 diff --git a/examples/can/python-can/kvaser/python_can_timing_issue.py b/examples/can/python-can/kvaser/python_can_timing_issue.py index c4e06425..4aa70371 100644 --- a/examples/can/python-can/kvaser/python_can_timing_issue.py +++ b/examples/can/python-can/kvaser/python_can_timing_issue.py @@ -14,7 +14,8 @@ message = Message(data=[0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0], arbitration_id=0x100) - for _ in range(100): + for _ in range(10): + timestamp_before_send = time() Timer(interval=0.1, function=kvaser_interface_1.send, args=(message,)).start() sent_message = buffered_reader.get_message(timeout=1) @@ -22,9 +23,10 @@ print(f"-----------------------------------------------\n" f"Result:\n" + f"Timestamp before send: {timestamp_before_send}\n" f"Message timestamp: {sent_message.timestamp}\n" f"Current timestamp: {timestamp_after_send}\n" - f"Message timestamp <= Current timestamp: {sent_message.timestamp <= timestamp_after_send} (expected `True`)") + f"Timestamp before send <= Message timestamp <= Current timestamp: {timestamp_before_send <= sent_message.timestamp <= timestamp_after_send} (expected `True`)") kvaser_interface_1.shutdown() kvaser_interface_2.shutdown() diff --git a/examples/can/python-can/kvaser/python_can_timing_issue_2.py b/examples/can/python-can/kvaser/python_can_timing_issue_2.py new file mode 100644 index 00000000..13a65e24 --- /dev/null +++ b/examples/can/python-can/kvaser/python_can_timing_issue_2.py @@ -0,0 +1,32 @@ +"""UDS Issue: https://github.com/mdabrowski1990/uds/issues/228""" + +from time import time + +from can import BufferedReader, Bus, Message, Notifier + +if __name__ == "__main__": + kvaser_interface_1 = Bus(interface="kvaser", channel=0, fd=True, receive_own_messages=True) + kvaser_interface_2 = Bus(interface="kvaser", channel=1, fd=True, receive_own_messages=True) # connected with bus 1 + + buffered_reader = BufferedReader() + notifier = Notifier(bus=kvaser_interface_1, listeners=[buffered_reader]) + + message = Message(data=[0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0], arbitration_id=0x100) + + for _ in range(10): + timestamp_before_send = time() + kvaser_interface_1.send(message) + sent_message = buffered_reader.get_message(timeout=1) + timestamp_after_send = time() + + print(f"-----------------------------------------------\n" + f"Result:\n" + f"Timestamp before send: {timestamp_before_send}\n" + f"Message timestamp: {sent_message.timestamp}\n" + f"Current timestamp: {timestamp_after_send}\n" + f"Message timestamp - Timestamp before send: {sent_message.timestamp - timestamp_before_send} (expected > 0)\n" + f"Current timestamp - Message timestamp: {timestamp_after_send - sent_message.timestamp} (expected > 0)\n" + f"Timestamp before send <= Message timestamp <= Current timestamp: {timestamp_before_send <= sent_message.timestamp <= timestamp_after_send} (expected `True`)") + + kvaser_interface_1.shutdown() + kvaser_interface_2.shutdown() diff --git a/tests/software_tests/segmentation/test_can_segmenter.py b/tests/software_tests/segmentation/test_can_segmenter.py index c86f4f15..7d72e0a2 100644 --- a/tests/software_tests/segmentation/test_can_segmenter.py +++ b/tests/software_tests/segmentation/test_can_segmenter.py @@ -199,6 +199,196 @@ def test_filler_byte__set(self, value): self.mock_validate_raw_byte.assert_called_once_with(value) assert self.mock_can_segmenter._CanSegmenter__filler_byte == value + # __physical_segmentation + + @pytest.mark.parametrize("message_payload_size", [CanFirstFrameHandler.MAX_LONG_FF_DL_VALUE + 1, + CanFirstFrameHandler.MAX_LONG_FF_DL_VALUE + 23]) + @patch(f"{SCRIPT_LOCATION}.len") + def test_physical_segmentation__too_long(self, mock_len, message_payload_size): + mock_len.return_value = message_payload_size + mock_message = Mock(spec=UdsMessage, addressing_type=AddressingType.PHYSICAL) + with pytest.raises(SegmentationError): + CanSegmenter._CanSegmenter__physical_segmentation(self=self.mock_can_segmenter, message=mock_message) + mock_len.assert_called_once_with(mock_message.payload) + + @pytest.mark.parametrize("message_payload_size, max_payload", [ + (2, 2), + (60, 62) + ]) + @pytest.mark.parametrize("physical_ai", [{"p1": 1, "p2": 2, "p3": 3}, {"abc": "something", "xyz": "else"}]) + @patch(f"{SCRIPT_LOCATION}.len") + def test_physical_segmentation__sf_with_data_optimization(self, mock_len, + message_payload_size, max_payload, physical_ai): + mock_len.return_value = message_payload_size + self.mock_get_max_sf_payload_size.return_value = max_payload + self.mock_can_segmenter.use_data_optimization = True + self.mock_can_segmenter.tx_packets_physical_ai = physical_ai + mock_message = Mock(spec=UdsMessage, addressing_type=AddressingType.PHYSICAL) + packets = CanSegmenter._CanSegmenter__physical_segmentation(self=self.mock_can_segmenter, + message=mock_message) + assert isinstance(packets, tuple) + assert len(packets) == 1 + assert packets[0] == self.mock_can_packet_class.return_value + self.mock_get_max_sf_payload_size.assert_called_once_with( + addressing_format=self.mock_can_segmenter.addressing_format, + dlc=self.mock_can_segmenter.dlc) + mock_len.assert_called_once_with(mock_message.payload) + self.mock_can_packet_class.assert_called_once_with(packet_type=CanPacketType.SINGLE_FRAME, + payload=mock_message.payload, + dlc=None, + filler_byte=self.mock_can_segmenter.filler_byte, + **physical_ai) + + @pytest.mark.parametrize("message_payload_size, max_payload", [ + (2, 2), + (60, 62) + ]) + @pytest.mark.parametrize("physical_ai", [{"p1": 1, "p2": 2, "p3": 3}, {"abc": "something", "xyz": "else"}]) + @patch(f"{SCRIPT_LOCATION}.len") + def test_physical_segmentation__sf_without_data_optimization(self, mock_len, + message_payload_size, max_payload, physical_ai): + mock_len.return_value = message_payload_size + self.mock_get_max_sf_payload_size.return_value = max_payload + self.mock_can_segmenter.use_data_optimization = False + self.mock_can_segmenter.tx_packets_physical_ai = physical_ai + mock_message = Mock(spec=UdsMessage, addressing_type=AddressingType.PHYSICAL) + packets = CanSegmenter._CanSegmenter__physical_segmentation(self=self.mock_can_segmenter, + message=mock_message) + assert isinstance(packets, tuple) + assert len(packets) == 1 + assert packets[0] == self.mock_can_packet_class.return_value + self.mock_get_max_sf_payload_size.assert_called_once_with( + addressing_format=self.mock_can_segmenter.addressing_format, dlc=self.mock_can_segmenter.dlc) + mock_len.assert_called_once_with(mock_message.payload) + self.mock_can_packet_class.assert_called_once_with(packet_type=CanPacketType.SINGLE_FRAME, + payload=mock_message.payload, + dlc=self.mock_can_segmenter.dlc, + filler_byte=self.mock_can_segmenter.filler_byte, + **physical_ai) + + @pytest.mark.parametrize("message_payload_size, max_payload, ff_size, cf_size", [ + (3, 2, 1, 2), + (150, 7, 6, 7), + ]) + @pytest.mark.parametrize("physical_ai", [{"p1": 1, "p2": 2, "p3": 3}, {"abc": "something", "xyz": "else"}]) + @patch(f"{SCRIPT_LOCATION}.len") + def test_physical_segmentation__ff_cf_with_data_optimization(self, mock_len, physical_ai, + message_payload_size, max_payload, ff_size, + cf_size): + mock_len.return_value = message_payload_size + self.mock_get_max_sf_payload_size.return_value = max_payload + self.mock_get_ff_payload_size.return_value = ff_size + self.mock_get_max_cf_payload_size.return_value = cf_size + self.mock_can_segmenter.use_data_optimization = True + self.mock_can_segmenter.tx_packets_physical_ai = physical_ai + mock_message = Mock(spec=UdsMessage, addressing_type=AddressingType.PHYSICAL, + payload=range(message_payload_size)) + packets = CanSegmenter._CanSegmenter__physical_segmentation(self=self.mock_can_segmenter, + message=mock_message) + assert isinstance(packets, tuple) + cf_number = (message_payload_size - ff_size + cf_size - 1) // cf_size + assert len(packets) == 1 + cf_number + self.mock_get_max_sf_payload_size.assert_called_once_with( + addressing_format=self.mock_can_segmenter.addressing_format, dlc=self.mock_can_segmenter.dlc) + mock_len.assert_called_once_with(mock_message.payload) + last_cf_payload = (message_payload_size - ff_size) % cf_size + if last_cf_payload == 0: + last_cf_payload = cf_size + ff_call = call(packet_type=CanPacketType.FIRST_FRAME, + payload=mock_message.payload[:ff_size], + dlc=self.mock_can_segmenter.dlc, + data_length=message_payload_size, + **physical_ai) + cf_calls = [] + for cf_i in range(cf_number - 1): + cf_payload_i_start = ff_size + cf_i * cf_size + cf_payload_i_stop = cf_payload_i_start + cf_size + cf_call = call(packet_type=CanPacketType.CONSECUTIVE_FRAME, + payload=mock_message.payload[cf_payload_i_start:cf_payload_i_stop], + sequence_number=(cf_i + 1) % 0x10, + dlc=self.mock_can_segmenter.dlc, + filler_byte=self.mock_can_segmenter.filler_byte, + **physical_ai) + cf_calls.append(cf_call) + last_cf_call = call(packet_type=CanPacketType.CONSECUTIVE_FRAME, + payload=mock_message.payload[-last_cf_payload:], + dlc=None, + filler_byte=self.mock_can_segmenter.filler_byte, + sequence_number=cf_number % 16, + **physical_ai) + self.mock_can_packet_class.assert_has_calls([ff_call, *cf_calls, last_cf_call]) + + # __functional_segmentation + + @pytest.mark.parametrize("message_payload_size, max_payload", [ + (2, 1), + (60, 59) + ]) + @patch(f"{SCRIPT_LOCATION}.len") + def test_functional_segmentation__too_long(self, mock_len, message_payload_size, max_payload): + mock_len.return_value = message_payload_size + self.mock_get_max_sf_payload_size.return_value = max_payload + mock_message = Mock(spec=UdsMessage, addressing_type=AddressingType.FUNCTIONAL) + with pytest.raises(SegmentationError): + CanSegmenter._CanSegmenter__functional_segmentation(self=self.mock_can_segmenter, message=mock_message) + self.mock_get_max_sf_payload_size.assert_called_once_with( + addressing_format=self.mock_can_segmenter.addressing_format, dlc=self.mock_can_segmenter.dlc) + mock_len.assert_called_once_with(mock_message.payload) + + @pytest.mark.parametrize("message_payload_size, max_payload", [ + (2, 2), + (60, 62) + ]) + @pytest.mark.parametrize("functional_ai", [{"p1": 1, "p2": 2, "p3": 3}, {"abc": "something", "xyz": "else"}]) + @patch(f"{SCRIPT_LOCATION}.len") + def test_functional_segmentation__with_data_optimization(self, mock_len, message_payload_size, max_payload, + functional_ai): + mock_len.return_value = message_payload_size + self.mock_get_max_sf_payload_size.return_value = max_payload + self.mock_can_segmenter.use_data_optimization = True + self.mock_can_segmenter.tx_packets_functional_ai = functional_ai + mock_message = Mock(spec=UdsMessage, addressing_type=AddressingType.FUNCTIONAL) + packets = CanSegmenter._CanSegmenter__functional_segmentation(self=self.mock_can_segmenter, + message=mock_message) + assert isinstance(packets, tuple) + assert len(packets) == 1 + assert packets[0] == self.mock_can_packet_class.return_value + self.mock_get_max_sf_payload_size.assert_called_once_with( + addressing_format=self.mock_can_segmenter.addressing_format, dlc=self.mock_can_segmenter.dlc) + mock_len.assert_called_once_with(mock_message.payload) + self.mock_can_packet_class.assert_called_once_with(packet_type=CanPacketType.SINGLE_FRAME, + payload=mock_message.payload, + dlc=None, + filler_byte=self.mock_can_segmenter.filler_byte, + **functional_ai) + + @pytest.mark.parametrize("message_payload_size, max_payload", [ + (2, 2), + (60, 62) + ]) + @pytest.mark.parametrize("functional_ai", [{"p1": 1, "p2": 2, "p3": 3}, {"abc": "something", "xyz": "else"}]) + @patch(f"{SCRIPT_LOCATION}.len") + def test_functional_segmentation__without_data_optimization(self, mock_len, message_payload_size, max_payload, + functional_ai): + mock_len.return_value = message_payload_size + self.mock_get_max_sf_payload_size.return_value = max_payload + self.mock_can_segmenter.use_data_optimization = False + self.mock_can_segmenter.tx_packets_functional_ai = functional_ai + mock_message = Mock(spec=UdsMessage, addressing_type=AddressingType.FUNCTIONAL) + packets = CanSegmenter._CanSegmenter__functional_segmentation(self=self.mock_can_segmenter, + message=mock_message) + assert isinstance(packets, tuple) + assert len(packets) == 1 + assert packets[0] == self.mock_can_packet_class.return_value + self.mock_get_max_sf_payload_size.assert_called_once_with( + addressing_format=self.mock_can_segmenter.addressing_format, dlc=self.mock_can_segmenter.dlc) + mock_len.assert_called_once_with(mock_message.payload) + self.mock_can_packet_class.assert_called_once_with(packet_type=CanPacketType.SINGLE_FRAME, + payload=mock_message.payload, + dlc=self.mock_can_segmenter.dlc, + filler_byte=self.mock_can_segmenter.filler_byte, + **functional_ai) + # is_input_packet @pytest.mark.parametrize("frame_can_id, frame_data", [ @@ -346,6 +536,46 @@ def test_is_desegmented_message__first_frame__true(self, packets): self.mock_is_initial_packet_type.assert_has_calls( [call(packet.packet_type) for packet in packets]) + # get_flow_control_packet + + @pytest.mark.parametrize("flow_status, block_size, st_min", [ + (Mock(), Mock(), Mock()), + ("flow_status", "block_size", "st_min"), + ]) + @pytest.mark.parametrize("physical_ai", [{"p1": 1, "p2": 2, "p3": 3}, {"abc": "something", "xyz": "else"}]) + def test_get_flow_control_packet__data_optimization(self, flow_status, block_size, st_min, physical_ai): + self.mock_can_segmenter.tx_packets_physical_ai = physical_ai + self.mock_can_segmenter.use_data_optimization = True + assert CanSegmenter.get_flow_control_packet(self=self.mock_can_segmenter, flow_status=flow_status, + block_size=block_size, st_min=st_min) \ + == self.mock_can_packet_class.return_value + self.mock_can_packet_class.assert_called_once_with(packet_type=CanPacketType.FLOW_CONTROL, + dlc=None, + filler_byte=self.mock_can_segmenter.filler_byte, + flow_status=flow_status, + block_size=block_size, + st_min=st_min, + **physical_ai) + + @pytest.mark.parametrize("flow_status, block_size, st_min", [ + (Mock(), Mock(), Mock()), + ("flow_status", "block_size", "st_min"), + ]) + @pytest.mark.parametrize("physical_ai", [{"p1": 1, "p2": 2, "p3": 3}, {"abc": "something", "xyz": "else"}]) + def test_get_flow_control_packet__no_data_optimization(self, flow_status, block_size, st_min, physical_ai): + self.mock_can_segmenter.tx_packets_physical_ai = physical_ai + self.mock_can_segmenter.use_data_optimization = False + assert CanSegmenter.get_flow_control_packet(self=self.mock_can_segmenter, flow_status=flow_status, + block_size=block_size, st_min=st_min) \ + == self.mock_can_packet_class.return_value + self.mock_can_packet_class.assert_called_once_with(packet_type=CanPacketType.FLOW_CONTROL, + dlc=self.mock_can_segmenter.dlc, + filler_byte=self.mock_can_segmenter.filler_byte, + flow_status=flow_status, + block_size=block_size, + st_min=st_min, + **physical_ai) + # desegmentation @pytest.mark.parametrize("packets", [ @@ -468,196 +698,6 @@ def test_segmentation__physical(self, mock_isinstance): == self.mock_can_segmenter._CanSegmenter__physical_segmentation.return_value self.mock_can_segmenter._CanSegmenter__physical_segmentation.assert_called_once_with(mock_message) - # __physical_segmentation - - @pytest.mark.parametrize("message_payload_size", [CanFirstFrameHandler.MAX_LONG_FF_DL_VALUE + 1, - CanFirstFrameHandler.MAX_LONG_FF_DL_VALUE + 23]) - @patch(f"{SCRIPT_LOCATION}.len") - def test_physical_segmentation__too_long(self, mock_len, message_payload_size): - mock_len.return_value = message_payload_size - mock_message = Mock(spec=UdsMessage, addressing_type=AddressingType.PHYSICAL) - with pytest.raises(SegmentationError): - CanSegmenter._CanSegmenter__physical_segmentation(self=self.mock_can_segmenter, message=mock_message) - mock_len.assert_called_once_with(mock_message.payload) - - @pytest.mark.parametrize("message_payload_size, max_payload", [ - (2, 2), - (60, 62) - ]) - @pytest.mark.parametrize("physical_ai", [{"p1": 1, "p2": 2, "p3": 3}, {"abc": "something", "xyz": "else"}]) - @patch(f"{SCRIPT_LOCATION}.len") - def test_physical_segmentation__sf_with_data_optimization(self, mock_len, - message_payload_size, max_payload, physical_ai): - mock_len.return_value = message_payload_size - self.mock_get_max_sf_payload_size.return_value = max_payload - self.mock_can_segmenter.use_data_optimization = True - self.mock_can_segmenter.tx_packets_physical_ai = physical_ai - mock_message = Mock(spec=UdsMessage, addressing_type=AddressingType.PHYSICAL) - packets = CanSegmenter._CanSegmenter__physical_segmentation(self=self.mock_can_segmenter, - message=mock_message) - assert isinstance(packets, tuple) - assert len(packets) == 1 - assert packets[0] == self.mock_can_packet_class.return_value - self.mock_get_max_sf_payload_size.assert_called_once_with( - addressing_format=self.mock_can_segmenter.addressing_format, - dlc=self.mock_can_segmenter.dlc) - mock_len.assert_called_once_with(mock_message.payload) - self.mock_can_packet_class.assert_called_once_with(packet_type=CanPacketType.SINGLE_FRAME, - payload=mock_message.payload, - dlc=None, - filler_byte=self.mock_can_segmenter.filler_byte, - **physical_ai) - - @pytest.mark.parametrize("message_payload_size, max_payload", [ - (2, 2), - (60, 62) - ]) - @pytest.mark.parametrize("physical_ai", [{"p1": 1, "p2": 2, "p3": 3}, {"abc": "something", "xyz": "else"}]) - @patch(f"{SCRIPT_LOCATION}.len") - def test_physical_segmentation__sf_without_data_optimization(self, mock_len, - message_payload_size, max_payload, physical_ai): - mock_len.return_value = message_payload_size - self.mock_get_max_sf_payload_size.return_value = max_payload - self.mock_can_segmenter.use_data_optimization = False - self.mock_can_segmenter.tx_packets_physical_ai = physical_ai - mock_message = Mock(spec=UdsMessage, addressing_type=AddressingType.PHYSICAL) - packets = CanSegmenter._CanSegmenter__physical_segmentation(self=self.mock_can_segmenter, - message=mock_message) - assert isinstance(packets, tuple) - assert len(packets) == 1 - assert packets[0] == self.mock_can_packet_class.return_value - self.mock_get_max_sf_payload_size.assert_called_once_with( - addressing_format=self.mock_can_segmenter.addressing_format, dlc=self.mock_can_segmenter.dlc) - mock_len.assert_called_once_with(mock_message.payload) - self.mock_can_packet_class.assert_called_once_with(packet_type=CanPacketType.SINGLE_FRAME, - payload=mock_message.payload, - dlc=self.mock_can_segmenter.dlc, - filler_byte=self.mock_can_segmenter.filler_byte, - **physical_ai) - - @pytest.mark.parametrize("message_payload_size, max_payload, ff_size, cf_size", [ - (3, 2, 1, 2), - (150, 7, 6, 7), - ]) - @pytest.mark.parametrize("physical_ai", [{"p1": 1, "p2": 2, "p3": 3}, {"abc": "something", "xyz": "else"}]) - @patch(f"{SCRIPT_LOCATION}.len") - def test_physical_segmentation__ff_cf_with_data_optimization(self, mock_len, physical_ai, - message_payload_size, max_payload, ff_size, - cf_size): - mock_len.return_value = message_payload_size - self.mock_get_max_sf_payload_size.return_value = max_payload - self.mock_get_ff_payload_size.return_value = ff_size - self.mock_get_max_cf_payload_size.return_value = cf_size - self.mock_can_segmenter.use_data_optimization = True - self.mock_can_segmenter.tx_packets_physical_ai = physical_ai - mock_message = Mock(spec=UdsMessage, addressing_type=AddressingType.PHYSICAL, - payload=range(message_payload_size)) - packets = CanSegmenter._CanSegmenter__physical_segmentation(self=self.mock_can_segmenter, - message=mock_message) - assert isinstance(packets, tuple) - cf_number = (message_payload_size - ff_size + cf_size - 1) // cf_size - assert len(packets) == 1 + cf_number - self.mock_get_max_sf_payload_size.assert_called_once_with( - addressing_format=self.mock_can_segmenter.addressing_format, dlc=self.mock_can_segmenter.dlc) - mock_len.assert_called_once_with(mock_message.payload) - last_cf_payload = (message_payload_size - ff_size) % cf_size - if last_cf_payload == 0: - last_cf_payload = cf_size - ff_call = call(packet_type=CanPacketType.FIRST_FRAME, - payload=mock_message.payload[:ff_size], - dlc=self.mock_can_segmenter.dlc, - data_length=message_payload_size, - **physical_ai) - cf_calls = [] - for cf_i in range(cf_number - 1): - cf_payload_i_start = ff_size + cf_i * cf_size - cf_payload_i_stop = cf_payload_i_start + cf_size - cf_call = call(packet_type=CanPacketType.CONSECUTIVE_FRAME, - payload=mock_message.payload[cf_payload_i_start:cf_payload_i_stop], - sequence_number=(cf_i + 1) % 0x10, - dlc=self.mock_can_segmenter.dlc, - filler_byte=self.mock_can_segmenter.filler_byte, - **physical_ai) - cf_calls.append(cf_call) - last_cf_call = call(packet_type=CanPacketType.CONSECUTIVE_FRAME, - payload=mock_message.payload[-last_cf_payload:], - dlc=None, - filler_byte=self.mock_can_segmenter.filler_byte, - sequence_number=cf_number % 16, - **physical_ai) - self.mock_can_packet_class.assert_has_calls([ff_call, *cf_calls, last_cf_call]) - - # __functional_segmentation - - @pytest.mark.parametrize("message_payload_size, max_payload", [ - (2, 1), - (60, 59) - ]) - @patch(f"{SCRIPT_LOCATION}.len") - def test_functional_segmentation__too_long(self, mock_len, message_payload_size, max_payload): - mock_len.return_value = message_payload_size - self.mock_get_max_sf_payload_size.return_value = max_payload - mock_message = Mock(spec=UdsMessage, addressing_type=AddressingType.FUNCTIONAL) - with pytest.raises(SegmentationError): - CanSegmenter._CanSegmenter__functional_segmentation(self=self.mock_can_segmenter, message=mock_message) - self.mock_get_max_sf_payload_size.assert_called_once_with( - addressing_format=self.mock_can_segmenter.addressing_format, dlc=self.mock_can_segmenter.dlc) - mock_len.assert_called_once_with(mock_message.payload) - - @pytest.mark.parametrize("message_payload_size, max_payload", [ - (2, 2), - (60, 62) - ]) - @pytest.mark.parametrize("functional_ai", [{"p1": 1, "p2": 2, "p3": 3}, {"abc": "something", "xyz": "else"}]) - @patch(f"{SCRIPT_LOCATION}.len") - def test_functional_segmentation__with_data_optimization(self, mock_len, message_payload_size, max_payload, - functional_ai): - mock_len.return_value = message_payload_size - self.mock_get_max_sf_payload_size.return_value = max_payload - self.mock_can_segmenter.use_data_optimization = True - self.mock_can_segmenter.tx_packets_functional_ai = functional_ai - mock_message = Mock(spec=UdsMessage, addressing_type=AddressingType.FUNCTIONAL) - packets = CanSegmenter._CanSegmenter__functional_segmentation(self=self.mock_can_segmenter, - message=mock_message) - assert isinstance(packets, tuple) - assert len(packets) == 1 - assert packets[0] == self.mock_can_packet_class.return_value - self.mock_get_max_sf_payload_size.assert_called_once_with( - addressing_format=self.mock_can_segmenter.addressing_format, dlc=self.mock_can_segmenter.dlc) - mock_len.assert_called_once_with(mock_message.payload) - self.mock_can_packet_class.assert_called_once_with(packet_type=CanPacketType.SINGLE_FRAME, - payload=mock_message.payload, - dlc=None, - filler_byte=self.mock_can_segmenter.filler_byte, - **functional_ai) - - @pytest.mark.parametrize("message_payload_size, max_payload", [ - (2, 2), - (60, 62) - ]) - @pytest.mark.parametrize("functional_ai", [{"p1": 1, "p2": 2, "p3": 3}, {"abc": "something", "xyz": "else"}]) - @patch(f"{SCRIPT_LOCATION}.len") - def test_functional_segmentation__without_data_optimization(self, mock_len, message_payload_size, max_payload, - functional_ai): - mock_len.return_value = message_payload_size - self.mock_get_max_sf_payload_size.return_value = max_payload - self.mock_can_segmenter.use_data_optimization = False - self.mock_can_segmenter.tx_packets_functional_ai = functional_ai - mock_message = Mock(spec=UdsMessage, addressing_type=AddressingType.FUNCTIONAL) - packets = CanSegmenter._CanSegmenter__functional_segmentation(self=self.mock_can_segmenter, - message=mock_message) - assert isinstance(packets, tuple) - assert len(packets) == 1 - assert packets[0] == self.mock_can_packet_class.return_value - self.mock_get_max_sf_payload_size.assert_called_once_with( - addressing_format=self.mock_can_segmenter.addressing_format, dlc=self.mock_can_segmenter.dlc) - mock_len.assert_called_once_with(mock_message.payload) - self.mock_can_packet_class.assert_called_once_with(packet_type=CanPacketType.SINGLE_FRAME, - payload=mock_message.payload, - dlc=self.mock_can_segmenter.dlc, - filler_byte=self.mock_can_segmenter.filler_byte, - **functional_ai) - @pytest.mark.integration class TestCanSegmenterIntegration: diff --git a/tests/software_tests/transport_interface/test_can_transport_interface.py b/tests/software_tests/transport_interface/test_can_transport_interface.py index b6c4d029..16ef7859 100644 --- a/tests/software_tests/transport_interface/test_can_transport_interface.py +++ b/tests/software_tests/transport_interface/test_can_transport_interface.py @@ -1,7 +1,7 @@ from random import randint import pytest -from mock import AsyncMock, MagicMock, Mock, patch +from mock import AsyncMock, MagicMock, Mock, call, patch from uds.can import CanAddressingFormat, CanAddressingInformation from uds.transmission_attributes import AddressingType @@ -10,14 +10,17 @@ AbstractCanTransportInterface, AbstractFlowControlParametersGenerator, BusABC, + CanFlowStatus, CanPacket, CanPacketRecord, CanPacketType, DefaultFlowControlParametersGenerator, PyCanTransportInterface, TransmissionDirection, + TransmissionInterruptionError, UdsMessage, UdsMessageRecord, + datetime, ) SCRIPT_LOCATION = "uds.transport_interface.can_transport_interface" @@ -76,6 +79,8 @@ def test_init__valid_mandatory_args(self, mock_isinstance, == addressing_information assert self.mock_can_transport_interface._AbstractCanTransportInterface__segmenter \ == self.mock_can_segmenter_class.return_value + assert self.mock_can_transport_interface._AbstractCanTransportInterface__n_bs_measured is None + assert self.mock_can_transport_interface._AbstractCanTransportInterface__n_cr_measured is None assert self.mock_can_transport_interface.n_as_timeout == self.mock_can_transport_interface.N_AS_TIMEOUT assert self.mock_can_transport_interface.n_ar_timeout == self.mock_can_transport_interface.N_AR_TIMEOUT assert self.mock_can_transport_interface.n_bs_timeout == self.mock_can_transport_interface.N_BS_TIMEOUT @@ -125,6 +130,8 @@ def test_init__valid_all_args(self, mock_isinstance, == addressing_information assert self.mock_can_transport_interface._AbstractCanTransportInterface__segmenter \ == self.mock_can_segmenter_class.return_value + assert self.mock_can_transport_interface._AbstractCanTransportInterface__n_bs_measured is None + assert self.mock_can_transport_interface._AbstractCanTransportInterface__n_cr_measured is None assert self.mock_can_transport_interface.n_as_timeout == n_as_timeout assert self.mock_can_transport_interface.n_ar_timeout == n_ar_timeout assert self.mock_can_transport_interface.n_bs_timeout == n_bs_timeout @@ -133,6 +140,86 @@ def test_init__valid_all_args(self, mock_isinstance, assert self.mock_can_transport_interface.n_cr_timeout == n_cr_timeout assert self.mock_can_transport_interface.flow_control_parameters_generator == flow_control_parameters_generator + # _update_n_bs_measured + + @pytest.mark.parametrize("message", [ + Mock(spec=UdsMessageRecord, packets_records=(Mock(spec=CanPacketRecord), )) + ]) + def test_update_n_bs_measured__1_record(self, message): + assert AbstractCanTransportInterface._update_n_bs_measured(self.mock_can_transport_interface, message=message) is None + assert self.mock_can_transport_interface._AbstractCanTransportInterface__n_bs_measured is None + + @pytest.mark.parametrize("message, expected_n_bs_measurements", [ + (Mock(spec=UdsMessageRecord, packets_records=( + Mock(spec=CanPacketRecord, packet_type=CanPacketType.FIRST_FRAME, + transmission_time=datetime(year=1234, month=1, day=2)), + Mock(spec=CanPacketRecord, packet_type=CanPacketType.FLOW_CONTROL, + transmission_time=datetime(year=1234, month=1, day=2, microsecond=1000)), + Mock(spec=CanPacketRecord, packet_type=CanPacketType.CONSECUTIVE_FRAME, + transmission_time=datetime(year=1234, month=1, day=2, microsecond=3000)))), + (1, )), + (Mock(spec=UdsMessageRecord, packets_records=( + Mock(spec=CanPacketRecord, packet_type=CanPacketType.FIRST_FRAME, + transmission_time=datetime(year=2024, month=9, day=22, hour=14, minute=43, second=12, microsecond=987654)), + Mock(spec=CanPacketRecord, packet_type=CanPacketType.FLOW_CONTROL, + transmission_time=datetime(year=2024, month=9, day=22, hour=14, minute=43, second=13, microsecond=154)), + Mock(spec=CanPacketRecord, packet_type=CanPacketType.FLOW_CONTROL, + transmission_time=datetime(year=2024, month=9, day=22, hour=14, minute=43, second=13, microsecond=57000)), + Mock(spec=CanPacketRecord, packet_type=CanPacketType.CONSECUTIVE_FRAME, + transmission_time=datetime(year=2024, month=9, day=22, hour=14, minute=43, second=13, microsecond=58954)), + Mock(spec=CanPacketRecord, packet_type=CanPacketType.FLOW_CONTROL, + transmission_time=datetime(year=2024, month=9, day=22, hour=14, minute=43, second=13, microsecond=58955)), + Mock(spec=CanPacketRecord, packet_type=CanPacketType.CONSECUTIVE_FRAME, + transmission_time=datetime(year=2024, month=9, day=22, hour=14, minute=43, second=13, microsecond=868955)), + Mock(spec=CanPacketRecord, packet_type=CanPacketType.CONSECUTIVE_FRAME, + transmission_time=datetime(year=2024, month=9, day=22, hour=14, minute=43, second=14, microsecond=955)), + )), + (12.5, 56.846, 0.001)), + ]) + def test_update_n_bs_measured__multiple_records(self, message, expected_n_bs_measurements): + assert AbstractCanTransportInterface._update_n_bs_measured(self.mock_can_transport_interface, message=message) is None + assert self.mock_can_transport_interface._AbstractCanTransportInterface__n_bs_measured == expected_n_bs_measurements + + # _update_n_cr_measured + + @pytest.mark.parametrize("message", [ + Mock(spec=UdsMessageRecord, packets_records=(Mock(spec=CanPacketRecord), )) + ]) + def test_update_n_cr_measured__1_record(self, message): + assert AbstractCanTransportInterface._update_n_cr_measured(self.mock_can_transport_interface, message=message) is None + assert self.mock_can_transport_interface._AbstractCanTransportInterface__n_cr_measured is None + + @pytest.mark.parametrize("message, expected_n_cr_measurements", [ + (Mock(spec=UdsMessageRecord, packets_records=( + Mock(spec=CanPacketRecord, packet_type=CanPacketType.FIRST_FRAME, + transmission_time=datetime(year=1234, month=1, day=2)), + Mock(spec=CanPacketRecord, packet_type=CanPacketType.FLOW_CONTROL, + transmission_time=datetime(year=1234, month=1, day=2, microsecond=1000)), + Mock(spec=CanPacketRecord, packet_type=CanPacketType.CONSECUTIVE_FRAME, + transmission_time=datetime(year=1234, month=1, day=2, microsecond=3000)))), + (2, )), + (Mock(spec=UdsMessageRecord, packets_records=( + Mock(spec=CanPacketRecord, packet_type=CanPacketType.FIRST_FRAME, + transmission_time=datetime(year=2024, month=9, day=22, hour=14, minute=43, second=12, microsecond=987654)), + Mock(spec=CanPacketRecord, packet_type=CanPacketType.FLOW_CONTROL, + transmission_time=datetime(year=2024, month=9, day=22, hour=14, minute=43, second=13, microsecond=154)), + Mock(spec=CanPacketRecord, packet_type=CanPacketType.FLOW_CONTROL, + transmission_time=datetime(year=2024, month=9, day=22, hour=14, minute=43, second=13, microsecond=57000)), + Mock(spec=CanPacketRecord, packet_type=CanPacketType.CONSECUTIVE_FRAME, + transmission_time=datetime(year=2024, month=9, day=22, hour=14, minute=43, second=13, microsecond=58954)), + Mock(spec=CanPacketRecord, packet_type=CanPacketType.FLOW_CONTROL, + transmission_time=datetime(year=2024, month=9, day=22, hour=14, minute=43, second=13, microsecond=58955)), + Mock(spec=CanPacketRecord, packet_type=CanPacketType.CONSECUTIVE_FRAME, + transmission_time=datetime(year=2024, month=9, day=22, hour=14, minute=43, second=13, microsecond=868955)), + Mock(spec=CanPacketRecord, packet_type=CanPacketType.CONSECUTIVE_FRAME, + transmission_time=datetime(year=2024, month=9, day=22, hour=14, minute=43, second=14, microsecond=955)), + )), + (1.954, 810.0, 132)), + ]) + def test_update_n_cr_measured__multiple_records(self, message, expected_n_cr_measurements): + assert AbstractCanTransportInterface._update_n_cr_measured(self.mock_can_transport_interface, message=message) is None + assert self.mock_can_transport_interface._AbstractCanTransportInterface__n_cr_measured == expected_n_cr_measurements + # segmenter @pytest.mark.parametrize("value", ["something", Mock()]) @@ -294,6 +381,11 @@ def test_n_bs_timeout__set__valid_without_warn(self, mock_isinstance): self.mock_warn.assert_not_called() assert self.mock_can_transport_interface._AbstractCanTransportInterface__n_bs_timeout == mock_value + @pytest.mark.parametrize("value", ["something", Mock()]) + def test_n_bs_measured(self, value): + self.mock_can_transport_interface._AbstractCanTransportInterface__n_bs_measured = value + assert AbstractCanTransportInterface.n_bs_measured.fget(self.mock_can_transport_interface) == value + # n_br @pytest.mark.parametrize("value", ["something", Mock()]) @@ -469,6 +561,11 @@ def test_n_cr_timeout__set__valid_without_warn(self, mock_isinstance): self.mock_warn.assert_not_called() assert self.mock_can_transport_interface._AbstractCanTransportInterface__n_cr_timeout == mock_value + @pytest.mark.parametrize("value", ["something", Mock()]) + def test_n_cr_measured(self, value): + self.mock_can_transport_interface._AbstractCanTransportInterface__n_cr_measured = value + assert AbstractCanTransportInterface.n_cr_measured.fget(self.mock_can_transport_interface) == value + # addressing_information @pytest.mark.parametrize("value", ["something", Mock()]) @@ -554,8 +651,12 @@ def setup_method(self): self.mock_can_id_handler = self._patcher_can_id_handler.start() self._patcher_can_dlc_handler = patch(f"{SCRIPT_LOCATION}.CanDlcHandler") self.mock_can_dlc_handler = self._patcher_can_dlc_handler.start() + self._patcher_can_st_min_handler = patch(f"{SCRIPT_LOCATION}.CanSTminTranslator") + self.mock_can_st_min_handler = self._patcher_can_st_min_handler.start() self._patcher_can_packet_record = patch(f"{SCRIPT_LOCATION}.CanPacketRecord") self.mock_can_packet_record = self._patcher_can_packet_record.start() + self._patcher_can_packet_type_is_initial_packet_type = patch(f"{SCRIPT_LOCATION}.CanPacketType.is_initial_packet_type") + self.mock_can_packet_type_is_initial_packet_type = self._patcher_can_packet_type_is_initial_packet_type.start() self._patcher_notifier = patch(f"{SCRIPT_LOCATION}.Notifier") self.mock_notifier = self._patcher_notifier.start() self._patcher_message = patch(f"{SCRIPT_LOCATION}.Message") @@ -571,7 +672,9 @@ def teardown_method(self): self._patcher_uds_message_record.stop() self._patcher_can_id_handler.stop() self._patcher_can_dlc_handler.stop() + self._patcher_can_st_min_handler.stop() self._patcher_can_packet_record.stop() + self._patcher_can_packet_type_is_initial_packet_type.stop() self._patcher_notifier.stop() self._patcher_message.stop() @@ -590,8 +693,6 @@ def test_init__default_args(self, can_bus_manager, addressing_information): addressing_information=addressing_information) assert self.mock_can_transport_interface._PyCanTransportInterface__n_as_measured is None assert self.mock_can_transport_interface._PyCanTransportInterface__n_ar_measured is None - assert self.mock_can_transport_interface._PyCanTransportInterface__n_bs_measured is None - assert self.mock_can_transport_interface._PyCanTransportInterface__n_cr_measured is None @pytest.mark.parametrize("can_bus_manager, addressing_information", [ ("can_bus_manager", "addressing_information"), @@ -611,8 +712,6 @@ def test_init__all_args(self, can_bus_manager, addressing_information, kwargs): **kwargs) assert self.mock_can_transport_interface._PyCanTransportInterface__n_as_measured is None assert self.mock_can_transport_interface._PyCanTransportInterface__n_ar_measured is None - assert self.mock_can_transport_interface._PyCanTransportInterface__n_bs_measured is None - assert self.mock_can_transport_interface._PyCanTransportInterface__n_cr_measured is None # __del__ @@ -635,20 +734,6 @@ def test_n_ar_measured(self, value): self.mock_can_transport_interface._PyCanTransportInterface__n_ar_measured = value assert PyCanTransportInterface.n_ar_measured.fget(self.mock_can_transport_interface) == value - # n_bs_measured - - @pytest.mark.parametrize("value", ["something", Mock()]) - def test_n_bs_measured(self, value): - self.mock_can_transport_interface._PyCanTransportInterface__n_bs_measured = value - assert PyCanTransportInterface.n_bs_measured.fget(self.mock_can_transport_interface) == value - - # n_cr_measured - - @pytest.mark.parametrize("value", ["something", Mock()]) - def test_n_cr_measured(self, value): - self.mock_can_transport_interface._PyCanTransportInterface__n_cr_measured = value - assert PyCanTransportInterface.n_cr_measured.fget(self.mock_can_transport_interface) == value - # _teardown_notifier def test_teardown_notifier__no_notifier(self): @@ -742,6 +827,193 @@ def test_setup_async_notifier__notifier_exists(self, loop): self.mock_notifier.assert_not_called() self.mock_can_transport_interface._teardown_notifier.assert_called_once_with() + # _send_cf_packets_block + + @pytest.mark.parametrize("packets", [ + (Mock(spec=CanPacket), Mock(spec=CanPacket)), + (Mock(spec=CanPacket), Mock(spec=CanPacket), Mock(spec=CanPacket)), + ]) + @pytest.mark.parametrize("delay", [0, 12.34]) + def test_send_cf_packets_block(self, packets, delay): + called = 0 + + def once_true_once_false(*args): + nonlocal called + called += 1 + return called % 2 + + mock_sub = MagicMock() + mock_add = MagicMock(__sub__=Mock(return_value=mock_sub)) + self.mock_time.return_value.__add__.return_value = mock_add + mock_lt = Mock(side_effect=once_true_once_false) + self.mock_time.return_value.__lt__ = mock_lt + self.mock_can_transport_interface.receive_packet.side_effect = TimeoutError + packet_records = PyCanTransportInterface._send_cf_packets_block(self=self.mock_can_transport_interface, + cf_packets_block=packets, delay=delay) + assert isinstance(packet_records, tuple) + assert all(packet_record == self.mock_can_transport_interface.send_packet.return_value + for packet_record in packet_records) + self.mock_can_transport_interface.send_packet.assert_has_calls(calls=[call(packet) for packet in packets]) + self.mock_can_transport_interface.receive_packet.assert_called() + self.mock_warn.assert_not_called() + + @pytest.mark.parametrize("packets", [ + (Mock(spec=CanPacket), Mock(spec=CanPacket)), + (Mock(spec=CanPacket), Mock(spec=CanPacket), Mock(spec=CanPacket)), + ]) + @pytest.mark.parametrize("delay", [0, 12.34]) + def test_send_cf_packets_block__packets_traffic(self, packets, delay): + called = 0 + + def once_true_once_false(*args): + nonlocal called + called += 1 + return called % 2 + + mock_sub = MagicMock() + mock_add = MagicMock(__sub__=Mock(return_value=mock_sub)) + self.mock_time.return_value.__add__.return_value = mock_add + mock_lt = Mock(side_effect=once_true_once_false) + self.mock_time.return_value.__lt__ = mock_lt + mock_received_packet_records = Mock(spec=CanPacketRecord) + self.mock_can_transport_interface.receive_packet.return_value = mock_received_packet_records + self.mock_can_packet_type_is_initial_packet_type.return_value = False + packet_records = PyCanTransportInterface._send_cf_packets_block(self=self.mock_can_transport_interface, + cf_packets_block=packets, delay=delay) + assert isinstance(packet_records, tuple) + assert all(packet_record == self.mock_can_transport_interface.send_packet.return_value + for packet_record in packet_records) + self.mock_can_transport_interface.send_packet.assert_has_calls(calls=[call(packet) for packet in packets]) + self.mock_can_transport_interface.receive_packet.assert_called() + self.mock_can_packet_type_is_initial_packet_type.assert_has_calls( + calls=[call(mock_received_packet_records.packet_type) for _ in packets]) + self.mock_warn.assert_called() + + @pytest.mark.parametrize("packets", [ + (Mock(spec=CanPacket), Mock(spec=CanPacket)), + (Mock(spec=CanPacket), Mock(spec=CanPacket), Mock(spec=CanPacket)), + ]) + @pytest.mark.parametrize("delay", [0, 12.34]) + def test_send_cf_packets_block__transmission_interruption(self, packets, delay): + called = 0 + + def once_true_once_false(*args): + nonlocal called + called += 1 + return called % 2 + + mock_sub = MagicMock() + mock_add = MagicMock(__sub__=Mock(return_value=mock_sub)) + self.mock_time.return_value.__add__.return_value = mock_add + mock_lt = Mock(side_effect=once_true_once_false) + self.mock_time.return_value.__lt__ = mock_lt + mock_received_packet_records = Mock(spec=CanPacketRecord) + self.mock_can_transport_interface.receive_packet.return_value = mock_received_packet_records + self.mock_can_packet_type_is_initial_packet_type.return_value = True + with pytest.raises(TransmissionInterruptionError): + PyCanTransportInterface._send_cf_packets_block(self=self.mock_can_transport_interface, + cf_packets_block=packets, delay=delay) + self.mock_can_transport_interface.receive_packet.assert_called() + self.mock_can_packet_type_is_initial_packet_type.assert_called_once_with( + mock_received_packet_records.packet_type) + self.mock_warn.assert_not_called() + + # _async_send_cf_packets_block + + @pytest.mark.parametrize("packets", [ + (Mock(spec=CanPacket), Mock(spec=CanPacket)), + (Mock(spec=CanPacket), Mock(spec=CanPacket), Mock(spec=CanPacket)), + ]) + @pytest.mark.parametrize("delay", [0, 12.34]) + @pytest.mark.asyncio + async def test_async_send_cf_packets_block(self, packets, delay): + called = 0 + + def once_true_once_false(*args): + nonlocal called + called += 1 + return called % 2 + + mock_sub = MagicMock() + mock_add = MagicMock(__sub__=Mock(return_value=mock_sub)) + self.mock_time.return_value.__add__.return_value = mock_add + mock_lt = Mock(side_effect=once_true_once_false) + self.mock_time.return_value.__lt__ = mock_lt + self.mock_can_transport_interface.async_receive_packet.side_effect = TimeoutError + packet_records = await PyCanTransportInterface._async_send_cf_packets_block( + self=self.mock_can_transport_interface, cf_packets_block=packets, delay=delay) + assert isinstance(packet_records, tuple) + assert all(packet_record == self.mock_can_transport_interface.async_send_packet.return_value + for packet_record in packet_records) + self.mock_can_transport_interface.async_send_packet.assert_has_calls( + calls=[call(packet, loop=None) for packet in packets]) + self.mock_can_transport_interface.async_receive_packet.assert_called() + self.mock_warn.assert_not_called() + + @pytest.mark.parametrize("packets", [ + (Mock(spec=CanPacket), Mock(spec=CanPacket)), + (Mock(spec=CanPacket), Mock(spec=CanPacket), Mock(spec=CanPacket)), + ]) + @pytest.mark.parametrize("delay", [0, 1234]) + @pytest.mark.asyncio + async def test_async_send_cf_packets_block__packets_traffic(self, packets, delay): + called = 0 + + def once_true_once_false(*args): + nonlocal called + called += 1 + return called % 2 + + mock_sub = MagicMock() + mock_add = MagicMock(__sub__=Mock(return_value=mock_sub)) + self.mock_time.return_value.__add__.return_value = mock_add + mock_lt = Mock(side_effect=once_true_once_false) + self.mock_time.return_value.__lt__ = mock_lt + mock_received_packet_records = Mock(spec=CanPacketRecord) + self.mock_can_transport_interface.async_receive_packet.return_value = mock_received_packet_records + self.mock_can_packet_type_is_initial_packet_type.return_value = False + packet_records = await PyCanTransportInterface._async_send_cf_packets_block( + self=self.mock_can_transport_interface, cf_packets_block=packets, delay=delay) + assert isinstance(packet_records, tuple) + assert all(packet_record == self.mock_can_transport_interface.async_send_packet.return_value + for packet_record in packet_records) + self.mock_can_transport_interface.async_send_packet.assert_has_calls( + calls=[call(packet, loop=None) for packet in packets]) + self.mock_can_transport_interface.async_receive_packet.assert_called() + self.mock_can_packet_type_is_initial_packet_type.assert_has_calls( + calls=[call(mock_received_packet_records.packet_type) for _ in packets]) + self.mock_warn.assert_called() + + @pytest.mark.parametrize("packets", [ + (Mock(spec=CanPacket), Mock(spec=CanPacket)), + (Mock(spec=CanPacket), Mock(spec=CanPacket), Mock(spec=CanPacket)), + ]) + @pytest.mark.parametrize("delay", [0, 1234]) + @pytest.mark.asyncio + async def test_async_send_cf_packets_block__transmission_interruption(self, packets, delay): + called = 0 + + def once_true_once_false(*args): + nonlocal called + called += 1 + return called % 2 + + mock_sub = MagicMock() + mock_add = MagicMock(__sub__=Mock(return_value=mock_sub)) + self.mock_time.return_value.__add__.return_value = mock_add + mock_lt = Mock(side_effect=once_true_once_false) + self.mock_time.return_value.__lt__ = mock_lt + mock_received_packet_records = Mock(spec=CanPacketRecord) + self.mock_can_transport_interface.async_receive_packet.return_value = mock_received_packet_records + self.mock_can_packet_type_is_initial_packet_type.return_value = True + with pytest.raises(TransmissionInterruptionError): + await PyCanTransportInterface._async_send_cf_packets_block( + self=self.mock_can_transport_interface, cf_packets_block=packets, delay=delay) + self.mock_can_transport_interface.async_receive_packet.assert_called() + self.mock_can_packet_type_is_initial_packet_type.assert_called_once_with( + mock_received_packet_records.packet_type) + self.mock_warn.assert_not_called() + # clear_frames_buffers @pytest.mark.parametrize("sync_queue_size", [0, 1, 7]) @@ -838,14 +1110,10 @@ def test_send_packet(self, packet): transmission_time=self.mock_datetime.fromtimestamp.return_value) if packet.packet_type == CanPacketType.FLOW_CONTROL: assert self.mock_can_transport_interface._PyCanTransportInterface__n_as_measured is None - assert self.mock_can_transport_interface._PyCanTransportInterface__n_ar_measured \ - == mock_get_message.return_value.timestamp.__sub__.return_value - mock_get_message.return_value.timestamp.__sub__.assert_called_once_with(self.mock_time.return_value) + assert self.mock_can_transport_interface._PyCanTransportInterface__n_ar_measured is not None else: - assert self.mock_can_transport_interface._PyCanTransportInterface__n_as_measured \ - == mock_get_message.return_value.timestamp.__sub__.return_value + assert self.mock_can_transport_interface._PyCanTransportInterface__n_as_measured is not None assert self.mock_can_transport_interface._PyCanTransportInterface__n_ar_measured is None - mock_get_message.return_value.timestamp.__sub__.assert_called_once_with(self.mock_time.return_value) # receive_packet @@ -950,14 +1218,10 @@ async def test_async_send_packet(self, packet): transmission_time=self.mock_datetime.fromtimestamp.return_value) if packet.packet_type == CanPacketType.FLOW_CONTROL: assert self.mock_can_transport_interface._PyCanTransportInterface__n_as_measured is None - assert self.mock_can_transport_interface._PyCanTransportInterface__n_ar_measured \ - == mock_get_message.return_value.timestamp.__sub__.return_value - mock_get_message.return_value.timestamp.__sub__.assert_called_once_with(self.mock_time.return_value) + assert self.mock_can_transport_interface._PyCanTransportInterface__n_ar_measured is not None else: - assert self.mock_can_transport_interface._PyCanTransportInterface__n_as_measured \ - == mock_get_message.return_value.timestamp.__sub__.return_value + assert self.mock_can_transport_interface._PyCanTransportInterface__n_as_measured is not None assert self.mock_can_transport_interface._PyCanTransportInterface__n_ar_measured is None - mock_get_message.return_value.timestamp.__sub__.assert_called_once_with(self.mock_time.return_value) # async_receive_packet @@ -1020,22 +1284,226 @@ async def test_async_receive_packet(self, timeout): def test_send_message__single_frame(self, message): mock_segmented_message = [Mock(spec=CanPacket)] self.mock_can_transport_interface.segmenter.segmentation = Mock(return_value=mock_segmented_message) - assert PyCanTransportInterface.send_message(self.mock_can_transport_interface, - message) == self.mock_uds_message_record.return_value + assert PyCanTransportInterface.send_message( + self.mock_can_transport_interface, message) == self.mock_uds_message_record.return_value self.mock_can_transport_interface.segmenter.segmentation.assert_called_once_with(message) self.mock_can_transport_interface.send_packet.assert_called_once_with(mock_segmented_message[0]) - self.mock_uds_message_record.assert_called_once_with((self.mock_can_transport_interface.send_packet.return_value, )) + self.mock_uds_message_record.assert_called_once_with([self.mock_can_transport_interface.send_packet.return_value]) + self.mock_warn.assert_not_called() + self.mock_can_transport_interface._update_n_bs_measured.assert_called_once_with( + self.mock_uds_message_record.return_value) + + @pytest.mark.parametrize("message", [ + Mock(spec=UdsMessage, payload=[0x22, 0xF1, 0x86, 0xF1, 0x87, 0xF1, 0x88], addressing_type=AddressingType.PHYSICAL), + Mock(spec=UdsMessage, payload=[0x3E, 0x80], addressing_type=AddressingType.PHYSICAL), + ]) + @pytest.mark.parametrize("st_min", [0x00, 0xFF]) + def test_send_message__multiple_packets__st_min__block_size_0(self, message, st_min): + mock_segmented_message = [Mock(spec=CanPacket, packet_type=CanPacketType.FIRST_FRAME)] + mock_segmented_message.extend([Mock(spec=CanPacket, packet_type=CanPacketType.CONSECUTIVE_FRAME) + for _ in range(randint(1, 20))]) + self.mock_can_transport_interface.segmenter.segmentation = Mock(return_value=mock_segmented_message) + self.mock_can_transport_interface.n_cs = None + mock_flow_control_record = Mock(spec=CanPacketRecord, + packet_type=CanPacketType.FLOW_CONTROL, + flow_status=CanFlowStatus.ContinueToSend, + block_size=0, + st_min=st_min) + self.mock_can_transport_interface.receive_packet = Mock(return_value=mock_flow_control_record) + mock_sent_packet_record = Mock(spec=CanPacketRecord) + self.mock_can_transport_interface._send_cf_packets_block.return_value = [mock_sent_packet_record] + assert PyCanTransportInterface.send_message( + self.mock_can_transport_interface, message) == self.mock_uds_message_record.return_value + self.mock_can_transport_interface.segmenter.segmentation.assert_called_once_with(message) + self.mock_can_transport_interface.receive_packet.assert_called_once_with( + timeout=self.mock_can_transport_interface.N_BS_TIMEOUT) + self.mock_can_transport_interface._send_cf_packets_block.assert_called_once_with( + cf_packets_block=mock_segmented_message[1:], + delay=self.mock_can_st_min_handler.decode.return_value) + self.mock_can_st_min_handler.decode.assert_called_once_with(st_min) + self.mock_uds_message_record.assert_called_once_with([ + self.mock_can_transport_interface.send_packet.return_value, + mock_flow_control_record, + mock_sent_packet_record + ]) + self.mock_warn.assert_not_called() + self.mock_can_transport_interface._update_n_bs_measured.assert_called_once_with( + self.mock_uds_message_record.return_value) + + @pytest.mark.parametrize("message", [ + Mock(spec=UdsMessage, payload=[0x22, 0xF1, 0x86, 0xF1, 0x87, 0xF1, 0x88], addressing_type=AddressingType.PHYSICAL), + Mock(spec=UdsMessage, payload=[0x3E, 0x80], addressing_type=AddressingType.PHYSICAL), + ]) + @pytest.mark.parametrize("n_cs, st_min", [ + (0, 0xFF), + (5, 0x00), + ]) + def test_send_message__multiple_packets__n_cs__block_size_1(self, message, n_cs, st_min): + mock_segmented_message = [Mock(spec=CanPacket, packet_type=CanPacketType.FIRST_FRAME)] + mock_segmented_message.extend([Mock(spec=CanPacket, packet_type=CanPacketType.CONSECUTIVE_FRAME) + for _ in range(randint(1, 20))]) + self.mock_can_transport_interface.segmenter.segmentation = Mock(return_value=mock_segmented_message[:]) + self.mock_can_transport_interface.n_cs = n_cs + mock_flow_control_record = Mock(spec=CanPacketRecord, + packet_type=CanPacketType.FLOW_CONTROL, + flow_status=CanFlowStatus.ContinueToSend, + block_size=1, + st_min=st_min) + self.mock_can_transport_interface.receive_packet = Mock(return_value=mock_flow_control_record) + mock_sent_packet_record = Mock(spec=CanPacketRecord) + self.mock_can_transport_interface._send_cf_packets_block.return_value = [mock_sent_packet_record] + assert PyCanTransportInterface.send_message( + self.mock_can_transport_interface, message) == self.mock_uds_message_record.return_value + self.mock_can_transport_interface.segmenter.segmentation.assert_called_once_with(message) + self.mock_can_transport_interface.receive_packet.assert_has_calls([ + call(timeout=self.mock_can_transport_interface.N_BS_TIMEOUT) for _ in mock_segmented_message[1:] + ]) + self.mock_can_transport_interface._send_cf_packets_block.assert_has_calls([ + call(cf_packets_block=[packet], delay=n_cs) for packet in mock_segmented_message[1:] + ]) + self.mock_can_st_min_handler.decode.assert_not_called() + self.mock_uds_message_record.assert_called_once_with([ + self.mock_can_transport_interface.send_packet.return_value, + *([mock_flow_control_record, mock_sent_packet_record]*len(mock_segmented_message[1:])) + ]) + self.mock_warn.assert_not_called() + self.mock_can_transport_interface._update_n_bs_measured.assert_called_once_with( + self.mock_uds_message_record.return_value) @pytest.mark.parametrize("message", [ Mock(spec=UdsMessage, payload=[0x22, 0xF1, 0x86, 0xF1, 0x87, 0xF1, 0x88], addressing_type=AddressingType.PHYSICAL), Mock(spec=UdsMessage, payload=[0x3E, 0x80], addressing_type=AddressingType.PHYSICAL), ]) - def test_send_message__multiple_packets(self, message): - mock_segmented_message = [Mock(spec=CanPacket) for _ in range(randint(2, 20))] + def test_send_message__multiple_packets__wait(self, message): + mock_segmented_message = [Mock(spec=CanPacket, packet_type=CanPacketType.FIRST_FRAME)] + mock_segmented_message.extend([Mock(spec=CanPacket, packet_type=CanPacketType.CONSECUTIVE_FRAME) + for _ in range(randint(1, 20))]) self.mock_can_transport_interface.segmenter.segmentation = Mock(return_value=mock_segmented_message) + mock_flow_control_record_wait = Mock(spec=CanPacketRecord, + packet_type=CanPacketType.FLOW_CONTROL, + flow_status=CanFlowStatus.Wait) + mock_flow_control_record_continue = Mock(spec=CanPacketRecord, + packet_type=CanPacketType.FLOW_CONTROL, + flow_status=CanFlowStatus.ContinueToSend, + block_size=0) + self.mock_can_transport_interface.receive_packet = Mock( + side_effect=[mock_flow_control_record_wait, mock_flow_control_record_continue]) + mock_sent_packet_record = Mock(spec=CanPacketRecord) + self.mock_can_transport_interface._send_cf_packets_block.return_value = [mock_sent_packet_record] + assert PyCanTransportInterface.send_message( + self.mock_can_transport_interface, message) == self.mock_uds_message_record.return_value + self.mock_can_transport_interface.segmenter.segmentation.assert_called_once_with(message) + self.mock_can_transport_interface.receive_packet.assert_has_calls([ + call(timeout=self.mock_can_transport_interface.N_BS_TIMEOUT), + call(timeout=self.mock_can_transport_interface.N_BS_TIMEOUT) + ]) + self.mock_uds_message_record.assert_called_once_with([ + self.mock_can_transport_interface.send_packet.return_value, + mock_flow_control_record_wait, + mock_flow_control_record_continue, + mock_sent_packet_record + ]) + self.mock_warn.assert_not_called() + self.mock_can_transport_interface._update_n_bs_measured.assert_called_once_with( + self.mock_uds_message_record.return_value) + + @pytest.mark.parametrize("message", [ + Mock(spec=UdsMessage, payload=[0x22, 0xF1, 0x86, 0xF1, 0x87, 0xF1, 0x88], addressing_type=AddressingType.PHYSICAL), + Mock(spec=UdsMessage, payload=[0x3E, 0x80], addressing_type=AddressingType.PHYSICAL), + ]) + def test_send_message__multiple_packets__unexpected_packet(self, message): + mock_segmented_message = [Mock(spec=CanPacket, packet_type=CanPacketType.FIRST_FRAME)] + mock_segmented_message.extend([Mock(spec=CanPacket, packet_type=CanPacketType.CONSECUTIVE_FRAME) + for _ in range(randint(1, 20))]) + self.mock_can_transport_interface.segmenter.segmentation = Mock(return_value=mock_segmented_message) + mock_flow_control_record = Mock(spec=CanPacketRecord, + packet_type=CanPacketType.FLOW_CONTROL, + flow_status=CanFlowStatus.ContinueToSend, + block_size=0) + mock_interrupting_record = Mock(spec=CanPacketRecord, + packet_type=Mock()) + self.mock_can_transport_interface.receive_packet = Mock( + side_effect=[mock_interrupting_record, mock_flow_control_record]) + self.mock_can_packet_type_is_initial_packet_type.return_value = False + mock_sent_packet_record = Mock(spec=CanPacketRecord) + self.mock_can_transport_interface._send_cf_packets_block.return_value = [mock_sent_packet_record] + assert PyCanTransportInterface.send_message( + self.mock_can_transport_interface, message) == self.mock_uds_message_record.return_value + self.mock_can_transport_interface.segmenter.segmentation.assert_called_once_with(message) + self.mock_can_transport_interface.receive_packet.assert_has_calls([ + call(timeout=self.mock_can_transport_interface.N_BS_TIMEOUT), + call(timeout=self.mock_can_transport_interface.N_BS_TIMEOUT) + ]) + self.mock_uds_message_record.assert_called_once_with([ + self.mock_can_transport_interface.send_packet.return_value, + mock_flow_control_record, + mock_sent_packet_record + ]) + self.mock_warn.assert_called() + self.mock_can_transport_interface._update_n_bs_measured.assert_called_once_with( + self.mock_uds_message_record.return_value) + + @pytest.mark.parametrize("message", [ + Mock(spec=UdsMessage, payload=[0x22, 0xF1, 0x86, 0xF1, 0x87, 0xF1, 0x88], addressing_type=AddressingType.PHYSICAL), + Mock(spec=UdsMessage, payload=[0x3E, 0x80], addressing_type=AddressingType.PHYSICAL), + ]) + def test_send_message__multiple_packets__transmission_interruption(self, message): + mock_segmented_message = [Mock(spec=CanPacket, packet_type=CanPacketType.FIRST_FRAME)] + mock_segmented_message.extend([Mock(spec=CanPacket, packet_type=CanPacketType.CONSECUTIVE_FRAME) + for _ in range(randint(1, 20))]) + self.mock_can_transport_interface.segmenter.segmentation = Mock(return_value=mock_segmented_message) + mock_interrupting_record = Mock(spec=CanPacketRecord, + packet_type=Mock()) + self.mock_can_transport_interface.receive_packet = Mock(return_value=mock_interrupting_record) + self.mock_can_packet_type_is_initial_packet_type.return_value = True + with pytest.raises(TransmissionInterruptionError): + PyCanTransportInterface.send_message(self.mock_can_transport_interface, message) + self.mock_can_transport_interface.segmenter.segmentation.assert_called_once_with(message) + self.mock_can_packet_type_is_initial_packet_type.assert_called_once_with(mock_interrupting_record.packet_type) + self.mock_warn.assert_not_called() + self.mock_can_transport_interface._update_n_bs_measured.assert_not_called() + + @pytest.mark.parametrize("message", [ + Mock(spec=UdsMessage, payload=[0x22, 0xF1, 0x86, 0xF1, 0x87, 0xF1, 0x88], addressing_type=AddressingType.PHYSICAL), + Mock(spec=UdsMessage, payload=[0x3E, 0x80], addressing_type=AddressingType.PHYSICAL), + ]) + def test_send_message__multiple_packets__overflow(self, message): + mock_segmented_message = [Mock(spec=CanPacket, packet_type=CanPacketType.FIRST_FRAME)] + mock_segmented_message.extend([Mock(spec=CanPacket, packet_type=CanPacketType.CONSECUTIVE_FRAME) + for _ in range(randint(1, 20))]) + self.mock_can_transport_interface.segmenter.segmentation = Mock(return_value=mock_segmented_message) + mock_flow_control_record_overflow = Mock(spec=CanPacketRecord, + packet_type=CanPacketType.FLOW_CONTROL, + flow_status=CanFlowStatus.Overflow) + self.mock_can_transport_interface.receive_packet = Mock(return_value=mock_flow_control_record_overflow) + with pytest.raises(OverflowError): + PyCanTransportInterface.send_message(self.mock_can_transport_interface, message) + self.mock_can_transport_interface.segmenter.segmentation.assert_called_once_with(message) + self.mock_can_transport_interface.receive_packet.assert_called_once_with( + timeout=self.mock_can_transport_interface.N_BS_TIMEOUT) + self.mock_warn.assert_not_called() + self.mock_can_transport_interface._update_n_bs_measured.assert_not_called() + + @pytest.mark.parametrize("message", [ + Mock(spec=UdsMessage, payload=[0x22, 0xF1, 0x86, 0xF1, 0x87, 0xF1, 0x88], addressing_type=AddressingType.PHYSICAL), + Mock(spec=UdsMessage, payload=[0x3E, 0x80], addressing_type=AddressingType.PHYSICAL), + ]) + def test_send_message__multiple_packets__unknown_flow_status(self, message): + mock_segmented_message = [Mock(spec=CanPacket, packet_type=CanPacketType.FIRST_FRAME)] + mock_segmented_message.extend([Mock(spec=CanPacket, packet_type=CanPacketType.CONSECUTIVE_FRAME) + for _ in range(randint(1, 20))]) + self.mock_can_transport_interface.segmenter.segmentation = Mock(return_value=mock_segmented_message) + mock_flow_control_record_overflow = Mock(spec=CanPacketRecord, + packet_type=CanPacketType.FLOW_CONTROL, + flow_status=Mock()) + self.mock_can_transport_interface.receive_packet = Mock(return_value=mock_flow_control_record_overflow) with pytest.raises(NotImplementedError): PyCanTransportInterface.send_message(self.mock_can_transport_interface, message) self.mock_can_transport_interface.segmenter.segmentation.assert_called_once_with(message) + self.mock_can_transport_interface.receive_packet.assert_called_once_with( + timeout=self.mock_can_transport_interface.N_BS_TIMEOUT) + self.mock_warn.assert_not_called() + self.mock_can_transport_interface._update_n_bs_measured.assert_not_called() # async_send_message @@ -1051,19 +1519,232 @@ async def test_async_send_message__single_frame(self, message): == self.mock_uds_message_record.return_value self.mock_can_transport_interface.segmenter.segmentation.assert_called_once_with(message) self.mock_can_transport_interface.async_send_packet.assert_called_once_with(mock_segmented_message[0], loop=None) - self.mock_uds_message_record.assert_called_once_with((self.mock_can_transport_interface.async_send_packet.return_value, )) + self.mock_uds_message_record.assert_called_once_with([self.mock_can_transport_interface.async_send_packet.return_value]) + self.mock_warn.assert_not_called() + self.mock_can_transport_interface._update_n_bs_measured.assert_called_once_with( + self.mock_uds_message_record.return_value) @pytest.mark.parametrize("message", [ Mock(spec=UdsMessage, payload=[0x22, 0xF1, 0x86, 0xF1, 0x87, 0xF1, 0x88], addressing_type=AddressingType.PHYSICAL), Mock(spec=UdsMessage, payload=[0x3E, 0x80], addressing_type=AddressingType.PHYSICAL), ]) + @pytest.mark.parametrize("st_min", [0x00, 0xFF]) @pytest.mark.asyncio - async def test_async_send_message__multiple_packets(self, message): - mock_segmented_message = [Mock(spec=CanPacket) for _ in range(randint(2, 20))] + async def test_async_send_message__multiple_packets__st_min__block_size_0(self, message, st_min): + mock_segmented_message = [Mock(spec=CanPacket, packet_type=CanPacketType.FIRST_FRAME)] + mock_segmented_message.extend([Mock(spec=CanPacket, packet_type=CanPacketType.CONSECUTIVE_FRAME) + for _ in range(randint(1, 20))]) self.mock_can_transport_interface.segmenter.segmentation = Mock(return_value=mock_segmented_message) + self.mock_can_transport_interface.n_cs = None + mock_flow_control_record = Mock(spec=CanPacketRecord, + packet_type=CanPacketType.FLOW_CONTROL, + flow_status=CanFlowStatus.ContinueToSend, + block_size=0, + st_min=st_min) + self.mock_can_transport_interface.async_receive_packet = AsyncMock(return_value=mock_flow_control_record) + mock_sent_packet_record = Mock(spec=CanPacketRecord) + self.mock_can_transport_interface._async_send_cf_packets_block.return_value = [mock_sent_packet_record] + assert await PyCanTransportInterface.async_send_message( + self.mock_can_transport_interface, message) == self.mock_uds_message_record.return_value + self.mock_can_transport_interface.segmenter.segmentation.assert_called_once_with(message) + self.mock_can_transport_interface.async_receive_packet.assert_called_once_with( + timeout=self.mock_can_transport_interface.N_BS_TIMEOUT, loop=None) + self.mock_can_transport_interface._async_send_cf_packets_block.assert_called_once_with( + cf_packets_block=mock_segmented_message[1:], + delay=self.mock_can_st_min_handler.decode.return_value, + loop=None) + self.mock_can_st_min_handler.decode.assert_called_once_with(st_min) + self.mock_uds_message_record.assert_called_once_with([ + self.mock_can_transport_interface.async_send_packet.return_value, + mock_flow_control_record, + mock_sent_packet_record + ]) + self.mock_warn.assert_not_called() + self.mock_can_transport_interface._update_n_bs_measured.assert_called_once_with( + self.mock_uds_message_record.return_value) + + @pytest.mark.parametrize("message", [ + Mock(spec=UdsMessage, payload=[0x22, 0xF1, 0x86, 0xF1, 0x87, 0xF1, 0x88], addressing_type=AddressingType.PHYSICAL), + Mock(spec=UdsMessage, payload=[0x3E, 0x80], addressing_type=AddressingType.PHYSICAL), + ]) + @pytest.mark.parametrize("n_cs, st_min", [ + (0, 0xFF), + (5, 0x00), + ]) + @pytest.mark.asyncio + async def test_async_send_message__multiple_packets__n_cs__block_size_1(self, message, n_cs, st_min): + mock_segmented_message = [Mock(spec=CanPacket, packet_type=CanPacketType.FIRST_FRAME)] + mock_segmented_message.extend([Mock(spec=CanPacket, packet_type=CanPacketType.CONSECUTIVE_FRAME) + for _ in range(randint(1, 20))]) + self.mock_can_transport_interface.segmenter.segmentation = Mock(return_value=mock_segmented_message[:]) + self.mock_can_transport_interface.n_cs = n_cs + mock_flow_control_record = Mock(spec=CanPacketRecord, + packet_type=CanPacketType.FLOW_CONTROL, + flow_status=CanFlowStatus.ContinueToSend, + block_size=1, + st_min=st_min) + self.mock_can_transport_interface.async_receive_packet = AsyncMock(return_value=mock_flow_control_record) + mock_sent_packet_record = Mock(spec=CanPacketRecord) + self.mock_can_transport_interface._async_send_cf_packets_block.return_value = [mock_sent_packet_record] + assert await PyCanTransportInterface.async_send_message( + self.mock_can_transport_interface, message) == self.mock_uds_message_record.return_value + self.mock_can_transport_interface.segmenter.segmentation.assert_called_once_with(message) + self.mock_can_transport_interface.async_receive_packet.assert_has_calls([ + call(timeout=self.mock_can_transport_interface.N_BS_TIMEOUT, loop=None) for _ in mock_segmented_message[1:] + ]) + self.mock_can_transport_interface._async_send_cf_packets_block.assert_has_calls([ + call(cf_packets_block=[packet], delay=n_cs, loop=None) for packet in mock_segmented_message[1:] + ]) + self.mock_can_st_min_handler.decode.assert_not_called() + self.mock_uds_message_record.assert_called_once_with([ + self.mock_can_transport_interface.async_send_packet.return_value, + *([mock_flow_control_record, mock_sent_packet_record]*len(mock_segmented_message[1:])) + ]) + self.mock_warn.assert_not_called() + self.mock_can_transport_interface._update_n_bs_measured.assert_called_once_with( + self.mock_uds_message_record.return_value) + + @pytest.mark.parametrize("message", [ + Mock(spec=UdsMessage, payload=[0x22, 0xF1, 0x86, 0xF1, 0x87, 0xF1, 0x88], addressing_type=AddressingType.PHYSICAL), + Mock(spec=UdsMessage, payload=[0x3E, 0x80], addressing_type=AddressingType.PHYSICAL), + ]) + @pytest.mark.asyncio + async def test_async_send_message__multiple_packets__wait(self, message): + mock_segmented_message = [Mock(spec=CanPacket, packet_type=CanPacketType.FIRST_FRAME)] + mock_segmented_message.extend([Mock(spec=CanPacket, packet_type=CanPacketType.CONSECUTIVE_FRAME) + for _ in range(randint(1, 20))]) + self.mock_can_transport_interface.segmenter.segmentation = Mock(return_value=mock_segmented_message) + mock_flow_control_record_wait = Mock(spec=CanPacketRecord, + packet_type=CanPacketType.FLOW_CONTROL, + flow_status=CanFlowStatus.Wait) + mock_flow_control_record_continue = Mock(spec=CanPacketRecord, + packet_type=CanPacketType.FLOW_CONTROL, + flow_status=CanFlowStatus.ContinueToSend, + block_size=0) + self.mock_can_transport_interface.async_receive_packet = AsyncMock( + side_effect=[mock_flow_control_record_wait, mock_flow_control_record_continue]) + mock_sent_packet_record = Mock(spec=CanPacketRecord) + self.mock_can_transport_interface._async_send_cf_packets_block.return_value = [mock_sent_packet_record] + assert await PyCanTransportInterface.async_send_message( + self.mock_can_transport_interface, message) == self.mock_uds_message_record.return_value + self.mock_can_transport_interface.segmenter.segmentation.assert_called_once_with(message) + self.mock_can_transport_interface.async_receive_packet.assert_has_calls([ + call(timeout=self.mock_can_transport_interface.N_BS_TIMEOUT, loop=None), + call(timeout=self.mock_can_transport_interface.N_BS_TIMEOUT, loop=None) + ]) + self.mock_uds_message_record.assert_called_once_with([ + self.mock_can_transport_interface.async_send_packet.return_value, + mock_flow_control_record_wait, + mock_flow_control_record_continue, + mock_sent_packet_record + ]) + self.mock_warn.assert_not_called() + self.mock_can_transport_interface._update_n_bs_measured.assert_called_once_with( + self.mock_uds_message_record.return_value) + + @pytest.mark.parametrize("message", [ + Mock(spec=UdsMessage, payload=[0x22, 0xF1, 0x86, 0xF1, 0x87, 0xF1, 0x88], addressing_type=AddressingType.PHYSICAL), + Mock(spec=UdsMessage, payload=[0x3E, 0x80], addressing_type=AddressingType.PHYSICAL), + ]) + @pytest.mark.asyncio + async def test_async_send_message__multiple_packets__unexpected_packet(self, message): + mock_segmented_message = [Mock(spec=CanPacket, packet_type=CanPacketType.FIRST_FRAME)] + mock_segmented_message.extend([Mock(spec=CanPacket, packet_type=CanPacketType.CONSECUTIVE_FRAME) + for _ in range(randint(1, 20))]) + self.mock_can_transport_interface.segmenter.segmentation = Mock(return_value=mock_segmented_message) + mock_flow_control_record = Mock(spec=CanPacketRecord, + packet_type=CanPacketType.FLOW_CONTROL, + flow_status=CanFlowStatus.ContinueToSend, + block_size=0) + mock_interrupting_record = Mock(spec=CanPacketRecord, + packet_type=Mock()) + self.mock_can_transport_interface.async_receive_packet = AsyncMock( + side_effect=[mock_interrupting_record, mock_flow_control_record]) + self.mock_can_packet_type_is_initial_packet_type.return_value = False + mock_sent_packet_record = Mock(spec=CanPacketRecord) + self.mock_can_transport_interface._async_send_cf_packets_block.return_value = [mock_sent_packet_record] + assert await PyCanTransportInterface.async_send_message( + self.mock_can_transport_interface, message) == self.mock_uds_message_record.return_value + self.mock_can_transport_interface.segmenter.segmentation.assert_called_once_with(message) + self.mock_can_transport_interface.async_receive_packet.assert_has_calls([ + call(timeout=self.mock_can_transport_interface.N_BS_TIMEOUT, loop=None), + call(timeout=self.mock_can_transport_interface.N_BS_TIMEOUT, loop=None) + ]) + self.mock_uds_message_record.assert_called_once_with([ + self.mock_can_transport_interface.async_send_packet.return_value, + mock_flow_control_record, + mock_sent_packet_record + ]) + self.mock_warn.assert_called() + self.mock_can_transport_interface._update_n_bs_measured.assert_called_once_with( + self.mock_uds_message_record.return_value) + + @pytest.mark.parametrize("message", [ + Mock(spec=UdsMessage, payload=[0x22, 0xF1, 0x86, 0xF1, 0x87, 0xF1, 0x88], addressing_type=AddressingType.PHYSICAL), + Mock(spec=UdsMessage, payload=[0x3E, 0x80], addressing_type=AddressingType.PHYSICAL), + ]) + @pytest.mark.asyncio + async def test_async_send_message__multiple_packets__transmission_interruption(self, message): + mock_segmented_message = [Mock(spec=CanPacket, packet_type=CanPacketType.FIRST_FRAME)] + mock_segmented_message.extend([Mock(spec=CanPacket, packet_type=CanPacketType.CONSECUTIVE_FRAME) + for _ in range(randint(1, 20))]) + self.mock_can_transport_interface.segmenter.segmentation = Mock(return_value=mock_segmented_message) + mock_interrupting_record = Mock(spec=CanPacketRecord, + packet_type=Mock()) + self.mock_can_transport_interface.async_receive_packet = AsyncMock(return_value=mock_interrupting_record) + self.mock_can_packet_type_is_initial_packet_type.return_value = True + with pytest.raises(TransmissionInterruptionError): + await PyCanTransportInterface.async_send_message(self.mock_can_transport_interface, message) + self.mock_can_transport_interface.segmenter.segmentation.assert_called_once_with(message) + self.mock_can_packet_type_is_initial_packet_type.assert_called_once_with(mock_interrupting_record.packet_type) + self.mock_warn.assert_not_called() + self.mock_can_transport_interface._update_n_bs_measured.assert_not_called() + + @pytest.mark.parametrize("message", [ + Mock(spec=UdsMessage, payload=[0x22, 0xF1, 0x86, 0xF1, 0x87, 0xF1, 0x88], addressing_type=AddressingType.PHYSICAL), + Mock(spec=UdsMessage, payload=[0x3E, 0x80], addressing_type=AddressingType.PHYSICAL), + ]) + @pytest.mark.asyncio + async def test_async_send_message__multiple_packets__overflow(self, message): + mock_segmented_message = [Mock(spec=CanPacket, packet_type=CanPacketType.FIRST_FRAME)] + mock_segmented_message.extend([Mock(spec=CanPacket, packet_type=CanPacketType.CONSECUTIVE_FRAME) + for _ in range(randint(1, 20))]) + self.mock_can_transport_interface.segmenter.segmentation = Mock(return_value=mock_segmented_message) + mock_flow_control_record_overflow = Mock(spec=CanPacketRecord, + packet_type=CanPacketType.FLOW_CONTROL, + flow_status=CanFlowStatus.Overflow) + self.mock_can_transport_interface.async_receive_packet = AsyncMock( + return_value=mock_flow_control_record_overflow) + with pytest.raises(OverflowError): + await PyCanTransportInterface.async_send_message(self.mock_can_transport_interface, message) + self.mock_can_transport_interface.segmenter.segmentation.assert_called_once_with(message) + self.mock_can_transport_interface.async_receive_packet.assert_called_once_with( + timeout=self.mock_can_transport_interface.N_BS_TIMEOUT, loop=None) + self.mock_warn.assert_not_called() + self.mock_can_transport_interface._update_n_bs_measured.assert_not_called() + + @pytest.mark.parametrize("message", [ + Mock(spec=UdsMessage, payload=[0x22, 0xF1, 0x86, 0xF1, 0x87, 0xF1, 0x88], addressing_type=AddressingType.PHYSICAL), + Mock(spec=UdsMessage, payload=[0x3E, 0x80], addressing_type=AddressingType.PHYSICAL), + ]) + @pytest.mark.asyncio + async def test_async_send_message__multiple_packets__unknown_flow_status(self, message): + mock_segmented_message = [Mock(spec=CanPacket, packet_type=CanPacketType.FIRST_FRAME)] + mock_segmented_message.extend([Mock(spec=CanPacket, packet_type=CanPacketType.CONSECUTIVE_FRAME) + for _ in range(randint(1, 20))]) + self.mock_can_transport_interface.segmenter.segmentation = Mock(return_value=mock_segmented_message) + mock_flow_control_record_overflow = Mock(spec=CanPacketRecord, + packet_type=CanPacketType.FLOW_CONTROL, + flow_status=Mock()) + self.mock_can_transport_interface.async_receive_packet = AsyncMock( + return_value=mock_flow_control_record_overflow) with pytest.raises(NotImplementedError): await PyCanTransportInterface.async_send_message(self.mock_can_transport_interface, message) self.mock_can_transport_interface.segmenter.segmentation.assert_called_once_with(message) + self.mock_can_transport_interface.async_receive_packet.assert_called_once_with( + timeout=self.mock_can_transport_interface.N_BS_TIMEOUT, loop=None) + self.mock_warn.assert_not_called() + self.mock_can_transport_interface._update_n_bs_measured.assert_not_called() # receive_message diff --git a/tests/system_tests/transport_interface/can_transport_interface/test_python_can.py b/tests/system_tests/transport_interface/can_transport_interface/test_python_can.py index d581f8ef..fa2b2e19 100644 --- a/tests/system_tests/transport_interface/can_transport_interface/test_python_can.py +++ b/tests/system_tests/transport_interface/can_transport_interface/test_python_can.py @@ -12,6 +12,7 @@ from uds.segmentation import CanSegmenter from uds.transmission_attributes import AddressingType, TransmissionDirection from uds.transport_interface import PyCanTransportInterface +from uds.utilities import TransmissionInterruptionError class TestPythonCanKvaser: @@ -24,7 +25,7 @@ class TestPythonCanKvaser: """ TASK_TIMING_TOLERANCE = 30. # ms - DELAY_AFTER_RECEIVING_FRAME = 1. # ms + DELAY_AFTER_RECEIVING_FRAME = 10. # ms DELAY_BETWEEN_CONSECUTIVE_FRAMES = 50. # ms def setup_class(self): @@ -132,6 +133,15 @@ def test_send_packet(self, packet_type, addressing_type, addressing_information, assert packet_record.target_address == packet.target_address == target_address assert packet_record.source_address == packet.source_address == source_address assert packet_record.address_extension == packet.address_extension == address_extension + # timing parameters + if packet_type == CanPacketType.FLOW_CONTROL: + assert can_transport_interface.n_as_measured is None + # TODO: https://github.com/mdabrowski1990/uds/issues/228 - uncomment when resolved + # assert 0 < can_transport_interface.n_ar_measured + else: + # TODO: https://github.com/mdabrowski1990/uds/issues/228 - uncomment when resolved + # assert 0 < can_transport_interface.n_as_measured + assert can_transport_interface.n_ar_measured is None # performance checks # TODO: https://github.com/mdabrowski1990/uds/issues/228 - uncomment when resolved # assert datetime_before_send < packet_record.transmission_time < datetime_after_send @@ -180,7 +190,7 @@ def test_send_packet(self, packet_type, addressing_type, addressing_information, Message(data=[0xFF, 0x02, 0x3E, 0x80, 0xAA, 0xAA, 0xAA, 0xAA])), ]) @pytest.mark.parametrize("timeout, send_after", [ - (1000, 1001), # ms + (1000, 1005), # ms (50, 55), ]) def test_receive_packet__timeout(self, addressing_information, addressing_type, frame, timeout, send_after): @@ -212,7 +222,11 @@ def test_receive_packet__timeout(self, addressing_information, addressing_type, can_transport_interface.receive_packet(timeout=timeout) time_after_receive = time() assert timeout < (time_after_receive - time_before_receive) * 1000. < timeout + self.TASK_TIMING_TOLERANCE - sleep((send_after - timeout) * 2 / 1000.) # wait till packet arrives + # timing parameters + assert can_transport_interface.n_as_measured is None + assert can_transport_interface.n_ar_measured is None + # wait till packet arrives + sleep((send_after - timeout) * 2 / 1000.) @pytest.mark.parametrize("addressing_information, frame", [ (CanAddressingInformation(addressing_format=CanAddressingFormat.NORMAL_11BIT_ADDRESSING, @@ -459,10 +473,20 @@ async def test_async_send_packet(self, packet_type, addressing_type, addressing_ assert packet_record.target_address == packet.target_address == target_address assert packet_record.source_address == packet.source_address == source_address assert packet_record.address_extension == packet.address_extension == address_extension + # timing parameters + if packet_type == CanPacketType.FLOW_CONTROL: + assert can_transport_interface.n_as_measured is None + # TODO: https://github.com/mdabrowski1990/uds/issues/228 - uncomment when resolved + # assert 0 < can_transport_interface.n_ar_measured + else: + # TODO: https://github.com/mdabrowski1990/uds/issues/228 - uncomment when resolved + # assert 0 < can_transport_interface.n_as_measured + assert can_transport_interface.n_ar_measured is None # performance checks # TODO: https://github.com/mdabrowski1990/uds/issues/228 - uncomment when resolved # assert datetime_before_send < packet_record.transmission_time < datetime_after_send + # async_receive_packet @pytest.mark.parametrize("addressing_type, addressing_information, frame", [ @@ -482,7 +506,7 @@ async def test_async_send_packet(self, packet_type, addressing_type, addressing_ Message(data=[0xFF, 0x02, 0x3E, 0x80, 0xAA, 0xAA, 0xAA, 0xAA])), ]) @pytest.mark.parametrize("timeout, send_after", [ - (1000, 1001), # ms + (1000, 1005), # ms (50, 55), ]) @pytest.mark.asyncio @@ -523,6 +547,7 @@ async def _send_frame(): await future_record time_after_receive = time() assert timeout < (time_after_receive - time_before_receive) * 1000. < timeout + self.TASK_TIMING_TOLERANCE + # wait till frame arrives await frame_sending_task sleep(self.DELAY_AFTER_RECEIVING_FRAME / 1000.) @@ -590,15 +615,12 @@ async def _send_frame(): # and `addressing_information` can_transport_interface = PyCanTransportInterface(can_bus_manager=self.can_interface_1, addressing_information=addressing_information) - future_record = can_transport_interface.async_receive_packet(timeout=timeout) - tasks = [asyncio.create_task(_send_frame()), asyncio.create_task(future_record)] + + send_frame_task = asyncio.create_task(_send_frame()) datetime_before_receive = datetime.now() - done_tasks, _ = await asyncio.wait(tasks) + packet_record = await can_transport_interface.async_receive_packet(timeout=timeout) datetime_after_receive = datetime.now() - received_records = tuple(filter(lambda result: isinstance(result, CanPacketRecord), - (done_task.result() for done_task in done_tasks))) - assert len(received_records) == 1, "CAN packet was received" - packet_record = received_records[0] + await send_frame_task assert isinstance(packet_record, CanPacketRecord) assert packet_record.direction == TransmissionDirection.RECEIVED assert packet_record.raw_frame_data == tuple(frame.data) @@ -677,15 +699,11 @@ async def _send_frame(): # and `addressing_information` can_transport_interface = PyCanTransportInterface(can_bus_manager=self.can_interface_1, addressing_information=addressing_information) - future_record = can_transport_interface.async_receive_packet(timeout=timeout) - tasks = [asyncio.create_task(_send_frame()), asyncio.create_task(future_record)] + send_frame_task = asyncio.create_task(_send_frame()) datetime_before_receive = datetime.now() - done_tasks, _ = await asyncio.wait(tasks) + packet_record = await can_transport_interface.async_receive_packet(timeout=timeout) datetime_after_receive = datetime.now() - received_records = tuple(filter(lambda result: isinstance(result, CanPacketRecord), - (done_task.result() for done_task in done_tasks))) - assert len(received_records) == 1, "CAN packet was received" - packet_record = received_records[0] + await send_frame_task assert isinstance(packet_record, CanPacketRecord) assert packet_record.direction == TransmissionDirection.RECEIVED assert packet_record.raw_frame_data == tuple(frame.data) @@ -705,9 +723,8 @@ async def _send_frame(): @pytest.mark.parametrize("message", [ UdsMessage(payload=[0x22, 0x12, 0x34], addressing_type=AddressingType.PHYSICAL), UdsMessage(payload=[0x10, 0x01], addressing_type=AddressingType.FUNCTIONAL), - # TODO: add more with https://github.com/mdabrowski1990/uds/issues/267 ]) - def test_send_message(self, example_addressing_information, message): + def test_send_message__sf(self, example_addressing_information, message): """ Check for a simple synchronous UDS message sending. @@ -729,24 +746,117 @@ def test_send_message(self, example_addressing_information, message): assert message_record.direction == TransmissionDirection.TRANSMITTED assert message_record.payload == message.payload assert message_record.addressing_type == message.addressing_type + assert message_record.transmission_start == message_record.transmission_end + assert len(message_record.packets_records) == 1 + assert message_record.packets_records[0].packet_type == CanPacketType.SINGLE_FRAME + # timing parameters + assert can_transport_interface.n_bs_measured is None # performance checks # TODO: https://github.com/mdabrowski1990/uds/issues/228 - uncomment when resolved # assert datetime_before_send < message_record.transmission_start # assert message_record.transmission_end < datetime_after_send - if len(message_record.packets_records) == 1: - assert message_record.transmission_start == message_record.transmission_end - else: - assert message_record.transmission_start < message_record.transmission_end + + @pytest.mark.parametrize("message", [ + UdsMessage(payload=[0x62, 0x12, 0x34, *range(100, 200)], addressing_type=AddressingType.PHYSICAL), + UdsMessage(payload=[0x22, *range(62)], addressing_type=AddressingType.PHYSICAL), + ]) + @pytest.mark.parametrize("send_after", [5, 950]) + def test_send_message__multi_packets(self, example_addressing_information, + example_addressing_information_2nd_node, + message, send_after): + """ + Check for a synchronous multi packet (FF + CF) UDS message sending. + + Procedure: + 1. Schedule Flow Control CAN Packet with information to continue sending all consecutive frame packets at once. + 2. Send a UDS message using Transport Interface (via CAN Interface). + Expected: UDS message record returned. + 3. Validate transmitted UDS message record attributes. + Expected: Attributes of UDS message record are in line with the transmitted UDS message. + + :param example_addressing_information: Example Addressing Information of a CAN Node. + :param example_addressing_information_2nd_node: Example Addressing Information of a CAN Node. + :param message: UDS message to send. + :param send_after: Delay to use for sending CAN flow control. + """ + can_transport_interface = PyCanTransportInterface(can_bus_manager=self.can_interface_1, + addressing_information=example_addressing_information) + other_node_segmenter = CanSegmenter(addressing_information=example_addressing_information_2nd_node) + flow_control_packet = other_node_segmenter.get_flow_control_packet(flow_status=CanFlowStatus.ContinueToSend, + block_size=0, + st_min=0) + flow_control_frame = Message(arbitration_id=flow_control_packet.can_id, data=flow_control_packet.raw_frame_data) + Timer(interval=send_after / 1000., function=self.can_interface_2.send, args=(flow_control_frame,)).start() + datetime_before_send = datetime.now() + message_record = can_transport_interface.send_message(message) + datetime_after_send = datetime.now() + assert isinstance(message_record, UdsMessageRecord) + assert message_record.direction == TransmissionDirection.TRANSMITTED + assert message_record.payload == message.payload + assert message_record.addressing_type == message.addressing_type + assert message_record.transmission_start < message_record.transmission_end + assert len(message_record.packets_records) > 1 + assert message_record.packets_records[0].packet_type == CanPacketType.FIRST_FRAME + assert message_record.packets_records[1].packet_type == CanPacketType.FLOW_CONTROL + assert message_record.packets_records[1].direction == TransmissionDirection.RECEIVED + assert all(following_packet.packet_type == CanPacketType.CONSECUTIVE_FRAME + for following_packet in message_record.packets_records[2:]) + # timing parameters + assert isinstance(can_transport_interface.n_bs_measured, tuple) + assert len(can_transport_interface.n_bs_measured) == 1 + # performance checks + # TODO: https://github.com/mdabrowski1990/uds/issues/228 - uncomment when resolved + # assert datetime_before_send < message_record.transmission_start + # assert message_record.transmission_end < datetime_after_send + # assert (send_after - self.TASK_TIMING_TOLERANCE + # <= can_transport_interface.n_bs_measured[0] + # <= send_after + self.TASK_TIMING_TOLERANCE) + + @pytest.mark.parametrize("message", [ + UdsMessage(payload=[0x62, 0x12, 0x34, *range(100, 200)], addressing_type=AddressingType.PHYSICAL), + UdsMessage(payload=[0x22, *range(62)], addressing_type=AddressingType.PHYSICAL), + ]) + def test_send_message__multi_packets__timeout(self, example_addressing_information, + example_addressing_information_2nd_node, + message): + """ + Check for a timeout (N_Bs timeout exceeded) during synchronous multi packet (FF + CF) UDS message sending. + + Procedure: + 1. Schedule Flow Control CAN Packet just after N_Bs timeout. + 2. Send a UDS message using Transport Interface (via CAN Interface). + Expected: Timeout exception is raised. + + :param example_addressing_information: Example Addressing Information of a CAN Node. + :param example_addressing_information_2nd_node: Example Addressing Information of a CAN Node. + :param message: UDS message to send. + """ + can_transport_interface = PyCanTransportInterface(can_bus_manager=self.can_interface_1, + addressing_information=example_addressing_information) + other_node_segmenter = CanSegmenter(addressing_information=example_addressing_information_2nd_node) + flow_control_packet = other_node_segmenter.get_flow_control_packet(flow_status=CanFlowStatus.ContinueToSend, + block_size=0, + st_min=0) + flow_control_frame = Message(arbitration_id=flow_control_packet.can_id, data=flow_control_packet.raw_frame_data) + Timer(interval=(can_transport_interface.n_bs_timeout + 1) / 1000., + function=self.can_interface_2.send, args=(flow_control_frame,)).start() + time_before_receive = time() + with pytest.raises(TimeoutError): + can_transport_interface.send_message(message) + time_after_receive = time() + assert (can_transport_interface.n_bs_timeout + < (time_after_receive - time_before_receive) * 1000. + < can_transport_interface.n_bs_timeout + self.TASK_TIMING_TOLERANCE) + sleep(self.TASK_TIMING_TOLERANCE / 1000.) # wait till packet arrives # async_send_message @pytest.mark.parametrize("message", [ UdsMessage(payload=[0x22, 0x12, 0x34], addressing_type=AddressingType.PHYSICAL), UdsMessage(payload=[0x10, 0x01], addressing_type=AddressingType.FUNCTIONAL), - # TODO: add more with https://github.com/mdabrowski1990/uds/issues/267 ]) @pytest.mark.asyncio - async def test_async_send_message(self, example_addressing_information, message): + async def test_async_send_message__sf(self, example_addressing_information, message): """ Check for a simple asynchronous UDS message sending. @@ -768,14 +878,120 @@ async def test_async_send_message(self, example_addressing_information, message) assert message_record.direction == TransmissionDirection.TRANSMITTED assert message_record.payload == message.payload assert message_record.addressing_type == message.addressing_type + assert message_record.transmission_start == message_record.transmission_end + assert len(message_record.packets_records) == 1 + assert message_record.packets_records[0].packet_type == CanPacketType.SINGLE_FRAME + # timing parameters + assert can_transport_interface.n_bs_measured is None # performance checks # TODO: https://github.com/mdabrowski1990/uds/issues/228 - uncomment when resolved # assert datetime_before_send < message_record.transmission_start # assert message_record.transmission_end < datetime_after_send - if len(message_record.packets_records) == 1: - assert message_record.transmission_start == message_record.transmission_end - else: - assert message_record.transmission_start < message_record.transmission_end + + @pytest.mark.parametrize("message", [ + UdsMessage(payload=[0x62, 0x12, 0x34, *range(100, 200)], addressing_type=AddressingType.PHYSICAL), + UdsMessage(payload=[0x22, *range(62)], addressing_type=AddressingType.PHYSICAL), + ]) + @pytest.mark.parametrize("send_after", [5, 950]) + @pytest.mark.asyncio + async def test_async_send_message__multi_packets(self, example_addressing_information, + example_addressing_information_2nd_node, + message, send_after): + """ + Check for an asynchronous multi packet (FF + CF) UDS message sending. + + Procedure: + 1. Schedule Flow Control CAN Packet with information to continue sending all consecutive frame packets at once. + 2. Send (using async method) a UDS message using Transport Interface (via CAN Interface). + Expected: UDS message record returned. + 3. Validate transmitted UDS message record attributes. + Expected: Attributes of UDS message record are in line with the transmitted UDS message. + + :param example_addressing_information: Example Addressing Information of a CAN Node. + :param example_addressing_information_2nd_node: Example Addressing Information of a CAN Node. + :param message: UDS message to send. + :param send_after: Delay to use for sending CAN flow control. + """ + can_transport_interface = PyCanTransportInterface(can_bus_manager=self.can_interface_1, + addressing_information=example_addressing_information) + other_node_segmenter = CanSegmenter(addressing_information=example_addressing_information_2nd_node) + flow_control_packet = other_node_segmenter.get_flow_control_packet(flow_status=CanFlowStatus.ContinueToSend, + block_size=0, + st_min=0) + flow_control_frame = Message(arbitration_id=flow_control_packet.can_id, data=flow_control_packet.raw_frame_data) + + async def _send_frame(): + await asyncio.sleep(send_after / 1000.) + self.can_interface_2.send(flow_control_frame) + + send_frame_task = asyncio.create_task(_send_frame()) + datetime_before_send = datetime.now() + message_record = await can_transport_interface.async_send_message(message) + datetime_after_send = datetime.now() + await send_frame_task + assert isinstance(message_record, UdsMessageRecord) + assert message_record.direction == TransmissionDirection.TRANSMITTED + assert message_record.payload == message.payload + assert message_record.addressing_type == message.addressing_type + assert len(message_record.packets_records) > 1 + assert message_record.packets_records[0].packet_type == CanPacketType.FIRST_FRAME + assert message_record.packets_records[1].packet_type == CanPacketType.FLOW_CONTROL + assert message_record.packets_records[1].direction == TransmissionDirection.RECEIVED + assert all(following_packet.packet_type == CanPacketType.CONSECUTIVE_FRAME + for following_packet in message_record.packets_records[2:]) + # timing parameters + assert isinstance(can_transport_interface.n_bs_measured, tuple) + assert len(can_transport_interface.n_bs_measured) == 1 + # performance checks + # TODO: https://github.com/mdabrowski1990/uds/issues/228 - uncomment when resolved + # assert datetime_before_send < message_record.transmission_start + # assert message_record.transmission_end < datetime_after_send + # assert (send_after - self.TASK_TIMING_TOLERANCE + # <= can_transport_interface.n_bs_measured[0] + # <= send_after + self.TASK_TIMING_TOLERANCE) + + @pytest.mark.parametrize("message", [ + UdsMessage(payload=[0x62, 0x12, 0x34, *range(100, 200)], addressing_type=AddressingType.PHYSICAL), + UdsMessage(payload=[0x22, *range(62)], addressing_type=AddressingType.PHYSICAL), + ]) + @pytest.mark.asyncio + async def test_async_send_message__multi_packets__timeout(self, example_addressing_information, + example_addressing_information_2nd_node, + message): + """ + Check for a timeout (N_Bs timeout exceeded) during asynchronous multi packet (FF + CF) UDS message sending. + + Procedure: + 1. Schedule Flow Control CAN Packet just after N_Bs timeout. + 2. Send (using async method) a UDS message using Transport Interface (via CAN Interface). + Expected: Timeout exception is raised. + + :param example_addressing_information: Example Addressing Information of a CAN Node. + :param example_addressing_information_2nd_node: Example Addressing Information of a CAN Node. + :param message: UDS message to send. + """ + can_transport_interface = PyCanTransportInterface(can_bus_manager=self.can_interface_1, + addressing_information=example_addressing_information) + other_node_segmenter = CanSegmenter(addressing_information=example_addressing_information_2nd_node) + flow_control_packet = other_node_segmenter.get_flow_control_packet(flow_status=CanFlowStatus.ContinueToSend, + block_size=0, + st_min=0) + flow_control_frame = Message(arbitration_id=flow_control_packet.can_id, data=flow_control_packet.raw_frame_data) + + async def _send_frame(): + await asyncio.sleep((can_transport_interface.n_bs_timeout + 1) / 1000.) + self.can_interface_2.send(flow_control_frame) + + send_frame_task = asyncio.create_task(_send_frame()) + time_before_receive = time() + with pytest.raises(TimeoutError): + await can_transport_interface.async_send_message(message) + time_after_receive = time() + assert (can_transport_interface.n_bs_timeout + < (time_after_receive - time_before_receive) * 1000. + < can_transport_interface.n_bs_timeout + self.TASK_TIMING_TOLERANCE) + await send_frame_task + sleep(self.DELAY_AFTER_RECEIVING_FRAME / 1000.) # receive_message @@ -784,7 +1000,7 @@ async def test_async_send_message(self, example_addressing_information, message) UdsMessage(payload=[0x10, 0x01], addressing_type=AddressingType.FUNCTIONAL), ]) @pytest.mark.parametrize("timeout, send_after", [ - (1000, 1001), # ms + (1000, 1005), # ms (50, 55), ]) def test_receive_message__sf__timeout(self, example_addressing_information, example_addressing_information_2nd_node, @@ -858,14 +1074,11 @@ def test_receive_message__sf(self, example_addressing_information, example_addre assert message_record.direction == TransmissionDirection.RECEIVED assert message_record.payload == message.payload assert message_record.addressing_type == message.addressing_type + assert message_record.transmission_start == message_record.transmission_end # performance checks # TODO: https://github.com/mdabrowski1990/uds/issues/228 - uncomment when resolved # assert datetime_before_send < message_record.transmission_start # assert message_record.transmission_end < datetime_after_send - if len(message_record.packets_records) == 1: - assert message_record.transmission_start == message_record.transmission_end - else: - assert message_record.transmission_start < message_record.transmission_end # TODO: add more with https://github.com/mdabrowski1990/uds/issues/266 @@ -876,7 +1089,7 @@ def test_receive_message__sf(self, example_addressing_information, example_addre UdsMessage(payload=[0x10, 0x01], addressing_type=AddressingType.FUNCTIONAL), ]) @pytest.mark.parametrize("timeout, send_after", [ - (1000, 1001), # ms + (1000, 1005), # ms (50, 55), ]) @pytest.mark.asyncio @@ -907,14 +1120,13 @@ async def _send_frame(): await asyncio.sleep(send_after / 1000.) self.can_interface_2.send(frame) - future_record = can_transport_interface.async_receive_message(timeout=timeout) - frame_sending_task = asyncio.create_task(_send_frame()) + send_frame_task = asyncio.create_task(_send_frame()) time_before_receive = time() with pytest.raises(TimeoutError): - await future_record + await can_transport_interface.async_receive_message(timeout=timeout) time_after_receive = time() + await send_frame_task assert timeout < (time_after_receive - time_before_receive) * 1000. < timeout + self.TASK_TIMING_TOLERANCE - await frame_sending_task sleep(self.DELAY_AFTER_RECEIVING_FRAME / 1000.) @pytest.mark.parametrize("message", [ @@ -957,26 +1169,19 @@ async def _send_frame(): await asyncio.sleep(send_after / 1000.) self.can_interface_2.send(frame) - future_record = can_transport_interface.async_receive_message(timeout=timeout) - tasks = [asyncio.create_task(_send_frame()), asyncio.create_task(future_record)] + send_frame_task = asyncio.create_task(_send_frame()) datetime_before_receive = datetime.now() - done_tasks, _ = await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED) + message_record = await can_transport_interface.async_receive_message(timeout=timeout) datetime_after_receive = datetime.now() - received_records = tuple(filter(lambda result: isinstance(result, UdsMessageRecord), - (done_task.result() for done_task in done_tasks))) - assert len(received_records) == 1, "UDS message was received" - message_record = received_records[0] + await send_frame_task assert message_record.direction == TransmissionDirection.RECEIVED assert message_record.payload == message.payload assert message_record.addressing_type == message.addressing_type + assert message_record.transmission_start == message_record.transmission_end # performance checks # TODO: https://github.com/mdabrowski1990/uds/issues/228 - uncomment when resolved # assert datetime_before_send < message_record.transmission_start # assert message_record.transmission_end < datetime_after_send - if len(message_record.packets_records) == 1: - assert message_record.transmission_start == message_record.transmission_end - else: - assert message_record.transmission_start < message_record.transmission_end # TODO: add more with https://github.com/mdabrowski1990/uds/issues/266 @@ -1057,26 +1262,26 @@ async def test_async_send_packet_on_one_receive_on_other_bus(self, example_addre addressing_information=example_addressing_information_2nd_node) uds_message = UdsMessage(payload=payload, addressing_type=addressing_type) packet = can_transport_interface_2.segmenter.segmentation(uds_message)[0] - tasks = [asyncio.create_task(can_transport_interface_2.async_send_packet(packet)), - asyncio.create_task(can_transport_interface_1.async_receive_packet(timeout=100))] - done_tasks, _ = await asyncio.wait(tasks) - packet_record_1, packet_record_2 = [done_task.result() for done_task in done_tasks] - assert isinstance(packet_record_1, CanPacketRecord) and isinstance(packet_record_2, CanPacketRecord) - assert {packet_record_1.direction, packet_record_2.direction} \ - == {TransmissionDirection.TRANSMITTED, TransmissionDirection.RECEIVED} - assert packet_record_1.addressing_format == packet_record_2.addressing_format - assert packet_record_1.can_id == packet_record_2.can_id - assert packet_record_1.raw_frame_data == packet_record_2.raw_frame_data - assert packet_record_1.addressing_type == packet_record_2.addressing_type + receive_packet_task = asyncio.create_task(can_transport_interface_1.async_receive_packet(timeout=100)) + sent_packet_record = await can_transport_interface_2.async_send_packet(packet) + received_packet_record = await receive_packet_task + assert isinstance(sent_packet_record, CanPacketRecord) + assert isinstance(received_packet_record, CanPacketRecord) + assert sent_packet_record.direction == TransmissionDirection.TRANSMITTED + assert received_packet_record.direction == TransmissionDirection.RECEIVED + assert sent_packet_record.addressing_format == received_packet_record.addressing_format + assert sent_packet_record.can_id == received_packet_record.can_id + assert sent_packet_record.raw_frame_data == received_packet_record.raw_frame_data + assert sent_packet_record.addressing_type == received_packet_record.addressing_type @pytest.mark.parametrize("message", [ UdsMessage(payload=[0x22, 0x12, 0x34], addressing_type=AddressingType.PHYSICAL), UdsMessage(payload=[0x10, 0x01], addressing_type=AddressingType.FUNCTIONAL), - # TODO: add more after https://github.com/mdabrowski1990/uds/issues/266 and - # https://github.com/mdabrowski1990/uds/issues/267 + # TODO: add more after https://github.com/mdabrowski1990/uds/issues/266 ]) def test_send_message_on_one_receive_on_other_bus(self, example_addressing_information, - example_addressing_information_2nd_node, message): + example_addressing_information_2nd_node, + message): """ Check for sending and receiving UDS message using two Transport Interfaces. @@ -1110,12 +1315,12 @@ def test_send_message_on_one_receive_on_other_bus(self, example_addressing_infor @pytest.mark.parametrize("message", [ UdsMessage(payload=[0x22, 0x12, 0x34], addressing_type=AddressingType.PHYSICAL), UdsMessage(payload=[0x10, 0x01], addressing_type=AddressingType.FUNCTIONAL), - # TODO: add more after https://github.com/mdabrowski1990/uds/issues/266 and - # https://github.com/mdabrowski1990/uds/issues/267 + # TODO: add more after https://github.com/mdabrowski1990/uds/issues/266 ]) @pytest.mark.asyncio async def test_async_send_message_on_one_receive_on_other_bus(self, example_addressing_information, - example_addressing_information_2nd_node, message): + example_addressing_information_2nd_node, + message): """ Check for asynchronous sending and receiving UDS message using two Transport Interfaces. @@ -1137,15 +1342,18 @@ async def test_async_send_message_on_one_receive_on_other_bus(self, example_addr addressing_information=example_addressing_information) can_transport_interface_2 = PyCanTransportInterface(can_bus_manager=self.can_interface_2, addressing_information=example_addressing_information_2nd_node) - tasks = [asyncio.create_task(can_transport_interface_2.async_send_message(message)), - asyncio.create_task(can_transport_interface_1.async_receive_message(timeout=100))] - done_tasks, _ = await asyncio.wait(tasks) - message_record_1, message_record_2 = [done_task.result() for done_task in done_tasks] - assert isinstance(message_record_1, UdsMessageRecord) and isinstance(message_record_2, UdsMessageRecord) - assert {message_record_1.direction, message_record_2.direction} \ - == {TransmissionDirection.TRANSMITTED, TransmissionDirection.RECEIVED} - assert message_record_1.addressing_type == message_record_2.addressing_type == message.addressing_type - assert message_record_1.payload == message_record_2.payload == message.payload + + receive_message_task = asyncio.create_task(can_transport_interface_1.async_receive_message(timeout=100)) + sent_message_record = await can_transport_interface_2.async_send_message(message) + received_message_record = await receive_message_task + assert isinstance(sent_message_record, UdsMessageRecord) + assert isinstance(received_message_record, UdsMessageRecord) + assert sent_message_record.direction == TransmissionDirection.TRANSMITTED + assert received_message_record.direction == TransmissionDirection.RECEIVED + assert sent_message_record.addressing_type == received_message_record.addressing_type == message.addressing_type + assert sent_message_record.payload == received_message_record.payload == message.payload + + # TODO: after https://github.com/mdabrowski1990/uds/issues/266: Flow Control CONTINUE TO SEND with changing block size and STmin (including max value) # error guessing @@ -1342,3 +1550,188 @@ async def test_async_observe_tx_packet(self, example_addressing_information, exa # performance checks # TODO: https://github.com/mdabrowski1990/uds/issues/228 - uncomment when resolved # assert packet_record.transmission_time > datetime_before_send + + # TODO: after https://github.com/mdabrowski1990/uds/issues/266: Flow Control max WAIT, then continue transmission + # TODO: after https://github.com/mdabrowski1990/uds/issues/266: Flow Control spam WAIT till timeout + + @pytest.mark.parametrize("message", [ + UdsMessage(payload=[0x62, 0x12, 0x34, *range(100, 200)], addressing_type=AddressingType.PHYSICAL), + UdsMessage(payload=[0x22, *range(62)], addressing_type=AddressingType.PHYSICAL), + ]) + @pytest.mark.parametrize("send_after", [5, 950]) + def test_overflow_during_message_sending(self, example_addressing_information, + example_addressing_information_2nd_node, + message, send_after): + """ + Check for handling Overflow status during synchronous multi packet (FF + CF) UDS message sending. + + Procedure: + 1. Schedule Flow Control CAN Packet with Overflow information. + 2. Send a UDS message using Transport Interface (via CAN Interface). + Expected: UDS message transmission stopped and an exception raised. + + :param example_addressing_information: Example Addressing Information of a CAN Node. + :param example_addressing_information_2nd_node: Example Addressing Information of a CAN Node. + :param message: UDS message to send. + :param send_after: Delay to use for sending CAN flow control. + """ + can_transport_interface = PyCanTransportInterface(can_bus_manager=self.can_interface_1, + addressing_information=example_addressing_information) + other_node_segmenter = CanSegmenter(addressing_information=example_addressing_information_2nd_node) + flow_control_packet = other_node_segmenter.get_flow_control_packet(flow_status=CanFlowStatus.Overflow) + flow_control_frame = Message(arbitration_id=flow_control_packet.can_id, data=flow_control_packet.raw_frame_data) + Timer(interval=send_after / 1000., function=self.can_interface_2.send, args=(flow_control_frame,)).start() + with pytest.raises(OverflowError): + can_transport_interface.send_message(message) + + @pytest.mark.parametrize("message", [ + UdsMessage(payload=[0x62, 0x12, 0x34, *range(100, 200)], addressing_type=AddressingType.PHYSICAL), + UdsMessage(payload=[0x22, *range(62)], addressing_type=AddressingType.PHYSICAL), + ]) + @pytest.mark.parametrize("send_after", [5, 950]) + @pytest.mark.asyncio + async def test_overflow_during_async_message_sending(self, example_addressing_information, + example_addressing_information_2nd_node, + message, send_after): + """ + Check for handling Overflow status during asynchronous multi packet (FF + CF) UDS message sending. + + Procedure: + 1. Schedule Flow Control CAN Packet with Overflow information. + 2. Send (using async method) a UDS message using Transport Interface (via CAN Interface). + Expected: UDS message transmission stopped and an exception raised. + + :param example_addressing_information: Example Addressing Information of a CAN Node. + :param example_addressing_information_2nd_node: Example Addressing Information of a CAN Node. + :param message: UDS message to send. + :param send_after: Delay to use for sending CAN flow control. + """ + can_transport_interface = PyCanTransportInterface(can_bus_manager=self.can_interface_1, + addressing_information=example_addressing_information) + other_node_segmenter = CanSegmenter(addressing_information=example_addressing_information_2nd_node) + flow_control_packet = other_node_segmenter.get_flow_control_packet(flow_status=CanFlowStatus.Overflow) + flow_control_frame = Message(arbitration_id=flow_control_packet.can_id, data=flow_control_packet.raw_frame_data) + + async def _send_frame(): + await asyncio.sleep(send_after / 1000.) + self.can_interface_2.send(flow_control_frame) + + future_record = can_transport_interface.async_send_message(message) + frame_sending_task = asyncio.create_task(_send_frame()) + with pytest.raises(OverflowError): + await future_record + await frame_sending_task + + @pytest.mark.parametrize("message", [ + UdsMessage(payload=[0x62, 0x12, 0x34, *range(100, 200)], addressing_type=AddressingType.PHYSICAL), + UdsMessage(payload=[0x22, *range(62)], addressing_type=AddressingType.PHYSICAL), + ]) + @pytest.mark.parametrize("new_message", [ + UdsMessage([0x10, 0x81], AddressingType.FUNCTIONAL), + UdsMessage(payload=[0x62, 0x12, 0x34, *range(100, 200)], addressing_type=AddressingType.PHYSICAL), + ]) + @pytest.mark.parametrize("fc_after, new_message_after, st_min", [ + (50, 1, 100), + (8, 10, 10), + (5, 100, 50), + ]) + def test_new_message_started_when_multi_packet_message_sending(self, example_addressing_information, + example_addressing_information_2nd_node, + message, new_message, fc_after, new_message_after, + st_min): + """ + Check for a synchronous multi packet (FF + CF) UDS message sending being interrupted by a new message. + + Procedure: + 1. Schedule Flow Control CAN packet with information to continue sending all consecutive frame packets at once. + 2. Schedule Single Frame/First Frame CAN packet starting a transmission of a new message. + 3. Send a UDS message using Transport Interface (via CAN Interface). + Expected: UDS message transmission stopped and an exception raised. + + :param example_addressing_information: Example Addressing Information of a CAN Node. + :param example_addressing_information_2nd_node: Example Addressing Information of a CAN Node. + :param message: UDS message to send. + :param new_message: UDS message to interrupt. + :param fc_after: Delay to use for sending CAN flow control. + :param new_message_after: Delay to use for sending interrupting message. + :param st_min: STmin value to transmit in Flow Control. + """ + can_transport_interface = PyCanTransportInterface(can_bus_manager=self.can_interface_1, + addressing_information=example_addressing_information) + other_node_segmenter = CanSegmenter(addressing_information=example_addressing_information_2nd_node) + flow_control_packet = other_node_segmenter.get_flow_control_packet(flow_status=CanFlowStatus.ContinueToSend, + block_size=0, + st_min=st_min) + flow_control_frame = Message(arbitration_id=flow_control_packet.can_id, data=flow_control_packet.raw_frame_data) + interrupting_packet = other_node_segmenter.segmentation(new_message)[0] + interrupting_frame = Message(arbitration_id=interrupting_packet.can_id, data=interrupting_packet.raw_frame_data) + Timer(interval=fc_after / 1000., function=self.can_interface_2.send, args=(flow_control_frame,)).start() + Timer(interval=new_message_after / 1000., function=self.can_interface_2.send, + args=(interrupting_frame,)).start() + with pytest.raises(TransmissionInterruptionError): + can_transport_interface.send_message(message) + sleep(self.DELAY_AFTER_RECEIVING_FRAME / 100.) + + @pytest.mark.parametrize("message", [ + UdsMessage(payload=[0x62, 0x12, 0x34, *range(100, 200)], addressing_type=AddressingType.PHYSICAL), + UdsMessage(payload=[0x22, *range(62)], addressing_type=AddressingType.PHYSICAL), + ]) + @pytest.mark.parametrize("new_message", [ + UdsMessage([0x10, 0x81], AddressingType.FUNCTIONAL), + UdsMessage(payload=[0x62, 0x12, 0x34, *range(100, 200)], addressing_type=AddressingType.PHYSICAL), + ]) + @pytest.mark.parametrize("fc_after, new_message_after, st_min", [ + (50, 1, 100), + (8, 10, 10), + (5, 100, 51), + ]) + @pytest.mark.asyncio + async def test_new_message_started_when_multi_packet_async_message_sending(self, example_addressing_information, + example_addressing_information_2nd_node, + message, new_message, fc_after, + new_message_after, st_min): + """ + Check for a asynchronous multi packet (FF + CF) UDS message sending being interrupted by a new message. + + Procedure: + 1. Schedule Flow Control CAN packet with information to continue sending all consecutive frame packets at once. + 2. Schedule Single Frame/First Frame CAN packet starting a transmission of a new message. + 3. Send (using async method) a UDS message using Transport Interface (via CAN Interface). + Expected: UDS message transmission stopped and an exception raised. + + :param example_addressing_information: Example Addressing Information of a CAN Node. + :param example_addressing_information_2nd_node: Example Addressing Information of a CAN Node. + :param message: UDS message to send. + :param new_message: UDS message to interrupt. + :param fc_after: Delay to use for sending CAN flow control. + :param new_message_after: Delay to use for sending interrupting message after Flow Control. + :param st_min: STmin value to transmit in Flow Control. + """ + can_transport_interface = PyCanTransportInterface(can_bus_manager=self.can_interface_1, + addressing_information=example_addressing_information) + other_node_segmenter = CanSegmenter(addressing_information=example_addressing_information_2nd_node) + flow_control_packet = other_node_segmenter.get_flow_control_packet(flow_status=CanFlowStatus.ContinueToSend, + block_size=0, + st_min=st_min) + flow_control_frame = Message(arbitration_id=flow_control_packet.can_id, data=flow_control_packet.raw_frame_data) + interrupting_packet = other_node_segmenter.segmentation(new_message)[0] + interrupting_frame = Message(arbitration_id=interrupting_packet.can_id, data=interrupting_packet.raw_frame_data) + + async def _send_fc_and_message(): + if fc_after > new_message_after: + await asyncio.sleep(new_message_after / 1000.) + self.can_interface_2.send(interrupting_frame) + await asyncio.sleep((fc_after - new_message_after) / 1000.) + self.can_interface_2.send(flow_control_frame) + else: + await asyncio.sleep(fc_after / 1000.) + self.can_interface_2.send(flow_control_frame) + await asyncio.sleep((new_message_after - fc_after) / 1000.) + self.can_interface_2.send(interrupting_frame) + + send_fc_and_message_task = asyncio.create_task(_send_fc_and_message()) + with pytest.raises(TransmissionInterruptionError): + await can_transport_interface.async_send_message(message) + + await send_fc_and_message_task + await asyncio.sleep(self.DELAY_AFTER_RECEIVING_FRAME / 100.) diff --git a/uds/segmentation/can_segmenter.py b/uds/segmentation/can_segmenter.py index 65be7de8..208fbca8 100644 --- a/uds/segmentation/can_segmenter.py +++ b/uds/segmentation/can_segmenter.py @@ -12,6 +12,7 @@ CanConsecutiveFrameHandler, CanDlcHandler, CanFirstFrameHandler, + CanFlowStatus, CanSingleFrameHandler, PacketAIParamsAlias, ) @@ -161,6 +162,78 @@ def filler_byte(self, value: int): validate_raw_byte(value) self.__filler_byte: int = value + def __physical_segmentation(self, message: UdsMessage) -> Tuple[CanPacket, ...]: + """ + Segment physically addressed diagnostic message. + + :param message: UDS message to divide into UDS packets. + + :raise SegmentationError: Provided diagnostic message cannot be segmented. + + :return: CAN packets that are an outcome of UDS message segmentation. + """ + message_payload_size = len(message.payload) + if message_payload_size > CanFirstFrameHandler.MAX_LONG_FF_DL_VALUE: + raise SegmentationError("Provided diagnostic message cannot be segmented to CAN Packet as it is too big " + "to transmit it over CAN bus.") + if message_payload_size <= CanSingleFrameHandler.get_max_payload_size(addressing_format=self.addressing_format, + dlc=self.dlc): + single_frame = CanPacket(packet_type=CanPacketType.SINGLE_FRAME, + payload=message.payload, + filler_byte=self.filler_byte, + dlc=None if self.use_data_optimization else self.dlc, + **self.tx_packets_physical_ai) + return (single_frame,) + ff_payload_size = CanFirstFrameHandler.get_payload_size( + addressing_format=self.addressing_format, + dlc=self.dlc, + long_ff_dl_format=message_payload_size > CanFirstFrameHandler.MAX_SHORT_FF_DL_VALUE) + first_frame = CanPacket(packet_type=CanPacketType.FIRST_FRAME, + payload=message.payload[:ff_payload_size], + dlc=self.dlc, + data_length=message_payload_size, + **self.tx_packets_physical_ai) + cf_payload_size = CanConsecutiveFrameHandler.get_max_payload_size(addressing_format=self.addressing_format, + dlc=self.dlc) + total_cfs_number = (message_payload_size - ff_payload_size + cf_payload_size - 1) // cf_payload_size + consecutive_frames = [] + for cf_index in range(total_cfs_number): + sequence_number = (cf_index + 1) % 0x10 + payload_i_start = ff_payload_size + cf_index * cf_payload_size + payload_i_stop = payload_i_start + cf_payload_size + consecutive_frame = CanPacket(packet_type=CanPacketType.CONSECUTIVE_FRAME, + payload=message.payload[payload_i_start: payload_i_stop], + dlc=None if self.use_data_optimization and cf_index == total_cfs_number - 1 + else self.dlc, + sequence_number=sequence_number, + filler_byte=self.filler_byte, + **self.tx_packets_physical_ai) + consecutive_frames.append(consecutive_frame) + return (first_frame, *consecutive_frames) + + def __functional_segmentation(self, message: UdsMessage) -> Tuple[CanPacket, ...]: + """ + Segment functionally addressed diagnostic message. + + :param message: UDS message to divide into UDS packets. + + :raise SegmentationError: Provided diagnostic message cannot be segmented. + + :return: CAN packets that are an outcome of UDS message segmentation. + """ + max_payload_size = CanSingleFrameHandler.get_max_payload_size(addressing_format=self.addressing_format, + dlc=self.dlc) + message_payload_size = len(message.payload) + if message_payload_size > max_payload_size: + raise SegmentationError("Provided diagnostic message cannot be segmented using functional addressing " + "as it will not fit into a Single Frame.") + single_frame = CanPacket(packet_type=CanPacketType.SINGLE_FRAME, + payload=message.payload, + filler_byte=self.filler_byte, + dlc=None if self.use_data_optimization else self.dlc, + **self.tx_packets_functional_ai) + return (single_frame,) + # TODO: consider moving this method to AbstractAddressingInformation (if defined) def is_input_packet(self, can_id: int, data: RawBytesAlias) -> Optional[AddressingType]: # type: ignore # noqa """ @@ -225,6 +298,29 @@ def is_desegmented_message(self, packets: PacketsContainersSequence) -> bool: return payload_bytes_found >= total_payload_size # type: ignore raise NotImplementedError(f"Unknown packet type received: {packets[0].packet_type}") + def get_flow_control_packet(self, + flow_status: CanFlowStatus, + block_size: Optional[int] = None, + st_min: Optional[int] = None) -> CanPacket: + """ + Create Flow Control CAN packet. + + :param flow_status: Value of Flow Status parameter. + :param block_size: Value of Block Size parameter. + This parameter is only required with ContinueToSend Flow Status, leave None otherwise. + :param st_min: Value of Separation Time minimum (STmin) parameter. + This parameter is only required with ContinueToSend Flow Status, leave None otherwise. + + :return: Flow Control CAN packet with provided parameters. + """ + return CanPacket(packet_type=CanPacketType.FLOW_CONTROL, + flow_status=flow_status, + block_size=block_size, + st_min=st_min, + filler_byte=self.filler_byte, + dlc=None if self.use_data_optimization else self.dlc, + **self.tx_packets_physical_ai) + def desegmentation(self, packets: PacketsContainersSequence) -> Union[UdsMessage, UdsMessageRecord]: """ Perform desegmentation of CAN packets. @@ -276,75 +372,3 @@ def segmentation(self, message: UdsMessage) -> Tuple[CanPacket, ...]: if message.addressing_type == AddressingType.FUNCTIONAL: return self.__functional_segmentation(message) raise NotImplementedError(f"Unknown addressing type received: {message.addressing_type}") - - def __physical_segmentation(self, message: UdsMessage) -> Tuple[CanPacket, ...]: - """ - Segment physically addressed diagnostic message. - - :param message: UDS message to divide into UDS packets. - - :raise SegmentationError: Provided diagnostic message cannot be segmented. - - :return: CAN packets that are an outcome of UDS message segmentation. - """ - message_payload_size = len(message.payload) - if message_payload_size > CanFirstFrameHandler.MAX_LONG_FF_DL_VALUE: - raise SegmentationError("Provided diagnostic message cannot be segmented to CAN Packet as it is too big " - "to transmit it over CAN bus.") - if message_payload_size <= CanSingleFrameHandler.get_max_payload_size(addressing_format=self.addressing_format, - dlc=self.dlc): - single_frame = CanPacket(packet_type=CanPacketType.SINGLE_FRAME, - payload=message.payload, - filler_byte=self.filler_byte, - dlc=None if self.use_data_optimization else self.dlc, - **self.tx_packets_physical_ai) - return (single_frame,) - ff_payload_size = CanFirstFrameHandler.get_payload_size( - addressing_format=self.addressing_format, - dlc=self.dlc, - long_ff_dl_format=message_payload_size > CanFirstFrameHandler.MAX_SHORT_FF_DL_VALUE) - first_frame = CanPacket(packet_type=CanPacketType.FIRST_FRAME, - payload=message.payload[:ff_payload_size], - dlc=self.dlc, - data_length=message_payload_size, - **self.tx_packets_physical_ai) - cf_payload_size = CanConsecutiveFrameHandler.get_max_payload_size(addressing_format=self.addressing_format, - dlc=self.dlc) - total_cfs_number = (message_payload_size - ff_payload_size + cf_payload_size - 1) // cf_payload_size - consecutive_frames = [] - for cf_index in range(total_cfs_number): - sequence_number = (cf_index + 1) % 0x10 - payload_i_start = ff_payload_size + cf_index * cf_payload_size - payload_i_stop = payload_i_start + cf_payload_size - consecutive_frame = CanPacket(packet_type=CanPacketType.CONSECUTIVE_FRAME, - payload=message.payload[payload_i_start: payload_i_stop], - dlc=None if self.use_data_optimization and cf_index == total_cfs_number - 1 - else self.dlc, - sequence_number=sequence_number, - filler_byte=self.filler_byte, - **self.tx_packets_physical_ai) - consecutive_frames.append(consecutive_frame) - return (first_frame, *consecutive_frames) - - def __functional_segmentation(self, message: UdsMessage) -> Tuple[CanPacket, ...]: - """ - Segment functionally addressed diagnostic message. - - :param message: UDS message to divide into UDS packets. - - :raise SegmentationError: Provided diagnostic message cannot be segmented. - - :return: CAN packets that are an outcome of UDS message segmentation. - """ - max_payload_size = CanSingleFrameHandler.get_max_payload_size(addressing_format=self.addressing_format, - dlc=self.dlc) - message_payload_size = len(message.payload) - if message_payload_size > max_payload_size: - raise SegmentationError("Provided diagnostic message cannot be segmented using functional addressing " - "as it will not fit into a Single Frame.") - single_frame = CanPacket(packet_type=CanPacketType.SINGLE_FRAME, - payload=message.payload, - filler_byte=self.filler_byte, - dlc=None if self.use_data_optimization else self.dlc, - **self.tx_packets_functional_ai) - return (single_frame,) diff --git a/uds/transport_interface/can_transport_interface.py b/uds/transport_interface/can_transport_interface.py index dfe513da..df09517a 100644 --- a/uds/transport_interface/can_transport_interface.py +++ b/uds/transport_interface/can_transport_interface.py @@ -6,7 +6,7 @@ from asyncio import AbstractEventLoop, get_running_loop, wait_for from datetime import datetime from time import time -from typing import Any, Optional +from typing import Any, List, Optional, Tuple from warnings import warn from can import AsyncBufferedReader, BufferedReader, BusABC, Message, Notifier @@ -14,14 +14,21 @@ AbstractCanAddressingInformation, AbstractFlowControlParametersGenerator, CanDlcHandler, + CanFlowStatus, CanIdHandler, + CanSTminTranslator, DefaultFlowControlParametersGenerator, ) from uds.message import UdsMessage, UdsMessageRecord from uds.packet import CanPacket, CanPacketRecord, CanPacketType from uds.segmentation import CanSegmenter from uds.transmission_attributes import TransmissionDirection -from uds.utilities import TimeMillisecondsAlias, ValueWarning +from uds.utilities import ( + TimeMillisecondsAlias, + TransmissionInterruptionError, + TransmissionInterruptionWarning, + ValueWarning, +) from .abstract_transport_interface import AbstractTransportInterface @@ -79,6 +86,8 @@ def __init__(self, raise TypeError("Unsupported type of Addressing Information was provided.") self.__addressing_information: AbstractCanAddressingInformation = addressing_information super().__init__(bus_manager=can_bus_manager) + self.__n_bs_measured: Optional[Tuple[TimeMillisecondsAlias, ...]] = None + self.__n_cr_measured: Optional[Tuple[TimeMillisecondsAlias, ...]] = None self.n_as_timeout = kwargs.pop("n_as_timeout", self.N_AS_TIMEOUT) self.n_ar_timeout = kwargs.pop("n_ar_timeout", self.N_AR_TIMEOUT) self.n_bs_timeout = kwargs.pop("n_bs_timeout", self.N_BS_TIMEOUT) @@ -89,6 +98,40 @@ def __init__(self, self.DEFAULT_FLOW_CONTROL_PARAMETERS) self.__segmenter = CanSegmenter(addressing_information=addressing_information, **kwargs) + # Common + + def _update_n_bs_measured(self, message: UdsMessageRecord) -> None: + """ + Update measured values of N_Bs according to timestamps of CAN packet records. + + :param message: Record of UDS message transmitted over CAN. + """ + if len(message.packets_records) == 1: + self.__n_bs_measured = None + else: + n_bs_measured = [] + for i, packet_record in enumerate(message.packets_records[1:]): + if packet_record.packet_type == CanPacketType.FLOW_CONTROL: + n_bs = packet_record.transmission_time - message.packets_records[i].transmission_time + n_bs_measured.append(round(n_bs.total_seconds() * 1000, 3)) + self.__n_bs_measured = tuple(n_bs_measured) + + def _update_n_cr_measured(self, message: UdsMessageRecord) -> None: + """ + Update measured values of N_Cr according to timestamps of CAN packet records. + + :param message: Record of UDS message transmitted over CAN. + """ + if len(message.packets_records) == 1: + self.__n_cr_measured = None + else: + n_cr_measured = [] + for i, packet_record in enumerate(message.packets_records[1:]): + if packet_record.packet_type == CanPacketType.CONSECUTIVE_FRAME: + n_cr = packet_record.transmission_time - message.packets_records[i].transmission_time + n_cr_measured.append(round(n_cr.total_seconds() * 1000, 3)) + self.__n_cr_measured = tuple(n_cr_measured) + @property def segmenter(self) -> CanSegmenter: """Value of the segmenter used by this CAN Transport Interface.""" @@ -98,13 +141,13 @@ def segmenter(self) -> CanSegmenter: @property def n_as_timeout(self) -> TimeMillisecondsAlias: - """Timeout value for N_As time parameter.""" + """Timeout value for :ref:`N_As ` time parameter.""" return self.__n_as_timeout @n_as_timeout.setter def n_as_timeout(self, value: TimeMillisecondsAlias): """ - Set timeout value for N_As time parameter. + Set timeout value for :ref:`N_As ` time parameter. :param value: Value of timeout to set. @@ -124,20 +167,20 @@ def n_as_timeout(self, value: TimeMillisecondsAlias): @abstractmethod def n_as_measured(self) -> Optional[TimeMillisecondsAlias]: """ - Get the last measured value of N_As time parameter. + Get the last measured value of :ref:`N_As ` time parameter. :return: Time in milliseconds or None if the value was never measured. """ @property def n_ar_timeout(self) -> TimeMillisecondsAlias: - """Timeout value for N_Ar time parameter.""" + """Timeout value for :ref:`N_Ar ` time parameter.""" return self.__n_ar_timeout @n_ar_timeout.setter def n_ar_timeout(self, value: TimeMillisecondsAlias): """ - Set timeout value for N_Ar time parameter. + Set timeout value for :ref:`N_Ar ` time parameter. :param value: Value of timeout to set. @@ -157,20 +200,20 @@ def n_ar_timeout(self, value: TimeMillisecondsAlias): @abstractmethod def n_ar_measured(self) -> Optional[TimeMillisecondsAlias]: """ - Get the last measured value of N_Ar time parameter. + Get the last measured value of :ref:`N_Ar ` time parameter. :return: Time in milliseconds or None if the value was never measured. """ @property def n_bs_timeout(self) -> TimeMillisecondsAlias: - """Timeout value for N_Bs time parameter.""" + """Timeout value for :ref:`N_Bs ` time parameter.""" return self.__n_bs_timeout @n_bs_timeout.setter def n_bs_timeout(self, value: TimeMillisecondsAlias): """ - Set timeout value for N_Bs time parameter. + Set timeout value for :ref:`N_Bs ` time parameter. :param value: Value of timeout to set. @@ -187,18 +230,23 @@ def n_bs_timeout(self, value: TimeMillisecondsAlias): self.__n_bs_timeout = value @property # noqa - @abstractmethod - def n_bs_measured(self) -> Optional[TimeMillisecondsAlias]: + def n_bs_measured(self) -> Optional[Tuple[TimeMillisecondsAlias, ...]]: """ - Get the last measured value of N_Bs time parameter. + Get the last measured values of :ref:`N_Bs ` time parameter. - :return: Time in milliseconds or None if the value was never measured. + .. note:: The last measurement comes from the last transmission of UDS message using either + :meth:`~uds.transport_interface.can_transport_interface.AbstractCanTransportInterface.send_message` + :meth:`~uds.transport_interface.can_transport_interface.AbstractCanTransportInterface.async_send_message` + method. + + :return: Tuple with times in milliseconds or None if the values could not be measured. """ + return self.__n_bs_measured @property def n_br(self) -> TimeMillisecondsAlias: """ - Get the value of N_Br time parameter which is currently set. + Get the value of :ref:`N_Br ` time parameter which is currently set. .. note:: The actual (observed on the bus) value will be slightly longer as it also includes computation and CAN Interface delays. @@ -208,7 +256,7 @@ def n_br(self) -> TimeMillisecondsAlias: @n_br.setter def n_br(self, value: TimeMillisecondsAlias): """ - Set the value of N_Br time parameter to use. + Set the value of :ref:`N_Br ` time parameter to use. :param value: The value to set. @@ -224,12 +272,12 @@ def n_br(self, value: TimeMillisecondsAlias): @property def n_br_max(self) -> TimeMillisecondsAlias: """ - Get the maximum valid value of N_Br time parameter. + Get the maximum valid value of :ref:`N_Br ` time parameter. .. warning:: To assess maximal value of :ref:`N_Br `, the actual value of :ref:`N_Ar ` time parameter is required. - Either the latest measured value of N_Ar would be used, or 0ms would be assumed (if there are - no measurement result). + Either the latest measured value of :ref:`N_Ar ` would be used, + or 0ms would be assumed (if there are no measurement result). """ n_ar_measured = 0 if self.n_ar_measured is None else self.n_ar_measured return 0.9 * self.n_bs_timeout - n_ar_measured @@ -237,7 +285,7 @@ def n_br_max(self) -> TimeMillisecondsAlias: @property def n_cs(self) -> Optional[TimeMillisecondsAlias]: """ - Get the value of N_Cs time parameter which is currently set. + Get the value of :ref:`N_Cs ` time parameter which is currently set. .. note:: The actual (observed on the bus) value will be slightly longer as it also includes computation and CAN Interface delays. @@ -247,7 +295,7 @@ def n_cs(self) -> Optional[TimeMillisecondsAlias]: @n_cs.setter def n_cs(self, value: Optional[TimeMillisecondsAlias]): """ - Set the value of N_Cs time parameter to use. + Set the value of :ref:`N_Cs ` time parameter to use. :param value: The value to set. - None - use timing compatible with STmin value received in a preceding Flow Control packet @@ -266,25 +314,25 @@ def n_cs(self, value: Optional[TimeMillisecondsAlias]): @property def n_cs_max(self) -> TimeMillisecondsAlias: """ - Get the maximum valid value of N_Cs time parameter. + Get the maximum valid value of :ref:`N_Cs ` time parameter. .. warning:: To assess maximal value of :ref:`N_Cs `, the actual value of :ref:`N_As ` time parameter is required. - Either the latest measured value of N_Ar would be used, or 0ms would be assumed (if there are - no measurement result). + Either the latest measured value of :ref:`N_As ` would be used, + or 0ms would be assumed (if there are no measurement result). """ n_as_measured = 0 if self.n_as_measured is None else self.n_as_measured return 0.9 * self.n_cr_timeout - n_as_measured @property def n_cr_timeout(self) -> TimeMillisecondsAlias: - """Timeout value for N_Cr time parameter.""" + """Timeout value for :ref:`N_Cr ` time parameter.""" return self.__n_cr_timeout @n_cr_timeout.setter def n_cr_timeout(self, value: TimeMillisecondsAlias): """ - Set timeout value for N_Cr time parameter. + Set timeout value for :ref:`N_Cr ` time parameter. :param value: Value of timeout to set. @@ -301,13 +349,18 @@ def n_cr_timeout(self, value: TimeMillisecondsAlias): self.__n_cr_timeout = value @property # noqa - @abstractmethod - def n_cr_measured(self) -> Optional[TimeMillisecondsAlias]: + def n_cr_measured(self) -> Optional[Tuple[TimeMillisecondsAlias, ...]]: """ - Get the last measured value of N_Cr time parameter. + Get the last measured values of :ref:`N_Cr ` time parameter. - :return: Time in milliseconds or None if the value was never measured. + .. note:: The last measurement comes from the last reception of UDS message using either + :meth:`~uds.transport_interface.can_transport_interface.AbstractCanTransportInterface.receive_message` + :meth:`~uds.transport_interface.can_transport_interface.AbstractCanTransportInterface.async_receive_message` + method. + + :return: Tuple with times in milliseconds or None if the values could not be measured. """ + return self.__n_cr_measured # Communication parameters @@ -423,15 +476,13 @@ def __init__(self, """ self.__n_as_measured: Optional[TimeMillisecondsAlias] = None self.__n_ar_measured: Optional[TimeMillisecondsAlias] = None - self.__n_bs_measured: Optional[TimeMillisecondsAlias] = None - self.__n_cr_measured: Optional[TimeMillisecondsAlias] = None super().__init__(can_bus_manager=can_bus_manager, addressing_information=addressing_information, **kwargs) - self.__notifier: Optional[Notifier] = None self.__frames_buffer = BufferedReader() - self.__async_notifier: Optional[Notifier] = None + self.__notifier: Optional[Notifier] = None self.__async_frames_buffer = AsyncBufferedReader() + self.__async_notifier: Optional[Notifier] = None def __del__(self): """Safely close all threads open by this object.""" @@ -441,7 +492,7 @@ def __del__(self): @property def n_as_measured(self) -> Optional[TimeMillisecondsAlias]: """ - Get the last measured value of N_As time parameter. + Get the last measured value of :ref:`N_As ` time parameter. :return: Time in milliseconds or None if the value was never measured. """ @@ -450,30 +501,12 @@ def n_as_measured(self) -> Optional[TimeMillisecondsAlias]: @property def n_ar_measured(self) -> Optional[TimeMillisecondsAlias]: """ - Get the last measured value of N_Ar time parameter. + Get the last measured value of :ref:`N_Ar ` time parameter. :return: Time in milliseconds or None if the value was never measured. """ return self.__n_ar_measured - @property # noqa - def n_bs_measured(self) -> Optional[TimeMillisecondsAlias]: - """ - Get the last measured value of N_Bs time parameter. - - :return: Time in milliseconds or None if the value was never measured. - """ - return self.__n_bs_measured - - @property # noqa - def n_cr_measured(self) -> Optional[TimeMillisecondsAlias]: - """ - Get the last measured value of N_Cr time parameter. - - :return: Time in milliseconds or None if the value was never measured. - """ - return self.__n_cr_measured - def _teardown_notifier(self, suppress_warning: bool = False) -> None: """ Stop and remove CAN frame notifier for synchronous communication. @@ -527,6 +560,68 @@ def _setup_async_notifier(self, loop: AbstractEventLoop) -> None: timeout=self._MIN_NOTIFIER_TIMEOUT, loop=loop) + def _send_cf_packets_block(self, + cf_packets_block: List[CanPacket], + delay: TimeMillisecondsAlias) -> Tuple[CanPacketRecord, ...]: + """ + Send block of Consecutive Frame CAN packets. + + :param cf_packets_block: Consecutive Frame CAN packets to send. + :param delay: Minimal delay between sending following Consecutive Frames [ms]. + + :raise TransmissionInterruptionError: A new UDS message transmission was started while sending this message. + + :return: Records with historic information about transmitted Consecutive Frame CAN packets. + """ + packet_records = [] + for cf_packet in cf_packets_block: + time_end = time() + (delay / 1000.) + while time() < time_end: + try: + received_packet = self.receive_packet(timeout=time_end - time()) + except TimeoutError: + pass + else: + if CanPacketType.is_initial_packet_type(received_packet.packet_type): + raise TransmissionInterruptionError("A new UDS message transmission was started while sending " + "this message.") + warn(message="An unrelated CAN packet was received during UDS message transmission.", + category=TransmissionInterruptionWarning) + packet_records.append(self.send_packet(cf_packet)) + return tuple(packet_records) + + async def _async_send_cf_packets_block(self, + cf_packets_block: List[CanPacket], + delay: TimeMillisecondsAlias, + loop: Optional[AbstractEventLoop] = None) -> Tuple[CanPacketRecord, ...]: + """ + Send block of Consecutive Frame CAN packets asynchronously. + + :param cf_packets_block: Consecutive Frame CAN packets to send. + :param delay: Minimal delay between sending following Consecutive Frames [ms]. + :param loop: An asyncio event loop to use for scheduling this task. + + :raise TransmissionInterruptionError: A new UDS message transmission was started while sending this message. + + :return: Records with historic information about transmitted Consecutive Frame CAN packets. + """ + packet_records = [] + for cf_packet in cf_packets_block: + time_end = time() + (delay / 1000.) + while time() < time_end: + try: + received_packet = await self.async_receive_packet(timeout=time_end - time()) + except TimeoutError: + pass + else: + if CanPacketType.is_initial_packet_type(received_packet.packet_type): + raise TransmissionInterruptionError("A new UDS message transmission was started while sending " + "this message.") + warn(message="An unrelated CAN packet was received during UDS message transmission.", + category=TransmissionInterruptionWarning) + packet_records.append(await self.async_send_packet(cf_packet, loop=loop)) + return tuple(packet_records) + def clear_frames_buffers(self) -> None: """ Clear buffers with transmitted and received frames. @@ -559,7 +654,6 @@ def send_packet(self, packet: CanPacket) -> CanPacketRecord: # type: ignore :return: Record with historic information about transmitted CAN packet. """ - time_start = time() if not isinstance(packet, CanPacket): raise TypeError("Provided packet value does not contain a CAN Packet.") is_flow_control_packet = packet.packet_type == CanPacketType.FLOW_CONTROL @@ -570,6 +664,7 @@ def send_packet(self, packet: CanPacket) -> CanPacketRecord: # type: ignore is_fd=CanDlcHandler.is_can_fd_specific_dlc(packet.dlc)) self._setup_notifier() self.clear_frames_buffers() + time_start = time() self.bus_manager.send(can_frame) observed_frame = None while observed_frame is None \ @@ -581,53 +676,19 @@ def send_packet(self, packet: CanPacket) -> CanPacketRecord: # type: ignore raise TimeoutError("Timeout was reached before observing a CAN Packet being transmitted.") observed_frame = self.__frames_buffer.get_message(timeout=timeout_left) if is_flow_control_packet: - self.__n_ar_measured = observed_frame.timestamp - time_start + # Temporary solution due to https://github.com/mdabrowski1990/uds/issues/228 + # self.__n_ar_measured = observed_frame.timestamp - time_start + self.__n_ar_measured = time() - time_start else: - self.__n_as_measured = observed_frame.timestamp - time_start + # Temporary solution due to https://github.com/mdabrowski1990/uds/issues/228 + # self.__n_as_measured = observed_frame.timestamp - time_start + self.__n_as_measured = time() - time_start return CanPacketRecord(frame=observed_frame, direction=TransmissionDirection.TRANSMITTED, addressing_type=packet.addressing_type, addressing_format=packet.addressing_format, transmission_time=datetime.fromtimestamp(observed_frame.timestamp)) - def receive_packet(self, timeout: Optional[TimeMillisecondsAlias] = None) -> CanPacketRecord: - """ - Receive CAN packet. - - .. warning:: Must not be called within an asynchronous function. - - :param timeout: Maximal time (in milliseconds) to wait. - - :raise TypeError: Provided timeout value is not None neither int nor float type. - :raise ValueError: Provided timeout value is less or equal 0. - :raise TimeoutError: Timeout was reached. - - :return: Record with historic information about received CAN packet. - """ - time_start = time() - if timeout is not None: - if not isinstance(timeout, (int, float)): - raise TypeError("Provided timeout value is not None neither int nor float type.") - if timeout <= 0: - raise ValueError("Provided timeout value is less or equal 0.") - self._setup_notifier() - packet_addressing_type = None - while packet_addressing_type is None: - time_now = time() - timeout_left = self._MAX_LISTENER_TIMEOUT if timeout is None else timeout / 1000. - (time_now - time_start) - if timeout_left <= 0: - raise TimeoutError("Timeout was reached before a CAN Packet was received.") - received_frame = self.__frames_buffer.get_message(timeout=timeout_left) - if received_frame is None: - raise TimeoutError("Timeout was reached before a CAN Packet was received.") - packet_addressing_type = self.segmenter.is_input_packet(can_id=received_frame.arbitration_id, - data=received_frame.data) - return CanPacketRecord(frame=received_frame, - direction=TransmissionDirection.RECEIVED, - addressing_type=packet_addressing_type, - addressing_format=self.segmenter.addressing_format, - transmission_time=datetime.fromtimestamp(received_frame.timestamp)) - async def async_send_packet(self, packet: CanPacket, # type: ignore loop: Optional[AbstractEventLoop] = None) -> CanPacketRecord: @@ -641,7 +702,6 @@ async def async_send_packet(self, :return: Record with historic information about transmitted CAN packet. """ - time_start = time() if not isinstance(packet, CanPacket): raise TypeError("Provided packet value does not contain a CAN Packet.") loop = get_running_loop() if loop is None else loop @@ -653,6 +713,7 @@ async def async_send_packet(self, is_extended_id=CanIdHandler.is_extended_can_id(packet.can_id), data=packet.raw_frame_data, is_fd=CanDlcHandler.is_can_fd_specific_dlc(packet.dlc)) + time_start = time() self.bus_manager.send(can_frame) observed_frame = None while observed_frame is None \ @@ -662,15 +723,57 @@ async def async_send_packet(self, timeout_left = timeout / 1000. - (time() - time_start) observed_frame = await wait_for(self.__async_frames_buffer.get_message(), timeout=timeout_left) if is_flow_control_packet: - self.__n_ar_measured = observed_frame.timestamp - time_start + # Temporary solution due to https://github.com/mdabrowski1990/uds/issues/228 + # self.__n_ar_measured = observed_frame.timestamp - time_start + self.__n_ar_measured = time() - time_start else: - self.__n_as_measured = observed_frame.timestamp - time_start + # Temporary solution due to https://github.com/mdabrowski1990/uds/issues/228 + # self.__n_as_measured = observed_frame.timestamp - time_start + self.__n_as_measured = time() - time_start return CanPacketRecord(frame=observed_frame, direction=TransmissionDirection.TRANSMITTED, addressing_type=packet.addressing_type, addressing_format=packet.addressing_format, transmission_time=datetime.fromtimestamp(observed_frame.timestamp)) + def receive_packet(self, timeout: Optional[TimeMillisecondsAlias] = None) -> CanPacketRecord: + """ + Receive CAN packet. + + .. warning:: Must not be called within an asynchronous function. + + :param timeout: Maximal time (in milliseconds) to wait. + + :raise TypeError: Provided timeout value is not None neither int nor float type. + :raise ValueError: Provided timeout value is less or equal 0. + :raise TimeoutError: Timeout was reached. + + :return: Record with historic information about received CAN packet. + """ + time_start = time() + if timeout is not None: + if not isinstance(timeout, (int, float)): + raise TypeError("Provided timeout value is not None neither int nor float type.") + if timeout <= 0: + raise ValueError("Provided timeout value is less or equal 0.") + self._setup_notifier() + packet_addressing_type = None + while packet_addressing_type is None: + time_now = time() + timeout_left = self._MAX_LISTENER_TIMEOUT if timeout is None else timeout / 1000. - (time_now - time_start) + if timeout_left <= 0: + raise TimeoutError("Timeout was reached before a CAN Packet was received.") + received_frame = self.__frames_buffer.get_message(timeout=timeout_left) + if received_frame is None: + raise TimeoutError("Timeout was reached before a CAN Packet was received.") + packet_addressing_type = self.segmenter.is_input_packet(can_id=received_frame.arbitration_id, + data=received_frame.data) + return CanPacketRecord(frame=received_frame, + direction=TransmissionDirection.RECEIVED, + addressing_type=packet_addressing_type, + addressing_format=self.segmenter.addressing_format, + transmission_time=datetime.fromtimestamp(received_frame.timestamp)) + async def async_receive_packet(self, timeout: Optional[TimeMillisecondsAlias] = None, loop: Optional[AbstractEventLoop] = None) -> CanPacketRecord: @@ -716,20 +819,98 @@ def send_message(self, message: UdsMessage) -> UdsMessageRecord: """ Transmit UDS message over CAN. + .. warning:: Must not be called within an asynchronous function. + + :param message: A message to send. + + :raise OverflowError: Flow Control packet with Flow Status equal to OVERFLOW was received. + :raise TransmissionInterruptionError: A new UDS message transmission was started while sending this message. + :raise NotImplementedError: Flow Control CAN packet with unknown Flow Status was received. + + :return: Record with historic information about transmitted UDS message. + """ + packets_to_send = list(self.segmenter.segmentation(message)) + packet_records = [self.send_packet(packets_to_send.pop(0))] + while packets_to_send: + record = self.receive_packet(timeout=self.N_BS_TIMEOUT) + if record.packet_type == CanPacketType.FLOW_CONTROL: + packet_records.append(record) + if record.flow_status == CanFlowStatus.ContinueToSend: + number_of_packets = len(packets_to_send) if record.block_size == 0 else record.block_size + delay_between_cf = self.n_cs if self.n_cs is not None else \ + CanSTminTranslator.decode(record.st_min) # type: ignore + packet_records.extend(self._send_cf_packets_block( + cf_packets_block=packets_to_send[:number_of_packets], + delay=delay_between_cf)) + packets_to_send = packets_to_send[number_of_packets:] + elif record.flow_status == CanFlowStatus.Wait: + continue + elif record.flow_status == CanFlowStatus.Overflow: + raise OverflowError("Flow Control with Flow Status `OVERFLOW` was received.") + else: + raise NotImplementedError(f"Unknown Flow Status received: {record.flow_status}") + elif CanPacketType.is_initial_packet_type(record.packet_type): + raise TransmissionInterruptionError("A new UDS message transmission was started while sending " + "this message.") + else: + warn(message="An unrelated CAN packet was received during UDS message transmission.", + category=TransmissionInterruptionWarning) + message_records = UdsMessageRecord(packet_records) + self._update_n_bs_measured(message_records) + return message_records + + async def async_send_message(self, + message: UdsMessage, + loop: Optional[AbstractEventLoop] = None) -> UdsMessageRecord: + """ + Transmit asynchronously UDS message over CAN. + :param message: A message to send. + :param loop: An asyncio event loop to use for scheduling this task. + + :raise OverflowError: Flow Control packet with Flow Status equal to OVERFLOW was received. + :raise TransmissionInterruptionError: A new UDS message transmission was started while sending this message. + :raise NotImplementedError: Flow Control CAN packet with unknown Flow Status was received. :return: Record with historic information about transmitted UDS message. """ - packets_to_send = self.segmenter.segmentation(message) - if len(packets_to_send) == 1: - packet_record = self.send_packet(*packets_to_send) - return UdsMessageRecord((packet_record,)) - raise NotImplementedError("TODO: https://github.com/mdabrowski1990/uds/issues/267") + packets_to_send = list(self.segmenter.segmentation(message)) + packet_records = [await self.async_send_packet(packets_to_send.pop(0), loop=loop)] + while packets_to_send: + record = await self.async_receive_packet(timeout=self.N_BS_TIMEOUT, loop=loop) + if record.packet_type == CanPacketType.FLOW_CONTROL: + packet_records.append(record) + if record.flow_status == CanFlowStatus.ContinueToSend: + number_of_packets = len(packets_to_send) if record.block_size == 0 else record.block_size + delay_between_cf = self.n_cs if self.n_cs is not None else \ + CanSTminTranslator.decode(record.st_min) # type: ignore + packet_records.extend( + await self._async_send_cf_packets_block(cf_packets_block=packets_to_send[:number_of_packets], + delay=delay_between_cf, + loop=loop)) + packets_to_send = packets_to_send[number_of_packets:] + elif record.flow_status == CanFlowStatus.Wait: + continue + elif record.flow_status == CanFlowStatus.Overflow: + raise OverflowError("Flow Control with Flow Status `OVERFLOW` was received.") + else: + raise NotImplementedError(f"Unknown Flow Status received: {record.flow_status}") + elif CanPacketType.is_initial_packet_type(record.packet_type): + raise TransmissionInterruptionError("A new UDS message transmission was started while sending " + "this message.") + else: + warn(message="An unrelated CAN packet was received during UDS message transmission.", + category=TransmissionInterruptionWarning) + message_records = UdsMessageRecord(packet_records) + self._update_n_bs_measured(message_records) + return message_records def receive_message(self, timeout: Optional[TimeMillisecondsAlias] = None) -> UdsMessageRecord: """ Receive UDS message over CAN. + .. warning:: Must not be called within an asynchronous function. + :param timeout: Maximal time (in milliseconds) to wait for UDS message transmission to start. This means that receiving might last longer if First Frame was received within provided time. @@ -751,23 +932,6 @@ def receive_message(self, timeout: Optional[TimeMillisecondsAlias] = None) -> Ud return UdsMessageRecord([received_packet]) raise NotImplementedError("TODO: https://github.com/mdabrowski1990/uds/issues/266") - async def async_send_message(self, - message: UdsMessage, - loop: Optional[AbstractEventLoop] = None) -> UdsMessageRecord: - """ - Transmit asynchronously UDS message over CAN. - - :param message: A message to send. - :param loop: An asyncio event loop to use for scheduling this task. - - :return: Record with historic information about transmitted UDS message. - """ - packets_to_send = self.segmenter.segmentation(message) - if len(packets_to_send) == 1: - packet_record = await self.async_send_packet(*packets_to_send, loop=loop) - return UdsMessageRecord((packet_record,)) # type - raise NotImplementedError("TODO: https://github.com/mdabrowski1990/uds/issues/267") - async def async_receive_message(self, timeout: Optional[TimeMillisecondsAlias] = None, loop: Optional[AbstractEventLoop] = None) -> UdsMessageRecord: diff --git a/uds/utilities/__init__.py b/uds/utilities/__init__.py index 519ac3a5..4ef562a6 100644 --- a/uds/utilities/__init__.py +++ b/uds/utilities/__init__.py @@ -11,6 +11,12 @@ validate_raw_byte, validate_raw_bytes, ) -from .custom_exceptions import AmbiguityError, InconsistentArgumentsError, ReassignmentError, UnusedArgumentError -from .custom_warnings import UnusedArgumentWarning, ValueWarning +from .custom_exceptions import ( + AmbiguityError, + InconsistentArgumentsError, + ReassignmentError, + TransmissionInterruptionError, + UnusedArgumentError, +) +from .custom_warnings import TransmissionInterruptionWarning, UnusedArgumentWarning, ValueWarning from .enums import ByteEnum, ExtendableEnum, NibbleEnum, ValidatedEnum diff --git a/uds/utilities/custom_exceptions.py b/uds/utilities/custom_exceptions.py index 62d53ee6..10c0f5a7 100644 --- a/uds/utilities/custom_exceptions.py +++ b/uds/utilities/custom_exceptions.py @@ -43,3 +43,11 @@ class UnusedArgumentError(ValueError): class AmbiguityError(ValueError): """Operation cannot be executed because it is ambiguous.""" + + +class TransmissionInterruptionError(RuntimeError): + """ + An unexpected packet was received during UDS message transmission. + + According to UDS ISO Standards the transmission shall be stopped. + """ diff --git a/uds/utilities/custom_warnings.py b/uds/utilities/custom_warnings.py index 8b41836e..dc479bd5 100644 --- a/uds/utilities/custom_warnings.py +++ b/uds/utilities/custom_warnings.py @@ -18,3 +18,11 @@ class UnusedArgumentWarning(Warning): class ValueWarning(Warning): """Value of the argument is out of typical range, but the package is able to handle it.""" + + +class TransmissionInterruptionWarning(RuntimeWarning): + """ + An unexpected packet was received during UDS message transmission. + + According to UDS ISO Standards a received packet shall be ignored. + """