Files
STM32F4-Base/docs/测试说明/串口助手使用指南.md
T
wandering1 511fd30cc6 docs: 清理全仓变更记录流水(43 篇整节删 / 23 篇留一行 / 5 篇保留)
用户裁定:文档只写现状,历史流水一律删掉,过程查 git log。

- 43 篇「变更记录 / 修订记录」整节删除:继电保护规划 12.6 KB、产品
  需求 11.8 KB、项目部署环境 5.5 KB、BSD Socket 指南 5.3 KB、架构
  总览 5.0 KB、实机测试实施指南 3.8 KB 等
- 23 篇别处仍有指向:只留节名 + 一行「本文件不再记流水账,过程查
  git log」,节号与锚点不位移(doc-links 不因此断链)
- 5 篇整表保留(被当唯一证据 / 活台账引用):看门狗设计说明、配置
  文件设计说明、RAM预算台账、内容页架构、本地工具路径环境变量方案
- 规范同步:文档规范 §5.1 改新裁定;测试文档模板规范删示例节与
  登记句;测试开发标准流程 / UI 两篇 / 实机测试实施指南 / 四篇单测
  说明 / 并行任务工作树方案 / 编号规范 的「登记变更记录」改「写进
  正文」
- 技能同步:stale-doc-count-sweep / ui-verify / id-family 三个 SKILL
  正文不再要求追加流水行(顺带消掉 SKILL.md:121「产品需求 §12」
  失效引用)
- 修 5 处悬空指针:采样有效性校验目录锚点、BSD 指南「文末变更
  记录」、docs/README 过期描述、网络自愈架构两处指向面

72 篇文档净减 600 余行;check_doc_links --all --sections-strict 可判
定比例 61% → 78%、失效引用 0;静态门禁 passed=50 failed=0;gitlint
10 条历史提交信息 0 failed。

未提交他人在飞改动:.dsh/skills/keil-build/*(%SystemDrive% 守卫)。
2026-10-28 22:00:34 +08:00

20 KiB
Raw Blame History

Python 串口助手(serial_tool)使用指南

定位:面向使用者的操作手册——命令怎么敲、输出怎么看、常见场景怎么组合、出问题怎么排查。 关联文档:设计决策与实现细节见 串口助手设计说明.md;@test 自动化闭环见 自动化测试流程.md;serial-assistant skill 有一页速查(用到自动加载,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' <命令> [参数]

两条心法:

  1. 所有输出都是 JSON(stdout),退出码 0=成功 / 1=领域错误 / 2=daemon 不可达 / 3=用法错误;
  2. 历史不用"采集"——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 静默(@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)。
  • @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 抓一次开机日志(手动版 @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

换端口只改这个文件(@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"}'