# Python 串口助手(serial_tool)使用指南 > **定位**:面向使用者的操作手册——命令怎么敲、输出怎么看、常见场景怎么组合、出问题怎么排查。 > **关联文档**:设计决策与实现细节见 `串口助手设计说明.md`;/serial-boot-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 均可): ```powershell & '.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. 快速上手 ```powershell # ① 看本机有哪些 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` — 枚举串口 ```powershell ... serial_tool.py ports ``` ```json {"ok": true, "ports": [{"name": "COM1", "description": "通信端口 (COM1)", "hwid": "ACPI\\PNP0501\\0"}, …], "configured": "COM6"} ``` - `configured` 来自 `.serialtest/config.json` 的 `portName`;找不到它 = 板子没接/驱动没装/口号变了。 - CH340 的 `description` 通常含"USB 串行设备"字样。 ### 3.2 `open` — 开串口 ```powershell ... 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` — 关串口 ```powershell ... serial_tool.py close # → {"ok":true} ``` 未开时返回 `{"ok":true,"note":"port not open"}`(不算错误)。daemon 继续运行。 ### 3.4 `send` — 发数据 ```powershell ... 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` — 全状态 ```powershell ... serial_tool.py status ``` ```json { "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` — 读日志 ```powershell ... 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 均可): ```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) ```powershell ... serial_tool.py clear # → {"ok":true,"gen":8} ``` - 原地截断 + 写 `[SYS] … log cleared (gen=N+1)` 标记行;**不重开文件、不动串口**。 - 用途:每次烧录/实验前 clear 一下,之后日志里就只属于本轮,新旧内容按 `gen`/标记行区分。 ### 3.8 `wait-idle` — 等 RX 静默 ```powershell ... 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 ``` ```json {"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 生命周期 ```powershell ... 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` — 无头自检 ```powershell ... 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 交互式调试(发命令看响应) ```powershell ... 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) ```powershell ... 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 长时运行监控 ```powershell ... serial_tool.py log --grep "ERROR" --tail 50 # 随时捞特定内容 ... serial_tool.py status # 看 counters.errors / last_rx.age_ms ``` daemon 常驻,日志持续累积(20MB 自动截头),几天不关也没压力。 ### 5.4 排障三件套 ```powershell ... 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` ```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:`(默认 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 示例: ```powershell Invoke-RestMethod http://127.0.0.1:18765/status Invoke-RestMethod -Method Post http://127.0.0.1:18765/send -Body '{"text":"ping"}' ```