refactor(DBNMQTTool): tkinter → PySide6

- main.py: 完整重写为 PySide6 (Qt for Python)
- requirements.txt: 加 PySide6>=6.6.0
- 使用 Qt 信号/槽替代 tkinter 回调
- 界面更现代化,Fusion 风格
- 分组框 (QGroupBox) 布局,QSplitter 分栏
This commit is contained in:
wangfq
2026-07-06 21:19:55 +08:00
parent a2cfe4602c
commit e1bf3dcfca
+341 -286
View File
@@ -1,283 +1,346 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
DBN MQTT Tool — DLD960 IoT MQTT 设备管理工具 DBN MQTT Tool — DLD960 IoT MQTT 设备管理工具
跨平台桌面软件 (PySide6)
跨平台桌面软件 (tkinter)
""" """
import sys import sys
import os import os
import json import json
import time
import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext
from threading import Thread
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
# 确保项目路径可导入
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QTreeWidget, QTreeWidgetItem,
QTabWidget, QTextEdit, QGroupBox, QGridLayout, QCheckBox,
QSplitter, QMessageBox, QHeaderView, QFrame,
)
from PySide6.QtCore import Qt, QTimer, Signal, Slot
from PySide6.QtGui import QFont, QColor, QTextCursor
from dbn_mqtt_tool.mqtt_client import MqttClient, BrokerConfig from dbn_mqtt_tool.mqtt_client import MqttClient, BrokerConfig
from dbn_mqtt_tool.device_manager import DeviceManager, DeviceInfo from dbn_mqtt_tool.device_manager import DeviceManager
from dbn_mqtt_tool.protocol import ( from dbn_mqtt_tool.protocol import (
CMD_DEV_INFO_QUERY, CMD_SSC_NET_QUERY, CMD_IOT_NET_QUERY, CMD_DEV_INFO_QUERY, CMD_SSC_NET_QUERY, CMD_IOT_NET_QUERY,
CMD_IOT_TOPIC_QUERY, CMD_LOOP_PARAM_QUERY, CMD_IOT_TOPIC_QUERY, CMD_LOOP_PARAM_QUERY,
CMD_SSC_NET_SET, CMD_IOT_NET_SET, CMD_IOT_TOPIC_SET, CMD_SSC_NET_SET, CMD_IOT_NET_SET, CMD_IOT_TOPIC_SET,
CMD_LOOP_PARAM_SET, CMD_REPORT_CONFIG,
CMD_PWD_VERIFY, CMD_PWD_SET, CMD_FACTORY_RESET, CMD_DEVICE_RESET, CMD_PWD_VERIFY, CMD_PWD_SET, CMD_FACTORY_RESET, CMD_DEVICE_RESET,
CMD_DEV_SERIAL_SET, ERROR_MSGS,
CMD_LOOP_DATA, CMD_EVENT_REPORT, CMD_HEARTBEAT,
ERROR_MSGS, FREQ_LEVELS, OUTPUT_MODES,
data_ssc_net_set, data_iot_net_set, data_iot_topic_set, data_ssc_net_set, data_iot_net_set, data_iot_topic_set,
data_loop_param_set, data_report_config, data_pwd_verify, data_pwd_set,
data_pwd_verify, data_pwd_set, data_dev_serial_set,
topic_data_loop, topic_data_event, topic_status,
) )
class DBNMqttTool(tk.Tk): class MainWindow(QMainWindow):
"""主窗口""" _mqtt_status = Signal(bool, str)
_mqtt_msg = Signal(str, str, object)
_devices_changed = Signal()
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.setWindowTitle("DBN MQTT Tool — DLD960 IoT 设备管理")
self.title("DBN MQTT Tool — DLD960 IoT 设备管理") self.resize(1280, 800)
self.geometry("1200x750") self.setMinimumSize(960, 640)
self.minsize(900, 600)
self._mqtt = MqttClient() self._mqtt = MqttClient()
self._devmgr = DeviceManager() self._devmgr = DeviceManager()
# MQTT 回调 self._mqtt.on_status_change(lambda c, m: self._mqtt_status.emit(c, m))
self._mqtt.on_status_change(self._on_mqtt_status) self._mqtt.on_message(lambda t, s, p: self._mqtt_msg.emit(t, s, p))
self._mqtt.on_message(self._on_mqtt_message) self._devmgr.on_change(lambda: self._devices_changed.emit())
# 设备列表变化 self._mqtt_status.connect(self._on_status)
self._devmgr.on_change(self._refresh_device_list) self._mqtt_msg.connect(self._on_message)
self._devices_changed.connect(self._refresh_devices)
self._pending: dict[int, str] = {} # msg_id → 命令名
self._log_lines: list[str] = []
self._build_ui() self._build_ui()
self._log("DBN MQTT Tool 已启动")
# ================================================================ # ================================================================
# UI 构建 # UI 构建
# ================================================================ # ================================================================
def _build_ui(self): def _build_ui(self):
# -- 顶栏:连接 -- central = QWidget()
top_bar = ttk.Frame(self, padding=5) self.setCentralWidget(central)
top_bar.pack(fill=tk.X) root = QVBoxLayout(central)
root.setContentsMargins(8, 8, 8, 8)
root.setSpacing(6)
ttk.Label(top_bar, text="Broker:").pack(side=tk.LEFT, padx=(0, 5)) # -- 顶栏 --
self._host_var = tk.StringVar(value="121.37.20.199") root.addWidget(self._build_top_bar())
ttk.Entry(top_bar, textvariable=self._host_var, width=16).pack(side=tk.LEFT, padx=2)
ttk.Label(top_bar, text="Port:").pack(side=tk.LEFT, padx=(5, 2))
self._port_var = tk.IntVar(value=1883)
ttk.Entry(top_bar, textvariable=self._port_var, width=6).pack(side=tk.LEFT, padx=2)
ttk.Label(top_bar, text="用户:").pack(side=tk.LEFT, padx=(5, 2))
self._user_var = tk.StringVar()
ttk.Entry(top_bar, textvariable=self._user_var, width=10).pack(side=tk.LEFT, padx=2)
ttk.Label(top_bar, text="密码:").pack(side=tk.LEFT, padx=(5, 2))
self._pass_var = tk.StringVar()
ttk.Entry(top_bar, textvariable=self._pass_var, width=10, show="*").pack(side=tk.LEFT, padx=2)
self._btn_connect = ttk.Button(top_bar, text="连接", command=self._toggle_connect) # -- 主体 --
self._btn_connect.pack(side=tk.LEFT, padx=10) splitter = QSplitter(Qt.Horizontal)
self._conn_status = ttk.Label(top_bar, text="● 未连接", foreground="red") left = self._build_device_panel()
self._conn_status.pack(side=tk.LEFT, padx=10) splitter.addWidget(left)
# -- 主体:左右分栏 -- right = QTabWidget()
main = ttk.PanedWindow(self, orient=tk.HORIZONTAL) right.addTab(self._build_info_tab(), "设备信息")
main.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) right.addTab(self._build_config_tab(), "参数配置")
right.addTab(self._build_data_tab(), "实时数据")
right.addTab(self._build_log_tab(), "日志")
splitter.addWidget(right)
# 左侧:设备列表 splitter.setSizes([280, 960])
left = ttk.Frame(main, width=280) root.addWidget(splitter, 1)
main.add(left, weight=0)
ttk.Label(left, text="设备列表", font=("", 11, "bold")).pack(anchor=tk.W, pady=(0, 5)) def _build_top_bar(self) -> QWidget:
self._dev_tree = ttk.Treeview(left, columns=("model",), show="tree headings", height=14) bar = QWidget()
self._dev_tree.heading("#0", text="设备序列码") layout = QHBoxLayout(bar)
self._dev_tree.heading("model", text="型号") layout.setContentsMargins(0, 0, 0, 0)
self._dev_tree.column("#0", width=140) layout.setSpacing(6)
self._dev_tree.column("model", width=80)
self._dev_tree.pack(fill=tk.BOTH, expand=True)
self._dev_tree.bind("<<TreeviewSelect>>", self._on_device_select)
btn_frame = ttk.Frame(left) layout.addWidget(QLabel("Broker:"))
btn_frame.pack(fill=tk.X, pady=5) self._host_edit = QLineEdit("121.37.20.199")
ttk.Button(btn_frame, text="查询设备信息", command=self._query_dev_info).pack(side=tk.LEFT, padx=2) self._host_edit.setMaximumWidth(140)
ttk.Button(btn_frame, text="查询全部", command=self._query_all).pack(side=tk.LEFT, padx=2) layout.addWidget(self._host_edit)
# 右侧:标签页 layout.addWidget(QLabel("Port:"))
right = ttk.Frame(main) self._port_edit = QLineEdit("1883")
main.add(right, weight=1) self._port_edit.setMaximumWidth(60)
layout.addWidget(self._port_edit)
self._notebook = ttk.Notebook(right) layout.addWidget(QLabel("用户:"))
self._notebook.pack(fill=tk.BOTH, expand=True) self._user_edit = QLineEdit()
self._user_edit.setMaximumWidth(90)
layout.addWidget(self._user_edit)
self._tab_info = self._add_info_tab() layout.addWidget(QLabel("密码:"))
self._tab_config = self._add_config_tab() self._pass_edit = QLineEdit()
self._tab_data = self._add_data_tab() self._pass_edit.setMaximumWidth(90)
self._tab_log = self._add_log_tab() self._pass_edit.setEchoMode(QLineEdit.Password)
layout.addWidget(self._pass_edit)
def _add_info_tab(self): self._btn_conn = QPushButton("连接")
f = ttk.Frame(self._notebook, padding=10) self._btn_conn.setFixedWidth(70)
self._notebook.add(f, text="设备信息") self._btn_conn.clicked.connect(self._toggle_connect)
layout.addWidget(self._btn_conn)
self._info_text = scrolledtext.ScrolledText(f, height=18, width=60, state=tk.DISABLED, self._status_label = QLabel("● 未连接")
font=("Consolas", 10)) self._status_label.setStyleSheet("color: red; font-weight: bold;")
self._info_text.pack(fill=tk.BOTH, expand=True) layout.addWidget(self._status_label)
return f
def _add_config_tab(self): layout.addStretch()
f = ttk.Frame(self._notebook, padding=10) return bar
self._notebook.add(f, text="参数配置")
row = 0 def _build_device_panel(self) -> QWidget:
# -- 网口配置 -- w = QWidget()
ttk.Label(f, text="▸ SSC 网络配置", font=("", 10, "bold")).grid(row=row, column=0, columnspan=4, sticky=tk.W, pady=(0, 5)) layout = QVBoxLayout(w)
row += 1 layout.setContentsMargins(0, 0, 0, 0)
for label, var_name, col in [
("设备 IP", "ssc_ip", 0), ("子网掩码", "ssc_mask", 1),
("网关", "ssc_gw", 2), ("LSSC IP", "ssc_lssc", 3),
("DNS", "ssc_dns", 0), ("端口", "ssc_port", 1),
]:
ttk.Label(f, text=label + ":").grid(row=row, column=col * 2, sticky=tk.E, padx=2, pady=1)
var = tk.StringVar()
setattr(self, f"_var_{var_name}", var)
ttk.Entry(f, textvariable=var, width=15).grid(row=row, column=col * 2 + 1, sticky=tk.W, padx=2, pady=1)
if col == 3 or (col == 1 and var_name == "ssc_port"):
row += 1
ttk.Button(f, text="查询 SSC 网络", command=self._query_ssc_net).grid(row=row, column=0, pady=5, padx=2, sticky=tk.W) layout.addWidget(QLabel("<b>设备列表</b>"))
ttk.Button(f, text="设置 SSC 网络", command=self._set_ssc_net).grid(row=row, column=1, pady=5, padx=2, sticky=tk.W)
row += 1
# -- IoT 配置 -- self._dev_tree = QTreeWidget()
ttk.Separator(f, orient=tk.HORIZONTAL).grid(row=row, column=0, columnspan=4, sticky=tk.EW, pady=10) self._dev_tree.setHeaderLabels(["设备序列码", "型号"])
row += 1 self._dev_tree.setRootIsDecorated(False)
ttk.Label(f, text="▸ IoT 网络配置", font=("", 10, "bold")).grid(row=row, column=0, columnspan=4, sticky=tk.W, pady=(0, 5)) self._dev_tree.header().setStretchLastSection(True)
row += 1 self._dev_tree.itemSelectionChanged.connect(self._on_dev_select)
for label, var_name in [ layout.addWidget(self._dev_tree, 1)
btn_row = QHBoxLayout()
btn_query = QPushButton("查询设备信息")
btn_query.clicked.connect(self._query_dev_info)
btn_row.addWidget(btn_query)
btn_all = QPushButton("查询全部")
btn_all.clicked.connect(self._query_all)
btn_row.addWidget(btn_all)
layout.addLayout(btn_row)
return w
def _build_info_tab(self) -> QWidget:
w = QWidget()
layout = QVBoxLayout(w)
self._info_text = QTextEdit()
self._info_text.setReadOnly(True)
self._info_text.setFont(QFont("Consolas", 10))
layout.addWidget(self._info_text)
return w
def _build_config_tab(self) -> QWidget:
w = QWidget()
layout = QVBoxLayout(w)
# -- SSC 网络 --
g1 = QGroupBox("SSC 网络配置")
g1_layout = QGridLayout(g1)
fields_ssc = [
("设备 IP", "ssc_ip"), ("子网掩码", "ssc_mask"),
("网关", "ssc_gw"), ("LSSC IP", "ssc_lssc"),
("DNS", "ssc_dns"), ("端口", "ssc_port"),
]
for i, (label, name) in enumerate(fields_ssc):
g1_layout.addWidget(QLabel(label + ":"), i // 2, (i % 2) * 2)
edit = QLineEdit()
edit.setMaximumWidth(140)
g1_layout.addWidget(edit, i // 2, (i % 2) * 2 + 1)
setattr(self, f"_edit_{name}", edit)
btn_row = QHBoxLayout()
b1 = QPushButton("查询")
b1.clicked.connect(self._query_ssc_net)
b2 = QPushButton("设置")
b2.clicked.connect(self._set_ssc_net)
btn_row.addWidget(b1)
btn_row.addWidget(b2)
btn_row.addStretch()
g1_layout.addLayout(btn_row, 3, 0, 1, 4)
layout.addWidget(g1)
# -- IoT 网络 --
g2 = QGroupBox("IoT 网络配置")
g2_layout = QGridLayout(g2)
fields_iot = [
("MQTT Host", "iot_host"), ("MQTT Port", "iot_port"), ("MQTT Host", "iot_host"), ("MQTT Port", "iot_port"),
("Client ID", "iot_cid"), ("用户名", "iot_user"), ("密码", "iot_pass"), ("Client ID", "iot_cid"), ("用户名", "iot_user"),
("密码", "iot_pass"),
]
for i, (label, name) in enumerate(fields_iot):
g2_layout.addWidget(QLabel(label + ":"), i, 0)
edit = QLineEdit()
edit.setMinimumWidth(200)
if "pass" in name:
edit.setEchoMode(QLineEdit.Password)
g2_layout.addWidget(edit, i, 1)
setattr(self, f"_edit_{name}", edit)
btn_row2 = QHBoxLayout()
b3 = QPushButton("查询")
b3.clicked.connect(self._query_iot_net)
b4 = QPushButton("设置")
b4.clicked.connect(self._set_iot_net)
btn_row2.addWidget(b3)
btn_row2.addWidget(b4)
btn_row2.addStretch()
g2_layout.addLayout(btn_row2, len(fields_iot), 0, 1, 2)
layout.addWidget(g2)
# -- Topic --
g3 = QGroupBox("IoT Topic 配置")
g3_layout = QGridLayout(g3)
self._chk_cid = QCheckBox("Client ID 启用")
self._chk_cid.setChecked(True)
g3_layout.addWidget(self._chk_cid, 0, 0, 1, 2)
g3_layout.addWidget(QLabel("Topic 发布:"), 1, 0)
self._edit_topic_pub = QLineEdit()
self._edit_topic_pub.setMinimumWidth(250)
g3_layout.addWidget(self._edit_topic_pub, 1, 1)
g3_layout.addWidget(QLabel("Topic 订阅:"), 2, 0)
self._edit_topic_sub = QLineEdit()
g3_layout.addWidget(self._edit_topic_sub, 2, 1)
btn_row3 = QHBoxLayout()
b5 = QPushButton("查询")
b5.clicked.connect(self._query_iot_topic)
b6 = QPushButton("设置")
b6.clicked.connect(self._set_iot_topic)
btn_row3.addWidget(b5)
btn_row3.addWidget(b6)
btn_row3.addStretch()
g3_layout.addLayout(btn_row3, 3, 0, 1, 2)
layout.addWidget(g3)
# -- 控制 --
g4 = QGroupBox("控制命令")
g4_layout = QGridLayout(g4)
g4_layout.addWidget(QLabel("密码:"), 0, 0)
self._edit_pwd = QLineEdit()
self._edit_pwd.setMaximumWidth(100)
self._edit_pwd.setEchoMode(QLineEdit.Password)
g4_layout.addWidget(self._edit_pwd, 0, 1)
g4_layout.addWidget(QLabel("新密码:"), 0, 2)
self._edit_new_pwd = QLineEdit()
self._edit_new_pwd.setMaximumWidth(100)
self._edit_new_pwd.setEchoMode(QLineEdit.Password)
g4_layout.addWidget(self._edit_new_pwd, 0, 3)
btn_row4 = QHBoxLayout()
for label, slot in [
("验证密码", self._do_pwd_verify), ("设置密码", self._do_pwd_set),
("出厂初始化", self._do_factory_reset), ("设备复位", self._do_device_reset),
]: ]:
ttk.Label(f, text=label + ":").grid(row=row, column=0, sticky=tk.E, padx=2, pady=1) btn = QPushButton(label)
var = tk.StringVar() btn.clicked.connect(slot)
setattr(self, f"_var_{var_name}", var) btn_row4.addWidget(btn)
w = ttk.Entry(f, textvariable=var, width=30) btn_row4.addStretch()
if "pass" in var_name.lower(): g4_layout.addLayout(btn_row4, 1, 0, 1, 4)
w.configure(show="*") layout.addWidget(g4)
w.grid(row=row, column=1, columnspan=3, sticky=tk.W, padx=2, pady=1)
row += 1
ttk.Button(f, text="查询 IoT 网络", command=self._query_iot_net).grid(row=row, column=0, pady=5, padx=2, sticky=tk.W) layout.addStretch()
ttk.Button(f, text="设置 IoT 网络", command=self._set_iot_net).grid(row=row, column=1, pady=5, padx=2, sticky=tk.W) return w
row += 1
# -- Topic 配置 -- def _build_data_tab(self) -> QWidget:
ttk.Separator(f, orient=tk.HORIZONTAL).grid(row=row, column=0, columnspan=4, sticky=tk.EW, pady=10) w = QWidget()
row += 1 layout = QVBoxLayout(w)
ttk.Label(f, text="▸ IoT Topic 配置", font=("", 10, "bold")).grid(row=row, column=0, columnspan=4, sticky=tk.W, pady=(0, 5))
row += 1
self._var_cid_enable = tk.BooleanVar(value=True)
ttk.Checkbutton(f, text="Client ID 启用", variable=self._var_cid_enable).grid(row=row, column=0, columnspan=2, sticky=tk.W)
row += 1
for label, var_name in [("Topic 发布", "topic_pub"), ("Topic 订阅", "topic_sub")]:
ttk.Label(f, text=label + ":").grid(row=row, column=0, sticky=tk.E, padx=2, pady=1)
var = tk.StringVar()
setattr(self, f"_var_{var_name}", var)
ttk.Entry(f, textvariable=var, width=40).grid(row=row, column=1, columnspan=3, sticky=tk.W, padx=2, pady=1)
row += 1
ttk.Button(f, text="查询 Topic", command=self._query_iot_topic).grid(row=row, column=0, pady=5, padx=2, sticky=tk.W) layout.addWidget(QLabel("<b>线圈传感数据 (loop_data)</b>"))
ttk.Button(f, text="设置 Topic", command=self._set_iot_topic).grid(row=row, column=1, pady=5, padx=2, sticky=tk.W) self._loop_text = QTextEdit()
row += 1 self._loop_text.setReadOnly(True)
self._loop_text.setFont(QFont("Consolas", 10))
layout.addWidget(self._loop_text, 2)
# -- 控制区 -- layout.addWidget(QLabel("<b>事件上报 (event_report)</b>"))
ttk.Separator(f, orient=tk.HORIZONTAL).grid(row=row, column=0, columnspan=4, sticky=tk.EW, pady=10) self._event_text = QTextEdit()
row += 1 self._event_text.setReadOnly(True)
ttk.Label(f, text="▸ 控制命令", font=("", 10, "bold")).grid(row=row, column=0, columnspan=4, sticky=tk.W, pady=(0, 5)) self._event_text.setFont(QFont("Consolas", 10))
row += 1 layout.addWidget(self._event_text, 1)
self._var_pwd = tk.StringVar() return w
ttk.Label(f, text="密码:").grid(row=row, column=0, sticky=tk.E, padx=2)
ttk.Entry(f, textvariable=self._var_pwd, width=10, show="*").grid(row=row, column=1, sticky=tk.W, padx=2)
self._var_new_pwd = tk.StringVar()
ttk.Label(f, text="新密码:").grid(row=row, column=2, sticky=tk.E, padx=2)
ttk.Entry(f, textvariable=self._var_new_pwd, width=10, show="*").grid(row=row, column=3, sticky=tk.W, padx=2)
row += 1
ttk.Button(f, text="验证密码", command=self._do_pwd_verify).grid(row=row, column=0, pady=5, padx=2)
ttk.Button(f, text="设置密码", command=self._do_pwd_set).grid(row=row, column=1, pady=5, padx=2)
ttk.Button(f, text="出厂初始化", command=self._do_factory_reset).grid(row=row, column=2, pady=5, padx=2)
ttk.Button(f, text="设备复位", command=self._do_device_reset).grid(row=row, column=3, pady=5, padx=2)
return f def _build_log_tab(self) -> QWidget:
w = QWidget()
layout = QVBoxLayout(w)
self._log_text = QTextEdit()
self._log_text.setReadOnly(True)
self._log_text.setFont(QFont("Consolas", 9))
layout.addWidget(self._log_text, 1)
def _add_data_tab(self): btn_clear = QPushButton("清空")
f = ttk.Frame(self._notebook, padding=10) btn_clear.clicked.connect(self._log_text.clear)
self._notebook.add(f, text="实时数据") layout2 = QHBoxLayout()
layout2.addStretch()
# 线圈数据 layout2.addWidget(btn_clear)
ttk.Label(f, text="▸ 线圈传感数据 (loop_data)", font=("", 10, "bold")).pack(anchor=tk.W) layout.addLayout(layout2)
self._loop_text = scrolledtext.ScrolledText(f, height=10, width=80, state=tk.DISABLED, return w
font=("Consolas", 10))
self._loop_text.pack(fill=tk.BOTH, expand=True, pady=5)
# 事件
ttk.Label(f, text="▸ 事件上报 (event_report)", font=("", 10, "bold")).pack(anchor=tk.W)
self._event_text = scrolledtext.ScrolledText(f, height=6, width=80, state=tk.DISABLED,
font=("Consolas", 10))
self._event_text.pack(fill=tk.BOTH, expand=True, pady=5)
return f
def _add_log_tab(self):
f = ttk.Frame(self._notebook, padding=10)
self._notebook.add(f, text="日志")
self._log_text = scrolledtext.ScrolledText(f, height=18, width=60, state=tk.DISABLED,
font=("Consolas", 9))
self._log_text.pack(fill=tk.BOTH, expand=True)
ttk.Button(f, text="清空", command=self._clear_log).pack(anchor=tk.E, pady=5)
return f
# ================================================================ # ================================================================
# MQTT 操作 # MQTT
# ================================================================ # ================================================================
def _toggle_connect(self): def _toggle_connect(self):
if self._mqtt.connected: if self._mqtt.connected:
self._mqtt.disconnect() self._mqtt.disconnect()
else: else:
config = BrokerConfig( cfg = BrokerConfig(
host=self._host_var.get(), host=self._host_edit.text(),
port=self._port_var.get(), port=int(self._port_edit.text() or "1883"),
username=self._user_var.get(), username=self._user_edit.text(),
password=self._pass_var.get(), password=self._pass_edit.text(),
) )
self._mqtt.connect(config) self._mqtt.connect(cfg)
self._log(f"正在连接 {config.host}:{config.port} ...") self._log(f"正在连接 {cfg.host}:{cfg.port} ...")
def _on_mqtt_status(self, connected: bool, msg: str): @Slot(bool, str)
self.after(0, lambda: self._update_conn_status(connected, msg)) def _on_status(self, connected: bool, msg: str):
def _update_conn_status(self, connected: bool, msg: str):
if connected: if connected:
self._btn_connect.configure(text="断开") self._btn_conn.setText("断开")
self._conn_status.configure(text="● 已连接", foreground="green") self._status_label.setText("● 已连接")
self._status_label.setStyleSheet("color: green; font-weight: bold;")
else: else:
self._btn_connect.configure(text="连接") self._btn_conn.setText("连接")
self._conn_status.configure(text="● 未连接", foreground="red") self._status_label.setText("● 未连接")
self._status_label.setStyleSheet("color: red; font-weight: bold;")
self._log(msg) self._log(msg)
def _on_mqtt_message(self, topic: str, dev_serial: str, payload: dict): @Slot(str, str, object)
self.after(0, lambda: self._handle_message(topic, dev_serial, payload)) def _on_message(self, topic: str, dev_serial: str, payload: dict):
def _handle_message(self, topic: str, dev_serial: str, payload: dict):
cmd = payload.get("cmd", "") cmd = payload.get("cmd", "")
ts = datetime.fromtimestamp(payload.get("ts", 0)).strftime("%H:%M:%S") ts = datetime.fromtimestamp(payload.get("ts", 0)).strftime("%H:%M:%S")
@@ -289,26 +352,25 @@ class DBNMqttTool(tk.Tk):
if code == 0: if code == 0:
if cmd == CMD_DEV_INFO_QUERY: if cmd == CMD_DEV_INFO_QUERY:
self._devmgr.update_from_dev_info(dev_serial, data) self._devmgr.update_from_dev_info(dev_serial, data)
self._show_info(data) self._show_json(data)
elif cmd in (CMD_SSC_NET_QUERY, CMD_IOT_NET_QUERY, CMD_IOT_TOPIC_QUERY): elif cmd in (CMD_SSC_NET_QUERY, CMD_IOT_NET_QUERY, CMD_IOT_TOPIC_QUERY, CMD_LOOP_PARAM_QUERY):
self._show_info({cmd: data}) self._show_json({cmd: data})
elif cmd == CMD_LOOP_PARAM_QUERY:
self._show_info({cmd: data})
else: else:
self._show_info({"error": f"code={code} {pmsg} ({ERROR_MSGS.get(code, '?')})"}) self._show_json({"error": f"code={code} {pmsg} ({ERROR_MSGS.get(code, '?')})"})
elif "/dev/data/loop" in topic: elif "/dev/data/loop" in topic:
data = payload.get("data", {}) data = payload.get("data", {})
self._devmgr.update_loop_data(dev_serial, data) self._devmgr.update_loop_data(dev_serial, data)
self._log(f"[{ts}] {dev_serial} ← loop_data ({len(data.get('channels', []))} 通道)") chs = len(data.get("channels", []))
self._append_data(self._loop_text, json.dumps(payload, indent=2, ensure_ascii=False)) self._log(f"[{ts}] {dev_serial} ← loop_data ({chs} 通道)")
self._append_text(self._loop_text, json.dumps(payload, indent=2, ensure_ascii=False))
elif "/dev/data/event" in topic: elif "/dev/data/event" in topic:
data = payload.get("data", {}) data = payload.get("data", {})
self._devmgr.update_event(dev_serial, data) self._devmgr.update_event(dev_serial, data)
events = data.get("events", []) evt_count = len(data.get("events", []))
self._log(f"[{ts}] {dev_serial} ← event_report ({len(events)} 事件)") self._log(f"[{ts}] {dev_serial} ← event_report ({evt_count} 事件)")
self._append_data(self._event_text, json.dumps(payload, indent=2, ensure_ascii=False)) self._append_text(self._event_text, json.dumps(payload, indent=2, ensure_ascii=False))
elif "/dev/status" in topic: elif "/dev/status" in topic:
data = payload.get("data", {}) data = payload.get("data", {})
@@ -320,18 +382,16 @@ class DBNMqttTool(tk.Tk):
# ================================================================ # ================================================================
def _selected_dev(self) -> Optional[str]: def _selected_dev(self) -> Optional[str]:
sel = self._dev_tree.selection() items = self._dev_tree.selectedItems()
if sel: return items[0].text(0).lstrip("●○ ") if items else None
return self._dev_tree.item(sel[0], "text")
return None
def _require_dev(self) -> Optional[str]: def _require_dev(self) -> Optional[str]:
sn = self._selected_dev() sn = self._selected_dev()
if not sn: if not sn:
messagebox.showwarning("提示", "请先在左侧选择一个设备") QMessageBox.warning(self, "提示", "请先选择一个设备")
return None return None
if not self._mqtt.connected: if not self._mqtt.connected:
messagebox.showwarning("提示", "请先连接 MQTT Broker") QMessageBox.warning(self, "提示", "请先连接 MQTT Broker")
return None return None
return sn return sn
@@ -340,10 +400,10 @@ class DBNMqttTool(tk.Tk):
if not sn: if not sn:
return return
try: try:
msg_id = self._mqtt.send_command(sn, cmd, data) mid = self._mqtt.send_command(sn, cmd, data)
self._log(f"{sn} {cmd} (msg_id={msg_id})") self._log(f"{sn} {cmd} (msg_id={mid})")
except Exception as e: except Exception as e:
messagebox.showerror("发送失败", str(e)) QMessageBox.critical(self, "发送失败", str(e))
def _query_dev_info(self): def _query_dev_info(self):
self._send_cmd(CMD_DEV_INFO_QUERY) self._send_cmd(CMD_DEV_INFO_QUERY)
@@ -364,12 +424,12 @@ class DBNMqttTool(tk.Tk):
def _set_ssc_net(self): def _set_ssc_net(self):
self._send_cmd(CMD_SSC_NET_SET, data_ssc_net_set( self._send_cmd(CMD_SSC_NET_SET, data_ssc_net_set(
dev_ip=self._var_ssc_ip.get(), dev_ip=self._edit_ssc_ip.text(),
subnet_mask=self._var_ssc_mask.get(), subnet_mask=self._edit_ssc_mask.text(),
route_ip=self._var_ssc_gw.get(), route_ip=self._edit_ssc_gw.text(),
lssc_ip=self._var_ssc_lssc.get(), lssc_ip=self._edit_ssc_lssc.text(),
dns=self._var_ssc_dns.get(), dns=self._edit_ssc_dns.text(),
port=int(self._var_ssc_port.get() or "0"), port=int(self._edit_ssc_port.text() or "0"),
)) ))
def _query_iot_net(self): def _query_iot_net(self):
@@ -377,11 +437,11 @@ class DBNMqttTool(tk.Tk):
def _set_iot_net(self): def _set_iot_net(self):
self._send_cmd(CMD_IOT_NET_SET, data_iot_net_set( self._send_cmd(CMD_IOT_NET_SET, data_iot_net_set(
host=self._var_iot_host.get(), host=self._edit_iot_host.text(),
port=int(self._var_iot_port.get() or "1883"), port=int(self._edit_iot_port.text() or "1883"),
client_id=self._var_iot_cid.get(), client_id=self._edit_iot_cid.text(),
username=self._var_iot_user.get(), username=self._edit_iot_user.text(),
password=self._var_iot_pass.get(), password=self._edit_iot_pass.text(),
)) ))
def _query_iot_topic(self): def _query_iot_topic(self):
@@ -389,60 +449,67 @@ class DBNMqttTool(tk.Tk):
def _set_iot_topic(self): def _set_iot_topic(self):
self._send_cmd(CMD_IOT_TOPIC_SET, data_iot_topic_set( self._send_cmd(CMD_IOT_TOPIC_SET, data_iot_topic_set(
client_id_enable=self._var_cid_enable.get(), client_id_enable=self._chk_cid.isChecked(),
topic_pub=self._var_topic_pub.get(), topic_pub=self._edit_topic_pub.text(),
topic_sub=self._var_topic_sub.get(), topic_sub=self._edit_topic_sub.text(),
)) ))
def _do_pwd_verify(self): def _do_pwd_verify(self):
pwd = self._var_pwd.get() pwd = self._edit_pwd.text()
if not pwd: if not pwd:
messagebox.showwarning("提示", "请输入密码") QMessageBox.warning(self, "提示", "请输入密码")
return return
self._send_cmd(CMD_PWD_VERIFY, data_pwd_verify(pwd)) self._send_cmd(CMD_PWD_VERIFY, data_pwd_verify(pwd))
def _do_pwd_set(self): def _do_pwd_set(self):
old = self._var_pwd.get() old = self._edit_pwd.text()
new = self._var_new_pwd.get() new = self._edit_new_pwd.text()
if not old or not new: if not old or not new:
messagebox.showwarning("提示", "请输入旧密码和新密码") QMessageBox.warning(self, "提示", "请输入旧密码和新密码")
return return
self._send_cmd(CMD_PWD_SET, data_pwd_set(old, new)) self._send_cmd(CMD_PWD_SET, data_pwd_set(old, new))
def _do_factory_reset(self): def _do_factory_reset(self):
if not messagebox.askyesno("确认", "确定要恢复出厂设置吗?此操作不可撤销!"): r = QMessageBox.question(self, "确认", "确定要恢复出厂设置吗?\n此操作不可撤销!",
return QMessageBox.Yes | QMessageBox.No)
self._send_cmd(CMD_FACTORY_RESET) if r == QMessageBox.Yes:
self._send_cmd(CMD_FACTORY_RESET)
def _do_device_reset(self): def _do_device_reset(self):
if not messagebox.askyesno("确认", "确定要复位设备吗?设备将重启。"): r = QMessageBox.question(self, "确认", "确定要复位设备吗?设备将重启。",
return QMessageBox.Yes | QMessageBox.No)
self._send_cmd(CMD_DEVICE_RESET) if r == QMessageBox.Yes:
self._send_cmd(CMD_DEVICE_RESET)
# ================================================================ # ================================================================
# UI 更新 # UI 更新
# ================================================================ # ================================================================
def _refresh_device_list(self): @Slot()
def _refresh_devices(self):
tree = self._dev_tree tree = self._dev_tree
current = set(tree.get_children("")) existing = {}
devices = self._devmgr.devices for i in range(tree.topLevelItemCount()):
item = tree.topLevelItem(i)
sn = item.text(0).lstrip("●○ ")
existing[sn] = item
for sn, dev in devices.items(): for sn, dev in self._devmgr.devices.items():
iid = sn
status = "" if dev.online else "" status = "" if dev.online else ""
display = f"{status} {sn}"
model = dev.model or "?" model = dev.model or "?"
if iid in current: if sn in existing:
tree.item(iid, text=display, values=(model,)) existing[sn].setText(0, f"{status} {sn}")
current.discard(iid) existing[sn].setText(1, model)
del existing[sn]
else: else:
tree.insert("", tk.END, iid=iid, text=display, values=(model,)) item = QTreeWidgetItem([f"{status} {sn}", model])
tree.addTopLevelItem(item)
for stale in current: for sn, item in existing.items():
tree.delete(stale) idx = tree.indexOfTopLevelItem(item)
tree.takeTopLevelItem(idx)
def _on_device_select(self, event): def _on_dev_select(self):
sn = self._selected_dev() sn = self._selected_dev()
if sn and sn in self._devmgr.devices: if sn and sn in self._devmgr.devices:
dev = self._devmgr.devices[sn] dev = self._devmgr.devices[sn]
@@ -458,40 +525,28 @@ class DBNMqttTool(tk.Tk):
"online": dev.online, "online": dev.online,
"last_seen": dev.last_seen.strftime("%Y-%m-%d %H:%M:%S") if dev.last_seen else "", "last_seen": dev.last_seen.strftime("%Y-%m-%d %H:%M:%S") if dev.last_seen else "",
} }
self._show_info(info) self._show_json(info)
def _show_info(self, data: dict): def _show_json(self, data: dict):
self._info_text.configure(state=tk.NORMAL) self._info_text.setPlainText(json.dumps(data, indent=2, ensure_ascii=False))
self._info_text.delete("1.0", tk.END)
self._info_text.insert(tk.END, json.dumps(data, indent=2, ensure_ascii=False))
self._info_text.configure(state=tk.DISABLED)
def _append_data(self, widget: scrolledtext.ScrolledText, text: str): def _append_text(self, widget: QTextEdit, text: str):
widget.configure(state=tk.NORMAL)
ts = datetime.now().strftime("%H:%M:%S") ts = datetime.now().strftime("%H:%M:%S")
widget.insert(tk.END, f"--- {ts} ---\n{text}\n\n") widget.append(f"--- {ts} ---\n{text}\n")
widget.see(tk.END) widget.moveCursor(QTextCursor.End)
widget.configure(state=tk.DISABLED)
def _log(self, msg: str): def _log(self, msg: str):
ts = datetime.now().strftime("%H:%M:%S") ts = datetime.now().strftime("%H:%M:%S")
line = f"[{ts}] {msg}" self._log_text.append(f"[{ts}] {msg}")
self._log_lines.append(line) self._log_text.moveCursor(QTextCursor.End)
self._log_text.configure(state=tk.NORMAL)
self._log_text.insert(tk.END, line + "\n")
self._log_text.see(tk.END)
self._log_text.configure(state=tk.DISABLED)
def _clear_log(self):
self._log_text.configure(state=tk.NORMAL)
self._log_text.delete("1.0", tk.END)
self._log_text.configure(state=tk.DISABLED)
self._log_lines.clear()
def main(): def main():
app = DBNMqttTool() app = QApplication(sys.argv)
app.mainloop() app.setStyle("Fusion")
win = MainWindow()
win.show()
sys.exit(app.exec())
if __name__ == "__main__": if __name__ == "__main__":