# AGENTS.md — SerialMate 工程档案(开发约束与进度) > 人类文档 [README.md](README.md) 回答"这是什么、怎么用";本文记录"代码怎么组织、 > 怎么构建发布、改动时哪些约束不能破坏",兼作本工具的进度记录。 > 修改本目录代码前先读完本文。仓库整体的开发需求与进度管理见根目录 `agent.md`。 ## 1. 项目定位 仓库 `HW-Debug-Tools` 是 LT8619C→DLPC3421 投影转接板的调试上位机工具集。 SerialMate 是其中的**通用串口/RS485 调试助手**:收发 + 协议解析 + 曲线。 默认帧格式与 DLPilot 光机 RS485 协议完全一致(`D5 01 LEN CRC`), 可直接调试板载 GD32E230 网关;帧格式(包头/板ID/CRC 算法与覆盖范围)可定制, 适配任意"包头+板ID+LEN(=payload长度)+payload+CRC"五段式协议。 协议语义权威同 DLPilot:上游固件仓 `protocols.md`,本仓不维护副本。 ## 2. 目录契约 ``` SerialMate/ ├── src/ 源代码区 —— 只有这里可以手改 ├── release/ 产物区 —— SerialMate.exe + README.md(exe 不入库本地保留,README 入库), │ *-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`(`_esc_log` 转义后拼 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。 **时间戳由 link 后台线程打**(字节到达/写入串口时刻):四个回调签名均带 尾参 ts 并随事件入队(事件元组尾部即 ts,如 `('frame', fr, ts)`)——GUI 消费侧只经 `_timestamp(t)` 格式化、不现取(消费时刻打戳会随 UI 卡顿成批 挤压失真,v0.1.0 教训,见 `ISSUE_rx_timestamp_and_lag.md`)。 2b. **接收日志治理**:日志行只入 `_rx_buf`(`_append_rx`/`_append_rx_frame` 负责转义),`_drain_events` 末尾 `_flush_rx` 一次 appendHtml——每行必须包 `

`(**`
` 连接只算 1 个 block,会让 maximumBlockCount 裁旧行失效**,offscreen 实验验证过);转义用 `_esc_log` (空格转 ` `,防富文本折叠连续空格破坏 TX 行对齐);`_update_counters` 由 drain 末尾统一调一次,勿回到每帧调用。事件积压超 `FOLD_QSIZE`(200) 时 该批帧走 `_absorb_frame`(曲线照常入库、计数器照常,日志折叠为带首尾到达 时刻的计数摘要行,消化到阈值内自动恢复逐帧)。 2c. **通道配置缓存**:`_on_frame` 帧路径只读 `self._ch_cfg`(`_on_channel` 用户改动配置时经 `_collect_channels` 重建),禁止每帧遍历 UI 控件读配置 (高频时放大 UI 线程占用,v0.1.0 延迟累积的帮凶之一)。 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 范围预判不同)。 5a. **解析不吞数据(v0.1.2 起的产品语义)**:串口助手是本体、协议解析是附加层。 FrameParser 重同步丢弃的字节累积进 `garbage`,`link.SerialLink._dispatch_rx` 每批 `drain_garbage()` 无条件取走(未接回调也不许在解析器里无限累积), 经 `on_garbage(data, ts)` 上抛;GUI 把它并入 `('raw', ...)` 事件,与透传行 同格式显示。纯文本设备/波特率不符的乱码在解析模式下必须全可见。 例外:只匹配上前缀、还在等后续字节的"半帧"缓冲不算垃圾(可能补全成真帧), 字节流恰好在帧头后截断时会扣留至多一个前缀长度的字节——可接受,勿改成 超时强制吐出(会把慢速真帧撕碎)。改此语义需同步 selftest 8b / loop 3b。 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 # 协议回归(67 项, 改 protocol.py 后必跑) python selftest_loop.py # 链路回环(21 项, 需 pyserial, 改 link.py 后必跑) python selftest_gui.py # GUI 功能回归(68 项, 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.2(2026-10-16,接收语义修复:解析模式不再吞数据) 背景:用户接 COM25(非 DLPilot 协议设备)开解析模式,接收区一片空白—— `_dispatch_rx` 在解析模式只上抛完整合法帧,不成帧的字节被 FrameParser 静默 丢弃;用户明确产品定位:**首先是基础串口调试工具,自定义协议解析是附加项**, 附加能力不得影响本体功能。 已完成: - protocol.py:FrameParser 丢弃字节不再销毁——重同步逐出的字节累积进 `garbage`,新增 `drain_garbage()` 取走清空;`clear()` 一并清垃圾; `feed()` 返回值不变(旧用法零改动)。 - link.py:新增 `on_garbage(data, ts)` 回调(区别于透传模式的 `on_raw`, 语义可分但 GUI 同样并入 raw 事件显示);`_dispatch_rx` 每批 drain 无条件 执行(未接回调也不累积),有字节且接了回调才上抛,ts=同一批到达时刻。 - main.py:接线 `on_garbage → ('raw', ...)`;"解析协议帧(收)"补 tooltip 说明两模式语义;APP_VERSION 0.1.2。 - 例外语义(有意为之):只匹配前缀、等待后续字节的半帧缓冲不算垃圾 (可能补全成真帧);字节流恰在帧头后截断时扣留至多一个前缀长度。 - 自检扩充:selftest 8b(纯文本全透出/噪声+帧+尾字节/坏CRC帧字节全透出/ 半帧头扣留后补全成帧)+10 项;selftest_loop 3b(解析模式非帧字节经 on_garbage 透出、回环 TX 帧只算非帧字节)+3 项;selftest_gui 5b (on_garbage 接线+非帧字节日志行)+2 项;全部通过。 - README 协议解析段同步"解析不吞数据"语义;exe 重建(v0.1.2); zip/Release 未发布(待用户验收)。 待办 / 计划:无新增,沿用 v0.1.0 待办。 ### v0.1.1(2026-10-10,接收路径修复:时间戳失真 + 显示延迟累积) 背景:FilmForge 固件联调(GD32F405 RS485 115200,M2 连续温度上报 2~10 帧/s) 发现日志时间戳成批挤压、显示延迟越跑越大;固件侧二分验证 100% 出帧,问题 独立存在于 SerialMate 显示层,见 `ISSUE_rx_timestamp_and_lag.md`。 已完成: - **根因 1**(时间戳在 GUI 消费时刻打):时间戳改在 link 后台线程打 (字节到达/写入串口时刻),`on_status/on_frame/on_raw/on_tx` 签名均追加 尾参 ts 并随事件入队;`_timestamp(t)` 变为纯格式化函数。日志时间恢复为 真实到达时间。 - **根因 2**(接收日志无限增长 → UI 递减性卡顿 → 延迟累积): - 接收日志批量插入:`_drain_events` 把本轮日志行攒入 `_rx_buf`,末尾 `_flush_rx` 一次 appendHtml(每行一个 `

`, maximumBlockCount 裁旧行照常生效;`_esc_log` 空格转 ` ` 防富文本 折叠连续空格);`_update_counters` 改为 drain 末尾统一一次; - 通道配置缓存 `_ch_cfg`:`_on_channel` 时重建,帧路径不再每帧遍历控件 (提取逻辑收敛进 `_extract_channels`); - 积压折叠:事件队列超 `FOLD_QSIZE`(200) 时该批帧走 `_absorb_frame`—— 曲线照常入库、计数器照常,日志折叠为一行计数摘要(含首尾到达时刻, 如 `← 12:00:00.100~12:00:02.300 ⏳ 显示滞后, 折叠 250 帧: B=0xF0 温度 上报 ×250`),消化到阈值内自动恢复逐帧; - 行数上限 5000(v0.1.0 末期已加的止血,本次提为常量 `RX_MAX_BLOCKS`)。 - 自检扩充:selftest_loop 增回调 ts 断言共 18 项;selftest_gui 增时间戳 格式化/帧日志用到达时刻/通道缓存同步/积压折叠 8 项共 69 项;另做端到端 模拟(600 帧积压一次性消费):日志压到 201 行、曲线 600 点全收、时间戳 单调且逐帧等于真实到达时刻。 - README 接收显示段同步时间戳/日志治理语义;exe 重建入库(v0.1.1); zip/Release 未发布(待用户验收)。 - **真机 M2S100"卡住"排查(2026-10-10,用户复测反馈)**:用户 v0.1.1 exe 发 M2S100 后 ack 与周期帧均不显示。本机分层复现均正常——裸 pyserial+ 解析器(帧 100% 到达、ack ok、100ms 间隔稳)、offscreen GUI 全链路 (60 帧/6s 一帧不丢)、桌面真渲染完整用户序列(连接→M1/M2→M2S100→帧流, 138 帧全显示)。exe 经归档解包确认含 v0.1.1 代码。未能复现,定性为 用户端环境/运行态问题,待用户按状态栏"积压"指示再复现时定位。 排查副产品: - link.py 串口参数加 `inter_byte_timeout=0.02`(Win32 ReadIntervalTimeout)——注意 **pyserial 的 timeout 是总超时**(150ms 内 必返回,不无限攒批),此参数把周期帧的 read 粒度从 ~150ms 一批改善为 单帧粒度(帧时间戳更准); - 状态栏计数器追加 **积压 N**(events.qsize())——常驻分层观测:数字 持续增长=事件产出快于消费(GUI 侧问题),恒 0=RX 链路断(串口/驱动/ 固件侧问题);"卡住"时 UI 整体无响应则是主线程被阻塞,另查。 - 勘误:修复初期曾把 M2S100 卡住归因"read(4096) 攒批 41 秒",该论述 错误(win32 timeout 为总超时),已修正注释;loop:// 虚拟口的 read 立即返回缓冲,**测不出真串口等待行为**,链路自检覆盖不到此处。 - 探测善后:COM5 FilmForge 固件曾残留 100ms 上报态(用户上次 M2S100 生效的证据),已发 M2S0 关闭(M2 查询=OFF)。 待办 / 计划:无新增,沿用 v0.1.0 待办。 ### 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 解析侧双字节序兼容开关(当前按算法约定单序)。