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:
@@ -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 地址: 0–31 (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
|
||||
Reference in New Issue
Block a user