venue_parse_diag_resp_blf
- 차(베뉴)에서 측정한 CAN 트레이스(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 메시지와 관련이 있다면 해당 메시지의 유무, 해당 신호의 State of Health를 점검한다.
- 통신 점검
- 제어기별로 통신을 정지하며 타 제어기들에 DTC가 저장되는지 확인한다.
- 제어기들의 DTC 저장과 스펙이 잘 맞는지 (DTC를 저장해야 하는 제어기가 DTC를 저장하는가? DTC를 저장하지 말아야 하는 제어기가 DTC를 저장하지 않는가?) 확인한다.
- 내가 지금하고 있는 것은 일종의 리버스 엔지니어링이다. 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가 맞지 않았을 수 있다.
- 위 두 가지는 진단 통신 스펙이 있었다면 쉽게 피할 수 있는 문제들이다.