7 文件 1331 行, 支持 DLD154Pro 双协议: - 700BS: 状态/频率/拨码/复位查询 + 自动轮询 - Modbus RTU: FC 0x02/0x03/0x04/0x06/0x10/0x11 + 全数据块(0x0100)美化解析 + 主动上报控制 - PySide6 GUI, 4 Tab, 后台串口线程 从独立仓库 wangfq/DLD154Tool 合并入 DLD154Pro monorepo
101 lines
3.3 KiB
Python
101 lines
3.3 KiB
Python
"""
|
|
serial_client.py — RS485/串口抽象层
|
|
|
|
封装 pyserial: 端口扫描/打开/关闭/收发, 线程安全。
|
|
"""
|
|
import threading
|
|
import time
|
|
from dataclasses import dataclass
|
|
from typing import Optional, Callable
|
|
|
|
import serial
|
|
import serial.tools.list_ports
|
|
|
|
|
|
@dataclass
|
|
class SerialConfig:
|
|
port: str = ""
|
|
baudrate: int = 9600
|
|
bytesize: int = serial.EIGHTBITS
|
|
parity: str = serial.PARITY_NONE
|
|
stopbits: int = serial.STOPBITS_ONE
|
|
timeout: float = 0.05 # 读超时
|
|
|
|
|
|
class SerialClient:
|
|
"""串口客户端 — 后台读线程 + 回调通知"""
|
|
|
|
def __init__(self):
|
|
self._ser: Optional[serial.Serial] = None
|
|
self._thread: Optional[threading.Thread] = None
|
|
self._running = False
|
|
self._lock = threading.Lock()
|
|
self._on_data: Optional[Callable[[bytes], None]] = None
|
|
|
|
# ── 静态工具 ──────────────────────────────────
|
|
|
|
@staticmethod
|
|
def list_ports() -> list[str]:
|
|
return [p.device for p in serial.tools.list_ports.comports()]
|
|
|
|
# ── 连接管理 ──────────────────────────────────
|
|
|
|
def connect(self, cfg: SerialConfig) -> None:
|
|
self.disconnect()
|
|
self._ser = serial.Serial(
|
|
port=cfg.port,
|
|
baudrate=cfg.baudrate,
|
|
bytesize=cfg.bytesize,
|
|
parity=cfg.parity,
|
|
stopbits=cfg.stopbits,
|
|
timeout=cfg.timeout,
|
|
)
|
|
self._running = True
|
|
self._thread = threading.Thread(target=self._read_loop, daemon=True)
|
|
self._thread.start()
|
|
|
|
def disconnect(self) -> None:
|
|
self._running = False
|
|
if self._thread and self._thread.is_alive():
|
|
self._thread.join(timeout=1.0)
|
|
with self._lock:
|
|
if self._ser and self._ser.is_open:
|
|
self._ser.close()
|
|
self._ser = None
|
|
|
|
@property
|
|
def is_connected(self) -> bool:
|
|
with self._lock:
|
|
return self._ser is not None and self._ser.is_open
|
|
|
|
# ── 发送 ──────────────────────────────────────
|
|
|
|
def send(self, data: bytes) -> None:
|
|
with self._lock:
|
|
if self._ser and self._ser.is_open:
|
|
self._ser.write(data)
|
|
|
|
# ── 接收回调 ──────────────────────────────────
|
|
|
|
def set_data_callback(self, cb: Optional[Callable[[bytes], None]]) -> None:
|
|
self._on_data = cb
|
|
|
|
# ── 内部读线程 ────────────────────────────────
|
|
|
|
def _read_loop(self) -> None:
|
|
while self._running:
|
|
try:
|
|
with self._lock:
|
|
if not self._ser or not self._ser.is_open:
|
|
break
|
|
waiting = self._ser.in_waiting
|
|
if waiting > 0:
|
|
with self._lock:
|
|
data = self._ser.read(waiting)
|
|
if data and self._on_data:
|
|
self._on_data(data)
|
|
else:
|
|
time.sleep(0.01)
|
|
except (serial.SerialException, OSError):
|
|
time.sleep(0.1)
|