application

진단 응답 해석하기

hsl7 2025. 1. 26. 18:17

venue_parse_diag_resp_blf

import os
from pathlib import Path
import can  # blf 파일 처리를 위해 python-can 모듈을 사용한다.
import pandas as pd
ldf is not supported
xls is not supported
xlsx is not supported
# 측정한 blf 파일들
k_dir_data = Path().absolute()/'data/venue'
blfs = list(k_dir_data.glob('*.blf'))
for i, blf in enumerate(blfs):
    print(f'{i:2}: {blf.name:60} {blf.stat().st_size:12,} bytes')
 0: uds_vehicle_config_2025_01_20_16_52_17.blf                      2,607,508 bytes
 1: uds_vehicle_config_2025_01_20_16_54_53.blf                        370,192 bytes
 2: uds_vehicle_config_2025_01_20_16_55_16.blf                      1,311,074 bytes
 3: uds_vehicle_config_2025_01_20_16_56_21.blf                      1,215,600 bytes
 4: uds_vehicle_config_2025_01_22_16_40_23_ecu_id.blf                 449,724 bytes
 5: uds_vehicle_config_2025_01_22_16_42_06_ecu_id.blf                 195,962 bytes
 6: uds_vehicle_config_2025_01_22_16_59_00_ecu_id.blf                     316 bytes
 7: uds_vehicle_config_2025_01_22_17_00_19_ecu_id.blf               3,032,788 bytes
 8: uds_vehicle_config_2025_01_22_17_03_05_dtc.blf                  3,032,318 bytes
 9: uds_vehicle_config_2025_01_22_17_05_54_testerPresent.blf        3,271,184 bytes
10: uds_vehicle_config_2025_01_22_17_08_52_sw_ver.blf               3,024,556 bytes

노트

  • 7: uds_vehicle_config_2025_01_22_17_00_19_ecu_id.blf 3,032,788 bytes
  • 8: uds_vehicle_config_2025_01_22_17_03_05_dtc.blf 3,032,318 bytes
  • 9: uds_vehicle_config_2025_01_22_17_05_54_testerPresent.blf 3,271,184 bytes
  • 10: uds_vehicle_config_2025_01_22_17_08_52_sw_ver.blf 3,024,556 bytes
  • 위 네 파일들이 내가 진단 요청과 응답 메시지들의 m_id 쌍을 찾기 위해서 베뉴에서 측정한 데이터 파일들이다.

진단 통신 응답을 해석한다.

  • Read ECU ID, Read DTC 요청의 응답을 해석한다.
def convert_dtc(dtc_bytes):
    '''
    written by claude.ai
    제어기가 응답한 결함 코드를 표준 코드로 변환한다.
    표준 코드는 Pxxxxxx, Cxxxxxx, Bxxxxxx, Uxxxxxx 형식이다.
    dtc_bytes: 3바이트의 결함 코드
    '''

    # DTC 타입 결정을 위한 딕셔너리
    # dictionary to determine DTC type
    dtc_type = {
        0: 'P', 
        1: 'C', 
        2: 'B', 
        3: 'U',
    }

    # 첫 번째 바이트의 상위 2비트를 사용하여 DTC 타입 결정
    # determine DTC type using the first byte's upper 2 bits
    first_char = dtc_type[(dtc_bytes[0] >> 6) & 0x3]

    # 첫 번째 바이트의 하위 6비트와 두 번째 바이트를 결합하여 중간 부분 생성
    # combine the lower 6 bits of the first byte and the second byte to create the middle part
    middle_part = ((dtc_bytes[0] & 0x3F) << 8) | dtc_bytes[1]

    # 세 번째 바이트를 마지막 부분으로 사용
    # use the third byte as the last part
    last_part = dtc_bytes[2]

    # DTC 문자열 생성
    # create the DTC string
    dtc_string = f"{first_char}{middle_part:04X}{last_part:02X}"

    return dtc_string

# 현대기술정보 홈페이지에서 복붙한 DTC와 DTC 설명이 있는xlsx 파일을 읽는다.
# xlsx 파일 경로
xlsx_dtc_table = Path('베뉴_VDC_DTC_현대기술정보.xlsx')

# DTC표를 Pandas 데이터프레임으로 읽는다. 
# 이 데이터프레임은 get_dtc_description 함수에서 사용된된다.
df_dtc = pd.read_excel(xlsx_dtc_table, index_col='DTC')

df_dtc.sample(3)
  Description
DTC  
P0105 VDC 모듈(Vehicle Dynamic Control Unit)는 비정상적인 클러...
C1702 전원 인가 후VDC 모듈은 베리언트 코드를 확인한다. 이때 부적절한 베리언트 코드가...
P0017 VDC 모듈은 조향각센서의 비정상적인 신호를 수신한 경우 이 고장코드를 나타낸다.
def get_dtc_description(dtc):
    '''
    입력으로 받은 DTC에 대한 설명인 description을 반환한다.
    '''

    global df_dtc

    try:
        description = df_dtc.loc[dtc, 'Description']
    except KeyError:
        description = 'DTC를 찾을 수 없습니다.'

    return description
# parse_resp()에서 사용되는 고정값들을 정의한다.

# 긍정 응답은 진단 요청에 0x40을 더한 값으로 시작한다.
k_offset_positive_resp = 0x40

# read DTC의 service id = 0x19
k_req_sid_readDtc = 0x19
k_resp_sid_readDtc = k_req_sid_readDtc + k_offset_positive_resp

# read ECU Id service id = 0x22
k_req_sid_readEcuId = 0x22
k_resp_sid_readEcuId = k_req_sid_readEcuId + k_offset_positive_resp
def parse_resp(m_id_hex) -> None:
    '''
    제어기의 진단 응답을 분석한다.
    '''

    global diag_resp

    # read DTC
    if diag_resp[m_id_hex]['s_id'] == k_resp_sid_readDtc:    
         # 처음 3 바이트는 dtc 정보가 아니다.
         # The first 3 bytes are not DTC information.
         # 4 바이트씩 DTC 정보가 있다.
        # DTC information is in 4 bytes each.
        n_dtc = (diag_resp[m_id_hex]['length'] - 3) // 4  

        if n_dtc > 0:
            print(f'{m_id_hex = }')
            for i_dtc in range(1, n_dtc + 1):
                dtc = diag_resp[m_id_hex]['assembled'][3 + 4 * i_dtc: 3 + 4 * i_dtc + 3]

                dtc_hex = []
                for byte_dtc in dtc:
                    dtc_hex.append(f'{byte_dtc:02X}')
                dtc_hex_joined = ''.join(dtc_hex)

                # DTC가 아니라 fill byte인 경우
                if dtc_hex_joined == 'aaaaaa':
                    continue

                dtc_converted = convert_dtc(dtc)
                dtc_description = get_dtc_description(dtc_converted)
                dtc_state = diag_resp[m_id_hex]['assembled'][3 + 4 * i_dtc + 3] 

                print(f'{i_dtc = }') 
                print(f'dtc = {dtc_converted} (0x{dtc_hex_joined})') 
                print(f'{dtc_description}')             
                print(f'{dtc_state = :02X}') 
                print('---') 


    ## read data by identifier
    # DID(Data ID)에 따라 read ECU ID, read Software Version 등 여러 가지가 있다. 
    if diag_resp[m_id_hex]['s_id'] == k_resp_sid_readEcuId:
        # data_id에 따라 처리한다.    
        data_id_high = diag_resp[m_id_hex]['assembled'][1] 
        data_id_low = diag_resp[m_id_hex]['assembled'][2] 

        # data_id 별로 response 처리 방법이 다르다.

        # data_id == ECU Id: 
        if data_id_high == 0xF1 and data_id_low == 0x00: # data_id for ECU Id
            ecu_id = ''.join([chr(c) for c in diag_resp[m_id_hex]['assembled'][3:] if c != 0xaa])
            print(f'{m_id_hex:4} {ecu_id = }') 

        # data_id == SW Version: 
        elif data_id_high == 0x10 and data_id_low == 0x00:
            sw_id = ''.join([chr(c) for c in diag_resp[m_id_hex]['assembled'][3:] if c != 0xaa])
            print(f'{m_id_hex:4} {sw_id = }')
def on_can_diag_resp(msg, verbose=False) -> None:
    '''
    Transport Protocol로 분할된 메시지들을 받아서 조립하려면 
    이전 함수 호출 시 데이터들을 기억해야 한다.
    이를 위해 전역 변수를 사용한다. 
    '''
    global diag_resp

    # response 메시지를 카운트한다.

    # message id
    m_id = msg.arbitration_id


    # diag_resp의 key로 사용하기 위해 int를 str로 변환한다.
    m_id_hex = hex(m_id)

    if m_id_hex in diag_resp:
        diag_resp[m_id_hex]['counter'] += 1
    else: 
        #   length_response     : 제어기가 알려준 응답의 길리 [바이트] 
        #   counter_response    : (응답이 길어 여러 메시지로 나뉘어 전송될 경우) 메시지 카운터
        #   serive_id           : 서비스 아이디
        #   response_assembled  : (응답이 길어 여러 메시지로 나뉘어 전송될 경우) 응답 메시지들 전체를 조립한 메시지
        #   response_hex        : response_assembled의 바이트 하나 하나를 hex로 표현 

        diag_resp[m_id_hex] = {}
        diag_resp[m_id_hex]['length'] = 0
        diag_resp[m_id_hex]['counter'] = 1
        diag_resp[m_id_hex]['s_id'] = 0x00
        diag_resp[m_id_hex]['assembled'] = []
        diag_resp[m_id_hex]['hex'] = []


    # 응답의 데이터 8 바이트를 hex로 표시한다.
    # 8은 클래식 CAN의 최대 메시지 길이이고, 
    # 모비스 VDC는 CAN-FD로 통신한다.
    # 진단 통신response 메시지의 길이는 64 바이트이지만,
    # 처음 8 바이트 외에는 0x00으로 채우고 사용하지 않는다.  

    data_hex = []
    for i in range(msg.dlc):
        data_hex.append(f'{msg.data[i]:02X}')
    # if verbose:
    #     print(f'{m_id_hex:4} counter = {diag_resp[m_id_hex]["counter"]:2}: {data_hex}')


    # TP로 전송된 분해된 response를 조립한다.
    pci = msg.data[0] & 0xF0   
    if pci == 0x00:        # single frame
        # 같은 m_id의 다음 응답 처리를 위해 초기화 한다.
        # counter는 초기화하지 않는다. 응답 횟수를 추적하기 위해서

        diag_resp[m_id_hex]['length'] = 0
        # diag_resp[m_id_hex]['counter'] = 1
        diag_resp[m_id_hex]['s_id'] = 0x00
        diag_resp[m_id_hex]['assembled'] = []
        diag_resp[m_id_hex]['hex'] = []

        diag_resp[m_id_hex]['length'] = msg.data[0] & 0x0F
        diag_resp[m_id_hex]['assembled'].extend(msg.data[1:8])
    elif pci == 0x10:      # first frame 
        # 같은 m_id의 다음 응답 처리를 위해 초기화 한다.
        # counter는 초기화하지 않는다. 응답 횟수를 추적하기 위해서

        diag_resp[m_id_hex]['length'] = 0
        # diag_resp[m_id_hex]['counter'] = 1
        diag_resp[m_id_hex]['s_id'] = 0x00
        diag_resp[m_id_hex]['assembled'] = []
        diag_resp[m_id_hex]['hex'] = []

        diag_resp[m_id_hex]['length'] = (msg.data[0] & 0x0F) * 256 + msg.data[1]
        diag_resp[m_id_hex]['assembled'].extend(msg.data[2:8])
    elif (pci == 0x20):    # consecutive frame
        diag_resp[m_id_hex]['assembled'].extend(msg.data[1:8])
    else:    # pci == 0x30: flow control 
        pass 
        # do nothing
        # blf replay로 데이터를 처리하는 경우 flow control 메시지를 
        # Rx 할 수도 있다. 
        # 제어기와 온-라인으로 연결한 상태에서는 여기로 올 수 없다.


    # TP로 전송된 메시지의 조립이 완료되면 출력한다. 
    if ((diag_resp[m_id_hex]['length'] != 0) and
        (diag_resp[m_id_hex]['length'] <= len(diag_resp[m_id_hex]['assembled']))):

        for byte_response in diag_resp[m_id_hex]['assembled']:
            # byte_response를 hex로 변환하여 diag_resp[m_id_hex]['hex']에 저장한다.
            # 전체 응답 메시지를 hex로 보기 위한 용도이다. 
            diag_resp[m_id_hex]['hex'].append(f'{byte_response:02X}')

        if verbose:
            print(f'{m_id_hex:4} {diag_resp[m_id_hex]["hex"]}')

        diag_resp[m_id_hex]['s_id'] = diag_resp[m_id_hex]['assembled'][0]

        parse_resp(m_id_hex)

    return    # on_can_diag_resp() 
def extract_diag_resp_from_blf(blf, verbose=False) -> None:
    '''
    blf 파일을 읽어서 진단 통신 관련된 부분만 출력한다.
    '''
    global diag_resp

    log = can.io.BLFReader(blf)

    for msg in log:
        if (msg.is_rx) and ((msg.arbitration_id >= 0x700) and (msg.arbitration_id <= 0x7FF)):
            on_can_diag_resp(msg, verbose=verbose)

    return    # extract_diag_resp_from_blf()

Read ECU ID로 스캐닝한 응답 해석

# Read DTC 요청으로 스캐닝한 결과
i = 7
blf = blfs[i]
diag_resp =  {}
print(f'{blf.name = }')
extract_diag_resp_from_blf(blf, verbose=False)
blf.name = 'uds_vehicle_config_2025_01_22_17_00_19_ecu_id.blf'
0x778 ecu_id = '19'
0x79e ecu_id = '0.3\x00\x00\x00\x00\x00\x00\x00\x00\x00'
0x7a8 ecu_id = '220'
0x7bb ecu_id = 'QX    97250-K2300HEATER-CONTROL FATC V1-02-903-00\x00\x00\x00'
0x7cc ecu_id = 'QX  MFC  AT KOR LHD 1.00 1.01 99211-K2100 210329'
0x7ce ecu_id = '941\x00'
0x7d9 ecu_id = 'QX ESC \x05 100\x19\x04\x01 58910-K2500'
0x7dc ecu_id = 'QX  MDPS C 1.00 1.06 56340-K2000 0B06'
0x7a8 ecu_id = '220'
  • 일부 제어기들의 이름을 파악할 수 있다.
    • 7CC = MFC, 7D9 = ESC, 7DC = MDPS
    • 원칙적으로는 모든 제어기들의 이름을 파악할 수 있었어야 한다고 생각한다.
  • 진단 통신 사양서가 있었다면 차량 형상을 보다 더 정확하게 파악할 수 있을 것이다.
  • 이렇게 파악한 차량 형상과 설계 기준 사양과 비교하여 문제가 있었다면, 문제를 발견할 수 있었을 것이다.
  • 진단 스펙과 CAN 스펙까지 있다면 아래와 같이 하도록 프로그램을 작성했을 것이다.
    • DTC 점검
      • 전체 제어기들의 DTC를 읽는다.
      • DTC가 있다면 그리고 그 원인이 CAN 메시지와 관련이 있다면 (예, CAN 신호 이상), 원인과 관련된 메시지의 유무, 해당 신호의 State of Health를 점검한다.
    • 통신 점검
      • 제어기별로 통신을 정지하며 (진단 명령 중에 stop transmission을 이용한다.) 타 제어기들에 DTC가 저장되는지 확인한다.
      • 제어기들의 DTC 저장과 스펙이 잘 맞는지 (DTC를 저장해야 하는 제어기가 DTC를 저장하는가? DTC를 저장하지 말아야 하는 제어기가 DTC를 저장하지 않는가? stop transmission을 안 하는 제어기도) 확인한다.
  • 내가 지금하고 있는 것은 일종의 리버스 엔지니어링이다. TSMaster 사용법 데모의 범위를 너무 넘어선다. 요기까지만 한다.

Read DTC로 스캐닝한 응답 해석

# Read DTC 요청으로 스캐닝한 결과
i = 8
blf = blfs[i]
diag_resp =  {}
print(f'{blf.name = }')
extract_diag_resp_from_blf(blf, verbose=True)
blf.name = 'uds_vehicle_config_2025_01_22_17_03_05_dtc.blf'
0x778 ['59', '02', '09', 'AA', 'AA', 'AA', 'AA']
0x79e ['59', '02', '08', 'AA', 'AA', 'AA', 'AA']
0x7a8 ['7F', '19', '78', 'AA', 'AA', 'AA', 'AA']
0x7a8 ['59', '02', '09', 'AA', 'AA', 'AA', 'AA']
0x7bb ['59', '02', '09', '00', '00', '00', '00']
0x7cb ['59', '02', '89', 'AA', 'AA', 'AA', 'AA']
0x7cc ['59', '02', '09', 'AA', 'AA', 'AA', 'AA']
0x7ce ['59', '02', '08', '00', '00', '00', '00']
0x7d9 ['59', '02', '89', 'AA', 'AA', 'AA', 'AA']
0x7da ['7F', '19', '11', 'AA', 'AA', 'AA', 'AA']
0x7dc ['59', '02', '89', 'AA', 'AA', 'AA', 'AA']
0x7dc ['59', '02', '89', 'AA', 'AA', 'AA', 'AA']
0x79e ['59', '02', '08', 'AA', 'AA', 'AA', 'AA']
0x7e8 ['59', '02', 'FF', 'AA', 'AA', 'AA', 'AA']
0x778 ['59', '02', '09', 'AA', 'AA', 'AA', 'AA']
0x7cb ['59', '02', '89', 'AA', 'AA', 'AA', 'AA']
0x7da ['59', '02', '89', 'AA', 'AA', 'AA', 'AA']
0x7e9 ['59', '02', 'FF', 'AA', 'AA', 'AA', 'AA']
0x7cc ['59', '02', '09', 'AA', 'AA', 'AA', 'AA']
0x7a8 ['7F', '19', '78', 'AA', 'AA', 'AA', 'AA']
0x7a8 ['59', '02', '09', 'AA', 'AA', 'AA', 'AA']
0x7e8 ['59', '02', 'FF', 'AA', 'AA', 'AA', 'AA']
0x7e9 ['59', '02', 'FF', 'AA', 'AA', 'AA', 'AA']
0x7f9 ['7F', '19', '11', 'AA', 'AA', 'AA', 'AA']
  • 나는 내가 갖고 있는 모비스 ESC에서 사용한 "0x19 0x02 0x09"을 베뉴에서 Read DTC 요청으로 사용했다.
    • 0x02의 자리는 DTCStatusMask 적용 여부를 지정한다.
    • 0x09 자리는 응답에 과거 DTC 포함 여부를 지정한다.
    • 나는 전체 DTC를 읽도록 설정했다.
  • 대부분 제어기들이 긍정 응답을 했다. 그런데 진단 코드를 회신하지 않았다.
    • 저장된 진단 코드가 없는 것인가?
    • 마스크 지정에 문제가 있는 건가?
    • 다른 마스크를 이용하여 요청을 해볼 수 있으나, 내 목적인 TSMaster 사용법 설명으로는 과하다. 요기까지.
  • 아래 m_id의 제어기들은 부정 응답을 했다.
    • 7A8, 7DA, 7F9
    • 부정 응답의 이유는 0x11과 0x78이다. 무슨 의미인지 모르겠다.

TesterPresent로 스캐닝한 응답 해석

# TesterPresent로 스캐닝한 결과
i = 9
blf = blfs[i]
diag_resp =  {}
print(f'{blf.name = }')
extract_diag_resp_from_blf(blf, verbose=True)
blf.name = 'uds_vehicle_config_2025_01_22_17_05_54_testerPresent.blf'
0x778 ['7E', '00', 'AA', 'AA', 'AA', 'AA', 'AA']
0x79e ['7E', '00', 'AA', 'AA', 'AA', 'AA', 'AA']
0x7a8 ['7F', '3E', '7F', 'AA', 'AA', 'AA', 'AA']
0x7bb ['7E', '00', '12', '00', '00', '00', '00']
0x7cb ['7E', '00', 'AA', 'AA', 'AA', 'AA', 'AA']
0x7cc ['7F', '3E', '7F', 'AA', 'AA', 'AA', 'AA']
0x7ce ['7E', '00', '00', '00', '00', '00', '00']
0x7d9 ['7F', '3E', '7F', 'AA', 'AA', 'AA', 'AA']
0x7da ['7F', '3E', '11', 'AA', 'AA', 'AA', 'AA']
0x7dc ['7E', '00', 'AA', 'AA', 'AA', 'AA', 'AA']
0x7dc ['7E', '00', 'AA', 'AA', 'AA', 'AA', 'AA']
0x778 ['7E', '00', 'AA', 'AA', 'AA', 'AA', 'AA']
0x7cb ['7E', '00', 'AA', 'AA', 'AA', 'AA', 'AA']
0x7d9 ['7F', '3E', '7F', 'AA', 'AA', 'AA', 'AA']
0x7e9 ['7E', '00', 'AA', 'AA', 'AA', 'AA', 'AA']
0x79e ['7E', '00', 'AA', 'AA', 'AA', 'AA', 'AA']
0x7e8 ['7E', '00', 'AA', 'AA', 'AA', 'AA', 'AA']
0x7cc ['7F', '3E', '7F', 'AA', 'AA', 'AA', 'AA']
0x7e8 ['7E', '00', 'AA', 'AA', 'AA', 'AA', 'AA']
0x7e9 ['7E', '00', 'AA', 'AA', 'AA', 'AA', 'AA']
0x7f9 ['7E', '00', 'AA', 'AA', 'AA', 'AA', 'AA']

노트

  • TesterPresent에 부정 응답(0x7F)를 하는 제어기도 있구나.

Read Software Version으로 스캐닝한 응답 해석

# Read Software Versin 요청으로 스캐닝한 결과
i = 10
blf = blfs[i]
diag_resp =  {}
print(f'{blf.name = }')
extract_diag_resp_from_blf(blf, verbose=True)
blf.name = 'uds_vehicle_config_2025_01_22_17_08_52_sw_ver.blf'
0x778 ['7F', '22', '31', 'AA', 'AA', 'AA', 'AA']
0x79e ['7F', '22', '31', 'AA', 'AA', 'AA', 'AA']
0x7a8 ['7F', '22', '31', 'AA', 'AA', 'AA', 'AA']
0x7bb ['7F', '22', '12', '00', '00', '00', '00']
0x7cb ['7F', '22', '31', 'AA', 'AA', 'AA', 'AA']
0x7cc ['7F', '22', '31', 'AA', 'AA', 'AA', 'AA']
0x7ce ['7F', '22', '22', '00', '00', '00', '00']
0x7d9 ['7F', '22', '31', 'AA', 'AA', 'AA', 'AA']
0x7da ['7F', '22', '11', 'AA', 'AA', 'AA', 'AA']
0x7dc ['7F', '22', '31', 'AA', 'AA', 'AA', 'AA']
0x7d9 ['7F', '22', '22', 'AA', 'AA', 'AA', 'AA']
0x7cc ['7F', '22', '31', 'AA', 'AA', 'AA', 'AA']
0x7e8 ['7F', '22', '31', 'AA', 'AA', 'AA', 'AA']
0x7e9 ['7F', '22', '31', 'AA', 'AA', 'AA', 'AA']
0x7f9 ['7F', '22', '22', 'AA', 'AA', 'AA', 'AA']

노트

  • Read Software Version 요청의 응답은 모두 부정(0x7F) 응답이다.
  • 나는 표준(Standard) 모드에서 요청을 했다. 요청을 진단(Extended Diagnostics) 모드에서 했어야 했을 수도 있다.
  • DID가 맞지 않았을 수 있다.
  • 위 두 가지는 진단 통신 스펙이 있었다면 쉽게 피할 수 있는 문제들이다.

 

참고

venue_parse_diag_resp_blf.ipynb
0.03MB