Skip to content

Commit 131bb47

Browse files
committed
Add Support for WWH OBD (0x42) ReadDtcInformation Subfunction
- New toplevel client function get_wwh_obd_dtc_by_status_mask() - Added constants for FunctionalGroupId in Dtc helper - Updated documentation - Updated unit tests
1 parent 9ff5f4a commit 131bb47

File tree

6 files changed

+196
-5
lines changed

6 files changed

+196
-5
lines changed

doc/source/udsoncan/client.rst

+1
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,7 @@ Methods by services
413413
.. automethod:: udsoncan.client.Client.get_emission_dtc_by_status_mask
414414
.. automethod:: udsoncan.client.Client.get_mirrormemory_dtc_by_status_mask
415415
.. automethod:: udsoncan.client.Client.get_dtc_by_status_severity_mask
416+
.. automethod:: udsoncan.client.Client.get_wwh_obd_dtc_by_status_mask
416417
.. automethod:: udsoncan.client.Client.get_number_of_dtc_by_status_mask
417418
.. automethod:: udsoncan.client.Client.get_mirrormemory_number_of_dtc_by_status_mask
418419
.. automethod:: udsoncan.client.Client.get_number_of_emission_dtc_by_status_mask

doc/source/udsoncan/helper_classes.rst

+13
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,19 @@ DTC.Format
107107

108108
-----
109109

110+
.. _DTC_FunctionalGroupIdentifiers:
111+
112+
DTC.FunctionalGroupIdentifiers
113+
------------------------------
114+
115+
.. autoclass:: udsoncan::Dtc.FunctionalGroupIdentifiers
116+
:members:
117+
:undoc-members:
118+
:member-order: bysource
119+
:exclude-members: get_name
120+
121+
-----
122+
110123
.. _IOValues:
111124

112125
IOValues

test/client/test_read_dtc_information.py

+97
Original file line numberDiff line numberDiff line change
@@ -2582,6 +2582,103 @@ def __init__(self, *args, **kwargs):
25822582
GenericTestNoParamRequest_DtcAndStatusMaskResponse.__init__(self, subfunction=0x15, client_function = 'get_dtc_with_permanent_status')
25832583

25842584

2585+
class TestreportDTCWWHOBDDTCByMaskRecord(ClientServerTest): # Subfn = 0x16
2586+
sb = struct.pack('B', 0x42)
2587+
badsb = struct.pack('B', 0x42+1)
2588+
functional_group_id = 0x1
2589+
status_mask = 0x2
2590+
severity_mask = 0x20
2591+
dtc_class = 0x4
2592+
expected_request_bytes = b'\x19' + sb + bytes([functional_group_id, status_mask, severity_mask | dtc_class])
2593+
2594+
def assert_no_data_response(self, response):
2595+
self.assertEqual(len(response.service_data.dtcs), 0)
2596+
self.assertEqual(response.service_data.dtc_count, 0)
2597+
2598+
def test_no_data(self):
2599+
request = self.conn.touserqueue.get(timeout=0.2)
2600+
self.assertEqual(request, self.expected_request_bytes)
2601+
self.conn.fromuserqueue.put(b'\x59' + self.sb + bytes([0x1, 0x2, 0x3, 0x4]))
2602+
2603+
def _test_no_data(self):
2604+
response = self.udsclient.get_wwh_obd_dtc_by_status_mask(self.functional_group_id, self.status_mask, self.severity_mask, self.dtc_class)
2605+
self.assert_no_data_response(response)
2606+
2607+
def test_functional_group_verification(self):
2608+
pass
2609+
2610+
def _test_functional_group_verification(self):
2611+
with self.assertRaises(ValueError):
2612+
response = self.udsclient.get_wwh_obd_dtc_by_status_mask(None, self.status_mask, self.severity_mask, self.dtc_class)
2613+
2614+
with self.assertRaises(ValueError):
2615+
response = self.udsclient.get_wwh_obd_dtc_by_status_mask(0xff, self.status_mask, self.severity_mask, self.dtc_class)
2616+
2617+
def assert_no_data_response_with_severity_class(self, response):
2618+
self.assertEqual(len(response.service_data.dtcs), 0)
2619+
self.assertEqual(response.service_data.dtc_count, 0)
2620+
2621+
def test_no_data_with_severity_class(self):
2622+
request = self.conn.touserqueue.get(timeout=0.2)
2623+
self.assertEqual(request, self.expected_request_bytes)
2624+
self.conn.fromuserqueue.put(b'\x59' + self.sb + bytes([0x1, 0x2, 0x3, 0x4]))
2625+
2626+
def _test_no_data_with_severity_class(self):
2627+
severity_class = Dtc.Severity()
2628+
severity_class.set_byte(self.severity_mask)
2629+
response = self.udsclient.get_wwh_obd_dtc_by_status_mask(self.functional_group_id, self.status_mask, severity_class, self.dtc_class)
2630+
self.assert_no_data_response(response)
2631+
2632+
def assert_single_data_response(self, response):
2633+
self.assertEqual(len(response.service_data.dtcs), 1)
2634+
self.assertEqual(response.service_data.dtc_count, 1)
2635+
2636+
dtc = response.service_data.dtcs[0]
2637+
2638+
self.assertEqual(dtc.id, 0x123456)
2639+
self.assertEqual(dtc.status.get_byte_as_int(), 0x20)
2640+
self.assertEqual(dtc.severity.get_byte_as_int(), 32)
2641+
2642+
self.assertEqual(len(dtc.extended_data), 0)
2643+
2644+
def test_single_data(self):
2645+
request = self.conn.touserqueue.get(timeout=0.2)
2646+
self.assertEqual(request, self.expected_request_bytes)
2647+
self.conn.fromuserqueue.put(b'\x59' + self.sb + bytes([0x1, 0x2, 0x3, 0x4, 0x33, 0x12, 0x34, 0x56, 0x20]))
2648+
2649+
def _test_single_data(self):
2650+
response = self.udsclient.get_wwh_obd_dtc_by_status_mask(self.functional_group_id, self.status_mask, self.severity_mask, self.dtc_class)
2651+
self.assert_single_data_response(response)
2652+
2653+
def assert_multiple_data_response(self, response):
2654+
self.assertEqual(len(response.service_data.dtcs), 2)
2655+
self.assertEqual(response.service_data.dtc_count, 2)
2656+
2657+
dtc = response.service_data.dtcs[0]
2658+
2659+
self.assertEqual(dtc.id, 0x123456)
2660+
self.assertEqual(dtc.status.get_byte_as_int(), 0x20)
2661+
self.assertEqual(dtc.severity.get_byte_as_int(), 32)
2662+
self.assertEqual(len(dtc.extended_data), 0)
2663+
2664+
dtc = response.service_data.dtcs[1]
2665+
2666+
self.assertEqual(dtc.id, 0x123457)
2667+
self.assertEqual(dtc.status.get_byte_as_int(), 0x20)
2668+
self.assertEqual(dtc.severity.get_byte_as_int(), 32)
2669+
self.assertEqual(len(dtc.extended_data), 0)
2670+
2671+
2672+
def test_multiple_data(self):
2673+
request = self.conn.touserqueue.get(timeout=0.2)
2674+
self.assertEqual(request, self.expected_request_bytes)
2675+
self.conn.fromuserqueue.put(b'\x59' + self.sb + bytes([0x1, 0x2, 0x3, 0x4])
2676+
+ bytes([0x33, 0x12, 0x34, 0x56, 0x20])
2677+
+ bytes([0x33, 0x12, 0x34, 0x57, 0x20]))
2678+
2679+
def _test_multiple_data(self):
2680+
response = self.udsclient.get_wwh_obd_dtc_by_status_mask(self.functional_group_id, self.status_mask, self.severity_mask, self.dtc_class)
2681+
self.assert_multiple_data_response(response)
25852682

25862683

25872684
class TestreportDTCExtDataRecordByRecordNumber(ClientServerTest): # Subfn = 0x16

udsoncan/client.py

+36-3
Original file line numberDiff line numberDiff line change
@@ -1361,6 +1361,38 @@ def get_dtc_by_status_severity_mask(self, status_mask: int, severity_mask: int)
13611361
"""
13621362
return self.read_dtc_information(services.ReadDTCInformation.Subfunction.reportDTCBySeverityMaskRecord, status_mask=status_mask, severity_mask=severity_mask)
13631363

1364+
def get_wwh_obd_dtc_by_status_mask(self, functional_group_id: int, status_mask: int, severity_mask: Union[int,Dtc.Severity], dtc_class: int) -> Optional[services.ReadDTCInformation.InterpretedResponse]:
1365+
"""
1366+
Performs a ``ReadDTCInformation`` service request with subfunction ``reportWWHOBDDTCByMaskRecord``
1367+
1368+
Reads all the WWH OBD Diagnostic Trouble Codes that have a functional_group, class, status and a severity matching the given masks.
1369+
The server will check all of its DTCs and if ( (Dtc.status & status_mask) != 0 && (Dtc.severity & severity) !=0), then the DTCs match the filter and are sent back to the client.
1370+
Note: severity_mask and dtc_class are combined into a single byte to populate DTCSeverityMask- see Table D.11.
1371+
1372+
:Effective configuration: ``exception_on_<type>_response`` ``tolerate_zero_padding`` ``ignore_all_zero_dtc``
1373+
1374+
:param functional_group_id: Functional Group ID to search for (FGID) (0x00 to 0xFF) :ref:`Dtc.FunctionalGroupIdentifiers<DTC_FunctionalGroupIdentifiers>`
1375+
:type functional_group_id: int
1376+
1377+
:param status_mask: The status mask against which the DTCs are tested.
1378+
:type status_mask: int or :ref:`Dtc.Status<DTC_Status>`
1379+
1380+
:param severity_mask: The severity mask against which the DTCs are tested. (Optionas 0x20, 0x40, or 0x80)
1381+
:type severity_mask: int or :ref:`Dtc.Severity<DTC_Severity>`
1382+
1383+
:param dtc_class: The GTR DTC class mask against which the DTCs are tested. (Options 0x01, 0x02, 0x04, 0x08)
1384+
:type dtc_class: int
1385+
1386+
:return: The server response parsed by :meth:`ReadDTCInformation.interpret_response<udsoncan.services.ReadDTCInformation.interpret_response>`
1387+
:rtype: :ref:`Response<Response>`
1388+
"""
1389+
dtc_severity_mask = dtc_class & 0x1f
1390+
if isinstance(severity_mask, Dtc.Severity):
1391+
dtc_severity_mask |= (severity_mask.get_byte_as_int() & 0xe0)
1392+
else:
1393+
dtc_severity_mask |= (severity_mask & 0xe0)
1394+
return self.read_dtc_information(services.ReadDTCInformation.Subfunction.reportWWHOBDDTCByMaskRecord, status_mask=status_mask, severity_mask=dtc_severity_mask, functional_group_id=functional_group_id)
1395+
13641396
def get_number_of_dtc_by_status_mask(self, status_mask: int) -> Optional[services.ReadDTCInformation.InterpretedResponse]:
13651397
"""
13661398
Performs a ``ReadDTCInformation`` service request with subfunction ``reportNumberOfDTCByStatusMask``
@@ -1733,8 +1765,8 @@ def read_dtc_information(self,
17331765
snapshot_record_number: Optional[int] = None,
17341766
extended_data_record_number: Optional[int] = None,
17351767
extended_data_size: Optional[int] = None,
1736-
memory_selection: Optional[int] = None
1737-
):
1768+
memory_selection: Optional[int] = None,
1769+
functional_group_id: Optional[int] = None):
17381770
if dtc is not None and isinstance(dtc, Dtc):
17391771
dtc = dtc.id
17401772

@@ -1745,7 +1777,8 @@ def read_dtc_information(self,
17451777
snapshot_record_number=snapshot_record_number,
17461778
extended_data_record_number=extended_data_record_number,
17471779
memory_selection=memory_selection,
1748-
standard_version=self.config['standard_version'])
1780+
standard_version=self.config['standard_version'],
1781+
functional_group_id=functional_group_id)
17491782

17501783
self.logger.info('%s - Sending request with subfunction "%s" (0x%02X).' % (self.service_log_prefix(services.ReadDTCInformation),
17511784
services.ReadDTCInformation.Subfunction.get_name(subfunction), subfunction))

udsoncan/common/dtc.py

+8
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ def get_name(cls, given_id: Optional[int]) -> Optional[str]:
3737

3838
return None
3939

40+
class FunctionalGroupIdentifiers:
41+
"""
42+
Provides a list of FunctionalGroupIdentifiers (Table D.15) which are used by the :ref:`ReadDTCInformation<ReadDtcInformation>` when requesting a number of DTCs.
43+
"""
44+
EMISSIONS_SYSTEM_GROUP = 0x33
45+
SAFETY_SYSTEM_GROUP = 0xD0
46+
VOBD_SYSTEM = 0xFE
47+
4048
# DTC Status byte
4149
# This byte is an 8-bit flag indicating how much we are sure that a DTC is active.
4250
class Status:

udsoncan/services/ReadDTCInformation.py

+41-2
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,10 @@ class Subfunction(BaseSubfunction):
106106
reportUserDefMemoryDTCByStatusMask = 0x17
107107
reportUserDefMemoryDTCSnapshotRecordByDTCNumber = 0x18
108108
reportUserDefMemoryDTCExtDataRecordByDTCNumber = 0x19
109+
reportWWHOBDDTCByMaskRecord = 0x42
109110

110111
# todo
111112
reportSupportedDTCExtDataRecord = 0x1A
112-
reportWWHOBDDTCByMaskRecord = 0x42
113113
reportWWHOBDDTCWithPermanentStatus = 0x55
114114
reportDTCInformationByDTCReadinessGroupIdentifier = 0x56
115115

@@ -161,6 +161,12 @@ def assert_extended_data_size_int_or_dict(cls,
161161
for dtcid in extended_data_size:
162162
tools.validate_int(extended_data_size[dtcid], min=0, max=0xFFF, name='Extended data size for DTC=0x%06x' % dtcid)
163163

164+
@classmethod
165+
def assert_functional_group_id(cls, functional_group_id: Optional[int], subfunction, maxval: int = 0xFF) -> None:
166+
if functional_group_id is None:
167+
raise ValueError('functional_group_id must be provided for subfunction 0x%02x' % subfunction)
168+
tools.validate_int(functional_group_id, min=0, max=maxval, name='Functional Group ID')
169+
164170
@classmethod
165171
def pack_dtc(cls, dtcid: int) -> bytes:
166172
return struct.pack('BBB', (dtcid >> 16) & 0xFF, (dtcid >> 8) & 0xFF, (dtcid >> 0) & 0xFF)
@@ -202,7 +208,9 @@ def make_request(cls,
202208
snapshot_record_number: Optional[int] = None,
203209
extended_data_record_number: Optional[int] = None,
204210
memory_selection: Optional[int] = None,
205-
standard_version: int = latest_standard) -> Request:
211+
standard_version: int = latest_standard,
212+
functional_group_id:Optional[int] = None,
213+
) -> Request:
206214
"""
207215
Generates a request for ReadDTCInformation.
208216
Each subfunction uses a subset of parameters.
@@ -361,6 +369,12 @@ def make_request(cls,
361369
cls.assert_extended_data_record_number(extended_data_record_number, subfunction, maxval=0xEF) # Maximum specified by ISO-14229:2020
362370
req.data = struct.pack('B', extended_data_record_number)
363371

372+
elif subfunction == ReadDTCInformation.Subfunction.reportWWHOBDDTCByMaskRecord:
373+
cls.assert_status_mask(status_mask, subfunction)
374+
cls.assert_severity_mask(severity_mask, subfunction)
375+
cls.assert_functional_group_id(functional_group_id, subfunction, maxval=0xFE) # Maximum specified by ISO-14229:2020
376+
req.data = struct.pack('BBB', functional_group_id, status_mask, severity_mask)
377+
364378
return req
365379

366380
@classmethod
@@ -872,6 +886,31 @@ def interpret_response(cls,
872886

873887
response.service_data.dtc_count = len(response.service_data.dtcs)
874888

889+
elif subfunction == ReadDTCInformation.Subfunction.reportWWHOBDDTCByMaskRecord:
890+
if len(response.data) < 4:
891+
raise InvalidResponseException(response, 'Incomplete response from server.')
892+
893+
_functional_group_id = response.data[1]
894+
_dtc_status_availability_mask = response.data[2]
895+
_dtc_severity_availability_mask = response.data[3]
896+
_dtc_format_identifier = response.data[4]
897+
898+
remaining_bytes = response.data[5:]
899+
900+
if len(remaining_bytes) % 5 != 0:
901+
raise InvalidResponseException(response, 'Incomplete response from server. Remaining bytes must be a multiple of 5')
902+
903+
while remaining_bytes:
904+
severity = remaining_bytes[0]
905+
dtc = Dtc(struct.unpack('>L', b'\x00' + remaining_bytes[1:4])[0])
906+
status_of_dtc = Dtc.Status.from_byte(remaining_bytes[4])
907+
dtc.severity.set_byte(severity)
908+
dtc.status = status_of_dtc
909+
remaining_bytes = remaining_bytes[5:]
910+
response.service_data.dtcs.append(dtc)
911+
912+
response.service_data.dtc_count = len(response.service_data.dtcs)
913+
875914
return cast(ReadDTCInformation.InterpretedResponse, response)
876915

877916
@classmethod

0 commit comments

Comments
 (0)