Tools: PID debug toolchain from V0.1.x bring-up

test_run.py: judged closed-loop test over COM5 (fixed sequence, J1~J4
This commit is contained in:
2026-10-15 20:55:29 +08:00
parent 8900240c65
commit 2f43efb94b
7 changed files with 618 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
"""FilmForge 温控闭环自动测试(COM5,RS485)。
固定测试序列:M999(版本) -> M2S500(0.5s 上报) -> M4S50(目标) -> M3S1(加热)
-> 采样 DURATION 秒 -> M3S0/M2S0 收尾。
不设温度上限(用户明确:真机靠物理温度开关保护,测试环境上限由用户控制)。
判定标准(脚本自动输出,不掺人工解释):
J1 上报流:>2 帧/s 连续; J2 稳态窗(最后 300s):|均值-目标|<=0.5C
J3 稳态窗极值:[目标-0.5, 目标+0.5](用户规格 ±0.5C)
J4 无 OPEN/SHRT 故障值
输出:test_data.csv(t,temp 全序列)+ test_events.log(全部收发)+ 终端摘要。
"""
import sys
import time
sys.path.insert(0, "GUI")
import filmforge_rs485 as ff
PORT = "COM5"
TARGET = 50.0
DURATION = float(sys.argv[1]) if len(sys.argv) > 1 else 600.0
rows = [] # (t, temp or None)
events = []
faults = []
version = None
t0 = time.time()
def on_event(line):
events.append((time.time() - t0, line))
with open("test_events.log", "a", encoding="utf-8") as f:
f.write(f"{time.time():.3f} {line}\n")
def on_status(msg):
on_event(f"[STATUS] {msg}")
def on_frame(frame, t_rx):
payload = frame.payload if isinstance(frame.payload, str) else \
frame.payload.decode("ascii", errors="replace")
try:
val = float(payload)
except ValueError:
val = None
if val is not None and val > -100.0: # 正常温度
rows.append((time.time() - t0, val))
elif val is not None: # OPEN=-273.15 / SHORT=-274.15
faults.append((time.time() - t0, val))
on_event(f"[FAULT] {payload}")
else:
if payload.startswith("v"):
global version
version = payload
on_event(f"[FRAME] {payload!r}")
link = ff.FF485(on_event=on_event, on_status=on_status, on_frame=on_frame)
link.open(PORT)
time.sleep(0.5)
if not link.is_connected():
print("OPEN COM5 FAILED")
sys.exit(1)
# ---- 序列 ----
link.send("M999")
time.sleep(1.0)
link.send("M2S500")
time.sleep(1.0)
link.send("M4S50")
time.sleep(1.0)
link.send("M3S1")
on_event("[SEQ] heat on, target 50")
t_end = time.time() + DURATION
while time.time() < t_end:
time.sleep(1.0)
link.send("M3S0")
time.sleep(0.5)
link.send("M2S0")
time.sleep(1.0)
link.close()
# ---- 判定输出 ----
with open("test_data.csv", "w", encoding="utf-8") as f:
f.write("t,temp\n")
for t, v in rows:
f.write(f"{t:.2f},{v:.2f}\n")
print(f"version={version} frames={len(rows)} faults={len(faults)}")
if rows:
temps = [v for _, v in rows]
print(f"info global_max={max(temps):.2f} global_min={min(temps):.2f}")
settle = [v for t, v in rows if t >= rows[-1][0] - 300.0] or temps
mean = sum(settle) / len(settle)
print(f"J2 settle_mean={mean:.2f} (|{-TARGET + mean:.2f}|<=0.5? {'PASS' if abs(mean - TARGET) <= 0.5 else 'FAIL'})")
ok3 = max(settle) <= TARGET + 0.5 and min(settle) >= TARGET - 0.5
print(f"J3 settle_min={min(settle):.2f} settle_max={max(settle):.2f} "
f"p2p={max(settle) - min(settle):.2f} ([{TARGET - 0.5:.1f},{TARGET + 0.5:.1f}]? {'PASS' if ok3 else 'FAIL'})")
cross = next((t for t, v in rows if abs(v - TARGET) <= 0.3), None)
if cross:
post = [(t, v) for t, v in rows if t >= cross]
print(f"reach={cross:.0f}s post_max={max(v for _, v in post):.2f} "
f"(overshoot {max(v for _, v in post) - TARGET:+.2f})")
print(f"J4 faults={'PASS' if not faults else 'FAIL(' + str(len(faults)) + ')'}")
print(f"J1 report_rate={'PASS' if len(rows) >= DURATION * 1.6 else 'FAIL'}")