串口采集/测试去重:删 tools/serial_tool 与 test/host run_boot_test/verify_log 副本,引用改指 .dsh/skills 串口 skill
This commit is contained in:
@@ -15,7 +15,7 @@ whenToUse: 新增任务/加大缓冲/改栈/接入日志之前之后、链接报
|
||||
# scripts/ram_report.py (.map → RAM/Flash 分区与按文件占用表)
|
||||
# scripts/stack_report.py (.htm 静态调用图 → 每任务最坏链 vs 配置栈)
|
||||
# 叠加运行时水位(有真机日志时):
|
||||
& '.venv\Scripts\python.exe' scripts/stack_report.py -s tools\serial_tool\state\serial.log
|
||||
& '.venv\Scripts\python.exe' scripts/stack_report.py -s .dsh\skills\serial-assistant\scripts\state\serial.log
|
||||
```
|
||||
|
||||
退出码:0=全部通过 / 1=有任务不达标 / 2=解析错误(.htm/.map 缺失先重跑构建)。
|
||||
|
||||
@@ -32,7 +32,7 @@ whenToUse: 换新机器/新克隆要把环境搭起来、构建或烧录报"环
|
||||
`Get-PnpDevice -Class Ports` 能看到 COMx、`reset.bat list` 能看到探针 SN。
|
||||
4. **验收三连**(全绿才算环境就绪):
|
||||
```powershell
|
||||
& '.venv\Scripts\python.exe' tools/serial_tool/serial_tool.py selftest # 23/23 PASS
|
||||
& '.venv\Scripts\python.exe' .dsh/skills/serial-assistant/scripts/serial_tool.py selftest # 23/23 PASS
|
||||
& "MDK-ARM\build.bat" # exit 0
|
||||
& "MDK-ARM\reset.bat" list # 列出探针
|
||||
```
|
||||
|
||||
@@ -36,5 +36,5 @@ cmake --build simulator/build --config Debug # 0 警告
|
||||
- **禁止**把源文件复制进 `simulator/`——两端共用 `Drivers/BSP/` 原件(UI_界面制作流程.md C7)
|
||||
- 只注册 `.c`(含 `.s`);`.h` 靠 include 路径找到,不加工程
|
||||
- 漏注册的典型症状:一端编译失败(undefined reference)或一端行为悄悄停留在旧逻辑
|
||||
- 注册完成后跑回归:UI 文件 → `ui-verify` skill;其他模块 → `@test`(`test/host/run_boot_test.py`)
|
||||
- 注册完成后跑回归:UI 文件 → `ui-verify` skill;其他模块 → `@test`(`.dsh/skills/serial-boot-test/scripts/run_boot_test.py`)
|
||||
- 改过 `CMakeLists.txt` 后需重新 `cmake -B build` 配置
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
输出结构化判定(格式与旧 PowerShell 版逐字一致,下游文档/习惯不受影响)。
|
||||
|
||||
用法:
|
||||
& '.venv\\Scripts\\python.exe' test/host/verify_log.py --log-path <file>
|
||||
& '.venv\\Scripts\\python.exe' .dsh/skills/serial-boot-test/scripts/verify_log.py --log-path <file>
|
||||
[--required 'marker1;marker2']
|
||||
[--forbidden 'FAIL;ERROR;HardFault;assert;[ERR]']
|
||||
[--require-suite-summary 'Storage Tests;GD5F2GQ5UE Tests']
|
||||
|
||||
+2
-2
@@ -128,8 +128,8 @@ docs/images/*.png
|
||||
# Python venv (unified env, see AGENTS.md "Python 环境"; rebuild via requirements.txt)
|
||||
.venv/
|
||||
|
||||
# Serial assistant (tools/serial_tool) runtime state (daemon log/meta, local runtime artifacts)
|
||||
tools/serial_tool/state/
|
||||
# Serial assistant runtime state (daemon log/meta, local runtime artifacts)
|
||||
.dsh/skills/serial-assistant/scripts/state/
|
||||
|
||||
# CI 运行时产物(.gitea/workflows/ci.yml 的 S3 步骤在 runner 工作区落盘的日志;
|
||||
# 零依赖存档方案——落在工作区目录而非上传 artifact,故必须忽略以免误提交)
|
||||
|
||||
@@ -16,7 +16,7 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v
|
||||
## 构建与验证
|
||||
|
||||
- `@build` = `& "MDK-ARM\build.bat"`(仓库根目录用**相对路径**);退出码 0 = 0 错误 0 警告,1 = 有警告或错误(查 `MDK-ARM/build_log.txt`),2 = `DTU_KEIL_UV4` 未设/无效(换机必设,见 `docs/环境与部署/本地工具路径环境变量方案.md`);UV4 路径经 `DTU_KEIL_UV4` 环境变量解析(**不再硬编码**;显式设置但无效会立即报错、不静默回退)
|
||||
- `@test` / `test/host/run_boot_test.py` 一键闭环(2026-09-20 起为纯 Python 标准库实现,替代 PowerShell 版;`& '.venv\Scripts\python.exe' test/host/run_boot_test.py`):开串口+清日志 → 烧录(`MDK-ARM/download.bat`,下载后 Reset&Run;快路径 `--flash-cmd "MDK-ARM\reset.bat flash"`)→ **固件身份戳**(从刚烧进去的 hex 里抠 `fw build: <DATE> <TIME>` 追加成 Required 标记)→ wait-idle(等 RX 静默 8s,超时 30s,首字节 12s 未到判 `no_data`)→ 提取日志 → **烧录回读校验**(`reset.bat cmp` = `-CmpFile`;不一致 = 本次运行作废 exit 2)→ `test/host/verify_log.py` 标记校验(含**启动锚点计数**:`reset cause:` 出现 0 次或 >1 次 = FAIL,`--max-boot-count` 调;Forbidden 命中**逐行打印行号+内容**;boot_log 每行带 `[+Nms]` daemon 毫秒时基 → 可查标记顺序 `--require-order` 与开机耗时 `--done-marker`/`--max-boot-ms`,实测 `net init: OK` = **+537ms**);退出码 0=PASS / 1=FAIL / 2=采集或烧录错误(含回读不一致)/ 3=用法/预检错误
|
||||
- `@test` / `.dsh/skills/serial-boot-test/scripts/run_boot_test.py` 一键闭环(2026-09-20 起为纯 Python 标准库实现,替代 PowerShell 版;`& '.venv\Scripts\python.exe' .dsh/skills/serial-boot-test/scripts/run_boot_test.py`):开串口+清日志 → 烧录(`MDK-ARM/download.bat`,下载后 Reset&Run;快路径 `--flash-cmd "MDK-ARM\reset.bat flash"`)→ **固件身份戳**(从刚烧进去的 hex 里抠 `fw build: <DATE> <TIME>` 追加成 Required 标记)→ wait-idle(等 RX 静默 8s,超时 30s,首字节 12s 未到判 `no_data`)→ 提取日志 → **烧录回读校验**(`reset.bat cmp` = `-CmpFile`;不一致 = 本次运行作废 exit 2)→ `.dsh/skills/serial-boot-test/scripts/verify_log.py` 标记校验(含**启动锚点计数**:`reset cause:` 出现 0 次或 >1 次 = FAIL,`--max-boot-count` 调;Forbidden 命中**逐行打印行号+内容**;boot_log 每行带 `[+Nms]` daemon 毫秒时基 → 可查标记顺序 `--require-order` 与开机耗时 `--done-marker`/`--max-boot-ms`,实测 `net init: OK` = **+537ms**);退出码 0=PASS / 1=FAIL / 2=采集或烧录错误(含回读不一致)/ 3=用法/预检错误
|
||||
- 默认校验标记:Required `App init OK` / `Network Init` / `net init: OK` + **自动追加的固件身份戳 `fw build: <__DATE__> <__TIME__>`**(`App/app/app_main.c` 开机打印,`run_boot_test.py` 从待烧 hex 里抠出该串当 Required——此前所有标记都是固定串,**旧镜像/错镜像照样全中**,"反复重下载、芯片一直是旧镜像"在自动闭环里不可见,2026-10-10 补;`--no-fw-check` 关闭);Forbidden `FAIL` / `ERROR` / `HardFault` / `[ERR]`(**四个 Fault 的崩溃日志都以 `[ERR]` 开头 → 一律判 FAIL**,这是刻意的:出现 Fault 即本次运行不通过);按模块定制标记(网络/存储/RTC/ADC/RS485/UI)见 `docs/测试说明/自动化测试流程.md`
|
||||
- ⚠️ 旧绝对路径 `D:\Code\DTU 程序\...` 出现即失效(仓库已搬迁),一律改用仓库相对路径
|
||||
- **下载/烧录铁律(2026-09-20 实测)**:走 `& "MDK-ARM\reset.bat" flash [文件]`(= `ST-LINK_CLI -c SWD -P <文件> -Rst -Run`),烧后 `-Rst -Run` **不可省略**——裸 `-P` 会报 `Programming Complete`,但目标核处于死循环/异常状态时写入并不落地(芯片内容不变;这就是 09/19~09/20 "反复重下载、芯片一直是旧镜像、二分法失效" 的根因);`.hex`/`.bin` 均可直烧(1MB ≈ 6.5s,快于 UV4 路径);只重启不烧录用 `reset.bat` 无参(= `-c SWD -Rst -Run`,本机 3 线接线唯一复位手段:nRST 未接、`-HardRst`/`RM=` 不可用)
|
||||
@@ -49,7 +49,7 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v
|
||||
- **内核启动(`osKernelStart`)前禁止任何 FreeRTOS/CMSIS-RTOS API 调用**(2026-09-20 启动失败实证根因,修复 `82411cf`):`osMessageQueueNew`/`osMessageQueuePut`/`osMutexNew`/`osSemaphoreNew` 等在内核启动前调用会杀死启动(09/19~09/20 "每次必死" 的代码层根因 = `syslog_mcu_init` 在内核启动前调用 `osMessageQueueNew`)。内核启动前代码的标准模式:纯寄存器/HAL 轮询操作 + **无锁静态缓冲**(如 `s_preboot[8]` 暂存内核前日志)+ **内核启动后惰性创建真队列**(任务上下文首次使用时,见 `syslog_drain`);凡在内核启动前执行的初始化函数里不得创建任何 CMSIS 对象
|
||||
- CH395F SPI:每次事务须 `ch395f_spi_begin()/end()` 包裹;分频系数**必须为 8**(PCB 约束,勿"优化")
|
||||
- **就绪屏障位必须 `osFlagsNoClear`**(2026-09-21 实证):`osEventFlagsWait` **默认消费**所等位,而 `SYS_EVT_NET_READY`/`SYS_EVT_STORAGE_READY` 是**状态位**——被 netTask/uiTask/timeSync 先消费掉后,`ftpTask` 的 `WaitAll(两位)` 永不成立 → FTP 21 端口静默不监听(无日志、无报错,纯靠可观测性才揪出来)。就绪位一律 `osFlagsWaitAny|osFlagsNoClear` / `osFlagsWaitAll|osFlagsNoClear`;命令位(`SYS_EVT_CFG_SAVE`/`SYS_EVT_NET_RECONFIG`)保持消费型等待
|
||||
- **内存预算紧张**:新增任何任务/缓冲/栈前必须先核算 RAM 余量(`docs/内存管理/`、`待处理内容.md`);**栈数据来源**:`& '.venv\Scripts\python.exe' scripts/stack_report.py -s tools\serial_tool\state\serial.log`(Keil 静态调用图 → 每任务最坏链,+ 串口 `[STACK]` 运行时水位)与 `scripts/ram_report.py`(.map 静态占用);改栈或加大局部变量前后各跑一次
|
||||
- **内存预算紧张**:新增任何任务/缓冲/栈前必须先核算 RAM 余量(`docs/内存管理/`、`待处理内容.md`);**栈数据来源**:`& '.venv\Scripts\python.exe' scripts/stack_report.py -s .dsh\skills\serial-assistant\scripts\state\serial.log`(Keil 静态调用图 → 每任务最坏链,+ 串口 `[STACK]` 运行时水位)与 `scripts/ram_report.py`(.map 静态占用);改栈或加大局部变量前后各跑一次
|
||||
- **自研代码禁用 C 库堆分配**(2026-09-18,P0-1):禁止 `malloc/calloc/realloc/strdup/free`——本机工具链 MicroLIB 的 C 堆由 `startup_stm32f407xx.s` 的 `Heap_Size EQU 0x200` 限定为 **512 B**,KB 级分配必然返回 NULL 且**模拟器(宿主 CRT 堆)完全正常、照不出来**。需要动态内存只用 `pvPortMalloc/vPortFree`(FreeRTOS 堆 `configTOTAL_HEAP_SIZE`),或像配置引擎那样把分配收进平台后端(`cfg_store_alloc/free/heap_free`:MCU=FreeRTOS 堆、模拟器=宿主堆,共享核心零分配符号);分配失败必须有 `FAIL` 日志 + 自检上报,暂态峰值要写编译期预算
|
||||
- **FatFs 打开模式陷阱(2026-09-20 台架实测,`Lib/FatFs` 移植版)**:`FA_OPEN_APPEND`(0x30) = `FA_OPEN_ALWAYS|FA_SEEKEND`,**不含 `FA_WRITE`(0x02)**——不带写位打开的文件 `f_write` 恒返回 `FR_DENIED`(7)(/log 写失败真根因);追加写必须用 `FA_WRITE|FA_OPEN_APPEND`。该移植的 `f_write` 簇分配失败时返回 `FR_OK`+短写(无错误码),判失败必须校验写回字节数。标志位为旧式编号(`FA_READ`=0x01 / `FA_WRITE`=0x02 / `FA_CREATE_NEW`=0x04 / `FA_CREATE_ALWAYS`=0x08 / `FA_OPEN_ALWAYS`=0x10 / `FA_OPEN_APPEND`=0x30 / `FA_SEEKEND`=0x20(ff.c 内部定义)),与上游 FatFs R0.14 不同,勿照抄上游代码示例
|
||||
- **FatFs 文件名约束(2026-09-20 台架实证,`.fschk` 无限重格式化事故根因)**:本移植 `Lib/FatFs/ffconf.h` 为 `FF_USE_LFN=0`,**只支持严格 8.3 短名**——主名≤8 字符、扩展≤3 字符、**不得以点开头**(`.fschk` 会被 `ff.c create_short` 把点后当扩展域解析超限 → `f_open` 恒返 `FR_INVALID_NAME`(6),纯 RAM 名字判定、不碰盘)。ff.c `create_short` 全部拒绝点已加 DEBUG 打印:`[ERR][FATFS] SFN reject (原因) seg='名字片段' bad_ch=坏字符`,非法名字调用看开机日志即可定位。自愈类逻辑**严禁**把名字/参数类错误码(`FR_INVALID_NAME` 等)归入卷损坏触发集(整卷重格式化不可逆)
|
||||
@@ -112,16 +112,16 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v
|
||||
|
||||
## 串口助手 serial_tool
|
||||
|
||||
- 入口:`& '.venv\Scripts\python.exe' tools/serial_tool/serial_tool.py <命令>`(daemon 自动拉起;全部命令 5s 超时、JSON 输出、exit 0/1/2/3)
|
||||
- 全量历史 = 日志文件 `tools/serial_tool/state/serial.log`(RX/TX/SYS 时间戳行);agent 直接 Read/Grep 文件,**无采集窗口仪式**;20MB 上限自动截头保尾 10MB
|
||||
- 入口:`& '.venv\Scripts\python.exe' .dsh/skills/serial-assistant/scripts/serial_tool.py <命令>`(daemon 自动拉起;全部命令 5s 超时、JSON 输出、exit 0/1/2/3)
|
||||
- 全量历史 = 日志文件 `.dsh/skills/serial-assistant/scripts/state/serial.log`(RX/TX/SYS 时间戳行);agent 直接 Read/Grep 文件,**无采集窗口仪式**;20MB 上限自动截头保尾 10MB
|
||||
- 常用:`open`(幂等)/ `send <文本>`(或 `--hex 0D0A`)/ `log --tail N --grep P [-i] [--regex]`(**先过滤后截尾**)/ `clear`(代际+1)/ `wait-idle --settle-ms 8000 --max-ms 30000 [--first-timeout-ms 12000]` / `status` / `close`
|
||||
- 串口端口单一配置源:`.serialtest/config.json`(当前 COM6,2026-09-21 台架重插后由 COM7 变更;2026-10-10 由 `.opencode/serial_config.json` 迁入,仓库脚本与 `.dsh/skills/` 串口 skill 共用此文件);端口变化**只改这个文件**
|
||||
- 串口端口单一配置源:`.serialtest/config.json`(当前 COM6,2026-09-21 台架重插后由 COM7 变更;2026-10-10 由 `.opencode/serial_config.json` 迁入;串口采集/测试实现已收敛到 `.dsh/skills/serial-assistant` + `serial-boot-test`,仓库内不再有 `tools/serial_tool`/`test/host/run_boot_test` 副本);端口变化**只改这个文件**
|
||||
- 命令/工作流/故障排查 `docs/测试说明/串口助手使用指南.md`;契约/daemon 行为/陷阱 `docs/测试说明/串口助手设计说明.md`
|
||||
- **抓开机日志铁律(2026-09-20 实测;②③ 已于 2026-10-10 修复)**:① 顺序 `open` → `clear` → 烧录/复位 → `wait-idle` → 读日志校验标记(**采集必须先于烧录**,否则错过开机头几行);② `wait-idle` 的 CLI 超时现按 `--max-ms` **自动推导**,不再需要手工带 `--timeout`(旧版固定 5s 会在 daemon 还在等时掐断连接、daemon.log 刷 traceback);③ `log --grep` 已改为**先过滤后截尾**——旧实现先 `tail`(默认 50)后 grep,开机 marker 必然查不到,这才是当年"`log --grep` 实测不可信"的真根因,现可直接作判定依据(Read/Grep 文件仍等效)
|
||||
- **`wait-idle` 判据(2026-10-10 修)**:`no_data` = **本次等待期间零字节**(旧实现比的是 daemon 累计 `rx_bytes` 是否为 0,常驻 daemon 下永不触发 → 板子全程静默也误报 `idle:true`);**首字节到达前不启动静默计时**(`--first-timeout-ms` 兜底,`run_boot_test.py --first-wait-ms` 默认 12000);返回的 `rx_bytes` 是本次增量而非累计
|
||||
- **采集卫生与查询面(2026-10-10 第三批)**:无换行残行 **64KB 封顶**(板子跑飞/烧录期的二进制垃圾旧实现无限攒内存,还会把后续每条正常行拼成一条读不出的长行);`open`/`close`/`clear` = **会话边界**,残行一并丢弃(旧实现里上一段被复位截断的半行会粘进新会话第一行,开机日志首行因此不可读);`cap_truncate` **按行边界**截头(旧实现按字节切 → 截出来的首行是半行,破坏"只含完整行"不变量);`open` 不带 `--dtr/--rts` = **保持当前线路状态**(旧实现把"没给"当 False,重开即静默拉低),显式给则现场应用并在 `note` 回显;`ports` 回 `configured_present` + `hint`(系统里只有一个候选才给,多候选不猜),`open` 的 `port not found` 也带 `[hint: ...]`;`status` 回 `rx_age_ms`
|
||||
- ⚠️ **看到两个 `serial_tool.py --serve` 进程是正常的**(2026-10-10 实测):`.venv\Scripts\python.exe` 是 venv stub,会派生基解释器(本机 = `C:\ProgramData\anaconda3\python.exe`)子进程跑真 daemon,`status.daemon.pid` 报的是子进程;**别把这对父子当成"多 daemon 并存"去 taskkill**(HTTP 口独占绑定 `SO_EXCLUSIVEADDRUSE` 已生效,`status.daemon.exclusive_bind: true` 可验)。真要清的是 `status`/`meta.json` 里那个 pid 之外的**独立**残留实例
|
||||
- **`open` 报"拒绝访问/端口被占用"时**:先查**残留旧 daemon**(上一会话遗留的 `serial_tool.py --serve` python 进程——口句柄已死但进程仍占着串口与 HTTP 口 18765)→ `taskkill /F /PID <旧PID>` → 重新 `open`(自动拉起新 daemon,系统自动释放口);改 `serial_config.json` 后同样必须先杀旧 daemon 再 `open`(daemon 启动时缓存配置)
|
||||
- **`open` 报"拒绝访问/端口被占用"时**:先查**残留旧 daemon**(上一会话遗留的 `serial_tool.py --serve` python 进程——口句柄已死但进程仍占着串口与 HTTP 口 18765)→ `taskkill /F /PID <旧PID>` → 重新 `open`(自动拉起新 daemon,系统自动释放口);改 `.serialtest/config.json` 后同样必须先杀旧 daemon 再 `open`(daemon 启动时缓存配置)
|
||||
|
||||
## LVGL PC 模拟器
|
||||
|
||||
|
||||
@@ -158,9 +158,7 @@ STM32F4-Base/
|
||||
│ │ ├── storage_test_task.c/h # 存储性能测试
|
||||
│ │ ├── net_test_task.c/h # 网络层测试
|
||||
│ │ └── net_latency_test.c/h # 链路时延探针
|
||||
│ └── host/ # 主机端脚本(PC 侧)
|
||||
│ ├── run_boot_test.py # 自动化启动测试(@test 入口)
|
||||
│ ├── verify_log.py # 日志标记校验
|
||||
│ └── host/ # 主机端脚本(PC 侧;@test 入口已迁至 .dsh/skills/serial-boot-test/)
|
||||
│ ├── ch395f_socket_test.py # Socket 通信测试
|
||||
│ ├── ftp_test.py / ftp_up_dl_test.py / ftp_speed_test.py / ftp_capture.py / ftp_regression.py
|
||||
│ ├── e02_probe.py # E-02 专项探针
|
||||
@@ -193,8 +191,7 @@ STM32F4-Base/
|
||||
│ ├── tests/ # 无头回归脚本 + 黄金参考(菜单像素回归 / 内容页黄金参考)
|
||||
│ └── SDL2/ # SDL2 2.32.10 预编译库
|
||||
├── tools/ # 开发工具
|
||||
│ ├── font_gen/ # 中文字体生成工具
|
||||
│ └── serial_tool/ # Python 串口助手(常驻 daemon + CLI,见 docs/测试说明/串口助手设计说明.md)
|
||||
│ └── font_gen/ # 中文字体生成工具
|
||||
├── ui_cn_chars.txt # 中文字体字符集
|
||||
├── requirements.txt # Python 依赖清单(统一 .venv 环境,见 AGENTS.md「Python 环境」)
|
||||
├── STM32F407-Demo.ioc # CubeMX 工程文件
|
||||
@@ -285,7 +282,7 @@ UI 改动回归门禁(菜单像素回归 + 内容页黄金参考 + 池水位
|
||||
|
||||
```powershell
|
||||
# 一键测试:串口捕获 → 烧录 → 日志校验(2026-09-20 起为纯 Python 标准库实现)
|
||||
& '.venv\Scripts\python.exe' test/host/run_boot_test.py
|
||||
& '.venv\Scripts\python.exe' .dsh/skills/serial-boot-test/scripts/run_boot_test.py
|
||||
```
|
||||
|
||||
退出码:0 = PASS | 1 = FAIL | 2 = 错误
|
||||
@@ -313,7 +310,7 @@ UI 改动回归门禁(菜单像素回归 + 内容页黄金参考 + 池水位
|
||||
|
||||
### 串口调试
|
||||
|
||||
通过仓库内置 Python 串口助手(`tools/serial_tool/`)自主读取 USART1 串口打印:
|
||||
通过仓库内置 Python 串口助手(`.dsh/skills/serial-assistant/`)自主读取 USART1 串口打印:
|
||||
|
||||
- 端口配置:`.serialtest/config.json`(单一配置源)
|
||||
- 用法详见 `docs/测试说明/串口助手使用指南.md`,设计见 `docs/测试说明/串口助手设计说明.md`
|
||||
@@ -377,7 +374,7 @@ pre-commit run --all-files
|
||||
| `Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c` | NAND Flash 驱动 |
|
||||
| `Drivers/BSP/NET/net_socket.c` | BSD Socket API |
|
||||
| `MDK-ARM/build.bat` | 编译脚本 |
|
||||
| `test/host/run_boot_test.py` | 自动化测试脚本 |
|
||||
| `.dsh/skills/serial-boot-test/scripts/run_boot_test.py` | 自动化测试脚本 |
|
||||
|
||||
## 已知问题
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@ docs/
|
||||
| [`控制台单写者日志设计说明.md`](子系统设计/控制台单写者日志设计说明.md) | 控制台日志单写者模型(避免多任务交错输出) |
|
||||
| [`运行与自检记录设计说明.md`](子系统设计/运行与自检记录设计说明.md) | `/log/run.log`·`selftest.log` 定长记录与待机显示 |
|
||||
| [`看门狗设计说明.md`](子系统设计/看门狗设计说明.md) | IWDG(CubeMX 配置)+ 任务心跳位图:选型与超时推导、掩码与埋点、取证机制、验证记录;已台架闭环(V2/V4/V5) |
|
||||
| [`ADC采样节拍设计说明.md`](子系统设计/ADC采样节拍设计说明.md) | **ADC 采集链路采样节拍**:现状(任务周期软触发)→ 目标(TIM6 更新中断触发 CONVST)的方案选型、CubeMX 配置规格、代码改造清单、时序/优先级分析与验收判据;**规划中(未实现)** |
|
||||
| [`网络/BSD_Socket_API_使用指南.md`](网络/BSD_Socket_API_使用指南.md) | **上层视图**:BSD Socket API 公开语义、示例、常见问题 |
|
||||
| [`网络/BSD_Socket_API_函数调用关系.md`](网络/BSD_Socket_API_函数调用关系.md) | **内部视图**:netTask / 消息队列 / L3 挂起 / 重试对账的调用关系与设计意图 |
|
||||
| [`网络/Net层阻塞模型架构设计.md`](网络/Net层阻塞模型架构设计.md) | **等待模型**:pending 请求架构、五路完成语义、设计边界与性能特征 |
|
||||
@@ -261,6 +262,7 @@ docs/
|
||||
|
||||
| 日期 | 变更 | 说明 |
|
||||
|---|---|---|
|
||||
| 2026-10-10 | 新增子系统设计 | 新建 `docs/子系统设计/ADC采样节拍设计说明.md`(ADC 采集链路 1 kHz 硬件定时器触发的 CubeMX 配置规格与代码改造清单,B1 遗留项规划);本索引 §5.1 补登记 |
|
||||
| 2026-10-10 | 新增算法专题 | 新建 `docs/算法/继电保护功能复现规划.md`(保护链路复现总规划);本索引 §1 目录树补 `算法/`、§5 拆为 5.1 功能子系统 / 5.2 算法专题并登记 `算法/` 两篇文档 |
|
||||
| 2026-10-05 | 新增产品需求文档 | 新建 `docs/产品需求/产品需求设计文档.md`(面向产品的需求基线);本索引新增 §3 产品需求、§2 推荐阅读顺序置顶、§1 目录树补 `产品需求/`;后续章节号顺延(架构设计 §4 … 变更记录 §12) |
|
||||
| 2026-09-25 | 目录重组 + 索引建立 | 顶层 16 篇散落 md 归入 5 个新目录(`架构设计/` `子系统设计/` `网络/` `审查记录/` `环境与部署/`);全仓库 90 处引用同步;新建本索引(D-5) |
|
||||
|
||||
@@ -249,7 +249,7 @@ flowchart TD
|
||||
## 六、P3:MCU 验证
|
||||
|
||||
1. `@build` → **0 错误 0 警告**(AGENTS.md 强制;有警告看 `MDK-ARM/build_log.txt`)
|
||||
2. `@test`(一键 `test/host/run_boot_test.py`):
|
||||
2. `@test`(一键 `.dsh/skills/serial-boot-test/scripts/run_boot_test.py`):
|
||||
- 采集窗口**先于烧录武装**(脚本 `--flash-cmd` 已保证顺序)
|
||||
- Required:`UI port init: OK;UI menu init: OK` + 本界面定制标记(按屏增补,如页头标题字样)
|
||||
- Forbidden:`FAIL;ERROR;HardFault`
|
||||
|
||||
@@ -550,7 +550,7 @@ mac=00:00:00:00:00:00
|
||||
| 端口 | USART1(PA9 TX / PA10 RX),115200 8N1 |
|
||||
| 用途 | printf 日志 + `@test` 采集通道 |
|
||||
| 配置源 | `.serialtest/config.json`(单一配置源) |
|
||||
| 工具 | 仓库内置 Python 串口助手(`tools/serial_tool/`,常驻 daemon + CLI) |
|
||||
| 工具 | 仓库内置 Python 串口助手(`.dsh/skills/serial-assistant/`,常驻 daemon + CLI) |
|
||||
|
||||
**启动标记(`@test` 判据)**:
|
||||
- Required:`App init OK` / `Network Init` / `net init: OK` / `PHY: 100M FULL`
|
||||
@@ -647,7 +647,7 @@ mac=00:00:00:00:00:00
|
||||
# 构建(@build):0 错 0 警
|
||||
& "MDK-ARM\build.bat" # 退出码 0=PASS / 1=构建失败 / 2=环境错误
|
||||
# 自动化测试(@test):串口采集 → 烧录 → 日志校验
|
||||
& '.venv\Scripts\python.exe' test/host/run_boot_test.py # 0=PASS / 1=FAIL / 2=采集或烧录错误 / 3=用法错误
|
||||
& '.venv\Scripts\python.exe' .dsh/skills/serial-boot-test/scripts/run_boot_test.py # 0=PASS / 1=FAIL / 2=采集或烧录错误 / 3=用法错误
|
||||
# 全库门禁
|
||||
pre-commit run --all-files
|
||||
```
|
||||
|
||||
@@ -287,14 +287,14 @@ void vApplicationMallocFailedHook(void)
|
||||
& '.venv\Scripts\python.exe' scripts/stack_report.py
|
||||
|
||||
# 推荐用法(静态 + 实测一起判,判定取两者更坏者;报告归档到 data/)
|
||||
& '.venv\Scripts\python.exe' scripts/stack_report.py -s tools\serial_tool\state\serial.log `
|
||||
& '.venv\Scripts\python.exe' scripts/stack_report.py -s .dsh\skills\serial-assistant\scripts\state\serial.log `
|
||||
-o 'docs\内存管理\data\stack_report_<日期>.md'
|
||||
```
|
||||
|
||||
| 参数 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `-i/--html` | `MDK-ARM/STM32F407-Demo/STM32F407-Demo.htm` | 静态调用图(Keil 构建产物) |
|
||||
| `-s/--serial` | 无 | 串口日志,解析其中 `[STACK]` 行(daemon 全量历史:`tools/serial_tool/state/serial.log`) |
|
||||
| `-s/--serial` | 无 | 串口日志,解析其中 `[STACK]` 行(daemon 全量历史:`.dsh/skills/serial-assistant/scripts/state/serial.log`) |
|
||||
| `-o/--output` | 无(打印到 stdout) | 报告输出文件(Markdown) |
|
||||
| `--isr-margin` | `1024` | ISR 嵌套预留(字节) |
|
||||
| `--min-free-pct` | `30` | 要求的最小余量百分比 |
|
||||
@@ -616,7 +616,7 @@ V4 逐项相加须与 V1 的 `Image component sizes` 逐 .o 吻合;不等则
|
||||
|
||||
```powershell
|
||||
& '.venv\Scripts\python.exe' scripts/ram_report.py -o "docs\内存管理\data\ram_report_<日期>.md" # 静态台账
|
||||
& '.venv\Scripts\python.exe' scripts/stack_report.py -s tools\serial_tool\state\serial.log `
|
||||
& '.venv\Scripts\python.exe' scripts/stack_report.py -s .dsh\skills\serial-assistant\scripts\state\serial.log `
|
||||
-o "docs\内存管理\data\stack_report_<日期>.md" # 栈预算(静态 × 实测)
|
||||
& '.venv\Scripts\python.exe' tools/arch_diagram/gen_memory_svg.py # 内存布局/任务栈预算图
|
||||
```
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
- Source: `MDK-ARM\STM32F407-Demo\STM32F407-Demo.htm` (Keil static call graph)
|
||||
- Linker run: Mon Sep 21 04:28:54 2026
|
||||
- Global maximum stack usage: **2776 B** (any call tree)
|
||||
- Runtime watermark: 6 task(s) from `tools\serial_tool\state\serial.log`
|
||||
- Runtime watermark: 6 task(s) from `.dsh\skills\serial-assistant\scripts\state\serial.log`
|
||||
|
||||
Rule: `margin = cfg - max(static_max_depth, runtime_peak)` must be >= max(1024 B ISR, 30% of cfg).
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
- Source: `MDK-ARM\STM32F407-Demo\STM32F407-Demo.htm` (Keil static call graph)
|
||||
- Linker run: Sat Oct 10 10:13:26 2026
|
||||
- Global maximum stack usage: **2776 B** (any call tree)
|
||||
- Runtime watermark: 5 task(s) from `tools\serial_tool\state\serial.log`
|
||||
- Runtime watermark: 5 task(s) from `.dsh\skills\serial-assistant\scripts\state\serial.log`
|
||||
|
||||
Rule: `margin = cfg - max(static_max_depth, runtime_peak)` must be >= max(1024 B ISR, 30% of cfg).
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ LVGL 集合明细(均在 SRAM):LVGL 池 40,960B(`lv_mem_core_builtin.o`
|
||||
任何 scatter / CCM / 堆大小变更后,依次通过以下全部关卡方可认定生效:
|
||||
|
||||
1. `@build`(0 错误 0 警告)
|
||||
2. 产品构建 `@test`(`test/host/run_boot_test.py`):标记校验 PASS,且**零 `[ERR]`**(默认 Forbidden 已含 `[ERR]`)
|
||||
2. 产品构建 `@test`(`.dsh/skills/serial-boot-test/scripts/run_boot_test.py`):标记校验 PASS,且**零 `[ERR]`**(默认 Forbidden 已含 `[ERR]`)
|
||||
3. 存储套件(`test_config.h` → `TEST_SUITE_STORAGE`):只读模式实测 **3/3 PASSED**(破坏性关,卷内容不变);开启 `ENABLE_DESTRUCTIVE_TESTS` 后为全量集成用例(2026-09-20 实测 **20/20 PASSED**,含 TC-STO-01/02/03/05/06)
|
||||
4. GD5F 套件(`TEST_SUITE_GD5F`):只读模式实测 **16/16 PASSED**(含只读的守卫负路径 TC-GD5F-704);开启 `ENABLE_DESTRUCTIVE_TESTS` 后 **68/68 PASSED**,并确认日志出现 2 条守卫负路径 `[ERR]`(TC-GD5F-704/705)——破坏性模式运行须放宽 Forbidden(`--forbidden "FAIL;HardFault"`)
|
||||
5. 恢复跑(`ENABLE_STORAGE_RECOVERY_TESTS`):期望 **3/3 PASSED**
|
||||
@@ -140,7 +140,7 @@ LVGL 集合明细(均在 SRAM):LVGL 池 40,960B(`lv_mem_core_builtin.o`
|
||||
| DMA 阈值 1024B | `Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h`(`GD5F_DMA_THRESHOLD`,公开供测试对齐上下界) |
|
||||
| DMA 可达性运行时守卫 | `Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c`(`gd5f_dma_buf_ok`,`gd5f_read_from_cache`/`gd5f_program_load` 两处接入) |
|
||||
| 守卫负路径用例(读/写) | `test/board/gd5f_test_task.c`(TC-GD5F-704/705) |
|
||||
| 错误级日志纳入自动闭环 | `test/host/verify_log.py` + `test/host/run_boot_test.py`(默认 Forbidden 含 `[ERR]`,`[FAIL]` 仍拦截断言失败) |
|
||||
| 错误级日志纳入自动闭环 | `.dsh/skills/serial-boot-test/scripts/verify_log.py` + `.dsh/skills/serial-boot-test/scripts/run_boot_test.py`(默认 Forbidden 含 `[ERR]`,`[FAIL]` 仍拦截断言失败) |
|
||||
| LVGL 越界保护带 32B | `Drivers/BSP/UI/ui_port.c`(`UI_DRAW_BUF_GUARD`) |
|
||||
| 栈溢出/分配失败钩子 | `Inc/FreeRTOSConfig.h` + `Src/freertos.c`(`vApplicationStackOverflowHook` / `vApplicationMallocFailedHook`) |
|
||||
| HardFault 现场上报 | `Src/stm32f4xx_it.c`(`HardFault_Handler` → `hard_fault_report`) |
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
# ADC 采样节拍设计说明(1 kHz 硬件定时器触发)
|
||||
|
||||
> **状态**:**规划中(未实现)**——B1 遗留项。本文件给出 **CubeMX 配置规格**与**代码改造清单**,
|
||||
> 供实施时按序执行;实现并台架验收后按 §7 判定,并把本行状态改为"已实现"。
|
||||
> **上位需求**:`docs/算法/继电保护功能复现规划.md` §3.2 L1-1(周期 CONVST 重触发 1000 Hz,
|
||||
> 与保护节拍同源)。**驱动契约**:`Drivers/BSP/TPAFE5160/README.md`。
|
||||
> **背景**:B1(2026-10-10)已把 `adc_task` 由空桩改为真实消费者,但采样节拍是
|
||||
> **任务周期软触发**(`osDelayUntil`)——CONVST 边沿 = 任务被调度到的时刻,抖动大、
|
||||
> 且与 FreeRTOS tick 同源。本项把节拍源换成**硬件定时器**。
|
||||
|
||||
---
|
||||
|
||||
## 1. 目标与现状
|
||||
|
||||
| 项 | 现状(B1 软触发) | 目标(硬件定时器) |
|
||||
|---|---|---|
|
||||
| CONVST 边沿时刻 | 任务被调度时刻(抖动 = 调度延迟,可达数十 µs~ms) | TIM 更新事件 + ISR 入口延迟(亚 µs~数 µs) |
|
||||
| 周期精度 | 依赖 FreeRTOS tick(TIM7) | 定时器硬件分频,**晶振级** |
|
||||
| 丢拍 | 任务被长时抢占会漏拍 | 硬件自动重载,不漏拍 |
|
||||
| 等周波调频 | 需改 tick 周期(不可行) | 运行期写 ARR 即可(§8) |
|
||||
|
||||
**判据(L1-1)**:周期 CONVST 重触发 1000 Hz;示波器见 U3 `BUSY` 连续脉冲;
|
||||
`tpafe5160_frame_seq()` 持续递增(每秒 +1000)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 方案选型
|
||||
|
||||
### 2.1 方案 A(**推荐**):TIM6 更新中断 → ISR 内脉冲 CONVST
|
||||
|
||||
- TIM6 配成 1 kHz 更新中断;ISR 内调 `tpafe5160_start_conv_irq()`(现成的 GPIO 脉冲)。
|
||||
- **零引脚改动**:PC9 保持 `GPIO_Output`,驱动与 `tpafe5160_init()` 首帧自检**完全不动**。
|
||||
- BUSY(PC7)EXTI 读帧路径**完全不动**。
|
||||
- 周期精度 = 定时器硬件(晶振级);CONVST 边沿抖动 = TIM6 ISR 入口延迟(见 §5)。
|
||||
- 运行期改 ARR 即改变采样率 → 天然支持等周波调频。
|
||||
|
||||
### 2.2 方案 B(备选,暂不采用):TIM3_CH4/TIM8_CH4 硬件 PWM 直驱 PC9
|
||||
|
||||
- PC9 复用为定时器通道,硬件直接产生 CONVST 方波,**零软件抖动**。
|
||||
- **代价**:① 需把 PC9 由 `GPIO_Output` 改为 AF(`.ioc` 引脚改动);
|
||||
② 驱动的 `TPAFE5160_CONVST_*` 宏(GPIO 写)全部失效 → `tpafe5160_init()` 的
|
||||
"确保 CONVST 低"与首帧自检(E-44)都要重写;③ 上电初值/空闲电平语义变化。
|
||||
- **结论**:收益(把抖动从"数 µs"降到"0")不足以抵消改动面与回归风险。
|
||||
**仅当 §5 实测抖动超过算法容限时再考虑**。
|
||||
|
||||
> 下文只展开**方案 A**。
|
||||
|
||||
---
|
||||
|
||||
## 3. CubeMX 配置规格(照此配置)
|
||||
|
||||
**前提**:SYSCLK = 168 MHz / HCLK = 168 MHz / APB1 = 42 MHz(÷4)→ **APB1 定时器时钟 = 84 MHz**;
|
||||
HAL 时基 = TIM7(已占用);**TIM1~TIM6、TIM8~TIM14 当前全部空闲**。
|
||||
|
||||
| 步骤 | 位置 | 值 | 说明 |
|
||||
|---|---|---|---|
|
||||
| 1 | Timers → **TIM6** → *Activated* | ✅ 勾选 | 基础定时器,**无输出引脚**(不占用任何 IO) |
|
||||
| 2 | Prescaler (PSC) | **83** | 84 MHz ÷ (83+1) = **1 MHz** 计数时钟(1 µs 分辨率) |
|
||||
| 3 | Counter Period (ARR) | **999** | 1 MHz ÷ (999+1) = **1.000 kHz**(= 1000 µs 周期) |
|
||||
| 4 | Counter Mode | Up | 向上计数 |
|
||||
| 5 | **auto-reload preload** | **Enable** | 运行期写 ARR 在下一个更新事件生效,不产生毛刺(§8 等周波调频要用) |
|
||||
| 6 | Clock Division | No Division | 与采样无关 |
|
||||
| 7 | Trigger Event Selection | Reset(默认) | 不用 TRGO |
|
||||
| 8 | NVIC Settings → **TIM6 global interrupt** | ✅ 勾选;**Preemption Priority = 6** | 与 BUSY EXTI(`EXTI9_5_IRQn` = 6)**同级**:两者互不抢占,避免"读 RD 时序被 CONVST 脉冲打断"(§5);且 6 ≥ `configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY`(=5),将来 ISR 里发任务通知也安全 |
|
||||
|
||||
**生成后**:CubeMX 会新建 `Src/tim.c` + `Inc/tim.h`(含 `htim6` 与 `MX_TIM6_Init()`)、
|
||||
在 `Src/stm32f4xx_it.c` 生成 `TIM6_IRQHandler()`、在 `Src/main.c` 的 `main()` 里插入 `MX_TIM6_Init()`。
|
||||
⚠ **`MX_TIM6_Init()` 只做配置,不启动计数**——启动由用户代码负责(§4 第 4 条)。
|
||||
|
||||
> **为何不用 TIM7 那种"1 MHz 计数 + ARR=999"以外的心跳**:本配置与 HAL 时基 TIM7
|
||||
> 的构造完全一致(同为 84 MHz / 1 MHz / 1000 计数),便于对照排查;PSC 取 83 使 ARR 与
|
||||
> "微秒"一一对应,等周波调频时可直接 `ARR = 周期µs - 1`。
|
||||
|
||||
---
|
||||
|
||||
## 4. 代码改造清单
|
||||
|
||||
> 约定:**生成区不改**(CubeMX 重生成会覆盖);所有手工代码落 `USER CODE` 保护区(L2)。
|
||||
|
||||
### 4.1 `Src/main.c`(CubeMX 生成;改 `USER CODE` 区)
|
||||
|
||||
- `USER CODE BEGIN Includes` 增加 `#include "tpafe5160.h"`。
|
||||
- `HAL_TIM_PeriodElapsedCallback()`(**注意:该回调被 TIM7 时基共用**)增加 TIM6 分支:
|
||||
|
||||
```c
|
||||
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
|
||||
{
|
||||
if (htim->Instance == TIM7) { HAL_IncTick(); } /* 既有:HAL 时基 */
|
||||
/* USER CODE BEGIN Callback 0 / 1 —— 加在既有 TIM7 分支之后 */
|
||||
if (htim->Instance == TIM6)
|
||||
{
|
||||
tpafe5160_start_conv_irq(); /* 1 kHz 采样节拍:ISR 内脉冲 CONVST(仅 GPIO 写,亚 µs) */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
⚠ **不得**破坏既有 `HAL_IncTick()` / `task_wd_tick_probe()` 两条 TIM7 分支。
|
||||
|
||||
### 4.2 `Src/tim.c` / `Inc/tim.h` / `Src/stm32f4xx_it.c`
|
||||
|
||||
- **CubeMX 自动生成,勿手改**(`TIM6_IRQHandler` → `HAL_TIM_IRQHandler(&htim6)`)。
|
||||
|
||||
### 4.3 `App/app/app_main.c`
|
||||
|
||||
- 增加 `#include "tim.h"`。
|
||||
- 第 213 行的**单次** `tpafe5160_start_conv_irq();` 替换为**启动周期触发**:
|
||||
|
||||
```c
|
||||
/* 6) 启动 1 kHz 采样节拍(TIM6 更新中断 → ISR 内触发 CONVST) */
|
||||
(void)HAL_TIM_Base_Start_IT(&htim6);
|
||||
```
|
||||
|
||||
- ⚠ **必须在 `tpafe5160_init()` 成功之后**再启动 TIM6,否则 ISR 会在驱动自检
|
||||
(首帧有效性探针,E-44)期间乱发 CONVST。
|
||||
|
||||
### 4.4 `App/task/adc_task.c`
|
||||
|
||||
- **删除**主循环里的 `tpafe5160_start_conv_irq();`(触发已交给 TIM6)。
|
||||
- 本任务退化为**纯消费者**:`tpafe5160_snapshot()` + 帧统计 + 10 s 心跳 + 看门狗心跳。
|
||||
1 ms 节拍(`osDelayUntil`)保留,仅用于"消费节奏"。
|
||||
- 更新文件头与函数注释(说明节拍由 TIM6 提供,本任务不再触发)。
|
||||
- **看门狗不变**:`TASK_WD_ADC` 心跳仍在循环内每拍调用。
|
||||
|
||||
### 4.5 文档同步
|
||||
|
||||
- `docs/算法/继电保护功能复现规划.md` §3.2 L1-1:把"🔶 软触发;硬件定时器为后续"改为 ✅。
|
||||
- `docs/待处理内容.md` §2.1:本项(B4)销项。
|
||||
- `AGENTS.md` 架构要点 `adcTask` 条:删"本阶段为任务周期软触发"的注。
|
||||
- `Drivers/BSP/TPAFE5160/README.md`:补一句"运行期周期触发由 TIM6 更新中断提供"。
|
||||
|
||||
---
|
||||
|
||||
## 5. 时序与优先级分析
|
||||
|
||||
**单周期时序(1 kHz,周期 1000 µs)**:
|
||||
|
||||
```
|
||||
t=0 TIM6 更新事件 → ISR → CONVST 脉冲(LOW→HIGH 上升沿)
|
||||
t≈0+ε TPAFE5160 开始转换(BUSY 拉高)
|
||||
t≈23 µs BUSY 拉低 → PC7 EXTI(优先级 6) → 读 8 通道(≈0.5 µs)→ seq++
|
||||
t=1000 µs 下一个 TIM6 更新事件
|
||||
```
|
||||
|
||||
- **读窗口与下一次 CONVST 相隔 ~977 µs**(转换仅 ~23 µs),互不重叠。
|
||||
- **TIM6 与 BUSY EXTI 同级(6)→ 互不抢占**:读 RD 时序(8 次 RD 翻转)不会被打断,
|
||||
避免"读到一半被新转换刷新数据总线"。这是选 6 而非 5 的原因。
|
||||
- **触发抖动**:TIM6 是最高优先级之一(5 为 USART,7 为 SPI/DMA);被更高优先级 ISR
|
||||
(USART,~数 µs)挡住的概率与时长都有限 → 最坏抖动约数 µs,占 1000 µs 的 <0.5%。
|
||||
- **ISR 时长**:`tpafe5160_start_conv_irq()` 只做两次 `HAL_GPIO_WritePin`(寄存器写)
|
||||
→ 亚 µs,不拖累其它中断。
|
||||
|
||||
---
|
||||
|
||||
## 6. 风险与注意事项
|
||||
|
||||
1. **共用回调**:`HAL_TIM_PeriodElapsedCallback` 同时服务 TIM7(HAL 时基)与 TIM6 →
|
||||
分支必须并列、不得互相干扰;改动后必须确认开机日志时间戳正常(时基未坏)。
|
||||
2. **启动顺序**:TIM6 必须在 `tpafe5160_init()` 之后启动(§4.3)。
|
||||
3. **不新增任务/栈**:本项**零新任务、零新栈、零动态分配** → 不动 `stack_report.py` 预算。
|
||||
4. **CubeMX 生存**:新增生成文件 `Src/tim.c` / `Inc/tim.h`;改后跑一次
|
||||
`python scripts/check_cubemx_survival.py`,若有新增生成文件被登记,按提示重建基线。
|
||||
5. **Keil 工程**:从 CubeMX 重生成工程(含 MDK-ARM)以把 `tim.c` 纳入编译;随后
|
||||
`@build` 必须 0 错 0 警。
|
||||
6. **`mem-numbers`**:本项改 Flash/RAM 极小但非零 → 若 `check_mem_numbers.py` FAIL,
|
||||
跑 `python scripts/check_mem_numbers.py --write` 刷新快照。
|
||||
7. **消费端与生产端同频**:TIM6(84 MHz 派生)与 FreeRTOS tick TIM7(同源)都恰好 1 kHz,
|
||||
相位固定 → 消费端每拍恰好取到 1 帧。**一旦进入等周波调频(采样率≠1 kHz),
|
||||
1 ms 轮询会与生产端失配 → 消费端须改为"帧就绪事件驱动"**(见 §8)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 验收判据
|
||||
|
||||
| # | 判据 | 手段 |
|
||||
|---|---|---|
|
||||
| 1 | `@build` 0 错 0 警,且镜像无源文件新于 hex | `MDK-ARM/build.bat` + `find -newermt` |
|
||||
| 2 | PC9 `CONVST` 周期 = 1.000 kHz(±0.1%) | 示波器 / 逻辑分析仪 |
|
||||
| 3 | PC7 `BUSY` 每周期一个脉冲,与 CONVST 相位固定 | 示波器双通道 |
|
||||
| 4 | `frame_seq()` 每秒 +1000(每 10 s +10000),严格递增 | `@test` 串口 `[ADC_TASK] frames=... seq=...` |
|
||||
| 5 | `frst_err=0`、`idle` 基本为 0 | 同上 |
|
||||
| 6 | 开机日志时间戳正常(TIM7 时基未坏) | 串口日志 |
|
||||
| 7 | 无 HardFault / `[ERR]` / 看门狗复位 | `@test` Forbidden 判定 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 后续:等周波调频(不在本项范围)
|
||||
|
||||
参考系统按 **20 点/周波**采样:电网频率漂移时需让采样率跟随(50.0 Hz → 1000 Hz;
|
||||
49.5 Hz → 990 Hz),以保持傅氏全波窗对齐一个工频周期。
|
||||
|
||||
- 本配置已为此预留:**1 µs 分辨率** + **ARR 预装载使能** → 运行期
|
||||
`__HAL_TIM_SET_AUTORELOAD(&htim6, 周期µs - 1)` 即可无毛刺改频。
|
||||
- 需要配套:① 测频源(过零/DFT 相位);② 把当前采样率同步给保护算法(相量/定值);
|
||||
③ **消费端改事件驱动**(§6.7)——否则 1 ms 轮询在非 1 kHz 采样率下会丢帧。
|
||||
|
||||
---
|
||||
|
||||
## 9. 变更记录
|
||||
|
||||
| 日期 | 变更 | 说明 |
|
||||
|---|---|---|
|
||||
| 2026-10-10 | 建立本文档 | B1(ADC 采集链路)遗留项"精确 1 kHz 硬件定时器触发"的规划:方案选型(A/B)、CubeMX 配置规格、代码改造清单、时序与优先级分析、风险与验收判据、等周波调频预留 |
|
||||
@@ -572,4 +572,4 @@ python scripts/check_cubemx_survival.py --write # 确认改动无误后重建
|
||||
| 2026-10-09 | **4 个 Fault Handler 由 L3 升级为 L2:重生成完全零 diff** | 用与 iwdg 同一招。`Src/stm32f4xx_it.c` 的 4 个 `__asm void X_Handler` 从**生成区**搬进 `USER CODE BEGIN 0`,且必须写在文件头那 4 个 `#define X X_Gen` **之前**(写在之后会被自己的宏改名);配套的 `#undef X` 随之删除(不再需要)。生成区恢复 CubeMX 原样(`void HardFault_Handler(void)` + `HardFault_IRQn 0` / `W1_HardFault_IRQn 0` 标记区),4 个锚点注释(`HARD-FAULT-ANCHOR` / `MEMFAULT-ANCHOR` / `BUSFAULT-ANCHOR` / `USAGEFAULT-ANCHOR`)全部退役。`.map` 复核:`startup_stm32f407xx.o(RESET) refers to stm32f4xx_it.o(.emb_text) for HardFault_Handler`(向量表仍指向我们的 `__asm` 版)、`.emb_text → i.fault_entry_* → i.hard_fault_report`、4 个 `*_Gen` 各 `Removing ... (2 bytes)`。审计项 `hardfault-handler` / `fault-handlers-3` 由 `wholefile`+锚点改为 `protected`(区 `0`)+ 代码符号断言。**L3 清单清空**,重生成预期零 diff。`@build` 0 错误 0 警告;审计 21 通过 / 1 警告 / 0 失败 |
|
||||
| 2026-10-10 | **正文按当前代码重写(清理立项前的描述)** | 正文原样保留了大量**立项前(2026-09-29)**的状态取证与已废弃的"路径 C 考古",与现状矛盾,故重写为现状设计说明(1144 → 571 行;**历史与逐次决策全部保留在下方变更记录,未删**)。主要更正:① §1.1"现状:无任何硬件看门狗"改为**历史背景**(IWDG 已在 `.ioc`/`hal_conf`/工程三处就位);② **ftpTask 早已纳入监视**——经 `lftpd_task_wd_beat()` 弱钩子(事件循环 / RETR·STOR 分片 / 连接与卷锁等待),旧正文"首期不埋点、由掩码剔除"及 R3/O1 已作废;③ 补齐正文缺失的三块现状实现:**FTL 心跳路由**(`task_wd_bind()` + `nand_ftl_wd_beat()`)、**FTL 直接喂狗**(`nand_ftl_wd_feed()`,含"设备存活但功能冻结"边界)、**round 7/8/8b 取证机制**(`/log/wdev.log`、`.noinit` 槽 + `UNINIT_CCM`、`PREVRUN`/`TICKRUN`/`MISS start`/`STALL`);④ 掩码改为按代码实况列表(产品 `0x37` / `ENABLE_HW_LAYER_TESTS` `0x35` / **testTask 任何构建都不入掩码**)并写明"精确相等"语义;⑤ 复位原因优先级按 `reset_cause_str()` 实际顺序改写(旧表把 POR 排第一,代码是 **IWDG 最优先**);⑥ §4.6 更正为**已集成**:看门狗复位写 `selftest.log`(旧正文"未改");⑦ §8 预算改用 `.map` 实测(`task_watchdog.o` 1098 B 代码 / 44+104 B RAM / `.noinit` 48 B;`iwdg.o` 仅 28 B,`HAL_IWDG_Init` 94 B 被移除);⑧ 附录 B 自相矛盾修正(旧表把"借助 CubeMX"列为否决,实际已选它);⑨ §7 由"验证计划"改为**验证记录**(V2/V4/V5 ✅、U1 39 断言 ✅,V3/V6/V7 标为未单独注入、V8 未做)。连带同步:`Src/iwdg.c`、`Src/freertos.c`、`App/task/default_task.c` 三处注释的章节引用(旧 §4.5/§4.6 → 新 §4.1/§4.4),`scripts/check_cubemx_survival.py` 提示语(§10.1 → §10.2/§10.3),`实机测试实施指南.md`(§10 → §7 ×3)、`看门狗单元测试说明.md`(§10 → §7)、`模拟器差距清单.md` H-1(§4.9 → §4.7,并更正"模拟器不编译本模块、无 IWDG 桩")、`docs/README.md`("实施中/待验证" → 已闭环)。⚠️ **下方历史行里的 §x.y 引用指当时编号**,不再回溯改写 |
|
||||
| 2026-10-10 | **adcTask 纳入监视(B1):必需掩码 `0x37`→`0x77`** | B1(ADC 采集链路)落地时新增 `adcTask`(`App/task/adc_task.c`:1 ms 采样节拍触发 CONVST + 消费 `tpafe5160_snapshot()`)并纳入 IWDG 监视:`task_wd_id_t` 新增 `TASK_WD_ADC = 6`;`wd_required_mask()` 产品 `0x37`→`0x77`、`ENABLE_HW_LAYER_TESTS` `0x35`→`0x75`(adcTask **不依赖任何就绪屏障** → 任何构建都置位)。adcTask 主循环每 1 ms `task_wd_beat(TASK_WD_ADC)`;**不做 NAND I/O → 不登记 FTL 心跳路由**(无需 `task_wd_bind()`)。主机侧单测 `beat_all()` 与掩码断言(`tc_003`/`tc_005`/`tc_011` 的 `0x37`→`0x77`)同步,39 断言全绿。任务由 `Src/freertos.c` 的 `USER CODE` 区手工创建(同 timeSyncTask,不在 `.ioc` 里),栈 512 words、优先级 Normal,`stack_report.py` 实测最坏链 504 B(余 1544 B / 75%)。见 `docs/算法/继电保护功能复现规划.md` §7 |
|
||||
| 2026-10-10 | **B1 `@test` 实机闭环:adcTask 掩码真机验证** | 台架闭环(`test/host/run_boot_test.py`)确认 `adcTask` 纳入掩码后**无 adc 相关误复位**:`[STACK] adc peak_used=496/2048 free_min=1552 (75%)`、`frames/seq` 每 10 s 严格 +10000(=1000 Hz)、`idle=0`/`frst_err=0`、全片回读 `No difference found`。启动早期两次 `MISS start snap=0x67 req=0x77` 仅缺 bit4(`timeSync` 未启动,属正常就绪窗;adc 的 bit6 自始置位),掩码随后收敛、全程无复位。同轮另修 `dbg_console_writer_ready()` 门控(adcTask 周期日志在写者注册前抑制,见 `控制台单写者日志设计说明.md`) |
|
||||
| 2026-10-10 | **B1 `@test` 实机闭环:adcTask 掩码真机验证** | 台架闭环(`.dsh/skills/serial-boot-test/scripts/run_boot_test.py`)确认 `adcTask` 纳入掩码后**无 adc 相关误复位**:`[STACK] adc peak_used=496/2048 free_min=1552 (75%)`、`frames/seq` 每 10 s 严格 +10000(=1000 Hz)、`idle=0`/`frst_err=0`、全片回读 `No difference found`。启动早期两次 `MISS start snap=0x67 req=0x77` 仅缺 bit4(`timeSync` 未启动,属正常就绪窗;adc 的 bit6 自始置位),掩码随后收敛、全程无复位。同轮另修 `dbg_console_writer_ready()` 门控(adcTask 周期日志在写者注册前抑制,见 `控制台单写者日志设计说明.md`) |
|
||||
|
||||
@@ -166,19 +166,19 @@ ST-LINK_CLI -c SWD HOTPLUG -Dump 20000000 1000000 dump.bin :: 转储 SRAM
|
||||
**A. 只重启不烧录**(固件没变,重新验证启动/串口行为;配合 serial_tool)
|
||||
|
||||
```powershell
|
||||
& '.venv\Scripts\python.exe' tools/serial_tool/serial_tool.py open
|
||||
& '.venv\Scripts\python.exe' tools/serial_tool/serial_tool.py clear
|
||||
& '.venv\Scripts\python.exe' .dsh/skills/serial-assistant/scripts/serial_tool.py open
|
||||
& '.venv\Scripts\python.exe' .dsh/skills/serial-assistant/scripts/serial_tool.py clear
|
||||
& "MDK-ARM\reset.bat" # 等价于 ST-LINK_CLI -c SWD -Rst -Run -Q
|
||||
# 等待开机完成后直接读日志文件校验标记:
|
||||
Get-Content tools\serial_tool\state\serial.log -Tail 100
|
||||
Get-Content .dsh\skills\serial-assistant\scripts\state\serial.log -Tail 100
|
||||
```
|
||||
|
||||
顺序关键:**先开串口清日志,再复位**,否则错过开机头几行(与 `run_boot_test.py` 的"采集先于烧录"原则一致)。
|
||||
ⓞ 实测补充三条:
|
||||
|
||||
1. **改过 `serial_config.json` 端口后必须重启 daemon**:daemon 在启动时缓存配置,改端口后 `close` + 杀旧进程再 `open`,否则一直按旧端口报 `port not found`;
|
||||
1. **改过 `.serialtest/config.json` 端口后必须重启 daemon**:daemon 在启动时缓存配置,改端口后 `close` + 杀旧进程再 `open`,否则一直按旧端口报 `port not found`;
|
||||
2. **(已修,2026-10-10)旧版 `wait-idle` 在复位突发期会报 `daemon unresponsive after auto-start: timed out`**:CLI 把**超时**误判成"daemon 不可达",顺手拉起一个绑不上 HTTP 口的实例再把命令撞一次超时(数据其实已落盘)。现 CLI 超时按 `--max-ms` 自动推导、超时只快速失败不再误拉起实例;
|
||||
3. **日志校验**:`tools/serial_tool/state/serial.log` 用 Read/Grep 直接查最直观;`serial_tool.py log --grep <标记>` 现已可作判定依据(2026-10-10 修:旧实现先 `--tail`(默认 50)后 grep,开机标记必然查不到——这就是当年"实测 0 命中、语义存疑"的真根因)。
|
||||
3. **日志校验**:`.dsh/skills/serial-assistant/scripts/state/serial.log` 用 Read/Grep 直接查最直观;`serial_tool.py log --grep <标记>` 现已可作判定依据(2026-10-10 修:旧实现先 `--tail`(默认 50)后 grep,开机标记必然查不到——这就是当年"实测 0 命中、语义存疑"的真根因)。
|
||||
|
||||
**B. 完整烧写 + 运行**:`MDK-ARM\reset.bat flash [xxx.hex]`(= `-c SWD -P <hex> -Rst -Run`;09/19 实测 1MB ≈ 6.5s,09/20 复测 6.47s + 全片 `-CmpFile` 逐字节一致)。正式流程仍以 `download.bat`(`UV4 -f`)为准,CLI 路径作为脚本化/快速路径。
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# 启动日志(boot_log)存档
|
||||
|
||||
本目录保存 `@test`(`test/host/run_boot_test.py`)产出的**开机串口日志快照**,
|
||||
本目录保存 `@test`(`.dsh/skills/serial-boot-test/scripts/run_boot_test.py`)产出的**开机串口日志快照**,
|
||||
用于**跨构建的栈 / 堆水位对照**(解析其中的 `[STACK]` 行)。
|
||||
|
||||
## 1. 为什么需要存档
|
||||
|
||||
- `@test` 默认把 boot_log 写到临时目录 `%TEMP%\opencode\boot_log_<时间戳>.txt`——**会被系统清理**;
|
||||
- `@test` 默认把 boot_log 写到临时目录 `%TEMP%\serial-boot-test\boot_log_<时间戳>.txt`——**会被系统清理**;
|
||||
- 台架长跑的 20MB 串口日志**截头会丢掉早期 `[STACK]` 水位行**(开机首次约 5s 后才报告);
|
||||
- 两者叠加导致**无法事后回溯**"改代码前后"的栈水位变化。
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
| 项 | 规则 |
|
||||
|---|---|
|
||||
| 来源 | `& '.venv\Scripts\python.exe' test/host/run_boot_test.py`(默认 `--out-dir %TEMP%\opencode`,产出 `boot_log_<stamp>.txt` + `selftest_log_<stamp>.log`) |
|
||||
| 来源 | `& '.venv\Scripts\python.exe' .dsh/skills/serial-boot-test/scripts/run_boot_test.py`(默认 `--out-dir %TEMP%\serial-boot-test`,产出 `boot_log_<stamp>.txt` + `selftest_log_<stamp>.log`) |
|
||||
| 命名 | `<日期>_<构建标识>_boot_log.txt`,例:`20261005_prod_none_boot_log.txt` |
|
||||
| 构建标识 | 建议含:产品 / 测试套件(`prod_none` / `suite_ch395f` …)+ 负载(`idle` / `v5_30min` / `ftp_stress`) |
|
||||
| 随文件登记 | 在下方 §3 清单登记日期、构建、负载、关键 `[STACK]` 水位(如 defaultTask 峰值) |
|
||||
|
||||
@@ -20,13 +20,13 @@
|
||||
|
||||
```powershell
|
||||
& "MDK-ARM\build.bat"
|
||||
& '.venv\Scripts\python.exe' tools\serial_tool\serial_tool.py clear
|
||||
& '.venv\Scripts\python.exe' .dsh\skills\serial-assistant\scripts\serial_tool.py clear
|
||||
& '.venv\Scripts\python.exe' test\latency_peer.py --host 192.168.1.100 --port 8080 `
|
||||
--json "docs\测试说明\m2_data\m2_quiet_runNN.json" --wait 170 # 后台作业
|
||||
& "MDK-ARM\download.bat"
|
||||
& '.venv\Scripts\python.exe' tools\serial_tool\serial_tool.py wait-idle `
|
||||
& '.venv\Scripts\python.exe' .dsh\skills\serial-assistant\scripts\serial_tool.py wait-idle `
|
||||
--timeout 120 --settle-ms 10000 --max-ms 110000
|
||||
# 判定:Grep tools\serial_tool\state\serial.log
|
||||
# 判定:Grep .dsh\skills\serial-assistant\scripts\state\serial.log
|
||||
```
|
||||
|
||||
判定标记:Required `App init OK` / `Network Init` / `net init: OK` / `[LAT] report complete` /
|
||||
|
||||
+16
-16
@@ -2,7 +2,7 @@
|
||||
|
||||
> **定位**:面向使用者的操作手册——命令怎么敲、输出怎么看、常见场景怎么组合、出问题怎么排查。
|
||||
> **关联文档**:设计决策与实现细节见 `串口助手设计说明.md`;@test 自动化闭环见 `自动化测试流程.md`;`AGENTS.md`「串口调试」节有一页速查。
|
||||
> **版本**:v1.1(与 `tools/serial_tool/serial_tool.py` 当前实现一致,2026-09-15)
|
||||
> **版本**:v1.1(与 `.dsh/skills/serial-assistant/scripts/serial_tool.py` 当前实现一致,2026-09-15)
|
||||
|
||||
## 1. 它是什么(60 秒版)
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
|---|---|---|
|
||||
| **常驻 daemon** | 后台 Python 进程,**串口唯一持有者**,把 RX/TX 持续写进日志文件 | 不用管它——任何命令发现它没在跑会**自动拉起** |
|
||||
| **薄 CLI** | `serial_tool.py <命令>`,每条命令都是带 5s 硬超时的短操作 | 你唯一直接打交道的界面 |
|
||||
| **日志文件** | `tools/serial_tool/state/serial.log`,**全量历史就在这里** | 想回看任何时刻的数据 → 直接读这个文件 |
|
||||
| **日志文件** | `.dsh/skills/serial-assistant/scripts/state/serial.log`,**全量历史就在这里** | 想回看任何时刻的数据 → 直接读这个文件 |
|
||||
|
||||
**统一入口**(仓库根目录执行,任意 cwd 均可):
|
||||
|
||||
```powershell
|
||||
& '.venv\Scripts\python.exe' 'tools/serial_tool/serial_tool.py' <命令> [参数]
|
||||
& '.venv\Scripts\python.exe' '.dsh/skills/serial-assistant/scripts/serial_tool.py' <命令> [参数]
|
||||
```
|
||||
|
||||
两条心法:
|
||||
@@ -28,19 +28,19 @@
|
||||
|
||||
```powershell
|
||||
# ① 看本机有哪些 COM 口(不依赖 daemon;configured = 配置目标口)
|
||||
& '.venv\Scripts\python.exe' 'tools/serial_tool/serial_tool.py' ports
|
||||
& '.venv\Scripts\python.exe' '.dsh/skills/serial-assistant/scripts/serial_tool.py' ports
|
||||
|
||||
# ② 开串口(自动拉起 daemon;端口取 .serialtest/config.json,也可显式指定)
|
||||
& '.venv\Scripts\python.exe' 'tools/serial_tool/serial_tool.py' open
|
||||
& '.venv\Scripts\python.exe' '.dsh/skills/serial-assistant/scripts/serial_tool.py' open
|
||||
|
||||
# ③ 发一条数据(文本;--hex 发字节;--nl 末尾加 CRLF)
|
||||
& '.venv\Scripts\python.exe' 'tools/serial_tool/serial_tool.py' send "hello" --nl
|
||||
& '.venv\Scripts\python.exe' '.dsh/skills/serial-assistant/scripts/serial_tool.py' send "hello" --nl
|
||||
|
||||
# ④ 看最近 20 行日志(含刚发的 TX 和收到的 RX)
|
||||
& '.venv\Scripts\python.exe' 'tools/serial_tool/serial_tool.py' log --tail 20
|
||||
& '.venv\Scripts\python.exe' '.dsh/skills/serial-assistant/scripts/serial_tool.py' log --tail 20
|
||||
|
||||
# ⑤ 不用了:关串口(daemon 继续常驻;要彻底退出用 stop)
|
||||
& '.venv\Scripts\python.exe' 'tools/serial_tool/serial_tool.py' close
|
||||
& '.venv\Scripts\python.exe' '.dsh/skills/serial-assistant/scripts/serial_tool.py' close
|
||||
```
|
||||
|
||||
## 3. 命令速查
|
||||
@@ -163,8 +163,8 @@
|
||||
- 等价的手动读法(Agent 用 Read/Grep 工具、人用 PowerShell 均可):
|
||||
|
||||
```powershell
|
||||
Get-Content 'tools\serial_tool\state\serial.log' -Tail 20
|
||||
Select-String -Path 'tools\serial_tool\state\serial.log' -Pattern 'App init OK'
|
||||
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)
|
||||
@@ -229,9 +229,9 @@ Select-String -Path 'tools\serial_tool\state\serial.log' -Pattern 'App init OK'
|
||||
|
||||
| 文件 | 内容 | 谁读 |
|
||||
|---|---|---|
|
||||
| `tools/serial_tool/state/serial.log` | RX/TX/SYS 全量日志 | 你(Read/Grep/`log` 命令) |
|
||||
| `tools/serial_tool/state/meta.json`(运行时未创建) | daemon pid / 版本 / 当前 gen / 口配置快照 | 工具内部 |
|
||||
| `tools/serial_tool/state/daemon.log` | daemon 自身 stderr(启动行、异常) | 排障时看 |
|
||||
| `.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——纯本机运行时产物。
|
||||
|
||||
@@ -283,7 +283,7 @@ Select-String -Path 'tools\serial_tool\state\serial.log' -Pattern 'App init OK'
|
||||
... serial_tool.py log --tail 0 # 取本轮全量 → 人眼/脚本检查标记
|
||||
```
|
||||
|
||||
自动化版本 = `test/host/run_boot_test.py`(同一序列 + 烧录 + 标记校验,见 `自动化测试流程.md`)。
|
||||
自动化版本 = `.dsh/skills/serial-boot-test/scripts/run_boot_test.py`(同一序列 + 烧录 + 标记校验,见 `自动化测试流程.md`)。
|
||||
|
||||
### 5.3 长时运行监控
|
||||
|
||||
@@ -298,7 +298,7 @@ daemon 常驻,日志持续累积(20MB 自动截头),几天不关也没
|
||||
|
||||
```powershell
|
||||
... serial_tool.py status # ① state/error/counters 一眼看全
|
||||
Get-Content 'tools\serial_tool\state\daemon.log' -Tail 30 # ② daemon 自身输出
|
||||
Get-Content '.dsh\skills\serial-assistant\scripts\state\daemon.log' -Tail 30 # ② daemon 自身输出
|
||||
... serial_tool.py restart # ③ 重建 daemon(顺带加载新代码)
|
||||
```
|
||||
|
||||
@@ -337,7 +337,7 @@ Get-Content 'tools\serial_tool\state\daemon.log' -Tail 30 # ② daemon 自身
|
||||
| `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: …]` | `serial_config.json` 缺失 / JSON 语法错 / `baudRate`·`httpPort` 非整数(现按 `utf-8-sig` 读并**显式回显**错误,不再静默回退成"没配端口") | 按 `config_error` 文本修文件;`status` 与 `ports` 也回显 `config_error` |
|
||||
| `open` 报 `no port specified … [config: …]` | `.serialtest/config.json` 缺失 / JSON 语法错 / `baudRate`·`httpPort` 非整数(现按 `utf-8-sig` 读并**显式回显**错误,不再静默回退成"没配端口") | 按 `config_error` 文本修文件;`status` 与 `ports` 也回显 `config_error` |
|
||||
| 旧日志"丢了" | 20MB 截头丢的是**文件头** | 查旧内容要看尾部;重要轮次开始前先 `clear` 分代 |
|
||||
|
||||
## 8. 常见问题
|
||||
|
||||
@@ -63,8 +63,8 @@ flowchart LR
|
||||
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 无关)
|
||||
- **单文件实现**:`.dsh/skills/serial-assistant/scripts/serial_tool.py`(stdlib + pyserial,约 1000 行);解释器统一 `.venv\Scripts\python.exe`
|
||||
- **Agent 调用式**:`& '.venv\Scripts\python.exe' '.dsh/skills/serial-assistant/scripts/serial_tool.py' <命令> [参数]`(任意 cwd;state 目录按脚本位置解析,与 cwd 无关)
|
||||
- **历史 = 日志文件**:Agent 用 Read/Grep 直接看历史,无需专门 API(`log` 命令只是便捷封装)
|
||||
- **配置沿用** `.serialtest/config.json`(`portName`/`baudRate` 为默认值;新增可选字段 `httpPort`,默认 18765)
|
||||
|
||||
@@ -128,9 +128,9 @@ flowchart LR
|
||||
|
||||
| 文件 | 用途 |
|
||||
|---|---|
|
||||
| `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(排障用) |
|
||||
| `.dsh/skills/serial-assistant/scripts/state/serial.log` | RX/TX/SYS 全量日志(Agent 直接读) |
|
||||
| `.dsh/skills/serial-assistant/scripts/state/meta.json`(运行时未创建) | daemon 元数据(pid / 版本 / 当前 gen) |
|
||||
| `.dsh/skills/serial-assistant/scripts/state/daemon.log` | daemon 自身 stderr(排障用) |
|
||||
|
||||
### 5.2 行格式
|
||||
|
||||
@@ -167,7 +167,7 @@ flowchart LR
|
||||
|
||||
| 项 | 规定 |
|
||||
|---|---|
|
||||
| 单实例 | HTTP 端口(默认 18765,`serial_config.json` 的 `httpPort` 可改)绑定即锁;端口被占 = 已有实例。Windows 上真独占靠 `SO_EXCLUSIVEADDRUSE`(**真名,值 -5**;2026-10-10 修:旧代码写成 `SO_EXCLUSIVEADDRPLACE` + 数值 `0x0040`,`setsockopt` 抛 `WinError 10042` 被静默吞掉,"独占绑定"从未生效),失败现打 WARN,`status.daemon.exclusive_bind` 可验。⚠️ 进程列表里出现**两个** `--serve` 是正常的:`.venv\Scripts\python.exe` 是 venv stub,会派生基解释器(本机 anaconda)子进程跑真 daemon(`status.daemon.pid` = 子进程),不是多实例 |
|
||||
| 单实例 | HTTP 端口(默认 18765,`.serialtest/config.json` 的 `httpPort` 可改)绑定即锁;端口被占 = 已有实例。Windows 上真独占靠 `SO_EXCLUSIVEADDRUSE`(**真名,值 -5**;2026-10-10 修:旧代码写成 `SO_EXCLUSIVEADDRPLACE` + 数值 `0x0040`,`setsockopt` 抛 `WinError 10042` 被静默吞掉,"独占绑定"从未生效),失败现打 WARN,`status.daemon.exclusive_bind` 可验。⚠️ 进程列表里出现**两个** `--serve` 是正常的:`.venv\Scripts\python.exe` 是 venv stub,会派生基解释器(本机 anaconda)子进程跑真 daemon(`status.daemon.pid` = 子进程),不是多实例 |
|
||||
| 拉起方式 | CLI detached 子进程(`CREATE_NO_WINDOW`),解释器 = `sys.executable`(保证同一 .venv) |
|
||||
| `open` 语义 | 幂等(同端口已开 = 成功返回,**不重开**);端口不存在 → ≤1s 报错并附可用端口列表;被占用 → ≤1s 报错(Windows 共享冲突即时返回)并提示持有者 |
|
||||
| DTR/RTS | 默认**不翻转**(`dtr=False, rts=False`,防 STM32 自动复位电路误复位目标板);`--dtr/--rts` 可显式置位 |
|
||||
@@ -201,7 +201,7 @@ flowchart LR
|
||||
| 对象 | 变更 |
|
||||
|---|---|
|
||||
| `requirements.txt` | 增 `pyserial`(**保持纯 ASCII** 注释) |
|
||||
| `.gitignore` | 增 `tools/serial_tool/state/` |
|
||||
| `.gitignore` | 增 `.dsh/skills/serial-assistant/scripts/state/` |
|
||||
| `opencode.json`(仓库根) | **删除 `mcp.serial` 段**(指向缺失二进制,已失效) |
|
||||
| `测试说明/serial-mcp-server_串口调试.md` | **已删除**(内容精华并入本文;工具历史见 §12) |
|
||||
| `AGENTS.md` | "串口调试"节改写为 serial_tool(配置源仍 `serial_config.json`);"自动化测试"节同步新流程 |
|
||||
@@ -238,7 +238,7 @@ flowchart LR
|
||||
| # | 步骤 | 产物 | 验证 |
|
||||
|---|---|---|---|
|
||||
| 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` 手动走通 ✅ |
|
||||
| S2 | daemon 核心:HTTP 服务 + 串口读线程 + 行日志写(含 clear/截头/DTR 策略) | `.dsh/skills/serial-assistant/scripts/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 接入** |
|
||||
@@ -253,7 +253,7 @@ flowchart LR
|
||||
| O1 | 换机器缺 CH340 驱动 → `ports` 无目标口 | `ports` 返回带提示("未找到 CH340,请装驱动");文档列驱动要求 |
|
||||
| O2 | 截头瞬间 Agent 恰在读文件 → 视图错位 | 已知可接受:Agent 读**尾部**;`log` 命令经 daemon 读更稳 |
|
||||
| O3 | daemon 强杀瞬间丢 ≤1 行(内存残行) | 可接受(debug 日志场景);`close` 路径保证冲刷 |
|
||||
| O4 | HTTP 端口 18765 与其他服务冲突 | `serial_config.json` `httpPort` 可改 |
|
||||
| O4 | HTTP 端口 18765 与其他服务冲突 | `.serialtest/config.json` `httpPort` 可改 |
|
||||
| O5 | TX 用途(本工程 USART1 单向输出) | 保留能力,面向未来交互/RS485 调试;v1 仅保证进日志 |
|
||||
| O6 | daemon 常驻内存(Python 进程 ~20MB) | 可接受;`stop` 即释放 |
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
| A5 | 全量静态门禁 | `pre-commit run --all-files` | **17/17 Passed**(2026-10-03 实测;含新增 S5 `test-cppcheck`) |
|
||||
| A6 | ST-LINK 探针可见 | `& "MDK-ARM\reset.bat" list`(CLI 经 `DTU_STLINK_CLI` / PATH 解析) | 列出探针 SN `0B250D12A218303030303032` |
|
||||
| A7 | 串口端口正确 | 检查 `.serialtest/config.json` 的 `portName` | 端口单一配置源 = 此文件(当前 COM6,2026-09-21 台架重插后由 COM7 变更);变更后先杀旧 daemon 再 `open` |
|
||||
| A8 | 串口助手 daemon 可用 | `& '.venv\Scripts\python.exe' tools/serial_tool/serial_tool.py status` | 返回 JSON,`open: true/false` 可读 |
|
||||
| A8 | 串口助手 daemon 可用 | `& '.venv\Scripts\python.exe' .dsh/skills/serial-assistant/scripts/serial_tool.py status` | 返回 JSON,`open: true/false` 可读 |
|
||||
|
||||
> ⚠️ **A4 的陷阱**(2026-10-02 实测):`@build` 是**增量**构建,`run_all_suites.py` 跑完
|
||||
> 套件后 `.map` 会停在**测试套件体积**;仅还原 `test_config.h` 不够,**必须再编一次产品固件**
|
||||
@@ -71,10 +71,10 @@
|
||||
|
||||
```bash
|
||||
# 一键闭环(selftest.log 自动 FTP 抓取;无故障记录 = PASS)
|
||||
& '.venv\Scripts\python.exe' test/host/run_boot_test.py --require-selftest-pass --device-ip 192.168.1.100
|
||||
& '.venv\Scripts\python.exe' .dsh/skills/serial-boot-test/scripts/run_boot_test.py --require-selftest-pass --device-ip 192.168.1.100
|
||||
|
||||
# 或手动联合判定(本轮串口日志 + selftest.log 本地副本)
|
||||
& '.venv\Scripts\python.exe' test/host/verify_log.py \
|
||||
& '.venv\Scripts\python.exe' .dsh/skills/serial-boot-test/scripts/verify_log.py \
|
||||
--log-path <本轮串口日志> \
|
||||
--require-selftest-pass \
|
||||
--selftest-log <下载的 selftest.log 本地副本>
|
||||
@@ -122,7 +122,7 @@
|
||||
```bash
|
||||
# --- V2/V4(故障注入,需临时改码)---
|
||||
# 1) 基线:跑一轮产品 @test 确认无复位
|
||||
& '.venv\Scripts\python.exe' test/host/run_boot_test.py
|
||||
& '.venv\Scripts\python.exe' .dsh/skills/serial-boot-test/scripts/run_boot_test.py
|
||||
|
||||
# 2) 注入:按设计说明 §10 在 netTask 循环插 while(1);(或 taskDISABLE_INTERRUPTS(); while(1);)
|
||||
# —— 改完 @build,然后烧录
|
||||
@@ -382,7 +382,7 @@ CH395F / STORAGE / GD5F,未含 RTC;怀疑 RTC 构建下 `testTask` 不被创
|
||||
`PASV→STOR` 间隔达 **11 s**(数据口 LISTEN 半开)。这是**与看门狗误复位不同的另一类
|
||||
问题**(CH395F 数据通道);若修复后仍复现 425,转 §7.1 专项。
|
||||
4. **串口采集铁律**:`open` → `clear` → 烧录/复位 → `wait-idle --timeout 90 --settle-ms 8000
|
||||
--max-ms 45000` → **直接 Read/Grep `tools/serial_tool/state/serial.log`**
|
||||
--max-ms 45000` → **直接 Read/Grep `.dsh/skills/serial-assistant/scripts/state/serial.log`**
|
||||
(`log --grep` 子命令不可信);daemon 残留 → `taskkill /F /PID <旧>` 后重新 `open`。
|
||||
5. **只用 `.venv\Scripts\python.exe`**(PATH 里的 `python` 是 msys64);每次烧录后**必须**
|
||||
ST-LINK 全片回读 `No difference found`。
|
||||
|
||||
@@ -266,10 +266,10 @@ static void phaseN_run(void) {
|
||||
|
||||
| 工具 | 位置 | 作用 |
|
||||
|---|---|---|
|
||||
| Python 串口助手(serial_tool) | `tools/serial_tool/serial_tool.py`(.venv + pyserial) | 常驻 daemon + 薄 CLI:列端口 / 开关 / 收发 / 全量日志 / wait-idle |
|
||||
| `test/host/run_boot_test.py` | `test/host/` | 一键闭环:采集 → 烧录 → 提取日志 → 校验 |
|
||||
| Python 串口助手(serial_tool) | `.dsh/skills/serial-assistant/scripts/serial_tool.py`(.venv + pyserial) | 常驻 daemon + 薄 CLI:列端口 / 开关 / 收发 / 全量日志 / wait-idle |
|
||||
| `.dsh/skills/serial-boot-test/scripts/run_boot_test.py` | `test/host/` | 一键闭环:采集 → 烧录 → 提取日志 → 校验 |
|
||||
| **`test/run_all_suites.py`** | `test/` | **多套件回归编排**(T-5)+ **`--check-build` 只编译不烧录**(S6,纯离机,验证 4 套件可编译性);见 §3.6 |
|
||||
| `test/host/verify_log.py` | `test/host/` | 日志标记校验(Required / Forbidden → PASS / FAIL)+ 可选套件汇总行校验(T-2) |
|
||||
| `.dsh/skills/serial-boot-test/scripts/verify_log.py` | `test/host/` | 日志标记校验(Required / Forbidden → PASS / FAIL)+ 可选套件汇总行校验(T-2) |
|
||||
| **`test/host/common/unit_test_harness.py`** | `test/host/common/` | **L0.5 通用单测 harness**:工具链探测 / `INCLUDE`-`LIB` 拼接 / 编译运行 / 退出码契约。各模块运行器只做声明式配置(T-3) |
|
||||
| **`test/host/watchdog/run_unit_test.py`** | `test/host/watchdog/` | **L0.5 主机侧单测**:声明 `TestBuild` 后调 harness,穷举看门狗判定状态机(零硬件、秒级)。**需本机装有 VS(`cl.exe`)**,见 §4.2 环境要求 |
|
||||
| **`test/host/sys_clock/run_unit_test.py`** | `test/host/sys_clock/` | **L0.5 主机侧单测**:穷举 `sys_clock` 时间换算 / 星期 / 惰性再基准化 / RTC 故障降级(100 断言,S1-a) |
|
||||
@@ -420,7 +420,7 @@ flowchart TD
|
||||
|
||||
```powershell
|
||||
# 测试套件构建回归(要求套件跑到汇总行,且断言数 > 0)
|
||||
& '.venv\Scripts\python.exe' test/host/run_boot_test.py `
|
||||
& '.venv\Scripts\python.exe' .dsh/skills/serial-boot-test/scripts/run_boot_test.py `
|
||||
--required "=== Storage Tests:" --forbidden "ERROR;HardFault;[ERR]" `
|
||||
--require-suite-summary "Storage Tests"
|
||||
```
|
||||
@@ -476,7 +476,7 @@ flowchart TD
|
||||
| 代码变更影响判定标准 | 立即同步更新模块 Test_Guide,并登记该文档变更记录 |
|
||||
| 有意改视觉 | 刷黄金参考 + 重生成目检 PNG + `UI_自动化测试流程.md` 变更记录登记 |
|
||||
| 新增测试工具 / 新标记 | `自动化测试流程.md` 命令清单登记(§4.2) |
|
||||
| 栈 / 堆预算相关改动或基线重采 | 把 `@test` 的 boot_log 快照(`%TEMP%\opencode\boot_log_*.txt`)存入 `docs/测试说明/boot_log_data/` 并在其 §3 清单登记(防临时目录被清理 / 长跑日志截头丢 `[STACK]` 行) |
|
||||
| 栈 / 堆预算相关改动或基线重采 | 把 `@test` 的 boot_log 快照(`%TEMP%\serial-boot-test\boot_log_*.txt`)存入 `docs/测试说明/boot_log_data/` 并在其 §3 清单登记(防临时目录被清理 / 长跑日志截头丢 `[STACK]` 行) |
|
||||
|
||||
> 文档与代码一体两面:**代码改了文档没同步 = 未完成**。
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
├─ Step 0 预检 .venv + serial_tool(selftest/ports 确认 COM)
|
||||
├─ Step 1 构建验证 MDK-ARM/build.bat (0 错误 0 警告才继续)
|
||||
├─ Step 2 烧录 + 采集(一条命令编排:开串口 + 清日志 → 烧录 → 固件身份戳 → wait-idle → 提取 → 回读校验)
|
||||
│ test/host/run_boot_test.py (内置 serial_tool + download.bat + reset.bat cmp + verify_log.py)
|
||||
│ .dsh/skills/serial-boot-test/scripts/run_boot_test.py (内置 serial_tool + download.bat + reset.bat cmp + verify_log.py)
|
||||
├─ Step 3 判定:
|
||||
│ PASS → 报告,结束
|
||||
│ FAIL → 依据 [MISS]/[HIT] + 日志尾部,修改代码后回到 Step 1
|
||||
@@ -23,10 +23,10 @@
|
||||
| `@build` | `keil-build` skill(`.dsh/skills/keil-build/`,自带 `keil_build.py`) | 仅编译验证(0 错误 0 警告) |
|
||||
| `@download` | `stlink-flash` skill(`.dsh/skills/stlink-flash/`,自带 `stlink_flash.py`) | 编译并烧录(ST-Link) |
|
||||
| `@test` | `serial-boot-test` skill(`.dsh/skills/serial-boot-test/`) | 完整闭环:build → capture+download → verify → iterate |
|
||||
| `test/host/run_boot_test.py` | `test/host/run_boot_test.py` | **一键闭环**:open + clear → 烧录 → 固件身份戳(从 hex 抠 `fw build:`)→ wait-idle(静默 8s/超时 30s/首字节 12s)→ 提取 `[RX]`(每行加 `[+Nms]` 时基)→ 回读校验(`reset.bat cmp`)→ verify;`--keep-out` 清旧产物;退出码 0=PASS/1=FAIL/2=采集·烧录·回读错误/3=用法错误 |
|
||||
| `.dsh/skills/serial-boot-test/scripts/run_boot_test.py` | `.dsh/skills/serial-boot-test/scripts/run_boot_test.py` | **一键闭环**:open + clear → 烧录 → 固件身份戳(从 hex 抠 `fw build:`)→ wait-idle(静默 8s/超时 30s/首字节 12s)→ 提取 `[RX]`(每行加 `[+Nms]` 时基)→ 回读校验(`reset.bat cmp`)→ verify;`--keep-out` 清旧产物;退出码 0=PASS/1=FAIL/2=采集·烧录·回读错误/3=用法错误 |
|
||||
| `MDK-ARM/reset.bat` | `MDK-ARM/reset.bat` | ST-LINK CLI 封装:`flash [hex]`(烧 + `-Rst -Run`)/ **`cmp [hex]`(回读比对,2026-10-10 加)**/ 无参复位 / `halt`/`run`/`list`/`hard` |
|
||||
| `tools/serial_tool/serial_tool.py` | `tools/serial_tool/` | Python 串口助手(常驻 daemon + 薄 CLI):ports/open/send/status/log/clear/wait-idle/selftest;全量日志 `state/serial.log`(20MB 自动截头) |
|
||||
| `test/host/verify_log.py` | `test/host/verify_log.py` | 对日志做标记校验:Required/Forbidden(**命中逐行打印行号+内容**)、启动锚点计数 `--max-boot-count`、原因白名单 `--expect-reset-cause`、标记顺序 `--require-order`、开机耗时 `--done-marker`/`--max-boot-ms`、`--json` 结构化输出;退出码 0/1/2 |
|
||||
| `.dsh/skills/serial-assistant/scripts/serial_tool.py` | `.dsh/skills/serial-assistant/` | Python 串口助手(常驻 daemon + 薄 CLI):ports/open/send/status/log/clear/wait-idle/selftest;全量日志 `state/serial.log`(20MB 自动截头) |
|
||||
| `.dsh/skills/serial-boot-test/scripts/verify_log.py` | `.dsh/skills/serial-boot-test/scripts/verify_log.py` | 对日志做标记校验:Required/Forbidden(**命中逐行打印行号+内容**)、启动锚点计数 `--max-boot-count`、原因白名单 `--expect-reset-cause`、标记顺序 `--require-order`、开机耗时 `--done-marker`/`--max-boot-ms`、`--json` 结构化输出;退出码 0/1/2 |
|
||||
|
||||
## 判定标准
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
| 现象 | 处理 |
|
||||
|---|---|
|
||||
| `selftest` FAIL / `ModuleNotFoundError: pyserial` | venv 未建或依赖缺失:按 AGENTS.md「Python 环境」重建 `.venv` + `pip install -r requirements.txt` |
|
||||
| daemon 无响应 / 自动拉起失败 | 查 `tools/serial_tool/state/daemon.log`;`serial_tool.py restart` 重建 daemon;检查 COM 是否被其他程序占用 |
|
||||
| daemon 无响应 / 自动拉起失败 | 查 `.dsh/skills/serial-assistant/scripts/state/daemon.log`;`serial_tool.py restart` 重建 daemon;检查 COM 是否被其他程序占用 |
|
||||
| 抓到日志缺 `Network Init` | `-SettleMs` 太小(boot 任务间有 ~3-4s 自然静默间隙),用默认 8000ms;2026-10-10 起首字节到达前不启动静默计时(`--first-wait-ms` 默认 12000),首行晚到不再被提前收尾 |
|
||||
| 烧录后无数据(`no_data`) | 2026-10-10 起 `no_data` = **本次等待期间零字节**(旧实现比 daemon 累计字节,常驻 daemon 下永不触发,只能靠"日志里没有 [RX] 行"兜底);开串口 + clear 必须在烧录**前**完成(`run_boot_test.py` 已保证);核对 COM 号、板子供电,看 `state/serial.log` 有无 SYS 异常行 |
|
||||
| `[MISS] fw build: <戳>`(其余标记全中) | 芯片里跑的不是这份构建:烧录没落地 / 烧错文件 / hex 未重编译。处置:`reset.bat flash <hex>` 重烧并看 `READBACK: OK`;确认 `@build` 已重跑(戳取自编译时刻) |
|
||||
|
||||
@@ -241,11 +241,11 @@ DTU-RemoteLCD 客户端;输出每帧 `req2first / total / recv 批次到达时
|
||||
# 2) 一键闭环:开串口+清日志 → 烧录(reset.bat flash,Reset&Run)→ wait-idle → 标记校验
|
||||
# 硬件层模式无 "Network Init" 标记(netTask 自挂起),且 TEST_REPORT 行含 "failed=0"
|
||||
# → Required/Forbidden 均用下方定制集(见 §4.4)
|
||||
& '.venv\Scripts\python.exe' test\run_boot_test.py `
|
||||
& '.venv\Scripts\python.exe' .dsh\skills\serial-boot-test\scripts\run_boot_test.py `
|
||||
--required "App init OK;net init: OK;PHY: 100M FULL;[LAT] report complete" `
|
||||
--forbidden "HardFault;assert;[ERR];[FAIL];verdict=FAIL"
|
||||
# 3) 提取 [LAT] 报告行(直接读日志文件最可靠)
|
||||
& '.venv\Scripts\python.exe' tools\serial_tool\serial_tool.py log --grep "\[LAT\]"
|
||||
& '.venv\Scripts\python.exe' .dsh\skills\serial-assistant\scripts\serial_tool.py log --grep "\[LAT\]"
|
||||
```
|
||||
|
||||
台架已跑通:7 段全部 OK、零尖峰、p95≈min(零抖动),§6.1 基线已按实测重锁。
|
||||
@@ -257,13 +257,13 @@ DTU-RemoteLCD 客户端;输出每帧 `req2first / total / recv 批次到达时
|
||||
# ENABLE_PHASE13_TESTS(Phase 13 必须独占 8080 socket,勿与其他 NET 层 Phase 同启)
|
||||
& "MDK-ARM\build.bat" # 必须 0 错 0 警
|
||||
# 1) 先 clear 再起 peer(顺序不可反:wait-idle 需要看到会话活跃期)
|
||||
& '.venv\Scripts\python.exe' tools\serial_tool\serial_tool.py clear
|
||||
& '.venv\Scripts\python.exe' .dsh\skills\serial-assistant\scripts\serial_tool.py clear
|
||||
& '.venv\Scripts\python.exe' test\latency_peer.py --host 192.168.1.100 --port 8080 `
|
||||
--json "docs\测试说明\m2_data\m2_quiet.json" --wait 170 # 后台作业
|
||||
# 2) 烧录(Reset&Run 不可省)
|
||||
& "MDK-ARM\download.bat"
|
||||
# 3) 等空闲:会话本身约 2s,但 peer 首连常因"芯片空闲超时 RST"失败一次并 2s 后重试
|
||||
& '.venv\Scripts\python.exe' tools\serial_tool\serial_tool.py wait-idle `
|
||||
& '.venv\Scripts\python.exe' .dsh\skills\serial-assistant\scripts\serial_tool.py wait-idle `
|
||||
--timeout 120 --settle-ms 10000 --max-ms 110000
|
||||
# 4) 判定:**直接 Grep serial.log 文件**(勿用 log --grep,勿只看 wait-idle)
|
||||
# Required: "App init OK" / "net init: OK" / "[LAT] report complete" / "=== Phase 13: 1/1 PASSED"
|
||||
@@ -282,7 +282,7 @@ DTU-RemoteLCD 客户端;输出每帧 `req2first / total / recv 批次到达时
|
||||
```powershell
|
||||
# M3(生产路径,需要设备已烧 Phase 14 固件)
|
||||
& "MDK-ARM\build.bat" # 0 错 0 警(Phase 14 构建)
|
||||
& '.venv\Scripts\python.exe' tools\serial_tool\serial_tool.py clear
|
||||
& '.venv\Scripts\python.exe' .dsh\skills\serial-assistant\scripts\serial_tool.py clear
|
||||
& "MDK-ARM\download.bat" # 先烧录,再起 peer(顺序不可反!)
|
||||
& '.venv\Scripts\python.exe' test\latency_peer.py --mode remo --host 192.168.1.100 --port 7003 `
|
||||
--samples 45 --stress-from 15 --ftp-stress --json "docs\测试说明\m3_data\m3_dev_stress.json"
|
||||
|
||||
@@ -518,14 +518,14 @@ Get-ChildItem 'C:\ProgramData\anaconda3\Lib\site-packages' -Filter 'cfgv*'
|
||||
**根因**:CI 的 `actions/checkout@v4` 拉的是**干净克隆**,而钩子校验的是
|
||||
**仓库根相对路径是否存在**(`check_backtick_paths`,见 `scripts/check_doc_links.py`)。
|
||||
若文档引用了 **gitignore 的运行时产物**——
|
||||
如 `tools/serial_tool/state/meta.json`(运行时未创建,`.gitignore:132` 排除整个 `state/`)——
|
||||
如 `.dsh/skills/serial-assistant/scripts/state/meta.json`(运行时未创建,`.gitignore:132` 排除整个 `state/`)——
|
||||
你本机因为跑过工具所以文件在,**克隆里则不存在** → 判为失效引用。
|
||||
|
||||
**修复**:按 `docs/文档规范.md` §3.3 给该行加**状态词**(脚本的 `STATUS_HINTS`:
|
||||
`计划 / 未创建 / 尚未创建 / 已删除 / 删除 / 不存在 / 缺失 / 断链 / 待建 / 待创建`)。
|
||||
|
||||
```markdown
|
||||
| `tools/serial_tool/state/meta.json`(运行时未创建) | daemon 元数据(pid / 版本 / gen) |
|
||||
| `.dsh/skills/serial-assistant/scripts/state/meta.json`(运行时未创建) | daemon 元数据(pid / 版本 / gen) |
|
||||
```
|
||||
|
||||
> ⚠️ **状态词必须与引用在【同一行】且 ≤40 字符**(`STATUS_PROXIMITY`)。
|
||||
@@ -536,9 +536,9 @@ Get-ChildItem 'C:\ProgramData\anaconda3\Lib\site-packages' -Filter 'cfgv*'
|
||||
|
||||
```bash
|
||||
# 把运行时目录临时移开,模拟干净克隆,跑完再移回
|
||||
mv tools/serial_tool/state tools/serial_tool/state.bak_ci
|
||||
mv .dsh/skills/serial-assistant/scripts/state .dsh/skills/serial-assistant/scripts/state.bak_ci
|
||||
& '.venv\Scripts\python.exe' scripts/check_doc_links.py --all # 或全局 python -m pre_commit run doc-links --all-files
|
||||
mv tools/serial_tool/state.bak_ci tools/serial_tool/state
|
||||
mv .dsh/skills/serial-assistant/scripts/state.bak_ci .dsh/skills/serial-assistant/scripts/state
|
||||
```
|
||||
|
||||
> 更一般的教训:**任何校验"文件是否存在/数字是否一致"的钩子,都要在
|
||||
@@ -662,7 +662,7 @@ date +%s # 对比 last_online
|
||||
| `l0-gates` 报 `TerminatorExpectedAtEndOfString` / 缺少右引号 | `run:` 块含**中文**,被 WinPS 5.1 按 GBK 解码撞坏引号 | `run:` 块改**纯 ASCII**(中文只留 `name:`/注释) |
|
||||
| `l0-gates` 报 `Cannot find: pwsh in PATH` | `shell: pwsh` = PS7,本机只有 WinPS 5.1 | 改 `shell: powershell` |
|
||||
| `l0-gates` 报 `runner missing: cppcheck` | cppcheck 仅在用户级 PATH(SYSTEM 不可见) | 设机器级 `DTU_CPPCHECK`(§5.1.3,**workflow 不再回落固定目录**),或把 cppcheck 目录加入系统 PATH 后重启任务 |
|
||||
| 🔴 `doc-links` 钩子本地过、CI 挂 | 引用了 **gitignore 的运行时文件**(如 `tools/serial_tool/state/*.json`),本地存在、干净克隆里没有 | 该行加状态词「未创建/不存在」等(§5.1.2) |
|
||||
| 🔴 `doc-links` 钩子本地过、CI 挂 | 引用了 **gitignore 的运行时文件**(如 `.dsh/skills/serial-assistant/scripts/state/*.json`),本地存在、干净克隆里没有 | 该行加状态词「未创建/不存在」等(§5.1.2) |
|
||||
| `l05` 日志 `::warning::runner has no MSVC` 但仍 **PASS** | 缺 MSVC 时 L0.5 **降级跳过**(不硬失败) | 装 VS BuildTools(§5.2),否则该层无覆盖 |
|
||||
| 任务跑满 72 小时被杀 | 计划任务默认 `ExecutionTimeLimit` | 设为 `0`(不限时长),见 §4.3 警示 |
|
||||
| `config set log.file ...` 报 `unknown config key` | **v4 的 log 段无 `file` 键** | 用 `log.job.dir` 或输出重定向(§3) |
|
||||
@@ -910,7 +910,7 @@ powershell -ExecutionPolicy Bypass -File "D:\Docker\act_runner\fix_cfgv_global.p
|
||||
| 日期 | 变更 |
|
||||
|---|---|
|
||||
| 2026-10-02 | **workflow 接入 `DTU_*` 机器级变量**(`本地工具路径环境变量方案.md` 实施):`l0-gates` 的 Python 探测改为 `DTU_PRECOMMIT_PYTHON`(显式,无效即硬失败)→ PATH → `.venv`(删除 `C:\ProgramData\anaconda3` 固定候选);cppcheck 改为 `DTU_CPPCHECK` 优先 → PATH(**移除 `C:\Program Files\Cppcheck` 固定回退**);`l05` 的 MSVC 探测改为 `DTU_MSVC_CL` 优先 → `%ProgramFiles*` 推导的标准根(与 `unit_test_harness.py` 同步,不再写死盘符);新增 §5.1.3 机器级变量设置方法(`Machine` scope + 重启任务)、§5.2 与 check_env 样例、排障表 cppcheck 行、TL;DR 5b 步骤。§5.1.1 的报错原文标注为历史记录 |
|
||||
| 2026-10-02 | ✅ **首次 CI 全绿(run#8:L0 + L0.5 双 job Success)**。定位并修复最后两个拦路虎:① **§5.1.1 用户态 site-packages 陷阱**——`pre-commit` 只装在 `%APPDATA%\Roaming\Python\...`(SYSTEM 不可见);且提权 `pip install --no-user` 会因 pip 扫**整个 `sys.path`**、见 user-site 已满足而**静默跳过**全局安装(9 依赖中唯 `cfgv` 因版本相同被跳过→`-s` 下 `ModuleNotFoundError`)。修法:提权 + `--force-reinstall --no-deps` 强制落全局,并以 `-s` 复验。② **§5.1.2 doc-links 假阳性**——文档引用了 gitignore 的运行时文件 `tools/serial_tool/state/meta.json`,本地存在、干净克隆无 → 按《文档规范》§3.3 加同状态词「运行时未创建」。排障表补 6 条(pre-commit 不可见 / GBK 中文 / pwsh / cppcheck / doc-links / MSVC 降级)。新增配套脚本 `install_precommit_global.ps1`、`install_precommit_global2.ps1`、`fix_cfgv_global.ps1` |
|
||||
| 2026-10-02 | ✅ **首次 CI 全绿(run#8:L0 + L0.5 双 job Success)**。定位并修复最后两个拦路虎:① **§5.1.1 用户态 site-packages 陷阱**——`pre-commit` 只装在 `%APPDATA%\Roaming\Python\...`(SYSTEM 不可见);且提权 `pip install --no-user` 会因 pip 扫**整个 `sys.path`**、见 user-site 已满足而**静默跳过**全局安装(9 依赖中唯 `cfgv` 因版本相同被跳过→`-s` 下 `ModuleNotFoundError`)。修法:提权 + `--force-reinstall --no-deps` 强制落全局,并以 `-s` 复验。② **§5.1.2 doc-links 假阳性**——文档引用了 gitignore 的运行时文件 `.dsh/skills/serial-assistant/scripts/state/meta.json`,本地存在、干净克隆无 → 按《文档规范》§3.3 加同状态词「运行时未创建」。排障表补 6 条(pre-commit 不可见 / GBK 中文 / pwsh / cppcheck / doc-links / MSVC 降级)。新增配套脚本 `install_precommit_global.ps1`、`install_precommit_global2.ps1`、`fix_cfgv_global.ps1` |
|
||||
| 2026-10-02 | 🔴 **常驻方式重大修正(实测推翻)**:`Start-Service gitea-runner` 必然失败。经二进制逆向确认 `gitea-runner v4.0.1` **无任何服务管理代码**(命令集仅 `bug-report/cache-server/config/daemon/exec/register`,无 `service.go`;`OpenSCManagerW` 等符号来自 Docker SDK,非服务安装代码),`daemon` 是纯控制台程序、不向 SCM 回报 `SERVICE_RUNNING`。**§4 全节重写为「任务计划程序」方案**(新增 §4.0 根因分析、§4.2 `deploy_runner_task.ps1`、§4.3 手动命令 + 72h 超时警示、§4.5 清理遗留服务、§4.6 提权);删除失效的 `deploy_runner.ps1`/`install_service.ps1`;排障表补「错误 1053」「LastTaskResult 非 0」「72 小时被杀」3 条;拓扑图/§5.1/§7 日志/§10 TL;DR 同步更新。另修 `config.yaml` 的 `fetch_timeout` 5m→1m(超出 RPC 1m 会被截断并告警) |
|
||||
| 2026-10-02 | **修 `.ps1` 编码坑(实测)**:无 BOM 的 UTF-8 脚本被 WinPS 5.1 按 GBK 解读 → 中文乱码(`鐒跺悗鎵ц`)并撞坏引号配对,报 `源文本中包含无法识别的标记`/`字符串缺少终止符`。**新增 §7.1「给含中文的 `.ps1` 加 UTF-8 BOM」**(两种修法 + 解析器校验命令);排障表补该条目;配套脚本已统一加 BOM |
|
||||
| 2026-10-02 | **按实测修正为 v4.0.1**:act_runner 已更名 **`gitea-runner`**,发布页迁至 `gitea.com/gitea/runner`。**v4 移除 `daemon install/start/stop/status`**;**v4 `log` 段无 `file` 键**(§3 改用 `log.job.dir`);补 §1.3 命令集对照、§6.1 MySQL 直查核验、排障表(`accepts at most 0 arg(s)` / `unknown config key log.file` 等) |
|
||||
|
||||
@@ -183,11 +183,11 @@ python tools/font_gen/extract_cn.py # 任意 python3:
|
||||
|
||||
| 项 | 要求 | 说明 |
|
||||
|---|---|---|
|
||||
| Python 串口助手 | `tools/serial_tool/serial_tool.py`(仓库内置,.venv + pyserial) | 常驻 daemon + 薄 CLI + 全量日志文件;无需单独安装,`pip install -r requirements.txt` 即就绪 |
|
||||
| Python 串口助手 | `.dsh/skills/serial-assistant/scripts/serial_tool.py`(仓库内置,.venv + pyserial) | 常驻 daemon + 薄 CLI + 全量日志文件;无需单独安装,`pip install -r requirements.txt` 即就绪 |
|
||||
| 调试串口 | USART1(PA9 TX / PA10 RX),115200 8N1 | **端口单一配置源 `.serialtest/config.json`**(本机 COM6);换端口只改这个文件 |
|
||||
| 一键测试 | `test\run_boot_test.py`(即 `@test`,`& '.venv\Scripts\python.exe' test/host/run_boot_test.py`) | 开串口 + 清日志 → 烧录 → wait-idle(静默 8s/超时 30s)→ 提取 → 标记校验;默认端口取 `serial_config.json` / 115200,退出码 0=PASS / 1=FAIL / 2=采集或烧录错误 / 3=用法错误 |
|
||||
| 一键测试 | `.dsh/skills/serial-boot-test/scripts/run_boot_test.py`(即 `@test`,`& '.venv\Scripts\python.exe' .dsh/skills/serial-boot-test/scripts/run_boot_test.py`) | 开串口 + 清日志 → 烧录 → wait-idle(静默 8s/超时 30s)→ 提取 → 标记校验;默认端口取 `.serialtest/config.json` / 115200,退出码 0=PASS / 1=FAIL / 2=采集或烧录错误 / 3=用法错误 |
|
||||
|
||||
> 采集 = 端口常开 + daemon 全量日志(`tools/serial_tool/state/serial.log`),无采集窗口/first-byte/EOF 重试仪式。
|
||||
> 采集 = 端口常开 + daemon 全量日志(`.dsh/skills/serial-assistant/scripts/state/serial.log`),无采集窗口/first-byte/EOF 重试仪式。
|
||||
> 命令契约与陷阱见 `docs/测试说明/串口助手设计说明.md`、`docs/测试说明/自动化测试流程.md`。
|
||||
|
||||
### 2.7 代码审查钩子(pre-commit)
|
||||
@@ -328,11 +328,11 @@ pre-commit run --all-files # 首次全库基线扫描
|
||||
|
||||
**第 7 步:Python 串口助手**(串口调试 / 自动测试,仓库内置)
|
||||
|
||||
无需单独安装:`tools/serial_tool/` 随仓库提供,依赖 `pyserial`(已入 `requirements.txt`,第 4 步建 `.venv` 时一并安装)。建成后验证:
|
||||
无需单独安装:`.dsh/skills/serial-assistant/` 随仓库提供,依赖 `pyserial`(已入 `requirements.txt`,第 4 步建 `.venv` 时一并安装)。建成后验证:
|
||||
|
||||
```powershell
|
||||
& '.venv\Scripts\python.exe' tools\serial_tool\serial_tool.py selftest # → SELFTEST PASS
|
||||
& '.venv\Scripts\python.exe' tools\serial_tool\serial_tool.py ports # 枚举 COM 口
|
||||
& '.venv\Scripts\python.exe' .dsh\skills\serial-assistant\scripts\serial_tool.py selftest # → SELFTEST PASS
|
||||
& '.venv\Scripts\python.exe' .dsh\skills\serial-assistant\scripts\serial_tool.py ports # 枚举 COM 口
|
||||
```
|
||||
|
||||
**第 8 步:下载器驱动**(ST-Link)
|
||||
@@ -344,7 +344,7 @@ pre-commit run --all-files # 首次全库基线扫描
|
||||
**第 9 步:串口配置**
|
||||
|
||||
```powershell
|
||||
& '.venv\Scripts\python.exe' tools\serial_tool\serial_tool.py ports # 查 COM 号
|
||||
& '.venv\Scripts\python.exe' .dsh\skills\serial-assistant\scripts\serial_tool.py ports # 查 COM 号
|
||||
```
|
||||
|
||||
把查到的端口写入 `.serialtest\config.json` 的 `portName`(唯一配置源,换口只改这里)。
|
||||
@@ -358,7 +358,7 @@ pre-commit run --all-files # 首次全库基线扫描
|
||||
& '.venv\Scripts\python.exe' simulator\tests\check_menu_rows.py # PASS
|
||||
& '.venv\Scripts\python.exe' simulator\tests\check_content_pages.py --update # 首次生成参考图
|
||||
& '.venv\Scripts\python.exe' simulator\tests\check_content_pages.py # 再跑 → PASS
|
||||
& '.venv\Scripts\python.exe' tools\serial_tool\serial_tool.py selftest # → SELFTEST PASS
|
||||
& '.venv\Scripts\python.exe' .dsh\skills\serial-assistant\scripts\serial_tool.py selftest # → SELFTEST PASS
|
||||
& '.venv\Scripts\python.exe' test\host\watchdog\run_unit_test.py # 主机侧单测 → 39/39 PASSED(退出码 0)
|
||||
pre-commit run --all-files
|
||||
```
|
||||
@@ -526,7 +526,7 @@ $env:DTU_KEIL_UV4 = 'D:\NotExist\UV4.exe' # 仅当前会话,不动用户级
|
||||
|---|---|
|
||||
| 下载方式 | `& "MDK-ARM\download.bat"`(Keil 命令行:烧写 + Reset & Run) |
|
||||
| 下载器 | 工程已配 `UL2CM3` 调试驱动 + `STM32F4xx_1024` Flash 算法;**物理下载器按现场选择**(本机为 ST-Link——不占 COM 口,可与串口调试并存);新机器在 Keil `Options for Target → Debug → Settings` 选择与硬件一致的驱动 |
|
||||
| 调试日志 | USART1 115200;抓开机日志必须**先武装采集再复位**(`test/host/run_boot_test.py --flash-cmd` 已保证顺序) |
|
||||
| 调试日志 | USART1 115200;抓开机日志必须**先武装采集再复位**(`.dsh/skills/serial-boot-test/scripts/run_boot_test.py --flash-cmd` 已保证顺序) |
|
||||
|
||||
### 3.5 功能配置开关
|
||||
|
||||
|
||||
@@ -367,7 +367,7 @@ D-2/D-3 的共同根因:**没有任何机械检查**保证"文档说的 = 代
|
||||
|
||||
> **实施记录(2026-09-18,按方案 A 落地,两处必要调整)**:① 分配接口**不写在 `cfg_file.c` 里,而是加到 `cfg_file_store` 后端**(`cfg_store_alloc` / `cfg_store_free` / `cfg_store_heap_free`)——`cfg_file.c` 是双端共享源码,直接写 `pvPortMalloc` 会让 PC 模拟器断链;MCU 后端 = `pvPortMalloc/vPortFree`,模拟器后端 = `malloc/free`。② 编译期峰值约束改用 **typedef 负数组**(`CFG_HEAP_BUDGET = 10,240 B`)——ARMCC 5 只到 C99,**没有 `_Static_assert`**(方案 A 原文此处需修正)。③ 另补 4 条失败路径的 `syslog_post(SYSLOG_SELFTEST, …)` 与 `load: OK …, heap free NB` 实测行。台账:`docs/待处理内容.md` **P0-1**;设计文档:`docs/子系统设计/配置文件设计说明.md` §6.2/§8 + v1.11。
|
||||
|
||||
> **诚实标注**:本结论由静态分析得出(映射表 + 启动文件 + 调用图三重证据),**尚未上机确认**。`test/host/verify_log.py`(PowerShell 版 `test/verify_log.ps1:34-35` 已删除,2026-09-20 起为纯 Python)的 Forbidden 标记含 `FAIL`,而失败路径会打印 `read: malloc 4097B FAIL` / `template: malloc 5120B FAIL`,因此**一次 `@test` 即可判定**(若近期 `@test` 曾 PASS,需先确认当时是否真的进入了文件读取分支,例如 `/config.ini` 不存在或大小为 0)。**(2026-09-18 已按上述实施;修复后的真机 `@test` 正向确认——`load: OK` 出现且 `FAIL` 零命中——已于 2026-10-03 执行 PASS,2026-10-04 独立复跑再次 PASS:`[CFG] load: OK (523B, heap free 8840B)`、Forbidden `FAIL`/`ERROR`/`HardFault`/`[ERR]` 零命中、`selftest: no fault record`、ST-LINK 全片回读 `No difference found`。)**
|
||||
> **诚实标注**:本结论由静态分析得出(映射表 + 启动文件 + 调用图三重证据),**尚未上机确认**。`.dsh/skills/serial-boot-test/scripts/verify_log.py`(PowerShell 版 `test/verify_log.ps1:34-35` 已删除,2026-09-20 起为纯 Python)的 Forbidden 标记含 `FAIL`,而失败路径会打印 `read: malloc 4097B FAIL` / `template: malloc 5120B FAIL`,因此**一次 `@test` 即可判定**(若近期 `@test` 曾 PASS,需先确认当时是否真的进入了文件读取分支,例如 `/config.ini` 不存在或大小为 0)。**(2026-09-18 已按上述实施;修复后的真机 `@test` 正向确认——`load: OK` 出现且 `FAIL` 零命中——已于 2026-10-03 执行 PASS,2026-10-04 独立复跑再次 PASS:`[CFG] load: OK (523B, heap free 8840B)`、Forbidden `FAIL`/`ERROR`/`HardFault`/`[ERR]` 零命中、`selftest: no fault record`、ST-LINK 全片回读 `No difference found`。)**
|
||||
|
||||
### 4.5 日志子系统(`App/log/sys_log_mcu.c`)
|
||||
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@
|
||||
& '.venv\Scripts\python.exe' scripts/check_test_suite.py # 门禁判定
|
||||
& '.venv\Scripts\python.exe' scripts/check_test_suite.py --allow-test-build # 放行测试构建
|
||||
& '.venv\Scripts\python.exe' scripts/ram_report.py
|
||||
& '.venv\Scripts\python.exe' scripts/stack_report.py -s tools\serial_tool\state\serial.log
|
||||
& '.venv\Scripts\python.exe' scripts/stack_report.py -s .dsh\skills\serial-assistant\scripts\state\serial.log
|
||||
```
|
||||
|
||||
## ⚠️ 脚本编写约定
|
||||
|
||||
+2
-2
@@ -16,10 +16,10 @@
|
||||
|
||||
```bash
|
||||
# @test 一键闭环(推荐)
|
||||
& '.venv\Scripts\python.exe' test/host/run_boot_test.py
|
||||
& '.venv\Scripts\python.exe' .dsh/skills/serial-boot-test/scripts/run_boot_test.py
|
||||
|
||||
# 单独校验日志
|
||||
& '.venv\Scripts\python.exe' test/host/verify_log.py <日志文件>
|
||||
& '.venv\Scripts\python.exe' .dsh/skills/serial-boot-test/scripts/verify_log.py <日志文件>
|
||||
|
||||
# 多套件回归编排(需真机;自动改开关 + 构建 + 烧录 + 采集,结束后还原)
|
||||
& '.venv\Scripts\python.exe' test/run_all_suites.py
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
1. 在 `test_config.h` 加开关(并入互斥 `#error` 检查)
|
||||
2. 新建 `<模块>_test_task.c/h`
|
||||
3. **注册进 Keil 工程**(`<FilePath>..\test\board\xxx.c</FilePath>`)
|
||||
4. 跑回归:`@test`(`test/host/run_boot_test.py`)
|
||||
4. 跑回归:`@test`(`.dsh/skills/serial-boot-test/scripts/run_boot_test.py`)
|
||||
|
||||
## 相关文档
|
||||
|
||||
|
||||
+2
-2
@@ -54,10 +54,10 @@
|
||||
|
||||
```bash
|
||||
# @test 一键闭环(推荐,需真机)
|
||||
& '.venv\Scripts\python.exe' test/host/run_boot_test.py
|
||||
& '.venv\Scripts\python.exe' .dsh/skills/serial-boot-test/scripts/run_boot_test.py
|
||||
|
||||
# 测试套件构建的回归:要求套件跑到汇总行且断言数 > 0(T-2)
|
||||
& '.venv\Scripts\python.exe' test/host/run_boot_test.py `
|
||||
& '.venv\Scripts\python.exe' .dsh/skills/serial-boot-test/scripts/run_boot_test.py `
|
||||
--required "=== Storage Tests:" --forbidden "ERROR;HardFault;[ERR]" `
|
||||
--require-suite-summary "Storage Tests"
|
||||
|
||||
|
||||
@@ -1,501 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""run_boot_test.py - 一键开机日志抓取 + 校验(纯标准库,2026-09-20 替代 run_boot_test.ps1)。
|
||||
|
||||
调用仓库内置串口助手(tools/serial_tool,常驻 daemon,子进程 CLI + JSON):
|
||||
1. open 串口 + clear 日志(本次运行独立代际)
|
||||
2. 烧录(默认 MDK-ARM\\download.bat:下载 + Reset & Run)
|
||||
2.5 固件身份戳:从刚烧进去的 .hex 里抠出 `fw build: <DATE> <TIME>`,追加成 Required
|
||||
标记——芯片里跑的必须就是这份构建(--no-fw-check 关闭)
|
||||
3. wait-idle:RX 静默 >= settle-ms 收尾;首字节超过 --first-wait-ms 未到 → no_data
|
||||
(快速失败);整体 max-wait-ms 超时
|
||||
4. 从日志提取 [RX] 载荷行(剥掉 "[RX] <ISO时间戳> " 前缀),并给每行加 `[+Nms]` 相对
|
||||
偏移(daemon 毫秒时基,本次第一行 = +0ms;--raw-log 出纯载荷)。MCU 侧时间戳只有
|
||||
秒级、开机头几行还没时钟,量不了耗时——这个前缀就是耗时口径
|
||||
4.5 烧录回读校验:MDK-ARM\\reset.bat cmp(= ST-LINK_CLI -CmpFile 全片逐字节比对,
|
||||
`No difference found` 才算真写进去;--no-verify-flash 关闭)。
|
||||
**必须排在采集之后**:实测 -CmpFile 连接时会复位目标(板子当场重打一遍开机日志)
|
||||
5. verify_log.py 标记校验(含启动锚点计数:`reset cause:` 出现 0 次或 >N 次 = FAIL,
|
||||
N 由 --max-boot-count 给,默认 1;--expect-reset-cause 可对第一个锚点做原因白名单;
|
||||
--require-order 查标记先后顺序;--done-marker + --max-boot-ms 量开机耗时)
|
||||
6. (仅当 --require-selftest-pass)自检记录联合判定(S7):
|
||||
--selftest-log 提供本地副本则直接用它,否则从设备 FTP 抓取 /log/selftest.log
|
||||
(lftpd :21 匿名);判定:文件缺失 / 0 字节 = 无故障 → PASS,有 64B 记录 = FAIL
|
||||
|
||||
采集在烧录**之前**武装(open + clear 先于烧录),daemon 常驻全量日志兜底,
|
||||
无采集窗口/首字节等待仪式(与旧 PowerShell 版行为一致)。
|
||||
|
||||
相对旧 PowerShell 版的加固(本机 Windows 环境实测教训,2026-09-20):
|
||||
- 子进程输出按**字节**读取、显式 UTF-8 解码 —— 不受控制台代码页(GBK)影响
|
||||
- serial_config.json 按 UTF-8(容忍 BOM)解析 —— 无 PS 5.1 按 GBK 读 JSON
|
||||
导致 "Unterminated string" 崩溃的问题
|
||||
- 不经 PowerShell —— 无执行策略(scripts disabled)拦截
|
||||
- 烧录带超时(默认 180s;旧版无超时,坏 SWD 连接会永久挂起)
|
||||
|
||||
用法:
|
||||
& '.venv\\Scripts\\python.exe' test/host/run_boot_test.py
|
||||
[--port COM7] [--baud-rate 115200] [--settle-ms 8000] [--max-wait-ms 30000]
|
||||
[--first-wait-ms 12000]
|
||||
[--flash-cmd MDK-ARM\\download.bat] [--flash-timeout 180]
|
||||
[--required "App init OK;Network Init;net init: OK"]
|
||||
[--forbidden "FAIL;ERROR;HardFault;[ERR]"]
|
||||
[--require-suite-summary "Storage Tests"]
|
||||
[--require-selftest-pass] [--selftest-log <本地副本>]
|
||||
[--device-ip 192.168.1.100] [--out-dir <目录>] [--show-tail-lines 15]
|
||||
[--fw-hex <构建产物 hex>] [--no-fw-check]
|
||||
[--no-verify-flash] [--readback-timeout 120]
|
||||
[--max-boot-count 1] [--expect-reset-cause 'software;power-on']
|
||||
[--require-order 'App init OK>Network Init>net init: OK']
|
||||
[--done-marker 'net init: OK'] [--max-boot-ms 0]
|
||||
[--raw-log] [--keep-out 20]
|
||||
|
||||
--port 缺省取 .serialtest/config.json 的 portName(单一配置源)
|
||||
|
||||
退出码(与旧版一致):
|
||||
0 = PASS(采集成功 + Required 全中 + Forbidden 零命中)
|
||||
1 = FAIL(标记缺失/命中)
|
||||
2 = 采集或烧录错误(含烧录回读不一致 = 芯片内容与刚编的镜像不同,本次运行作废)
|
||||
3 = 用法/预检错误
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import ftplib
|
||||
import json
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
# 本脚本位于 test/host/,仓库根需上溯两级(2026-10 实机窗口修:原 .parent 只到 test/,
|
||||
# 导致 .serialtest/config.json 与 tools/serial_tool 路径解析错误)
|
||||
REPO_ROOT = SCRIPT_DIR.parent.parent
|
||||
SERIAL_TOOL = REPO_ROOT / "tools" / "serial_tool" / "serial_tool.py"
|
||||
CFG_FILE = REPO_ROOT / ".serialtest" / "config.json"
|
||||
DEFAULT_FLASH = REPO_ROOT / "MDK-ARM" / "download.bat"
|
||||
RESET_BAT = REPO_ROOT / "MDK-ARM" / "reset.bat"
|
||||
DEFAULT_HEX = REPO_ROOT / "MDK-ARM" / "STM32F407-Demo" / "STM32F407-Demo.hex"
|
||||
|
||||
REQUIRED_DEFAULT = "App init OK;Network Init;net init: OK"
|
||||
FORBIDDEN_DEFAULT = "FAIL;ERROR;HardFault;[ERR]"
|
||||
|
||||
# 固件身份戳(App/app/app_main.c 的 `DBG_INFO("fw build: " __DATE__ " " __TIME__)`)。
|
||||
# 编译期展开成一段连续 ASCII,故能直接从 .hex 里抠出来当 Required 标记用:
|
||||
# 芯片里跑的镜像 == 刚编出来的镜像,这才叫"烧进去了"。此前所有标记都是固定串,
|
||||
# 旧镜像照样全中——AGENTS.md 烧录铁律记的"反复重下载、芯片一直是旧镜像"在
|
||||
# 自动闭环里完全不可见(2026-10-10 加)。
|
||||
FW_MARKER = "fw build: "
|
||||
|
||||
|
||||
def _split_cmd(cmd_str):
|
||||
r"""把 `--flash-cmd` 这类命令串拆成 [exe, arg, ...](2026-10-10 实测坑)。
|
||||
|
||||
直接交给 subprocess、**不经 `cmd /c`**:CreateProcess 能直接跑 .bat,且 Python
|
||||
会给含空格的路径正确加引号。旧写法 `['cmd','/c', 串]` 在本仓库(目录名
|
||||
`DTU Code` 含空格)会被 cmd 的 /c 引号规则玩坏——实测报
|
||||
`'D:\Code\DTU' is not recognized`,回读校验被假判成"芯片内容不一致"。
|
||||
路径本身含空格时把该段用引号包住(`"D:\some dir\reset.bat" flash`)。
|
||||
posix=False 是必须的:posix=True 会吃掉 Windows 路径里的反斜杠。
|
||||
"""
|
||||
return [p.strip('"') for p in shlex.split(cmd_str, posix=False)]
|
||||
|
||||
|
||||
def _run_cmd(argv, timeout):
|
||||
"""跑一条外部命令,按字节取回输出并显式 UTF-8 解码(不受控制台代码页影响)。"""
|
||||
try:
|
||||
proc = subprocess.run(argv, capture_output=True, timeout=timeout)
|
||||
rc = proc.returncode
|
||||
raw = (proc.stdout or b"") + (proc.stderr or b"")
|
||||
except subprocess.TimeoutExpired as e:
|
||||
rc = -1
|
||||
raw = (e.stdout or b"") + (e.stderr or b"")
|
||||
return rc, raw.decode("utf-8", errors="replace")
|
||||
|
||||
|
||||
def extract_fw_stamp(hex_path):
|
||||
"""从 Intel HEX 里抠固件身份戳(只扫数据记录,不建整片镜像,内存 O(镜像体积))。
|
||||
|
||||
返回戳串(如 'Oct 10 2026 02:38:12');找不到(旧固件/未重编译)返回 None。
|
||||
"""
|
||||
data = bytearray()
|
||||
for line in Path(hex_path).read_text(errors="replace").splitlines():
|
||||
line = line.strip()
|
||||
if not line.startswith(":"):
|
||||
continue
|
||||
try:
|
||||
rec = bytes.fromhex(line[1:])
|
||||
except ValueError:
|
||||
continue
|
||||
if len(rec) > 4 and rec[3] == 0:
|
||||
data += rec[4:4 + rec[0]]
|
||||
i = data.find(FW_MARKER.encode("ascii"))
|
||||
if i < 0:
|
||||
return None
|
||||
j = i + len(FW_MARKER)
|
||||
end = j
|
||||
while end < len(data) and 0x20 <= data[end] < 0x7F:
|
||||
end += 1
|
||||
return data[j:end].decode("ascii", errors="replace")
|
||||
|
||||
|
||||
def _cli(cmd_args, timeout):
|
||||
"""调用串口助手 CLI,返回 (rc, stdout_text, parsed_json_or_None)。
|
||||
|
||||
全程字节级处理(capture_output 取 bytes 后显式 UTF-8 解码),
|
||||
任何 Windows 控制台代码页都不参与数据路径。
|
||||
"""
|
||||
try:
|
||||
proc = subprocess.run(cmd_args, capture_output=True, timeout=timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
return 2, "client timeout ({}s)".format(timeout), None
|
||||
text = proc.stdout.decode("utf-8", errors="replace")
|
||||
try:
|
||||
return proc.returncode, text, json.loads(text)
|
||||
except ValueError:
|
||||
return proc.returncode, text, None
|
||||
|
||||
|
||||
def _tool_cmd(py, cmd, *rest, timeout=15):
|
||||
rc, out, data = _cli([py, str(SERIAL_TOOL), cmd, *rest], timeout)
|
||||
if rc != 0 or data is None:
|
||||
return None, out
|
||||
return data, out
|
||||
|
||||
|
||||
def _ftp_quit(ftp):
|
||||
"""尽力关闭 FTP 控制连接(QUIT 失败不影响判定)。"""
|
||||
try:
|
||||
ftp.quit()
|
||||
except (OSError, ftplib.Error):
|
||||
pass
|
||||
|
||||
|
||||
def _prune_out_dir(out_dir, keep):
|
||||
"""只保留最近 N 份产物(`boot_log_*.txt` / `selftest_log_*.log`)。
|
||||
|
||||
这些文件按 `%Y%m%d_%H%M%S` 命名、每次 @test 各留一份,旧实现从不清理——
|
||||
本机实测 out 目录已堆到 31 份 boot_log + 7 份 selftest_log(2026-10-10)。
|
||||
文件名含时间戳 → 按名字排序即按时间排序。
|
||||
"""
|
||||
if keep <= 0:
|
||||
return 0
|
||||
removed = 0
|
||||
for pat in ("boot_log_*.txt", "selftest_log_*.log"):
|
||||
for p in sorted(out_dir.glob(pat), key=lambda q: q.name)[:-keep]:
|
||||
try:
|
||||
p.unlink()
|
||||
removed += 1
|
||||
except OSError:
|
||||
pass
|
||||
return removed
|
||||
|
||||
|
||||
def _fetch_selftest_log(device_ip, out_dir, stamp):
|
||||
"""从设备 FTP 抓取 /log/selftest.log(lftpd :21 匿名,根目录 = 卷根)。
|
||||
|
||||
成功返回本地路径字符串;失败(网络不可达 / FTP 错误 / 设备侧文件缺失)返回 ""
|
||||
(按 S7 语义视为无故障记录;此处打 WARN,调用方不判整体 FAIL)。
|
||||
|
||||
2026-10-02 台架修复:原实现只 `except OSError`,而 ftplib 的 error_temp /
|
||||
error_perm 继承自 Exception 而非 OSError —— 台账"仅故障才写",干净设备上
|
||||
/log/selftest.log 根本不存在,RETR 返回 450 直接抛出未捕获异常,令整个
|
||||
`@test --require-selftest-pass` 在最后一步崩掉(而它本该是 PASS 路径)。
|
||||
现把"设备侧无此文件"单列为正常路径(INFO),其余 FTP 错误保留 WARN。
|
||||
"""
|
||||
local = Path(out_dir) / "selftest_log_{}.log".format(stamp)
|
||||
ftp = ftplib.FTP()
|
||||
try:
|
||||
ftp.connect(device_ip, 21, timeout=10)
|
||||
ftp.login() # 匿名(lftpd 接受 USER anonymous / 空 PASS)
|
||||
except (OSError, ftplib.Error) as e:
|
||||
print("RUN_TEST_WARN: selftest fetch failed: {} "
|
||||
"(按 S7 语义视为无记录;设备网络/FTP 需另行排查)".format(e))
|
||||
return ""
|
||||
|
||||
buf = bytearray()
|
||||
try:
|
||||
ftp.retrbinary("RETR /log/selftest.log", buf.extend)
|
||||
except ftplib.all_errors as e:
|
||||
# 450/550 = 设备侧无此文件:干净台账,S7 判 PASS(正常路径,非故障)
|
||||
print("SELFTEST_FETCH: {} -> (ledger absent: {}) 按 S7 视为无故障记录"
|
||||
.format(device_ip, str(e).strip()))
|
||||
_ftp_quit(ftp)
|
||||
return ""
|
||||
|
||||
_ftp_quit(ftp)
|
||||
local.write_bytes(bytes(buf))
|
||||
print("SELFTEST_FETCH: {} -> {} ({} bytes)".format(device_ip, local, len(buf)))
|
||||
return str(local)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
# 控制台输出固定 UTF-8:Windows 代码页只影响显示,不会让脚本崩溃
|
||||
for stream in (sys.stdout, sys.stderr):
|
||||
try:
|
||||
stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ap = argparse.ArgumentParser(description="boot log capture + verify (pure stdlib, no PowerShell)")
|
||||
ap.add_argument("--port", default="",
|
||||
help="serial port (default: portName from .serialtest/config.json)")
|
||||
ap.add_argument("--baud-rate", type=int, default=115200)
|
||||
ap.add_argument("--settle-ms", type=int, default=8000,
|
||||
help="RX silence that ends the capture")
|
||||
ap.add_argument("--max-wait-ms", type=int, default=30000,
|
||||
help="overall wait-idle timeout")
|
||||
ap.add_argument("--first-wait-ms", type=int, default=12000,
|
||||
help="give up as no_data if the board sent ZERO bytes this long "
|
||||
"after flash started the wait (0 = same as --max-wait-ms)")
|
||||
ap.add_argument("--flash-cmd", default="",
|
||||
help="flash command (default: MDK-ARM/download.bat; pass reset.bat for reset-only)")
|
||||
ap.add_argument("--flash-timeout", type=int, default=180,
|
||||
help="flash subprocess timeout in seconds")
|
||||
ap.add_argument("--required", default=REQUIRED_DEFAULT,
|
||||
help="required markers, ';' separated")
|
||||
ap.add_argument("--forbidden", default=FORBIDDEN_DEFAULT,
|
||||
help="forbidden patterns, ';' separated")
|
||||
ap.add_argument("--require-suite-summary", default="",
|
||||
help="suite names that MUST appear as '=== <name>: N/M PASSED (failed=0) ===' "
|
||||
"with N>0, ';' separated (T-2). Empty = off")
|
||||
ap.add_argument("--require-selftest-pass", action="store_true",
|
||||
help="jointly judge /log/selftest.log (S7): missing/empty = PASS, "
|
||||
"any 64B record = FAIL")
|
||||
ap.add_argument("--selftest-log", default="",
|
||||
help="local copy of /log/selftest.log (skips device-side fetch when given)")
|
||||
ap.add_argument("--device-ip", default="192.168.1.100",
|
||||
help="device IP for fetching /log/selftest.log via FTP (lftpd anonymous)")
|
||||
ap.add_argument("--out-dir", default=str(Path(tempfile.gettempdir()) / "serial-boot-test"))
|
||||
ap.add_argument("--show-tail-lines", type=int, default=15)
|
||||
ap.add_argument("--fw-hex", default=str(DEFAULT_HEX),
|
||||
help="built image for the firmware-identity stamp and the readback "
|
||||
"compare (default: latest build hex)")
|
||||
ap.add_argument("--no-fw-check", action="store_true",
|
||||
help="do not require the 'fw build: <DATE> <TIME>' stamp in the boot log")
|
||||
ap.add_argument("--no-verify-flash", action="store_true",
|
||||
help="skip the ST-LINK -CmpFile readback (AGENTS.md makes it mandatory)")
|
||||
ap.add_argument("--readback-timeout", type=int, default=120,
|
||||
help="timeout for the readback compare, seconds")
|
||||
ap.add_argument("--max-boot-count", type=int, default=1,
|
||||
help="fail if the boot banner ('reset cause:') appears 0 times or more "
|
||||
"than N times (0 = check off)")
|
||||
ap.add_argument("--expect-reset-cause", default="",
|
||||
help="';'-separated substrings the FIRST reset cause must match "
|
||||
"(case-insensitive); empty = report only, no verdict")
|
||||
ap.add_argument("--require-order", default="",
|
||||
help="'>'-separated markers that must appear in that line order "
|
||||
"(catches out-of-order boot steps); empty = off")
|
||||
ap.add_argument("--done-marker", default="net init: OK",
|
||||
help="marker whose line marks boot completion (for --max-boot-ms); "
|
||||
"empty = don't measure")
|
||||
ap.add_argument("--max-boot-ms", type=int, default=0,
|
||||
help="fail if time from first line to --done-marker exceeds N ms "
|
||||
"(0 = measure and report only)")
|
||||
ap.add_argument("--raw-log", action="store_true",
|
||||
help="write payload-only boot log (no '[+Nms]' relative-time prefix)")
|
||||
ap.add_argument("--keep-out", type=int, default=20,
|
||||
help="keep the newest N boot_log_*/selftest_log_* files in --out-dir "
|
||||
"(0 = keep everything)")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
# 串口助手 CLI 用当前解释器(AGENTS.md 统一入口 = 仓库 .venv python)
|
||||
py = sys.executable
|
||||
|
||||
# 端口缺省:单一配置源 .serialtest/config.json(UTF-8,容忍 BOM)
|
||||
port = args.port
|
||||
if not port and CFG_FILE.is_file():
|
||||
try:
|
||||
cfg = json.loads(CFG_FILE.read_text(encoding="utf-8-sig"))
|
||||
port = str(cfg.get("portName") or "")
|
||||
except (OSError, ValueError) as e:
|
||||
print("RUN_TEST_ERROR: cannot parse {}: {}".format(CFG_FILE, e))
|
||||
return 3
|
||||
if not port:
|
||||
print("RUN_TEST_ERROR: no --port given and no portName in {}".format(CFG_FILE))
|
||||
return 3
|
||||
|
||||
# --flash-cmd 允许带参数(如 "MDK-ARM\reset.bat flash"):拆成 argv,
|
||||
# 存在性只查 exe 段(整串当文件名必然 missing,2026-10-10 实测)
|
||||
flash_argv = _split_cmd(args.flash_cmd) if args.flash_cmd else [str(DEFAULT_FLASH)]
|
||||
flash_cmd = Path(flash_argv[0])
|
||||
|
||||
# 预检(对应旧版 missing-file 检查)
|
||||
for p in (SERIAL_TOOL, SCRIPT_DIR / "verify_log.py", flash_cmd):
|
||||
if not p.is_file():
|
||||
print("RUN_TEST_ERROR: missing {}".format(p))
|
||||
return 3
|
||||
|
||||
out_dir = Path(args.out_dir)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
pruned = _prune_out_dir(out_dir, args.keep_out)
|
||||
if pruned:
|
||||
print("OUT: pruned {} old artifact file(s) in {} (keep={})".format(
|
||||
pruned, out_dir, args.keep_out))
|
||||
|
||||
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
boot_txt = out_dir / "boot_log_{}.txt".format(stamp)
|
||||
|
||||
def tool(cmd, *rest, timeout=15):
|
||||
return _tool_cmd(py, cmd, *rest, timeout=timeout)
|
||||
|
||||
print("=== Boot test {} ===".format(stamp))
|
||||
print("Port: {} @ {} settle={}ms max={}ms".format(
|
||||
port, args.baud_rate, args.settle_ms, args.max_wait_ms))
|
||||
|
||||
# 1. open(幂等;未运行则自动拉起常驻 daemon)
|
||||
data, out = tool("open", "--port", port, "--baud", str(args.baud_rate), "--timeout", "15")
|
||||
if data is None:
|
||||
print("RUN_TEST_ERROR: open failed: {}".format(out))
|
||||
return 2
|
||||
|
||||
# 2. clear(新代际;此后数据才属于本次运行)
|
||||
data, out = tool("clear", "--timeout", "15")
|
||||
if data is None:
|
||||
print("RUN_TEST_ERROR: clear failed: {}".format(out))
|
||||
return 2
|
||||
|
||||
# 3. flash(下载 + Reset & Run)。烧录失败不中断:板子可能已复位,
|
||||
# 由采集 + 标记判定结果(与旧版一致)
|
||||
print("FLASH_START: {}".format(" ".join(flash_argv)))
|
||||
t0 = time.monotonic()
|
||||
try:
|
||||
proc = subprocess.run(flash_argv,
|
||||
capture_output=True, timeout=args.flash_timeout)
|
||||
flash_rc = proc.returncode
|
||||
flash_bytes = (proc.stdout or b"") + (proc.stderr or b"")
|
||||
except subprocess.TimeoutExpired as e:
|
||||
flash_rc = -1
|
||||
flash_bytes = (e.stdout or b"") + (e.stderr or b"")
|
||||
elapsed_ms = int((time.monotonic() - t0) * 1000)
|
||||
for line in flash_bytes.decode("utf-8", errors="replace").strip().splitlines()[-12:]:
|
||||
print(line)
|
||||
print("FLASH_RC: {} elapsed_ms={}".format(flash_rc, elapsed_ms))
|
||||
if flash_rc != 0:
|
||||
print("RUN_TEST_WARN: flash rc={} (continuing: board may still have reset)".format(flash_rc))
|
||||
|
||||
# 3.5 固件身份戳:从**刚烧进去的那份 .hex** 里抠出 `fw build: <DATE> <TIME>`,
|
||||
# 追加成 Required 标记 → 芯片里跑的必须就是这份构建。旧 hex 里没有该串
|
||||
# (未重编译)时只 WARN 跳过,不假判 FAIL。
|
||||
required = args.required
|
||||
fw_hex = Path(args.fw_hex)
|
||||
if args.no_fw_check:
|
||||
print("FW ID: skipped (--no-fw-check)")
|
||||
elif not fw_hex.is_file():
|
||||
print("RUN_TEST_WARN: fw hex not found {} -> identity + readback skipped".format(fw_hex))
|
||||
else:
|
||||
fw_stamp = extract_fw_stamp(fw_hex)
|
||||
if fw_stamp is None:
|
||||
print("RUN_TEST_WARN: no '{}' stamp in {} -> identity check skipped "
|
||||
"(old build: run @build first)".format(FW_MARKER, fw_hex.name))
|
||||
else:
|
||||
marker = FW_MARKER + fw_stamp
|
||||
required = (args.required + ";" + marker) if args.required else marker
|
||||
print("FW ID: expected stamp '{}' (from {})".format(marker, fw_hex.name))
|
||||
|
||||
# 4. wait-idle(RX 静默收尾或整体超时)
|
||||
# --first-timeout-ms:首字节门——烧录后板子全程不吐字节时按 no_data 快速失败
|
||||
# (旧版 daemon 常驻累计 rx_bytes>0,no_data 永不触发,只能靠后面"日志里没有
|
||||
# [RX] 行"兜底,报错信息误导成 device silent?)
|
||||
wait_timeout_s = args.max_wait_ms // 1000 + 20
|
||||
wait_args = ["--settle-ms", str(args.settle_ms),
|
||||
"--max-ms", str(args.max_wait_ms),
|
||||
"--timeout", str(wait_timeout_s)]
|
||||
if args.first_wait_ms:
|
||||
wait_args += ["--first-timeout-ms", str(args.first_wait_ms)]
|
||||
data, out = tool("wait-idle", *wait_args, timeout=wait_timeout_s + 15)
|
||||
if data is None:
|
||||
print("RUN_TEST_ERROR: wait-idle failed: {}".format(out))
|
||||
return 2
|
||||
print("WAIT: reason={} rx_bytes={} waited_ms={}".format(
|
||||
data.get("reason"), data.get("rx_bytes"), data.get("waited_ms")))
|
||||
if data.get("reason") == "no_data":
|
||||
print("RUN_TEST_ERROR: no RX data at all (check port / board power / flash)")
|
||||
return 2
|
||||
|
||||
# 5. 提取 [RX] 载荷行(剥掉 "[RX] <ISO时间戳> " 前缀)
|
||||
# 默认给每行加 `[+Nms]` 相对偏移(daemon 毫秒时基,本次采集第一行 = +0ms)。
|
||||
# 为什么加:MCU 侧时间戳只有**秒级**,且开机头几行时钟还没起(`----/--/--`),
|
||||
# 根本量不了耗时;有了它 verify_log 才能查"能开机但慢了几秒"(--max-boot-ms)。
|
||||
# --raw-log 恢复旧格式(纯载荷)。
|
||||
data, out = tool("log", "--tail", "0", "--timeout", "15")
|
||||
if data is None:
|
||||
print("RUN_TEST_ERROR: log read failed: {}".format(out))
|
||||
return 2
|
||||
rx = []
|
||||
t0 = None
|
||||
for l in data.get("lines", []):
|
||||
if not (isinstance(l, str) and l.startswith("[RX] ")):
|
||||
continue
|
||||
rest = l[5:]
|
||||
sp = rest.find(" ")
|
||||
ts_txt = rest[:sp] if sp >= 0 else ""
|
||||
payload = rest[sp + 1:] if sp >= 0 else rest
|
||||
if args.raw_log:
|
||||
rx.append(payload)
|
||||
continue
|
||||
ts = None
|
||||
if ts_txt:
|
||||
try:
|
||||
ts = datetime.fromisoformat(ts_txt)
|
||||
except ValueError:
|
||||
ts = None
|
||||
if ts is None:
|
||||
rx.append(payload) # 时间戳读不出来就不加前缀(不猜)
|
||||
continue
|
||||
if t0 is None:
|
||||
t0 = ts
|
||||
rx.append("[+{}ms] {}".format(int((ts - t0).total_seconds() * 1000), payload))
|
||||
if not rx:
|
||||
print("RUN_TEST_ERROR: no [RX] lines in log (device silent?)")
|
||||
return 2
|
||||
boot_txt.write_bytes(("\r\n".join(rx) + "\r\n").encode("utf-8"))
|
||||
print("Boot log: {} (lines={} gen={} ts={})".format(
|
||||
boot_txt, len(rx), data.get("gen"), "raw" if args.raw_log else "+ms"))
|
||||
|
||||
# 5.6 烧录回读校验(AGENTS.md 铁律:`No difference found` 才算真写进去)。
|
||||
# 必须排在**采集之后**:实测 -CmpFile 连接时会复位目标(板子当场重打一遍开机
|
||||
# 日志),排在采集前会截断开机日志并让启动锚点计数误判成"中途复位"。
|
||||
if args.no_verify_flash:
|
||||
print("READBACK: skipped (--no-verify-flash)")
|
||||
elif not fw_hex.is_file():
|
||||
print("READBACK: skipped (no fw hex: {})".format(fw_hex))
|
||||
else:
|
||||
rb_rc, rb_text = _run_cmd(_split_cmd('{} cmp "{}"'.format(RESET_BAT, fw_hex)),
|
||||
args.readback_timeout)
|
||||
# 判定按**行首**匹配:进度条和 reset.bat 的帮助文本里都含 "No difference found"
|
||||
# 字样,只有 ST-LINK 自己那行才独占行首(实测 `...100%\r\nNo difference found.`)。
|
||||
rb_ok = any(ln.strip().lower().startswith("no difference found")
|
||||
for ln in rb_text.splitlines())
|
||||
if rb_ok:
|
||||
print("READBACK: OK (No difference found) rc={}".format(rb_rc))
|
||||
else:
|
||||
print("READBACK: FAIL -- device flash does NOT match {}".format(fw_hex.name))
|
||||
for line in rb_text.strip().splitlines()[-10:]:
|
||||
print(" | {}".format(line))
|
||||
print("RUN_TEST_ERROR: flash readback mismatch (chip content != built image)")
|
||||
return 2
|
||||
|
||||
# 5.5 自检记录抓取(S7):启用且未提供本地副本时才走 FTP
|
||||
selftest_local = args.selftest_log
|
||||
if args.require_selftest_pass and not selftest_local:
|
||||
selftest_local = _fetch_selftest_log(args.device_ip, out_dir, stamp)
|
||||
|
||||
# 6. 标记校验(含固件身份戳、启动锚点计数、可选的自检记录联合判定)
|
||||
from verify_log import verify
|
||||
rc = verify(str(boot_txt), required, args.forbidden, args.show_tail_lines,
|
||||
suite_summary=args.require_suite_summary,
|
||||
selftest_pass=args.require_selftest_pass,
|
||||
selftest_log=selftest_local,
|
||||
max_boot_count=args.max_boot_count,
|
||||
expect_reset_cause=args.expect_reset_cause,
|
||||
require_order=args.require_order,
|
||||
done_marker=args.done_marker,
|
||||
max_boot_ms=args.max_boot_ms)
|
||||
print("RUN_TEST_RC={}".format(rc))
|
||||
return rc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,448 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""verify_log.py - 对抓取的 MCU 调试日志做标记校验(纯标准库,2026-09-20 替代 verify_log.ps1)。
|
||||
|
||||
用于 @test 闭环:烧录后从串口助手日志提取的开机日志交给本脚本 grep 标记,
|
||||
输出结构化判定(格式与旧 PowerShell 版逐字一致,下游文档/习惯不受影响)。
|
||||
|
||||
用法:
|
||||
& '.venv\\Scripts\\python.exe' test/host/verify_log.py --log-path <file>
|
||||
[--required 'marker1;marker2']
|
||||
[--forbidden 'FAIL;ERROR;HardFault;assert;[ERR]']
|
||||
[--require-suite-summary 'Storage Tests;GD5F2GQ5UE Tests']
|
||||
[--require-selftest-pass] [--selftest-log <selftest.log 本地副本>]
|
||||
[--show-tail-lines 40]
|
||||
[--max-boot-count 1] [--boot-marker 'reset cause:']
|
||||
[--expect-reset-cause 'software;power-on']
|
||||
[--require-order 'App init OK>Network Init>net init: OK']
|
||||
[--done-marker 'net init: OK'] [--max-boot-ms 8000] [--json]
|
||||
|
||||
退出码(与旧版一致):
|
||||
0 = PASS(所有 Required 标记找到,Forbidden 零命中,汇总行要求满足)
|
||||
1 = FAIL(缺 Required / 命中 Forbidden / 汇总行要求不满足)
|
||||
2 = ERROR(日志文件不存在/不可读)
|
||||
|
||||
默认 Required 标记对应 STM32F4-Base 启动序列(见 App/app/app_main.c + App/task/net_task.c),
|
||||
按测试用例覆盖,例如:
|
||||
--required 'App init OK;net init: OK;PHY: 100M FULL'
|
||||
--forbidden 'FAIL;ERROR;HardFault;assert;[ERR]'
|
||||
|
||||
语义约定(与旧 PowerShell 版对齐):
|
||||
- Required:大小写敏感子串匹配(全文)
|
||||
- Forbidden:大小写**不敏感**子串匹配、逐行计数(旧版 -like 语义;
|
||||
如 Forbidden 'FAIL' 会命中 "write /log/run.log fail (7)" 这类行)
|
||||
- 固件错误级日志前缀是 `[ERR]`(`DBG_ERROR`,形如 `[时间][ERR][TAG] 消息`),
|
||||
与 `ERROR` 子串**不匹配**——默认 Forbidden 因此显式包含 `[ERR]`,驱动/模块
|
||||
错误行不再漏检(2026-09-21 修复)。测试套件中**预期**出现 `[ERR]` 的用例
|
||||
(如 GD5F DMA 守卫负路径、FatFs 非法名校验)须用 --forbidden 覆盖默认值。
|
||||
- 读取编码:先按 UTF-8,解码失败回退系统 locale 编码(旧版 Get-Content 行为)
|
||||
- 控制台输出重配置为 UTF-8(errors=replace):任何 Windows 控制台代码页
|
||||
都只影响终端显示,不会导致本脚本崩溃
|
||||
- 汇总行(2026-10-01 加,T-2):纯 Required 子串匹配有个盲区——套件启动横幅
|
||||
命中即判 PASS,**套件是否跑到汇总行、断言数是否为 0 完全不可见**(套件中途
|
||||
挂死/复位、或 Phase 宏全关时会被误判为通过)。`--require-suite-summary`
|
||||
要求日志中出现 `=== <名称>: N/M PASSED (failed=0) ===` 且 **N > 0**。
|
||||
- 自检记录联合判定(S7,2026-10 实机窗口):`--require-selftest-pass` 对 /log/selftest.log
|
||||
的本地副本(定长 64B 记录,格式见 App/log/sys_log.h)判定。自检仅故障写盘(v1 不记
|
||||
通过项):**文件缺失 / 0 字节 = 无故障 → PASS**;存在任何一条记录 = 有故障 → FAIL
|
||||
(退出码 1),并逐条打印时间戳 + 描述。抓取失败(设备不可达 / FTP 错误等)由调用方
|
||||
(run_boot_test.py)先打 WARN,本地文件按缺失处理 → 按上述规则不判整体 FAIL。
|
||||
- 启动锚点计数(2026-10-10):`--max-boot-count N`(run_boot_test.py 默认传 1)统计
|
||||
`reset cause:` 锚点出现次数——**0 次** = 采集没从复位开始(标记全中也可能只是上一轮
|
||||
的残留日志);**>N 次** = 板子在采集窗口内自己复位过(IWDG 咬狗 / Fault 后
|
||||
`NVIC_SystemReset` / 掉电重启),此前这类运行只要不命中 Forbidden 就判 PASS。
|
||||
`--expect-reset-cause` 对第一个锚点做原因白名单(大小写不敏感子串),空 = 只报告不判。
|
||||
- Forbidden 命中行(2026-10-10):命中时逐行打印**行号 + 内容**(最多 6 行,超出报剩余数)。
|
||||
旧实现只说 `forbidden pattern: [ERR] (3 line(s))`,人还得自己再 grep 一遍才知道是哪几行
|
||||
——而"是哪几行"恰恰是定位要的东西。
|
||||
- 标记顺序(`--require-order 'a>b>c'`):按**首次出现的行序**校验启动步骤先后。标记全中
|
||||
但顺序颠倒 = 某步在另一步之后补做(屏障失序、重试后补跑),旧实现完全看不见。
|
||||
- 开机耗时(`--done-marker` + `--max-boot-ms`):依赖行首 `[+Nms]` 前缀(run_boot_test.py
|
||||
2026-10-10 起按 daemon 毫秒时基给每行加,MCU 侧时间戳只有秒级且开机头几行根本没有)。
|
||||
量"第一行 → done-marker"的真实耗时,抓"能开机但慢了几秒"(mount 重试 / PHY 重协商)。
|
||||
日志里没有该前缀 → 只 WARN 跳过,不假判 FAIL。
|
||||
- `--json`:同一套判定以 JSON 输出(供 CI / 上层脚本消费;退出码语义不变)。
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import locale
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REQUIRED_DEFAULT = "App init OK;Network Init;net init: OK"
|
||||
FORBIDDEN_DEFAULT = "FAIL;ERROR;HardFault;assert;[ERR]"
|
||||
|
||||
# Forbidden 命中时最多打印几条命中行(够定位又不刷屏;剩余条数会报出来)
|
||||
HIT_LINES_MAX = 6
|
||||
|
||||
# 行首 `[+1234ms] `:run_boot_test.py 按 daemon 毫秒时基给每行加的相对偏移
|
||||
# (MCU 侧时间戳只有秒级,且开机头几行时钟还没起 → 量不了耗时)
|
||||
_TS_OFF_RE = re.compile(r"^\[\+(\d+)ms\]\s")
|
||||
|
||||
# 每轮启动的锚点行(App/app/app_main.c:`reset cause: <原因> (CSR=0x…)`)。
|
||||
# 一次采集里出现 >1 次 = 板子在采集窗口内自己复位过(IWDG 咬狗 / Fault 后
|
||||
# NVIC_SystemReset / 掉电重启),此前只有 Forbidden 命中时才被发现,
|
||||
# "跑完 PASS 但中途重启过"完全不可见(2026-10-10 加)。
|
||||
BOOT_MARKER_DEFAULT = "reset cause:"
|
||||
|
||||
# 套件汇总行:`=== Storage Tests: 3/3 PASSED (failed=0) ===`
|
||||
# 名称可含空格;N/M 为十进制;failed 允许前导 0。宽松容忍前后空白与
|
||||
# 日志时间戳前缀(`[2026-10-01 12:00:00.000]`)——故用 search 而非 match。
|
||||
_SUMMARY_RE = re.compile(
|
||||
r"===\s*(?P<name>[^:=]+?)\s*:\s*(?P<passed>\d+)\s*/\s*(?P<total>\d+)\s+"
|
||||
r"PASSED\s*\(\s*failed\s*=\s*(?P<failed>\d+)\s*\)\s*==="
|
||||
)
|
||||
|
||||
|
||||
def _split_list(spec):
|
||||
"""';' 分隔列表 → 去空白、去空项。"""
|
||||
return [p.strip() for p in spec.split(";") if p.strip()]
|
||||
|
||||
|
||||
def _read_lines(path):
|
||||
raw = Path(path).read_bytes()
|
||||
for enc in ("utf-8", locale.getpreferredencoding(False)):
|
||||
try:
|
||||
return raw.decode(enc).splitlines()
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
return raw.decode("utf-8", errors="replace").splitlines()
|
||||
|
||||
|
||||
def _reset_cause_of(line, marker):
|
||||
"""从 `reset cause: software (NVIC_SystemReset) (CSR=0x14000000)` 取原因文本。"""
|
||||
i = line.find(marker)
|
||||
if i < 0:
|
||||
return ""
|
||||
tail = line[i + len(marker):].strip()
|
||||
j = tail.find(" (CSR=")
|
||||
return tail[:j] if j > 0 else tail
|
||||
|
||||
|
||||
def verify(log_path, required=REQUIRED_DEFAULT, forbidden=FORBIDDEN_DEFAULT,
|
||||
show_tail=40, out=print, suite_summary="",
|
||||
selftest_pass=False, selftest_log="",
|
||||
boot_marker=BOOT_MARKER_DEFAULT, max_boot_count=0,
|
||||
expect_reset_cause="", require_order="", done_marker="",
|
||||
max_boot_ms=0, as_json=False):
|
||||
"""标记校验核心(可被 run_boot_test.py 复用)。返回退出码 0/1/2。
|
||||
|
||||
suite_summary: ';' 分隔的套件名列表。非空时,要求日志中每个套件都出现
|
||||
`=== <名称>: N/M PASSED (failed=0) ===` 且 **N > 0**。
|
||||
selftest_pass: True 时对 selftest_log(/log/selftest.log 本地副本)联合判定:
|
||||
文件缺失 / 0 字节 = 无故障(PASS);存在任何 64B 记录 = FAIL(S7)。
|
||||
selftest_log: 自检记录文件本地路径(空串 = 按文件缺失处理)。
|
||||
boot_marker / max_boot_count(2026-10-10):统计启动锚点出现次数。
|
||||
max_boot_count>0 时启用:0 次 = 采集没从复位开始;>N 次 = 板子
|
||||
在采集窗口内自己复位过(IWDG / Fault 后 NVIC_SystemReset)。
|
||||
expect_reset_cause: ';' 分隔的原因子串白名单,只对**第一个**锚点生效(大小写不敏感)。
|
||||
空 = 只报告不判定。
|
||||
require_order: '>' 分隔的标记序列(2026-10-10),按**首次出现的行序**校验先后;
|
||||
顺序颠倒 = 某步在另一步之后补做(屏障失序 / 重试补跑)。空 = 不查。
|
||||
done_marker / max_boot_ms(2026-10-10):量"第一行 → done_marker"的毫秒耗时
|
||||
(靠行首 `[+Nms]` 前缀);超过 max_boot_ms 判 FAIL。
|
||||
日志无该前缀 → WARN 跳过,不假判。
|
||||
as_json: True = 不打印文本判定,最后输出一个 JSON 对象(退出码不变)。
|
||||
"""
|
||||
res = {"log_path": str(log_path), "lines": 0, "bytes": 0, "required_missing": [],
|
||||
"forbidden_hits": [], "boot": None, "suites_bad": [], "selftest": None,
|
||||
"order_bad": [], "timing": None, "tail": [], "verdict": None, "exit": None}
|
||||
emit = out
|
||||
|
||||
def finish(code):
|
||||
res["exit"] = code
|
||||
res["verdict"] = {0: "PASS", 1: "FAIL", 2: "ERROR"}[code]
|
||||
if as_json:
|
||||
emit(json.dumps(res, ensure_ascii=False, indent=2))
|
||||
return code
|
||||
|
||||
if as_json:
|
||||
out = lambda *a, **k: None # 文本判定静默,结果走 JSON
|
||||
|
||||
p = Path(log_path)
|
||||
if not p.is_file():
|
||||
out("VERIFY_ERROR: log file not found: {}".format(log_path))
|
||||
return finish(2)
|
||||
try:
|
||||
lines = _read_lines(log_path)
|
||||
except OSError:
|
||||
out("VERIFY_ERROR: cannot read log file: {}".format(log_path))
|
||||
return finish(2)
|
||||
|
||||
full = "\n".join(lines)
|
||||
req_list = _split_list(required)
|
||||
forb_list = _split_list(forbidden)
|
||||
summary_list = _split_list(suite_summary)
|
||||
|
||||
out("=== Log verdict: {} ===".format(log_path))
|
||||
out("Lines: {}, bytes: {}".format(len(lines), p.stat().st_size))
|
||||
res["lines"] = len(lines)
|
||||
res["bytes"] = p.stat().st_size
|
||||
|
||||
missing = []
|
||||
for m in req_list:
|
||||
if m in full:
|
||||
out("[OK] required marker found: {}".format(m))
|
||||
else:
|
||||
out("[MISS] required marker NOT found: {}".format(m))
|
||||
missing.append(m)
|
||||
res["required_missing"] = list(missing)
|
||||
|
||||
hits = []
|
||||
for f in forb_list:
|
||||
fl = f.lower()
|
||||
matched = [(i + 1, ln) for i, ln in enumerate(lines) if fl in ln.lower()]
|
||||
if matched:
|
||||
out("[HIT] forbidden pattern: {} ({} line(s))".format(f, len(matched)))
|
||||
# 命中行必须打出来(2026-10-10):旧实现只报 "(3 line(s))",人还得自己
|
||||
# 再 grep 一遍才知道是哪几行——而"哪几行"正是定位所需的那一步
|
||||
for ln_no, ln in matched[:HIT_LINES_MAX]:
|
||||
out(" L{}: {}".format(ln_no, ln.strip()[:200]))
|
||||
if len(matched) > HIT_LINES_MAX:
|
||||
out(" ... {} more".format(len(matched) - HIT_LINES_MAX))
|
||||
hits.append(f)
|
||||
res["forbidden_hits"].append({
|
||||
"pattern": f, "count": len(matched),
|
||||
"lines": [{"no": n, "text": t.strip()[:200]}
|
||||
for n, t in matched[:HIT_LINES_MAX]]})
|
||||
else:
|
||||
out("[OK] forbidden pattern absent: {}".format(f))
|
||||
|
||||
# 启动锚点计数(2026-10-10):抓"跑完但中途复位过"和"采集没从复位开始"
|
||||
boot_issue = False
|
||||
if max_boot_count > 0:
|
||||
banner_lines = [ln for ln in lines if boot_marker in ln]
|
||||
n_boot = len(banner_lines)
|
||||
causes = [_reset_cause_of(ln, boot_marker) for ln in banner_lines]
|
||||
if n_boot == 0:
|
||||
out("[FAIL] no boot banner (marker '{}'): capture did not start at a reset".format(
|
||||
boot_marker))
|
||||
boot_issue = True
|
||||
elif n_boot > max_boot_count:
|
||||
out("[FAIL] boot banner x{} > allowed {} -> board reset itself during capture".format(
|
||||
n_boot, max_boot_count))
|
||||
for ln in banner_lines[:6]:
|
||||
out(" {}".format(ln.strip()))
|
||||
boot_issue = True
|
||||
else:
|
||||
out("[OK] boot banner x{} (marker '{}')".format(n_boot, boot_marker))
|
||||
if causes:
|
||||
out("[INFO] reset cause(s): {}".format("; ".join(causes)))
|
||||
exp = _split_list(expect_reset_cause)
|
||||
if exp:
|
||||
got = causes[0].lower()
|
||||
if any(e.lower() in got for e in exp):
|
||||
out("[OK] reset cause '{}' matches expected {}".format(
|
||||
causes[0], expect_reset_cause))
|
||||
else:
|
||||
out("[FAIL] reset cause '{}' not in expected: {}".format(
|
||||
causes[0], expect_reset_cause))
|
||||
boot_issue = True
|
||||
res["boot"] = {"marker": boot_marker, "count": n_boot, "allowed": max_boot_count,
|
||||
"causes": causes, "issue": boot_issue}
|
||||
|
||||
# 标记顺序(2026-10-10):标记全中但**顺序颠倒** = 某步在另一步之后补做
|
||||
# (屏障失序 / 失败重试补跑)。旧实现只看子串存在性,这类失序完全看不见。
|
||||
order_issue = False
|
||||
if require_order:
|
||||
seq = [s.strip() for s in require_order.split(">") if s.strip()]
|
||||
pos = []
|
||||
for m in seq:
|
||||
idx = next((i for i, ln in enumerate(lines) if m in ln), -1)
|
||||
pos.append((m, idx))
|
||||
absent = [m for m, i in pos if i < 0]
|
||||
if absent:
|
||||
out("[FAIL] require-order: marker(s) not found: {}".format("; ".join(absent)))
|
||||
order_issue = True
|
||||
else:
|
||||
bad = [(pos[k][0], pos[k + 1][0]) for k in range(len(pos) - 1)
|
||||
if pos[k][1] > pos[k + 1][1]]
|
||||
if bad:
|
||||
out("[FAIL] marker order violated (expected {})".format(require_order))
|
||||
for a, b in bad:
|
||||
out(" '{}' first appeared AFTER '{}'".format(a, b))
|
||||
for m, i in pos:
|
||||
out(" L{}: {}".format(i + 1, m))
|
||||
order_issue = True
|
||||
else:
|
||||
out("[OK] marker order ok: {}".format(require_order))
|
||||
res["order_bad"] = [m for m, i in pos if i < 0] + [
|
||||
"{}>{}".format(a, b) for a, b in
|
||||
[(pos[k][0], pos[k + 1][0]) for k in range(len(pos) - 1) if pos[k][1] > pos[k + 1][1]]]
|
||||
|
||||
# 开机耗时(2026-10-10):行首 [+Nms] = daemon 毫秒时基的相对偏移
|
||||
# (MCU 侧时间戳只有秒级,且开机头几行时钟还没起,量不出耗时)。
|
||||
timing = None
|
||||
timing_issue = False
|
||||
offs = []
|
||||
for i, ln in enumerate(lines):
|
||||
m = _TS_OFF_RE.match(ln)
|
||||
if m:
|
||||
offs.append((i, int(m.group(1))))
|
||||
if offs:
|
||||
timing = {"ts_lines": len(offs), "first_ms": offs[0][1], "last_ms": offs[-1][1],
|
||||
"span_ms": offs[-1][1] - offs[0][1]}
|
||||
out("[INFO] capture span: +{}ms .. +{}ms ({} timestamped line(s))".format(
|
||||
timing["first_ms"], timing["last_ms"], timing["ts_lines"]))
|
||||
if done_marker:
|
||||
di = next((i for i, ln in enumerate(lines) if done_marker in ln), -1)
|
||||
if di < 0:
|
||||
out("[WARN] done marker '{}' not found -> boot time not measured".format(
|
||||
done_marker))
|
||||
else:
|
||||
doff = next((off for i, off in offs if i >= di), None)
|
||||
if doff is None:
|
||||
out("[WARN] done marker line has no [+Nms] prefix -> not measured")
|
||||
else:
|
||||
boot_ms = doff - offs[0][1]
|
||||
timing["boot_ms"] = boot_ms
|
||||
timing["done_marker"] = done_marker
|
||||
out("[INFO] boot time to '{}': {} ms".format(done_marker, boot_ms))
|
||||
if max_boot_ms > 0 and boot_ms > max_boot_ms:
|
||||
out("[FAIL] boot time {} ms > limit {} ms".format(boot_ms, max_boot_ms))
|
||||
timing_issue = True
|
||||
elif done_marker or max_boot_ms > 0:
|
||||
out("[WARN] no [+Nms] line prefix in this log -> timing checks skipped "
|
||||
"(run_boot_test.py adds it; --raw-log turns it off)")
|
||||
res["timing"] = timing
|
||||
|
||||
# 套件汇总行解析(T-2):抓"没跑到汇总行 / 断言数为 0"的盲区
|
||||
bad_summary = []
|
||||
if summary_list:
|
||||
parsed = {}
|
||||
for ln in lines:
|
||||
m = _SUMMARY_RE.search(ln)
|
||||
if m:
|
||||
parsed.setdefault(m.group("name").strip(), []).append(m)
|
||||
for name in summary_list:
|
||||
cands = parsed.get(name)
|
||||
if not cands:
|
||||
out("[MISS] suite summary NOT found: '=== {}: N/M PASSED (failed=0) ==='".format(name))
|
||||
bad_summary.append(name)
|
||||
continue
|
||||
ok = False
|
||||
for m in cands:
|
||||
passed = int(m.group("passed"))
|
||||
total = int(m.group("total"))
|
||||
failed = int(m.group("failed"))
|
||||
if failed != 0:
|
||||
out("[FAIL] suite summary reports failures: {} -> {}/{} PASSED (failed={})".format(
|
||||
name, passed, total, failed))
|
||||
continue
|
||||
if total == 0:
|
||||
out("[FAIL] suite summary total is 0 (no assertion ran): '{}'".format(name))
|
||||
continue
|
||||
if passed != total:
|
||||
out("[FAIL] suite summary inconsistent: {} -> {}/{} PASSED".format(
|
||||
name, passed, total))
|
||||
continue
|
||||
out("[OK] suite summary ok: {} -> {}/{} PASSED".format(name, passed, total))
|
||||
ok = True
|
||||
if not ok:
|
||||
bad_summary.append(name)
|
||||
res["suites_bad"] = list(bad_summary)
|
||||
|
||||
# 自检记录联合判定(S7):/log/selftest.log 定长 64B 记录(格式见 App/log/sys_log.h),
|
||||
# 仅故障写盘:文件缺失 / 0 字节 = 无故障 → PASS;存在任何一条记录 → FAIL
|
||||
selftest_fail = False
|
||||
if selftest_pass:
|
||||
sp = Path(selftest_log) if selftest_log else None
|
||||
if sp is None or not sp.is_file() or sp.stat().st_size == 0:
|
||||
out("[OK] selftest: no fault record (file missing or empty)")
|
||||
res["selftest"] = {"path": str(sp) if sp else None, "faults": 0, "records": []}
|
||||
else:
|
||||
raw = sp.read_bytes()
|
||||
n = len(raw) // 64
|
||||
if len(raw) % 64:
|
||||
out("[WARN] selftest log has {} trailing incomplete bytes (ignored)".format(len(raw) % 64))
|
||||
recs = []
|
||||
for i in range(n):
|
||||
rec = raw[i * 64:(i + 1) * 64]
|
||||
ts = rec[0:19].decode("ascii", errors="replace")
|
||||
desc = rec[31:59].split(b"\0", 1)[0].decode("utf-8", errors="replace")
|
||||
recs.append({"no": i, "ts": ts, "desc": desc})
|
||||
res["selftest"] = {"path": str(sp), "faults": n, "records": recs}
|
||||
if n > 0:
|
||||
selftest_fail = True
|
||||
out("[FAIL] selftest: {} fault record(s) in {}".format(n, selftest_log))
|
||||
for r in recs:
|
||||
out(" #{}: {} {}".format(r["no"], r["ts"], r["desc"]))
|
||||
else:
|
||||
out("[OK] selftest: no fault record (0 complete records)")
|
||||
|
||||
if show_tail > 0 and lines:
|
||||
res["tail"] = lines[-show_tail:]
|
||||
out("")
|
||||
out("=== Last {} line(s) of log ===".format(show_tail))
|
||||
for ln in lines[-show_tail:]:
|
||||
out(ln)
|
||||
|
||||
out("")
|
||||
if (missing or hits or bad_summary or selftest_fail or boot_issue
|
||||
or order_issue or timing_issue):
|
||||
out("VERDICT: FAIL")
|
||||
return finish(1)
|
||||
out("VERDICT: PASS")
|
||||
return finish(0)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
# 控制台输出固定 UTF-8:Windows 代码页(GBK/UTF-8 终端)均不会让本脚本崩溃
|
||||
for stream in (sys.stdout, sys.stderr):
|
||||
try:
|
||||
stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ap = argparse.ArgumentParser(description="MCU debug log marker check (pure stdlib)")
|
||||
ap.add_argument("--log-path", required=True, help="boot log file to check")
|
||||
ap.add_argument("--required", default=REQUIRED_DEFAULT,
|
||||
help="required markers, ';' separated (case-sensitive)")
|
||||
ap.add_argument("--forbidden", default=FORBIDDEN_DEFAULT,
|
||||
help="forbidden patterns, ';' separated (case-insensitive)")
|
||||
ap.add_argument("--require-suite-summary", default="",
|
||||
help="suite names that MUST appear as '=== <name>: N/M PASSED (failed=0) ===' "
|
||||
"with N>0, ';' separated (T-2). Empty = off")
|
||||
ap.add_argument("--show-tail-lines", type=int, default=40,
|
||||
help="show last N log lines (0 = off)")
|
||||
ap.add_argument("--require-selftest-pass", action="store_true",
|
||||
help="judge /log/selftest.log local copy (S7): "
|
||||
"missing/empty = PASS, any 64B record = FAIL")
|
||||
ap.add_argument("--selftest-log", default="",
|
||||
help="local copy path of /log/selftest.log (with --require-selftest-pass)")
|
||||
ap.add_argument("--boot-marker", default=BOOT_MARKER_DEFAULT,
|
||||
help="per-boot banner substring (default '{}')".format(BOOT_MARKER_DEFAULT))
|
||||
ap.add_argument("--max-boot-count", type=int, default=0,
|
||||
help="fail if the boot banner appears 0 times or more than N times "
|
||||
"(0 = check off). run_boot_test.py passes 1 by default")
|
||||
ap.add_argument("--expect-reset-cause", default="",
|
||||
help="';'-separated substrings the FIRST reset cause must match "
|
||||
"(case-insensitive). Empty = report only, no verdict")
|
||||
ap.add_argument("--require-order", default="",
|
||||
help="'>'-separated markers that must appear in that line order "
|
||||
"(e.g. 'App init OK>Network Init>net init: OK'). Empty = off")
|
||||
ap.add_argument("--done-marker", default="",
|
||||
help="marker whose line marks boot completion, for --max-boot-ms")
|
||||
ap.add_argument("--max-boot-ms", type=int, default=0,
|
||||
help="fail if time from first line to --done-marker exceeds N ms "
|
||||
"(needs '[+Nms]' line prefix; 0 = off)")
|
||||
ap.add_argument("--json", action="store_true",
|
||||
help="emit one JSON object instead of the text verdict (exit code unchanged)")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
return verify(args.log_path, args.required, args.forbidden,
|
||||
args.show_tail_lines, suite_summary=args.require_suite_summary,
|
||||
selftest_pass=args.require_selftest_pass,
|
||||
selftest_log=args.selftest_log,
|
||||
boot_marker=args.boot_marker,
|
||||
max_boot_count=args.max_boot_count,
|
||||
expect_reset_cause=args.expect_reset_cause,
|
||||
require_order=args.require_order,
|
||||
done_marker=args.done_marker,
|
||||
max_boot_ms=args.max_boot_ms,
|
||||
as_json=args.json)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -10,7 +10,7 @@
|
||||
1. 改 test_config.h 启用该套件(其它全注释)
|
||||
2. @build(0 错 0 警,否则跳过该套件)
|
||||
3. 烧录(MDK-ARM/download.bat,Reset & Run)
|
||||
4. @test 采集 + 判读(verify_log.py,含套件汇总行校验 T-2)
|
||||
4. @test 采集 + 判读(.dsh/skills/serial-boot-test 的 verify_log.py,含套件汇总行校验 T-2)
|
||||
结束后:
|
||||
5. **无条件还原 test_config.h**(try/finally,含异常/KeyboardInterrupt 路径)
|
||||
|
||||
@@ -55,7 +55,7 @@ REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
CONFIG = REPO_ROOT / "test" / "board" / "test_config.h"
|
||||
BUILD_BAT = REPO_ROOT / "MDK-ARM" / "build.bat"
|
||||
DOWNLOAD_BAT = REPO_ROOT / "MDK-ARM" / "download.bat"
|
||||
RUN_BOOT = REPO_ROOT / "test" / "host" / "run_boot_test.py"
|
||||
RUN_BOOT = REPO_ROOT / ".dsh" / "skills" / "serial-boot-test" / "scripts" / "run_boot_test.py"
|
||||
|
||||
# 套件 -> (宏名, verify_log 需要的汇总行名称, 该套件的 Required 标记, --forbidden 覆盖)
|
||||
# 汇总行名称与套件 `XXX_TEST_REPORT("...")` 里的字符串一致(见各模块 Test_Guide)。
|
||||
@@ -207,7 +207,7 @@ def main(argv=None):
|
||||
print("[WARN] 烧录 rc={}(继续:板子可能已复位)".format(frc))
|
||||
|
||||
# 3) 采集 + 判读(含套件汇总行校验 T-2)
|
||||
cmd = [py, str(RUN_BOOT)]
|
||||
cmd = [py, str(RUN_BOOT), "--flash-cmd", "MDK-ARM\\download.bat"]
|
||||
if required:
|
||||
cmd += ["--required", required]
|
||||
if summary_name:
|
||||
|
||||
+1
-2
@@ -12,7 +12,6 @@
|
||||
| `menu_gen/` | 菜单树代码生成 | UI 菜单代码 | `docs/UI设计/菜单页面设计.md` |
|
||||
| `netlist_semantics/` | 网表语义分析 | 引脚知识库 JSON + 网表报告 | `docs/硬件平台/` |
|
||||
| `refgraph/` | 文件引用关系图 | `docs/images/引用关系图.*` | `docs/架构设计/引用关系图说明.md` |
|
||||
| `serial_tool/` | Python 串口助手 | 日志状态(`state/`) | `docs/测试说明/串口助手使用指南.md` |
|
||||
|
||||
## 使用方式
|
||||
|
||||
@@ -27,7 +26,7 @@
|
||||
& 'tools\refgraph\gen_refgraph.bat'
|
||||
|
||||
# 串口助手(常驻 daemon)
|
||||
& '.venv\Scripts\python.exe' tools/serial_tool/serial_tool.py
|
||||
& '.venv\Scripts\python.exe' .dsh/skills/serial-assistant/scripts/serial_tool.py
|
||||
```
|
||||
|
||||
## ⚠️ 约定
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
# tools/serial_tool/ — Python 串口助手
|
||||
|
||||
> **常驻 daemon + CLI** 架构的串口工具:后台持续收串口日志,CLI 按需查询/发送,
|
||||
> 避免反复开关串口导致日志丢失(台架与测试的主力观测工具)。
|
||||
|
||||
## 文件一览
|
||||
|
||||
| 文件/目录 | 作用 |
|
||||
|---|---|
|
||||
| `serial_tool.py` | 串口助手主程序(daemon 与 CLI 合一) |
|
||||
| `state/` | **运行时状态目录** |
|
||||
| `state/serial.log` | 串口原始日志(`stack_report.py -s` 的输入之一) |
|
||||
| `state/daemon.log` | daemon 自身运行日志 |
|
||||
| `state/meta.json` | daemon 元数据(端口、状态等) |
|
||||
|
||||
## 使用方式
|
||||
|
||||
```bash
|
||||
# 打开串口(daemon 自动拉起;端口缺省取 .serialtest/config.json,不必带参数)
|
||||
& '.venv\Scripts\python.exe' tools/serial_tool/serial_tool.py open
|
||||
|
||||
# 看有哪些口 / 配置口是否还在位(单一候选时回 hint)
|
||||
& '.venv\Scripts\python.exe' tools/serial_tool/serial_tool.py ports
|
||||
|
||||
# 读日志(先过滤后截尾)/ 发送 / 等静默
|
||||
& '.venv\Scripts\python.exe' tools/serial_tool/serial_tool.py log --tail 50 --grep "App init OK"
|
||||
& '.venv\Scripts\python.exe' tools/serial_tool/serial_tool.py send "<命令>"
|
||||
& '.venv\Scripts\python.exe' tools/serial_tool/serial_tool.py wait-idle --settle-ms 8000 --max-ms 30000
|
||||
|
||||
# 状态 / 关闭
|
||||
& '.venv\Scripts\python.exe' tools/serial_tool/serial_tool.py status
|
||||
& '.venv\Scripts\python.exe' tools/serial_tool/serial_tool.py close
|
||||
```
|
||||
|
||||
> 全部命令输出 JSON、退出码 0/1/2/3;具体子命令以 `--help` 为准。
|
||||
> 抓开机日志的顺序是 `open` → `clear` → 烧录/复位 → `wait-idle` → 读日志(采集必须先于烧录)。
|
||||
|
||||
## ⚠️ 注意
|
||||
|
||||
- **单写者**:串口同一时刻只能被一个进程占用——别同时用本工具和其它串口程序
|
||||
- `state/serial.log` 是 `[STACK]` 运行时水位的来源,`scripts/stack_report.py -s` 直接读它
|
||||
- `state/` 是运行时数据,**不入库**
|
||||
|
||||
## 相关文档
|
||||
|
||||
- `docs/测试说明/串口助手使用指南.md` — 命令用法 / 工作流 / 排障
|
||||
- `docs/测试说明/串口助手设计说明.md` — 契约 / daemon 行为 / 陷阱
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user