feat: 集成 DLD154Tool — PySide6 桌面串口调试工具

7 文件 1331 行, 支持 DLD154Pro 双协议:
- 700BS: 状态/频率/拨码/复位查询 + 自动轮询
- Modbus RTU: FC 0x02/0x03/0x04/0x06/0x10/0x11
  + 全数据块(0x0100)美化解析 + 主动上报控制
- PySide6 GUI, 4 Tab, 后台串口线程

从独立仓库 wangfq/DLD154Tool 合并入 DLD154Pro monorepo
This commit is contained in:
wangfq
2026-07-30 16:06:58 +08:00
parent df6e38f118
commit e97388500b
7 changed files with 1331 additions and 0 deletions
+207
View File
@@ -0,0 +1,207 @@
"""
protocol_700bs.py — DLD154Pro 700BS 串口协议编解码
帧格式:
主机→从机: STX(1) + ADDR(1) + XOR(1) = 3 字节
从机→主机: 可变长, XOR 校验
命令码:
0xFA 查询有车状态 + 线圈好坏 → RSP 0xF5 (3B)
0x1B 查询线圈频率 → RSP 0x14 (6B, 含 3B Freq + ADDR + XOR)
0x2C 复位初始化线圈 → 无响应
0x3D 查询拨码 DIP1-5 → RSP 0x32 (4B)
0xAA 心跳 (从机主动上报) → 9B 帧
RS485 地址: 031 (5-bit, 帧中 ADDR 低 5 位)
"""
from dataclasses import dataclass, field
from typing import Optional
import struct
# ══════════════════════════════════════════════════
# 协议常量
# ══════════════════════════════════════════════════
CMD_QUERY_STATUS = 0xFA # 查询有车状态
CMD_QUERY_FREQ = 0x1B # 查询频率
CMD_RESET_INIT = 0x2C # 复位初始化
CMD_QUERY_DIP = 0x3D # 查询拨码
RSP_STATUS = 0xF5 # 状态回复
RSP_FREQ = 0x14 # 频率回复
RSP_DIP = 0x32 # 拨码回复
HB_PREAMBLE = 0xAA # 心跳帧头
# ══════════════════════════════════════════════════
# 数据类
# ══════════════════════════════════════════════════
@dataclass
class StatusResponse:
addr: int = 0 # bit4-0
car_present: bool = False # bit7
loop_fault: bool = False # bit5
@property
def summary(self) -> str:
car = "有车" if self.car_present else "无车"
coil = "故障" if self.loop_fault else "正常"
return f"地址={self.addr} {car} 线圈={coil}"
@dataclass
class FreqResponse:
freq_hz: int = 0 # 3 字节频率 (Hz)
addr: int = 0
@property
def summary(self) -> str:
return f"频率={self.freq_hz} Hz (地址={self.addr})"
@dataclass
class DipResponse:
dip: int = 0 # DIP1-5 位图
addr: int = 0
@property
def summary(self) -> str:
sens = self.dip & 0x03
pulse = "脉冲" if (self.dip & 0x04) else "存在"
perm = "永久" if (self.dip & 0x08) else "有限"
delay = "有延时" if (self.dip & 0x10) else "无延时"
return f"灵敏度={sens} {pulse} {perm} {delay} (地址={self.addr})"
@dataclass
class Heartbeat:
init_flag: bool = False # 首次心跳 (A1)
car_present: bool = False # bit7
loop_fault: bool = False # bit5
freq_hz: int = 0 # 3 字节
addr: int = 0
@property
def summary(self) -> str:
init = "首次" if self.init_flag else "周期"
car = "有车" if self.car_present else "无车"
coil = "故障" if self.loop_fault else "正常"
return f"[{init}] 地址={self.addr} {car} 线圈={coil} 频率={self.freq_hz} Hz"
# ══════════════════════════════════════════════════
# 编码 (主机→从机)
# ══════════════════════════════════════════════════
def encode_query_status(addr: int) -> bytes:
"""0xFA — 查询有车状态"""
stx = CMD_QUERY_STATUS
adr = addr & 0x1F
xorr = stx ^ adr
return bytes([stx, adr, xorr])
def encode_query_freq(addr: int) -> bytes:
"""0x1B — 查询频率"""
stx = CMD_QUERY_FREQ
adr = addr & 0x1F
xorr = stx ^ adr
return bytes([stx, adr, xorr])
def encode_reset_init(addr: int) -> bytes:
"""0x2C — 复位初始化 (全地址响应)"""
stx = CMD_RESET_INIT
adr = addr & 0x1F
xorr = stx ^ adr
return bytes([stx, adr, xorr])
def encode_query_dip(addr: int) -> bytes:
"""0x3D — 查询拨码"""
stx = CMD_QUERY_DIP
adr = addr & 0x1F
xorr = stx ^ adr
return bytes([stx, adr, xorr])
# ══════════════════════════════════════════════════
# 解码 (从机→主机)
# ══════════════════════════════════════════════════
def xor_checksum(data: bytes) -> int:
"""XOR 累加校验"""
result = 0
for b in data:
result ^= b
return result & 0xFF
def parse_status(data: bytes) -> Optional[StatusResponse]:
"""解析 0xF5 状态回复 (3 字节: STX DATA XOR)"""
if len(data) < 3 or data[0] != RSP_STATUS:
return None
if xor_checksum(data[:2]) != data[2]:
return None
d = data[1]
return StatusResponse(
addr=d & 0x1F,
car_present=bool(d & 0x80),
loop_fault=bool(d & 0x20),
)
def parse_freq(data: bytes) -> Optional[FreqResponse]:
"""解析 0x14 频率回复 (6 字节: STX FREQ_H FREQ_M FREQ_L ADDR XOR)"""
if len(data) < 6 or data[0] != RSP_FREQ:
return None
if xor_checksum(data[:5]) != data[5]:
return None
freq = (data[1] << 16) | (data[2] << 8) | data[3]
return FreqResponse(freq_hz=freq, addr=data[4] & 0x1F)
def parse_dip(data: bytes) -> Optional[DipResponse]:
"""解析 0x32 拨码回复 (4 字节: STX DIP ADDR XOR)"""
if len(data) < 4 or data[0] != RSP_DIP:
return None
if xor_checksum(data[:3]) != data[3]:
return None
return DipResponse(dip=data[1], addr=data[2] & 0x1F)
def parse_heartbeat(data: bytes) -> Optional[Heartbeat]:
"""解析 0xAA 心跳 (9 字节)"""
if len(data) < 9 or data[0] != HB_PREAMBLE:
return None
if xor_checksum(data[:8]) != data[8]:
return None
init = data[1] # 0xA1=首次, 0xA0=正常
st = data[2]
freq = (data[4] << 16) | (data[5] << 8) | data[6]
return Heartbeat(
init_flag=(init == 0xA1),
car_present=bool(st & 0x80),
loop_fault=bool(st & 0x20),
freq_hz=freq,
addr=data[7] & 0x1F,
)
def parse_frame(data: bytes) -> Optional[object]:
"""自动检测帧类型并解析"""
if not data:
return None
stx = data[0]
parsers = {
RSP_STATUS: parse_status,
RSP_FREQ: parse_freq,
RSP_DIP: parse_dip,
HB_PREAMBLE: parse_heartbeat,
}
parser = parsers.get(stx)
if parser:
return parser(data)
return None