feat: KT mBMS 펌웨어 3종(HW1/2/3) 및 통합 빌드 환경 초기 커밋 (V4.0.0.1)
This commit is contained in:
302
package_multi_fw.py
Normal file
302
package_multi_fw.py
Normal file
@@ -0,0 +1,302 @@
|
||||
#!/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}")
|
||||
|
||||
Reference in New Issue
Block a user