现状口径:/keil-build 与 /serial-boot-test 是 DSH 会话的技能调用名, 不是 shell 命令;「名 + 命令行参数」的命令形态改为可执行体 run_boot_test.py 的真实调用。 - AGENTS.md:别名说明重写为「技能调用名不是 shell 命令」,两条目改 「编译(/keil-build)」「串口回归(/serial-boot-test)」 - keil-build / serial-boot-test 两 skill 调用名与示例同步;task-worktree 方案文档去掉 @build/@test 惯例从句;@task 保留(不在点名范围) - 123 文件现状陈述全改;台架捕获、变更记录/时间线、rename 映射输入 等 41 行历史证据保留旧名 - gen_memory_svg.py 6 处措辞随改,3 张 SVG 产物同批重跑 - 顺手补并行方案同行漏网计数 51→52 钩子 验证:提交期快钩子对 123 文件实跑全 Passed;check_doc_links --all rc=0(3038 处、无法判定 0);check_mermaid --all rc=0;YAML 解析 52 钩子不变;skill-size rc=0;py_compile 全过;字节卫生零漂移。
20 KiB
Python 串口助手(serial_tool)使用指南
定位:面向使用者的操作手册——命令怎么敲、输出怎么看、常见场景怎么组合、出问题怎么排查。 关联文档:设计决策与实现细节见
串口助手设计说明.md;/serial-boot-test 自动化闭环见自动化测试流程.md;serial-assistantskill 有一页速查(用到自动加载,AGENTS.md 不再重复收录)。 版本:v1.1(与.dsh/skills/serial-assistant/scripts/serial_tool.py当前实现一致,2026-09-15)
1. 它是什么(60 秒版)
仓库内置的串口调试工具,三个组成部分:
| 组成 | 是什么 | 你要知道的 |
|---|---|---|
| 常驻 daemon | 后台 Python 进程,串口唯一持有者,把 RX/TX 持续写进日志文件 | 不用管它——任何命令发现它没在跑会自动拉起 |
| 薄 CLI | serial_tool.py <命令>,每条命令都是带 5s 硬超时的短操作 |
你唯一直接打交道的界面 |
| 日志文件 | .dsh/skills/serial-assistant/scripts/state/serial.log,全量历史就在这里 |
想回看任何时刻的数据 → 直接读这个文件 |
统一入口(仓库根目录执行,任意 cwd 均可):
& '.venv\Scripts\python.exe' '.dsh/skills/serial-assistant/scripts/serial_tool.py' <命令> [参数]
两条心法:
- 所有输出都是 JSON(stdout),退出码 0=成功 / 1=领域错误 / 2=daemon 不可达 / 3=用法错误;
- 历史不用"采集"——daemon 一直在写日志文件,随时 Read/Grep 就是完整历史。
2. 快速上手
# ① 看本机有哪些 COM 口(不依赖 daemon;configured = 配置目标口)
& '.venv\Scripts\python.exe' '.dsh/skills/serial-assistant/scripts/serial_tool.py' ports
# ② 开串口(自动拉起 daemon;端口取 .serialtest/config.json,也可显式指定)
& '.venv\Scripts\python.exe' '.dsh/skills/serial-assistant/scripts/serial_tool.py' open
# ③ 发一条数据(文本;--hex 发字节;--nl 末尾加 CRLF)
& '.venv\Scripts\python.exe' '.dsh/skills/serial-assistant/scripts/serial_tool.py' send "hello" --nl
# ④ 看最近 20 行日志(含刚发的 TX 和收到的 RX)
& '.venv\Scripts\python.exe' '.dsh/skills/serial-assistant/scripts/serial_tool.py' log --tail 20
# ⑤ 不用了:关串口(daemon 继续常驻;要彻底退出用 stop)
& '.venv\Scripts\python.exe' '.dsh/skills/serial-assistant/scripts/serial_tool.py' close
3. 命令速查
| 命令 | 作用 | 依赖 daemon |
|---|---|---|
ports |
枚举 COM 口,标注配置目标口(configured_present;系统里只有一个候选时给 hint) |
否(本地执行) |
open |
开串口(幂等:同口已开 = 直接成功;不带 --dtr/--rts = 保持现状,显式给则现场应用并在 note 里回显) |
是(未运行自动拉起) |
close |
关串口 | 是 |
send |
发 TX 字节(进日志) | 是 |
status |
全状态一条 JSON(含 state、rx_age_ms = 距最后一个 RX 字节的毫秒数,null = 这个 daemon 还没收过字节) |
是 |
log |
读日志文件(--tail N / --grep P) |
否(直接读文件,daemon 死了也能用) |
clear |
清空日志 + 代际 +1 | 是 |
wait-idle |
等 RX 静默(/serial-boot-test 的收尾判据) | 是 |
start / stop / restart |
daemon 生命周期 | — |
selftest |
无头自检(行拆分/解码/截头算法,无需硬件) | 否 |
所有命令都支持 --timeout <秒>(默认 5)覆盖 CLI→daemon 硬超时。
3.1 ports — 枚举串口
... serial_tool.py ports
{"ok": true,
"ports": [{"name": "COM1", "description": "通信端口 (COM1)", "hwid": "ACPI\\PNP0501\\0"}, …],
"configured": "COM6"}
configured来自.serialtest/config.json的portName;找不到它 = 板子没接/驱动没装/口号变了。- CH340 的
description通常含"USB 串行设备"字样。
3.2 open — 开串口
... serial_tool.py open # 端口/波特率取配置文件
... serial_tool.py open --port COM6 --baud 115200
... serial_tool.py open --dtr 1 --rts 0 # 显式置位(默认都不翻转,见下)
- 幂等:同端口同波特率已开 → 直接返回成功(
note: already open),不会重开端口。 - DTR/RTS 默认不翻转——防 STM32 自动复位电路把目标板误复位;确需翻转才用
--dtr 1/--rts 1。 - 成功返回:
{"ok":true,"port":"COM6","baud":115200,"dtr":false,"rts":false,"gen":7,"since":"…ISO…"}。 - 失败(≤1s 快速返回,exit 1):
| 错误 | 含义 |
|---|---|
port not found: COM6 (available: COM1, …) |
该口不存在(Windows 对此常报 Access Denied,工具已按端口枚举结果纠正判定) |
port busy: COM1 (held by another process) |
口被其他进程占用 |
- 重开退避:距上次关口/daemon 启动 <1.5s 时,open 会先等待再开(CH340"释放后立即重开 → 读 0 字节"陷阱的规避)。
3.3 close — 关串口
... serial_tool.py close # → {"ok":true}
未开时返回 {"ok":true,"note":"port not open"}(不算错误)。daemon 继续运行。
3.4 send — 发数据
... serial_tool.py send "hello" # 文本(UTF-8 编码发出,精确字节,不加换行)
... serial_tool.py send "hello" --nl # 末尾追加 CRLF
... serial_tool.py send --hex 0D0A # 发字节 0x0D 0x0A(支持空格分隔:0D 0A 亦可)
... serial_tool.py send --hex 48454C4C4F # = "HELLO"
- 文本与
--hex二选一;都不给 = 用法错误(exit 3)。 - 成功返回:
{"ok":true,"bytes":7}(实际发出字节数)。 - 端口未开 →
port not open (run 'open' first)。 - 日志中的 TX 行会把控制字符转义成字面文本(
…hello\r\n),保持日志行式完整。
3.5 status — 全状态
... serial_tool.py status
{
"ok": true,
"daemon": {"pid": 16816, "version": "1.0.0", "uptime_s": 31},
"port": {"ready": true, "name": "COM1", "baud": 115200, "dtr": false, "rts": false,
"since": "2026-09-15T02:30:57.739+08:00"},
"counters": {"rx_bytes": 1834, "tx_bytes": 22, "rx_lines": 57, "tx_lines": 3, "errors": 0},
"last_rx": {"ts": "…", "line": "UI menu init: OK", "age_ms": 320},
"log": {"path": "…\\tools\\serial_tool\\state\\serial.log", "size": 4523, "gen": 7},
"state": "active",
"error": null
}
| 字段 | 说明 |
|---|---|
state |
active(正常收数据)/ stopped(口未开)/ error(读错/拔线,error 字段有详情;重开 open 即恢复) |
port.ready |
已开 && 非 error |
counters |
本 daemon 生命周期累计(daemon 重启清零) |
last_rx.age_ms |
距最后一行 RX 的毫秒数——判断"还在刷/已静默" |
log.gen |
当前日志代际(clear 会 +1) |
3.6 log — 读日志
... serial_tool.py log # 末 50 行
... serial_tool.py log --tail 200 # 末 200 行
... serial_tool.py log --tail 0 # 全部行
... serial_tool.py log --grep "App init OK" # 只留含子串的行(全文件搜,再截尾)
... serial_tool.py log --grep "init" --tail 100
... serial_tool.py log --grep "app init" -i # 忽略大小写
... serial_tool.py log --regex "\[STACK\]|Network Init" --tail 5 # 正则
- 直接读日志文件实现——daemon 死机/被杀也不受影响;
gen取自meta.json。 - 先过滤、后截尾(2026-10-10 修):旧实现先
--tail(默认 50)后 grep,log --grep "App init OK"只要开机后又刷了 50 行就必然查无此 marker——这就是旧文档"log --grep 实测不可信"的根因,现已修复,可直接作判定依据。 - 返回
{"ok":true,"gen":N,"lines":[…],"scanned":总行数,"matched":命中行数};--tail截掉了部分命中时另带note说明。--regex写错 →ok:false+ 错误文本(exit 3)。 - 等价的手动读法(Agent 用 Read/Grep 工具、人用 PowerShell 均可):
Get-Content '.dsh\skills\serial-assistant\scripts\state\serial.log' -Tail 20
Select-String -Path '.dsh\skills\serial-assistant\scripts\state\serial.log' -Pattern 'App init OK'
3.7 clear — 清空日志(代际 +1)
... serial_tool.py clear # → {"ok":true,"gen":8}
- 原地截断 + 写
[SYS] … log cleared (gen=N+1)标记行;不重开文件、不动串口。 - 用途:每次烧录/实验前 clear 一下,之后日志里就只属于本轮,新旧内容按
gen/标记行区分。
3.8 wait-idle — 等 RX 静默
... serial_tool.py wait-idle # 静默 8s 或超时 30s
... serial_tool.py wait-idle --settle-ms 5000 --max-ms 60000 # 自定义
... serial_tool.py wait-idle --first-timeout-ms 12000 # 首字节 12s 未到即判 no_data
{"ok": true, "rx_bytes": 1834, "idle": true, "waited_ms": 8041, "reason": "idle"}
reason |
含义 | idle |
|---|---|---|
idle |
收到过数据,且已连续静默 ≥ --settle-ms(正常收尾) |
true |
no_data |
本次等待期间一个字节都没收到(板子没输出/口不对/没烧进去) | false |
timeout |
--max-ms 到了仍在持续吐数据 |
false |
--settle-ms默认 8000 不是随便定的:本工程 boot 任务间有 3~4s 自然静默间隙,判据小于它会把开机日志截断。- 首字节门(2026-10-10 加):首字节到达之前不启动静默计时——旧实现从进入即计时,板子第一行比
--settle-ms来得晚就提前收尾("开机日志缺尾巴"的另一半成因)。--first-timeout-ms(0 = 同--max-ms)到点仍零字节 →no_data快速失败。 rx_bytes是本次增量(旧实现回的是 daemon 累计值,常驻 daemon 下动辄百万,无判读价值)。- CLI 超时自动推导:未显式带
--timeout时按--max-ms/1000 + 5取值,不再需要手工加(旧版固定 5s 会在 daemon 还在等时掐断连接 → CLI exit 2 + daemon.log 刷ConnectionAbortedError)。 - /serial-boot-test 流程(
run_boot_test.py)用它作为"烧录后日志收齐"的判据(--first-wait-ms默认 12000)。
3.9 start / stop / restart — daemon 生命周期
... serial_tool.py start # 拉起(已在跑 → already_running:true,不做任何事)
... serial_tool.py stop # 优雅退出:关串口 → 冲刷尾行 → 退出
... serial_tool.py restart # 停旧 + 拉新,保证新进程加载的是当前代码
- 日常不需要 start(任何命令都会自动拉起)。
- 改了
serial_tool.py之后要restart,否则运行中的还是旧代码。 stop后 COM 端口立即释放,可交给其他程序;daemon 死掉/被杀同理(下次命令自动拉起,但串口不会自动重开——需要时再open)。
3.10 selftest — 无头自检
... serial_tool.py selftest # 10 项进程内回环测试,无需硬件
# [PASS] T1a CRLF 行 … SELFTEST PASS(exit 0)
新机器搭完环境先跑它:过了 = 解释器/pyserial/核心算法都正常。
4. 日志文件(全量历史)
4.1 三个文件
以下文件均在 state/ 下,属运行时产物,未创建于仓库(首次运行工具后才生成):
| 文件 | 内容 | 谁读 |
|---|---|---|
.dsh/skills/serial-assistant/scripts/state/serial.log |
RX/TX/SYS 全量日志 | 你(Read/Grep/log 命令) |
.dsh/skills/serial-assistant/scripts/state/meta.json(运行时未创建) |
daemon pid / 版本 / 当前 gen / 口配置快照 | 工具内部 |
.dsh/skills/serial-assistant/scripts/state/daemon.log |
daemon 自身 stderr(启动行、异常) | 排障时看 |
整个 state/ 目录已 gitignore——纯本机运行时产物。
4.2 行格式
[SYS] 2026-09-15T02:30:57.739+08:00 port opened COM1 115200 (gen=0)
[RX] 2026-09-15T02:31:23.101+08:00 App init OK
[TX] 2026-09-15T02:31:23.986+08:00 hello_from_tool\r\n
[SYS] 2026-09-15T02:31:24.245+08:00 log cleared (gen=1)
[SYS] 2026-09-15T02:39:54.598+08:00 log truncated to 10MB (cap=20MB)
| 规则 | 说明 |
|---|---|
| 方向标记 | [RX] 串口收到 / [TX] 你发出 / [SYS] 工具事件(open/close/error/cleared/truncated) |
| 时间戳 | 本地 ISO8601 含毫秒与偏移;= 该行首字节到达时刻 |
| 只含完整行 | 残行留 daemon 内存,200ms 无后续数据才冲刷成行 → 你永远读不到半行 |
| 解码回退 | UTF-8 → GBK → 都不行则该文本域写 HEX:ab12…(纯二进制流时整行转 hex) |
| TX 转义 | 发出的控制字符写成字面 \r \n(日志按行存储,真换行会把一行劈成两行) |
| flush | 每行即 flush——文件随时可读、内容即时落盘 |
4.3 体积上限与截头
- 单文件上限 20MB;超过后 daemon 原地截头保留尾部 10MB 并写
[SYS] … log truncated标记行。 - 截头不重开文件、不重开端口、不影响正在收的数据——但文件头部的老内容会消失。
- 因此:查旧数据要看文件尾部(
log --tail N/Get-Content -Tail),别从文件头读。 - 嫌它涨得快/想立刻清空 →
clear(代际 +1,比截头干净)。
5. 典型工作流
5.1 交互式调试(发命令看响应)
... serial_tool.py open
... serial_tool.py send "AT+STATUS" --nl
... serial_tool.py log --tail 10 # 看响应(或直接 Read 日志文件)
# … 反复 send / log …
... serial_tool.py close
5.2 抓一次开机日志(手动版 /serial-boot-test)
... serial_tool.py open --port COM6
... serial_tool.py clear # 代际 +1:此后内容只属于本轮
# ……此刻去烧录/复位板子(ST-Link 烧录不占 COM,串口全程保持打开)……
... serial_tool.py wait-idle --settle-ms 8000 --max-ms 30000
... serial_tool.py log --tail 0 # 取本轮全量 → 人眼/脚本检查标记
自动化版本 = .dsh/skills/serial-boot-test/scripts/run_boot_test.py(同一序列 + 烧录 + 标记校验,见 自动化测试流程.md)。
5.3 长时运行监控
... serial_tool.py log --grep "ERROR" --tail 50 # 随时捞特定内容
... serial_tool.py status # 看 counters.errors / last_rx.age_ms
daemon 常驻,日志持续累积(20MB 自动截头),几天不关也没压力。
5.4 排障三件套
... serial_tool.py status # ① state/error/counters 一眼看全
Get-Content '.dsh\skills\serial-assistant\scripts\state\daemon.log' -Tail 30 # ② daemon 自身输出
... serial_tool.py restart # ③ 重建 daemon(顺带加载新代码)
6. 配置
唯一配置源:.serialtest/config.json
{
"portName": "COM6",
"portHint": "USB 串行设备",
"baudRate": 115200,
"httpPort": 18765,
"note": "……"
}
| 字段 | 用途 | 缺省 |
|---|---|---|
portName |
open 的默认端口;ports 的 configured 标注 |
无(必须给) |
baudRate |
默认波特率 | 115200 |
httpPort |
daemon HTTP 端口(一般不动) | 18765 |
换端口只改这个文件(/serial-boot-test 与手动调试都读它)。httpPort 冲突时才需要改。
7. 故障排查
| 现象 | 原因 | 处理 |
|---|---|---|
port not found: COMx (available: …) |
板子没接 / CH340 驱动没装 / 口号变了 | 对照 available 列表;装 CH340 驱动;改 .serialtest/config.json 的 portName |
port busy: COMx (held by another process) |
别的程序占着口 | Get-Process 找占用者关掉;或 … serial_tool.py close 释放本工具占的口 |
daemon start timeout / daemon unreachable |
daemon 起不来(端口冲突/解释器问题) | 看 state/daemon.log;… serial_tool.py restart;确认 18765 未被占(改 httpPort) |
daemon did not answer within Ns (busy? raise --timeout) |
命令耗时超过 --timeout——daemon 活着、只是还在等(典型:显式给 wait-idle 带了小 --timeout) |
调大 --timeout,或不显式给(wait-idle 按 --max-ms 自动推导)。2026-10-10 起这种情况不再顺手拉起第二个 daemon |
wait-idle 返回 no_data |
全程无 RX | 核对 COM 号与波特率;板子供电;烧录是否成功;status 看 port.ready |
日志里出现 HEX:… 行 |
该段字节不是 UTF-8/GBK 文本(二进制流/乱码) | 正常回退行为,按 hex 对照协议解析 |
state: error + error: read: … |
读串口出错/拔线 | daemon 仍活着;重插后 open 即恢复(自动 1.5s 退避) |
改了 serial_tool.py 行为没变 |
运行中的 daemon 还是旧代码 | … serial_tool.py restart |
开机日志"缺尾巴"(如缺 Network Init) |
① --settle-ms 太小,在 boot 自然静默间隙就收尾;② 首字节晚于 --settle-ms 到达,静默计时提前触发 |
用默认 8000(run_boot_test.py 已内置);2026-10-10 起首字节到达前不启动静默计时,必要时调 --first-timeout-ms |
open 报 no port specified … [config: …] |
.serialtest/config.json 缺失 / JSON 语法错 / baudRate·httpPort 非整数(现按 utf-8-sig 读并显式回显错误,不再静默回退成"没配端口") |
按 config_error 文本修文件;status 与 ports 也回显 config_error |
| 旧日志"丢了" | 20MB 截头丢的是文件头 | 查旧内容要看尾部;重要轮次开始前先 clear 分代 |
8. 常见问题
- daemon 会一直占着内存吗? 一个 Python 进程约 20MB;不需要时
stop即释放。常驻与否都不影响正确性。 - daemon 被强杀(taskkill)会怎样? 端口由系统自动释放;下次任何命令自动拉起新 daemon。串口不会自动重开,需要时再
open(这是设计:避免在设备异常时盲目重连)。 - 能同时开两个串口吗? v1 单口(一个 daemon 管一个口)。多口需求见
docs/测试说明/测试工具/串口助手设计说明.md§8 v2 候选。 - 能直接删
serial.log吗? 可以,但代际gen不会变、写句柄不变——建议用clear(原子 + 代际 +1 + 标记行)。 - 为什么 TX 日志里是字面
\r\n而不是真换行? 日志按行存储;真换行会把一条 TX 劈成两行,转义后一条 TX 恒一行。 - ST-Link 烧录会抢串口吗? 不会(烧录走 SWD,不走 COM)——这正是"串口全程保持打开、烧录前后不重开"的设计基础,也彻底绕开了 CH340 释放/重开陷阱。
- 别的程序/Agent 能绕过 CLI 直接用吗? 能——daemon 暴露本机 HTTP JSON 接口(§9)。
9. 附录:HTTP 协议(绕过 CLI)
daemon 在 127.0.0.1:<httpPort>(默认 18765)提供 JSON API,与 CLI 一一对应:
| 方法 路径 | 请求体(JSON,可空) | 说明 |
|---|---|---|
GET /ping |
— | {"ok":true,"version":"1.0.0","pid":…} |
GET /status |
— | 同 status 命令 |
GET /ports |
— | 同 ports 命令 |
POST /open |
{"port":"COM6","baud":115200,"dtr":0,"rts":0}(均可省) |
同 open |
POST /close |
— | 同 close |
POST /send |
{"text":"hi","nl":true} 或 {"hex":"0D0A"} |
同 send |
POST /clear |
— | 同 clear |
POST /wait-idle |
{"settle_ms":8000,"max_ms":30000,"first_timeout_ms":12000}(均可省) |
同步阻塞至静默/首字节超时/整体超时才返回 |
POST /stop |
— | 关口后 daemon 退出(响应先于退出发出) |
响应码:200 成功 / 400 领域错误(ok:false+error)/ 504 daemon 忙(命令 20s 未完成)/ 404 未知端点。
PowerShell 示例:
Invoke-RestMethod http://127.0.0.1:18765/status
Invoke-RestMethod -Method Post http://127.0.0.1:18765/send -Body '{"text":"ping"}'