Files
wangfq e97388500b 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
2026-07-30 16:06:58 +08:00

208 lines
6.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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