부트로더는 됐고, 이제 PC 쪽 업로더.

python-can 라이브러리 사용.


설치

pip install python-can

PCAN-USB 쓰면:

pip install python-can[pcan]

기본 구조

import can
import struct
import time
from pathlib import Path

class BMSUploader:
    CAN_ID_TX = 0x5FF
    CAN_ID_RX = 0x5FE
    PAGE_SIZE = 2048
    
    def __init__(self, interface='pcan', channel='PCAN_USBBUS1', bitrate=500000):
        self.bus = can.interface.Bus(
            interface=interface,
            channel=channel,
            bitrate=bitrate
        )
    
    def send(self, data):
        msg = can.Message(arbitration_id=self.CAN_ID_TX, data=data, is_extended_id=False)
        self.bus.send(msg)
    
    def recv(self, timeout=5.0):
        msg = self.bus.recv(timeout=timeout)
        if msg and msg.arbitration_id == self.CAN_ID_RX:
            return msg.data
        return None

연결 및 인증

def connect(self):
    # CONNECT 전송
    self.send([0x30])
    
    # CHALLENGE 대기
    resp = self.recv()
    if resp is None or resp[0] != 0x40:
        raise Exception("연결 실패")
    
    seed = struct.unpack('<I', bytes(resp[1:5]))[0]
    print(f"Seed: 0x{seed:08X}")
    
    # Key 계산
    key = self.calc_key(seed)
    print(f"Key: 0x{key:08X}")
    
    # AUTH 전송
    auth_data = [0x31] + list(struct.pack('<I', key))
    self.send(auth_data)
    
    # AUTH_OK 대기
    resp = self.recv()
    if resp is None or resp[0] != 0x41:
        raise Exception("인증 실패")
    
    print("인증 성공")

def calc_key(self, seed):
    # 간단한 XOR (실제론 더 복잡하게)
    return seed ^ 0x12345678

펌웨어 업로드

def upload(self, filepath):
    data = Path(filepath).read_bytes()
    fw_size = len(data)
    total_pages = (fw_size + self.PAGE_SIZE - 1) // self.PAGE_SIZE
    
    print(f"펌웨어 크기: {fw_size} bytes ({total_pages} pages)")
    
    # SIZE_REQ 대기
    resp = self.recv()
    if resp[0] != 0x42:
        raise Exception("크기 요청 안 옴")
    
    # SIZE 응답
    size_data = [0x32] + list(struct.pack('<I', fw_size))
    self.send(size_data)
    
    # 페이지 전송
    for page_num in range(total_pages):
        self.send_page(data, page_num, fw_size)
        progress = (page_num + 1) * 100 // total_pages
        print(f"\r진행률: {progress}%", end='')
    
    print("\n업로드 완료")

페이지 전송

def send_page(self, data, page_num, fw_size):
    # PAGE_REQ 대기
    resp = self.recv(timeout=10)
    if resp is None or resp[0] != 0x43:
        raise Exception(f"페이지 요청 안 옴: {resp}")
    
    start = page_num * self.PAGE_SIZE
    end = min(start + self.PAGE_SIZE, fw_size)
    page_data = data[start:end]
    
    # CRC 계산
    crc = self.crc32(page_data)
    
    # PAGE_START
    self.send([0x33, page_num & 0xFF, (page_num >> 8) & 0xFF])
    
    # 데이터 전송 (8바이트씩)
    for i in range(0, len(page_data), 8):
        chunk = page_data[i:i+8]
        self.send(list(chunk))
        time.sleep(0.001)  # 1ms 딜레이
    
    # PAGE_END + CRC
    end_data = [0x34] + list(struct.pack('<I', crc))
    self.send(end_data)
    
    # PAGE_ACK 대기
    resp = self.recv(timeout=10)
    if resp is None or resp[0] != 0x44:
        raise Exception(f"페이지 ACK 실패: {resp}")

CRC32

import binascii

def crc32(self, data):
    return binascii.crc32(data) & 0xFFFFFFFF

Python binascii가 IEEE CRC32 사용.


메인

if __name__ == '__main__':
    import sys
    
    if len(sys.argv) < 2:
        print("Usage: python uploader.py firmware.bin")
        sys.exit(1)
    
    uploader = BMSUploader()
    
    try:
        uploader.connect()
        uploader.upload(sys.argv[1])
    except Exception as e:
        print(f"에러: {e}")
    finally:
        uploader.bus.shutdown()

실행

python uploader.py app.bin
Seed: 0xA5B6C7D8
Key: 0xB783F1A0
인증 성공
펌웨어 크기: 51234 bytes (26 pages)
진행률: 100%
업로드 완료

다음 글에서 테스트 및 검증.

#18 - 테스트 및 검증