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 设备发现与状态管理
This commit is contained in:
@@ -0,0 +1,111 @@
|
|||||||
|
"""
|
||||||
|
设备管理器 — 管理已发现的设备及其状态
|
||||||
|
"""
|
||||||
|
|
||||||
|
import threading
|
||||||
|
from typing import Callable, Optional
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DeviceInfo:
|
||||||
|
dev_serial: str
|
||||||
|
model: str = ""
|
||||||
|
hard_ver: str = ""
|
||||||
|
soft_ver: str = ""
|
||||||
|
product_code: str = ""
|
||||||
|
net_enabled: bool = False
|
||||||
|
iot_enabled: bool = False
|
||||||
|
bus: dict = field(default_factory=dict)
|
||||||
|
last_seen: Optional[datetime] = None
|
||||||
|
online: bool = False
|
||||||
|
loop_data: Optional[dict] = None
|
||||||
|
events: list = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceManager:
|
||||||
|
"""管理设备列表和状态"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._devices: dict[str, DeviceInfo] = {}
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self._callbacks: list[Callable] = []
|
||||||
|
|
||||||
|
def on_change(self, callback: Callable):
|
||||||
|
self._callbacks.append(callback)
|
||||||
|
|
||||||
|
def _notify(self):
|
||||||
|
for cb in self._callbacks:
|
||||||
|
try:
|
||||||
|
cb()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_or_create(self, dev_serial: str) -> DeviceInfo:
|
||||||
|
with self._lock:
|
||||||
|
if dev_serial not in self._devices:
|
||||||
|
self._devices[dev_serial] = DeviceInfo(dev_serial=dev_serial)
|
||||||
|
return self._devices[dev_serial]
|
||||||
|
|
||||||
|
def update_from_dev_info(self, dev_serial: str, data: dict):
|
||||||
|
with self._lock:
|
||||||
|
dev = self.get_or_create(dev_serial)
|
||||||
|
dev.dev_serial = data.get("dev_serial", dev_serial)
|
||||||
|
dev.model = data.get("model", dev.model)
|
||||||
|
dev.hard_ver = data.get("hard_ver", dev.hard_ver)
|
||||||
|
dev.soft_ver = data.get("soft_ver", dev.soft_ver)
|
||||||
|
dev.product_code = data.get("product_code", dev.product_code)
|
||||||
|
sc = data.get("sub_code", {})
|
||||||
|
dev.net_enabled = sc.get("net", dev.net_enabled)
|
||||||
|
dev.iot_enabled = sc.get("iot", dev.iot_enabled)
|
||||||
|
dev.bus = data.get("bus", dev.bus)
|
||||||
|
dev.last_seen = datetime.now()
|
||||||
|
dev.online = True
|
||||||
|
self._notify()
|
||||||
|
|
||||||
|
def update_loop_data(self, dev_serial: str, data: dict):
|
||||||
|
with self._lock:
|
||||||
|
dev = self.get_or_create(dev_serial)
|
||||||
|
dev.loop_data = data
|
||||||
|
dev.last_seen = datetime.now()
|
||||||
|
dev.online = True
|
||||||
|
self._notify()
|
||||||
|
|
||||||
|
def update_event(self, dev_serial: str, data: dict):
|
||||||
|
with self._lock:
|
||||||
|
dev = self.get_or_create(dev_serial)
|
||||||
|
events = data.get("events", [])
|
||||||
|
for evt in events:
|
||||||
|
evt["_ts"] = datetime.now()
|
||||||
|
dev.events.insert(0, evt)
|
||||||
|
dev.events = dev.events[:100] # 保留最近 100 条
|
||||||
|
dev.last_seen = datetime.now()
|
||||||
|
dev.online = True
|
||||||
|
self._notify()
|
||||||
|
|
||||||
|
def update_heartbeat(self, dev_serial: str, data: dict):
|
||||||
|
with self._lock:
|
||||||
|
dev = self.get_or_create(dev_serial)
|
||||||
|
dev.last_seen = datetime.now()
|
||||||
|
dev.online = True
|
||||||
|
self._notify()
|
||||||
|
|
||||||
|
def set_offline(self):
|
||||||
|
"""标记所有设备离线(断开连接时调用)"""
|
||||||
|
with self._lock:
|
||||||
|
now = datetime.now()
|
||||||
|
for dev in self._devices.values():
|
||||||
|
if dev.last_seen and (now - dev.last_seen).seconds > 120:
|
||||||
|
dev.online = False
|
||||||
|
self._notify()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def devices(self) -> dict[str, DeviceInfo]:
|
||||||
|
with self._lock:
|
||||||
|
return dict(self._devices)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def online_devices(self) -> list[DeviceInfo]:
|
||||||
|
with self._lock:
|
||||||
|
return [d for d in self._devices.values() if d.online]
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
"""
|
||||||
|
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"]
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
"""
|
||||||
|
DLD960 IoT MQTT 协议定义
|
||||||
|
|
||||||
|
基于《DLD960_IoT_MQTT协议.md》V1.00
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from typing import Optional, Any
|
||||||
|
from dataclasses import dataclass, field, asdict
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Topic 结构
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def topic_config_set(dev_serial: str) -> str:
|
||||||
|
"""服务器下发 — 设置配置"""
|
||||||
|
return f"dld960/{dev_serial}/srv/config/set"
|
||||||
|
|
||||||
|
|
||||||
|
def topic_config_query(dev_serial: str) -> str:
|
||||||
|
"""服务器下发 — 查询配置"""
|
||||||
|
return f"dld960/{dev_serial}/srv/config/query"
|
||||||
|
|
||||||
|
|
||||||
|
def topic_ctrl(dev_serial: str) -> str:
|
||||||
|
"""服务器下发 — 控制命令"""
|
||||||
|
return f"dld960/{dev_serial}/srv/ctrl"
|
||||||
|
|
||||||
|
|
||||||
|
def topic_config_resp(dev_serial: str) -> str:
|
||||||
|
"""设备上报 — 配置响应"""
|
||||||
|
return f"dld960/{dev_serial}/dev/config/resp"
|
||||||
|
|
||||||
|
|
||||||
|
def topic_data_loop(dev_serial: str) -> str:
|
||||||
|
"""设备上报 — 线圈数据"""
|
||||||
|
return f"dld960/{dev_serial}/dev/data/loop"
|
||||||
|
|
||||||
|
|
||||||
|
def topic_data_event(dev_serial: str) -> str:
|
||||||
|
"""设备上报 — 事件"""
|
||||||
|
return f"dld960/{dev_serial}/dev/data/event"
|
||||||
|
|
||||||
|
|
||||||
|
def topic_status(dev_serial: str) -> str:
|
||||||
|
"""设备上报 — 状态/心跳"""
|
||||||
|
return f"dld960/{dev_serial}/dev/status"
|
||||||
|
|
||||||
|
|
||||||
|
# 订阅通配符(监听所有设备)
|
||||||
|
TOPIC_ALL_CONFIG_RESP = "dld960/+/dev/config/resp"
|
||||||
|
TOPIC_ALL_DATA_LOOP = "dld960/+/dev/data/loop"
|
||||||
|
TOPIC_ALL_DATA_EVENT = "dld960/+/dev/data/event"
|
||||||
|
TOPIC_ALL_STATUS = "dld960/+/dev/status"
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 命令枚举
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
CMD_DEV_SERIAL_SET = "dev_serial_set"
|
||||||
|
CMD_DEV_INFO_QUERY = "dev_info_query"
|
||||||
|
CMD_SSC_NET_SET = "ssc_net_set"
|
||||||
|
CMD_SSC_NET_QUERY = "ssc_net_query"
|
||||||
|
CMD_IOT_NET_SET = "iot_net_set"
|
||||||
|
CMD_IOT_NET_QUERY = "iot_net_query"
|
||||||
|
CMD_IOT_TOPIC_SET = "iot_topic_set"
|
||||||
|
CMD_IOT_TOPIC_QUERY = "iot_topic_query"
|
||||||
|
CMD_PWD_VERIFY = "pwd_verify"
|
||||||
|
CMD_PWD_SET = "pwd_set"
|
||||||
|
CMD_FACTORY_RESET = "factory_reset"
|
||||||
|
CMD_DEVICE_RESET = "device_reset"
|
||||||
|
CMD_LOOP_PARAM_SET = "loop_param_set"
|
||||||
|
CMD_LOOP_PARAM_QUERY = "loop_param_query"
|
||||||
|
CMD_REPORT_CONFIG = "report_config"
|
||||||
|
|
||||||
|
# 设备上报
|
||||||
|
CMD_LOOP_DATA = "loop_data"
|
||||||
|
CMD_EVENT_REPORT = "event_report"
|
||||||
|
CMD_HEARTBEAT = "heartbeat"
|
||||||
|
|
||||||
|
# 配置类命令 → topic
|
||||||
|
CONFIG_SET_COMMANDS = {CMD_DEV_SERIAL_SET, CMD_SSC_NET_SET, CMD_IOT_NET_SET, CMD_IOT_TOPIC_SET, CMD_LOOP_PARAM_SET}
|
||||||
|
CONFIG_QUERY_COMMANDS = {CMD_DEV_INFO_QUERY, CMD_SSC_NET_QUERY, CMD_IOT_NET_QUERY, CMD_IOT_TOPIC_QUERY, CMD_LOOP_PARAM_QUERY}
|
||||||
|
CTRL_COMMANDS = {CMD_PWD_VERIFY, CMD_PWD_SET, CMD_FACTORY_RESET, CMD_DEVICE_RESET, CMD_REPORT_CONFIG}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 错误码
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
ERR_SUCCESS = 0
|
||||||
|
ERR_PARAM_ERROR = 1
|
||||||
|
ERR_PWD_FAILED = 2
|
||||||
|
ERR_DEVICE_BUSY = 3
|
||||||
|
ERR_UNSUPPORTED = 4
|
||||||
|
ERR_INTERNAL = 5
|
||||||
|
ERR_DATA_TOO_LONG = 6
|
||||||
|
|
||||||
|
ERROR_MSGS = {
|
||||||
|
0: "成功",
|
||||||
|
1: "参数错误",
|
||||||
|
2: "密码验证失败",
|
||||||
|
3: "设备忙",
|
||||||
|
4: "不支持的命令",
|
||||||
|
5: "内部错误",
|
||||||
|
6: "数据超长",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 频率档位 / 输出模式 / 事件类型
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
FREQ_LEVELS = ["high", "mid_high", "mid_low", "low"]
|
||||||
|
OUTPUT_MODES = ["exist", "enter_pulse", "leave_pulse", "direction"]
|
||||||
|
EVENT_TYPES = ["car_enter", "car_leave", "loop_cut", "loop_restore"]
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 消息构建
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
_msg_id_counter = 0
|
||||||
|
|
||||||
|
def next_msg_id() -> int:
|
||||||
|
global _msg_id_counter
|
||||||
|
_msg_id_counter += 1
|
||||||
|
return _msg_id_counter
|
||||||
|
|
||||||
|
|
||||||
|
def build_request(cmd: str, data: Optional[dict] = None) -> dict:
|
||||||
|
"""构建通用请求消息"""
|
||||||
|
msg: dict[str, Any] = {
|
||||||
|
"msg_id": next_msg_id(),
|
||||||
|
"cmd": cmd,
|
||||||
|
"ts": int(time.time()),
|
||||||
|
}
|
||||||
|
if data is not None:
|
||||||
|
msg["data"] = data
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
def build_response(cmd: str, msg_id: int, code: int = 0, msg_text: str = "success", data: Optional[dict] = None) -> dict:
|
||||||
|
"""构建响应消息"""
|
||||||
|
resp: dict[str, Any] = {
|
||||||
|
"msg_id": msg_id,
|
||||||
|
"cmd": cmd,
|
||||||
|
"ts": int(time.time()),
|
||||||
|
"code": code,
|
||||||
|
"msg": msg_text,
|
||||||
|
}
|
||||||
|
if data is not None:
|
||||||
|
resp["data"] = data
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
def get_topic_for_cmd(cmd: str, dev_serial: str) -> str:
|
||||||
|
"""根据命令获取对应的下发 topic"""
|
||||||
|
if cmd in CONFIG_SET_COMMANDS:
|
||||||
|
return topic_config_set(dev_serial)
|
||||||
|
elif cmd in CONFIG_QUERY_COMMANDS:
|
||||||
|
return topic_config_query(dev_serial)
|
||||||
|
elif cmd in CTRL_COMMANDS:
|
||||||
|
return topic_ctrl(dev_serial)
|
||||||
|
else:
|
||||||
|
return topic_config_set(dev_serial) # 默认
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 特定命令的 data 构建器
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def data_dev_serial_set(dev_serial: str) -> dict:
|
||||||
|
return {"dev_serial": dev_serial}
|
||||||
|
|
||||||
|
|
||||||
|
def data_ssc_net_set(dev_ip: str, subnet_mask: str, route_ip: str,
|
||||||
|
lssc_ip: str, dns: str, port: int) -> dict:
|
||||||
|
return {
|
||||||
|
"dev_ip": dev_ip,
|
||||||
|
"subnet_mask": subnet_mask,
|
||||||
|
"route_ip": route_ip,
|
||||||
|
"lssc_ip": lssc_ip,
|
||||||
|
"dns": dns,
|
||||||
|
"port": port,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def data_iot_net_set(host: str, port: int = 1883,
|
||||||
|
client_id: str = "", username: str = "", password: str = "") -> dict:
|
||||||
|
return {
|
||||||
|
"host": host,
|
||||||
|
"port": port,
|
||||||
|
"client_id": client_id,
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def data_iot_topic_set(client_id_enable: bool, topic_pub: str, topic_sub: str) -> dict:
|
||||||
|
return {
|
||||||
|
"client_id_enable": client_id_enable,
|
||||||
|
"topic_pub": topic_pub,
|
||||||
|
"topic_sub": topic_sub,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def data_pwd_verify(password: str) -> dict:
|
||||||
|
return {"password": password}
|
||||||
|
|
||||||
|
|
||||||
|
def data_pwd_set(old_password: str, new_password: str) -> dict:
|
||||||
|
return {"old_password": old_password, "new_password": new_password}
|
||||||
|
|
||||||
|
|
||||||
|
def data_loop_param_set(channels: list[dict], auto_mode: bool = False) -> dict:
|
||||||
|
return {"auto_mode": auto_mode, "channels": channels}
|
||||||
|
|
||||||
|
|
||||||
|
def data_report_config(sensor_type: int = 12, enable: bool = True,
|
||||||
|
once: bool = False, env_eval: bool = False,
|
||||||
|
interval: int = 5, ack_required: bool = False,
|
||||||
|
timeout: int = 0) -> dict:
|
||||||
|
return {
|
||||||
|
"sensor_type": sensor_type,
|
||||||
|
"enable": enable,
|
||||||
|
"once": once,
|
||||||
|
"env_eval": env_eval,
|
||||||
|
"interval": interval,
|
||||||
|
"ack_required": ack_required,
|
||||||
|
"timeout": timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 解析设备上报
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def parse_topic_dev_serial(topic: str) -> Optional[str]:
|
||||||
|
"""从 topic 中提取设备序列码"""
|
||||||
|
parts = topic.split("/")
|
||||||
|
if len(parts) >= 2 and parts[0] == "dld960":
|
||||||
|
return parts[1]
|
||||||
|
return None
|
||||||
@@ -0,0 +1,498 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
DBN MQTT Tool — DLD960 IoT MQTT 设备管理工具
|
||||||
|
|
||||||
|
跨平台桌面软件 (tkinter)
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 dbn_mqtt_tool.mqtt_client import MqttClient, BrokerConfig
|
||||||
|
from dbn_mqtt_tool.device_manager import DeviceManager, DeviceInfo
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DBNMqttTool(tk.Tk):
|
||||||
|
"""主窗口"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.title("DBN MQTT Tool — DLD960 IoT 设备管理")
|
||||||
|
self.geometry("1200x750")
|
||||||
|
self.minsize(900, 600)
|
||||||
|
|
||||||
|
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._devmgr.on_change(self._refresh_device_list)
|
||||||
|
|
||||||
|
self._pending: dict[int, str] = {} # msg_id → 命令名
|
||||||
|
self._log_lines: list[str] = []
|
||||||
|
|
||||||
|
self._build_ui()
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# UI 构建
|
||||||
|
# ================================================================
|
||||||
|
|
||||||
|
def _build_ui(self):
|
||||||
|
# -- 顶栏:连接 --
|
||||||
|
top_bar = ttk.Frame(self, padding=5)
|
||||||
|
top_bar.pack(fill=tk.X)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
self._btn_connect = ttk.Button(top_bar, text="连接", command=self._toggle_connect)
|
||||||
|
self._btn_connect.pack(side=tk.LEFT, padx=10)
|
||||||
|
|
||||||
|
self._conn_status = ttk.Label(top_bar, text="● 未连接", foreground="red")
|
||||||
|
self._conn_status.pack(side=tk.LEFT, padx=10)
|
||||||
|
|
||||||
|
# -- 主体:左右分栏 --
|
||||||
|
main = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
|
||||||
|
main.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
|
# 左侧:设备列表
|
||||||
|
left = ttk.Frame(main, width=280)
|
||||||
|
main.add(left, weight=0)
|
||||||
|
|
||||||
|
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("<<TreeviewSelect>>", self._on_device_select)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 右侧:标签页
|
||||||
|
right = ttk.Frame(main)
|
||||||
|
main.add(right, weight=1)
|
||||||
|
|
||||||
|
self._notebook = ttk.Notebook(right)
|
||||||
|
self._notebook.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
def _add_info_tab(self):
|
||||||
|
f = ttk.Frame(self._notebook, padding=10)
|
||||||
|
self._notebook.add(f, text="设备信息")
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def _add_config_tab(self):
|
||||||
|
f = ttk.Frame(self._notebook, padding=10)
|
||||||
|
self._notebook.add(f, text="参数配置")
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# -- 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 [
|
||||||
|
("MQTT Host", "iot_host"), ("MQTT Port", "iot_port"),
|
||||||
|
("Client ID", "iot_cid"), ("用户名", "iot_user"), ("密码", "iot_pass"),
|
||||||
|
]:
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# -- 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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# -- 控制区 --
|
||||||
|
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)
|
||||||
|
|
||||||
|
return f
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# 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(),
|
||||||
|
)
|
||||||
|
self._mqtt.connect(config)
|
||||||
|
self._log(f"正在连接 {config.host}:{config.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):
|
||||||
|
if connected:
|
||||||
|
self._btn_connect.configure(text="断开")
|
||||||
|
self._conn_status.configure(text="● 已连接", foreground="green")
|
||||||
|
else:
|
||||||
|
self._btn_connect.configure(text="连接")
|
||||||
|
self._conn_status.configure(text="● 未连接", foreground="red")
|
||||||
|
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):
|
||||||
|
cmd = payload.get("cmd", "")
|
||||||
|
ts = datetime.fromtimestamp(payload.get("ts", 0)).strftime("%H:%M:%S")
|
||||||
|
|
||||||
|
if "/dev/config/resp" in topic:
|
||||||
|
code = payload.get("code", -1)
|
||||||
|
pmsg = payload.get("msg", "")
|
||||||
|
data = payload.get("data", {})
|
||||||
|
self._log(f"[{ts}] {dev_serial} ← {cmd} code={code} {pmsg}")
|
||||||
|
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})
|
||||||
|
else:
|
||||||
|
self._show_info({"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))
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
elif "/dev/status" in topic:
|
||||||
|
data = payload.get("data", {})
|
||||||
|
self._devmgr.update_heartbeat(dev_serial, data)
|
||||||
|
self._log(f"[{ts}] {dev_serial} ← heartbeat (uptime={data.get('uptime', 0)}s)")
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# 设备操作
|
||||||
|
# ================================================================
|
||||||
|
|
||||||
|
def _selected_dev(self) -> Optional[str]:
|
||||||
|
sel = self._dev_tree.selection()
|
||||||
|
if sel:
|
||||||
|
return self._dev_tree.item(sel[0], "text")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _require_dev(self) -> Optional[str]:
|
||||||
|
sn = self._selected_dev()
|
||||||
|
if not sn:
|
||||||
|
messagebox.showwarning("提示", "请先在左侧选择一个设备")
|
||||||
|
return None
|
||||||
|
if not self._mqtt.connected:
|
||||||
|
messagebox.showwarning("提示", "请先连接 MQTT Broker")
|
||||||
|
return None
|
||||||
|
return sn
|
||||||
|
|
||||||
|
def _send_cmd(self, cmd: str, data: Optional[dict] = None):
|
||||||
|
sn = self._require_dev()
|
||||||
|
if not sn:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
msg_id = self._mqtt.send_command(sn, cmd, data)
|
||||||
|
self._log(f"→ {sn} {cmd} (msg_id={msg_id})")
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.showerror("发送失败", str(e))
|
||||||
|
|
||||||
|
def _query_dev_info(self):
|
||||||
|
self._send_cmd(CMD_DEV_INFO_QUERY)
|
||||||
|
|
||||||
|
def _query_all(self):
|
||||||
|
sn = self._require_dev()
|
||||||
|
if not sn:
|
||||||
|
return
|
||||||
|
for cmd in [CMD_DEV_INFO_QUERY, CMD_SSC_NET_QUERY, CMD_IOT_NET_QUERY,
|
||||||
|
CMD_IOT_TOPIC_QUERY, CMD_LOOP_PARAM_QUERY]:
|
||||||
|
try:
|
||||||
|
self._mqtt.send_command(sn, cmd)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _query_ssc_net(self):
|
||||||
|
self._send_cmd(CMD_SSC_NET_QUERY)
|
||||||
|
|
||||||
|
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"),
|
||||||
|
))
|
||||||
|
|
||||||
|
def _query_iot_net(self):
|
||||||
|
self._send_cmd(CMD_IOT_NET_QUERY)
|
||||||
|
|
||||||
|
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(),
|
||||||
|
))
|
||||||
|
|
||||||
|
def _query_iot_topic(self):
|
||||||
|
self._send_cmd(CMD_IOT_TOPIC_QUERY)
|
||||||
|
|
||||||
|
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(),
|
||||||
|
))
|
||||||
|
|
||||||
|
def _do_pwd_verify(self):
|
||||||
|
pwd = self._var_pwd.get()
|
||||||
|
if not pwd:
|
||||||
|
messagebox.showwarning("提示", "请输入密码")
|
||||||
|
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()
|
||||||
|
if not old or not new:
|
||||||
|
messagebox.showwarning("提示", "请输入旧密码和新密码")
|
||||||
|
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)
|
||||||
|
|
||||||
|
def _do_device_reset(self):
|
||||||
|
if not messagebox.askyesno("确认", "确定要复位设备吗?设备将重启。"):
|
||||||
|
return
|
||||||
|
self._send_cmd(CMD_DEVICE_RESET)
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# UI 更新
|
||||||
|
# ================================================================
|
||||||
|
|
||||||
|
def _refresh_device_list(self):
|
||||||
|
tree = self._dev_tree
|
||||||
|
current = set(tree.get_children(""))
|
||||||
|
devices = self._devmgr.devices
|
||||||
|
|
||||||
|
for sn, dev in devices.items():
|
||||||
|
iid = sn
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
tree.insert("", tk.END, iid=iid, text=display, values=(model,))
|
||||||
|
|
||||||
|
for stale in current:
|
||||||
|
tree.delete(stale)
|
||||||
|
|
||||||
|
def _on_device_select(self, event):
|
||||||
|
sn = self._selected_dev()
|
||||||
|
if sn and sn in self._devmgr.devices:
|
||||||
|
dev = self._devmgr.devices[sn]
|
||||||
|
info = {
|
||||||
|
"dev_serial": dev.dev_serial,
|
||||||
|
"model": dev.model,
|
||||||
|
"hard_ver": dev.hard_ver,
|
||||||
|
"soft_ver": dev.soft_ver,
|
||||||
|
"product_code": dev.product_code,
|
||||||
|
"net_enabled": dev.net_enabled,
|
||||||
|
"iot_enabled": dev.iot_enabled,
|
||||||
|
"bus": dev.bus,
|
||||||
|
"online": dev.online,
|
||||||
|
"last_seen": dev.last_seen.strftime("%Y-%m-%d %H:%M:%S") if dev.last_seen else "—",
|
||||||
|
}
|
||||||
|
self._show_info(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 _append_data(self, widget: scrolledtext.ScrolledText, text: str):
|
||||||
|
widget.configure(state=tk.NORMAL)
|
||||||
|
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)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
app = DBNMqttTool()
|
||||||
|
app.mainloop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user