ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • xlsx2dbc
    tip 2025. 8. 22. 16:10

     

    시작하기 전에 

    • H사는 협력사와 CAN 메시지/신호(매트릭스, matrix) 변경을 협의할 때 xlsx 파일을 이용한다고 한다.
    • 변경된 CAN 매트릭스를 툴에 적용하려면 dbc 파일이 필요하다.
    • xlsx의 변경 내용을 수동으로 dbc에 입력하는 것은 불편하다. 귀찮은 일이다. 입력하고 확인하는데 시간도 많이 든다. 꼼꼼하게 하는데 에너지도 많이 든다. 실수가 있을 수도 있다.
    • xlsx를 dbc로 만드는 파이썬 스크립트를 작성하였다.
    • 시간, 노력을 아끼는데 도움이 되기를 바란다.

     

    개요

    • xlsx 구조 살펴보기
    • xlsx2dbc.py 작성하기
    • 실행 결과 (생성된 dbc)

     

    xlsx 구조 살펴보기

    • xlsx의 구조는 아래 그림과 같다.

    CAN Matrix 정보를 xlsx로 관리하는 예

     

    • Message: 메시지 이름
    • ID: 메시지 아이디, 헥스 (string)
    • DLC [byte]: 메시지 길이
    • Cycle Time [msec]: 전송 주기
    • Signal: 시그날(신호) 이름
    • Start Bit: 신호 시작 비트 위치
    • Length [bit]: 신호 길이. 비트
    • Byte Order:
      • LSB 혹은 MSB 인 것 같다. LSB가 Intel 방식인 것 같다. Intel 방식은 Little Endian 방식이다.
      • Little Endian
        • 예: 0x12345678을 저장할 때 → 78 56 34 12 순서로 저장
        • 최하위 바이트(LSB)가 가장 낮은 주소에 저장. 0x12345678에서 0x78이 LSB이다.
        • Intel x86, x86-64 프로세서에서 사용

    xlsx의 Byte Order는 Vector사 CAN db++ Editor 신호 정의 화면의 Byte Order 항목이 xlsx의 Byte Order로 보인다.

    • Value Type: 
      • Signed: Signed Integer. 신호 길이에서 부호 (+/-)로 1 비트가 사용된다.  
      • Unsigned: Unsigned Integer
      • Float: (Signed) Float. 32 bit 고정이다.
      • Double: (Signed) Double. 64 bit 고정이다.

    xlsx의 Value Type은 Vector사 CAN db++ Editor 신호 정의 화면의 Value Type으로 보인다.

     

    • Initial Value:
      • 신호 초기값
      • TSMaster에서 RBS (Remaining Bus Simulation, Rest Bus Simulation)를 할 경우, 별도의 연산 함수를 정의/연결하지 않으면 Initial Value가 반복 전송된다. 
    • Factor, 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이다.
    • Comment: 신호에 관한 설명이다.
    • ECU_1, ECU_2:
      • 예제에는 ECU(제어기)가 2개 있는 것으로 가정했다. 
      • 신형 고급 차들은 제어기가 100개가 넘는 경우도 있다. ECU_100 이상 간다.
      • 신호의 송수신 제어기를 표시한다. R: Rx, T: Tx
    • 아래 xlsx를 dbc로 변환하는 파이썬 스크립트를 작성한다.

    xlsx2dbc_example.xlsx
    0.01MB

     

     

     

    xlsx2dbc.py 작성하기

    • 위에 설명한 구조로 된 xlsx을 TSMaster 같은 툴에서 불러오기를 할 수 없다. 그렇게 하기 위해서 dbc로 변환해야 한다.
    • xlsx 파일의 표를 읽기 위해 파이썬의 pandas 모듈을 사용한다.
    • xlsx의 CAN matrix를 dbc로 변환하기 위해 cantools 모듈을 사용한다.
    • 아래 커맨드라인 명령으로 실행할 수 있다.
      • -i (입력 파일 이름)는 필수이다.
      • -o (출력 파일 이름)과 -v (진행 상황을 장황하게 (verbose) 출력)는 선택이다.
    # 기본 모드 (신호 이름만 출력)
    python xlsx2dbc.py -i dbc2xlsx_example.xlsx -o my_output.dbc
    
    # 상세 모드 (신호 상세 정보 모두 출력)
    python xlsx2dbc.py -i dbc2xlsx_example.xlsx -o my_output.dbc -v
    • 코드에 대한 설명은 주석을 참조해 주십시오.

    xlsx2dbc.py
    0.01MB

    #!/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()

     

     

    실행 결과 (생성된 dbc)

    • 위 xlsx를 처리하여 생성된 dbc는 아래와 같다.

    xlsx2dbc_example.dbc
    0.00MB

    • dbc 안에 Networks, ECUs, Netowrk nodes, Messages, Signals가 잘 정의되어 있다.

    dbc에 Networks, ECUs, Network nodes, Messages, Signals가 잘 정의되어 있다.

     

    • 메시지 안에 신호들이 잘 정의되어 있다.

    MSG_2를 보면, 그 안에 신호들이 잘 정의되어 있다.

     

    • MSG_2의 레이아웃을 보면, 신호들이 정의에 따라 잘 배치되어 있다.

    신호들이 메시지 안에 잘 배치되어 있다.

     

     

    결론

    • CAN 매트릭스 xlsx를 dbc로 변환하는 파이썬 스크립트를 작성했다.
    • 위 파이썬 스크립트는 예제이다. 이를 발전시키면 복잡한 xlsx를 dbc로 변환할 수 있을 것이다.

     

     

    목차 :: hsl's tsmaster 사용기