feat(DBNMQTTool): 脱机事件日志命令支持 — log_stat/log_query/log_clear (MQTT V1.06)
- protocol.py: CMD_LOG_* 常量 + LOG_COMMANDS + data_log_query 边界钳制 (start_seq>=1, count 1~4) + LOG_EVENT_TYPE_DESC 事件类型中文描述 - main.py: 参数配置 Tab 新增脱机日志面板 (统计/拉取/清除 + 显示区) - _on_message 响应分发: log_stat 统计展示, log_query 记录格式化 (unix_ts 真实时间 / boot+ts_ms 未同步), log_clear 二次确认 - devlog 追加
This commit is contained in:
@@ -61,6 +61,11 @@ CMD_LOOP_PARAM_SET = "loop_param_set"
|
|||||||
CMD_LOOP_PARAM_QUERY = "loop_param_query"
|
CMD_LOOP_PARAM_QUERY = "loop_param_query"
|
||||||
CMD_REPORT_CONFIG = "report_config"
|
CMD_REPORT_CONFIG = "report_config"
|
||||||
|
|
||||||
|
# V1.06: 脱机事件日志
|
||||||
|
CMD_LOG_STAT = "log_stat"
|
||||||
|
CMD_LOG_QUERY = "log_query"
|
||||||
|
CMD_LOG_CLEAR = "log_clear"
|
||||||
|
|
||||||
# 设备上报
|
# 设备上报
|
||||||
CMD_LOOP_DATA = "loop_data"
|
CMD_LOOP_DATA = "loop_data"
|
||||||
CMD_EVENT_REPORT = "event_report"
|
CMD_EVENT_REPORT = "event_report"
|
||||||
@@ -71,6 +76,7 @@ CMD_INITIALIZE = "initialize" # V1.03: 设备上电初始化
|
|||||||
CONFIG_SET_COMMANDS = {CMD_DEV_SERIAL_SET, CMD_SSC_NET_SET, CMD_IOT_NET_SET, CMD_IOT_TOPIC_SET, CMD_LOOP_PARAM_SET}
|
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}
|
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}
|
CTRL_COMMANDS = {CMD_PWD_VERIFY, CMD_PWD_SET, CMD_FACTORY_RESET, CMD_DEVICE_RESET, CMD_REPORT_CONFIG}
|
||||||
|
LOG_COMMANDS = {CMD_LOG_STAT, CMD_LOG_QUERY, CMD_LOG_CLEAR} # V1.06 脱机日志
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -104,6 +110,21 @@ FREQ_LEVELS = ["high", "mid_high", "mid_low", "low"]
|
|||||||
OUTPUT_MODES = ["exist", "enter_pulse", "leave_pulse", "direction"]
|
OUTPUT_MODES = ["exist", "enter_pulse", "leave_pulse", "direction"]
|
||||||
EVENT_TYPES = ["car_enter", "car_leave", "loop_cut", "loop_restore"]
|
EVENT_TYPES = ["car_enter", "car_leave", "loop_cut", "loop_restore"]
|
||||||
|
|
||||||
|
# 脱机事件日志类型描述 (log_query 响应 records[].type, MQTT V1.06)
|
||||||
|
LOG_EVENT_TYPE_DESC = {
|
||||||
|
"boot": "上电/复位",
|
||||||
|
"iot_connect": "MQTT TCP 连接成功",
|
||||||
|
"iot_ready": "MQTT 订阅完成→发 initialize",
|
||||||
|
"iot_disconnect": "MQTT 断连",
|
||||||
|
"iot_reconn": "重连退避",
|
||||||
|
"evt_retry": "event ACK 超时重发",
|
||||||
|
"evt_giveup": "event 重试耗尽挂起",
|
||||||
|
"coil": "线圈事件",
|
||||||
|
"time_anchor": "时钟同步锚点",
|
||||||
|
"log_clear": "日志清除(审计)",
|
||||||
|
"unknown": "未知类型",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 消息构建
|
# 消息构建
|
||||||
@@ -214,6 +235,14 @@ def data_report_config(sensor_type: int = 12, enable: bool = True,
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def data_log_query(start_seq: int = 1, count: int = 4) -> dict:
|
||||||
|
"""V1.06 log_query 分页拉取脱机事件日志 (count 上限 4, 设备侧超限按 4 处理)"""
|
||||||
|
return {
|
||||||
|
"start_seq": max(1, int(start_seq)),
|
||||||
|
"count": min(4, max(1, int(count))),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 解析设备上报
|
# 解析设备上报
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
@@ -139,3 +139,30 @@ DBNMQTTool/
|
|||||||
- **主动上报配置**: `report_config` 面板支持完整 7 参数独立开关
|
- **主动上报配置**: `report_config` 面板支持完整 7 参数独立开关
|
||||||
- `right` 局部变量 → `self._notebook`,修复 tab 引用丢失
|
- `right` 局部变量 → `self._notebook`,修复 tab 引用丢失
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-08-05 — 脱机事件日志命令支持 (MQTT V1.06)
|
||||||
|
|
||||||
|
配合 vd960DBN 固件 P1.3(commit 1a01316)+ 协议 V1.06,工具新增脱机事件日志面板(参数配置 Tab 底部):
|
||||||
|
|
||||||
|
### 新增命令
|
||||||
|
|
||||||
|
| 命令 | 按钮 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `log_stat` | 统计 | 日志统计/分页定位: enabled/boot_seq/count/capacity/seq_first/seq_last |
|
||||||
|
| `log_query` | 拉取日志 | 按全局序号分页拉取, 起始序号 + 条数(1~4) 可调 |
|
||||||
|
| `log_clear` | 清除日志 | 二次确认弹窗, 审计留痕不可撤销 |
|
||||||
|
|
||||||
|
### 实现要点
|
||||||
|
|
||||||
|
- `protocol.py`: 新增 `CMD_LOG_*` 常量、`LOG_COMMANDS` 集合、`data_log_query()` 构建器(start_seq≥1 / count 1~4 边界钳制)、`LOG_EVENT_TYPE_DESC` 事件类型中文描述(10 类 + unknown)
|
||||||
|
- `main.py`: 参数配置 Tab 新增 g6 组(起始序号/条数 spinbox + 三按钮 + 只读显示区 QPlainTextEdit)
|
||||||
|
- `_on_message` code==0 分支新增 `log_stat`/`log_query`/`log_clear` 响应处理
|
||||||
|
- 显示优化: log_query 记录带事件类型中文描述; unix_ts 已同步显示真实时间, 未同步显示 `boot+ts_ms(未同步)`
|
||||||
|
- 清除日志前 QMessageBox 二次确认(防误触, 与出厂初始化/设备复位同模式)
|
||||||
|
|
||||||
|
### 验证
|
||||||
|
|
||||||
|
- `py_compile` 四个模块通过
|
||||||
|
- protocol 逻辑单测: data_log_query 边界钳制 (0→1, 99→4, 0→1)、命令枚举一致性、build_request 带 data
|
||||||
|
|||||||
+101
-2
@@ -31,9 +31,10 @@ from dbn_mqtt_tool.protocol import (
|
|||||||
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_PWD_VERIFY, CMD_PWD_SET, CMD_FACTORY_RESET, CMD_DEVICE_RESET,
|
CMD_PWD_VERIFY, CMD_PWD_SET, CMD_FACTORY_RESET, CMD_DEVICE_RESET,
|
||||||
CMD_LOOP_DATA, CMD_EVENT_REPORT, CMD_HEARTBEAT, CMD_INITIALIZE,
|
CMD_LOOP_DATA, CMD_EVENT_REPORT, CMD_HEARTBEAT, CMD_INITIALIZE,
|
||||||
ERROR_MSGS, FREQ_LEVELS, OUTPUT_MODES, EVENT_TYPES,
|
CMD_LOG_STAT, CMD_LOG_QUERY, CMD_LOG_CLEAR,
|
||||||
|
ERROR_MSGS, FREQ_LEVELS, OUTPUT_MODES, EVENT_TYPES, LOG_EVENT_TYPE_DESC,
|
||||||
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_pwd_verify, data_pwd_set, data_report_config,
|
data_pwd_verify, data_pwd_set, data_report_config, data_log_query,
|
||||||
build_request, next_msg_id,
|
build_request, next_msg_id,
|
||||||
topic_down, topic_up,
|
topic_down, topic_up,
|
||||||
)
|
)
|
||||||
@@ -326,6 +327,42 @@ class MainWindow(QMainWindow):
|
|||||||
g5_layout.addLayout(btn_row5, 2, 0, 1, 6)
|
g5_layout.addLayout(btn_row5, 2, 0, 1, 6)
|
||||||
layout.addWidget(g5)
|
layout.addWidget(g5)
|
||||||
|
|
||||||
|
# -- 脱机事件日志 (V1.06) --
|
||||||
|
g6 = QGroupBox("脱机事件日志 (log_stat / log_query / log_clear)")
|
||||||
|
g6_layout = QGridLayout(g6)
|
||||||
|
|
||||||
|
g6_layout.addWidget(QLabel("起始序号:"), 0, 0)
|
||||||
|
self._edit_log_start_seq = QSpinBox()
|
||||||
|
self._edit_log_start_seq.setRange(1, 2**31 - 1)
|
||||||
|
self._edit_log_start_seq.setValue(1)
|
||||||
|
g6_layout.addWidget(self._edit_log_start_seq, 0, 1)
|
||||||
|
|
||||||
|
g6_layout.addWidget(QLabel("条数:"), 0, 2)
|
||||||
|
self._edit_log_count = QSpinBox()
|
||||||
|
self._edit_log_count.setRange(1, 4)
|
||||||
|
self._edit_log_count.setValue(4)
|
||||||
|
g6_layout.addWidget(self._edit_log_count, 0, 3)
|
||||||
|
|
||||||
|
btn_row6 = QHBoxLayout()
|
||||||
|
b9 = QPushButton("统计")
|
||||||
|
b9.clicked.connect(self._query_log_stat)
|
||||||
|
b10 = QPushButton("拉取日志")
|
||||||
|
b10.clicked.connect(self._query_log)
|
||||||
|
b11 = QPushButton("清除日志")
|
||||||
|
b11.clicked.connect(self._clear_log)
|
||||||
|
btn_row6.addWidget(b9)
|
||||||
|
btn_row6.addWidget(b10)
|
||||||
|
btn_row6.addWidget(b11)
|
||||||
|
btn_row6.addStretch()
|
||||||
|
g6_layout.addLayout(btn_row6, 1, 0, 1, 4)
|
||||||
|
|
||||||
|
self._log_rec_text = QPlainTextEdit()
|
||||||
|
self._log_rec_text.setReadOnly(True)
|
||||||
|
self._log_rec_text.setMaximumHeight(150)
|
||||||
|
self._log_rec_text.setFont(QFont("Consolas", 9))
|
||||||
|
g6_layout.addWidget(self._log_rec_text, 2, 0, 1, 4)
|
||||||
|
layout.addWidget(g6)
|
||||||
|
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
return w
|
return w
|
||||||
|
|
||||||
@@ -873,6 +910,12 @@ class MainWindow(QMainWindow):
|
|||||||
self._show_json({cmd: data})
|
self._show_json({cmd: data})
|
||||||
elif cmd == CMD_REPORT_CONFIG:
|
elif cmd == CMD_REPORT_CONFIG:
|
||||||
self._apply_report_config_data(data)
|
self._apply_report_config_data(data)
|
||||||
|
elif cmd == CMD_LOG_STAT:
|
||||||
|
self._apply_log_stat_data(data)
|
||||||
|
elif cmd == CMD_LOG_QUERY:
|
||||||
|
self._apply_log_query_data(data)
|
||||||
|
elif cmd == CMD_LOG_CLEAR:
|
||||||
|
self._log(f"脱机日志已清除 (审计留痕, 需重新统计确认)")
|
||||||
else:
|
else:
|
||||||
self._show_json({"error": f"code={code} {pmsg} ({ERROR_MSGS.get(code, '?')})"})
|
self._show_json({"error": f"code={code} {pmsg} ({ERROR_MSGS.get(code, '?')})"})
|
||||||
|
|
||||||
@@ -1078,6 +1121,62 @@ class MainWindow(QMainWindow):
|
|||||||
timeout=self._edit_report_timeout.value(),
|
timeout=self._edit_report_timeout.value(),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
# ---- 脱机事件日志 (V1.06) ----
|
||||||
|
|
||||||
|
def _query_log_stat(self):
|
||||||
|
"""log_stat: 日志统计/分页定位"""
|
||||||
|
self._send_cmd(CMD_LOG_STAT)
|
||||||
|
|
||||||
|
def _query_log(self):
|
||||||
|
"""log_query: 按全局序号分页拉取 (count≤4)"""
|
||||||
|
self._send_cmd(CMD_LOG_QUERY, data_log_query(
|
||||||
|
start_seq=self._edit_log_start_seq.value(),
|
||||||
|
count=self._edit_log_count.value(),
|
||||||
|
))
|
||||||
|
|
||||||
|
def _clear_log(self):
|
||||||
|
"""log_clear: 清除脱机日志 (审计留痕, 设备侧阻塞 ~2.8s)"""
|
||||||
|
r = QMessageBox.question(self, "确认",
|
||||||
|
"确定要清除设备脱机事件日志吗?\n"
|
||||||
|
"清除动作本身会写入审计记录,不可撤销!",
|
||||||
|
QMessageBox.Yes | QMessageBox.No)
|
||||||
|
if r == QMessageBox.Yes:
|
||||||
|
self._send_cmd(CMD_LOG_CLEAR)
|
||||||
|
|
||||||
|
def _apply_log_stat_data(self, data: dict):
|
||||||
|
lines = [
|
||||||
|
f"日志统计: enabled={data.get('enabled', False)} boot_seq={data.get('boot_seq', 0)}",
|
||||||
|
f"记录数: {data.get('count', 0)} / {data.get('capacity', 0)}",
|
||||||
|
f"全局序号范围: {data.get('seq_first', 0)} ~ {data.get('seq_last', 0)}",
|
||||||
|
f"提示: 拉取日志用 log_query, 起始序号填 seq_first",
|
||||||
|
]
|
||||||
|
self._log_rec_text.setPlainText("\n".join(lines))
|
||||||
|
self._log(f"log_stat: count={data.get('count', 0)} seq={data.get('seq_first', 0)}~{data.get('seq_last', 0)}")
|
||||||
|
|
||||||
|
def _apply_log_query_data(self, data: dict):
|
||||||
|
records = data.get("records", [])
|
||||||
|
start_seq = data.get("start_seq", 0)
|
||||||
|
lines = [f"log_query start_seq={start_seq} → 返回 {len(records)} 条:"]
|
||||||
|
for rec in records:
|
||||||
|
rtype = rec.get("type", "?")
|
||||||
|
desc = LOG_EVENT_TYPE_DESC.get(rtype, rtype)
|
||||||
|
ts_ms = rec.get("ts_ms", 0)
|
||||||
|
unix_ts = rec.get("unix_ts", 0)
|
||||||
|
if unix_ts:
|
||||||
|
ts_str = datetime.fromtimestamp(unix_ts).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
else:
|
||||||
|
ts_str = f"boot+{ts_ms}ms (未同步)"
|
||||||
|
rdata = rec.get("data")
|
||||||
|
data_str = json.dumps(rdata, ensure_ascii=False) if rdata else ""
|
||||||
|
lines.append(
|
||||||
|
f" seq={rec.get('seq', 0)} boot={rec.get('boot_seq', 0)} "
|
||||||
|
f"{ts_str} [{desc}] {data_str}"
|
||||||
|
)
|
||||||
|
if not records:
|
||||||
|
lines.append(" (无记录 / 起始序号越界, 可先用 log_stat 确认范围)")
|
||||||
|
self._log_rec_text.setPlainText("\n".join(lines))
|
||||||
|
self._log(f"log_query: start_seq={start_seq} got {len(records)}")
|
||||||
|
|
||||||
# ================================================================
|
# ================================================================
|
||||||
# UI 更新
|
# UI 更新
|
||||||
# ================================================================
|
# ================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user