generated from hulk/gd32f4xx_template_cmake_vscode
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:
@@ -160,9 +160,40 @@
|
|||||||
(app+window 两级 setWindowIcon,_icon_path() 兼容源码态与 _MEIPASS 打包态)、
|
(app+window 两级 setWindowIcon,_icon_path() 兼容源码态与 _MEIPASS 打包态)、
|
||||||
Windows 任务栏独立标识 AppUserModelID "hulk.FilmForgeGUI"。README"无应用
|
Windows 任务栏独立标识 AppUserModelID "hulk.FilmForgeGUI"。README"无应用
|
||||||
图标"遗留项关闭。
|
图标"遗留项关闭。
|
||||||
|
- v0.2.8:**慢性卡顿修复**——`_log()` 手工 cursor 裁剪(`cursor.Start`/
|
||||||
|
`BlockUnderCursor` 实例级枚举访问)在 PySide6 6.11 上必然抛 AttributeError,
|
||||||
|
日志超 500 行(100ms 上报 ≈50s)后每个 100ms tick 异常中止 `_tick` 后半段:
|
||||||
|
应答超时清理失效(pending 永不清 → "停止"点击被幂等去重永久拦截,实拍状态栏
|
||||||
|
"M2S0 应答中,已忽略重复发送")、RX 队列积压 ratchet(帧处理滞后 54ms→均值
|
||||||
|
1037ms/峰值 2701ms,日志/chip/曲线显示滞后数秒,"停止 5s 后才见命令发出"实为
|
||||||
|
日志显示积压,TX 本身 5~166ms 即出)、日志无限增长、CPU +30%。改原生
|
||||||
|
`setMaximumBlockCount(500)` 环形裁剪;selftest 新增"500 行封顶/封顶后可追加/
|
||||||
|
最新行在末尾"3 条回归。离屏 A/B 实测(GUI/_diag_latency_buggy.py,假固件
|
||||||
|
100ms 上报+定时点击):buggy 组 5.5 分钟异常 2241 次、修复组全程漂移 ≤15ms/
|
||||||
|
零异常/零积压。v0.2.7 exe(2026-10-10 打包)内嵌同版本 PySide6 6.11.2,
|
||||||
|
同带此 bug,需重新打包生效。
|
||||||
|
- v0.2.9:**整体审查修复 5 项**——①CSV 写失败(磁盘满/记录盘被拔)自动停记录
|
||||||
|
并提示,不再沿 _tick 帧处理路径抛 OSError 中止主循环(与 v0.2.8 日志裁剪同款
|
||||||
|
失效管道);②故障标记 faults 封顶 1000 条(曾是唯一无上限缓冲:NTC 抖动时
|
||||||
|
10 条/s 无限增长,且 _draw_faults 每 paint 全量扫描);③串口写超时单独捕获
|
||||||
|
SerialTimeoutException——只丢该条命令、链路保留,不再按 OSError 当"设备拔出"
|
||||||
|
关端口断链(适配器停滞曾迫使手动重连);④日志自动滚动仅在贴底时跟随(上报
|
||||||
|
运行时可上翻历史,不再被拽回底部);⑤冻结视图切片右缘同样二分(暂停时长不再
|
||||||
|
增加 render 成本)。selftest 新增 7 项回归(封顶/滚动跟随/CSV 失败自停/冻结
|
||||||
|
切片)共 59 项全过。整体审查报告(2026-10-12)其余 P3 遗留:selftest 未覆盖
|
||||||
|
_tick 主循环(超时清理/丢失判定)、"全部"窗口全量切片有 MAX_SAMPLES 兜底、
|
||||||
|
_last_set 单槽位罕见误补偿(8s 自愈)、build_exe remove_stale 无提示强杀
|
||||||
|
运行实例、M888 后固定 2s 重查可能早于固件就绪、_danger_reset 把去重忽略记成
|
||||||
|
"已下发"。协议建议的 300ms×3 自动重发维持不做(设计决策,用户拍板)。
|
||||||
- selftest 全过(累计新增 15 项回归用例,含窗口图标非空检查);exe 已本地
|
- selftest 全过(累计新增 15 项回归用例,含窗口图标非空检查);exe 已本地
|
||||||
打包冒烟,Gitea 发布待用户确认后执行。
|
打包冒烟,Gitea 发布待用户确认后执行。
|
||||||
遗留见其 README"已知事项":USB-RS485 适配器更换。
|
遗留见其 README"已知事项":USB-RS485 适配器更换。
|
||||||
|
- [x] **GUI 慢性卡顿修复(根因已定位并修复 v0.2.8,待真机确认)**:
|
||||||
|
根因/机理/实测数据见上方 v0.2.8 条目(`cursor.Start` 实例级枚举访问在
|
||||||
|
PySide6 6.11 必抛 → tick 异常中止 → 应答超时清理失效 + RX 积压 ratchet +
|
||||||
|
日志无限增长)。复现/验证脚本:`GUI/_diag_latency.py`(修复基线)/
|
||||||
|
`GUI/_diag_latency_buggy.py`(带 bug 对照,假固件 100ms 上报+定时点击)。
|
||||||
|
遗留:待真机长时间运行确认 + 重新打包 exe。
|
||||||
- [ ] **LED 引脚确认**:FilmForge 自制板 LED 引脚未定(当前沿用模板默认 PA15,
|
- [ ] **LED 引脚确认**:FilmForge 自制板 LED 引脚未定(当前沿用模板默认 PA15,
|
||||||
仅为构建占位),按原理图修改。
|
仅为构建占位),按原理图修改。
|
||||||
- [ ] **delay_10us 校准**:M4@168MHz 系数为估算值(/300000),需实测修正。
|
- [ ] **delay_10us 校准**:M4@168MHz 系数为估算值(/300000),需实测修正。
|
||||||
|
|||||||
@@ -75,6 +75,19 @@ python publish_gitea.py release/FilmForge-GUI-v<版本>-win64.zip --replace --in
|
|||||||
才返回(Windows 串口"字节间隔超时"150ms 因帧间隔更短永不触发,100ms 上报时
|
才返回(Windows 串口"字节间隔超时"150ms 因帧间隔更短永不触发,100ms 上报时
|
||||||
≈2.5s 一批),表现为曲线/命令全部 3 秒一卡、一股脑一大坨。已改为
|
≈2.5s 一批),表现为曲线/命令全部 3 秒一卡、一股脑一大坨。已改为
|
||||||
`read(in_waiting or 1)`,延迟回到 ≤150ms 量级(v0.2.2,真机实测确认)。
|
`read(in_waiting or 1)`,延迟回到 ≤150ms 量级(v0.2.2,真机实测确认)。
|
||||||
|
- **日志裁剪走原生 `setMaximumBlockCount(500)`**:手工 `textCursor` 裁剪用了
|
||||||
|
`cursor.Start`/`cursor.BlockUnderCursor` 实例级枚举访问,PySide6 6.11 已移除
|
||||||
|
(类上仍在),必然抛 AttributeError——日志超 500 行(100ms 上报 ≈50s)后每个
|
||||||
|
100ms tick 在 `_log` 处异常中止:`_tick` 后半段(应答超时清理/连接状态机/统计
|
||||||
|
节流)永不执行(pending 永不清 → "停止"点击被幂等去重永久拦截)、RX 队列积压
|
||||||
|
ratchet(帧处理滞后涨到 1~2.7s,日志/chip/曲线显示全面滞后)、日志无限增长。
|
||||||
|
v0.2.8 改原生环形裁剪修复(离屏 A/B 实测:修复后 6 分钟漂移 ≤15ms、零异常、
|
||||||
|
零积压;selftest 新增 3 条回归用例)。
|
||||||
|
- **日志自动滚动仅在贴底时跟随**:上报运行时可上翻查看历史(每行强制滚底会抢
|
||||||
|
滚动条);CSV 写失败(磁盘满/记录盘被拔)自动停止记录并提示——`_csv_write` 在
|
||||||
|
`_tick` 帧处理路径上,未捕获异常会中止主循环(v0.2.8 日志裁剪同款教训);故障
|
||||||
|
标记封顶 1000 条;冻结视图切片右缘二分(暂停时长不增加渲染成本);串口写超时
|
||||||
|
只丢该条命令、链路保留(不按设备拔出断链)。均 v0.2.9。
|
||||||
- Fluent 控件自定义样式必须走 `setCustomStyleSheet`(覆盖 hover/pressed/disabled),
|
- Fluent 控件自定义样式必须走 `setCustomStyleSheet`(覆盖 hover/pressed/disabled),
|
||||||
直接 `setStyleSheet` 会在切主题时被库重刷(DLPilot 同款坑);标题字号走 QFont。
|
直接 `setStyleSheet` 会在切主题时被库重刷(DLPilot 同款坑);标题字号走 QFont。
|
||||||
- 图表刷新双节流:数据到达即重绘(≤10Hz)+ 0.5s 时间轴推进;“全部”窗口超 4000 点
|
- 图表刷新双节流:数据到达即重绘(≤10Hz)+ 0.5s 时间轴推进;“全部”窗口超 4000 点
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""诊断 v2: 干净基线 — 旁路 app_fluent._log 的坏裁剪(cursor.Start 在 PySide6
|
||||||
|
6.11.2 上抛 AttributeError), 用 Qt 原生 setMaximumBlockCount 替代, 其余不动源码。
|
||||||
|
目的: 验证 100ms 持续上报数分钟下, 修复裁剪后 点击->TX 是否仍瞬时、事件循环
|
||||||
|
是否仍流畅(对照 buggy 版的 tick 中止 + 队列积压)。"""
|
||||||
|
import ctypes
|
||||||
|
import gc
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
import filmforge_rs485 as ff
|
||||||
|
|
||||||
|
|
||||||
|
class FakeSerial:
|
||||||
|
"""模拟 CH34x + 固件: M2S<ms> 控制周期温度帧, M2S0 回 ok 并停流。"""
|
||||||
|
|
||||||
|
def __init__(self, *a, **kw):
|
||||||
|
self._buf = bytearray()
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self._alive = True
|
||||||
|
self.write_log = [] # [(time, cmd_bytes)]
|
||||||
|
self.report_ms = 0
|
||||||
|
self._next_tx = 0.0
|
||||||
|
self._th = threading.Thread(target=self._fw, daemon=True)
|
||||||
|
self._th.start()
|
||||||
|
FakeSerial.last = self
|
||||||
|
|
||||||
|
def _fw(self):
|
||||||
|
while self._alive:
|
||||||
|
now = time.time()
|
||||||
|
if self.report_ms and now >= self._next_tx:
|
||||||
|
self._push(self._frame("34.6"))
|
||||||
|
self._next_tx = now + self.report_ms / 1000.0
|
||||||
|
time.sleep(0.002)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _frame(payload, type_code=0xF0):
|
||||||
|
body = bytes([type_code, len(payload)]) + payload.encode("ascii")
|
||||||
|
return bytes([0xB5]) + body + bytes([ff.sum_crc(body)])
|
||||||
|
|
||||||
|
def _push(self, b):
|
||||||
|
with self._lock:
|
||||||
|
self._buf += b
|
||||||
|
|
||||||
|
@property
|
||||||
|
def in_waiting(self):
|
||||||
|
with self._lock:
|
||||||
|
return len(self._buf)
|
||||||
|
|
||||||
|
def read(self, n):
|
||||||
|
deadline = time.time() + 0.15
|
||||||
|
while True:
|
||||||
|
with self._lock:
|
||||||
|
if self._buf:
|
||||||
|
out = bytes(self._buf[:n])
|
||||||
|
del self._buf[:n]
|
||||||
|
return out
|
||||||
|
if time.time() >= deadline:
|
||||||
|
return b""
|
||||||
|
time.sleep(0.004)
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
self.write_log.append((time.time(), bytes(data)))
|
||||||
|
cmd = bytes(data[3:-1]).decode("ascii", "replace").strip()
|
||||||
|
threading.Timer(0.05, lambda: self._on_cmd(cmd)).start()
|
||||||
|
return len(data)
|
||||||
|
|
||||||
|
def _on_cmd(self, cmd):
|
||||||
|
if cmd.startswith("M2S"):
|
||||||
|
try:
|
||||||
|
ms = int(cmd[3:])
|
||||||
|
except ValueError:
|
||||||
|
ms = 0
|
||||||
|
self.report_ms = ms
|
||||||
|
self._next_tx = time.time() + ms / 1000.0
|
||||||
|
self._push(self._frame("ok"))
|
||||||
|
elif cmd in ("M1", "M2", "M3", "M4", "M999"):
|
||||||
|
self._push(self._frame({"M1": "34.6", "M2": "OFF", "M3": "OFF",
|
||||||
|
"M4": "35.0", "M999": "v41.0.0.1"}[cmd]))
|
||||||
|
else:
|
||||||
|
self._push(self._frame("ok"))
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self._alive = False
|
||||||
|
try:
|
||||||
|
self._th.join(timeout=0.5)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
ff.serial.Serial = FakeSerial
|
||||||
|
|
||||||
|
from PySide6.QtCore import QTimer
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
|
app = QApplication([])
|
||||||
|
import app_fluent
|
||||||
|
|
||||||
|
# ---- 测试侧修复: 旁路坏裁剪(原生上限 500 行, 语义等同原意图) ----
|
||||||
|
window = app_fluent.App(0)
|
||||||
|
window.txt_log.setMaximumBlockCount(500) # blockCount 永不 >500, 坏分支不触发
|
||||||
|
window.show()
|
||||||
|
fake = None
|
||||||
|
exception_count = [0]
|
||||||
|
|
||||||
|
# ---- 指标 ----
|
||||||
|
stats = {"paint": [0, 0.0, 0.0], "tick": [0, 0.0, 0.0],
|
||||||
|
"gc": [0, 0.0, 0.0], "drift": [], "qmax": [0], "click": []}
|
||||||
|
|
||||||
|
_orig_paint = app_fluent.TemperatureChart.paintEvent
|
||||||
|
def timed_paint(self, ev):
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
_orig_paint(self, ev)
|
||||||
|
dt = time.perf_counter() - t0
|
||||||
|
s = stats["paint"]; s[0] += 1; s[1] += dt; s[2] = max(s[2], dt)
|
||||||
|
app_fluent.TemperatureChart.paintEvent = timed_paint
|
||||||
|
|
||||||
|
_orig_tick = app_fluent.App._tick
|
||||||
|
def timed_tick(self):
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
try:
|
||||||
|
_orig_tick(self)
|
||||||
|
except Exception as e:
|
||||||
|
exception_count[0] += 1
|
||||||
|
if exception_count[0] <= 3:
|
||||||
|
print("TICK EXCEPTION:", e, flush=True)
|
||||||
|
dt = time.perf_counter() - t0
|
||||||
|
s = stats["tick"]; s[0] += 1; s[1] += dt; s[2] = max(s[2], dt)
|
||||||
|
app_fluent.App._tick = timed_tick
|
||||||
|
|
||||||
|
_gc_t = [0.0]
|
||||||
|
def gc_cb(phase, info):
|
||||||
|
if phase == "start":
|
||||||
|
_gc_t[0] = time.perf_counter()
|
||||||
|
else:
|
||||||
|
dt = time.perf_counter() - _gc_t[0]
|
||||||
|
s = stats["gc"]; s[0] += 1; s[1] += dt; s[2] = max(s[2], dt)
|
||||||
|
gc.callbacks.append(gc_cb)
|
||||||
|
|
||||||
|
probe = {"expect": None}
|
||||||
|
def probe_fire():
|
||||||
|
now = time.time()
|
||||||
|
if probe["expect"] is None:
|
||||||
|
probe["expect"] = now + 0.02
|
||||||
|
return
|
||||||
|
stats["drift"].append((now, now - probe["expect"]))
|
||||||
|
probe["expect"] += 0.02
|
||||||
|
if now - probe["expect"] > 0.5:
|
||||||
|
probe["expect"] = now + 0.02
|
||||||
|
pt = QTimer(); pt.timeout.connect(probe_fire); pt.start(20)
|
||||||
|
|
||||||
|
CLICK_PERIOD = 30
|
||||||
|
click = {"expect": None, "n": 0}
|
||||||
|
|
||||||
|
def do_click_stop():
|
||||||
|
now = time.time()
|
||||||
|
drift_ms = (now - click["expect"]) * 1000 if click["expect"] else 0.0
|
||||||
|
q0 = window._rx_q.qsize()
|
||||||
|
window._send_set("M2S0", "report", ("OFF", None)) # "停止"按钮真实路径
|
||||||
|
click["n"] += 1
|
||||||
|
stats["click"].append(dict(n=click["n"], t=now, drift_ms=drift_ms,
|
||||||
|
qsize=q0, pending=len(window._pending),
|
||||||
|
samples=len(window.samples_t),
|
||||||
|
logblk=window.txt_log.blockCount()))
|
||||||
|
QTimer.singleShot(3000, do_click_restart)
|
||||||
|
|
||||||
|
def do_click_restart():
|
||||||
|
window._send_set("M2S100", "report", ("ON", 100))
|
||||||
|
click["expect"] = time.time() + CLICK_PERIOD
|
||||||
|
QTimer.singleShot(CLICK_PERIOD * 1000, do_click_stop)
|
||||||
|
|
||||||
|
def start():
|
||||||
|
global fake
|
||||||
|
window.link.open("COMFAKE", 115200)
|
||||||
|
def wait_conn():
|
||||||
|
if window.link.is_connected():
|
||||||
|
fake = FakeSerial.last
|
||||||
|
click["expect"] = time.time() + 15
|
||||||
|
QTimer.singleShot(15000, do_click_stop)
|
||||||
|
print("[t0] connected; stop-clicks every %ds from +15s" % CLICK_PERIOD,
|
||||||
|
flush=True)
|
||||||
|
else:
|
||||||
|
QTimer.singleShot(200, wait_conn)
|
||||||
|
QTimer.singleShot(200, wait_conn)
|
||||||
|
QTimer.singleShot(100, start)
|
||||||
|
|
||||||
|
t_start = time.time()
|
||||||
|
DURATION = float(sys.argv[1]) if len(sys.argv) > 1 else 320.0
|
||||||
|
INTERVAL = float(sys.argv[2]) if len(sys.argv) > 2 else 40.0
|
||||||
|
|
||||||
|
def monitor():
|
||||||
|
while time.time() - t_start < DURATION:
|
||||||
|
time.sleep(INTERVAL)
|
||||||
|
el = time.time() - t_start
|
||||||
|
recent = [d for t, d in stats["drift"] if t >= time.time() - INTERVAL]
|
||||||
|
rmax = max(recent) * 1000 if recent else 0
|
||||||
|
stats["qmax"][0] = max(stats["qmax"][0], window._rx_q.qsize())
|
||||||
|
print(f"[{el:6.1f}s] cpu={time.process_time():7.2f}s "
|
||||||
|
f"samples={len(window.samples_t):6d} q={window._rx_q.qsize():4d} "
|
||||||
|
f"(max {stats['qmax'][0]:4d}) logblk={window.txt_log.blockCount():4d} "
|
||||||
|
f"paint(n={stats['paint'][0]:5d} max={stats['paint'][2]*1000:6.1f}ms) "
|
||||||
|
f"tick_max={stats['tick'][2]*1000:6.1f}ms "
|
||||||
|
f"gc_max={stats['gc'][2]*1000:5.1f}ms drift_max={rmax:7.1f}ms "
|
||||||
|
f"exc={exception_count[0]}", flush=True)
|
||||||
|
print("==== CLICK 明细 ====", flush=True)
|
||||||
|
for c in stats["click"]:
|
||||||
|
tx = next((t for t, d in FakeSerial.last.write_log
|
||||||
|
if d[3:-1].decode("ascii", "replace") == "M2S0"
|
||||||
|
and c["t"] <= t <= c["t"] + 2.0), None)
|
||||||
|
total = (tx - c["t"]) * 1000 if tx else -1
|
||||||
|
print(f"click#{c['n']} t=+{c['t']-t_start:6.1f}s 调度漂移={c['drift_ms']:8.1f}ms "
|
||||||
|
f"click->TX={total:7.1f}ms q={c['qsize']:4d} pending={c['pending']} "
|
||||||
|
f"samples={c['samples']:6d} logblk={c['logblk']}", flush=True)
|
||||||
|
ds = sorted(d for _t, d in stats["drift"])
|
||||||
|
if ds:
|
||||||
|
print(f"drift: n={len(ds)} p50={ds[len(ds)//2]*1000:.1f}ms "
|
||||||
|
f"p99={ds[int(len(ds)*0.99)]*1000:.1f}ms max={ds[-1]*1000:.1f}ms "
|
||||||
|
f">500ms次数={sum(1 for d in ds if d>0.5)}", flush=True)
|
||||||
|
print(f"paint total={stats['paint'][1]:.2f}s max={stats['paint'][2]*1000:.1f}ms | "
|
||||||
|
f"tick total={stats['tick'][1]:.2f}s max={stats['tick'][2]*1000:.1f}ms | "
|
||||||
|
f"gc total={stats['gc'][1]:.2f}s max={stats['gc'][2]*1000:.1f}ms "
|
||||||
|
f"n={stats['gc'][0]} | exceptions={exception_count[0]}", flush=True)
|
||||||
|
os._exit(0)
|
||||||
|
|
||||||
|
threading.Thread(target=monitor, daemon=True).start()
|
||||||
|
app.exec()
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""诊断 v2b: BUGGY 对照组 — 不加裁剪修复, 复现 PySide6 6.11.2 上
|
||||||
|
_log 裁剪 AttributeError 触发后的行为: tick 中止 / RX 处理滞后 ratchet /
|
||||||
|
事件循环漂移 / 点击->TX。指标同 v2, 另增帧处理年龄(frame_age)。"""
|
||||||
|
import gc
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
import filmforge_rs485 as ff
|
||||||
|
|
||||||
|
|
||||||
|
class FakeSerial:
|
||||||
|
def __init__(self, *a, **kw):
|
||||||
|
self._buf = bytearray()
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self._alive = True
|
||||||
|
self.write_log = []
|
||||||
|
self.report_ms = 0
|
||||||
|
self._next_tx = 0.0
|
||||||
|
self._th = threading.Thread(target=self._fw, daemon=True)
|
||||||
|
self._th.start()
|
||||||
|
FakeSerial.last = self
|
||||||
|
|
||||||
|
def _fw(self):
|
||||||
|
while self._alive:
|
||||||
|
now = time.time()
|
||||||
|
if self.report_ms and now >= self._next_tx:
|
||||||
|
self._push(self._frame("34.6"))
|
||||||
|
self._next_tx = now + self.report_ms / 1000.0
|
||||||
|
time.sleep(0.002)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _frame(payload, type_code=0xF0):
|
||||||
|
body = bytes([type_code, len(payload)]) + payload.encode("ascii")
|
||||||
|
return bytes([0xB5]) + body + bytes([ff.sum_crc(body)])
|
||||||
|
|
||||||
|
def _push(self, b):
|
||||||
|
with self._lock:
|
||||||
|
self._buf += b
|
||||||
|
|
||||||
|
@property
|
||||||
|
def in_waiting(self):
|
||||||
|
with self._lock:
|
||||||
|
return len(self._buf)
|
||||||
|
|
||||||
|
def read(self, n):
|
||||||
|
deadline = time.time() + 0.15
|
||||||
|
while True:
|
||||||
|
with self._lock:
|
||||||
|
if self._buf:
|
||||||
|
out = bytes(self._buf[:n])
|
||||||
|
del self._buf[:n]
|
||||||
|
return out
|
||||||
|
if time.time() >= deadline:
|
||||||
|
return b""
|
||||||
|
time.sleep(0.004)
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
self.write_log.append((time.time(), bytes(data)))
|
||||||
|
cmd = bytes(data[3:-1]).decode("ascii", "replace").strip()
|
||||||
|
threading.Timer(0.05, lambda: self._on_cmd(cmd)).start()
|
||||||
|
return len(data)
|
||||||
|
|
||||||
|
def _on_cmd(self, cmd):
|
||||||
|
if cmd.startswith("M2S"):
|
||||||
|
try:
|
||||||
|
ms = int(cmd[3:])
|
||||||
|
except ValueError:
|
||||||
|
ms = 0
|
||||||
|
self.report_ms = ms
|
||||||
|
self._next_tx = time.time() + ms / 1000.0
|
||||||
|
self._push(self._frame("ok"))
|
||||||
|
elif cmd in ("M1", "M2", "M3", "M4", "M999"):
|
||||||
|
self._push(self._frame({"M1": "34.6", "M2": "OFF", "M3": "OFF",
|
||||||
|
"M4": "35.0", "M999": "v41.0.0.1"}[cmd]))
|
||||||
|
else:
|
||||||
|
self._push(self._frame("ok"))
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self._alive = False
|
||||||
|
try:
|
||||||
|
self._th.join(timeout=0.5)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
ff.serial.Serial = FakeSerial
|
||||||
|
|
||||||
|
from PySide6.QtCore import QTimer
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
|
app = QApplication([])
|
||||||
|
import app_fluent
|
||||||
|
|
||||||
|
# 注意: 不打 setMaximumBlockCount 补丁 —— 原样复现 bug
|
||||||
|
window = app_fluent.App(0)
|
||||||
|
window.show()
|
||||||
|
fake = None
|
||||||
|
exception_count = [0]
|
||||||
|
|
||||||
|
stats = {"tick": [0, 0.0, 0.0], "gc": [0, 0.0, 0.0], "drift": [],
|
||||||
|
"qmax": [0], "click": [], "age": [0, 0.0, 0.0]} # age: n,total,max
|
||||||
|
|
||||||
|
_orig_tick = app_fluent.App._tick
|
||||||
|
def timed_tick(self):
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
try:
|
||||||
|
_orig_tick(self)
|
||||||
|
except Exception as e:
|
||||||
|
exception_count[0] += 1
|
||||||
|
if exception_count[0] <= 3:
|
||||||
|
print("TICK EXCEPTION:", type(e).__name__, e, flush=True)
|
||||||
|
dt = time.perf_counter() - t0
|
||||||
|
s = stats["tick"]; s[0] += 1; s[1] += dt; s[2] = max(s[2], dt)
|
||||||
|
app_fluent.App._tick = timed_tick
|
||||||
|
|
||||||
|
_orig_handle = app_fluent.App._handle_frame
|
||||||
|
def timed_handle(self, fr, t_rx):
|
||||||
|
_orig_handle(self, fr, t_rx)
|
||||||
|
age = time.time() - t_rx # 帧从收到到被主线程处理的滞后
|
||||||
|
s = stats["age"]; s[0] += 1; s[1] += age; s[2] = max(s[2], age)
|
||||||
|
app_fluent.App._handle_frame = timed_handle
|
||||||
|
|
||||||
|
_gc_t = [0.0]
|
||||||
|
def gc_cb(phase, info):
|
||||||
|
if phase == "start":
|
||||||
|
_gc_t[0] = time.perf_counter()
|
||||||
|
else:
|
||||||
|
dt = time.perf_counter() - _gc_t[0]
|
||||||
|
s = stats["gc"]; s[0] += 1; s[1] += dt; s[2] = max(s[2], dt)
|
||||||
|
gc.callbacks.append(gc_cb)
|
||||||
|
|
||||||
|
probe = {"expect": None}
|
||||||
|
def probe_fire():
|
||||||
|
now = time.time()
|
||||||
|
if probe["expect"] is None:
|
||||||
|
probe["expect"] = now + 0.02
|
||||||
|
return
|
||||||
|
stats["drift"].append((now, now - probe["expect"]))
|
||||||
|
probe["expect"] += 0.02
|
||||||
|
if now - probe["expect"] > 0.5:
|
||||||
|
probe["expect"] = now + 0.02
|
||||||
|
pt = QTimer(); pt.timeout.connect(probe_fire); pt.start(20)
|
||||||
|
|
||||||
|
CLICK_PERIOD = 30
|
||||||
|
click = {"expect": None, "n": 0}
|
||||||
|
|
||||||
|
def do_click_stop():
|
||||||
|
now = time.time()
|
||||||
|
drift_ms = (now - click["expect"]) * 1000 if click["expect"] else 0.0
|
||||||
|
q0 = window._rx_q.qsize()
|
||||||
|
window._send_set("M2S0", "report", ("OFF", None))
|
||||||
|
click["n"] += 1
|
||||||
|
stats["click"].append(dict(n=click["n"], t=now, drift_ms=drift_ms,
|
||||||
|
qsize=q0, pending=len(window._pending),
|
||||||
|
samples=len(window.samples_t),
|
||||||
|
logblk=window.txt_log.blockCount()))
|
||||||
|
QTimer.singleShot(3000, do_click_restart)
|
||||||
|
|
||||||
|
def do_click_restart():
|
||||||
|
window._send_set("M2S100", "report", ("ON", 100))
|
||||||
|
click["expect"] = time.time() + CLICK_PERIOD
|
||||||
|
QTimer.singleShot(CLICK_PERIOD * 1000, do_click_stop)
|
||||||
|
|
||||||
|
def start():
|
||||||
|
global fake
|
||||||
|
window.link.open("COMFAKE", 115200)
|
||||||
|
def wait_conn():
|
||||||
|
if window.link.is_connected():
|
||||||
|
fake = FakeSerial.last
|
||||||
|
click["expect"] = time.time() + 15
|
||||||
|
QTimer.singleShot(15000, do_click_stop)
|
||||||
|
print("[t0] connected; stop-clicks every %ds from +15s (BUGGY mode)"
|
||||||
|
% CLICK_PERIOD, flush=True)
|
||||||
|
else:
|
||||||
|
QTimer.singleShot(200, wait_conn)
|
||||||
|
QTimer.singleShot(200, wait_conn)
|
||||||
|
QTimer.singleShot(100, start)
|
||||||
|
|
||||||
|
t_start = time.time()
|
||||||
|
DURATION = float(sys.argv[1]) if len(sys.argv) > 1 else 320.0
|
||||||
|
INTERVAL = float(sys.argv[2]) if len(sys.argv) > 2 else 40.0
|
||||||
|
|
||||||
|
def monitor():
|
||||||
|
while time.time() - t_start < DURATION:
|
||||||
|
time.sleep(INTERVAL)
|
||||||
|
el = time.time() - t_start
|
||||||
|
recent = [d for t, d in stats["drift"] if t >= time.time() - INTERVAL]
|
||||||
|
rmax = max(recent) * 1000 if recent else 0
|
||||||
|
stats["qmax"][0] = max(stats["qmax"][0], window._rx_q.qsize())
|
||||||
|
n, tot, mx = stats["age"]
|
||||||
|
print(f"[{el:6.1f}s] cpu={time.process_time():7.2f}s "
|
||||||
|
f"samples={len(window.samples_t):6d} q={window._rx_q.qsize():5d} "
|
||||||
|
f"(max {stats['qmax'][0]:5d}) logblk={window.txt_log.blockCount():5d} "
|
||||||
|
f"frame_age avg={tot/max(n,1)*1000:7.1f}ms max={mx*1000:8.1f}ms "
|
||||||
|
f"tick_max={stats['tick'][2]*1000:6.1f}ms drift_max={rmax:8.1f}ms "
|
||||||
|
f"exc={exception_count[0]}", flush=True)
|
||||||
|
print("==== CLICK 明细 ====", flush=True)
|
||||||
|
for c in stats["click"]:
|
||||||
|
tx = next((t for t, d in FakeSerial.last.write_log
|
||||||
|
if d[3:-1].decode("ascii", "replace") == "M2S0"
|
||||||
|
and c["t"] <= t <= c["t"] + 2.0), None)
|
||||||
|
total = (tx - c["t"]) * 1000 if tx else -1
|
||||||
|
print(f"click#{c['n']} t=+{c['t']-t_start:6.1f}s 调度漂移={c['drift_ms']:8.1f}ms "
|
||||||
|
f"click->TX={total:7.1f}ms q={c['qsize']:5d} pending={c['pending']} "
|
||||||
|
f"samples={c['samples']:6d} logblk={c['logblk']}", flush=True)
|
||||||
|
ds = sorted(d for _t, d in stats["drift"])
|
||||||
|
if ds:
|
||||||
|
print(f"drift: n={len(ds)} p50={ds[len(ds)//2]*1000:.1f}ms "
|
||||||
|
f"p99={ds[int(len(ds)*0.99)]*1000:.1f}ms max={ds[-1]*1000:.1f}ms "
|
||||||
|
f">500ms次数={sum(1 for d in ds if d>0.5)}", flush=True)
|
||||||
|
n, tot, mx = stats["age"]
|
||||||
|
print(f"frame_age avg={tot/max(n,1)*1000:.1f}ms max={mx*1000:.1f}ms | "
|
||||||
|
f"exceptions={exception_count[0]} | tick n={stats['tick'][0]} "
|
||||||
|
f"max={stats['tick'][2]*1000:.1f}ms", flush=True)
|
||||||
|
os._exit(0)
|
||||||
|
|
||||||
|
threading.Thread(target=monitor, daemon=True).start()
|
||||||
|
app.exec()
|
||||||
+33
-14
@@ -60,7 +60,7 @@ import config
|
|||||||
import filmforge_rs485 as ff
|
import filmforge_rs485 as ff
|
||||||
|
|
||||||
APP_NAME = "FilmForge"
|
APP_NAME = "FilmForge"
|
||||||
APP_VERSION = "0.2.7"
|
APP_VERSION = "0.2.9"
|
||||||
REPO_URL = "https://gitea.hulk.wang/hulk/FilmForge"
|
REPO_URL = "https://gitea.hulk.wang/hulk/FilmForge"
|
||||||
MONO = QFont("Consolas", 10)
|
MONO = QFont("Consolas", 10)
|
||||||
CHIP_IDLE = "#666666"
|
CHIP_IDLE = "#666666"
|
||||||
@@ -69,6 +69,7 @@ CHIP_ERR = "#D9534F"
|
|||||||
CHIP_WARN = "#E8833A"
|
CHIP_WARN = "#E8833A"
|
||||||
PENDING_TIMEOUT = 1.2 # 应答超时(秒): 协议建议 300ms 重发, 监视台不自动重发只提示
|
PENDING_TIMEOUT = 1.2 # 应答超时(秒): 协议建议 300ms 重发, 监视台不自动重发只提示
|
||||||
RESPONSE_FRESH = 0.5 # 应答新鲜度: 该窗口内的温度帧优先按“查询应答”派发
|
RESPONSE_FRESH = 0.5 # 应答新鲜度: 该窗口内的温度帧优先按“查询应答”派发
|
||||||
|
MAX_FAULTS = 1000 # 故障标记内存上限(条): NTC 抖动场景防无限增长
|
||||||
|
|
||||||
CHART_COLORS_DARK = dict(bg="#242424", grid="#3a3a3a", text="#c9c9c9",
|
CHART_COLORS_DARK = dict(bg="#242424", grid="#3a3a3a", text="#c9c9c9",
|
||||||
measured="#00c8d7", target=CHIP_WARN, fault=CHIP_ERR)
|
measured="#00c8d7", target=CHIP_WARN, fault=CHIP_ERR)
|
||||||
@@ -133,7 +134,7 @@ class TemperatureChart(QWidget):
|
|||||||
ts, vs = self._samples_t, self._samples_v
|
ts, vs = self._samples_t, self._samples_v
|
||||||
if not ts:
|
if not ts:
|
||||||
return [], []
|
return [], []
|
||||||
lo, hi = 0, len(ts) # 二分找左边界(留一点前文保证连线)
|
lo, hi = 0, len(ts) # 左边界二分(留一点前文保证连线)
|
||||||
while lo < hi:
|
while lo < hi:
|
||||||
m = (lo + hi) // 2
|
m = (lo + hi) // 2
|
||||||
if ts[m] < t0:
|
if ts[m] < t0:
|
||||||
@@ -141,7 +142,14 @@ class TemperatureChart(QWidget):
|
|||||||
else:
|
else:
|
||||||
hi = m
|
hi = m
|
||||||
lo = max(lo - 1, 0)
|
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:
|
if len(t_out) > 4000:
|
||||||
stride = math.ceil(len(t_out) / 4000)
|
stride = math.ceil(len(t_out) / 4000)
|
||||||
gt, gv = [], []
|
gt, gv = [], []
|
||||||
@@ -674,6 +682,9 @@ class App(QMainWindow):
|
|||||||
self.txt_log.setFont(MONO)
|
self.txt_log.setFont(MONO)
|
||||||
self.txt_log.setLineWrapMode(QPlainTextEdit.NoWrap)
|
self.txt_log.setLineWrapMode(QPlainTextEdit.NoWrap)
|
||||||
self.txt_log.setMinimumHeight(200)
|
self.txt_log.setMinimumHeight(200)
|
||||||
|
# 裁剪走原生环形上限: 手工 cursor 裁剪在 PySide6 6.11 上抛 AttributeError
|
||||||
|
# (实例级枚举访问已移除), 曾致每 tick 异常中止 + 日志无限增长 + 显示滞后
|
||||||
|
self.txt_log.setMaximumBlockCount(500)
|
||||||
body.addWidget(self.txt_log)
|
body.addWidget(self.txt_log)
|
||||||
self._log(f"帧格式: D5 01 LEN <命令> CRC / B5 TYPE LEN <payload> CRC; "
|
self._log(f"帧格式: D5 01 LEN <命令> CRC / B5 TYPE LEN <payload> CRC; "
|
||||||
f"响应超时建议 {ff.REPLY_TIMEOUT * 1000:.0f}ms 重发")
|
f"响应超时建议 {ff.REPLY_TIMEOUT * 1000:.0f}ms 重发")
|
||||||
@@ -959,6 +970,8 @@ class App(QMainWindow):
|
|||||||
self.samples_t.append(t)
|
self.samples_t.append(t)
|
||||||
self.samples_v.append(float("nan"))
|
self.samples_v.append(float("nan"))
|
||||||
self.faults.append((t, code))
|
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.lbl_temp.setText("—.— ℃")
|
||||||
self._csv_write(t, code, ok=False)
|
self._csv_write(t, code, ok=False)
|
||||||
self.chart.update()
|
self.chart.update()
|
||||||
@@ -1266,9 +1279,19 @@ class App(QMainWindow):
|
|||||||
target = f"{self.target_hist[-1][1]:g}" if self.target_hist else ""
|
target = f"{self.target_hist[-1][1]:g}" if self.target_hist else ""
|
||||||
heater = self.heater_state or ""
|
heater = self.heater_state or ""
|
||||||
sensor = self.sensor_state or ("OK" if ok else "")
|
sensor = self.sensor_state or ("OK" if ok else "")
|
||||||
self._csv.write(f"{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(t))},"
|
try:
|
||||||
f"{t - self._csv_t0:.3f},{temp_text},{target},{heater},{sensor}\n")
|
self._csv.write(f"{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(t))},"
|
||||||
self._csv.flush()
|
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._csv_count += 1
|
||||||
self.lbl_csv.setText(f"记录中({self._csv_count} 行) → "
|
self.lbl_csv.setText(f"记录中({self._csv_count} 行) → "
|
||||||
f"{os.path.basename(self._csv_path)}")
|
f"{os.path.basename(self._csv_path)}")
|
||||||
@@ -1279,15 +1302,11 @@ class App(QMainWindow):
|
|||||||
|
|
||||||
# ================= 日志 =================
|
# ================= 日志 =================
|
||||||
def _log(self, line):
|
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 = 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):
|
def _clear_log(self):
|
||||||
self.txt_log.clear()
|
self.txt_log.clear()
|
||||||
|
|||||||
+11
-2
@@ -347,8 +347,17 @@ class FF485:
|
|||||||
pass
|
pass
|
||||||
if item is not None:
|
if item is not None:
|
||||||
cmd, frame = item
|
cmd, frame = item
|
||||||
self.ser.write(frame)
|
sent = False
|
||||||
self.on_event(f'→ {cmd} [{frame.hex(" ").upper()}]')
|
try:
|
||||||
|
self.ser.write(frame)
|
||||||
|
sent = True
|
||||||
|
except serial.SerialTimeoutException:
|
||||||
|
# 写超时(适配器停滞): 只丢这条命令, 链路保留。
|
||||||
|
# 勿并入下方 OSError——瞬时停滞曾被当成"设备拔出"
|
||||||
|
# 关端口断链, 迫使用户手动重连(适配器换向本就不稳)
|
||||||
|
self.on_event(f'✕ {cmd} 写超时未发出, 链路保留(可重发)')
|
||||||
|
if sent:
|
||||||
|
self.on_event(f'→ {cmd} [{frame.hex(" ").upper()}]')
|
||||||
# 按到达量读取, 不攒大块: 固定 read(256) 在持续帧流下要攒满
|
# 按到达量读取, 不攒大块: 固定 read(256) 在持续帧流下要攒满
|
||||||
# 256 字节才返回(win32 字节间隔超时 150ms 因帧间隔更短永不
|
# 256 字节才返回(win32 字节间隔超时 150ms 因帧间隔更短永不
|
||||||
# 触发, 100ms 上报 ≈ 2.5s 一批), 表现为曲线/命令全按 3s 一卡
|
# 触发, 100ms 上报 ≈ 2.5s 一批), 表现为曲线/命令全按 3s 一卡
|
||||||
|
|||||||
@@ -227,5 +227,57 @@ with open(window._csv_path, encoding='utf-8-sig') as f:
|
|||||||
check('CSV 表头+2 行', len(lines), 3)
|
check('CSV 表头+2 行', len(lines), 3)
|
||||||
check('CSV 故障行', lines[2].split(',')[2], 'SHRT')
|
check('CSV 故障行', lines[2].split(',')[2], 'SHRT')
|
||||||
|
|
||||||
|
# 6) 日志环形裁剪回归: 写满 500 行后封顶, 继续追加不抛异常、不无限增长。
|
||||||
|
# (手工 cursor 裁剪用 cursor.Start/BlockUnderCursor 实例级枚举访问,
|
||||||
|
# PySide6 6.11 上必然抛 AttributeError → 每 tick 异常中止, v0.2.8 改原生裁剪)
|
||||||
|
for i in range(600):
|
||||||
|
window._log(f'日志压力行 {i}')
|
||||||
|
check('日志 500 行封顶', window.txt_log.blockCount() <= 500, True)
|
||||||
|
window._log('封顶后追加行')
|
||||||
|
check('封顶后可继续追加', window.txt_log.blockCount() <= 500, True)
|
||||||
|
check('封顶后最新行在末尾',
|
||||||
|
window.txt_log.toPlainText().splitlines()[-1].endswith('封顶后追加行'), True)
|
||||||
|
|
||||||
|
# 7) v0.2.9 回归: 故障标记封顶 / 滚动条跟随 / CSV 写失败自停 / 冻结切片右边界
|
||||||
|
t_end = time.time()
|
||||||
|
for i in range(app_fluent.MAX_FAULTS + 50):
|
||||||
|
window._record_fault(t_end + i * 0.01, ff.SENSOR_OPEN)
|
||||||
|
check('故障标记封顶', len(window.faults), app_fluent.MAX_FAULTS)
|
||||||
|
|
||||||
|
sb = window.txt_log.verticalScrollBar()
|
||||||
|
sb.setValue(0)
|
||||||
|
window._log('上翻后新行')
|
||||||
|
check('上翻时不抢滚动条', sb.value() <= 1, True)
|
||||||
|
sb.setValue(sb.maximum())
|
||||||
|
window._log('贴底跟随行')
|
||||||
|
check('贴底时跟随滚动', abs(sb.value() - sb.maximum()) <= 4, True)
|
||||||
|
|
||||||
|
|
||||||
|
class _BoomFile:
|
||||||
|
"""模拟写坏盘: write/flush 必抛 OSError(磁盘满/记录盘被拔)"""
|
||||||
|
def write(self, *_a):
|
||||||
|
raise OSError(28, '模拟磁盘满')
|
||||||
|
def flush(self):
|
||||||
|
raise OSError(28, '模拟磁盘满')
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
window._csv = _BoomFile()
|
||||||
|
window._csv_path = 'dummy.csv'
|
||||||
|
window._add_sample(time.time(), 25.0)
|
||||||
|
check('CSV 写失败自动停止记录', window._csv is None, True)
|
||||||
|
check('CSV 失败后按钮复位', window.btn_csv.text(), '开始记录')
|
||||||
|
|
||||||
|
c = window.chart
|
||||||
|
c._samples_t, c._samples_v = [1.0, 2.0, 3.0, 4.0], [1, 2, 3, 4]
|
||||||
|
c._frozen_at = 2.5
|
||||||
|
ts, _vs = c._visible(0, 2.5)
|
||||||
|
check('冻结切片不含冻结尾后样本', ts, [1.0, 2.0])
|
||||||
|
c._frozen_at = None
|
||||||
|
ts, _vs = c._visible(0, 99)
|
||||||
|
check('实时切片完整', ts, [1.0, 2.0, 3.0, 4.0])
|
||||||
|
c._samples_t, c._samples_v = window.samples_t, window.samples_v
|
||||||
|
|
||||||
print('RESULT:', 'FAIL:' + ','.join(fails) if fails else 'ALL PASS')
|
print('RESULT:', 'FAIL:' + ','.join(fails) if fails else 'ALL PASS')
|
||||||
sys.exit(1 if fails else 0)
|
sys.exit(1 if fails else 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user