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,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
|
||||
Reference in New Issue
Block a user