feat(DBNMQTTool): 脱机日志支持快照流 stream=snapshot (2026-08-18)
- protocol.py: STREAM_EVENT/STREAM_SNAPSHOT 常量, data_log_query 加 stream 参数(快照流 count≤2), 新增 data_log_clear(stream), SNAP_MISC_TYPE_DESC - main.py: 日志流选择器 + 三命令按流发请求 + log_stat/log_query 响应区分流(快照 channels 逐字段展示) - 验证: protocol 断言 6 例 + offscreen MainWindow 快照/事件流展示实测全过 - devlog 置顶 + .gitignore (venv/)
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.spec.bak
|
||||
build/
|
||||
dist/
|
||||
@@ -78,6 +78,10 @@ CONFIG_QUERY_COMMANDS = {CMD_DEV_INFO_QUERY, CMD_SSC_NET_QUERY, CMD_IOT_NET_QUER
|
||||
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 脱机日志
|
||||
|
||||
# V1.07: log_* 命令日志流 (与 BLE 0x28/0x2A 同语义)
|
||||
STREAM_EVENT = "event"
|
||||
STREAM_SNAPSHOT = "snapshot"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 错误码
|
||||
@@ -125,6 +129,14 @@ LOG_EVENT_TYPE_DESC = {
|
||||
"unknown": "未知类型",
|
||||
}
|
||||
|
||||
# 传感快照 channels[].misc_type 描述 (log_query stream=snapshot, MQTT V1.07)
|
||||
SNAP_MISC_TYPE_DESC = {
|
||||
"time": "时间量(50ms)",
|
||||
"cut_count": "线圈断开次数",
|
||||
"flow_count": "车流量",
|
||||
"relay_count": "继电器输出次数",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 消息构建
|
||||
@@ -235,12 +247,29 @@ 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 {
|
||||
def data_log_query(start_seq: int = 1, count: int = 4, stream: str = STREAM_EVENT) -> dict:
|
||||
"""V1.07 log_query 分页拉取脱机日志
|
||||
stream=event: 事件流, count 上限 4 (设备侧超限按 4 处理)
|
||||
stream=snapshot: 快照流, count 上限 2 (64B×2 记录); 显式传 stream 字段
|
||||
"""
|
||||
d: dict[str, Any] = {
|
||||
"start_seq": max(1, int(start_seq)),
|
||||
"count": min(4, max(1, int(count))),
|
||||
}
|
||||
if stream == STREAM_SNAPSHOT:
|
||||
d["stream"] = STREAM_SNAPSHOT
|
||||
d["count"] = min(2, max(1, int(count)))
|
||||
return d
|
||||
|
||||
|
||||
def data_log_clear(stream: str = STREAM_EVENT) -> dict:
|
||||
"""V1.07 log_clear 清除脱机日志 (审计留痕)
|
||||
stream=snapshot: 快照流清除 (~45ms, 审计写事件流)
|
||||
event 流省略 data (设备缺省 event, 兼容老固件)
|
||||
"""
|
||||
if stream == STREAM_SNAPSHOT:
|
||||
return {"stream": STREAM_SNAPSHOT}
|
||||
return {}
|
||||
|
||||
|
||||
# ============================================================
|
||||
|
||||
@@ -6,6 +6,26 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-18 — 脱机日志支持快照流 (stream=snapshot, 协议 V1.07)
|
||||
|
||||
### 变更
|
||||
|
||||
- **protocol.py**:新增 `STREAM_EVENT` / `STREAM_SNAPSHOT` 常量;`data_log_query` 加 `stream` 参数(快照流显式传 `stream` 字段 + count≤2);新增 `data_log_clear(stream)`(事件流省略 data 兼容老固件);新增 `SNAP_MISC_TYPE_DESC`(time/cut_count/flow_count/relay_count 中文描述)
|
||||
- **main.py**:脱机日志区加"日志流"选择器(事件日志/传感快照);统计/拉取/清除三按钮按当前流发请求(log_clear 确认框提示阻塞时长 2.8s/45ms);`log_stat`/`log_query` 响应展示区分流——快照记录解析 `channels[]` 逐字段(freq_level/direction/freq_type/sens/cond/loop/car/freq/variation/misc)
|
||||
|
||||
### 验证
|
||||
|
||||
- protocol 数据构建器断言 6 例全过(事件流不带 stream / 快照流 count≤2 / log_clear 流参数)
|
||||
- `QT_QPA_PLATFORM=offscreen` 实例化 MainWindow 实测:`log_stat(snapshot)` capacity=48064 展示、`log_query(snapshot)` 2 条快照记录逐字段展示(含负 variation、misc_type 中文)、事件流展示回归无损、空记录边界
|
||||
- 依赖:venv + PySide6 + paho-mqtt(`venv/` 已加 .gitignore)
|
||||
|
||||
### 配套
|
||||
|
||||
- 固件 vd960DBN `log_*` stream=snapshot 分支(同日实现,devlog V4.1)
|
||||
- 协议文档:TCP JSON V1.03 / IoT MQTT V1.07
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-06 (晚) — 模拟上报 + 协议Topic + 自定义Topic `c19e465`
|
||||
|
||||
### 1. 模拟上报 Tab
|
||||
|
||||
+66
-22
@@ -32,9 +32,10 @@ from dbn_mqtt_tool.protocol import (
|
||||
CMD_PWD_VERIFY, CMD_PWD_SET, CMD_FACTORY_RESET, CMD_DEVICE_RESET,
|
||||
CMD_LOOP_DATA, CMD_EVENT_REPORT, CMD_HEARTBEAT, CMD_INITIALIZE,
|
||||
CMD_LOG_STAT, CMD_LOG_QUERY, CMD_LOG_CLEAR,
|
||||
ERROR_MSGS, FREQ_LEVELS, OUTPUT_MODES, EVENT_TYPES, LOG_EVENT_TYPE_DESC,
|
||||
STREAM_EVENT, STREAM_SNAPSHOT,
|
||||
ERROR_MSGS, FREQ_LEVELS, OUTPUT_MODES, EVENT_TYPES, LOG_EVENT_TYPE_DESC, SNAP_MISC_TYPE_DESC,
|
||||
data_ssc_net_set, data_iot_net_set, data_iot_topic_set,
|
||||
data_pwd_verify, data_pwd_set, data_report_config, data_log_query,
|
||||
data_pwd_verify, data_pwd_set, data_report_config, data_log_query, data_log_clear,
|
||||
build_request, next_msg_id,
|
||||
topic_down, topic_up,
|
||||
)
|
||||
@@ -327,21 +328,28 @@ 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)")
|
||||
# -- 脱机日志 (V1.06 事件流 / V1.07 快照流) --
|
||||
g6 = QGroupBox("脱机日志 (log_stat / log_query / log_clear)")
|
||||
g6_layout = QGridLayout(g6)
|
||||
|
||||
g6_layout.addWidget(QLabel("起始序号:"), 0, 0)
|
||||
g6_layout.addWidget(QLabel("日志流:"), 0, 0)
|
||||
self._combo_log_stream = QComboBox()
|
||||
self._combo_log_stream.addItem("事件日志 (event)", STREAM_EVENT)
|
||||
self._combo_log_stream.addItem("传感快照 (snapshot)", STREAM_SNAPSHOT)
|
||||
self._combo_log_stream.setCurrentIndex(0)
|
||||
g6_layout.addWidget(self._combo_log_stream, 0, 1)
|
||||
|
||||
g6_layout.addWidget(QLabel("起始序号:"), 0, 2)
|
||||
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(self._edit_log_start_seq, 0, 3)
|
||||
|
||||
g6_layout.addWidget(QLabel("条数:"), 0, 2)
|
||||
g6_layout.addWidget(QLabel("条数:"), 0, 4)
|
||||
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)
|
||||
g6_layout.addWidget(self._edit_log_count, 0, 5)
|
||||
|
||||
btn_row6 = QHBoxLayout()
|
||||
b9 = QPushButton("统计")
|
||||
@@ -1121,46 +1129,82 @@ class MainWindow(QMainWindow):
|
||||
timeout=self._edit_report_timeout.value(),
|
||||
))
|
||||
|
||||
# ---- 脱机事件日志 (V1.06) ----
|
||||
# ---- 脱机日志 (V1.06 事件流 / V1.07 快照流) ----
|
||||
|
||||
def _selected_log_stream(self) -> str:
|
||||
"""当前 UI 选择的日志流 (event/snapshot)"""
|
||||
return self._combo_log_stream.currentData() or STREAM_EVENT
|
||||
|
||||
def _query_log_stat(self):
|
||||
"""log_stat: 日志统计/分页定位"""
|
||||
self._send_cmd(CMD_LOG_STAT)
|
||||
"""log_stat: 日志统计/分页定位 (按当前流)"""
|
||||
stream = self._selected_log_stream()
|
||||
data = {"stream": STREAM_SNAPSHOT} if stream == STREAM_SNAPSHOT else None
|
||||
self._send_cmd(CMD_LOG_STAT, data)
|
||||
|
||||
def _query_log(self):
|
||||
"""log_query: 按全局序号分页拉取 (count≤4)"""
|
||||
"""log_query: 按全局序号分页拉取 (事件流 count≤4 / 快照流 count≤2)"""
|
||||
self._send_cmd(CMD_LOG_QUERY, data_log_query(
|
||||
start_seq=self._edit_log_start_seq.value(),
|
||||
count=self._edit_log_count.value(),
|
||||
stream=self._selected_log_stream(),
|
||||
))
|
||||
|
||||
def _clear_log(self):
|
||||
"""log_clear: 清除脱机日志 (审计留痕, 设备侧阻塞 ~2.8s)"""
|
||||
"""log_clear: 清除脱机日志 (按当前流, 审计留痕, 设备侧阻塞)"""
|
||||
stream = self._selected_log_stream()
|
||||
target = "传感快照" if stream == STREAM_SNAPSHOT else "脱机事件日志"
|
||||
block = "~45ms (逻辑清除+当前扇区)" if stream == STREAM_SNAPSHOT else "~2.8s (63 扇区擦除)"
|
||||
r = QMessageBox.question(self, "确认",
|
||||
"确定要清除设备脱机事件日志吗?\n"
|
||||
"清除动作本身会写入审计记录,不可撤销!",
|
||||
f"确定要清除设备{target}吗?\n"
|
||||
f"清除动作本身会写入审计记录,不可撤销!\n"
|
||||
f"设备侧阻塞约 {block}。",
|
||||
QMessageBox.Yes | QMessageBox.No)
|
||||
if r == QMessageBox.Yes:
|
||||
self._send_cmd(CMD_LOG_CLEAR)
|
||||
self._send_cmd(CMD_LOG_CLEAR, data_log_clear(stream))
|
||||
|
||||
def _apply_log_stat_data(self, data: dict):
|
||||
stream = data.get("stream", "event")
|
||||
stream_name = "传感快照" if stream == "snapshot" else "事件日志"
|
||||
lines = [
|
||||
f"日志统计: enabled={data.get('enabled', False)} boot_seq={data.get('boot_seq', 0)}",
|
||||
f"日志统计[{stream_name}]: 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",
|
||||
f"提示: 拉取日志用 log_query, 起始序号填 seq_first"
|
||||
+ (" (快照流 count 上限 2)" if stream == "snapshot" else ""),
|
||||
]
|
||||
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)}")
|
||||
self._log(f"log_stat[{stream_name}]: 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)} 条:"]
|
||||
# 快照记录带 coil_count/channels 字段, 事件记录带 type 字段
|
||||
is_snap = bool(records) and ("coil_count" in records[0] or "channels" in records[0])
|
||||
stream_name = "传感快照" if is_snap else "事件日志"
|
||||
lines = [f"log_query[{stream_name}] start_seq={start_seq} → 返回 {len(records)} 条:"]
|
||||
for rec in records:
|
||||
ts_ms = rec.get("ts_ms", 0)
|
||||
if is_snap:
|
||||
coil_count = rec.get("coil_count", 0)
|
||||
lines.append(
|
||||
f" seq={rec.get('seq', 0)} boot={rec.get('boot_seq', 0)} "
|
||||
f"boot+{ts_ms}ms ({coil_count}ch)"
|
||||
)
|
||||
for ch in rec.get("channels", []):
|
||||
mt = ch.get("misc_type", "?")
|
||||
misc_desc = SNAP_MISC_TYPE_DESC.get(mt, mt)
|
||||
lines.append(
|
||||
f" ch{ch.get('ch', 0)}: {ch.get('freq_level', '?')} "
|
||||
f"dir={ch.get('direction', 0)} ftype={ch.get('freq_type', 0)} "
|
||||
f"sens={ch.get('sensitivity', 0)} cond={ch.get('condition', 0)} "
|
||||
f"loop={'OK' if ch.get('loop_ok') else '断'} "
|
||||
f"car={'有' if ch.get('has_car') else '无'} "
|
||||
f"freq={ch.get('freq', 0)}Hz Δ={ch.get('variation', 0)} "
|
||||
f"[{misc_desc}] misc={ch.get('misc', 0)}"
|
||||
)
|
||||
else:
|
||||
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")
|
||||
@@ -1175,7 +1219,7 @@ class MainWindow(QMainWindow):
|
||||
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)}")
|
||||
self._log(f"log_query[{stream_name}]: start_seq={start_seq} got {len(records)}")
|
||||
|
||||
# ================================================================
|
||||
# UI 更新
|
||||
|
||||
Reference in New Issue
Block a user