Skip to content

Commit d6d2f3a

Browse files
committed
Handle client timeouts in configuration. Ensure to never exceed global timeout + minor tweaks in timeout logic.
1 parent c914bd7 commit d6d2f3a

File tree

6 files changed

+124
-51
lines changed

6 files changed

+124
-51
lines changed

doc/source/udsoncan/client.rst

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,36 @@ The client configuration must be a dictionary with the following keys defined:
182182

183183
The number of bytes used to encode a data identifier specifically for :ref:`ReadDTCInformation<ReadDTCInformation>` subfunction ``reportDTCSnapshotRecordByDTCNumber`` and ``reportDTCSnapshotRecordByRecordNumber``. The UDS standard does not specify a DID size although all other services expect a DID encoded over 2 bytes (16 bits). Default value of 2
184184

185+
.. _config_timeouts:
186+
.. _config_request_timeout:
187+
188+
.. attribute:: request_timeout
189+
:annotation: (int)
190+
191+
Maximum amount of time in seconds to wait for a response (positive or negative except NRC 0x78) after sending a request.
192+
After this time is elapsed, a TimeoutException will be raised regardless of other timeouts value or previous client response.
193+
Ensure an exit path if the ECU keeps requesting to wait.
194+
Default value of 5
195+
196+
.. _config_p2_timeout:
197+
198+
.. attribute:: p2_timeout
199+
:annotation: (int)
200+
201+
Maximum amount of time in seconds to wait for a first response (positive, negative, or NRC 0x78). After this time is elapsed, a TimeoutException will be raised if no response has been received.
202+
See ISO 14229-2:2013 (UDS Session Layer Services) for more details.
203+
Default value of 1
204+
205+
.. _config_p2_star_timeout:
206+
207+
.. attribute:: p2_star_timeout
208+
:annotation: (int)
209+
210+
Maximum amount of time in seconds to wait for a response (positive, negative, or NRC0x78) after the reception of a negative response with code 0x78
211+
(requestCorrectlyReceived-ResponsePending). After this time is elapsed, a TimeoutException will be raised if no response has been received.
212+
See ISO 14229-2:2013 (UDS Session Layer Services) for more details.
213+
Default value of 5
214+
185215
-------------
186216

187217
Suppress positive response

test/client/test_client.py

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ class TestClient(ClientServerTest):
99
def __init__(self, *args, **kwargs):
1010
ClientServerTest.__init__(self, *args, **kwargs)
1111

12-
def test_timeout(self):
12+
def test_timeout_override(self):
1313
pass
1414

15-
def _test_timeout(self):
15+
def _test_timeout_override(self):
1616
req = Request(service = services.TesterPresent, subfunction=0)
1717
timeout = 0.5
1818
try:
@@ -24,13 +24,14 @@ def _test_timeout(self):
2424
self.assertGreater(diff, timeout, 'Timeout raised after %.3f seconds when it should be %.3f sec' % (diff, timeout))
2525
self.assertLess(diff, timeout+0.5, 'Timeout raised after %.3f seconds when it should be %.3f sec' % (diff, timeout))
2626

27-
def test_param_timeout(self):
27+
# Server does not respond. Overall timeout is set smaller than P2 timeout. Overall timeout should trig
28+
def test_no_response_overall_timeout(self):
2829
pass
2930

30-
def _test_param_timeout(self):
31+
def _test_no_response_overall_timeout(self):
3132
req = Request(service = services.TesterPresent, subfunction=0)
3233
timeout = 0.5
33-
self.udsclient.request_timeout = timeout
34+
self.udsclient.set_configs({'request_timeout': timeout, 'p2_timeout' : 2, 'p2_star_timeout':2})
3435
try:
3536
t1 = time.time()
3637
response = self.udsclient.send_request(req)
@@ -40,8 +41,25 @@ def _test_param_timeout(self):
4041
self.assertGreater(diff, timeout, 'Timeout raised after %.3f seconds when it should be %.3f sec' % (diff, timeout))
4142
self.assertLess(diff, timeout+0.5, 'Timeout raised after %.3f seconds when it should be %.3f sec' % (diff, timeout))
4243

43-
def test_timeout_pending_response(self):
44+
# Server does not respond. P2 timeout is smaller than overall timeout. P2 timeout should trig
45+
def test_no_response_p2_timeout(self):
46+
pass
4447

48+
def _test_no_response_p2_timeout(self):
49+
req = Request(service = services.TesterPresent, subfunction=0)
50+
timeout = 0.5
51+
self.udsclient.set_configs({'request_timeout': 2, 'p2_timeout' : timeout, 'p2_star_timeout':2})
52+
try:
53+
t1 = time.time()
54+
response = self.udsclient.send_request(req)
55+
raise Exception('Request did not raise a TimeoutException')
56+
except TimeoutException as e:
57+
diff = time.time() - t1
58+
self.assertGreater(diff, timeout, 'Timeout raised after %.3f seconds when it should be %.3f sec' % (diff, timeout))
59+
self.assertLess(diff, timeout+0.5, 'Timeout raised after %.3f seconds when it should be %.3f sec' % (diff, timeout))
60+
61+
# overall timeout is set to 0.5. Server respond "pendingResponse" for 1 sec. Client should timeout first.
62+
def test_overall_timeout_pending_response(self):
4563
if not hasattr(self, 'completed'):
4664
self.completed = False
4765
self.conn.touserqueue.get(timeout=0.2)
@@ -51,17 +69,41 @@ def test_timeout_pending_response(self):
5169
self.conn.fromuserqueue.put(response.get_payload())
5270
time.sleep(0.1)
5371

54-
def _test_timeout_pending_response(self):
72+
def _test_overall_timeout_pending_response(self):
5573
req = Request(service = services.TesterPresent, subfunction=0)
5674
timeout = 0.5
75+
self.udsclient.set_configs({'request_timeout': timeout, 'p2_timeout' : 2, 'p2_star_timeout':2})
5776
try:
5877
t1 = time.time()
59-
response = self.udsclient.send_request(req, timeout=timeout)
78+
response = self.udsclient.send_request(req)
6079
raise Exception('Request did not raise a TimeoutException')
6180
except TimeoutException as e:
6281
self.assertIsNotNone(self.udsclient.last_response, 'Client never received the PendingResponse message')
82+
self.completed = True
83+
diff = time.time() - t1
84+
self.assertGreater(diff, timeout, 'Timeout raised after %.3f seconds when it should be %.3f sec' % (diff, timeout))
85+
self.assertLess(diff, timeout+0.5, 'Timeout raised after %.3f seconds when it should be %.3f sec' % (diff, timeout))
6386

87+
# Sends 2 "pending response" response to switch to P2* timeout.
88+
def test_p2_star_timeout(self):
89+
self.conn.touserqueue.get(timeout=0.2)
90+
response = Response(service=services.TesterPresent, code=Response.Code.RequestCorrectlyReceived_ResponsePending)
91+
self.conn.fromuserqueue.put(response.get_payload())
92+
time.sleep(0.1)
93+
self.conn.fromuserqueue.put(response.get_payload())
94+
95+
def _test_p2_star_timeout(self):
96+
req = Request(service = services.TesterPresent, subfunction=0)
97+
timeout = 2
98+
self.udsclient.set_configs({'request_timeout': 5, 'p2_timeout' : 0.5, 'p2_star_timeout':timeout})
99+
try:
100+
t1 = time.time()
101+
response = self.udsclient.send_request(req)
102+
raise Exception('Request did not raise a TimeoutException')
103+
except TimeoutException as e:
104+
self.assertIsNotNone(self.udsclient.last_response, 'Client never received the PendingResponse message')
64105
self.completed = True
65106
diff = time.time() - t1
66107
self.assertGreater(diff, timeout, 'Timeout raised after %.3f seconds when it should be %.3f sec' % (diff, timeout))
67108
self.assertLess(diff, timeout+0.5, 'Timeout raised after %.3f seconds when it should be %.3f sec' % (diff, timeout))
109+

test/client/test_request_download.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def test_request_download_config_format(self):
3535
self.conn.fromuserqueue.put(b"\x74\x20\xab\xcd") # Positive response
3636

3737
def _test_request_download_config_format(self):
38-
self.udsclient.config = {'server_address_format':32, 'server_memorysize_format':16}
38+
self.udsclient.set_configs({'server_address_format':32, 'server_memorysize_format':16})
3939
memloc = MemoryLocation(address=0x1234, memorysize=0xFF)
4040
response = self.udsclient.request_download(memory_location=memloc)
4141
self.assertEqual(response.service_data.max_length,0xabcd)

test/client/test_request_upload.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def test_request_upload_config_format(self):
3535
self.conn.fromuserqueue.put(b"\x75\x20\xab\xcd") # Positive response
3636

3737
def _test_request_upload_config_format(self):
38-
self.udsclient.config = {'server_address_format':32, 'server_memorysize_format':16}
38+
self.udsclient.set_configs({'server_address_format':32, 'server_memorysize_format':16})
3939
memloc = MemoryLocation(address=0x1234, memorysize=0xFF)
4040
response = self.udsclient.request_upload(memory_location=memloc)
4141
self.assertEqual(response.service_data.max_length,0xabcd)

udsoncan/client.py

Lines changed: 38 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
class Client:
1313
"""
14-
__init__(self, conn, config=default_client_config, request_timeout = 1)
14+
__init__(self, conn, config=default_client_config, request_timeout = None)
1515
1616
Object that interacts with a UDS server.
1717
It builds a service request, sends it to the server, receives and parses its response, detects communication anomalies and logs what it is doing for further debugging.
@@ -22,17 +22,8 @@ class Client:
2222
:param config: The :ref:`client configuration<client_config>`
2323
:type config: dict
2424
25-
:param request_timeout: Maximum amount of time in seconds to wait for the final response (positive or negative except NRC 0x78) after sending a request.
26-
After this time is elapsed, a TimeoutException will be raised. If less than or equal to p2_star, NRC 0x78 (requestCorrectlyReceived-ResponsePending) responses are not supported.
25+
:param request_timeout: Maximum amount of time to wait for a response. This parameter exists for backward compatibility only. For detailed timeout handling, see :ref:`Client configuration<config_timeouts>`
2726
:type request_timeout: int
28-
29-
:param p2_timeout: Maximum amount of time in seconds to wait for a response (positive, negative, or NRC 0x78). After this time is elapsed, a TimeoutException will be raised.
30-
See ISO 14229-2 (UDS Session Layer Services).
31-
:type p2_timeout: int
32-
33-
:param p2_star_timeout: Maximum amount of time in seconds to wait for a response (positive, negative, or NRC0x78) after the reception of a negative response with code 0x78
34-
(requestCorrectlyReceived-ResponsePending). After this time is elapsed, a TimeoutException will be raised. See ISO 14229-2 (UDS Session Layer Services).
35-
:type p2_star_timeout: int
3627
"""
3728

3829
class SuppressPositiveResponse:
@@ -46,12 +37,13 @@ def __enter__(self):
4637
def __exit__(self, type, value, traceback):
4738
self.enabled = False
4839

49-
def __init__(self, conn, config=default_client_config, request_timeout=5, p2_timeout=1, p2_star_timeout=5):
40+
def __init__(self, conn, config=default_client_config, request_timeout=None):
5041
self.conn = conn
51-
self.request_timeout = request_timeout
52-
self.p2_timeout = p2_timeout
53-
self.p2_star_timeout = p2_star_timeout
5442
self.config = dict(config) # Makes a copy of given configuration
43+
44+
#For backward compatibility
45+
if request_timeout is not None:
46+
self.config['request_timeout'] = request_timeout
5547
self.suppress_positive_response = Client.SuppressPositiveResponse()
5648
self.last_response = None
5749

@@ -82,6 +74,10 @@ def set_config(self, key, value):
8274
self.config[key] = value
8375
self.refresh_config()
8476

77+
def set_configs(self, dic):
78+
for k in dic:
79+
self.set_config(k, dic[k])
80+
8581
def refresh_config(self):
8682
self.configure_logger()
8783
for k in default_client_config:
@@ -1422,18 +1418,14 @@ def read_dtc_information(self, subfunction, status_mask=None, severity_mask=None
14221418
def send_request(self, request, timeout=-1):
14231419
if timeout < 0:
14241420
# Timeout not provided by user: defaults to Client request_timeout value
1425-
overall_timeout = self.request_timeout
1421+
overall_timeout = self.config['request_timeout']
1422+
single_request_timeout = min(overall_timeout, self.config['p2_timeout'])
14261423
else:
14271424
overall_timeout = timeout
1428-
1429-
if overall_timeout > self.p2_star_timeout:
1430-
current_timeout = self.p2_timeout
1431-
response_pending_supported = True
1432-
else:
1433-
# NRC 0x78 (RequestCorrectlyReceived_ResponsePending) not supported
1434-
current_timeout = overall_timeout
1435-
response_pending_supported = False
1436-
1425+
single_request_timeout = timeout
1426+
overall_timeout_time = time.time() + overall_timeout
1427+
using_p2_star = False # Will switch to true when Nrc 0x78 will be received the first time.
1428+
14371429
self.conn.empty_rxqueue()
14381430
self.logger.debug("Sending request to server")
14391431
override_suppress_positive_response = False
@@ -1453,22 +1445,29 @@ def send_request(self, request, timeout=-1):
14531445

14541446
done_receiving = False
14551447

1456-
if response_pending_supported:
1457-
overall_timeout_time = time.time() + overall_timeout
1458-
else:
1459-
overall_timeout_time = None
1460-
14611448
while not done_receiving:
14621449
done_receiving = True
14631450
self.logger.debug("Waiting for server response")
1451+
14641452
try:
1465-
payload = self.conn.wait_frame(timeout=current_timeout, exception=True)
1453+
if time.time() + single_request_timeout < overall_timeout_time:
1454+
timeout_type_used = 'single_request'
1455+
timeout_value = single_request_timeout
1456+
else:
1457+
timeout_type_used = 'overall'
1458+
timeout_value = max(overall_timeout_time - time.time(), 0)
1459+
1460+
payload = self.conn.wait_frame(timeout=timeout_value, exception=True)
14661461
except TimeoutException:
1467-
raise TimeoutException('Did not receive response in time (timeout=%.3f sec)' % current_timeout)
1462+
if timeout_type_used == 'single_request':
1463+
timeout_name_to_report = 'P2* timeout' if using_p2_star else 'P2 timeout'
1464+
elif timeout_type_used == 'overall':
1465+
timeout_name_to_report = 'Global request timeout'
1466+
else: # Shouldn't go here.
1467+
timeout_name_to_report = 'Timeout'
1468+
raise TimeoutException('Did not receive response in time. %s time has expired (timeout=%.3f sec)' % (timeout_name_to_report, timeout_value))
14681469
except Exception as e:
14691470
raise e
1470-
if overall_timeout_time is not None and overall_timeout_time - time.time() < 0:
1471-
raise TimeoutException('Did not receive final response in time (request timeout=%.3f sec)' % overall_timeout)
14721471

14731472
response = Response.from_payload(payload)
14741473
self.last_response = response
@@ -1486,13 +1485,12 @@ def send_request(self, request, timeout=-1):
14861485
self.logger.warning('Given response code "%s" (0x%02x) is not a supported negative response code according to UDS standard.' % (response.code_name, response.code))
14871486

14881487
if response.code == Response.Code.RequestCorrectlyReceived_ResponsePending:
1489-
if response_pending_supported:
1490-
# Received a 0x78 NRC: timeout is now set to P2*
1491-
current_timeout = self.p2_star_timeout
14921488
done_receiving = False
1493-
self.logger.debug("Server requested to wait with response code %s (0x%02x), timeout is now set to %.3f seconds" % (response.code_name, response.code, current_timeout))
1494-
else:
1495-
raise NegativeResponseException(response, "RequestCorrectlyReceived_ResponsePending not supported")
1489+
if not using_p2_star:
1490+
# Received a 0x78 NRC: timeout is now set to P2*
1491+
single_request_timeout = self.config['p2_star_timeout']
1492+
using_p2_star = True
1493+
self.logger.debug("Server requested to wait with response code %s (0x%02x), single request timeout is now set to P2* (%.3f seconds)" % (response.code_name, response.code, single_request_timeout))
14961494
else:
14971495
raise NegativeResponseException(response)
14981496

udsoncan/configs.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,8 @@
1010
'server_address_format' : None, # 8,16,24,32,40
1111
'server_memorysize_format' : None, # 8,16,24,32,40
1212
'data_identifiers' : {},
13-
'input_output' : {}
13+
'input_output' : {},
14+
'request_timeout' : 5,
15+
'p2_timeout' : 1,
16+
'p2_star_timeout' : 5,
1417
}

0 commit comments

Comments
 (0)