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