GUI v0.2.9: fix chronic-lag root cause (log trim crash on PySide6 6.11) + 5 hardening fixes

- log trim used instance-level enum access (removed in PySide6 6.11), crashed
This commit is contained in:
2026-10-12 16:14:53 +08:00
parent 60908a7709
commit 8035f9b7d3
7 changed files with 597 additions and 16 deletions
+33 -14
View File
@@ -60,7 +60,7 @@ import config
import filmforge_rs485 as ff
APP_NAME = "FilmForge"
APP_VERSION = "0.2.7"
APP_VERSION = "0.2.9"
REPO_URL = "https://gitea.hulk.wang/hulk/FilmForge"
MONO = QFont("Consolas", 10)
CHIP_IDLE = "#666666"
@@ -69,6 +69,7 @@ CHIP_ERR = "#D9534F"
CHIP_WARN = "#E8833A"
PENDING_TIMEOUT = 1.2 # 应答超时(秒): 协议建议 300ms 重发, 监视台不自动重发只提示
RESPONSE_FRESH = 0.5 # 应答新鲜度: 该窗口内的温度帧优先按“查询应答”派发
MAX_FAULTS = 1000 # 故障标记内存上限(条): NTC 抖动场景防无限增长
CHART_COLORS_DARK = dict(bg="#242424", grid="#3a3a3a", text="#c9c9c9",
measured="#00c8d7", target=CHIP_WARN, fault=CHIP_ERR)
@@ -133,7 +134,7 @@ class TemperatureChart(QWidget):
ts, vs = self._samples_t, self._samples_v
if not ts:
return [], []
lo, hi = 0, len(ts) # 二分找左边界(留一点前文保证连线)
lo, hi = 0, len(ts) # 左边界二分(留一点前文保证连线)
while lo < hi:
m = (lo + hi) // 2
if ts[m] < t0:
@@ -141,7 +142,14 @@ class TemperatureChart(QWidget):
else:
hi = m
lo = max(lo - 1, 0)
t_out, v_out = ts[lo:], vs[lo:]
end, hi = lo, len(ts) # 右边界二分: 暂停(冻结)后不切冻结尾后新样本
while end < hi:
m = (end + hi) // 2
if ts[m] <= t1:
end = m + 1
else:
hi = m
t_out, v_out = ts[lo:end], vs[lo:end]
if len(t_out) > 4000:
stride = math.ceil(len(t_out) / 4000)
gt, gv = [], []
@@ -674,6 +682,9 @@ class App(QMainWindow):
self.txt_log.setFont(MONO)
self.txt_log.setLineWrapMode(QPlainTextEdit.NoWrap)
self.txt_log.setMinimumHeight(200)
# 裁剪走原生环形上限: 手工 cursor 裁剪在 PySide6 6.11 上抛 AttributeError
# (实例级枚举访问已移除), 曾致每 tick 异常中止 + 日志无限增长 + 显示滞后
self.txt_log.setMaximumBlockCount(500)
body.addWidget(self.txt_log)
self._log(f"帧格式: D5 01 LEN <命令> CRC / B5 TYPE LEN <payload> CRC; "
f"响应超时建议 {ff.REPLY_TIMEOUT * 1000:.0f}ms 重发")
@@ -959,6 +970,8 @@ class App(QMainWindow):
self.samples_t.append(t)
self.samples_v.append(float("nan"))
self.faults.append((t, code))
if len(self.faults) > MAX_FAULTS: # 封顶: 抖动场景不无限增长(_draw_faults 按 paint 全量扫描)
del self.faults[:len(self.faults) - MAX_FAULTS]
self.lbl_temp.setText("—.— ℃")
self._csv_write(t, code, ok=False)
self.chart.update()
@@ -1266,9 +1279,19 @@ class App(QMainWindow):
target = f"{self.target_hist[-1][1]:g}" if self.target_hist else ""
heater = self.heater_state or ""
sensor = self.sensor_state or ("OK" if ok else "")
self._csv.write(f"{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(t))},"
f"{t - self._csv_t0:.3f},{temp_text},{target},{heater},{sensor}\n")
self._csv.flush()
try:
self._csv.write(f"{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(t))},"
f"{t - self._csv_t0:.3f},{temp_text},{target},{heater},{sensor}\n")
self._csv.flush()
except OSError as e:
# 写失败(磁盘满/记录盘被拔): 自动停记录。本函数在 _tick 帧处理路径上,
# 未捕获异常会中止整段消费(与 v0.2.8 日志裁剪同款失效管道, 勿回退)
self._csv.close()
self._csv = None
self.btn_csv.setText("开始记录")
self.lbl_csv.setText(f"记录失败已停止: {e}")
self._log(f"⚠ CSV 记录失败已停止: {e}")
return
self._csv_count += 1
self.lbl_csv.setText(f"记录中({self._csv_count} 行) → "
f"{os.path.basename(self._csv_path)}")
@@ -1279,15 +1302,11 @@ class App(QMainWindow):
# ================= 日志 =================
def _log(self, line):
self.txt_log.appendPlainText(time.strftime("%H:%M:%S ") + line)
if self.txt_log.blockCount() > 500:
cursor = self.txt_log.textCursor()
cursor.movePosition(cursor.Start)
cursor.select(cursor.BlockUnderCursor)
cursor.removeSelectedText()
cursor.deleteChar()
sb = self.txt_log.verticalScrollBar()
sb.setValue(sb.maximum())
at_bottom = sb.value() >= sb.maximum() - 4
self.txt_log.appendPlainText(time.strftime("%H:%M:%S ") + line)
if at_bottom: # 仅原本贴底时跟随, 不抢用户上翻看历史
sb.setValue(sb.maximum())
def _clear_log(self):
self.txt_log.clear()