fix: SerialMate v0.1.1 接收路径修复——时间戳改 link 线程到达时刻打戳、接收日志批量插入+5000行上限+积压折叠、通道配置缓存;自测扩至 selftest_gui 69 项,新增 ISSUE 报告与 _sim_periodic 复现脚本;重打包
This commit is contained in:
+110
-39
@@ -49,7 +49,7 @@ from protocol import (BIN_FORMATS, CRC_ALGOS, CRC_ALGO_LABELS, CRC_SCOPE_LABELS,
|
||||
from theme import ThemeManager
|
||||
|
||||
APP_NAME = 'SerialMate'
|
||||
APP_VERSION = '0.1.0'
|
||||
APP_VERSION = '0.1.1'
|
||||
REPO_URL = 'https://gitea.hulk.wang/hulk/HW-Debug-Tools'
|
||||
|
||||
BAUDS = (1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600)
|
||||
@@ -69,10 +69,23 @@ FMT_LABELS = (('u8', 'u8'), ('s8', 's8'),
|
||||
('u32be', 'u32 大端'), ('u32le', 'u32 小端'),
|
||||
('s32be', 's32 大端'), ('s32le', 's32 小端'),
|
||||
('f32be', 'f32 大端'), ('f32le', 'f32 小端'))
|
||||
# 接收日志治理: 行数上限(超量丢最旧); 事件队列积压超阈值时, 本轮帧日志折叠为
|
||||
# 计数摘要(曲线照常), 防长跑时排版成本拖垮 30ms 轮询导致显示延迟越积越大
|
||||
RX_MAX_BLOCKS = 5000
|
||||
FOLD_QSIZE = 200
|
||||
|
||||
|
||||
def _timestamp():
|
||||
t = time.time()
|
||||
def _esc_log(text):
|
||||
"""日志行入 HTML 缓冲前的转义: html.escape + 空格改 ——
|
||||
富文本会折叠连续空格, 日志对齐(如 TX 行 '命令 [hex]')依赖原空格"""
|
||||
return html.escape(text).replace(' ', ' ')
|
||||
|
||||
|
||||
def _timestamp(t=None):
|
||||
"""epoch 秒 -> 'HH:MM:SS.mmm' 纯格式化; t=None 取当前时刻。
|
||||
日志时间必须传入 link 后台线程打的到达时刻, 不能消费时现取(会随 UI 卡顿挤压失真)"""
|
||||
if t is None:
|
||||
t = time.time()
|
||||
return time.strftime('%H:%M:%S', time.localtime(t)) + f'.{int(t * 1000) % 1000:03d}'
|
||||
|
||||
|
||||
@@ -232,14 +245,17 @@ class Window(QMainWindow):
|
||||
self._plot_win = None # 曲线独立窗口(弹出后非 None)
|
||||
self.tx_history, self._hist_pos = [], -1
|
||||
self._tx_n = self._rx_n = self._tx_b = self._rx_b = 0
|
||||
self._rx_buf = [] # 待写入接收日志的 HTML 行(批量 flush)
|
||||
self._ch_cfg = [] # 通道配置缓存(_build_ui 后重建)
|
||||
self.link = SerialLink(cfg=self._proto_from_store(),
|
||||
on_status=lambda s: self.events.put(('status', s)))
|
||||
self.link.on_frame = lambda fr: self.events.put(('frame', fr))
|
||||
self.link.on_raw = lambda b: self.events.put(('raw', b))
|
||||
self.link.on_tx = lambda lb, d: self.events.put(('tx', lb, d))
|
||||
on_status=lambda s, ts: self.events.put(('status', s, ts)))
|
||||
self.link.on_frame = lambda fr, ts: self.events.put(('frame', fr, ts))
|
||||
self.link.on_raw = lambda b, ts: self.events.put(('raw', b, ts))
|
||||
self.link.on_tx = lambda lb, d, ts: self.events.put(('tx', lb, d, ts))
|
||||
self._build_ui()
|
||||
self.plot.on_yrange_changed = self._on_plot_yrange
|
||||
self.plot.on_span_changed = self._sync_time_slider
|
||||
self._ch_cfg = self._collect_channels() # 通道配置缓存(_on_channel 时重建)
|
||||
self._apply_store_to_ui()
|
||||
self.theme_manager.set_window(self)
|
||||
self.ed_tx.installEventFilter(self)
|
||||
@@ -634,7 +650,8 @@ class Window(QMainWindow):
|
||||
return out
|
||||
|
||||
def _on_channel(self, *_):
|
||||
for i, c in enumerate(self._collect_channels()):
|
||||
self._ch_cfg = self._collect_channels() # 用户改配置才重建, 帧路径只读缓存
|
||||
for i, c in enumerate(self._ch_cfg):
|
||||
self.plot.set_channel_name(i, c['name'] if c['enabled'] else '')
|
||||
self._update_preview()
|
||||
|
||||
@@ -823,7 +840,7 @@ class Window(QMainWindow):
|
||||
self.text_rx.setLineWrapMode(QPlainTextEdit.NoWrap)
|
||||
self.text_rx.setFont(QFontDatabase.systemFont(QFontDatabase.FixedFont))
|
||||
self.text_rx.setMinimumHeight(120)
|
||||
self.text_rx.document().setMaximumBlockCount(5000)
|
||||
self.text_rx.document().setMaximumBlockCount(RX_MAX_BLOCKS)
|
||||
lay.addWidget(self.text_rx, 1)
|
||||
return card
|
||||
|
||||
@@ -1245,61 +1262,114 @@ class Window(QMainWindow):
|
||||
# ------------------------------------------------------- 事件与回显
|
||||
|
||||
def _drain_events(self):
|
||||
for _ in range(400): # 单次上限, 防极端速率卡 UI
|
||||
"""30ms 轮询消费 link 事件。事件尾部携带 link 后台线程打的发生时刻 ts
|
||||
(字节到达/写入串口), 日志时间因此是到达时间而非显示时间。
|
||||
接收日志行先进 _rx_buf 攒批, 本轮结束 _flush_rx 一次写入——把 N 次
|
||||
UI 重排压成 1 次; 队列积压超 FOLD_QSIZE 时帧日志折叠为计数摘要
|
||||
(显示本批首尾到达时刻, 曲线照常入库), 防消费不逮时越积越卡。"""
|
||||
n = 0
|
||||
folded = None # None=未折叠; {'n':帧数,'t0/t1':首尾到达,'cnt':{描述:计数}}
|
||||
for _ in range(400): # 单次上限, 防极端速率卡 UI
|
||||
try:
|
||||
ev = self.events.get_nowait()
|
||||
except queue.Empty:
|
||||
return
|
||||
break
|
||||
kind = ev[0]
|
||||
ts = ev[-1]
|
||||
if kind == 'status':
|
||||
self._on_status(ev[1])
|
||||
self._on_status(ev[1], ts)
|
||||
elif kind == 'frame':
|
||||
self._on_frame(ev[1])
|
||||
if folded is None and self.events.qsize() > FOLD_QSIZE:
|
||||
folded = {'n': 0, 't0': ts, 't1': ts, 'cnt': {}}
|
||||
if folded is not None:
|
||||
self._absorb_frame(ev[1], ts, folded)
|
||||
else:
|
||||
self._on_frame(ev[1], ts)
|
||||
elif kind == 'raw':
|
||||
self._on_raw(ev[1])
|
||||
self._on_raw(ev[1], ts)
|
||||
elif kind == 'tx':
|
||||
self._on_tx(ev[1], ev[2])
|
||||
self._on_tx(ev[1], ev[2], ts)
|
||||
n += 1
|
||||
if folded:
|
||||
detail = ' '.join(f'{desc} ×{c}' for desc, c in folded['cnt'].items())
|
||||
self._append_rx(f'← {_timestamp(folded["t0"])}~{_timestamp(folded["t1"])}'
|
||||
f' ⏳ 显示滞后, 折叠 {folded["n"]} 帧: {detail}')
|
||||
if n:
|
||||
self._flush_rx()
|
||||
self._update_counters()
|
||||
|
||||
def _hint(self, text):
|
||||
def _hint(self, text, ts=None):
|
||||
self.lb_state.setText(text)
|
||||
self.statusBar().showMessage(text, 8000)
|
||||
self._append_rx(f'· {_timestamp()} {text}')
|
||||
self._append_rx(f'· {_timestamp(ts)} {text}')
|
||||
|
||||
def _on_status(self, text):
|
||||
def _on_status(self, text, ts=None):
|
||||
"""连接状态: 同步连接按钮文案 + 提示"""
|
||||
if text.startswith('已连接'):
|
||||
self.btn_conn.setText('断开')
|
||||
elif text.startswith(('已断开', '连接失败', '串口丢失')):
|
||||
self.btn_conn.setText('连接')
|
||||
self._hint(text)
|
||||
self._hint(text, ts)
|
||||
|
||||
def _append_rx(self, line):
|
||||
self.text_rx.appendPlainText(line)
|
||||
"""接收日志行: 只入批量缓冲, 由 _flush_rx 统一写入"""
|
||||
self._rx_buf.append(_esc_log(line))
|
||||
|
||||
def _append_rx_frame(self, prefix, core, suffix=''):
|
||||
"""帧日志行: payload 核心内容垫一层浅背景色, 目光一眼锁定。
|
||||
用半透明 rgba —— 主题无关, 明暗切换后历史行不会残留异色"""
|
||||
self.text_rx.appendHtml(
|
||||
f'{html.escape(prefix)}'
|
||||
self._rx_buf.append(
|
||||
f'{_esc_log(prefix)}'
|
||||
f'<span style="background-color:rgba(96,125,180,0.16);">'
|
||||
f'{html.escape(core)}</span>'
|
||||
f'{html.escape(suffix)}')
|
||||
f'{_esc_log(core)}</span>'
|
||||
f'{_esc_log(suffix)}')
|
||||
|
||||
def _on_frame(self, fr):
|
||||
def _flush_rx(self):
|
||||
"""批量写入接收日志: 每行一个 <p>(一个 block), maximumBlockCount
|
||||
裁旧行照常生效; 一次 appendHtml 替代逐行 N 次, 长跑不再因排版
|
||||
成本拖垮事件轮询"""
|
||||
if not self._rx_buf:
|
||||
return
|
||||
self.text_rx.appendHtml(
|
||||
''.join(f'<p style="margin:0">{line}</p>' for line in self._rx_buf))
|
||||
self._rx_buf.clear()
|
||||
|
||||
def _on_frame(self, fr, ts=None):
|
||||
self._rx_n += 1
|
||||
self._rx_b += len(fr.raw)
|
||||
ts = f'{_timestamp()} ' if self.chk_ts.isChecked() else ''
|
||||
t = f'{_timestamp(ts)} ' if self.chk_ts.isChecked() else ''
|
||||
disp = RX_DISPLAYS[self.combo_display.currentIndex()]
|
||||
payload = hexs(fr.payload) if disp == 'hex' else \
|
||||
(clean_text(fr.payload.decode('ascii', 'replace')) or '(空)')
|
||||
if fr.code is None: # 布局无 ID 字段
|
||||
head = f'← {ts}'
|
||||
head = f'← {t}'
|
||||
else:
|
||||
name = TYPE_NAMES.get(fr.code)
|
||||
head = f'← {ts}B=0x{fr.code:02X}' + (f' {name}' if name else '')
|
||||
head = f'← {t}B=0x{fr.code:02X}' + (f' {name}' if name else '')
|
||||
suffix = f' [{hexs(fr.raw)}]' if self.chk_raw.isChecked() else ''
|
||||
self._append_rx_frame(f'{head} LEN={len(fr.payload)} ', payload, suffix)
|
||||
for i, c in enumerate(self._collect_channels()):
|
||||
self._extract_channels(fr)
|
||||
|
||||
def _absorb_frame(self, fr, ts, folded):
|
||||
"""积压期帧处理: 只计数 + 曲线照常入库, 日志行省略(_drain_events 汇总)"""
|
||||
self._rx_n += 1
|
||||
self._rx_b += len(fr.raw)
|
||||
if not folded['n']:
|
||||
folded['t0'] = ts
|
||||
folded['n'] += 1
|
||||
folded['t1'] = ts
|
||||
if fr.code is None: # 布局无 ID 字段
|
||||
desc = '帧'
|
||||
else:
|
||||
name = TYPE_NAMES.get(fr.code)
|
||||
desc = f'B=0x{fr.code:02X}' + (f' {name}' if name else '')
|
||||
folded['cnt'][desc] = folded['cnt'].get(desc, 0) + 1
|
||||
self._extract_channels(fr)
|
||||
|
||||
def _extract_channels(self, fr):
|
||||
"""按通道配置缓存(self._ch_cfg, _on_channel 时重建)提取数值:
|
||||
曲线入库 + 最新值标签。帧路径不得遍历 UI 控件——高频时放大 UI 占用"""
|
||||
for i, c in enumerate(self._ch_cfg):
|
||||
if not c['enabled']:
|
||||
continue
|
||||
v = extract_value(fr.payload, c['mode'], offset=c['offset'],
|
||||
@@ -1308,29 +1378,30 @@ class Window(QMainWindow):
|
||||
if v is not None:
|
||||
self._ch_ui[i]['val'].setText(fmt_val(v))
|
||||
self.plot.add(i, v)
|
||||
self._update_counters()
|
||||
|
||||
def _on_raw(self, data):
|
||||
def _on_raw(self, data, ts=None):
|
||||
self._rx_n += 1
|
||||
self._rx_b += len(data)
|
||||
ts = f'{_timestamp()} ' if self.chk_ts.isChecked() else ''
|
||||
t = f'{_timestamp(ts)} ' if self.chk_ts.isChecked() else ''
|
||||
disp = RX_DISPLAYS[self.combo_display.currentIndex()]
|
||||
body = hexs(data) if disp == 'hex' else clean_text(data.decode('ascii', 'replace'))
|
||||
self._append_rx(f'← {ts}{body}')
|
||||
self._update_counters()
|
||||
self._append_rx(f'← {t}{body}')
|
||||
|
||||
def _on_tx(self, label, data):
|
||||
def _on_tx(self, label, data, ts=None):
|
||||
self._tx_n += 1
|
||||
self._tx_b += len(data)
|
||||
if label is None:
|
||||
self._append_rx(f'→ {_timestamp()} [HEX] {hexs(data)}')
|
||||
self._append_rx(f'→ {_timestamp(ts)} [HEX] {hexs(data)}')
|
||||
else:
|
||||
self._append_rx(f'→ {_timestamp()} {label} [{hexs(data)}]')
|
||||
self._update_counters()
|
||||
self._append_rx(f'→ {_timestamp(ts)} {label} [{hexs(data)}]')
|
||||
|
||||
def _update_counters(self):
|
||||
"""状态栏: 收发计数 + 事件队列积压量。正常恒为 0; 数字增长=事件
|
||||
产出快于消费(GUI 卡或数据量异常), 便于用户把"不显示"现场分层"""
|
||||
self.statusBar().showMessage(
|
||||
f'发送 {self._tx_n} 帧 / {self._tx_b} 字节 接收 {self._rx_n} 帧 / {self._rx_b} 字节')
|
||||
f'发送 {self._tx_n} 帧 / {self._tx_b} 字节 '
|
||||
f'接收 {self._rx_n} 帧 / {self._rx_b} 字节 '
|
||||
f'积压 {self.events.qsize()}')
|
||||
|
||||
def _on_theme(self, idx):
|
||||
self.theme_manager.set_mode(('system', 'light', 'dark')[idx])
|
||||
|
||||
Reference in New Issue
Block a user