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:
wangfq
2026-08-05 09:06:11 +08:00
parent 1a01316c7f
commit 5ebd8247f0
3 changed files with 157 additions and 2 deletions
+101 -2
View File
@@ -31,9 +31,10 @@ from dbn_mqtt_tool.protocol import (
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_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_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,
topic_down, topic_up,
)
@@ -326,6 +327,42 @@ class MainWindow(QMainWindow):
g5_layout.addLayout(btn_row5, 2, 0, 1, 6)
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()
return w
@@ -873,6 +910,12 @@ class MainWindow(QMainWindow):
self._show_json({cmd: data})
elif cmd == CMD_REPORT_CONFIG:
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:
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(),
))
# ---- 脱机事件日志 (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 更新
# ================================================================