feat: complete assembly mesh relay control path
- synchronize STM8 application and external flash layout updates\n- add assembly bootloader and assembly application test projects\n- add UART1 interrupt receive ring buffer\n- parse JDY-28M Mesh outer frames and source addresses\n- add CRC-16/MODBUS validation\n- implement relay control command 0x01 and response 0x81\n- support unicast and broadcast relay control frames\n- update host protocol, OTA documents, and serial tools
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""合并Bootloader和主程序的Intel HEX文件。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
EOF_RECORD = ":00000001FF"
|
||||
|
||||
|
||||
def read_records(path: Path) -> list[str]:
|
||||
"""读取HEX记录,并去掉输入文件末尾的EOF记录。"""
|
||||
records = []
|
||||
for raw_line in path.read_text(encoding="ascii").splitlines():
|
||||
line = raw_line.strip().upper()
|
||||
if not line:
|
||||
continue
|
||||
if not line.startswith(":"):
|
||||
raise ValueError(f"{path}包含非法HEX记录:{line}")
|
||||
if line != EOF_RECORD:
|
||||
records.append(line)
|
||||
return records
|
||||
|
||||
|
||||
def record_range(record: str) -> tuple[int, int] | None:
|
||||
"""返回数据记录地址范围,用于检查两个镜像是否重叠。"""
|
||||
count = int(record[1:3], 16)
|
||||
record_type = int(record[7:9], 16)
|
||||
if record_type != 0x00 or count == 0:
|
||||
return None
|
||||
address = int(record[3:7], 16)
|
||||
return address, address + count
|
||||
|
||||
|
||||
def merge(output: Path, inputs: list[Path]) -> None:
|
||||
"""合并绝对地址HEX文件,并检查数据区间重叠。"""
|
||||
occupied: list[tuple[int, int, Path]] = []
|
||||
records: list[str] = []
|
||||
for path in inputs:
|
||||
for record in read_records(path):
|
||||
current = record_range(record)
|
||||
if current is not None:
|
||||
start, end = current
|
||||
for old_start, old_end, old_path in occupied:
|
||||
if start < old_end and old_start < end:
|
||||
raise ValueError(
|
||||
f"HEX地址重叠:{path} 0x{start:04X}~0x{end - 1:04X},"
|
||||
f"与 {old_path} 冲突"
|
||||
)
|
||||
occupied.append((start, end, path))
|
||||
records.append(record)
|
||||
output.write_text("\n".join(records + [EOF_RECORD]) + "\n", encoding="ascii")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""解析参数并执行HEX合并。"""
|
||||
parser = argparse.ArgumentParser(description="合并STM8 Bootloader和主程序HEX")
|
||||
parser.add_argument("output", type=Path, help="输出HEX文件")
|
||||
parser.add_argument("inputs", nargs="+", type=Path, help="输入HEX文件")
|
||||
args = parser.parse_args()
|
||||
merge(args.output, args.inputs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+20
-18
@@ -10,7 +10,7 @@
|
||||
|
||||
当前协议约定:
|
||||
- JDY-28M发送外层:41 54 2B AA A1 | DST_H DST_L | A0 | BUSINESS | 0D 0A
|
||||
- 业务帧:SRC_H SRC_L | CMD | LEN | DATA | CRC16_H CRC16_L
|
||||
- 业务帧:CMD | LEN | DATA | CRC16_H CRC16_L
|
||||
- CRC算法:CRC-16/MODBUS
|
||||
- DATA中的多字节数值和CRC均为高字节在前
|
||||
- CMD=0x09每帧最多携带2字节固件数据
|
||||
@@ -46,10 +46,10 @@ except ImportError as exc:
|
||||
# JDY-28M Mesh发送外层固定字段。
|
||||
MESH_TX_PREFIX = bytes.fromhex("41 54 2B AA A1")
|
||||
MESH_DATA_FLAG = 0xA0
|
||||
MESH_TAIL = bytes.fromhex("0D 0A")
|
||||
MESH_TAIL = bytes.fromhex("0A")
|
||||
|
||||
# JDY-28M接收数据时,串口侧看到的固定前缀。
|
||||
MESH_RX_PREFIX = bytes.fromhex("F1 DD 09 00 0A")
|
||||
MESH_RX_PREFIX = bytes.fromhex("F1 DD 09")
|
||||
|
||||
# OTA命令定义。
|
||||
CMD_OTA_START = 0x08
|
||||
@@ -115,7 +115,8 @@ def make_business_frame(source: int, command: int, data: bytes) -> bytes:
|
||||
|
||||
if len(data) > 4:
|
||||
raise ValueError("当前JDY-28M外层20字节限制下,业务DATA不能超过4字节")
|
||||
body = struct.pack(">HBB", source, command, len(data)) + data
|
||||
# JDY-28M外层已经携带发送方和接收方地址,业务帧不再重复嵌入source。
|
||||
body = struct.pack(">BB", command, len(data)) + data
|
||||
crc = crc16_modbus(body)
|
||||
return body + struct.pack(">H", crc)
|
||||
|
||||
@@ -145,27 +146,28 @@ def parse_received_business(frame: bytes) -> tuple[int, int, bytes] | None:
|
||||
"""从JDY-28M接收外层帧中提取业务命令并校验CRC。
|
||||
|
||||
接收格式为:
|
||||
F1 DD 09 00 0A | 外层地址2字节 | BUSINESS | 0D 0A
|
||||
F1 DD 09 | SRC地址2字节 | DST地址2字节 | BUSINESS | 0A
|
||||
|
||||
外层地址字段由模块提供;真正的发送方地址仍取业务帧SRC字段。
|
||||
发送方和接收方地址由JDY-28M外层提供,业务帧不再重复携带地址。
|
||||
"""
|
||||
|
||||
if len(frame) < len(MESH_RX_PREFIX) + 2 + 6 + 2:
|
||||
if len(frame) < len(MESH_RX_PREFIX) + 4 + 4 + 1:
|
||||
return None
|
||||
if not frame.startswith(MESH_RX_PREFIX) or not frame.endswith(MESH_TAIL):
|
||||
return None
|
||||
|
||||
business = frame[len(MESH_RX_PREFIX) + 2:-2]
|
||||
if len(business) < 6:
|
||||
source = struct.unpack(">H", frame[3:5])[0]
|
||||
business = frame[len(MESH_RX_PREFIX) + 4:-1]
|
||||
if len(business) < 4:
|
||||
return None
|
||||
source, command, length = struct.unpack(">HBB", business[:4])
|
||||
if len(business) != 4 + length + 2 or length > 4:
|
||||
command, length = struct.unpack(">BB", business[:2])
|
||||
if len(business) != 2 + length + 2 or length > 4:
|
||||
return None
|
||||
body = business[:4 + length]
|
||||
received_crc = struct.unpack(">H", business[4 + length:])[0]
|
||||
body = business[:2 + length]
|
||||
received_crc = struct.unpack(">H", business[2 + length:])[0]
|
||||
if crc16_modbus(body) != received_crc:
|
||||
return None
|
||||
return source, command, business[4:4 + length]
|
||||
return source, command, business[2:2 + length]
|
||||
|
||||
|
||||
def receive_response(ser: serial.Serial, timeout: float) -> tuple[int, int, bytes]:
|
||||
@@ -192,11 +194,11 @@ def receive_response(ser: serial.Serial, timeout: float) -> tuple[int, int, byte
|
||||
break
|
||||
if start:
|
||||
del buffer[:start]
|
||||
if len(buffer) < len(MESH_RX_PREFIX) + 2 + 4 + 2 + 2:
|
||||
if len(buffer) < len(MESH_RX_PREFIX) + 4 + 4 + 1:
|
||||
break
|
||||
|
||||
data_length = buffer[len(MESH_RX_PREFIX) + 2 + 3]
|
||||
total = len(MESH_RX_PREFIX) + 2 + 4 + data_length + 2 + 2
|
||||
data_length = buffer[len(MESH_RX_PREFIX) + 4 + 1]
|
||||
total = len(MESH_RX_PREFIX) + 4 + 2 + data_length + 2 + 1
|
||||
if data_length > 4:
|
||||
del buffer[0]
|
||||
continue
|
||||
@@ -232,7 +234,7 @@ def wait_upgrade_result(ser: serial.Serial, timeout: float) -> tuple[int, int]:
|
||||
|
||||
0x8B只表示外部Flash固件校验成功。设备随后进入Bootloader搬运固件,
|
||||
重启主程序并在蓝牙初始化完成后广播0x0D。因此这里除了结果码,还要
|
||||
保留业务帧中的SRC地址,以便后续发送0x8D确认。
|
||||
保留JDY-28M外层提供的SRC地址,以便后续发送0x8D确认。
|
||||
|
||||
@return (设备短地址, 升级结果码)。
|
||||
@raises TimeoutError 在指定时间内未收到合法0x0D。
|
||||
|
||||
Reference in New Issue
Block a user