From 296e220f20813620de1e4ce820540b4fcf5d13fa Mon Sep 17 00:00:00 2001 From: hulk Date: Tue, 13 Oct 2026 11:40:19 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20v0.4.3=20=E4=BF=AE=E5=A4=8D=E9=95=BF?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E8=B6=8A=E8=B7=91=E8=B6=8A=E5=8D=A1/?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E6=97=A0=E5=93=8D=E5=BA=94=E2=80=94=E2=80=94?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E6=89=8B=E5=B7=A5=20cursor=20=E8=A3=81?= =?UTF-8?q?=E5=89=AA=E5=9C=A8=20PySide6=206.11=20=E5=BF=85=E6=8A=9B?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=E4=B8=94=E6=89=93=E6=96=AD=20=5Fpoll=5Fdlpc?= =?UTF-8?q?=20=E6=B6=88=E8=B4=B9=EF=BC=9B=E6=94=B9=20setMaximumBlockCount(?= =?UTF-8?q?500)=20=E7=8E=AF=E5=BD=A2=E8=A3=81=E5=89=AA+=E9=80=90=E4=BA=8B?= =?UTF-8?q?=E4=BB=B6=E5=85=9C=E5=BA=95=EF=BC=9B=E6=96=B0=E5=A2=9E=20selfte?= =?UTF-8?q?st=5Flog=20=E5=9B=9E=E5=BD=92=EF=BC=9B=E9=87=8D=E6=89=93?= =?UTF-8?q?=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DLPilot/AGENTS.md | 21 +++++++++++ DLPilot/src/app_fluent.py | 21 +++++++---- DLPilot/src/selftest_log.py | 70 +++++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 DLPilot/src/selftest_log.py diff --git a/DLPilot/AGENTS.md b/DLPilot/AGENTS.md index c725112..3d66bcf 100644 --- a/DLPilot/AGENTS.md +++ b/DLPilot/AGENTS.md @@ -42,6 +42,8 @@ DLPilot/ | `src/config.py` | 运行目录定位(BASE_DIR)与 pattern_slots.json 读写(slots/area/serial 三键) | | `src/dlpc_rs485.py` | 光机 RS485 协议模块: 帧构造/增量解析/译码表/预设命令/串口线程(DLPC485) | | `src/selftest_rs485.py` | 光机协议自检(对照上游实测帧向量, 无硬件回归, 改协议代码后必跑) | +| `src/selftest_theme.py` | 主题切换回归(离屏, swatch 底色/渲染像素/标题字号, 改样式后必跑) | +| `src/selftest_log.py` | 收发日志上限与队列消费回归(离屏, 改 _dlpc_log/_poll_dlpc 后必跑) | | `src/build_exe.py` | PyInstaller 打包 → `release/DLPilot.exe`(onefile/noconsole, 内嵌图标+pyserial+PySide6-Fluent-Widgets) | | `src/package_release.py` | 复制图案到 release/(平铺) → `release/DLPilot-v-win64.zip`(校验栏位引用完整性; release/ 与 zip 内容一致) | | `src/publish_gitea.py` | 建/复用 Gitea Release 并上传 zip(tag 默认取 zip 文件名) | @@ -174,6 +176,8 @@ python publish_gitea.py ../release/DLPilot-v<版本>-win64.zip --replace --insec 2. `python selftest_rs485.py` —— 光机协议回归(改 dlpc_rs485.py 后必跑,向量来自上游 protocols.md); 3. `python selftest_theme.py` —— 主题切换回归(改 app_fluent.py 涉及样式/主题后必跑; 离屏渲染,Dark/Light 往返后校验 swatch 底色样式与渲染像素、标题字号); +3b. `python selftest_log.py` —— 收发日志上限/队列消费回归(改 `_dlpc_log`/`_poll_dlpc` + 后必跑;600 行封顶 500、坏条目不中断 drain); 4. `python hdmi_gui.py --seconds 5` —— GUI 冒烟(自动开关); 5. `python hdmi_gui.py --test-pattern Checker --area 800x600 --seconds 3` —— 投图链路; 6. `python hdmi_gui.py --dump` —— EDID 解算不抛异常; @@ -234,3 +238,20 @@ python publish_gitea.py ../release/DLPilot-v<版本>-win64.zip --replace --insec 本机 COM5 运行态已还原未入库), https://gitea.hulk.wang/hulk/HW-Debug-Tools/releases/tag/DLPilot-v0.4.2 。 本次发布说明已用 --notes-file 传入(临时文件,未入库)。 +- 2026-10-13 v0.4.3 修复"长会话越跑越卡、命令点了没反应"(FilmForge 同案): + 根因是 `_dlpc_log` 的手工 textCursor 裁剪用了**实例级枚举访问** + (`cursor.Start`/`cursor.BlockUnderCursor`),PySide6 6.11 移除该形式,收发日志 + 超 500 行后每次追加必抛 AttributeError;异常又逃出 `_poll_dlpc` 的 drain 循环 + (旧 try 只捕获 queue.Empty),每 tick 最多消费到第一条 event,`_dlpc_log_q` + 棘轮积压、日志裁剪/自动滚动全失效;电源自检/功率检测序列在 `_pd_step_next()` + 之前的 `_dlpc_log` 调用处被打断,`_pd_seq` 永不清空,后续所有手动命令被 + "序列进行中"拦截直到断开重连。整改:①改用 `setMaximumBlockCount(500)` 环形 + 裁剪(O(1),不依赖枚举写法);②`_poll_dlpc` 改逐事件兜底 try/except,单条异常 + 只打栈放弃该条、不再中断本轮消费;③新增 `selftest_log.py` 回归(600 行封顶 + + 坏条目 drain 不中断)。验证:compileall / selftest_rs485 / selftest_theme / + selftest_log ALL PASS;GUI 冒烟 exit 0;`--dump` / `--dlpc-cmd`(无串口)正常。 + APP_VERSION → 0.4.3 重打包(67.1 MB),按 §8 约定临时还原预置 pattern_slots.json + 后 `package_release.py 0.4.3` 生成 `DLPilot-v0.4.3-win64.zip`(66.8 MB,zip 内 + 为预置 COM15 配置,本机 COM5 运行态已还原未入库)。遗留(未随本版处理): + `dlpc_rs485.py` 的 `except OSError` 把写超时(SerialTimeoutException)与设备 + 拔出混判,一次转换器停滞即断链误报"设备拔出"。 diff --git a/DLPilot/src/app_fluent.py b/DLPilot/src/app_fluent.py index 6ebaa91..e8d34f5 100644 --- a/DLPilot/src/app_fluent.py +++ b/DLPilot/src/app_fluent.py @@ -13,6 +13,7 @@ import queue import re import sys import time +import traceback # PyInstaller keeps Shiboken's ABI DLLs in a sibling directory. Register both # Qt and Shiboken before importing QtCore; this is required by one-file builds @@ -63,7 +64,7 @@ import edid_parse import patterns APP_NAME = "DLPilot" -APP_VERSION = "0.4.2" +APP_VERSION = "0.4.3" REPO_URL = "https://gitea.hulk.wang/hulk/HW-Debug-Tools" MONO = QFont("Consolas", 10) CHIP_IDLE = "#666666" @@ -440,6 +441,10 @@ class App(QMainWindow): trailing=self._button("清空", self._dlpc_clear)) self.txt_dlpc = self._text() self.txt_dlpc.setMinimumHeight(230) + # 收发日志环形裁剪: 超 500 行自动丢最旧(O(1))。勿改回 textCursor 手工裁剪—— + # PySide6 6.11 移除实例级枚举访问, cursor.Start 必抛 AttributeError 且异常会 + # 打断 _poll_dlpc 的队列消费(事件棘轮积压), 回归见 selftest_log.py + self.txt_dlpc.document().setMaximumBlockCount(500) body.addWidget(self.txt_dlpc) self._dlpc_log("帧格式: D5 01 LEN <命令> CRC / B5 TYPE LEN CRC") layout.addWidget(card) @@ -755,9 +760,12 @@ class App(QMainWindow): def _query_lamp(self): self._tl_order.append('LAMP'); self._dlpc_send('M202') def _poll_dlpc(self): - try: - while True: + while True: + try: kind, value = self._dlpc_log_q.get_nowait() + except queue.Empty: + break + try: if kind == 'status': self.lbl_dlpc_st.setText(value) if value.startswith(('已断开', '串口丢失')): self._serial_reset_state() @@ -766,15 +774,16 @@ class App(QMainWindow): elif kind == 'pdframe': self._pd_step_done(value.type_code == 0xF0, value) elif kind == 'pwrframe': self._pwr_step_done(value) else: self._dlpc_log(value) - except queue.Empty: pass + except Exception: + # 单条事件处理异常只打栈放弃该条, 不得中断本轮消费——否则积压随事件流 + # 单调增长(v0.4.3 前日志裁剪异常即此路径, 显示延迟越跑越大) + traceback.print_exc() connected = self.dlpc.is_connected() if connected != self._dlpc_connected: self._dlpc_connected = connected; self.btn_dlpc.setText("断开" if connected else "连接") def _dlpc_log(self, line): self.txt_dlpc.appendPlainText(time.strftime('%H:%M:%S ') + line) - if self.txt_dlpc.blockCount() > 500: - cursor = self.txt_dlpc.textCursor(); cursor.movePosition(cursor.Start); cursor.select(cursor.BlockUnderCursor); cursor.removeSelectedText(); cursor.deleteChar() self.txt_dlpc.verticalScrollBar().setValue(self.txt_dlpc.verticalScrollBar().maximum()) def _dlpc_clear(self): self.txt_dlpc.clear() diff --git a/DLPilot/src/selftest_log.py b/DLPilot/src/selftest_log.py new file mode 100644 index 0000000..2e0a8f8 --- /dev/null +++ b/DLPilot/src/selftest_log.py @@ -0,0 +1,70 @@ +# coding: utf-8 +"""收发日志上限与队列消费回归自检(无窗口, 离屏渲染)。 + +背景: v0.4.3 之前收发日志用 textCursor 手工裁剪, 其中 `cursor.Start` / +`cursor.BlockUnderCursor` 是实例级枚举访问 —— PySide6 6.11 移除了该形式, +日志一旦超过 500 行, 每次 _dlpc_log 追加后必抛 AttributeError; 且异常会 +逃出 _poll_dlpc 的 drain 循环(旧 try 只捕获 queue.Empty), 每 tick 最多 +消费到第一条 event, `_dlpc_log_q` 棘轮积压、日志裁剪与自动滚动全部失效, +电源自检/功率检测序列还会被异常打断在启动处(之后所有命令被"序列进行中" +拦截)。v0.4.3 改为 setMaximumBlockCount 环形裁剪 + 逐事件异常兜底。 + +本测试保证: + 1. 连续追加 600 行日志不抛异常, document().blockCount() 环形封顶 500, 末行在; + 2. 单条异常事件(模拟未来引入的坏数据)不中断 _poll_dlpc 本轮队列消费 + —— 其后的事件仍全部入日志、队列清空。 + +运行: python selftest_log.py (依赖 PySide6-Fluent-Widgets 与 pyserial —— +串口卡构建后 txt_dlpc 才存在) +""" +import os +import sys + +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from PySide6.QtWidgets import QApplication +from qfluentwidgets import Theme, setTheme + +import app_fluent +import dlpc_rs485 + + +def check_cap(win): + for i in range(600): + win._dlpc_log(f"cap-line-{i}") + blocks = win.txt_dlpc.document().blockCount() + has_last = "cap-line-599" in win.txt_dlpc.toPlainText() + print(f" 追加 600 行: blockCount={blocks}(封顶 500), 末行在: {has_last}") + return blocks <= 500 and has_last + + +def check_drain_resilience(win): + win._timer.stop() # 排除 80ms 轮询定时器干扰 + win._dlpc_log_q.put(("pdframe", "poison-not-a-frame")) # 该条分发必抛异常 + win._dlpc_log_q.put(("event", "after-poison-1")) + win._dlpc_log_q.put(("event", "after-poison-2")) + win._poll_dlpc() # 单次手动驱动(不进事件循环) + left = win._dlpc_log_q.qsize() + text = win.txt_dlpc.toPlainText() + drained = left == 0 and "after-poison-1" in text and "after-poison-2" in text + print(f" 坏条目后消费不中断: 残留队列={left}, 后续事件全部写入: {drained}") + return drained + + +def main(): + if not dlpc_rs485.HAS_SERIAL: + print("FAILED 缺少 pyserial: 串口卡未构建, txt_dlpc 不存在") + return 1 + app = QApplication.instance() or QApplication(sys.argv) + setTheme(Theme.AUTO) + win = app_fluent.App(0) + passed = check_cap(win) + passed &= check_drain_resilience(win) + win.close() + print("ALL PASS" if passed else "FAILED") + return 0 if passed else 1 + + +if __name__ == "__main__": + sys.exit(main())