基于《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 设备发现与状态管理
142 lines
4.6 KiB
Python
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"]
|