Files
vd_960/DBNMQTTool/dbn_mqtt_tool/mqtt_client.py
T
wangfq baa82c895d fix: MqttClient 重构为 QObject — publish 通过 Qt 信号排队避免 C 层崩溃
问题: paho-mqtt publish() C 扩展在 Windows 上从 GUI 线程调用时静默崩溃。

修复:
- MqttClient 改为 QObject 子类
- publish() 通过 Qt Signal(Qt.QueuedConnection) 排队,_do_publish()
  始终在主线程事件循环中执行 paho 调用
- 状态/消息通知改用 Qt Signal 跨线程传递
- 新增 subscribe()/unsubscribe() 公开 API,消除 main.py 直接
  访问 _client 的脆弱代码
- 移除 MainWindow 中已不再需要的中间信号 _mqtt_status/_mqtt_msg
2026-07-07 14:25:49 +08:00

151 lines
5.1 KiB
Python

"""
MQTT 客户端封装
"""
import json
import time
import threading
from typing import Callable, Optional
from dataclasses import dataclass
import paho.mqtt.client as mqtt
from PySide6.QtCore import QObject, Signal, Slot, Qt
from .protocol import parse_topic_dev_serial
@dataclass
class BrokerConfig:
host: str = "121.37.20.199"
port: int = 1883
username: str = ""
password: str = ""
client_id: str = ""
keepalive: int = 60
class MqttClient(QObject):
"""DLD960 IoT MQTT 客户端 — QObject 基类确保信号线程安全"""
# 供外部监听
_status_notify = Signal(bool, str) # connected, message
_msg_notify = Signal(str, str, object) # topic, dev_serial, payload_dict
# 内部线程安全的 publish 队列
_publish_sig = Signal(str, str, int) # topic, json_data, qos
def __init__(self):
super().__init__()
self._client: Optional[mqtt.Client] = None
self._config = BrokerConfig()
self._connected = False
self._lock = threading.Lock()
# 信号→槽: publish 通过排队连接确保在主线程的 paho context 执行
self._publish_sig.connect(self._do_publish, Qt.QueuedConnection)
@property
def connected(self) -> bool:
return self._connected
# ---- 公开回调 (兼容旧接口) ----
def on_status_change(self, callback: Callable[[bool, str], None]):
self._status_notify.connect(callback)
def on_message(self, callback: Callable[[str, str, dict], None]):
"""回调参数: (topic, dev_serial, payload_dict)"""
self._msg_notify.connect(callback)
# ---- MQTT callbacks (paho 网络线程) ----
def _on_connect(self, client, userdata, flags, reason_code, properties=None):
rc = reason_code if isinstance(reason_code, int) else reason_code.value
if rc == 0:
self._connected = True
self._status_notify.emit(True, "已连接到 Broker")
from .protocol import TOPIC_ALL_DEVICE_UP
client.subscribe(TOPIC_ALL_DEVICE_UP, qos=1)
else:
self._connected = False
self._status_notify.emit(False, f"连接失败 (rc={rc})")
def _on_disconnect(self, client, userdata, *args):
self._connected = False
self._status_notify.emit(False, "已断开连接")
def _on_message(self, client, userdata, msg: mqtt.MQTTMessage):
try:
payload = json.loads(msg.payload.decode("utf-8"))
except (json.JSONDecodeError, UnicodeDecodeError):
payload = {"_raw": msg.payload.hex()}
dev_serial = parse_topic_dev_serial(msg.topic) or ""
self._msg_notify.emit(msg.topic, dev_serial, payload)
# ---- Public API ----
def connect(self, config: Optional[BrokerConfig] = None):
if config:
self._config = config
if self._client:
self.disconnect()
client_id = self._config.client_id or f"dbn_mqtt_tool_{int(time.time())}"
self._client = mqtt.Client(
client_id=client_id,
protocol=mqtt.MQTTv311,
callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
)
if self._config.username:
self._client.username_pw_set(self._config.username, self._config.password)
self._client.on_connect = self._on_connect
self._client.on_disconnect = self._on_disconnect
self._client.on_message = self._on_message
self._client.connect_async(self._config.host, self._config.port, self._config.keepalive)
self._client.loop_start()
def disconnect(self):
if self._client:
self._client.loop_stop()
self._client.disconnect()
self._client = None
self._connected = False
def publish(self, topic: str, payload: dict, qos: int = 1):
"""线程安全的发布 — 通过 Qt 信号排队到主线程执行"""
if not self._client or not self._connected:
raise ConnectionError("MQTT 未连接")
data = json.dumps(payload, ensure_ascii=False)
self._publish_sig.emit(topic, data, qos)
def subscribe(self, topic: str, qos: int = 1):
"""订阅主题"""
if self._client:
self._client.subscribe(topic, qos=qos)
def unsubscribe(self, topic: str):
"""取消订阅"""
if self._client:
self._client.unsubscribe(topic)
@Slot(str, str, int)
def _do_publish(self, topic: str, data: str, qos: int):
"""实际的 paho publish 调用 — 始终在主线程执行"""
if self._client and self._connected:
try:
self._client.publish(topic, data, qos=qos)
except Exception as e:
print(f"[MqttClient] publish error: {e}")
def send_command(self, dev_serial: str, cmd: str, data: Optional[dict] = None, qos: int = 1) -> int:
"""向设备发送命令,返回 msg_id"""
from .protocol import build_request, get_topic_for_cmd
msg = build_request(cmd, data)
topic = get_topic_for_cmd(cmd, dev_serial)
self.publish(topic, msg, qos=qos)
return msg["msg_id"]