Files
STM32F4-Base/docs/测试说明/串口助手设计说明.md
T
wandering1 a6cdebde62 文档体系重组与 D-7 一致性门禁落地,清理芯片手册孤儿图片
- D-6 目录规范化:docs 根目录散落文档归入 UI设计/子系统设计/架构设计/审查记录/
  环境与部署/网络 等子目录,去掉目录名空格,重命名 嵌入式C语言代码规范_V1.0.md
- D-7 三档一致性门禁:新增 scripts/check_doc_links.py(链接存在性)、
  scripts/gen_task_table.py(FreeRTOS 任务表数值)、scripts/check_mem_numbers.py
  (内存/Flash 体积),均支持 --check/--write/--diff,pre-commit 钩子同步注册
- D-5 文档索引:新建 docs/README.md,新建 docs/文档规范.md 约束目录命名与链接写法
- D-1 芯片手册瘦身:删除 1069 张无引用孤儿图片(77.06 MB),
  docs/芯片手册 120 MB → 41 MB,已引用图片零断链
- 同步 C 源码注释中的文档路径引用(D-6 重组后),构建 0 错误 0 警告
2026-09-26 00:51:53 +08:00

18 KiB
Raw Blame History

Python 串口助手(serial_tool)设计说明

版本:v1.1(需求 + 设计 + 实施记录) 日期:2026-09-15 状态:已实施(S1–S6 完成;本机验收 A1–A5/A8 通过,A6/A7 正路径待 CH340 硬件接入后补验) 关联文档:串口助手使用指南.md(命令用法/工作流/排障) · 自动化测试流程.md · AGENTS.md(串口调试/自动化测试节) · .opencode/serial_config.json(端口单一配置源,沿用) 取代:serial-mcp-server_串口调试.md(实施时删除,工具历史并入本文 §12)

1. 背景与目标

1.1 为什么重写

现串口调试 = Adancurusul/serial-mcp-server(Rust 二进制,手编译置于用户档案)+ MCP/CLI 双形态。两个致命缺陷(2026-09-15 本机实测):

# 缺陷 影响
F1 二进制绑定用户档案:opencode.json 的 mcp.serial 指向 C:\Users\16241\AppData\Local\serial-mcp-server\*.exe,本机实际用户为 Administrator(该档案不存在,二进制缺失) serial MCP 工具不可用、@test 采集步骤断裂;换机器/换用户无法复现
F2 无历史缓冲:read 只能读调用之后到达的字节 抓开机日志被迫走"采集窗口 + first-byte 触发 + EOF 重试"仪式(6 个参数 + JSON 解析 + CH340 释放后重开陷阱特判)

Python 重写一并解决:依赖收敛到仓库 .venv + pyserial(任意机器/用户可复现);常驻 daemon 把全量 RX/TX 持续写入日志文件,历史 = 日志文件本身,采集窗口仪式整体消失。

1.2 目标

# 目标
G1 Agent 可枚举 COM 口、连接;失败 ≤1s 快速返回,绝不阻塞
G2 RX/TX 全量写入纯文本日志文件,Agent 直接读文件即得完整历史
G3 Agent 一切操作 = 带硬超时(5s)的短命令;串口所有权归常驻 daemon
G4 一条 status 命令看全状态(daemon/端口/计数/最后 RX/日志文件/错误)
G5 随时清空日志(原子 + 代际号,新旧内容可区分)
G6 @test 一键流程切换新工具:open → clear → 烧录 → 读文件 → verify,无采集窗口
G7 全流程不关端口(ST-Link 烧录不占 COM),从根上规避 CH340 释放/重开陷阱

2. 需求清单

# 需求 优先级 来源
R1 ports:枚举 COM 口(名称/友好名/厂商,标注配置目标口) P0 用户 #1
R2 open:连接端口;不存在/被占用 ≤1s 返回明确错误;参数 port/baud/DTR/RTS P0 用户 #1
R3 RX/TX 全量时间戳日志(单文件,纯文本,行格式见 §5) P0 用户 #2
R4 全部命令硬超时 5s + 失败返回 JSON(exit code 区分),CLI 永不挂起 P0 用户 #3
R5 status:单条 JSON 全状态 P0 用户 #4
R6 clear:原子清空 + 代际递增 + 文件内标记行 P0 用户 #5
R7 常驻 daemon + 单实例 + CLI 自动拉起(Agent 无需记生命周期) P0 补充
R8 日志 20MB 上限,超限原地截头保留尾部 10MB(不重开端口/不 rename) P0 补充(用户确认)
R9 开端口不翻转 DTR/RTS(防 STM32 自动复位电路误复位),可配置 P0 补充
R10 串口读错/拔线 → daemon 存活,state=error,可重开;daemon 被杀可自恢复 P0 补充
R11 @test 集成:run_boot_test.py 采集步改写 + verify_log.py 兼容 + 旧文档/配置迁移 P0 用户场景
R12 send(TX 进日志)、log --tail/--grep、start/stop/restart、selftest 无头自检 P1 补充

3. 总体方案

3.1 形态:常驻 daemon + 薄 CLI + "日志文件即接口"

flowchart LR
    A["Agent(opencode bash 工具)"] -->|"短命令(硬超时 5s)"| C["serial_tool.py CLI"]
    C -->|"HTTP 127.0.0.1:18765(JSON)"| D["daemon 进程(detached 单实例)"]
    D --> S["串口读线程(pyserial,后台)"]
    S --> W["行拆分/解码/flush"]
    W --> F[("state/serial.log<br/>纯文本,Agent 直接读")]
    D --> M[("state/meta.json<br/>pid/gen/版本")]
    D --> L[("state/daemon.log<br/>daemon 自身 stderr")]
  • 单文件实现:tools/serial_tool/serial_tool.py(stdlib + pyserial,约 1000 行);解释器统一 .venv\Scripts\python.exe
  • Agent 调用式:& '.venv\Scripts\python.exe' 'tools/serial_tool/serial_tool.py' <命令> [参数](任意 cwd;state 目录按脚本位置解析,与 cwd 无关)
  • 历史 = 日志文件:Agent 用 Read/Grep 直接看历史,无需专门 API(log 命令只是便捷封装)
  • 配置沿用 .opencode/serial_config.json(portName/baudRate 为默认值;新增可选字段 httpPort,默认 18765)

3.2 "不卡死"的归属(对用户 #3 的重新表述)

串口读是长时操作,归 daemon 后台线程;Agent 侧只存在短命令。"不卡死" = 命令层契约:每条 CLI 命令有 5s 硬截止(--timeout 可覆盖),到期即返回超时 JSON 与 exit 2,不做任何无限等待。

4. 命令契约

4.1 子命令一览

命令 用途 关键参数 返回(stdout,恒 JSON)
ports 枚举 COM 口 — {"ok":true,"ports":[{"name":"COM6","description":"USB 串行设备 (CH340)"},…],"configured":"COM6"}
open 开端口(幂等:同端口已开 = 直接成功,不重开) --port --baud --dtr 0/1 --rts 0/1(默认取配置文件;DTR/RTS 默认 0) {"ok":true,"port":"COM6","baud":115200,"gen":7,"since":"…ISO…"}
close 关端口(flush 后释放) — {"ok":true}
send 发 TX 字节(进日志);默认精确字节不加换行 位置参数文本(UTF-8)或 --hex 0D0A;--nl 追加 CRLF {"ok":true,"bytes":12}
status 全状态(见 §4.2) — 见 §4.2
log 读日志(文件尾) --tail N(默认 50;0 = 全部)--grep P {"ok":true,"gen":8,"lines":["…"]}
clear 清空日志:原地截断 + 写 [SYS] log cleared (gen=N+1) — {"ok":true,"gen":8}
start / stop / restart daemon 生命周期 — {"ok":true,"pid":…} / stop 后 {"ok":true,"stopped":true}
selftest 无头自检(行拆分器/解码回退/截头算法 单测回环,无需硬件) — 0=PASS

4.2 status 返回示例

{
  "ok": true,
  "daemon": {"pid": 1234, "version": "1.0.0", "uptime_s": 61},
  "port": {"ready": true, "name": "COM6", "baud": 115200, "dtr": false, "rts": false,
           "since": "2026-09-15T10:29:58.112+08:00"},
  "counters": {"rx_bytes": 1834, "tx_bytes": 12, "rx_lines": 57, "errors": 0},
  "last_rx": {"ts": "2026-09-15T10:31:02.441+08:00", "line": "UI menu init: OK", "age_ms": 320},
  "log": {"path": "D:\\…\\tools\\serial_tool\\state\\serial.log", "size": 4523, "gen": 7},
  "state": "active",
  "error": null
}

state ∈ {stopped, active, error};port.ready = 已开 && state≠error。Agent 轮询等待只用本命令。

4.3 CLI 退出码契约

exit 含义
0 命令成功(ok:true)
1 领域错误(ok:false + error:端口不存在/被占用/参数错…)——快速失败,≤1s
2 daemon 不可达或命令超时(5s 硬截止)
3 参数/用法错误

4.4 自动拉起

任何命令(除 start 本身)发现 daemon 未运行 → CLI 以 sys.executable(同一 .venv 解释器)detached 拉起 daemon(CREATE_NO_WINDOW,stderr → state/daemon.log),轮询 HTTP 就绪(≤5s)后重试原命令一次;仍失败按 exit 2 返回。Agent 无需任何显式 start 步骤。

5. 日志文件规范

5.1 位置与配套文件(目录整体 gitignore)

文件 用途
tools/serial_tool/state/serial.log RX/TX/SYS 全量日志(Agent 直接读)
tools/serial_tool/state/meta.json daemon 元数据(pid / 版本 / 当前 gen)
tools/serial_tool/state/daemon.log daemon 自身 stderr(排障用)

5.2 行格式

[SYS] 2026-09-15T10:29:58.112+08:00 port opened COM6 115200 (gen=7)
[SYS] 2026-09-15T10:29:58.301+08:00 log cleared (gen=8)
[RX] 2026-09-15T10:30:03.445+08:00 App init OK
[RX] 2026-09-15T10:30:07.912+08:00 UI menu init: OK
[TX] 2026-09-15T10:30:09.872+08:00 AT+PING
[SYS] 2026-09-15T10:31:00.000+08:00 log truncated to 10MB (cap=20MB)
规则 规定
方向标记 [RX] 串口入 / [TX] Agent 发出 / [SYS] 工具事件(open/close/error/cleared/truncated)
时间戳 本地 ISO8601 含毫秒与偏移(2026-09-15T10:30:03.445+08:00);行时间戳 = 该行首字节到达时刻
完整行不变量 文件只含完整行:残行留 daemon 内存,200ms 无后续数据即冲刷成行(或 clear/close 时强制冲刷)→ Agent 读文件永不见半行
分行 按 \n 分(\r 剥除);纯二进制流(无换行)按 200ms 窗口成行
解码回退 UTF-8 → GBK → 仍失败则该行文本域写 HEX:<原始字节 hex>(MCU 流 = ASCII 为主 + UTF-8 中文,前两级足够)
flush 每行即 flush(串口速率低,无缓冲压力)→ 文件随时可读、内容即时可见

5.3 清空与代际

  • clear = 写句柄上原地截断(seek(0)+truncate)+ 写 [SYS] log cleared (gen=N+1) 标记行;对写线程原子(单写者,截断与写入同一线程串行)
  • 代际 gen 持久化于 meta.json(daemon 重启不丢);Agent 以"最后一个 cleared/opened 标记行"或 status.log.gen 区分新旧内容

5.4 体积上限(20MB,用户确认)

  • 每次写后检查:size > 20MB → 原地截头:在同一写句柄上读尾部 10MB → 截空 → 写回(Windows 无"丢弃头部"单系统调用,ftruncate 只能截尾;单写者线程内执行,无并发风险)
  • 禁止 rename 轮转(Windows 下写句柄跟随被 rename 的文件)与重开文件/端口(CH340 释放后立即重开 → 读 0 字节 EOF 陷阱)
  • 截头后写 [SYS] … log truncated to 10MB (cap=20MB);文件头内容可能随时消失 → Agent 读日志应看尾部(log --tail 或直接读文件末段)

6. daemon 行为规范

项 规定
单实例 HTTP 端口(默认 18765,serial_config.json 的 httpPort 可改)绑定即锁;端口被占 = 已有实例
拉起方式 CLI detached 子进程(CREATE_NO_WINDOW),解释器 = sys.executable(保证同一 .venv)
open 语义 幂等(同端口已开 = 成功返回,不重开);端口不存在 → ≤1s 报错并附可用端口列表;被占用 → ≤1s 报错(Windows 共享冲突即时返回)并提示持有者
DTR/RTS 默认不翻转(dtr=False, rts=False,防 STM32 自动复位电路误复位目标板);--dtr/--rts 可显式置位
读线程容错 串口读异常/设备拔出 → 记 [SYS] … serial error: …,state=error(status.error 非空),daemon 不退出;重插后 open 重开恢复
重开退避 错误恢复路径重开端口前 ≥1.5s 退避(CH340 特性);正常流程不重开
stop 关端口(flush 尾行)→ 写 [SYS] port closed → 退出
进程被强杀 Windows 进程退出自动释放 COM 端口;下次命令自动拉起新实例(open 幂等路径)

7. 集成与迁移(@test 切换)

7.1 新 @test 流程(run_boot_test.py 采集步改写,退出码约定不变:0/1/2/3)

flowchart LR
    A["open(自动拉起 daemon;幂等)"] --> B["clear → gen=N"]
    B --> C["MDK-ARM\download.bat(ST-Link 烧录+复位,端口全程打开)"]
    C --> D["wait-idle:rx 静默 8s(-SettleMs)或超时 30s(-MaxWaitMs)"]
    D --> E["提取本代际 [RX] 行,剥 [RX] 时间戳前缀 → boot txt"]
    E --> F["verify_log.py(标记匹配)"]
  • 对比旧流程:6 个采集窗口参数 + JSON 解析 + EOF 重试 全部删除;采集 = 端口常开 + 读文件
  • verify_log.py 与纯文本日志契约一致(2026-09-20 随 @test 迁移 Python,判定语义不变)
  • 参数保留:-Port -BaudRate -FlashCmd -Required -Forbidden -OutDir;删除 -DurationMs 等窗口参数,新增 -SettleMs(静默判据,默认 8000——boot 任务间有 3~4s 自然静默间隙,2000 会截断)-MaxWaitMs(默认 30000)

7.2 配套变更清单

对象 变更
requirements.txt 增 pyserial(保持纯 ASCII 注释)
.gitignore 增 tools/serial_tool/state/
opencode.json(仓库根) 删除 mcp.serial 段(指向缺失二进制,已失效)
测试说明/serial-mcp-server_串口调试.md 已删除(内容精华并入本文;工具历史见 §12)
AGENTS.md "串口调试"节改写为 serial_tool(配置源仍 serial_config.json);"自动化测试"节同步新流程
.opencode/command/test.md Step 0/2/Notes 改写(前置检查 = serial_tool 可用性 + ports;流程 = §7.1)
docs/测试说明/自动化测试流程.md 采集机制描述同步(去 serial-mcp-server 表述)
.opencode/serial_config.json 结构不变(可选增 httpPort);note 更新

8. 非目标与 v2 候选

v1 明确不做:多端口并发、MCP 封装、协议级 HEX 分析、硬件流控(RTS/CTS)、波特率自适应、GUI。 v2 候选:多端口(每口一 daemon 实例或子会话)、MCP wrapper(如需工具面板呈现)、按 tag 过滤的日志订阅(WebSocket/SSE)。

9. 测试与验收

9.1 无头自检(selftest,0=PASS)

行拆分器(CRLF/残行冲刷/纯二进制窗口)、解码回退链(UTF-8/GBK/HEX)、截头算法(合成大文件 → ftruncate 语义)——纯进程内,无需硬件。

9.2 端到端验收(逐项过)

# 用例 判据
A1 ports 列表含 CH340 端口,configured 标注正确
A2 端口被占用(另一进程先开 COM6)→ open ≤1s 返回 ok:false(占用),无挂起
A3 open→status→send→log→clear→close 全走一遍 全 ok:true;日志含 TX 行与 cleared 标记;gen 递增
A4 stop 后强杀 daemon(taskkill)→ 再发任意命令 自动拉起成功,open 幂等路径无 CH340 EOF 错误
A5 日志灌至 >20MB 自动截头至 ~10MB,写入连续、无重开端口,[SYS] truncated 行存在
A6 运行中拔 CH340 → status daemon 存活,state=error;重插 + open 恢复
A7 @test E2E ×3(含一轮"上轮端口未关"场景) 三轮全 PASS(Required 标记齐、无 Forbidden),boot 日志完整
A8 selftest 0=PASS

10. 实施步骤(按序执行)

# 步骤 产物 验证
S1 requirements.txt 增 pyserial + .venv 安装 requirements.txt pip show pyserial 有版本 ✅
S2 daemon 核心:HTTP 服务 + 串口读线程 + 行日志写(含 clear/截头/DTR 策略) tools/serial_tool/serial_tool.py(daemon 部分) start→status→send→stop 手动走通 ✅
S3 CLI 全子命令 + 自动拉起 + 超时契约 + selftest serial_tool.py(CLI 部分) selftest 0=PASS + 命令走查 ✅
S4 验收 A1–A6、A8 测试通过记录 A1–A5/A8 PASS ✅(本机无 CH340,A6 待硬件)
S5 @test 集成:run_boot_test.py 采集步改写 + 参数收敛 run_boot_test.py 负路径(端口缺失→exit 2)+ 假烧录全链路 + verify 正/负判定 ✅;A7 正路径待 CH340 接入
S6 文档/配置迁移(§7.2 全清单) docs + 配置 全库 grep 无 serial-mcp-server 残留引用(本文 §12 工具历史除外) ✅

提交:全部完成后单提交(pre-commit 门禁;纯宿主侧 Python,不涉及 Keil 构建)。

11. 风险与开放问题

# 项 策略
O1 换机器缺 CH340 驱动 → ports 无目标口 ports 返回带提示("未找到 CH340,请装驱动");文档列驱动要求
O2 截头瞬间 Agent 恰在读文件 → 视图错位 已知可接受:Agent 读尾部;log 命令经 daemon 读更稳
O3 daemon 强杀瞬间丢 ≤1 行(内存残行) 可接受(debug 日志场景);close 路径保证冲刷
O4 HTTP 端口 18765 与其他服务冲突 serial_config.json httpPort 可改
O5 TX 用途(本工程 USART1 单向输出) 保留能力,面向未来交互/RS485 调试;v1 仅保证进日志
O6 daemon 常驻内存(Python 进程 ~20MB) 可接受;stop 即释放

12. 工具历史

代 方案 结局
1 Serial Studio MCP(GUI 依赖、导出文件定位问题) 已清理(2026-09 前)
2 Adancurusul/serial-mcp-server(Rust,MCP+CLI) 被本工具取代:二进制绑定用户档案(本机缺失)+ 无历史缓冲(采集窗口仪式)
3 serial_tool(本文,Python 仓库内) 当前方案

13. 变更记录

日期 变更
2026-09-15 v1.1 实施完成(S1–S6)。实施中实测发现并修复 4 个设计文档未覆盖的 Windows 实现级缺陷:① SO_REUSEADDR 在 Windows 允许多个活跃 socket 绑同端口 → HTTP 服务改独占绑定(SO_EXCLUSIVEADDRPLACE,发行版未导出时用数值 0x40 兜底)+ restart 强制确认旧实例死亡,防"already_running"假成功留下旧代码;② 写句柄 "ab" 追加模式在 Windows 禁读 → 溢出截头的"读尾部"必崩,改 "a+b"(自测用独立句柄故未暴露,真实 daemon 溢出路径已 E2E 验证);③ Windows 对不存在的 COM 口常报 Access Denied → 误报"port busy",改以 list_ports 枚举结果为"不存在/占用"判定准绳;④ 静默判据默认 2s 会截断 boot(任务间 3~4s 自然静默间隙),-SettleMs 默认改 8000ms。验收状态:A1–A5/A8 PASS(本机无 CH340,用 COM1 替代口验证;A5 溢出用进程内驱动真实 writer_loop 验证);A6/A7 正路径待硬件接入补验