# SerialMate 接收路径问题报告(时间戳失真 + 显示延迟累积) > 来源:FilmForge 固件联调(GD32F405 RS485,115200,M2 连续温度上报 2~10 帧/s)。 > 现象与固件侧二分验证结论一起整理,供后续修复。 > 日期:2026-10-10 > > **【已修复 v0.1.1,2026-10-10】** 根因 1 与根因 2 的 1/2/3/4 全部落地, > 实现与本文建议的差别:日志批量插入每行包 `

`(`
` > 连接只算 1 个 block,会让 maximumBlockCount 失效);转义空格转 ` ` > 防富文本折叠连续空格;通道缓存放 `_ch_cfg`(`_on_channel` 时重建)。 > 自检已覆盖(selftest_gui 69 项含积压折叠/到达时间戳用例), > 端到端模拟 600 帧积压:日志 201 行、曲线 600 点全收、时间戳=到达时刻。 > 详见 AGENTS.md §7 v0.1.1。 ## 现象(联调实测) 1. **时间戳挤压**:M2 以 500ms/100ms 周期上报时,日志里出现 13 帧"0.2~1ms 间隔" 挤在同一批(如 23:59:55.221~227),完全不是真实到达间隔。 2. **显示延迟累积**:命令 ack 延迟 120~160ms 才显示;长时间运行后越来越卡, 最终表现为"命令发了没反应"(实际帧在队列里排队,晚些时候才显示)。 3. **偶发"丢帧"观感**:与 2 叠加后,ack 迟迟不显示,上位机/人工判断为无响应。 固件侧已用调试串口镜像做过二分验证:**帧到达固件、固件发出响应均为 100%**, SerialMate 显示层的时间戳与延迟失真是独立存在的第二层问题。 ## 根因 1:时间戳在 GUI 消费时刻打,而非字节到达时刻(确凿) - `link.py` `_dispatch_rx()` 后台线程解析出帧后回调 `on_frame(fr)`; - `main.py:237` `self.link.on_frame = lambda fr: self.events.put(('frame', fr))` —— **入队时不带时间戳**; - `main.py:1291` `_on_frame()` 里才调 `_timestamp()` —— **消费时打戳**。 后台读线程 20ms 轮询、GUI QTimer 30ms drain,再叠加 UI 卡顿,一串积压帧的 时间戳全部落在消费瞬间,毫秒级挤在一起。**这不是到达时间,是显示时间。** ### 修复建议 时间戳统一在 link 后台线程产生(字节到达时刻),随事件入队: ```python # link.py _dispatch_rx() ts = time.time() # 或 time.perf_counter() for fr in self.parser.feed(data): if self.on_frame is not None: self.on_frame(fr, ts) # 携带到达时刻 # raw 同理: self.on_raw(data, ts) # main.py self.link.on_frame = lambda fr, ts: self.events.put(('frame', fr, ts)) ... def _on_frame(self, fr, ts): # 显示时用 ts 格式化,不再调 _timestamp() ``` `on_raw` / `on_tx`(若也要求发送时刻)/ `on_status` 同理改造。 `_timestamp()` 改为接受 epoch 秒参数的纯格式化函数。 ## 根因 2:接收日志无限增长 → UI 递减性卡顿 → 延迟累积(确凿) - `_append_rx`/`_append_rx_frame` 对 `text_rx` 逐行 `appendPlainText/appendHtml`, **无任何行数上限**。长跑后 QPlainTextEdit 文档越滚越大,排版/paint 成本上升, 30ms 的 `_drain_events` 开始处理不完,事件积压,显示延迟越拉越大。 - `_on_frame` 每帧调用 `_collect_channels()`(遍历全部通道 UI 控件读 isChecked/text/value),帧率高时放大 UI 线程占用。 ### 修复建议 1. 行数上限:`self.text_rx.setMaximumBlockCount(5000)`(一行代码,先止血); 或定时(如每 5s)裁剪超量历史。 2. 批量插入:`_drain_events` 把本轮全部行拼成一个字符串/HTML 片段,**一次** append,减少 N 次 UI 重排为 1 次。 3. 通道配置缓存:维护 `self._ch_cfg`,仅在 `_on_channel`(用户改动配置)时 重建;`_on_frame` 直接读缓存,不再每帧遍历控件。 4. 帧率自适应:积压超过阈值(如 events.qsize() > 200)时,帧日志折叠为 计数摘要("…+250 帧 (温度上报)"),保曲线、压日志。 ## 排除项(验证过没有问题) - `protocol.FrameParser`(前缀对齐 + LEN + CRC,失配丢 1 字节重同步)解析器 本身健壮,未发现丢帧逻辑缺陷;`src/selftest.py` 覆盖良好。 - `events = queue.Queue()` 无上限,不会丢事件,只会延迟。 - `link.py` 读线程 20ms 轮询 + `read(4096)`,常规速率下不会溢出驱动缓冲 (115200 下 20ms 最多 288 字节)。 ## 优先级建议 1. 先做根因 2 的"1. 行数上限"(一行代码)——立刻止血卡顿。 2. 再做根因 1 的时间戳随事件携带——日志时间恢复可信。 3. 根因 2 的 2/3/4 与根因 1 的状态/tx 同步改造,作为一轮完整性优化。 ## 复现方法 固件端(FilmForge,GD32F405,RS485 115200): - `M2S500` 开启 500ms 周期温度上报(帧 `B5 F0 04 "25.6" CRC`); - 连续观察 10 分钟以上,或把上报间隔调到 `M2S100` 加速日志增长。 预期:修复前时间戳随 UI 卡顿成批挤压、显示延迟随运行时间增大; 修复后时间戳单调且间隔≈设定周期,长时间运行无卡顿增长。