303 lines
12 KiB
Python
303 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
=============================================================================
|
|
KT mBMS 3종 통합 펌웨어 패키지 생성 스크립트 (Unified Multi-FW Packager)
|
|
=============================================================================
|
|
기능:
|
|
- 3종의 mBMS 펌웨어 이미지(V1, V2, V3)를 하나의 통합 .bin 파일로 병합
|
|
- 패키지 헤더(256 바이트)에 각 펌웨어의 타겟 HW 구분, 버전, 크기, CRC32,
|
|
체크섬(Sum32), 오프셋 메타데이터 패킹
|
|
- GUI(LFP_Manager_GL)에서 통합 bin 파일을 읽어 보드 버전 확인 및 자동 다운로드 지원
|
|
|
|
작성일자: 2026-10-23
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import struct
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 상 수 정 의
|
|
# ---------------------------------------------------------------------------
|
|
MULTI_FW_MAGIC = 0x4D4D4657 # 'MMFW' (Multi mBMS FirmWare)
|
|
HEADER_VERSION = 0x0001 # 헤더 버전 1.0
|
|
TOTAL_HEADER_SIZE = 256 # 전체 헤더 크기 (64B Master + 64B * 3 Entries)
|
|
ENTRY_COUNT = 3
|
|
|
|
HW_TYPE_V1 = 0x11 # STM32F407 + BQ76PL455 + Relay
|
|
HW_TYPE_V2 = 0x22 # HC32F460 + BQ76940 + FET
|
|
HW_TYPE_V3 = 0x33 # HC32F460 + ADBMS1818 + FET
|
|
|
|
POLYNOMIAL_CRC32 = 0x04C11DB7
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CRC32 및 CheckSum 계산기 (GUI의 C# csMultiFwPackage와 100% 동일)
|
|
# ---------------------------------------------------------------------------
|
|
_crc_table = None
|
|
|
|
def init_crc_table():
|
|
global _crc_table
|
|
if _crc_table is not None:
|
|
return
|
|
_crc_table = []
|
|
for i in range(256):
|
|
crc = (i << 24) & 0xFFFFFFFF
|
|
for _ in range(8):
|
|
if crc & 0x80000000:
|
|
crc = ((crc << 1) ^ POLYNOMIAL_CRC32) & 0xFFFFFFFF
|
|
else:
|
|
crc = (crc << 1) & 0xFFFFFFFF
|
|
_crc_table.append(crc)
|
|
|
|
def calculate_crc32(data: bytes) -> int:
|
|
init_crc_table()
|
|
crc = 0
|
|
for b in data:
|
|
idx = ((crc >> 24) ^ b) & 0xFF
|
|
crc = (((crc << 8) & 0xFFFFFFFF) ^ _crc_table[idx]) & 0xFFFFFFFF
|
|
return crc
|
|
|
|
def calculate_checksum32(data: bytes) -> int:
|
|
return sum(data) & 0xFFFFFFFF
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 패키지 빌더
|
|
# ---------------------------------------------------------------------------
|
|
def build_multi_fw_package(
|
|
v1_bin_path: str,
|
|
v2_bin_path: str,
|
|
v3_bin_path: str,
|
|
output_bin_path: str,
|
|
version: tuple = (4, 0, 0, 1),
|
|
package_name: str = "mBMS_KT_UNIFIED_FW_V4.0.0.1"
|
|
):
|
|
print("=" * 70)
|
|
print(" KT mBMS 통합 펌웨어 패키지 생성 시작")
|
|
print("=" * 70)
|
|
|
|
# 1. 각 바이너리 파일 로드 (보드 버전 판별 규칙: x1xx -> V1, x2xx -> V2, x3xx -> V3)
|
|
inputs = [
|
|
{
|
|
"hw_type": HW_TYPE_V1,
|
|
"hw_name": "HW1:STM32+PL455", # x1xx (V1)
|
|
"path": v1_bin_path,
|
|
"data": b""
|
|
},
|
|
{
|
|
"hw_type": HW_TYPE_V2,
|
|
"hw_name": "HW2:HC32+BQ76940", # x2xx (V2)
|
|
"path": v2_bin_path,
|
|
"data": b""
|
|
},
|
|
{
|
|
"hw_type": HW_TYPE_V3,
|
|
"hw_name": "HW3:HC32+ADBMS18", # x3xx (V3)
|
|
"path": v3_bin_path,
|
|
"data": b""
|
|
}
|
|
]
|
|
|
|
for item in inputs:
|
|
if not os.path.exists(item["path"]):
|
|
raise FileNotFoundError(f"펌웨어 파일이 존재하지 않습니다: {item['path']}")
|
|
with open(item["path"], "rb") as f:
|
|
item["data"] = f.read()
|
|
item["size"] = len(item["data"])
|
|
item["crc32"] = calculate_crc32(item["data"])
|
|
item["chksum"] = calculate_checksum32(item["data"])
|
|
item["filename"] = os.path.basename(item["path"])
|
|
print(f"[*] {item['hw_name']:<18}: {item['filename']} ({item['size']:,} bytes)")
|
|
print(f" - CRC32: 0x{item['crc32']:08X}, CheckSum(Sum32): 0x{item['chksum']:08X}")
|
|
|
|
# 2. 오프셋 계산
|
|
current_offset = TOTAL_HEADER_SIZE
|
|
for item in inputs:
|
|
item["offset"] = current_offset
|
|
current_offset += item["size"]
|
|
|
|
total_package_size = current_offset
|
|
print("-" * 70)
|
|
print(f"[*] 통합 헤더 크기 : {TOTAL_HEADER_SIZE} bytes")
|
|
print(f"[*] 최종 패키지 크기 : {total_package_size:,} bytes")
|
|
|
|
# 3. 마스터 헤더 (64 바이트) 패킹
|
|
# 구조:
|
|
# Magic (4B, uint32)
|
|
# HeaderVersion (2B, uint16)
|
|
# ImageCount (2B, uint16)
|
|
# TotalPackageSize (4B, uint32)
|
|
# HeaderCrc32 (4B, uint32) - 임시 0
|
|
# PackageVer (4B, 4 bytes)
|
|
# PackageName (32B, char[32])
|
|
# Reserved (12B, 12 bytes)
|
|
pkg_ver_bytes = bytes([version[0], version[1], version[2], version[3]])
|
|
pkg_name_bytes = package_name.encode("ascii")[:31].ljust(32, b"\x00")
|
|
reserved12 = b"\x00" * 12
|
|
|
|
master_header = struct.pack(
|
|
"<IIHHII4s32s12s",
|
|
MULTI_FW_MAGIC,
|
|
HEADER_VERSION,
|
|
ENTRY_COUNT,
|
|
0, # TotalPackageSize (포맷 통일을 위해 아래서 정합)
|
|
0, # HeaderCrc32 (임시)
|
|
total_package_size, # TotalPackageSize
|
|
pkg_ver_bytes,
|
|
pkg_name_bytes,
|
|
reserved12
|
|
)
|
|
# C# 구조체:
|
|
# UInt32 Magic; (4B)
|
|
# UInt16 HeaderVersion; (2B)
|
|
# UInt16 ImageCount; (2B)
|
|
# UInt32 TotalPackageSize; (4B)
|
|
# UInt32 HeaderCrc32; (4B)
|
|
# byte[4] PackageVer; (4B)
|
|
# byte[32] PackageName; (32B)
|
|
# byte[12] Reserved; (12B)
|
|
master_header = struct.pack(
|
|
"<IHHII4s32s12s",
|
|
MULTI_FW_MAGIC,
|
|
HEADER_VERSION,
|
|
ENTRY_COUNT,
|
|
total_package_size,
|
|
0, # HeaderCrc32 (추후 계산)
|
|
pkg_ver_bytes,
|
|
pkg_name_bytes,
|
|
reserved12
|
|
)
|
|
assert len(master_header) == 64, f"Master header size mismatch: {len(master_header)}"
|
|
|
|
# 4. 서브 펌웨어 엔트리 3개 (각 64 바이트, 총 192 바이트) 패킹
|
|
# C# 구조체:
|
|
# byte HwType; (1B)
|
|
# byte HwSubType; (1B)
|
|
# UInt16 EntryFlags; (2B)
|
|
# byte[16] HwName; (16B)
|
|
# byte[4] FwVer; (4B)
|
|
# UInt32 FileOffset; (4B)
|
|
# UInt32 ImageSize; (4B)
|
|
# UInt32 Crc32; (4B)
|
|
# UInt32 CheckSum; (4B)
|
|
# byte[20] OrigFileName; (20B)
|
|
# byte[4] Reserved; (4B)
|
|
entries_bytes = bytearray()
|
|
for item in inputs:
|
|
hw_name_bytes = item["hw_name"].encode("ascii")[:15].ljust(16, b"\x00")
|
|
fn_bytes = item["filename"].encode("ascii")[:19].ljust(20, b"\x00")
|
|
entry = struct.pack(
|
|
"<BBH16s4sIIII20s4s",
|
|
item["hw_type"],
|
|
0, # HwSubType
|
|
0x0001, # EntryFlags (Active)
|
|
hw_name_bytes,
|
|
pkg_ver_bytes, # FwVer
|
|
item["offset"],
|
|
item["size"],
|
|
item["crc32"],
|
|
item["chksum"],
|
|
fn_bytes,
|
|
b"\x00" * 4 # Reserved
|
|
)
|
|
assert len(entry) == 64, f"Entry size mismatch: {len(entry)}"
|
|
entries_bytes.extend(entry)
|
|
|
|
# 5. 헤더 결합 및 헤더 CRC32 계산
|
|
header_block = bytearray(master_header + entries_bytes)
|
|
assert len(header_block) == TOTAL_HEADER_SIZE, f"Total header size mismatch: {len(header_block)}"
|
|
|
|
header_crc = calculate_crc32(bytes(header_block))
|
|
# HeaderCrc32 필드는 offset 12 (4 + 2 + 2 + 4)
|
|
struct.pack_into("<I", header_block, 12, header_crc)
|
|
|
|
# 6. 최종 파일 쓰기 (헤더 + V1 데이터 + V2 데이터 + V3 데이터)
|
|
os.makedirs(os.path.dirname(os.path.abspath(output_bin_path)), exist_ok=True)
|
|
with open(output_bin_path, "wb") as f_out:
|
|
f_out.write(header_block)
|
|
for item in inputs:
|
|
f_out.write(item["data"])
|
|
|
|
print("-" * 70)
|
|
print(f"[SUCCESS] 통합 bin 파일 생성 완료:")
|
|
print(f" ▶ 파일 경로: {output_bin_path}")
|
|
print(f" ▶ 파일 크기: {os.path.getsize(output_bin_path):,} bytes")
|
|
print(f" ▶ 헤더 CRC : 0x{header_crc:08X}")
|
|
print("=" * 70)
|
|
|
|
# 7. 자체 검증 테스트 (Self-Verification)
|
|
verify_package(output_bin_path)
|
|
|
|
def verify_package(package_path: str):
|
|
print("\n[검증 테스트] 생성된 통합 bin 파일 검증 중...")
|
|
with open(package_path, "rb") as f:
|
|
data = f.read()
|
|
|
|
assert len(data) >= TOTAL_HEADER_SIZE, "파일 크기가 헤더보다 작습니다."
|
|
magic, hdr_ver, img_cnt, total_sz, hdr_crc = struct.unpack_from("<IHHII", data, 0)
|
|
|
|
assert magic == MULTI_FW_MAGIC, f"Magic mismatch: 0x{magic:08X} != 0x{MULTI_FW_MAGIC:08X}"
|
|
assert hdr_ver == HEADER_VERSION, f"Header version mismatch: {hdr_ver}"
|
|
assert img_cnt == ENTRY_COUNT, f"Image count mismatch: {img_cnt}"
|
|
assert total_sz == len(data), f"Total size mismatch: {total_sz} != {len(data)}"
|
|
|
|
for i in range(img_cnt):
|
|
offset = 64 + (i * 64)
|
|
hw_type, hw_sub, flags, hw_name, fw_ver, f_offset, img_sz, crc, chksum, fn, _ = struct.unpack_from(
|
|
"<BBH16s4sIIII20s4s", data, offset
|
|
)
|
|
hw_name_str = hw_name.decode("ascii").rstrip("\x00")
|
|
fn_str = fn.decode("ascii").rstrip("\x00")
|
|
ver_str = f"V{fw_ver[0]}.{fw_ver[1]}.{fw_ver[2]}.{fw_ver[3]}"
|
|
|
|
fw_payload = data[f_offset : f_offset + img_sz]
|
|
calc_crc = calculate_crc32(fw_payload)
|
|
calc_sum = calculate_checksum32(fw_payload)
|
|
|
|
assert calc_crc == crc, f"CRC32 mismatch on entry {i}: 0x{calc_crc:08X} != 0x{crc:08X}"
|
|
assert calc_sum == chksum, f"CheckSum mismatch on entry {i}: 0x{calc_sum:08X} != 0x{chksum:08X}"
|
|
|
|
print(f" [OK] 엔트리 #{i+1}: HW 0x{hw_type:02X} ({hw_name_str}) - {ver_str}, {img_sz:,}B, CRC 검증 통과")
|
|
|
|
print("[SUCCESS] 패키지 무결성 및 바이너리 검증 100% 완료!\n")
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 메인 진입점
|
|
# ---------------------------------------------------------------------------
|
|
if __name__ == "__main__":
|
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
# 기본 경로 설정 (V1, V2, V3 모두 V4001 적용)
|
|
v1_path = os.path.join(base_dir, "mBMS_APP_V3117", "USER", "FLASH", "mBMS_APP_V4001.bin")
|
|
if not os.path.exists(v1_path):
|
|
v1_path = os.path.join(base_dir, "mBMS_APP_V3117", "USER", "FLASH", "mBMS_APP_V3117.bin")
|
|
v2_path = os.path.join(base_dir, "APP_R3221", "App", "MDK", "output", "mBMS2_KT_APP_V4001.bin")
|
|
if not os.path.exists(v2_path):
|
|
v2_path = os.path.join(base_dir, "APP_R3221", "App", "MDK", "output", "mBMS2_KT_APP_V3221.bin")
|
|
v3_path = os.path.join(base_dir, "APP_R3323", "App", "MDK", "output", "mBMS3_KT_APP_V4001.bin")
|
|
|
|
# 출력 경로: 루트 디렉토리 (mBMS_KT_V4001.bin)
|
|
root_out_path = os.path.join(base_dir, "mBMS_KT_V4001.bin")
|
|
|
|
build_multi_fw_package(
|
|
v1_bin_path=v1_path,
|
|
v2_bin_path=v2_path,
|
|
v3_bin_path=v3_path,
|
|
output_bin_path=root_out_path,
|
|
version=(4, 0, 0, 1),
|
|
package_name="mBMS_KT_UNIFIED_FW_V4.0.0.1"
|
|
)
|
|
|
|
import shutil
|
|
# GUI 디렉토리가 남아있을 경우에만 복사
|
|
gui_rel_bin = os.path.join(base_dir, "gui", "LFP_Manager_GL(V3.3.0.16)_AG", "LFP_Manager", "bin", "Release", "mBMS_KT_V4001.bin")
|
|
if os.path.exists(os.path.dirname(gui_rel_bin)):
|
|
shutil.copyfile(root_out_path, gui_rel_bin)
|
|
print(f"[*] GUI Release 디렉토리 복사 완료: {gui_rel_bin}")
|
|
|
|
debug_path = os.path.join(base_dir, "gui", "LFP_Manager_GL(V3.3.0.16)_AG", "LFP_Manager", "bin", "Debug", "mBMS_KT_V4001.bin")
|
|
if os.path.exists(os.path.dirname(debug_path)):
|
|
shutil.copyfile(root_out_path, debug_path)
|
|
print(f"[*] GUI Debug 디렉토리 복사 완료: {debug_path}")
|
|
|