228 lines
17 KiB
Markdown
228 lines
17 KiB
Markdown
# AGENTS.md — SerialMate 工程档案(开发约束与进度)
|
||
|
||
> 人类文档 [README.md](README.md) 回答"这是什么、怎么用";本文记录"代码怎么组织、
|
||
> 怎么构建发布、改动时哪些约束不能破坏",兼作本工具的进度记录。
|
||
> 修改本目录代码前先读完本文。仓库整体的开发需求与进度管理见根目录 `agent.md`。
|
||
|
||
## 1. 项目定位
|
||
|
||
仓库 `HW-Debug-Tools` 是 LT8619C→DLPC3421 投影转接板的调试上位机工具集。
|
||
SerialMate 是其中的**通用串口/RS485 调试助手**:收发 + 协议解析 + 曲线。
|
||
默认帧格式与 DLPilot 光机 RS485 协议完全一致(`D5 01 LEN <payload> CRC`),
|
||
可直接调试板载 GD32E230 网关;帧格式(包头/板ID/CRC 算法与覆盖范围)可定制,
|
||
适配任意"包头+板ID+LEN(=payload长度)+payload+CRC"五段式协议。
|
||
协议语义权威同 DLPilot:上游固件仓 `protocols.md`,本仓不维护副本。
|
||
|
||
## 2. 目录契约
|
||
|
||
```
|
||
SerialMate/
|
||
├── src/ 源代码区 —— 只有这里可以手改
|
||
├── release/ 产物区 —— SerialMate.exe + README.md(入库),
|
||
│ *-win64.zip(gitignore,只上传 Release)
|
||
├── assets/ 图标等生成资产(暂无;build_exe.py 已预留 assets/serial-mate.ico)
|
||
├── README.md 人类文档
|
||
└── AGENTS.md 本文
|
||
```
|
||
|
||
### 文件角色速查
|
||
|
||
| 文件 | 角色 |
|
||
|---|---|
|
||
| `src/main.py` | Fluent GUI 入口(含 `--seconds N` 冒烟参数),argparse 仅此一处 |
|
||
| `src/protocol.py` | 协议模块(纯标准库):FrameLayout 帧构成令牌、ProtoConfig 组帧/校验、FrameParser 增量解析、CRC 算法表、extract_value 数值提取 |
|
||
| `src/link.py` | 串口工作线程 SerialLink(pyserial 只准在这里 import),支持 `loop://` 虚拟回环 |
|
||
| `src/plot.py` | 滚动曲线控件(纯 QPainter,刻意不引 matplotlib/pyqtgraph) |
|
||
| `src/config.py` | BASE_DIR 定位与 serial_mate.json / board_profiles.json 读写(缺键补默认,读容错) |
|
||
| `src/theme.py` | 主题三件套同步(Fluent setTheme + QPalette/QSS + DWM 标题栏),与 EDID-Viewer 同源 |
|
||
| `src/selftest.py` | 协议自检(无依赖、无硬件,改 protocol.py 后必跑) |
|
||
| `src/selftest_loop.py` | 链路自检(loop:// 回环验证 组帧→发送→接收→解析→提取,缺 pyserial 跳过) |
|
||
| `src/selftest_gui.py` | GUI 功能自检(offscreen 驱动事件处理函数:预览/回显/提取/配置往返,改 main.py 后建议重跑) |
|
||
| `src/build_exe.py` | PyInstaller onefile 打包 → `release/SerialMate.exe` |
|
||
| `src/package_release.py` | exe+README → `release/SerialMate-v<版本>-win64.zip` |
|
||
| `src/publish_gitea.py` | 建/复用 Gitea Release 并上传 zip(`GITEA_TOKEN`,需 `--insecure`) |
|
||
|
||
## 3. 代码不变量(改代码必须维持)
|
||
|
||
1. **依赖分层**:protocol.py / config.py 纯标准库;pyserial 只准在 link.py
|
||
import(main.py 经 `SerialLink` 间接使用);plot.py 只依赖 PySide6;
|
||
GUI 硬依赖 PySide6-Fluent-Widgets + pyserial(串口工具缺 pyserial 即无意义,
|
||
与 DLPilot 的"缺失降级"策略不同,只保留 try-import 提示)。
|
||
1a. **卡片布局约定**:`_card(side=True)` 把短副标题放标题右侧省一行(串口/
|
||
协议/发送卡);右列是纵向 QSplitter(接收/发送高度用户可拖拽,拉伸余量
|
||
默认给接收、发送卡保持自然高度防空腔);发送卡末尾 `lay.addStretch(1)`
|
||
保证内容贴顶(卡片被拖高后富余高度收在卡底,勿删——否则拉伸被各行均分,
|
||
标题上方出现空隙);发送卡行序:板型管理行(下拉+新建+删除+配置+重载) /
|
||
预设行(下拉+填入+直接发送) / 模式行(下拉+存为板型+存入预设) / 输入行 /
|
||
周期行;帧日志 payload 高亮走 `_append_rx_frame`(html.escape 后拼 span
|
||
背景,toPlainText 内容不变,selftest 的日志断言因此不受影响);预设下拉
|
||
显示"名称 · 命令",存入命令走 `_add_command_named`(弹窗核 `_add_command`
|
||
包装,测试用核函数),预设直发走 `_send_preset_direct`(经输入框复用
|
||
组帧/历史链路)。
|
||
1b. **窗口最小高度**:首帧布局后 `_apply_minimum_size` 按内容实际最小高度
|
||
抬高窗口下限(高度拉过矮时布局约束无法满足,曲线卡绘图区会被压塌并与
|
||
通道行互相叠压——用户实测);勿删, 调整曲线卡行数/高度后该值自动跟随。
|
||
2. **回调线程边界**:SerialLink 的 on_status/on_frame/on_raw/on_tx 全在后台线程
|
||
触发,一律 `events.put(...)` 投递,主线程 30ms QTimer 轮询 `_drain_events`
|
||
分发(与 DLPilot `_poll_dlpc` 同模式),禁止回调里直接碰 GUI。
|
||
3. **半双工节奏**:link 工作线程每个循环周期最多出队写一帧(20ms 间隔),
|
||
RS485 总线多帧连发留应答间隙,勿改成批量 drain 一次写。
|
||
4. **帧构成语义**:帧由 `FrameLayout` 令牌序列定义(`HDR ID LEN PAYLOAD CRC`
|
||
令牌 + 十六进制字面量,收发共用布局、仅包头值分方向);约束:LEN/PAYLOAD/CRC
|
||
恰好各一次、CRC 必须最后、LEN 前至少一个 HDR 或字面量(同步定界)、ID 最多一次。
|
||
LEN 口径(`LEN_SEMS`):payload 长度 / 到帧尾(payload+CRC) / 整帧;
|
||
CRC 算法/覆盖范围见 protocol.CRC_ALGOS/CRC_SCOPES,覆盖按**帧内位置**定义
|
||
(with_header=第1字节起 / no_header=跳过第1字节 / payload_only=仅载荷),
|
||
任意布局通用;2 字节 CRC 线上字节序 MODBUS 低前、其余高前,按算法约定单序
|
||
校验(改动需同步 selftest 向量)。
|
||
5. **解析重同步策略**:前缀令牌逐位对齐(HDR/字面量须匹配、ID 通配),LEN 值经
|
||
口径换算为 payload 长度,负值视为伪帧头逐出;CRC 失配丢 1 字节重同步——
|
||
通用工具不预设合法 LEN 区间(与 DLPilot 的 LEN 范围预判不同)。
|
||
6. **ID 位不校验只透出**:`Frame.code` 原样上抛(布局无 ID 时为 None,GUI 日志
|
||
与 selftest 均需兼容 None),TYPE_NAMES 仅用于显示译码(DLPilot 语义)。
|
||
7. **BASE_DIR 语义**(config.py):frozen 时 =exe 所在目录,源码运行时 =src/;
|
||
设置平铺存 `serial_mate.json`,不建子目录。`tx.periodic` 不随配置
|
||
自动启动(防误连发),其余配置启动恢复。
|
||
8. **曲线数据流**:每帧对每个启用通道调 `extract_value`(失败静默跳过),
|
||
`PlotWidget.add` 内部 deque(maxlen=3000) 滚动丢弃;数值格式统一
|
||
`fmt_val`(4 位有效数字)。
|
||
9. **主题**:theme.py 三件套同步(见 EDID-Viewer 教训:只 setTheme 会黑块);
|
||
plot 曲线颜色固定 4 色,文字/网格色取自 QPalette 自动跟随明暗。
|
||
9a. **曲线时间窗**:PlotWidget 持有 `_span`(窗宽秒, None=跟随全部) 与
|
||
`_offset`(右端距最新秒数, 0=实时);横轴域换算在 `visible_domain`(纯计算,
|
||
selftest 有断言)。**窗口不向数据收敛**(用户实测反馈):选 5min 而数据只有
|
||
23s 时显示完整 5min 轴、曲线贴右端随采集填满(示波器式),右下角标注
|
||
"已采集 Xs / 窗口 Ns";滞后超出全部历史 = 空窗保宽。弹窗内 时间窗下拉+
|
||
滑条 只改这两个状态;滑条 0..1000 线性映射 (1000-v)/1000×maxpan,实时
|
||
跟随=拉到最右(1000);poll 定时器 `_sync_time_slider` 回写位置(用户拖动中
|
||
跳过);嵌回卡片时强制重置为跟随全部(卡内无滑条, 防困在滞后窗里)。纵轴
|
||
数值刻度画在每条横向参考线上。
|
||
悬停读值:mouseTracking 开启, mouseMove 只记录 `_hover_pos` 并请求重绘;
|
||
读值内容由 paintEvent **每帧**经 `_hover_lines_at(_hover_pos)` 重算(光标
|
||
时间 T 经 `_plot_geometry` 换算, `_nearest` 二分取 ±10px 容差内最近采样),
|
||
画在顶部居中反色小框(`_hover_box`)——鼠标停住不动时数值也随实时数据持续
|
||
刷新(勿改回 mouseMove 里一次性算好/或 QToolTip, 那样不动鼠标就不更新——
|
||
用户实测反馈)。`_nearest`/`_hover_lines_at` 有单测。绘图区内边距统一用
|
||
`M_LEFT/TOP/RIGHT/BOTTOM` 类常量(绘制与鼠标换算共用, 勿在 paint 里另写)。
|
||
9b. **暂停(两级语义)**:主界面"暂停"= `_paused`, add() 丢弃暂停期间的数据;
|
||
弹窗"暂停渲染"= `_frozen`, 数据照常入库但跳过重绘——冻结是**绝对状态**:
|
||
PlotWidget 覆写 `update()` 直接吞掉一切重绘请求(任何代码路径都无法在
|
||
冻结期打破), 只有 `set_frozen(False)` 恢复并一次性渲染积累;冻结时画面
|
||
中央显示"已冻结渲染(采集继续)"角标让状态可见。两者互不干扰, 均有
|
||
selftest 断言。
|
||
9c. **弹窗曲线交互**:自动Y(勾选按钮, 与主界面 chk_auto_y 双向同步, 关闭时
|
||
以 `auto_yrange()` 初始化手动量程);图上左键拖动 = 平移手动 Y 量程
|
||
(`on_yrange_changed` 回调同步主界面 Ymin/Ymax 输入框, 阻断信号防回环);
|
||
滚轮 = 缩放时间窗 2~900s(滞后按比例缩放, 经 `on_span_changed` 回调让
|
||
`_sync_time_slider` 把时间窗下拉同步为匹配档位/自定义(-1));"实时"按钮
|
||
= offset 归零跳回最新。自动 Y 量程抽取为 `auto_yrange()`(纯计算, 有单测)。
|
||
11a. **曲线弹出窗口**:绘图区在曲线卡留有固定槽位布局 `_plot_slot`,弹出 =
|
||
`setParent(None)` 后装入顶层窗口(通道/暂停/Y 轴控件留在主界面控制同一
|
||
PlotWidget 实例);关掉弹出窗口(eventFilter 捕 Close)或主窗 closeEvent
|
||
都会走 `_dock_plot()` 嵌回 —— `_dock_plot` 必须先摘 `self._plot_win` 引用
|
||
再 `win.close()`(防 Close 递归),且 `setParent(None)` 后必须补 `show()`
|
||
(重挂布局不会自动恢复可见)。
|
||
10. **多实例**:有意不加单实例锁(双口双开的合法场景),串口冲突靠
|
||
pyserial 报错提示("串口被占用"文案在 link.py,勿删)。
|
||
11. **板型档案**:`board_profiles.json`(BASE_DIR 平铺,用户本机数据不入库,
|
||
首次运行自动生成默认"DLPilot光机")。档案 = name + proto(7 个帧协议字段,
|
||
**不含 parse** —— 解析开关是查看偏好) + commands[[名,命令]..];切换板型
|
||
应用协议参数并装载命令表,"存入"命令以命令文本为名去重;启动恢复选中
|
||
板型时**只装载命令表不重复应用协议参数**(协议以上次会话 store 为准);
|
||
GUI 增删后经 config.save_profiles 落盘,"配置/重载"支持手改 JSON。
|
||
|
||
## 4. 构建与发布(全部在 src/ 下执行)
|
||
|
||
```powershell
|
||
python -m compileall -q . # 全模块可编译
|
||
python selftest.py # 协议回归(47 项, 改 protocol.py 后必跑)
|
||
python selftest_loop.py # 链路回环(需 pyserial, 改 link.py 后必跑)
|
||
python selftest_gui.py # GUI 功能回归(offscreen, 改 main.py 后建议重跑)
|
||
python main.py --seconds 5 # GUI 冒烟(源码); offscreen: set QT_QPA_PLATFORM=offscreen
|
||
python build_exe.py # → ../release/SerialMate.exe(缺 PyInstaller 自动装)
|
||
python package_release.py <版本> # → ../release/SerialMate-v<版本>-win64.zip
|
||
python publish_gitea.py ../release/SerialMate-v<版本>-win64.zip --replace --insecure
|
||
```
|
||
|
||
发布约定与 Gitea 实例怪癖(TLS 抽风加 `--insecure`、assets 两种形状兼容、
|
||
token 读 `GITEA_TOKEN` 用户级环境变量)同 DLPilot,详见 `DLPilot/AGENTS.md` §5;
|
||
tag/release 命名 `SerialMate-vX.Y.Z`,zip 命名
|
||
`SerialMate-vX.Y.Z-win64.zip`(publish_gitea.py 从文件名推 tag)。
|
||
|
||
## 5. 已知坑(本工具踩过/继承的)
|
||
|
||
- `qfluentwidgets.CaptionLabel` 无 `setDim` 方法(本机版本),灰字用
|
||
`CaptionLabel` 本身即可;`ComboBox` 不要用 `currentData`(qfluentwidgets
|
||
自绘下拉 userData 支持不稳),一律 currentIndex + 平行元组映射。
|
||
- **qfluentwidgets 控件默认水平 Expanding**:紧凑行不设 stretch 项时空白会被
|
||
CheckBox/Label 等瓜分出大空隙 —— 一律走 `Window._row()`(行尾默认补
|
||
stretch;Qt 规则:有 stretch 因子时额外空间只按因子分配),需要自管拉伸的
|
||
行传 `trailing=False`。
|
||
- **qfluentwidgets SpinBox/DoubleSpinBox 设固定宽后内部行编辑器会塌缩**
|
||
(minimumSizeHint 142/193 只是参考,强设小于它的宽度时行编辑器只剩几像素,
|
||
数值被裁成残字——整数 0 在 96px 时渲染成 "]"):整数 SpinBox ≥110px、
|
||
DoubleSpinBox ≥130px 才能完整显示数值;曲线卡偏移/序号/倍率/Y 量程框的
|
||
宽度有 selftest 断言把守, 调布局时勿低于阈值。
|
||
- 组帧/解析回路自测时必须让 `tx_hdr == rx_hdr`(收发方向包头本就允许不同,
|
||
D5 组的帧 B5 解析器看不见——selftest 的 fuzz/回路用例已按此约定)。
|
||
- offscreen 冒烟报 `QFontDatabase: Cannot find font directory` 属 Qt offscreen
|
||
平台噪音,桌面运行无此问题,勿当 bug 修。
|
||
- onefile 运行时定位 exe 用 `sys.executable`(`__file__` 在 `_MEIPASS`),
|
||
config.py 已按此实现。
|
||
|
||
## 6. 验证清单(改完代码后)
|
||
|
||
1. `python -m compileall -q .` —— 全模块可编译;
|
||
2. `python selftest.py` —— 协议回归(改 protocol.py 后必跑);
|
||
3. `python selftest_loop.py` —— 链路回环(改 link.py 后必跑);
|
||
4. `python main.py --seconds 5` —— GUI 冒烟自动开关;
|
||
5. `python build_exe.py` → 双击 release/SerialMate.exe 确认界面渲染;
|
||
6. (有硬件时)USB-RS485 接光机发 M1/M201/M731 验证真机收发与曲线。
|
||
|
||
## 7. 进度记录
|
||
|
||
### v0.1.0(2026-09-27,首版)
|
||
|
||
已完成:
|
||
- 协议模块:**帧构成令牌序列可定制**(`HDR ID LEN PAYLOAD CRC` 令牌 + 十六进制
|
||
字面量同步字节,收发共用布局,支持字段换序/无板ID/多字节同步头;LEN 口径三种:
|
||
payload 长度 / 到帧尾 / 整帧)、5 种 CRC 算法 × 3 种覆盖范围(按帧内位置定义,
|
||
任意布局通用)、增量解析(前缀逐位对齐 + CRC 失败逐字节重同步)、
|
||
二进制/文本/正则三种曲线数值提取;自测 64 项全 PASS(含 DLPilot 实测帧向量、
|
||
CRC 标准校验值 check("123456789")、布局换序/字面量/LEN 口径向量与布局模糊往返)。
|
||
- 串口链路:参数热切换、发送队列(半双工友好 + 未连接丢弃)、解析/透传双模式
|
||
热切、`loop://` 虚拟回环自检 13 项全 PASS(组帧→发送→接收→解析→曲线提取)。
|
||
- GUI(PySide6-Fluent-Widgets):串口/协议/曲线/接收/发送五卡,帧构成与 LEN 口径
|
||
可视化编辑(非法输入即时提示),payload 字符串/HEX 切换(帧日志的 payload
|
||
段垫半透明底色高亮——主题无关 rgba, 明暗切换不残留, 经 appendHtml 实现)、
|
||
原始帧 hex、时间戳、日志保存;发送:自动包装(`/` 分隔多条)、HEX 直发、
|
||
周期发送、组帧实时预览、历史回溯(↑↓)、回车发送;主题跟随系统。
|
||
- 曲线(plot.py + 弹窗,2026-09-27 实机反馈多轮迭代):4 通道三种提取方式 +
|
||
倍率,自动/手动 Y 量程;绘图区可**弹出独立窗口**(嵌回自动重置视图);
|
||
纵轴每条参考线标数值刻度;时间窗(跟随/10s~15min, 示波器式不向数据收敛,
|
||
右下角"已采集/窗口"标注)+ 横向滑条回看历史;悬停读值框固定顶部、鼠标
|
||
不动也随数据持续刷新;**暂停渲染**(冻结显示继续收数, 与主界面"暂停"
|
||
两级语义, 冻结有中央角标);自动Y 开关、图上拖动自定义 Y 轴(同步主界面
|
||
输入框)、滚轮缩放时间窗(2~900s)、实时按钮;窗口最小高度按内容自适应
|
||
(防高度拉小时通道行叠压绘图区)。
|
||
- 板型档案:发送卡"板型"下拉一键切换协议参数 + 命令预设;**新建板型**(协议
|
||
沿用当前、命令预设空白,防遗留其他板子的命令)/存为板型(同名覆盖)/
|
||
**删除板型**(确认弹窗,至少保留一个,删除后切第一个板型且协议参数不动)/
|
||
存入预设命令(弹窗自定义显示名称)/配置=内嵌表格编辑器(**CommandTable**:
|
||
第 0 列 ≡ 手柄按住上下拖动整行排序——内部 QDrag,move_row 整行搬移;
|
||
网格线/隔行底色按 palette 明暗自适配,空行清晰可见;+加行 −删行;空命令与
|
||
重复命令保存时忽略/去重,写回 board_profiles.json)/重载;预设下拉
|
||
"名称 · 命令"同显。
|
||
- 发布流水线:build_exe/package_release/publish_gitea 与全仓一致;exe 已重建
|
||
入库,zip/Release 未发布(待用户验收)。
|
||
|
||
待办 / 计划:
|
||
- 【无图标】assets/serial-mate.ico 未生成(build_exe.py 已预留路径,
|
||
参照 DLPilot logos/make_ico.py 流程补)。
|
||
- 【真机已过】用户已实机联调(COM15 真机收发/曲线提取/多板型档案实际使用,
|
||
2026-09-27 多轮界面反馈即来自真机数据);温度曲线量纲(倍率取值)待用户
|
||
按传感器手册确认。
|
||
- 【计划】发送区多条 payload 的"批处理脚本"化(延时/循环/条件等待应答)。
|
||
- 【计划】曲线数据导出 CSV;接收日志按日期自动分文件。
|
||
- 【考虑】CRC 解析侧双字节序兼容开关(当前按算法约定单序)。
|