feat: I2C-Lab v0.1.0 代码入库——RP2040 PIO 被动监控 probe 固件(pico-sdk C, 250MHz 双核) + Fluent 上位机(事务流/波形快照/CSV/演示模式);release/ 交付物(exe/UF2/BIN/zip)不入库、本地保留待真机验收发布;根 README 工具表与 agent.md 登记;I2C-Inject README 标注后续并入 I2C-Lab;移除误登记的 OBI-1/ui-style-demo 相关内容

This commit is contained in:
2026-10-13 15:41:29 +08:00
parent 74469cfe66
commit 42ff258b35
25 changed files with 3292 additions and 20 deletions
+151
View File
@@ -0,0 +1,151 @@
"""GUI 功能自检(offscreen 无窗口): 事务入表/地址过滤/暂停滚动/波形喂数/
CSV 导出池/固件统计行/配置往返
====================================================================
不依赖硬件与串口, 直接驱动 Window 的事件处理函数; 改 main.py 后建议重跑。
用法: python selftest_gui.py (全部通过退出码 0)
"""
import os
import sys
os.environ['QT_QPA_PLATFORM'] = 'offscreen'
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from PySide6.QtWidgets import QApplication
import i2cdec
import main as m
from i2cdec import Transaction
fails = []
def check(name, got, want):
ok = got == want
print(('PASS' if ok else 'FAIL'), name, '->', repr(got)[:120],
'' if ok else f'(期望 {want!r})')
if not ok:
fails.append(name)
app = QApplication(sys.argv)
win = m.Window()
win.show()
T = Transaction
# 1) 事务入表 + 全量池
tr1 = T(t0_us=100, t1_us=200, addr7=0x32, read=False,
acks=(True, True), data=b'\xff\x60', ended='stop')
tr2 = T(t0_us=900, t1_us=1100, addr7=0x50, read=True,
acks=(True, False), data=b'\x12', ended='stop')
win._paused = False
for tr in (tr1, tr2):
win._on_trans(tr)
check('表格行数', win.table.rowCount(), 2)
check('导出池数量', len(win.transactions), 2)
check('事务计数', win._trans_total, 2)
check('地址列', win.table.item(0, 3).text(), '0x32')
check('RW 列', win.table.item(1, 4).text(), 'R')
check('ACK 列', win.table.item(0, 5).text(), 'AA')
check('数据列', win.table.item(0, 6).text(), 'FF 60')
check('收尾列', win.table.item(0, 7).text(), 'STOP')
# 2) 地址过滤: 只留 0x32; 非法过滤项被忽略
win.ed_filter.setText('32 zz')
win._on_trans(tr2)
check('过滤生效(0x50 不入表)', win.table.rowCount(), 2)
check('计数仍累计', win._trans_total, 3)
win.ed_filter.setText('')
win._on_trans(tr2)
check('清过滤后入表', win.table.rowCount(), 3)
# 3) 地址 NAK 标红 / 未收尾标橙(前景色判断)
tr_nak = T(t0_us=1, t1_us=2, addr7=0x10, read=False, acks=(False,),
data=b'', ended='stop')
tr_cut = T(t0_us=3, t1_us=9, addr7=0x11, read=False, acks=(True,),
data=b'', ended='cut')
win._on_trans(tr_nak)
win._on_trans(tr_cut)
r_nak = win.table.rowCount() - 2
check('地址 NAK 前景红色',
win.table.item(r_nak, 5).foreground().color().name(), '#e0564c')
check('cut 收尾前景橙色',
win.table.item(r_nak + 1, 7).foreground().color().name(), '#e08a2e')
# 4) 长事务截断显示 + tooltip 全量
tr_long = T(t0_us=1, t1_us=2, addr7=0x2a, read=False, acks=(True,) * 20,
data=bytes(range(20)), ended='stop')
win._on_trans(tr_long)
r = win.table.rowCount() - 1
check('长数据显示截断省略', win.table.item(r, 6).text().endswith('…'), True)
check('长数据 tooltip 全量',
win.table.item(r, 6).toolTip() == tr_long.data_hex(), True)
# 5) 暂停滚动: 不再 scrollToBottom, 但行照加
win._toggle_pause()
check('暂停按钮文案', win.btn_pause.text(), '继续滚动')
win._on_trans(tr1)
check('暂停时行照加', win.table.rowCount() >= 6, True)
win._toggle_pause()
# 6) 表格行数上限: 批量灌 5100 条, 顶行被裁
win._clear_table()
for i in range(5100):
win._on_trans(T(t0_us=i, t1_us=i + 1, addr7=0x01, read=False,
acks=(True,), data=b'', ended='stop'))
check('表格行数封顶', win.table.rowCount(), m.TABLE_CAP)
check('导出池保留全部(<STORE_CAP)', len(win.transactions), win._trans_total)
check('导出池未达上限', len(win.transactions) <= m.STORE_CAP, True)
# 7) 波形喂数 + 注记解码
fam = [T(addr7=0x32, read=False, acks=(True, True, True),
data=bytes((0xFF, 0x60)), ended='stop')]
edges, _refs = i2cdec.encode_edges(fam, khz=100.0, t0_us=1000)
win._on_wave(edges, 0)
check('波形有数据', win.wave.has_data(), True)
check('波形注记解码 1 事务', len(win.wave._trans), 1)
check('波形快照标签', win.lb_wave.text(),
f'{len(edges)} 边沿, 解码 1 事务')
# 8) 固件 stats 行解析
win._on_info('= stats edges=1234 trans=56 trans_drop=0 susp=0 '
'ring_ovf=0 last_edge_ago_us=12 mon=1')
check('固件计数显示', win.lb_fw.text().startswith('固件计数: edges=1234'), True)
# 9) 配置往返
win.ed_filter.setText('32 50')
win.spin_wave_n.setValue(4096)
cfg = win._store_from_ui()
check('配置: 过滤', cfg['mon']['addr_filter'], '32 50')
check('配置: 波形边沿数', cfg['mon']['wave_n'], 4096)
config_path = m.config.CONFIG_FILE
try:
os.remove(config_path)
except OSError:
pass
m.config.save(cfg)
check('配置落盘读回', m.config.load()['mon']['wave_n'], 4096)
with open(config_path, 'w', encoding='utf-8') as f: # 损坏文件容错
f.write('{not json')
check('配置容错(损坏回落)', m.config.load()['mon']['addr_filter'], '')
check('配置容错(缺键补齐)', m.config.load()['mon']['wave_n'],
m.config.DEFAULTS['mon']['wave_n'])
# 10) 演示模式一键开停(不开定时器轮询, 仅验证状态翻转)
win._toggle_demo()
check('演示启动', win._demo_timer.isActive(), True)
win._demo_tick()
win._drain_events()
check('演示 tick 产生事务', win._trans_total > 0, True)
win._toggle_demo()
check('演示停止', not win._demo_timer.isActive(), True)
win.close()
app.quit()
print()
if fails:
print(f'共 {len(fails)} 项失败:', ', '.join(fails))
sys.exit(1)
print('全部通过')