Files
vd_960/DBNMQTTool/dbn_mqtt_tool/mqtt_client.py
T
wangfq a2cfe4602c feat: DBNMQTTool — DLD960 IoT MQTT 设备管理工具 (Python/tkinter 跨平台)
基于《DLD960_IoT_MQTT协议.md》V1.00 实现

功能:
- MQTT Broker 连接管理
- 设备自动发现(订阅 dld960/+/dev/# 通配符)
- 设备信息查询(dev_info_query)
- 网络配置(SSC/IoT TCP)+ Topic 配置
- 实时线圈数据监控(loop_data)+ 事件上报(event_report)
- 心跳监控(heartbeat)
- 控制命令:密码验证/设置、出厂初始化、设备复位

项目结构:
- main.py          主窗口 (tkinter GUI)
- dbn_mqtt_tool/
  - protocol.py    DLD960 IoT MQTT 协议定义
  - mqtt_client.py MQTT 客户端封装 (paho-mqtt)
  - device_manager.py 设备发现与状态管理
2026-07-06 18:14:38 +08:00

142 lines
4.6 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 .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:
"""DLD960 IoT MQTT 客户端"""
def __init__(self):
self._client: Optional[mqtt.Client] = None
self._config = BrokerConfig()
self._connected = False
self._lock = threading.Lock()
self._message_callbacks: list[Callable[[str, str, dict], None]] = []
self._status_callbacks: list[Callable[[bool, str], None]] = []
@property
def connected(self) -> bool:
return self._connected
def on_status_change(self, callback: Callable[[bool, str], None]):
self._status_callbacks.append(callback)
def on_message(self, callback: Callable[[str, str, dict], None]):
"""回调参数: (topic, dev_serial, payload_dict)"""
self._message_callbacks.append(callback)
def _notify_status(self, connected: bool, msg: str):
for cb in self._status_callbacks:
try:
cb(connected, msg)
except Exception:
pass
def _notify_message(self, topic: str, payload: dict):
dev_serial = parse_topic_dev_serial(topic) or ""
for cb in self._message_callbacks:
try:
cb(topic, dev_serial, payload)
except Exception:
pass
# ---- MQTT callbacks ----
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._notify_status(True, "已连接到 Broker")
# 订阅所有设备的上报 topic
from .protocol import (
TOPIC_ALL_CONFIG_RESP, TOPIC_ALL_DATA_LOOP,
TOPIC_ALL_DATA_EVENT, TOPIC_ALL_STATUS
)
client.subscribe(TOPIC_ALL_CONFIG_RESP, qos=1)
client.subscribe(TOPIC_ALL_DATA_LOOP, qos=1)
client.subscribe(TOPIC_ALL_DATA_EVENT, qos=1)
client.subscribe(TOPIC_ALL_STATUS, qos=0)
else:
self._connected = False
self._notify_status(False, f"连接失败 (rc={rc})")
def _on_disconnect(self, client, userdata, flags, reason_code, properties=None):
self._connected = False
self._notify_status(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()}
self._notify_message(msg.topic, 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())}"
# MQTT 5.0 用 CallbackAPIVersion.VERSION2
self._client = mqtt.Client(
client_id=client_id,
protocol=mqtt.MQTTv311,
)
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):
"""发送 JSON 消息到指定 topic"""
if not self._client or not self._connected:
raise ConnectionError("MQTT 未连接")
data = json.dumps(payload, ensure_ascii=False)
self._client.publish(topic, data, qos=qos)
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"]