원시 (raw) 헥스 (hex) 값을 물리량으로 변환할 때, 물리량 = a x 헥스 + b 식으로 변환한다. a가 factor이고, b가 offset이다.
헥스 4F3가 factor와 offset으로 속도 39.59375 kph로 변환되는 과정
Minimum, Maximum:
신호의 최대값과 최소값이다.
신호 범위의 이론적 최대값과 최소값이 아닐 수 있다.
예)
신호: singed 8 bit
범위: -128 ~ 127
만일 이 신호가 냉각수 온도라면, -35 C ~ 105 C 범위를 사용할 것이다.
Minimum과 Maximum을 각각 -35와 105로 설정한다.
- 35 이하와 105 이상은 소프트웨어 이상을 감지하는데 사용할 수 있다.
Unit: 신호의 단위
Value Table:
헥스 값을 factor와 offset을 이용해서 변환하여 물리량으로 사용하지 않는 경우도 많다. 신호의 특정 헥스 값이 on 혹은 off 같은 상태를 나타내도록 하는 경우다. 이런 경우, 헥스 값과 상태의 관계를 정의하여, 헥스 값대신 상태가 표시되도록 하면 편리하다. 이런 관계를 표로 정의할 수 있다. Value Table이다.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
xlsx2dbc.py - 엑셀 파일(.xlsx)을 DBC 파일로 변환하는 도구
사용법:
python xlsx2dbc.py -i input.xlsx [-o output.dbc] [-v]
- H사는 CAN Matrix를 xlsx로 관리한다고 한다.
- xlsx에 정의된 CAN Matrix를 읽어서 dbc로 변환한다.
"""
# import
import argparse # 커맨드라인 인자 처리를 위해 사용한다.
from pathlib import Path # 파일 경로를 다루기 위해 사용한다.
import pandas as pd # xlsx의 CAN Matrix를 표 형태로 읽기 위해서 사용한다.
import cantools # CAN matrix를 dbc로 변환하기 위해 사용한다.
from cantools.database.conversion import LinearConversion # Value Table을 딕셔너리로 변환하기 위해 사용한다.
import os # 화면을 깨끗하게 지우기 위해 사용한다.
def parse_value_table(value_table_str):
"""Value Table 문자열을 딕셔너리로 변환하는 함수"""
if pd.isnull(value_table_str) or value_table_str.strip() == '':
return None
choices = {}
# 세미콜론으로 항목들 분리
items = value_table_str.split(';')
for item in items:
if '=' in item:
key_str, value = item.split('=', 1)
# 0x로 시작하면 16진수로 변환
if key_str.strip().startswith('0x'):
key = int(key_str.strip(), 16)
else:
key = int(key_str.strip())
choices[key] = value.strip()
return choices if choices else None
def convert_xlsx_to_dbc(xlsx_path, dbc_path=None, verbose=False):
"""엑셀 파일을 DBC 파일로 변환하는 함수
Args:
xlsx_path: 입력 엑셀 파일 경로
dbc_path: 출력 DBC 파일 경로 (None이면 입력 파일명.dbc)
verbose: 상세 정보 출력 여부
"""
xlsx_input = Path(xlsx_path)
# dbc 파일 경로가 지정되지 않은 경우, xlsx 파일과 같은 이름으로 .dbc 확장자로 설정
if dbc_path is None:
dbc_output = xlsx_input.with_suffix('.dbc')
else:
dbc_output = Path(dbc_path)
print(f"converting: {xlsx_input} -> {dbc_output}")
# 엑셀 파일 읽기 (첫 번째 행은 헤더로 간주하고 건너뜀)
df = pd.read_excel(xlsx_input, skiprows=1)
# column names에서 'Comment'의 위치를 찾는다.
# 그 이후의 column name들을 ecus 리스트에 저장한다.
comment_index = df.columns.get_loc('Comment')
ecus = df.columns[comment_index + 1:].tolist()
print(f'{ecus = }')
# 빈 CAN matrix(database)를 만든다.
db = cantools.database.Database()
for _, row in df.iterrows(): # 표를 한 행씩 처리한다.
message_name = row['Message'].strip()
signal_name = row['Signal']
frame_id = int(row['ID'], 16) if isinstance(row['ID'], str) else int(row['ID'])
senders = [] # sender가 복수 일 수 있다.
for ecu in ecus:
if row[ecu].strip() == 'T':
senders.append(ecu.strip())
if verbose:
print(f'Adding sender {ecu.strip()} to message {message_name}')
# Value Table이 있다면 딕셔너리로 변환한다.
value_table = parse_value_table(row['Value Table'])
# db에 메시지가 없으면 추가한다.
try:
message = db.get_message_by_name(message_name)
except Exception as e:
message = cantools.database.Message(
frame_id=frame_id,
name=message_name,
length=int(row['DLC [byte]']),
cycle_time=int(row['Cycle Time [ms]']) if not pd.isnull(row['Cycle Time [ms]']) else None,
signals=[],
senders=senders,
)
db.messages.append(message)
db.refresh() # refresh()를 하지 않으면 메시지에 신호를 추가할 때 오류가 발생한다.
if verbose:
print(f'Exception: get_message_by_name({e}) - message added')
# 신호 추가
if verbose:
print(f'Adding signal {signal_name} to message {message.name}')
# 신호 객체 생성
# 신호 객체에 아래 있는 것들보다 더 많은 정의할 속성들이 있다.
# 이 스크립트는 사용법을 설명하는 목적이니까, 개념 설명에 필요한 정도만 포함한다.
signal = cantools.database.Signal(
name=signal_name,
start=int(row['Start Bit']),
length=int(row['Length [bit]']),
byte_order='big_endian' if row['Byte Order'] == 'Motorola' else 'little_endian',
is_signed=row['Value Type'] == 'signed',
raw_initial=int(row['Initial Value']) if not pd.isnull(row['Initial Value']) else 0,
conversion=LinearConversion(
scale=float(row['Factor']),
offset=float(row['Offset']),
is_float=False
),
minimum=float(row['Minimum']) if not pd.isnull(row['Minimum']) else None,
maximum=float(row['Maximum']) if not pd.isnull(row['Maximum']) else None,
comment=row['Comment'] if not pd.isnull(row['Comment']) else None
)
# 값 테이블(choices) 설정
if value_table:
signal.choices = value_table
# 신호의 receivers에 해당 ecu를 추가한다.
for ecu in ecus:
if row[ecu].strip() == 'R':
signal.receivers.append(ecu.strip())
if verbose:
print(f'Adding receiver {ecu.strip()} to signal {signal.name}')
# 신호를 메시지에 추가한다.
message.signals.append(signal)
db.refresh()
# 메시지와 신호 정보를 출력한다.
for message in db.messages:
print(f'Message: {message.name}, ID: 0x{message.frame_id:0x}, Senders: {", ".join(message.senders)}')
for signal in message.signals:
print(f' Signal: {signal.name}')
# verbose 모드에서만 신호 상세 정보 출력
if verbose:
print(f' Byte Order: {signal.byte_order}')
print(f' Is Signed: {signal.is_signed}')
print(f' Raw Initial: {signal.raw_initial}')
print(f' Conversion: Scale: {signal.conversion.scale}, Offset: {signal.conversion.offset}')
print(f' Minimum: {signal.minimum}, Maximum: {signal.maximum}')
print(f' Receivers: {", ".join(signal.receivers)}')
print(' Value Table:')
if signal.choices is None:
print(' None')
else:
for key, value in signal.choices.items():
print(f' {key}: {value}')
print(f' Comment: {signal.comment}')
print()
print()
# db를 dbc 파일로 저장한다.
with dbc_output.open('w') as f:
f.write(db.as_dbc_string())
print(f"변환 완료: {dbc_output}")
return dbc_output
def main():
# 기존에 화면에 출력된 내용을 지운다.
os.system('cls' if os.name == 'nt' else 'clear')
"""메인 함수"""
parser = argparse.ArgumentParser(description='엑셀 파일(.xlsx)을 DBC 파일로 변환')
parser.add_argument('-i', '--input', required=True, help='입력 엑셀 파일 경로')
parser.add_argument('-o', '--output', help='출력 DBC 파일 경로 (기본값: 입력 파일명.dbc)')
parser.add_argument('-v', '--verbose', action='store_true', help='신호 상세 정보 출력')
args = parser.parse_args()
# 변환 실행
convert_xlsx_to_dbc(args.input, args.output, args.verbose)
if __name__ == "__main__":
main()