Add PA11 power-hold output (PMOS self-latch keep-alive)

This commit is contained in:
2026-10-10 09:51:51 +08:00
parent 9c7aafb78c
commit 6246a87c3c
16 changed files with 2573 additions and 3 deletions
+174
View File
@@ -0,0 +1,174 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""app_fluent 离屏回归(无硬件): 曲线渲染像素 + 帧派发状态机 + 主题往返 + CSV
用法: python selftest_gui.py (全部通过退出码 0; 需 PySide6-Fluent-Widgets)
"""
import math
import os
from collections import deque
import sys
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
from PySide6.QtGui import QImage, QPainter
from PySide6.QtWidgets import QApplication
fails = []
def check(name, got, want):
ok = got == want
print(('PASS' if ok else 'FAIL'), name, '->', got, '' if ok else f'(期望 {want})')
if not ok:
fails.append(name)
def mk_frame(payload, type_code=0xF0):
body = bytes([type_code, len(payload)]) + payload.encode('ascii')
return ff.FrameParser().feed(bytes([0xB5]) + body + bytes([ff.sum_crc(body)]))[0]
app = QApplication([])
import app_fluent
window = app_fluent.App(0)
window.show()
app.processEvents()
# 1) 空数据渲染不抛异常(占位文案)
img = QImage(window.chart.size(), QImage.Format_ARGB32)
window.chart.render(img)
check('空数据渲染', not img.isNull(), True)
# 2) 注入合成数据后渲染: 实测曲线/目标线/故障标记像素均存在
t0 = time.time() - 120
for i in range(600):
window.samples_t.append(t0 + i * 0.2)
window.samples_v.append(24 + 5 * math.sin(i / 40) + i * 0.01)
window.target_hist.append((t0, 30.0))
window.target_hist.append((t0 + 60, 35.0))
window.faults.append((t0 + 30, ff.SENSOR_OPEN))
window._set_theme('Dark') # 像素比对按 Dark 配色
window.chart.update()
img = QImage(window.chart.size(), QImage.Format_ARGB32)
window.chart.render(img)
def count_pixels(img, rgb, tol=30):
n = 0
for y in range(0, img.height(), 2):
for x in range(0, img.width(), 2):
c = img.pixelColor(x, y)
if (abs(c.red() - rgb[0]) < tol and abs(c.green() - rgb[1]) < tol
and abs(c.blue() - rgb[2]) < tol):
n += 1
return n
measured = (0, 200, 215) # Dark 主题实测曲线色 #00c8d7
target = (232, 131, 58) # 目标线 #E8833A
fault = (217, 83, 79) # 故障 #D9534F
n_meas, n_tgt, n_flt = count_pixels(img, measured), count_pixels(img, target), count_pixels(img, fault)
print(f'曲线像素={n_meas} 目标线像素={n_tgt} 故障像素={n_flt}')
check('实测曲线已绘制', n_meas > 50, True)
check('目标阶梯线已绘制', n_tgt > 5, True)
check('NTC 故障标记已绘制', n_flt > 0, True)
# 3) 帧派发状态机(经 _handle_frame, 等价真实串口路径)
n0 = len(window.samples_t)
window._handle_frame(mk_frame('24.5'), time.time())
check('上报帧入曲线', len(window.samples_t), n0 + 1)
check('大字读数更新', window.lbl_temp.text(), '24.5 ℃')
window._pending = deque([{'cmd': 'M4', 'kind': 'target', 't': time.time(), 'apply': None}])
window._handle_frame(mk_frame('120.0'), time.time())
check('M4 应答拆到目标', window.target_hist[-1][1], 120.0)
check('目标 chip 更新', window.chip_target.text(), '目标: 120 ℃')
check('pending 已消费', len(window._pending), 0)
window._pending = deque([{'cmd': 'M4', 'kind': 'target', 't': time.time() - 2, 'apply': None}])
n1 = len(window.samples_t)
window._handle_frame(mk_frame('33.3'), time.time())
check('过期 pending 不拦截温度帧', len(window.samples_t), n1 + 1)
check('目标不被污染', window.target_hist[-1][1], 120.0)
window._pending.clear()
window._pending = deque([{'cmd': 'M2', 'kind': 'report', 't': time.time(), 'apply': None}])
window._handle_frame(mk_frame('ON 500'), time.time())
check('M2 状态解析', window.report_state, ('ON', 500))
check('上报 chip 更新', window.chip_report.text(), '上报: ON 500ms')
window._pending = deque([{'cmd': 'M3', 'kind': 'heater', 't': time.time(), 'apply': None}])
window._handle_frame(mk_frame('OFF'), time.time())
check('M3 状态解析', window.heater_state, 'OFF')
window._pending = deque([{'cmd': 'M4S77', 'kind': 'ack', 't': time.time(),
'apply': ('target', 77)}])
window._handle_frame(mk_frame('ok'), time.time())
check('M4S ack 落目标', window.target_hist[-1][1], 77.0)
nf = len(window.faults)
window._handle_frame(mk_frame('OPEN'), time.time())
check('OPEN 记故障', len(window.faults), nf + 1)
check('故障断口 NaN', math.isnan(window.samples_v[-1]), True)
check('传感器 chip 故障态', window.chip_sensor.text(), 'NTC: 开路(OPEN)')
window._pending = deque([{'cmd': 'M999', 'kind': 'version', 't': time.time(), 'apply': None}])
window._handle_frame(mk_frame('v41.0.0.1'), time.time())
check('版本解析', window.firmware, 'v41.0.0.1')
window._pending = deque([{'cmd': 'M4S300', 'kind': 'ack', 't': time.time(),
'apply': ('target', 300)}])
window._handle_frame(mk_frame('err', 0xFD), time.time())
check('FD 错误消费 pending', len(window._pending), 0)
check('目标不被错误帧污染', window.target_hist[-1][1], 77.0)
# 3b) 连接序列 FIFO 回归: M999/M2/M3/M4 同时在途, 应答按序到达
window._pending.clear()
for cmd, kind in (('M999', 'version'), ('M2', 'report'), ('M3', 'heater'), ('M4', 'target')):
window._pending.append({'cmd': cmd, 'kind': kind, 't': time.time(), 'apply': None})
window._handle_frame(mk_frame('v41.0.0.1'), time.time())
window._handle_frame(mk_frame('OFF'), time.time()) # M2 应答, 不得被 heater 吃掉
window._handle_frame(mk_frame('OFF'), time.time()) # M3 应答
window._handle_frame(mk_frame('0.0'), time.time()) # M4 应答, 不得混入曲线
check('FIFO 版本', window.firmware, 'v41.0.0.1')
check('FIFO 上报', window.report_state, ('OFF', None))
check('FIFO 加热', window.heater_state, 'OFF')
check('FIFO 目标', window.target_hist[-1][1], 0.0)
check('FIFO 队列清空', len(window._pending), 0)
# 3c) 杂散应答不消费队首: 队首等版本, 先来 OFF(杂散)再来 v41
window._pending.clear()
window._pending.append({'cmd': 'M999', 'kind': 'version', 't': time.time(), 'apply': None})
window._handle_frame(mk_frame('OFF'), time.time())
check('杂散不消费队首', len(window._pending), 1)
window._handle_frame(mk_frame('v41.0.0.1'), time.time())
check('杂散后正常消费', len(window._pending), 0)
# 4) 主题往返后曲线配色随动 + 渲染不崩
window._set_theme('Light')
check('Light 曲线配色', window.chart._colors['measured'].lower(),
app_fluent.CHART_COLORS_LIGHT['measured'])
window._set_theme('Dark')
check('Dark 曲线配色', window.chart._colors['measured'].lower(),
app_fluent.CHART_COLORS_DARK['measured'])
window._set_theme('Light')
window._set_theme('System')
# 5) CSV 记录往返
window._toggle_csv()
window._add_sample(time.time(), 25.1)
window._record_fault(time.time(), ff.SENSOR_SHORT)
window._toggle_csv()
with open(window._csv_path, encoding='utf-8-sig') as f:
lines = f.read().strip().splitlines()
check('CSV 表头+2 行', len(lines), 3)
check('CSV 故障行', lines[2].split(',')[2], 'SHRT')
print('RESULT:', 'FAIL:' + ','.join(fails) if fails else 'ALL PASS')
sys.exit(1 if fails else 0)