feat: 完善Mesh继电器OTA升级闭环

This commit is contained in:
十七
2026-11-04 15:41:29 +08:00
parent be8bd7eaec
commit df1aad3d16
27 changed files with 1372 additions and 243 deletions
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""生成写入W25Q80BV的出厂固件合并镜像。
合并镜像布局如下:
0x000000:主程序裸二进制固件
0x002000:出厂固件元数据
脚本会自动读取主程序BIN、计算CRC-16/MODBUS,并把版本号写入输出文件名
和出厂元数据。CH341A打开生成的文件后,从W25Q80BV的0x000000地址开始烧录。
"""
from pathlib import Path
import re
# 主程序Makefile生成的裸二进制文件,已经去除了Bootloader地址空间。
FIRMWARE_PATH = Path("build/mesh_relay.bin")
# 主程序版本定义文件,脚本会自动读取其中的FIRMWARE_VERSION宏。
VERSION_HEADER_PATH = Path("include/app_config.h")
# 出厂固件元数据在W25Q80BV中的固定地址。
FACTORY_METADATA_ADDRESS = 0x002000
def read_firmware_version() -> int:
"""从主程序配置头文件读取FIRMWARE_VERSION宏。"""
if not VERSION_HEADER_PATH.is_file():
raise SystemExit(f"找不到版本配置文件:{VERSION_HEADER_PATH}")
content = VERSION_HEADER_PATH.read_text(encoding="utf-8")
match = re.search(
r"^\s*#define\s+FIRMWARE_VERSION\s+((?:0[xX][0-9A-Fa-f]+)|(?:[0-9]+))",
content,
flags=re.MULTILINE,
)
if match is None:
raise SystemExit(
f"在{VERSION_HEADER_PATH}中找不到FIRMWARE_VERSION宏定义"
)
version = int(match.group(1), 0)
if not 0 <= version <= 0xFFFF:
raise SystemExit(f"FIRMWARE_VERSION超出16位范围:{version}")
return version
def calculate_crc16_modbus(data: bytes) -> int:
"""计算一段数据的CRC-16/MODBUS校验值。
Args:
data: 需要计算CRC的完整固件数据。
Returns:
16位CRC结果。发送和存储时由调用方按高字节、低字节写入。
"""
crc = 0xFFFF
for value in data:
crc ^= value
for _ in range(8):
if crc & 1:
crc = (crc >> 1) ^ 0xA001
else:
crc >>= 1
return crc & 0xFFFF
def build_factory_metadata(firmware: bytes, crc16: int, version: int) -> bytes:
"""按照Bootloader固定布局生成18字节出厂元数据。"""
metadata = bytearray(18)
# FACTORY_METADATA_MAGIC=0x46414331。
# 当前Bootloader兼容的魔数字节布局为31 43 41 46。
metadata[0:4] = bytes([0x31, 0x43, 0x41, 0x46])
# 固件长度、版本号和CRC16统一使用高字节在前。
metadata[4:8] = len(firmware).to_bytes(4, "big")
metadata[8:10] = version.to_bytes(2, "big")
metadata[10:12] = crc16.to_bytes(2, "big")
# 0x0C~0x0D为保留字段,保持0x00。
metadata[14] = 0xA5 # 镜像有效标志。
metadata[15] = 0x01 # 请求Bootloader处理出厂镜像。
metadata[16] = 0x00 # 初始升级结果。
metadata[17] = 0x00 # 保留字段。
return bytes(metadata)
def main() -> None:
"""读取固件、生成出厂镜像并输出烧录文件信息。"""
if not FIRMWARE_PATH.is_file():
raise SystemExit(
f"找不到固件文件:{FIRMWARE_PATH}\n"
"请先在项目根目录执行 make。"
)
firmware = FIRMWARE_PATH.read_bytes()
firmware_version = read_firmware_version()
crc16 = calculate_crc16_modbus(firmware)
metadata = build_factory_metadata(firmware, crc16, firmware_version)
# 输出文件名包含4位十六进制版本号,例如factory_flash_v0100.bin。
output_path = Path(f"factory_flash_v{firmware_version:04X}.bin")
# 文件覆盖到元数据末尾,空白区域填充0xFF,符合NOR Flash擦除状态。
image = bytearray([0xFF] * (FACTORY_METADATA_ADDRESS + len(metadata)))
image[0:len(firmware)] = firmware
image[FACTORY_METADATA_ADDRESS:] = metadata
output_path.write_bytes(image)
print(f"固件长度:{len(firmware)} 字节")
print(f"固件版本:0x{firmware_version:04X}")
print(f"固件CRC16:0x{crc16:04X}")
print(f"输出文件:{output_path}")
print(f"输出大小:{len(image)} 字节")
if __name__ == "__main__":
main()