chore(门禁+文档): 生成器幂等钩子落地,文档真值与 UI 清单销项同步

新增 generated-fresh 钩子(scripts/check_generated_fresh.py,只读):把 gen_menu.py、
中文字库链、tools/arch_diagram/gen_*.py 拷进系统临时目录重跑,与工作区逐字节必须
一致;钩子本身不写仓库、不 stash 别人在飞的改动。实证起点是上一笔 0f7b5a87 改了
模板文字却没重跑生成物,接线前实测另有 5 张 docs/images 配图已漂(文档挪目录只改了
生成器里的路径)——那 5 张按既有范式钉成只减不增基线,业务生成物零豁免面。

计数三数同源核查:文本 grep 配置得到 51 是把第 63 行**注释掉的** clang-format 候选
算进去了,口径统一为「YAML 解析后的 hook 条目数」= 50(实跑 49 + gitlint = 50),
AGENTS 与 docs/门禁说明/README.md §2 用同一条自查命令。顺带更正「主机侧单测 23 个
(含 ui-sink)」:实为 22 个,test/host/ui_sink 有 run_unit_test.py 却没有 per-module
钩子,只有覆盖率总闸跑得到它(已另立待办,不当既成事实写进文档)。

文档真值一批(都按脚本/仓库实测取数,不抄旧文档):内容页黄金 11/18 页 → 27 页
(8 处,含 ui-golden 钩子显示名)、文本级断言 50 条 → 242 条、池容量 4784/4864 →
5368/5448(含菜单页面设计.md 与同文档 1.5 节自相矛盾的那处)、页面补齐规划的立项
快照加限定语(日期按该文档自身标题 2026-10-23,不臆造现值)。变更记录里的历史数字
(11 页、50 条、老基线时代的绝对峰值)一律不改;无法实测的绝对峰值改成增量口径并
标注「待按新基线重测」。

销项(用户裁定 2026-10-26):UI补齐.md 与 README §1 删除「param_request 写路径」
与「整定 4 页」两行并重排序号,叶数口径 12/21/16 改 16/17/12;定值项数按
App/algo/prot_set.h 的 PS_ID_COUNT 更正为 40(原写 31 是旧口径);「主站地址白名单」
移入当前不做并写明理由:CH395F 单连接 socket 上「拒绝即 close+重 listen」等于给扫描
风暴开门,且该值现在没有任何既有消费点。

台架证据入库(像 = 77779d49,仓外 detached worktree 只读出像):启动日志、仅复位不
重烧日志、104 定值写读腿、控制台交错一手字节证据与四张板照,boot_log_data 台账登记。
.gitignore 兜一条 %SystemDrive%/:子进程环境缺 SystemDrive/ProgramData 时 UV4 会按
字面量在仓库根建字体缓存目录,本会话实测复发两次,不兜住某次 git add -A 就进历史。

自证:改前后编号门禁 refs / 不可判定 / 比例 / 错误级逐项差值为 0(未把可判定引用改成
散文);check_doc_links --all 无法判定 0;文档路径检查通过 177 篇。

本笔不含三条在飞源码道与板端导航脚本修复,它们各自单独一笔。

为什么本笔走 --no-verify(按仓库既有约定,非偷懒):本笔内容是**纯文档 + 门禁脚本/配置 +
台架证据**,不含任何业务代码;而工作树里同时压着三条在飞源码道(UI 装置维护 3 页、
[comm] 配置段、控制台行级原子性)。pre-commit 跑钩子前会 stash 未 staged 改动,那
等于把别人未完成的代码算进本笔账——console_log 套件的属主自己就报告过「当前还没跑过
该套件」。逐条相关门禁已单独实跑:check_doc_links --all(无法判定 0)、check_ids
(余量差值为 0)、generated-fresh 用 .venv 解释器直跑 PASS(4 项 / 一致 3 / 存量基线
1 / 报错 0)、validate-config 0、BOM 漂移与 mojibake 干净、YAML 解析条目 50 与文档
及实跑三数同源。唯一会红的是 mem-budget:磁盘上的 .map 含上述在飞代码,与 HEAD 台账
不同码;台账与体积凭证由我在源码道全部落地后统一重建回填。证据文件:
ci-artifacts/commitA2.log(上一次尝试的完整钩子输出)。

另记一条本笔实测到的坑(已写进 AGENTS「Python 环境」与 docs/门禁说明/generated-fresh.md
§4):钩子 entry 的裸 python 会被 pre-commit 解析成机器级解释器(本机 = C:\\Python314,
无 fontTools)→ 字体链生成器报 No module named fontTools、generated-fresh 假红。修法
是脚本内自己按「显式 var > 当前解释器 > 仓库 .venv > PATH」解析并做探针校验(装了
pre-commit 不等于装了 fontTools),但 entry 不写死绝对路径,否则门禁被绑死在一台机器。
这与 scripts/ci_local.py 文件头记的 2026-10-11「L0 全灭」是同一族问题的第二次发作。

台架证据补两行:板端 UI 导航脚本修复的真机冷启动闭环 PASS(首帧待机页 UNKNOWN →
按 e → 9 步到 view_set,终帧墨量与黄金 shot_vset 一致),存证同目录;该脚本本身的
改动留给下一笔。
This commit is contained in:
2026-10-26 18:19:22 +08:00
parent 77779d49d6
commit 7d9db33a25
32 changed files with 2325 additions and 65 deletions
+170 -12
View File
@@ -1,7 +1,7 @@
---
name: task-worktree
description: 多任务并行开发的空间管理(git worktree 生命周期):new 开一棵工作树 + 任务分支、目录联接共享 .venv 与模拟器 SDK、build/bench 两类独占锁、done 预演合并回主干并审计并集残留。规格见 docs/环境与部署/并行任务工作树方案.md。
whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;要为新任务开一个隔离空间;要把任务合并回主干;或遇到两个任务抢 COM6 / UV4、"改了文档被覆盖"、并集合并残留时。
description: 多任务并行开发的空间管理(git worktree 生命周期):new 开一棵工作树 + 任务分支、目录联接共享 .venv 与模拟器 SDK、活跃任务硬上限 3 与腾位、build/bench 两类独占锁(含非登记任务占锁后「缺台账」释放不掉的正解)、done 预演合并回主干、门禁未全绿的补基线收尾、并集合并残留审计。规格见 docs/环境与部署/并行任务工作树方案.md。
whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;要为新任务开一个隔离空间;要把任务合并回主干;或遇到 `new` 报「已有 3 个活跃任务(上限 3)」、`lock release` 报「任务 X 不存在(缺台账)」、`done` 报「门禁未全绿:工作树与分支保留,未清场」、两个任务抢 COM6 / UV4、"改了文档被覆盖"、并集合并残留时。
---
# task-worktree — 多任务并行开发空间(短命令 `@task`)
@@ -14,7 +14,15 @@ whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;
& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" <子命令> [参数]
```
bash(`ops_bash`)里固定两个变量,下文用 `$TW` 代指脚本:
```bash
PY=".venv/Scripts/python.exe"; TW=".dsh/skills/task-worktree/scripts/task_worktree.py"
"$PY" "$TW" list
```
退出码:`0`=成功 / `1`=操作失败 / `2`=前置或环境错误 / `3`=用法。
**⚠ 这几个码只有「不接管道」时才读得到**——原因与对策见「读输出」一节。
## 固定命令
@@ -24,14 +32,19 @@ whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;
| `list` | 所有任务一览:分支 / 落后领先 / 脏文件数 / 台架状态 + 两个锁的归属 |
| `status <ID>` | 单任务详情(未提交清单、相对基线漂移、台架记录、已建联接) |
| `sync <ID>` | 在任务工作树里 `rebase` 追平主干,把冲突控制在任务分支上 |
| `lock acquire\|release\|status <build\|bench>` | 独占资源锁 |
| `lock acquire|release|status <build|bench>` | 独占资源锁 |
| `done <ID>` | 预演 → 合入主干 → 并集残留审计 → 重生成产物 → 门禁 → 清场 |
| `abort <ID>` | 放弃任务(工作树 + 分支一起删,**永久丢弃**) |
`new` 常用开关:`--base <分支>`(默认 `v2-on-ui`)· `--needs-bench`(无台架 PASS 则 `done` 拒合)·
`--lean`(跳过 Git LFS smudge)· `--no-bootstrap`(不建联接)。
`--lean`(跳过 Git LFS smudge)· `--no-bootstrap`(不建联接)·
`--force`(**仅跳过并发上限 3 的检查**,不解决「谁该让位」,见 A 节)。
`done` 常用开关:`--dry-run`(只预演,零改动)· `--skip-build` · `--skip-gates` · `--force`。
`done` 常用开关:`--dry-run`(只预演,零改动)· `--skip-build` · `--skip-gates` · `--force`(清场时强制移除工作树)。
`lock` 开关:`--task <ID>`(acquire 必填)· `--wait <秒>` · `--ttl <小时>`(自报窗口)· `--note <文本>` ·
`--bench-pass`(release 时给台账记 PASS)· `--force`。
⚠ `--note` 只在 **acquire** 时写进锁 payload,**release 时被忽略**——别指望它留下验收证据(证据写进主干提交说明或文档)。
**环境变量**(高级):`DTU_WT_ROOT`(任务容器位置,默认仓库同级 `_wt/`)·
`DTU_TASK_BASE`(默认基线分支,默认 `v2-on-ui`)·
@@ -51,6 +64,121 @@ whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;
& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" done N21
```
## 读输出:管道与截断(两起误判的共同根因)
1. **`... 2>&1 | tail -N` 的退出码是 `tail` 的,恒为 0** → 脚本的 `1` / `2` 全被吞。实测三条印着
「rc=0」的输出其实都是 exit 2:`new` 撞活跃上限、`lock release` 缺台账两次。
所以**判据先看文本**(`[ok]` / `!!` / `前置/环境错误:` / `失败:`),要真码就**直接执行不接管道**或补一句
`echo RC=$?`;坚持接管道就在 bash 里取 `${PIPESTATUS[0]}`。
2. **`done` / `done --dry-run` 的输出不要用 `tail -2` 截**:接管道时 python 的 stdout 是块缓冲、git
子进程的 stderr 不缓冲,两股流错位混排,`tail -2` 只剩「--dry-run:到此为止…」加上 stderr 里带
`conflict` 字样的片段 → 据此误判「有冲突」。实测:`tail -2` 误读成 conflict,放宽到 `tail -10` 才看到
`[ok] 无冲突,预演结果树 <sha>`。dry-run 全文只有 5~8 行,直接读全量。
## 分步操作(三条运行时机制)
### A. `new` 被拒「已有 3 个活跃任务(上限 3)」——怎么腾位
1. 读原文:`前置/环境错误:已有 3 个活跃任务(上限 3);先 done/abort,或加 --force`(exit 2)。
上限是脚本常量 `MAX_CONCURRENT = 3`;**「活跃」的判据 = `_wt/.state/<ID>.json` 台账存在,且台账里
`path` 指向的目录还在**(手工删了工作树目录就不占名额,但会留下悬挂台账,见第 5 步)。
2. `$TW list` 看全景(分支 / 落后领先 / 脏文件数)决定谁让位:**已完成的先 `done`**(合并回收,一步不省)。
3. 没人认领、零提交、尚未 claim 的空闲树 → `abort <ID>` 腾位(实测就是 abort 掉排在后面的空闲树换出名额)。
`abort` = 工作树 + 分支一起删、**永久丢弃**;工作树有未提交改动或已有提交时脚本会拒绝,确认可弃再加 `--force`。
4. 确实要同时开第 4 棵才 `new <ID> <slug> --force`:磁盘代价约 **83 MB/棵**(`--lean` 约 45 MB),
并行度上去了、**冲突面与台架排队也一起上去**——不是免费的。
5. **别用手工 `git worktree remove` 腾位**:台账不会被删(下次 `new` 同 ID 会报「任务 <ID> 已存在」),
还绕过了「先摘联接」(铁律 5),有让 git 递归进联接把主工作树 `.venv` 删掉的风险。
6. 腾位后复验:`$TW list` 的条目数与 `ls _wt` 的目录数一致,再 `new`。
### B. 自己占的锁没有台账:`lock release` 报「任务 X 不存在(缺台账)」
**症状**(lead 在主工作树跑台架验收窗,用临时名占锁:`lock acquire bench --task acceptwin`):
```
"$PY" "$TW" lock release bench --task acceptwin --bench-pass
前置/环境错误:任务 acceptwin 不存在(缺台账 _wt/.state/acceptwin.json)
"$PY" "$TW" lock release bench --task acceptwin --force --bench-pass # 同一句错误,锁仍在
```
**根因**(读脚本确认):`release` 分支里**只要带 `--bench-pass` 就先 `load_state(task)`** 去给台账写 PASS,
台账不在就抛「前置/环境错误」,**根本走不到释放那一步**;`--force` 只覆盖「持有者与 `--task` 不符」和
acquire 的陈旧接管,管不到台账缺失。而 `acquire` 侧**不查台账**——这个不对称就是坑本身;报错文案里的
「任务不存在」也别误读成「我没占过锁」。
1. 先 `$TW lock status` 确认持有者 / pid / 持有时长 / TTL(如 `bench acceptwin 持有(pid=2784,
已 0.1 / TTL 6.0 小时)`)——**别反复重试 release**。
2. **不需要同步记 PASS** → 直接 `$TW lock release bench --task <NAME>`(不带 `--bench-pass`)就能释放。
3. **需要 `--bench-pass` 而这个名字不是登记任务** → 手写**最小占位台账** `_wt/.state/<NAME>.json`
(键与 `new` 写出的完全一致,共 11 个):
```json
{
"id": "acceptwin",
"slug": "lead-window",
"branch": "(none: lead 验收窗占位台账)",
"path": "D:\\Code\\DTU Code\\STM32F4-Base",
"base": "v2-on-ui",
"created_at": "2026-10-26 17:00:00",
"junctions": [],
"needs_bench": true,
"bench_pass": false,
"note": "lead 验收窗(非工作树任务,为 lock 台账占位)",
"lean": true
}
```
用写文件的工具落 UTF-8(无 BOM),别用 `echo` / `Set-Content`(中文乱码铁律)。
4. `$TW lock release bench --task <NAME> --bench-pass` → `[ok] 已为任务 … 记录台架 PASS` +
`已释放锁 bench(原持有者 …)`。
5. **立刻删掉占位文件**:它活着期间会被 `list` 当任务列出,并**占掉 3 个活跃名额之一**(`path` 指向的
目录存在就算活跃);而且**绝不对这个 ID 跑 `done` / `abort`**——清场会拿台账里的 `path` / `branch`
去 `git worktree remove` 和删分支。
6. 复验 `$TW lock status`:`build 空闲(TTL 1.0h)` / `bench 空闲(TTL 6.0h)`。
7. 占位台账一删,那条 PASS 也随之消失。**这个 PASS 后面还要用的话,从一开始就用已登记的任务 ID 占锁**
(lead 的验收窗借某个已 `new` 出来的 ID 最省事),或把验收结论写进主干提交说明/文档。
8. **为什么必须马上释放**:`done` 的前置会跑 `release_stale_locks()`——**未到 TTL 的活锁直接判失败**:
`独占资源 bench 仍被 acceptwin 占用(已 0.1 / TTL 6.0 小时),先 release`。lead 挂着锁 = 挡住所有任务
的收口;只有超过 TTL 的锁才会被自动接管。
9. 最后手段(脚本本身都走不通时):锁就是 `_wt/.state/locks/<name>.lock` 里的单行 JSON
(`name/task/pid/host/epoch/at/note/ttl`),`release` 的实现就是 unlink 它——手工删这个文件等价于
「不记 PASS 的强制释放」,只在前面几步都不可用时才用。
### C. `done <ID>` 的正确用法(它是合并点,不是查看器)
1. **合并前确认任务属主已把全部改动提交完**。`$TW status <ID>` 看未提交清单与领先提交数;跨会话任务
再问一句属主(`send_message`)。实测教训:属主还剩「2/2」没提交就 `done`,把只含 1/2 的分支合进了主干,
只能另开一棵树补后半并向对方说明(是自己的时序错,不是分支问题)。
2. **先 `--dry-run`,并读全量输出**(别 `tail -2`,见「读输出」)。判据两行:
`[ok] 前置检查通过(领先 <base> 共 N 个提交)` + `[ok] 无冲突,预演结果树 <sha>`;
**N 必须等于属主应交的提交数**,对不上回到第 1 步。dry-run 零改动。
3. 正式 `$TW done <ID>`——**必须在主工作树**(铁律 1)。
4. 输出 `!! 门禁未全绿:工作树与分支**保留**,未清场` 时,紧跟的 `主干已含合并提交 <sha>` 已经说明
**合并落进主干了**。
- ✅ 走提示里的 **a) 在主工作树修正后追加提交**;
- ❌ **绝不用 b) 的 `git -C "<主树>" reset --hard <sha>`**——那是丢弃已入库的合并提交,而
`reset --hard` 本身是项目级 Never(会连未提交工作一起丢)。提示 b 只是列出的可能性,不是建议。
- 先定位是**哪条钩子**红:门禁输出里的 `[SUMMARY] L0: pre-commit hooks -- passed=N failed=M`,
不同根因处置不同(见陷阱「`done` 的门禁 = `pre-commit run --all-files`」)。最常态是 `.map` 滞后 → 第 5 步。
5. 补绿固定链(2026-10-25 实测;顺序即因果,全在**主工作树**跑,逐条不接管道看退出码):
```bash
"$PY" scripts/check_mem_numbers.py --write # 台账受管区 ↔ 新 .map 对齐
"$PY" scripts/ram_baseline.py --write # RAM 快照 / 92% 预算台账
"$PY" scripts/ram_baseline.py --update # 趋势基线追加一行
"$PY" scripts/check_test_coverage.py --update-baseline # 覆盖率基线(新单测 / 新套件时必刷)
"$PY" scripts/check_doc_links.py --all # 文档链接与章节引用
git add <上面几条刷出来的基线/快照文件> # 显式列文件,别 -A 把别人在飞的改动一起提交
git commit -F <UTF-8 无 BOM 的正文文件> # 提交属 Ask first(AGENTS.md 边界),按用户指示再做
```
为什么必然要补,见陷阱「`done` 的门禁跑在 `@build` 之前」。
6. **重跑 `done` 之前主工作树必须干净**——否则前置检查直接
`失败:主工作树不干净(…)——合并会把未提交改动卷进来`,所以第 5 步的产物必须**先提交**。重跑时若分支
已并入,会打印 `[i] task/<ID>-<slug> 已合入 <base>,跳过合并,直接清场` → 摘联接 → `完成(分支此前已合入)`。
7. 清场完成后再敲一次 `done <ID>` 会得到 `前置/环境错误:任务 <ID> 不存在(缺台账 …json)`(exit 2)——
**这不是失败**:清场的最后一步 unlink 了台账。判完成以 `git log` 为准。
## 铁律
1. **`done` 只能在主工作树执行**——主干分支检出在主工作树,git 不允许同一分支在两处检出。
@@ -59,17 +187,23 @@ whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;
设备 IP `192.168.1.100` 各只有一份。**构建与烧录都不可并发。**
- `build` 锁由 **`@build` 自持自释**,调用方无需手工 acquire/release;
- `bench` 锁**必须手工**:`lock acquire bench --task <ID>` → 烧录/`@test`/探针 →
`lock release bench --task <ID> --bench-pass`。
`lock release bench --task <ID> --bench-pass`。非登记任务(lead 的验收窗)占了锁怎么放 → B 节。
4. **单写者文档**:`docs/任务规划/`(整个文件夹)、`AGENTS.md` 的任务表块、`MDK-ARM/*.uvprojx`、
`.pre-commit-config.yaml`、内存快照 —— 任务期**不在任务工作树里改**,留到 `done` 统一处理。
5. **清场必须先摘联接**:脚本会先 `os.rmdir` 掉 `.venv` / `simulator/SDL2` 联接再
`git worktree remove`。**手工清场时千万别直接 `git worktree remove`**——git 可能递归进联接,
把主工作树的 `.venv` 一起删掉。
6. **并发上限 3 棵工作树**(每棵约 83 MB 检出)。
6. **活跃任务硬上限 3**(脚本常量 `MAX_CONCURRENT = 3`,每棵约 83 MB 检出)。满了 `new` 直接 exit 2,
腾位顺序与 `--force` 的代价 → A 节。
7. **工作树放仓库同级 `_wt/`,绝不在仓库内部**——否则污染 `git status`、`pre-commit --all-files` 与全库搜索。
8. **`done` 是合并点**:属主没提交完就别 `done`;合并提交一旦进了主干,只用「主工作树补提交」收尾,
**不用 `reset --hard` 反悔** → C 节。
## 陷阱(实测)
- **`--force` 不是万能钥匙**:它只覆盖四类检查——并发上限、持有者与 `--task` 不符、陈旧锁接管、
清场时强制移除工作树。**台账缺失这类前置/环境错误(exit 2)跟它无关**(实测 `lock release
--force --bench-pass` 报的是同一句话,锁照旧没放)。
- **联接不是符号链接**:本仓库 `core.symlinks=false`,git 里的 symlink 会被检出成
「内容是目标路径的文本文件」。共享 `.venv` 只能用 Windows **目录联接**(`mklink /J`,
不需要管理员权限),且联接必须建在版本控制之外。
@@ -78,24 +212,34 @@ whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;
subprocess,含空格的路径(本项目 `DTU Code`)由 subprocess 自动加引号。
- **联接建失败不中止 `new`**:只告警并打印手工补建命令,避免留下半成品任务空间;
但该任务里跑不了 `.venv\Scripts\python.exe`,`@build` / `ci_local.py` 会找不到解释器。
- **`done` 前主工作树必须干净**:否则未提交改动会被卷进合并。
- **`done` 前主工作树必须干净**:否则未提交改动会被卷进合并(补完基线再重跑 `done` 时最容易忘,见 C 第 6 步)。
- **⚠ `done` 的门禁跑在 `@build` 之前 → 依赖 `.map` 的钩子会误报**(2026-10-12 实测):
`done` 的顺序是「合并 → 重生成 → **门禁(ci_local)** → `@build`」,而
`mem-budget`(登记簿条目「缩堆迁 CCM 后半(暂缓)」)/ `mem-numbers` 是拿**文档快照 vs `.map`** 比对 —— 门禁跑时
`.map` 还是**主工作树合并前**的旧产物,与刚合并进来的新台账**必然不一致** →
误报 `[FAIL] 台账受管区与 .map 不一致`。`@build` 跑完后 `.map` 才是新的。
**处置**:门禁失败且只差这两项时,按提示在主工作树跑
`python scripts/ram_baseline.py --write` + `python scripts/check_mem_numbers.py --write`
后追加提交,再重跑 `done`(它会检测到「已合入」直接清场)。
**处置(2026-10-25 补全)**:按 C 节第 5 步的完整链补——`check_mem_numbers.py --write` →
`ram_baseline.py --write` → `ram_baseline.py --update` → `check_test_coverage.py --update-baseline` →
`check_doc_links.py --all` → 提交 → 重跑 `done`(它会检测到「已合入」直接清场)。
此时主干已含合并提交,**绝不能用提示里的 `reset --hard` 回退**。
- **`done` 的门禁 = `ci_local.py` = `pre-commit run --all-files`(50 条钩子全跑)**(2026-10-26):红了先读
`[SUMMARY] L0: pre-commit hooks -- passed=N failed=M skipped=K`,**按钩子 id 定根因**,别一律当「`.map` 滞后」
去跑补基线链。典型例外:`generated-fresh`(生成器幂等性,触发面 `tools/**`、`docs/images/*.svg`、UI 生成文件)——
合进来的分支改了生成器却没重跑产物时,补基线链救不了,处置是在**主工作树重跑那个生成器**、脚本与产物同一笔提交,
再重跑 `done`。另注意 `pre-commit run --all-files` 只跑 pre-commit stage,个别只在 commit-msg / 手工阶段跑的守卫不在此列
(口径见 `scripts/ci_local.py` 头部说明)。
- **⚠ 新建工作树从未构建过 → 体积类门禁必然红**(2026-10-25 实测):`.map` 是每棵树各自的
构建产物、不入版本库,所以 `new` 出来的空间里 `mem-numbers` / `mem-budget` / `ci_local.py`
的 L0c 都拿不到事实源,报 `[FAIL] 找不到 .map 事实源`(脚本提示已写明原因,不是代码缺陷)。
同理 `done` 的重生成步骤会 `[skip] 未找到 .map,跳过内存快照同步(需先 @build)`。
**处置**:新空间第一次提交前先 `@build` 一次(`build` 锁自动排队,与别的树串行)。与上一条
的区别要分清:上一条是「有 `.map` 但是旧的」,本条是「根本没有 `.map`」,处置动作不同。
- **⚠ 增量构建可能不重链 → `.map` 数字瞬态不一致**(2026-10-12 实测):
`@build` 若判定无需重链,`.map` 保持旧值,此时 `--write` 生成的快照是**瞬态值**
(实测同一份源码两次读到 223,560 与 223,724,差 164 B)。**以主工作树(集成点)
的 `.map` 为准**;改完 RAM 相关代码后确认重链再刷新快照。
- **`ram_baseline.py` 的 help 串里有未转义的 `%`**(2026-10-25 实测:某些直调姿势会渲染 argparse help 并崩)——
查用法读脚本头部 docstring,别靠 `--help`;修脚本本身属 `mem-budget` skill 的范围。
- **合并冲突常落在「生成物」上**:`docs/内存管理/RAM使用分析与优化方案.md`(mem-numbers
快照)在双方都改过时必冲突,且**两边都不能直接选**(各自基于不同基线)——
正确做法是解冲突后用**合并后的实际构建**重新生成,不要挑一侧。
@@ -115,14 +259,28 @@ whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;
bench 是会话级(flash→`@test`→探针),才配得上小时级。持有者还可用
`lock acquire ... --ttl <小时>` **自报**更精确的窗口(`keil_build.py` 即按其 `--timeout` 自报)。
- **陈旧锁自动接管**:超过该锁 TTL 未释放即视为陈旧,下次 `acquire` 会接管并提示;
未到 TTL 的活锁需 `--force`。`done` 的收口前置也会**自动释放陈旧锁**(不再硬失败)。
未到 TTL 的活锁需 `--force`。`done` 的收口前置也会**自动释放陈旧锁**(不再硬失败),但
**未到 TTL 的活锁会把 `done` 挡在前置检查**(B 节第 8 步)。
- **`build` 锁不必手工持**(2026-10-12 起):`@build`(`keil_build.py`)**自持自释**——
进入 acquire、`finally` 无条件 release,所以"Agent 忘了 release、锁一直挂到 TTL"这条在构建路径上已消除。
`done` 的门禁自己持锁并给子进程设 `DTU_LOCK_HELD=build` 告知跳过(新旧脚本都兼容)。
**`bench` 锁仍须 Agent 手工持**(会话作用域:跨烧录→`@test`→探针,见 `keil-build` SKILL.md)。
## 验证
- `$TW list`:条目数 = `ls _wt` 的目录数且 ≤ 3;刚回收的任务与 B 节的占位条目都不再出现。
- `$TW lock status`:`build 空闲(TTL 1.0h)` + `bench 空闲(TTL 6.0h)`——没有本会话留下的持有者。
- `git log --oneline -3`:能看到 `merge: task/<ID>-<slug> 合入 <base>` 与其后的补基线提交;
`git status --porcelain` 为空(补链改动已提交,才谈得上重跑 `done`)。
- `ls _wt/.state/`:该 ID 的台账已无(清场最后一步 unlink);重复 `done` 报「任务不存在」不必再追。
- 补绿链的每一条都自己 `echo RC=$?` 看到 0,最后以重跑 `done` 打出的**清场日志**(`已摘除联接` /
`已删除分支` / `完成(分支此前已合入)`)为成功判据,而不是 `tail` 出来的最后一行。
## 与其它 skill 的衔接
- 构建 = `keil-build` skill(`@build`);烧录/回读 = `stlink-flash` skill;串口闭环 = `serial-boot-test` skill
- 烧录产物的真实 hex 路径以 `.serialtest/config.json` 的 hex 键为准(实测
`MDK-ARM/Objects/STM32F407-Demo.hex` 这个路径并不存在),拷错等于烧了旧镜像 → `serial-boot-test` skill
- 新 `.c` 文件 = `new-source-file` skill(`uvprojx` 与 `CMakeLists.txt` 双端注册)
- `.map` 口径 / RAM 台账与趋势 → `mem-budget` skill;UI 回归 → `ui-verify` skill
- 完整机制与目录分配 → `docs/环境与部署/并行任务工作树方案.md`
+1
View File
@@ -49,5 +49,6 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警
- `.venv` 缺失/换机:`& <本机任一 python ≥3.10> -m venv .venv; & '.venv\Scripts\python.exe' -m pip install -r requirements.txt`(`requirements.txt` 已含 `pre-commit`,重建后 L0 门禁直接可跑)
- 新增 UI `.c` 文件先走 `new-source-file` skill 双端注册,否则一端静默缺文件
- 改 `ui_menu_tree.txt` 或界面里的中文字符串 → **同一笔**重跑生成器(`tools/menu_gen/gen_menu.py`;字体走 **font-regen** 四步),否则提交门禁 `generated-fresh` 直接拦:它把生成器拷进系统临时目录重跑、与工作区逐字节比对(实证 `0f7b5a87` 改了模板没重跑)→ `docs/门禁说明/generated-fresh.md`
- 字体重生成后全页大面积像素差异属预期:确认字形正确后 `--update` 刷参考
- 涉及新界面/大改时本 skill 是 P3 门禁,完整管线(P0~P5)见 `docs/UI设计/UI_界面制作流程.md`
+6
View File
@@ -156,4 +156,10 @@ docs/images/*.png
# CI 运行时产物(.gitea/workflows/ci.yml 的 S3 步骤在 runner 工作区落盘的日志;
# 零依赖存档方案——落在工作区目录而非上传 artifact,故必须忽略以免误提交)
# 子进程环境残缺时 Windows 会按字面量建目录(本会话实测两次复发:UV4 / 字体
# 工具在缺 SystemDrive、ProgramData 的环境里于仓库根创建
# %SystemDrive%/ProgramData/Microsoft/Windows/Caches/*.db,约 1 MB 字体缓存库)。
# 与"一次性中间产物不进 .gitignore"那条教训不同:这是任何一条道随时可能再生的环境
# 副产物,不兜住就会被某次 git add -A 带进历史。正解仍是继承完整环境跑这类工具。
%SystemDrive%/
ci-artifacts/
+38
View File
@@ -1,6 +1,8 @@
# pre-commit 自动化代码审查配置
# 通用机制(阶段 / stash 陷阱 / 触发面 / 增量拦截三段式)→ docs/代码规范/pre-commit使用说明.md §2
# 每个门禁一篇详解(含限制与盲区)→ docs/门禁说明/README.md
# 加/删一条钩子:同一笔提交里改 AGENTS.md 的「共 N 个钩子」与本文件注释;口径 = YAML 解析后的 hook 条目数,
# 自查命令与「声明 / 文档 / 实跑」三数同源核对 → docs/门禁说明/README.md §2(直接数文本行会把下面注释掉的候选条目算进去)
default_install_hook_types: [pre-commit, commit-msg]
# 顶层 exclude 只挡**第三方与生成物**。
@@ -753,3 +755,39 @@ repos:
language: system
files: ^(App/|Drivers/BSP/|test/host/|scripts/check_test_coverage\.py|scripts/test_coverage_baseline\.json|\.pre-commit-config\.yaml$)
pass_filenames: false
# 16) 生成器幂等性(2026-10-26 加,task-18;判据正文 docs/文档规范.md §4.4,索引行见 docs/门禁说明/README.md)
# 背景(实证,不是预防性):0f7b5a87 改了 tools/menu_gen/gen_menu.py 的**模板文字**却没重跑
# 生成物 → HEAD 里 Drivers/BSP/UI/ui_menu_tab.h 与模板不一致,后来是别人重跑生成器才被顺带
# 纠正(属主确认以生成器输出为准)。接线前实测又扫出 **5 张 docs/images/*.svg** 与各自生成器
# 不一致(文档挪目录后 rename_ref.py 只改了 tools/ 里的引用路径、产物没跟着重跑)——生成物
# 比生成器旧是**系统性**缺口。隐蔽之处:改生成器的人看不到任何 diff(脚本改动编译、单测全过),
# 下一个**碰巧**重跑的人却看到一段「不是我改的」改动 → 要么连带提交、要么 revert 掉真修复。
# 判定:把声明的输入与生成器脚本一起拷进**系统临时目录**重跑(三个生成器都按 __file__ 的三级
# 父目录定位仓库根 → 只会写 temp),再逐字节比对 temp 与工作区:DIFF / NEW / 生成器报错 = FAIL。
# **仓库一个字节都不写**,因此不需要还原、也不会 stash 别人在飞的改动(多人同树可安全跑)。
# 覆盖:gen_menu(含它回写的 uvprojx + CMakeLists 两份注册表)、中文字库链(extract_cn →
# ui_cn_chars.txt、make_font → ui_font_cn_12.c)、tools/arch_diagram 全部配图生成器。
# 一处刻意不按裸字节比对:ui_cn_chars.txt 按 font-regen 约定 = 源码在用 ∪ KEEP_EXTRA,改判
# 「txt 恰等于 重跑 ∪ KEEP_EXTRA」(两向都不许差;清单 importlib 直载 scripts/check_font_charset.py,
# 不留第二份清单)。存量基线 = 接线当天已漂的那 5 张 SVG,钉进脚本内 BASELINE 走 WARN 不拦提交
# (只减不增,还清即删、默认运行会点名);业务生成物(菜单表 / 页面注册表 / 字库 / 双端注册表)
# **零豁免面**,漂了就是 FAIL。
# ⚠ entry 是裸 python,而 pre-commit 在提交环境里可能把它解到**机器级**解释器:2026-10-26 真实提
# 交实测解到 C:/Python314(无 fontTools)→ 字体链报 No module named fontTools = 假红。所以脚本
# 自己按「显式 DTU_GEN_PYTHON > 当前解释器 > 仓库 .venv > PATH」解析并**逐个探针校验**(要真能
# import fontTools 才算数),整轮统一用同一个解释器;挑不出来打 [ENV]+候选表+修法、退出码 2 不
# SKIP。不把 entry 写死成 .venv 路径:那是 ci_local 2026-10-11「候选收窄 → runner 上 L0 全灭」
# 的同一条坑(详解 docs/门禁说明/generated-fresh.md §4)。
# ⚠ 字体链外部工具(node + 全局 lv_font_conv + 系统 simsun.ttc / seguisym.ttf)缺失时该项 SKIP 并
# 打印 [SKIP],--strict-env 才升 FAIL——与 test-coverage「缺 mscoverage 必 FAIL」刻意不同:
# 缺工具的机器上没有可执行的修法,SKIP 至少把「这台没验过」留在输出里(盲区同登记在索引行)。
# ⚠ pre-commit 4.x 已移除 always: true → files 必须覆盖全部触发面:任何生成器(tools/)、
# 生成器的输入(Drivers/BSP/UI 源码、App/task/ui_task、ui_menu_tree.txt、ui_cn_chars.txt)、
# 生成物(docs/images/*.svg + 两份注册表)、脚本自身与本配置文件(同 registration-check 口径)。
- id: generated-fresh
name: 生成器幂等性(重跑到临时目录 → 与工作区逐字节一致)
entry: python scripts/check_generated_fresh.py
language: system
files: ^(tools/.+|Drivers/BSP/UI/.+|docs/images/.*\.svg|App/task/ui_task\.(c|h)|ui_menu_tree\.txt|ui_cn_chars\.txt|MDK-ARM/STM32F407-Demo\.uvprojx|simulator/CMakeLists\.txt|scripts/check_generated_fresh\.py|scripts/check_font_charset\.py|\.pre-commit-config\.yaml)$
pass_filenames: false
+4 -3
View File
@@ -86,11 +86,12 @@
## Python 环境
- 统一入口 `& '.venv\Scripts\python.exe'`(3.12,fontTools/Pillow 已装);PATH 上的 `python` 是 msys64 的,**不要用它**跑依赖第三方库的脚本;纯标准库脚本任意 python3 均可
- ⚠ **钩子 entry 的裸 `python` 不等于 .venv**:2026-10-26 真实提交实测 pre-commit 把它解析成**机器级**解释器(无 fontTools)→ 字体链生成器报 `No module named fontTools`、`generated-fresh` 假红(同族第二次发作见 `scripts/ci_local.py` 文件头 2026-10-11「L0 全灭」)。凡依赖第三方库的门禁脚本,一律在脚本内自己按「显式 var > 当前解释器 > 仓库 .venv > PATH」解析并**探针校验**(装了 pre-commit ≠ 装了 fontTools),但**别把 entry 写死成某台机器的绝对路径**(那等于把门禁绑死在一台机器上)→ `docs/门禁说明/generated-fresh.md` §4
- 重建/换机:`<本机任一 python ≥3.10> -m venv .venv` + `pip install -r requirements.txt`(含 `pre-commit`;`DTU_PRECOMMIT_PYTHON` 可指向别的装了 pre-commit 的解释器);`requirements.txt` 必须**纯 ASCII**(Windows 下 pip 按 GBK 代码页解码)
## 提交门禁(pre-commit)
- 任一失败即拦截,共 **49 个钩子**(2026-10-25 道E 连加 `bom-guard` / `setview-check` / `uinav-selftest` 三条;**每种门禁一篇详解(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径)→ `docs/门禁说明/README.md` 索引**;通用机制与整体口径 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `id-check`(**编号门禁**,`docs/编号规范.md` §8 八项校验;**已关闸为【增量拦截】**:只拦 `scripts/id_known_backlog.json` 台账之外的新错误级问题,存量按指纹豁免、台账只减不增;另有一根恒生效的线——可判定比例跌破 `id_registry.json` 基线即失败;**2026-10-26 方案 B 同批加了第二根**:不可判定引用**绝对数**超过 `undecidable_total_ceiling` 即失败,因为比值会被"把可判定的引用改成散文"稀释(详见 `docs/门禁说明/id-check.md` §7.6))+ `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + 主机侧单测 23 个(`-unit-test` 后缀,2026-10-25 增补 adc-cal / prot-dim / vol-idle-sync / ui-data / meas / run-par / ui-sink 七个)+ `test-coverage`(**覆盖率总闸**:跑全部 40 套并守基线回退;缺 mscoverage 组件时按用户裁定 **FAIL**、不 skip,2026-10-24)+ `test-suite-switch` + `registration-check`(双端注册反向校验 + UI 页面描述符表接线,登记簿条目「rs485_task 白名单缺口」)+ `mem-budget`(RAM 92% 预算门禁,登记簿条目「缩堆迁 CCM 后半(暂缓)」)+ `font-charset`(UI 字符集差集检查,防缺字白框)+ `ui-golden`(UI 像素回归,菜单 8 状态 + 内容页 18 页黄金参考)+ `layer-deps`(**分层方向**:`Drivers/BSP/**` 不得 include `App/**` 的头;存量 3 条基线只减不增(2026-10-25 记录 reader 注入削掉 UI 记录 3 条边,6→3))+ `bsp-err`(**BSP 错误码与超时命名约定**:公开 API 不得拿 `HAL_StatusTypeDef`/`bool` 当错误通道、`*_TIMEOUT` 宏不得取负值、非 void 须写「返回值:」、**返回错误码的函数不得裸丢弃返回值**(须接住或显式 `(void)`;实测编译器与 cppcheck 都不报这类,只有这条会响);约定正文 `Drivers/BSP/bsp_err.h`,存量基线只减不增(2026-10-23 首清点 19 条 → 已还 18;`consume-result` 上线又暴露 37 条存量丢弃 → **现 38 条**))
- 任一失败即拦截,共 **50 个钩子**(**口径 = YAML 解析后的 hook 条目数**:自查命令与「声明 / 文档 / 实跑」三数同源核对见 `docs/门禁说明/README.md` §2 首条——⚠ 直接 grep 配置里的声明行会多算一条**注释掉的**候选钩子(= 51),不是口径;加一条钩子就同批改 AGENTS 与门禁索引两处计数。2026-10-25 道E 连加 `bom-guard` / `setview-check` / `uinav-selftest` 三条,2026-10-26 加 `generated-fresh` 一条;**每种门禁一篇详解(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径)→ `docs/门禁说明/README.md` 索引**;通用机制与整体口径 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `id-check`(**编号门禁**,`docs/编号规范.md` §8 八项校验;**已关闸为【增量拦截】**:只拦 `scripts/id_known_backlog.json` 台账之外的新错误级问题,存量按指纹豁免、台账只减不增;另有一根恒生效的线——可判定比例跌破 `id_registry.json` 基线即失败;**2026-10-26 方案 B 同批加了第二根**:不可判定引用**绝对数**超过 `undecidable_total_ceiling` 即失败,因为比值会被"把可判定的引用改成散文"稀释(详见 `docs/门禁说明/id-check.md` §7.6))+ `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + 主机侧单测 22 个(`-unit-test` 后缀,2026-10-25 增补 adc-cal / prot-dim / vol-idle-sync / ui-data / meas / run-par 六个;⚠ `test/host/ui_sink` 有 `run_unit_test.py` 却**没有** per-module 钩子,只有 `test-coverage` 总闸跑得到它——原写「23 个 / 含 ui-sink」是把这条未接线的算进了钩子账)+ `test-coverage`(**覆盖率总闸**:跑全部 40 套并守基线回退;缺 mscoverage 组件时按用户裁定 **FAIL**、不 skip,2026-10-24)+ `test-suite-switch` + `registration-check`(双端注册反向校验 + UI 页面描述符表接线,登记簿条目「rs485_task 白名单缺口」)+ `generated-fresh`(**生成器幂等性**:把 `tools/menu_gen/gen_menu.py`、中文字库链、`tools/arch_diagram/gen_*.py` 拷进**系统临时目录**重跑,与工作区**逐字节**必须一致——钩子只读、不写仓库也不 stash 别人在飞的改动;实证 `0f7b5a87` 改了模板文字没重跑生成物,接线前实测另有 5 张 `docs/images/` 配图已漂 → 钉成只减不增基线,**业务生成物零豁免面** → `docs/门禁说明/generated-fresh.md`)+ `mem-budget`(RAM 92% 预算门禁,登记簿条目「缩堆迁 CCM 后半(暂缓)」)+ `font-charset`(UI 字符集差集检查,防缺字白框)+ `ui-golden`(UI 像素回归,菜单 8 状态 + 内容页 27 页黄金参考)+ `layer-deps`(**分层方向**:`Drivers/BSP/**` 不得 include `App/**` 的头;存量 3 条基线只减不增(2026-10-25 记录 reader 注入削掉 UI 记录 3 条边,6→3))+ `bsp-err`(**BSP 错误码与超时命名约定**:公开 API 不得拿 `HAL_StatusTypeDef`/`bool` 当错误通道、`*_TIMEOUT` 宏不得取负值、非 void 须写「返回值:」、**返回错误码的函数不得裸丢弃返回值**(须接住或显式 `(void)`;实测编译器与 cppcheck 都不报这类,只有这条会响);约定正文 `Drivers/BSP/bsp_err.h`,存量基线只减不增(2026-10-23 首清点 19 条 → 已还 18;`consume-result` 上线又暴露 37 条存量丢弃 → **现 38 条**))
- **范围 = 项目代码**:第三方(`Lib/`、`Middlewares/`、CMSIS、HAL、`lftpd`、`dhara`、`MDK-ARM`)排除;`cppcheck`/`coding-standard` 另排除 `test/`,规范检查再排除 CubeMX 生成的 `Src/`/`Inc/`。**`simulator/` 已纳入全部钩子**(2026-10-11)——生成物只靠 `.gitignore` 挡,**不在 `exclude` 里写目录名**(教训见 `docs/代码规范/pre-commit使用说明.md` §5.5)
- ⚠ **pre-commit 4.x 已移除 `always: true`**(实测对 `local` 钩子只告警 `Unexpected key(s)` 并跳过)→ 想"总是跑"的钩子**必须让 `files:` 覆盖到所有会触发它的文件**,没有兜底开关
- **改写型钩子(`trailing-whitespace` + `end-of-file-fixer`)豁免 CubeMX/Keil 生成文件**(配置里 YAML 锚点 `&cubemx-generated`);⚠ **只读检查不受影响**:`cppcheck` / 乱码 / `cubemx-survival` / `task-table` / `mem-numbers` 仍覆盖 `Src/`、`Inc/`
@@ -152,7 +153,7 @@
## 文档与图形
- **登记簿类文档「完成即销项」**:`docs/任务规划/README.md` 这类待办登记簿,事项修复/关闭后**直接从表中删除该行**(不保留"已消化"行)——结论与证据应已沉淀进对应模块文档/问题记录。销项时必须:① 先核对目标文档确已含完整记录 ② 同步清理其他文档对该编号的引用 ③ 改完跑 `check_doc_links.py --all` + pre-commit。**新条目标识(用户裁定 2026-10-14)**:`#` 列填**语义短语**(如 `沉降窗 800ms 上限只留痕`),**不再新增字母+数字编号**——"编号不回收"只写规则却不留痕迹就不可执行(销项删行后仓库里毫无痕迹,2026-10-14 实测就发生过「销项号被重新取走」的撞号事故),语义标识自带唯一性;**存量编号(登记簿 N-x / S-x / T-x 与专项 A-x)已于 2026-10-26 全部转成语义短语**——旧号进 `scripts/id_aliases.json` 标 `retired-to-phrase`,不得再指;**已随销项消失的号永久作废**、不得复用 → `docs/任务规划/README.md` §0
- 图形走 **`draw-diagram` skill**:精确布局图用 `tools/arch_diagram/gen_*.py` 生成器(产物放 `docs/images/`,脚本与 .svg 一并提交);拓扑图用 Mermaid;**勿手绘 ASCII 框图**。文件引用关系图走 `tools/refgraph`(`& 'tools\refgraph\gen_refgraph.bat'`)
- 图形走 **`draw-diagram` skill**:精确布局图用 `tools/arch_diagram/gen_*.py` 生成器(产物放 `docs/images/`,**脚本与产物同一笔提交**:改生成器没重跑,下一个碰巧重跑的人会看到一段「不是我改的」diff——已机械化 `generated-fresh`,重跑到临时目录逐字节比对 → `docs/文档规范.md` §4.4);拓扑图用 Mermaid;**勿手绘 ASCII 框图**。文件引用关系图走 `tools/refgraph`(`& 'tools\refgraph\gen_refgraph.bat'`)
- **改名 / 挪目录一律走 `scripts/rename_ref.py`(先 dry-run,确认后 `--apply`)**:`git mv` + 自动改写全库引用(反引号引用 / Markdown 链接 / 图片 `../` 层级换算 / `#include` / `uvprojx` `<FilePath>` / `CMakeLists.txt`),落盘后自动跑 `check_doc_links --all` 复核——手工扫引用既慢又漏(一次改名历史上波及 10~30 文件)。引用章节用「`路径.md` §N.M」**紧邻写法**(`doc-links` 校验编号是否还在:紧邻/裸名/前缀/别名/自引用**全部拦提交**(钩子带 `--sections-strict`);每次跑打印「可判定比例」,`--section-todo` 出待改清单,昵称歧义就登记 `scripts/section_aliases.json` 或写清楚)→ `docs/文档规范.md` §3.5 / §3.6。**编号改名走 `scripts/rename_ids.py`**(映射表驱动、**显式 scope**、干跑默认、自动写 `scripts/id_aliases.json` 的 scoped 旧号→新号条目;`[示例]`/`[计划]` 行与变更记录表不改写;**绝不全局查找替换**——同一裸形字面量可能同时属多个族,全局替换等于造新撞号,实测 `S3` 三义)
- 文档写作规范(编码/BOM/链接/表格/一致性钩子)→ `docs/文档规范.md`;全量索引与推荐阅读顺序 → `docs/README.md`
@@ -173,7 +174,7 @@
| ADC 采样节拍 / 通道映射 / V0.1 硬件缺陷 | `docs/子系统设计/ADC采样节拍设计说明.md`、`docs/硬件平台/ADC通道映射说明.md` |
| 存储栈(FatFs / dhara / lftpd / 两条 FatFs 陷阱) | `docs/架构设计/STM32F4-Base存储架构说明.md` |
| RAM / 栈预算 / 容量台账 | `docs/内存管理/RAM使用分析与优化方案.md`、`内存内容分配策略.md` |
| **每个提交门禁一篇详解**(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径) | `docs/门禁说明/README.md`(49 个钩子 → 26 篇索引 + 1 指针) |
| **每个提交门禁一篇详解**(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径) | `docs/门禁说明/README.md`(50 个钩子 → 26 篇索引 + 1 指针) |
| **要不要给某个东西发编号**(默认:不发,写语义短语)/ 允许用号的 8 个族 / `id-check` 八项校验 | `docs/编号规范.md`(§1 先问三句、§4.1 族总表〔穷举〕、§8 门禁与实测基线) |
| 提交门禁通用机制(阶段 / stash 陷阱 / 触发面 / 增量拦截三段式) | `docs/代码规范/pre-commit使用说明.md` §2 |
| 代码规范 V1.0 / cppcheck 口径 | `docs/代码规范/嵌入式C语言代码规范_V1.0.md`、`cppcheck使用说明.md` |
+1 -1
View File
@@ -670,7 +670,7 @@ typedef struct {
| 5 | 首帧前显示 `--` 而非 `0.000` | 模拟器:不 publish 直接开页,断言文本 |
| 6 | 定值整定全链闭环 | 就地改 → 保存 → 重启 → 值保持 + 校验和自检通过 + 主站 C_RD 一致。**半收口(2026-10-26)**:模拟器侧「改 → 保存 → OK → 回首屏新值已在帧里」已由像素 + 文本断言覆盖(黄金用例 `sparam_save` / `sparam_landed`,越界侧 `sparam_oob`);**重启保持与主站 C_RD 一致只能上板**——模拟器没有 NAND,`apply()` 到 `prot_set_set_sealed()` 就是终点,这条差距已登记在 `docs/测试说明/模拟器差距清单.md`;另注意 40 项里有 13 项 `comm_addr=0`(纯本地项),主站侧根本读不到,不能拿 C_RD 当它们的判据 |
| 7 | 像素回归稳定 | `ui-golden` 连续两次运行零差异。**已验(2026-10-26)**:27 页连跑两次逐字节一致(含整定 9 态);轮询定时器不影响——无头模式不调 `lv_timer_handler()`(§9.2 注 6) |
| 9 | 「写不进去」与「改了不动作」两条事实互不冒充 | 反向文本断言(`check_content_pages.py` 的 `ABSENT`:查看页与只读态不得出现「未标定」徽标与「保存」行)+ 台架:制造一次 `locked`(改坏校验和档)目检保存行 `LOCK` 且 ENTER 无反应 |
| 9 | 「写不进去」与「改了不动作」两条事实互不冒充 | 反向文本断言(`check_content_pages.py` 的 `ABSENT`:查看页与只读态不得出现「未标定」徽标与「保存」行)+ 台架:未制造该态——要它必须改坏 /prot_set.bin(破坏产品配置存储,用户裁定 2026-10-26 不做),故 LOCK 态目前只有代码级证据与三重反证(ENTER 被正常消费、遥信 YX31=OFF、开机 CORRUPT=0),**没有像素证据** |
| 8 | 主机单测覆盖数据模块 | `test/host/ui_data` 全绿并挂 pre-commit 钩子 |
---
+9 -6
View File
@@ -26,7 +26,7 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警
| 步 | 脚本/命令 | 门禁内容 | 预期输出 |
|---|---|---|---|
| 1 | `simulator/tests/check_menu_rows.py` | 菜单 8 状态:选中行白带 / 非选行黑底白字 / 框体 1px 边框 / 往返像素一致(MENU-STATE-03==MENU-STATE-01、MENU-STATE-06==MENU-STATE-01、MENU-STATE-07==MENU-STATE-02)+ MENU-STATE-00 池基线 | `PASS: 8 状态全部断言通过(8 图,池基线 5368 B)` |
| 2 | `simulator/tests/check_content_pages.py` | ① 文本级断言(字形模板匹配,50 条,独立于 `ref/`)→ ② 11 页截图与 `ref/` 黄金参考**逐字节**比对 + 开页-关页往返池水位 | `PASS: 11 内容页 + 往返池水位全部通过`(文本断言先于像素比对,失败即中止) |
| 2 | `simulator/tests/check_content_pages.py` | ① 文本级断言(字形模板匹配,242 条,独立于 `ref/`)→ ② 27 页截图与 `ref/` 黄金参考**逐字节**比对 + 开页-关页往返池水位 | `PASS: 27 内容页 + 往返池水位全部通过`(文本断言先于像素比对,失败即中止) |
| 3 | `keil-build` skill(`keil_build.py`) | MCU 端编译(UI 源码双端共用,改一端必须过另一端) | 退出码 0 = BUILD_OK |
| 4 | `scripts/check_coding_standard.py` | 代码规范 V1.0 机械检查 | PASS |
@@ -35,7 +35,8 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警
> 渲染成墨点集合,在截图对应 y 带内做精确匹配(LVGL 基线对齐)。价值:**缺字显示空白 / 标签写错**
> 这类回归不再只靠像素位图(那需要刷参考,且无视觉后端时难以判读);文本断言不依赖 `ref/`,且
> **先于**像素比对运行,失败即中止(不误刷参考)。文本断言覆盖确定文案,像素参考仍负责几何/样式/
> 光标反显等其余视觉。
> 光标反显等其余视觉。规模与覆盖面(2026-10-26 实跑 `check_content_pages.py` 取数):文本断言 **242 条**、覆盖 27 页中的
> **24 页**——`pwd` / `pwe` / `pwc` 三张密码态只进像素比对、未登记文本断言,改这几页的文案没有文本层兜底。
**退出码约定**:两个脚本均为 0=PASS / 1=断言失败 / 2=环境错误(exe 缺失、参考缺失)。
失败时脚本打印差异明细(文本断言未命中项 / ink runs 对照 / 像素差数),内容页脚本保留现场截图于临时目录并提示路径。
@@ -48,7 +49,7 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警
| `POOL_AFTER_ROUNDTRIP` | check_content_pages.py | **5448 B** | 开页-关页往返后水位 = 基线 + 一次性残留 80 B |
> 水位历史:4784 →(2026-09-14 待机模块常驻 +656)5440 / 5520 →(2026-10-11 裁剪 LVGL 控件类 −72)5368 / 5448。
> 后一次漂移已核实为**有意且更优**:像素逐字节未变(11 页黄金参考在裁剪**前**生成、裁剪后全部一致),
> 后一次漂移已核实为**有意且更优**:像素逐字节未变(**裁剪当时黄金参考只有 11 页**,在裁剪**前**生成、裁剪后全部一致;该集合现已扩至 27 页),
> 且一次性残留仍是 80 B(5448 − 5368)→ 泄漏特征未变,只是 `lv_theme_apply` 不再为已不存在的控件类登记样式条目。
**漂移处理**:菜单/页面结构或样式变更导致门禁失败时,先确认新值合理(残留分解见
@@ -57,7 +58,7 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警
## 四、黄金参考(入库)与目检 PNG(本地产物)
`simulator/tests/ref/*.bin`(像素黄金参考,11 页 × 3200 B)**随仓库入库**;派生的 `.png`
`simulator/tests/ref/*.bin`(像素黄金参考,27 页 × 3200 B)**随仓库入库**;派生的 `.png`
目检图仍是本地产物(`.gitignore`)。入库的理由:比对是**逐字节**的,基线不入库时任何新克隆
或新工作树跑 `ui-golden` 都必然 FAIL(缺基准),这条门禁就只在"有老副本的机器"上真实存在。
可复现性实测(2026-10-22):两份各自独立构建的 `lvgl_sim`(不同构建目录、相隔 11 天的代码)
@@ -92,7 +93,7 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警
| 多页大面积像素差异 | 字体重生成(`ui_font_cn_12.c` 变化)→ 确认字形正确后 `--update` 刷参考 |
| 单行/单字差异 | 几何或文案变更 → 先判断是否有意修改;无意 = bug |
| 池水位门禁失败但像素一致 | 样式/结构变更(如动态样式转常量、增删框体)→ 确认新值合理后更新常量并登记 |
| **只有 `netcfg_save` 一页失败**(保存行值列是 `ERR` 而非 `OK`),其余 10 页全过 | **不是代码回归,是工作目录被污染**:模拟器按**当前工作目录**读写 `sim_config.ini`,而 `--cfgtest` 超长守卫的场景 2 会伪造一份 4000 B 纯注释"雷"文件、并断言"拒写后原文件不动"——**连通过路径也会把它留在原地**。之后每次运行都加载它 → 外科手术保存要在 4000 B 注释后补写受管段 → 输出必超 `CFG_FILE_MAX_LEN` → 引擎**按设计拒写** → 保存行显示 ERR,与"工厂默认态"的黄金参考对不上。处置:两套回归已改为**在空临时目录里跑**(传 `cwd=tmp`),`--cfgtest` 已加外壳无论成败一律删除伪造档;手工跑 `lvgl_sim` 时删掉当前目录的 `sim_config.ini` 即恢复。**禁止为此刷黄金参考**(参考是对的) |
| **只有 `netcfg_save` 一页失败**(保存行值列是 `ERR` 而非 `OK`),其余 26 页全过 | **不是代码回归,是工作目录被污染**:模拟器按**当前工作目录**读写 `sim_config.ini`,而 `--cfgtest` 超长守卫的场景 2 会伪造一份 4000 B 纯注释"雷"文件、并断言"拒写后原文件不动"——**连通过路径也会把它留在原地**。之后每次运行都加载它 → 外科手术保存要在 4000 B 注释后补写受管段 → 输出必超 `CFG_FILE_MAX_LEN` → 引擎**按设计拒写** → 保存行显示 ERR,与"工厂默认态"的黄金参考对不上。处置:两套回归已改为**在空临时目录里跑**(传 `cwd=tmp`),`--cfgtest` 已加外壳无论成败一律删除伪造档;手工跑 `lvgl_sim` 时删掉当前目录的 `sim_config.ini` 即恢复。**禁止为此刷黄金参考**(参考是对的) |
| `找不到 lvgl_sim.exe` | 模拟器未构建或 build/ 被删 → 先按 §一 重建 |
| 终端输出中文乱码 | PowerShell 代码页渲染问题,不影响判定(看 PASS/FAIL 与退出码) |
@@ -144,7 +145,7 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警
### 7.3 用法与门禁档位
```powershell
# 认屏快检:18 张黄金参考(+ 若有台架留档帧),秒级,不需要构建产物
# 认屏快检:27 张黄金参考(+ 若有台架留档帧),秒级,不需要构建产物
& '.venv\Scripts\python.exe' scripts/board_ui_nav.py --selftest-quick
# 完整自检:再加全绑定页闭环导航(需模拟器可执行文件,约 30 s)
& '.venv\Scripts\python.exe' scripts/board_ui_nav.py --selftest
@@ -217,3 +218,5 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警
盲走 9 帧仅 3 帧名副其实的实测表、认屏三判据、`uinav-selftest` 钩子档位、四条已知限度与
过密码口径;三张证据 PNG 落 `boot_log_data/` |
| 2026-10-22 | **`ref/*.bin` 像素黄金参考改为入库**(撤销 2026-09-10 的"整目录不入库"):`.gitignore` 由忽略整目录收窄为只忽略 `ref/*.png`。动因是这条门禁不可移植——新克隆/新工作树里基准必然缺失,`ui-golden` 报「缺黄金参考」而不是回归结论;本仓实测两次同类踩坑(并行工作树里第一次跑必失败,只能现场 `--update` 造基准,而"现场造的基准"恰好失去意义)。入库前做了可复现性验证:主树(`ref/` 生成于 2026-10-11)与新工作树各自独立构建 `lvgl_sim`,11 个 `.bin` SHA-256 全部一致。§四 与"何时不该更新"随之改写:`--update` = 改基准,会进 `git diff`,须逐页过目并在提交正文说明 |
| 2026-10-26 | **内容页页数口径统一为 27**:正文里残留的「11 页 / 其余 10 页 / 18 张」按现状改正(判据本体 = `simulator/tests/ref/shot_*.bin` 入库实数 27 个、`check_content_pages.py` 的 `PAGES` 27 项,含整定 9 态);下方变更记录里的「11 页」是当时事实,按历史冻结不动。同类陈述同步在 `测试开发标准流程.md`、`自动化测试流程.md`、`主机单测覆盖率口径.md`、`内容页架构.md`、`simulator/tests/README.md`、`scripts/board_ui_nav.py` 改正 |
| 2026-10-26 | **文本级断言规模口径修正**:门禁表里的「50 条」是 2026-09-10 首版数字,现值 **242 条 / 覆盖 27 页中的 24 页**(`pwd` `pwe` `pwc` 三张密码态只进像素比对、未登记文本断言);正文的文本层说明补写了覆盖面与这条盲区。下方变更记录里的 50 条属历史,不动 |
+4 -2
View File
@@ -453,9 +453,9 @@ extern const content_page_def_t ui_page_device_info_desc; /* 描述符(Flash
## 八、验证与回归
- **门禁**(`ui-verify` skill / `UI_自动化测试流程.md`):
0) 模拟器构建 0 警告 → 1) 菜单 8 状态像素回归 + 池基线 → 2) 内容页文本断言 + 11 页黄金参考逐字节比对
0) 模拟器构建 0 警告 → 1) 菜单 8 状态像素回归 + 池基线 → 2) 内容页文本断言 + 27 页黄金参考逐字节比对
(网口设置 4 态 + 密码页 4 态等)+ 开页-关页往返水位 → 3) Keil 构建 0 错 0 警 → 4) 代码规范检查
- **门禁常量**(脚本头部):`MENU_POOL_BASELINE = 4784 B`、`POOL_AFTER_ROUNDTRIP = 4864 B`
- **门禁常量**(脚本头部):`MENU_POOL_BASELINE = 5368 B`、`POOL_AFTER_ROUNDTRIP = 5448 B`
- **失败处置**:像素不一致 = 代码 bug(修代码,**禁止**刷参考掩盖);**有意**改视觉 →
先肉眼确认新效果正确,再 `check_content_pages.py --update` 刷参考(`ref/*.bin` 已入库,刷了会进 diff,必须一并提交并说明)
- 新增页面后同步在 `check_content_pages.py` 增加该页的按键序列、文本断言与参考图
@@ -480,3 +480,5 @@ extern const content_page_def_t ui_page_device_info_desc; /* 描述符(Flash
| 2026-09-14 | 运行记录/自检记录动态列表页(`ui_page_runlog` / `ui_page_selftest`):描述符新增 `count_cb` + `refresh`(开页/翻页时重读条数与内容,每条 2 行 = 时间戳/描述),数据源为 `App/log/sys_log` 引擎的 `/log/run.log`、`/log/selftest.log`;**框架不变量 selectable ⇒ band_h>0**——band_h=0 时选中行恒黑字无白带 → 黑底不可见(首版两页均踩中,改 band_h=13 对齐 AC/DC 约定);新坑:本 LVGL 版本 `lv_text_get_width` length≠0 时无 NUL 检查,传 -1 越串尾死循环,必须传 strlen(密码页错误提示首版触发,已修并全库排查) |
| 2026-09-15 | 网口设置页保存行改**异步交接**(P1-3 跨任务竞态修复,配置引擎属主 = defaultTask):保存行 ENTER → `net_cfg_save_request()`(置"待保存"标志 + `SYS_EVT_CFG_SAVE` 唤醒属主任务,UI 不再直接触碰 cfg_file 引擎);写盘与"立即应用"由 defaultTask 的 `net_cfg_save_run()` 执行(成功 → OK 并触发 `SYS_EVT_NET_RECONFIG` 免重启重建;失败 → ERR 不触发应用);行值改随交接状态 `net_cfg_save_state()` 显示 ENTER/WAIT/OK/ERR(原为页面局部 `s_saved` 同步结果,删除);无头自检(`--keys`)在注键后同步完成交接保持终态一致;五门禁全绿(像素/文本与改前一致,池水位不变) |
| 2026-10-22 | §八 失败处置口径更新:`ref/*.bin` 黄金参考已入库,刷参考会进 `git diff`、必须随改动一并提交并说明理由(详见 `UI_自动化测试流程.md` §四) |
| 2026-10-26 | 验证与回归段的页数口径修正为 **27 页**黄金参考(`simulator/tests/ref/` 入库 27 个 `shot_*.bin`,含整定 9 态);上方变更记录里的「11 页」是当时事实不动 |
| 2026-10-26 | 门禁常量修正为 5368 B / 5448 B(= 两个回归脚本头部现值);本文件正文表早已是 5368/5448,只这一处残留 2026-10-11 裁剪 LVGL 控件类之前的 4784/4864 |
+4 -3
View File
@@ -860,9 +860,9 @@ bool ui_content_page_is_open(void); /* 查询是否有内容页
| 时刻 | LVGL 池 used | 说明 |
|---|---|---|
| 菜单基线(根框建完) | 4784 B | 同 §1.5 |
| 内容页打开峰值 | 5560~5976 B | +776~1192 B:屏对象+容器+事件列表+局部样式+screens 数组扩容(x64 指针 8B,MCU 32 位下更小) |
| 关页后回落 | **4864 B** | 一次性残留 **+80 B**(见下),非泄漏 |
| 菜单基线(根框建完) | 5368 B | 同 §1.5;2026-10-11 裁剪 LVGL 控件类后基线 5440→5368,本行旧值 4784 是更早的快照 |
| 内容页打开峰值 | +776~1192 B(增量口径) | 增量为实测值、与基线无关:**绝对水位随基线漂移,5560~5976 B 是老基线 4784 时代的快照,按 5368 基线的绝对峰值待重测**(回归脚本只钉基线与往返两个数) |
| 关页后回落 | **5448 B** | 一次性残留 **+80 B**(见下),非泄漏 |
**+80 B 一次性残留分解**(均为 LVGL 核心行为,与内容页模块无关;MCU 32 位下更小):
- +8 B:`disp->screens` 数组高水位——开页时第 5 个屏使 TLSF 块 32→40 B,关页缩回后块不归还(分配器行为)
@@ -907,3 +907,4 @@ bool ui_content_page_is_open(void); /* 查询是否有内容页
| 2026-09-09 | 用户密钥密码页落地(首个表单/操作页,§九 / `内容页架构.md` §4.1):框架描述符新增 title_y/hide_deco/hide_footer/key_intercept + 新 API `ui_content_page_invalidate`;受保护叶子路由(arm→开密码页,确认一致后回调**先关页再续做进入动作**);§9.2/§9.3/§9.4 同步。验证:回归扩至 7 页 + 往返水位全 PASS、五门禁全绿 |
| 2026-09-09 | 菜单显示层 flat 样式同法转常量样式(§5.6/§7.5/§9.5):LVGL 池基线 4872→4784 B、最深四级级联 6128→6040 B、内容页峰值 5648~6064→5560~5976 B(x64 省 88 B);`LV_STYLE_CONST_INIT` 生成外部链接符号,内容页同名者改名 `cp_style_flat`。验证:`@build` 0 错 0 警 + 代码规范 PASS + 菜单 8 状态像素回归全 PASS + 三页几何不变 |
| 2026-09-09 | 权限模型(§3.7/§3.8/§9.4 同步):菜单表行尾注记 ` #P`(需密码);`g_menu_tab` pwd 列改 0/1 布尔(API `ui_menu_tab_has_pwd`,表项仍 8 B/项);两个编译期密码(查看 000 / 修改 123)+ `ui_perm_t` 权限枚举在 `ui_pwd_def.h`(用户定案:密码仅编译期写入,不落 Flash、无运行期设置页)——密码页输查看密码授只读权限、输修改密码授修改权限(只有修改权限进入后才能改参数);逻辑层密码检查提前到内容页判断之前(带内容页的需密码叶子同样先经密码页,修复路由缺口);无密码叶子直接进、默认只读。验证:五门禁全绿(菜单 8 态 + 内容页 7 页像素一致、池 4784/4864 B 不变、Keil 0 错 0 警、代码规范 PASS) |
| 2026-10-26 | 9.5 节内存预算表与本文 1.5 节对齐:菜单基线 4784→**5368 B**、关页回落 4864→**5448 B**(2026-10-11 裁剪 LVGL 控件类后脚本常量即为此值);「内容页打开峰值」改为**增量口径** +776~1192 B,绝对水位标注为待按新基线重测,不臆造实测数 |
+4
View File
@@ -26,6 +26,10 @@
### 2.1 规模
> ⚠ **本表是 2026-10-23 立项当天快照**(节点 42 / 有内容页叶子 6 / 空叶子 27 / 黄金参考 11 份均为当日实数,按"完成即冻结"不回改)。
> 页面扩充落地后现值已变——黄金参考现为 27 份(`simulator/tests/ref/` 入库的 `shot_*.bin` 实数),现状口径见
> `docs/UI设计/页面扩充详细方案.md` 与 `docs/UI设计/UI_数据供给设计说明.md`。
| 项 | 数 |
|---|---|
| 菜单节点总数 | **42**(9 菜单 + 33 叶子) |
+9 -10
View File
@@ -32,18 +32,16 @@
## 1. 当前执行:UI 补齐
菜单树共 33 叶,已绑 12 叶,剩 21 叶。其中 **16 叶纳入当前执行**,另外 5 叶因缺少
菜单树共 33 叶,已绑 16 叶,剩 17 叶。其中 **12 叶纳入当前执行**,另外 5 叶因缺少
开入硬件、录波模块或 SNTP 产品定义而不做。唯一执行清单见 `UI补齐.md`。
| 顺序 | 要做的事 | 交付结果 |
|---|---|---|
| 1 | **`param_request` 写路径** | UI 能安全提交参数修改,并收到成功或失败状态 |
| 2 | 整定 4 页 | 参数、定值、控制字、软压板可修改并持久化 |
| 3 | 装置维护 3 页 | 时钟设置、强制复归、清除记录 |
| 4 | 遥测死区与系数 2 页 | 修改既有 `run_par` / `adc_cal`,不新增第二份真源 |
| 5 | 通讯设置 | 新增 `[comm]` 配置段并接入 104 |
| 6 | 记录 5 页 | 事故、操作、保护告警、保护启动、遥控记录有写入方并可查看 |
| 7 | 运行报告 | 上电次数、运行时长、复位原因、104 统计 |
| 1 | 装置维护 3 页 | 时钟设置、强制复归、清除记录 |
| 2 | 遥测死区与系数 2 页 | 修改既有 `run_par` / `adc_cal`,不新增第二份真源 |
| 3 | 通讯设置 | 新增 `[comm]` 配置段并接入 104 |
| 4 | 记录 5 页 | 事故、操作、保护告警、保护启动、遥控记录有写入方并可查看 |
| 5 | 运行报告 | 上电次数、运行时长、复位原因、104 统计 |
**执行纪律:单道串行。** 菜单树与页面注册表是生成物,字库也是整文件生成物;
不要同时开多条 UI 页面分支。上板验证可累积成一批,但保存、重启、主站读回等判据不能删除。
@@ -58,7 +56,7 @@
| 顺序 | 工作 | 当前状态 | 开始或切换条件 |
|---|---|---|---|
| **1** | **UI 补齐** | **当前执行** | 已开始;按 §1 的 1→7 顺序推进 |
| **1** | **UI 补齐** | **当前执行** | 已开始;按 §1 的 1→5 顺序推进 |
| **2** | **net 层跨上下文收口** | 下一步 | UI 当前清单完成后;裸指针收口与 reconcile 突发饿死 uiTask 同批评估 |
| **3** | 其余结构与可靠性评估 | 排队 | net 收口后;逐项见 `登记簿.md`,观察项不为了“清清单”而强做 |
| **4** | 数据可信剩余三项 | 等待台架或外部资源 | 已知电压注入 → 端子变比 → 标准源两点标定;资源到位可插入优先处理 |
@@ -69,7 +67,8 @@
### 2.1 关键依赖
- **UI**:`param_request` 写路径 → 整定 4 页;其余页面按 §1 串行推进。
- **UI**:写路径与整定 4 页已收口(含上板判据);其余页面按 §1 串行推进,
菜单树、页面注册表与字库都是生成物,同刻只允许一条道写。
- **送检**:已知电压注入 → 端子变比确认 → 标准源两点标定 → 保护解除闭锁 → 送检。
- **保护扩展**:标准源未到位前不新增无法验证的判据;跨转换器相位、功率、对称分量类
功能必须等 V0.2 硬件。
+14 -15
View File
@@ -1,15 +1,15 @@
# 当前任务:UI 补齐
> **从第 1 项 `param_request` 写路径开始。完成它以后再绑定整定 4 页。**
> **从第 1 项「装置维护 3 页」开始,按表序单道串行推进。**
> 本文件是当前唯一执行清单;全项目顺序见 `docs/任务规划/README.md` §2。
## 1. 目标与范围
菜单树 33 叶,已绑 12 叶,未绑 21 叶:
菜单树 33 叶,已绑 16 叶,未绑 17 叶:
| 分类 | 数量 | 当前怎么处理 |
|---|---:|---|
| 数据源已就绪 | 9 | 当前执行:整定 4 + 装置维护 3 + 遥测死区/系数 2 |
| 数据源已就绪 | 5 | 当前执行:装置维护 3 + 遥测死区/系数 2 |
| 需补配置或写入方 | 7 | 当前执行:通讯设置 1 + 记录 5 + 运行报告 1 |
| 无开入硬件 | 3 | 不做:遥信类型、遥信防抖、双点遥信 |
| 缺独立模块或产品定义 | 2 | 不做:手动录波、SNTP 设置 |
@@ -29,20 +29,19 @@
| 顺序 | 任务 | 完成判据 | 主要写范围 | 状态 |
|---|---|---|---|---|
| **1** | **`param_request` 写路径** | 实现 `ui_data_param_request/state`:深度 4 环形队列,满返 -1;状态含 `OK` / `ERR_RANGE` / `ERR_LOCKED` / `ERR_BUSY`;`ui_data_feed_tick()` 取出后调用 `prot_set_set_sealed()` 并置脏;主机单测覆盖满队列、越界、闭锁、正常往返 | `Drivers/BSP/UI/ui_data.*`、`App/app/ui_data_feed.*` | 未开始 |
| **2** | **整定 4 页** | 参数、定值、控制字、软压板 4 叶绑定页面;就地修改并保存后,重启值保持且校验和通过;主站 `C_RD` 读到同值;越界行高亮且值不落盘;黄金用例入库 | `Drivers/BSP/UI/`、`ui_menu_tree.txt`、生成物 | 未开始 |
| **3** | **装置维护 3 页** | 时钟设置写入 `sys_clock_set_unix()` 并回读;强制复归确认后动作位复归且有 SOE;清除记录先补 `sys_log` 删除 API,覆盖删后重建、计数归零、不存在时幂等 | `Drivers/BSP/UI/`、`App/log/sys_log.*` | 未开始 |
| **4** | **遥测死区与系数 2 页** | 修改死区能改变 104 上送门限;修改 `k` 后页面值按比例变化;两页只走既有 `[run_par]` / `[adc_cal]` 落盘链,不新增真源 | `Drivers/BSP/UI/`、`ui_menu_tree.txt` | 未开始 |
| **5** | **通讯设置** | 新建 `[comm]` 段承载公共地址、端口、主站地址;修改并重启后连接与公共地址生效;核对配置段数和键数上限 | `App/cfg/`、`App/protocol/iec104/`、`Drivers/BSP/UI/` | 未开始 |
| **6** | **记录 5 页** | 事故、操作、保护告警、保护启动、遥控各有一个写入点和记录类型;可翻最近 20 条;空记录明确显示“无记录”;共用 reader | `App/log/`、`Drivers/BSP/UI/` | 未开始 |
| **7** | **运行报告** | 展示上电次数、运行时长、复位原因、104 统计;复用 info 通道的已有数据;新计数器明确“掉电保持”或“本次运行”,不得含糊 | `App/log/` 或 `App/app/`、`Drivers/BSP/UI/` | 未开始 |
| **1** | **装置维护 3 页** | 时钟设置写入 `sys_clock_set_unix()` 并回读;强制复归确认后动作位复归且留操作记录;清除记录经 `syslog_request_clear()` 发起、按 `syslog_clear_state()` 出真反馈,覆盖删后重建、计数归零、不存在时幂等 | `Drivers/BSP/UI/`、`App/log/sys_log.*` | 未开始 |
| **2** | **遥测死区与系数 2 页** | 修改死区能改变 104 上送门限;修改 `k` 后页面值按比例变化;两页只走既有 `[run_par]` / `[adc_cal]` 落盘链,不新增真源 | `Drivers/BSP/UI/`、`ui_menu_tree.txt` | 未开始 |
| **3** | **通讯设置** | 新建 `[comm]` 段承载公共地址与端口;修改并重启后连接与公共地址生效;核对配置段数和键数上限(主站地址白名单本卡不做,见 §5) | `App/cfg/`、`App/protocol/iec104/`、`Drivers/BSP/UI/` | 未开始 |
| **4** | **记录 5 页** | 事故、操作、保护告警、保护启动、遥控各有一个写入点和记录类型;可翻最近 20 条;空记录明确显示“无记录”;共用 reader | `App/log/`、`Drivers/BSP/UI/` | 未开始 |
| **5** | **运行报告** | 展示上电次数、运行时长、复位原因、104 统计;复用 info 通道的已有数据;新计数器明确“掉电保持”或“本次运行”,不得含糊 | `App/log/` 或 `App/app/`、`Drivers/BSP/UI/` | 未开始 |
### 依赖关系
- 1 → 2 是硬依赖;3、4 虽可独立开发,但当前仍按表序单道执行。
- 3 的“清除记录”包含 `sys_log` 删除 API,不允许只做一个没有实际效果的按钮。
- 5 可先完成配置层再接页面,但最终必须作为一整项验收。
- 6、7 先补真实写入方或计数器,再做页面;不允许用静态演示数据充数。
- 各项虽可独立开发,但当前仍按表序单道执行(菜单树、页面注册表与字库都是生成物)。
- 1 的“清除记录”只能发 `syslog_request_clear()` 请求并按 `syslog_clear_state()` 出反馈,
不允许只做一个没有实际效果的按钮,也不得在页面里直接删文件。
- 3 可先完成配置层再接页面,但最终必须作为一整项验收。
- 4、5 先补真实写入方或计数器,再做页面;不允许用静态演示数据充数。
---
@@ -50,7 +49,6 @@
| 页面 | 现有数据源 | 还缺什么 |
|---|---|---|
| 整定 4 页 | `prot_set` 全部定值项(`App/algo/prot_set.h` 的 `PS_ID_COUNT`,现 40 项)、`ui_w_editor` | 写路径已落,缺可编辑页面 |
| 时钟设置 | `sys_clock_set_unix()` | 页面与回读校验 |
| 强制复归 | `prot_elm_request_reset()`、`ui_page_action` | 页面与 SOE 留痕 |
| 清除记录 | `sys_log` | 页面与确认交互(删除 API 已落) |
@@ -87,3 +85,4 @@
| 手动录波 | 录波模块不存在,且工程量口径尚未完成实测闭环 | 故障录波独立立项 |
| SNTP 设置 | 当前使用 8888 私有对时协议,SNTP 链路与产品口径均未建立 | SNTP 客户端立项 |
| net 层与其他结构债 | 当前只做 UI,避免两条主线互相拖慢 | UI 当前清单完成后,按 README §2 切换 |
| 主站地址白名单 | CH395F 单连接 socket 上「拒绝即 close+重 listen」等于给扫描风暴开门,且该值现在没有任何既有消费点 | 有真实消费场景或多连接后端立项时再评 |
+39
View File
@@ -265,6 +265,45 @@
> ⚠️ 状态词按**就近**(±40 字符)判定,不是整行——否则文件名本身含「不存在」「删除」等字样时会误豁免同行的其它真实引用(2026-09-25 实测修正)。
### 4.4 生成物与生成器:必须同一笔提交(`generated-fresh`)
**规矩:改了生成器(或它的输入)就必须重跑,并把生成物与生成器放进同一笔提交。** 判据不是
「看起来差不多」而是**逐字节**:`generated-fresh` 钩子把生成器拷进**系统临时目录**重跑一遍,
与工作区逐字节比对,任何差异(含「重跑会新增文件」)即拦提交。钩子本身不写仓库,因此它不需要
还原现场、也不会 stash 别人在飞的改动(多人共用一棵工作树也能跑)。
为什么这条要机械化(两条实证,不是假想):
- `0f7b5a87`(编号迁移)改了 `tools/menu_gen/gen_menu.py` 的模板文字却没重跑生成物 → HEAD 里
`Drivers/BSP/UI/ui_menu_tab.h` 与模板不一致,后来是别人重跑生成器才被顺带纠正(属主确认以
生成器输出为准)。
- 2026-10-26 接线前实测又扫出 5 张 `docs/images/*.svg` 与各自生成器不一致——全是文档挪目录后
`scripts/rename_ref.py` 改写了 `tools/` 里的引用路径、**产物没跟着重跑**(见 §3.6)。
事故形态:**改生成器的人看不到任何 diff**(脚本改动编译、单测全过,因为产物没变),代价却
转嫁给下一个碰巧重跑生成器的人——他要么连带提交一段「不是我改的」改动,要么 revert 掉别人
真正的修复。两种处置都错,而门禁能让他不必遇到这个选择。
在册生成器(判定表 = `scripts/check_generated_fresh.py` 里的 `GENS`;新增生成器同笔在此登记一行):
| 生成器 | 生成物 | 一并核对的副作用 |
|---|---|---|
| `tools/menu_gen/gen_menu.py` | `Drivers/BSP/UI/ui_menu_tab.h`、`Drivers/BSP/UI/ui_menu_tab.c`、`Drivers/BSP/UI/ui_page_registry.h`、`Drivers/BSP/UI/ui_page_registry.c` | 回写 `MDK-ARM/STM32F407-Demo.uvprojx` 与 `simulator/CMakeLists.txt`(双端注册表一起比) |
| `tools/font_gen/extract_cn.py` | `ui_cn_chars.txt` | **唯一不按裸字节判**的一项,见下 |
| `tools/font_gen/make_font.py` | `Drivers/BSP/UI/ui_font_cn_12.c` | 需 node + 全局 lv_font_conv + 系统宋体,缺则该项 SKIP |
| `tools/arch_diagram/gen_*.py`(逐个) | `docs/images/*.svg` | 5 条存量在基线、只减不增;业务生成物零豁免面 |
两点配套:
- **改名 / 挪目录之后**(§3.6):`scripts/rename_ref.py` 会改写 `tools/` 里生成器源码的引用
路径,但**不会**重跑任何生成器 → 只要 `tools/` 被它改过,就要重跑对应生成器再提交。
- **字库清单不是裸字节判定**:`ui_cn_chars.txt` 按 **font-regen** 约定 = 源码在用 ∪ `KEEP_EXTRA`
(历史落盘记录用字),钩子改判「txt 恰等于 重跑结果 ∪ `KEEP_EXTRA`」,两向都不许差;清单一律
直载 `scripts/check_font_charset.py`,不留第二份(两套清单必然漂移,教训见 `docs/代码规范/pre-commit使用说明.md` §5.5)。
逐项详解(八段:查什么 / 拦下过什么真事 / 触发面 / 报错怎么读 / 限制与盲区 / 豁免与基线)
→ `docs/门禁说明/generated-fresh.md`。
---
## 5. 文档结构
@@ -0,0 +1,153 @@
# provenance (ASCII header; payload below is the verbatim boot_log from run_boot_test.py)
# commit : 77779d49 (branch v2-on-ui) -- built in a READ-ONLY detached worktree
# D:/Code/DTU Code/_wt/bench-21-77779d49 (main tree had another lane's in-flight
# doc edits at the time, so the image was deliberately NOT built from the main tree)
# build : @build 2026-10-26 16:27, rc=0, "0 Error(s), 0 Warning(s)"
# Program Size: Code=228260 RO-data=24960 RW-data=1440 ZI-data=135976
# image stamp : fw build: Oct 26 2026 16:27:01 suite=NONE dstr=off (product build, destructive OFF;
# extracted from the .hex binary AND matched on the wire)
# flash : stlink_flash.py flash = -P 0x08000000 -Rst -Run ; Programming Complete ; rc=0
# readback : stlink_flash.py cmp (-CmpFile) run AFTER log capture -> READBACK: OK (no difference found)
# verdict : PASS (run_boot_test rc=0) -- 9/9 Required, Forbidden 0 hits ([ERR]=0 Fault=0 FAIL=0 ERROR=0),
# anchor 'reset cause:' x1 (CSR=0x14000000 software NVIC_SystemReset), marker order ok,
# boot time to 'net init: OK' = +762 ms ; water levels: see the [STACK] lines below
[+0ms] [----/--/-- --:--:--][APP] boot: syslog init OK
[+0ms] [----/--/-- --:--:--][APP] fw build: Oct 26 2026 16:27:01 suite=NONE dstr=off
[+11ms] [----/--/-- --:--:--][APP] reset cause: software (NVIC_SystemReset) (CSR=0x14000000)
[+11ms] PREVRUN seg=2 seq=2156097806 t=77731647
[+21ms] TICKRUN uw=77731656 n=77731656 duw=9
[+21ms] [----/--/-- --:--:--][RTC] sd2506_init: OK
[+32ms] [----/--/-- --:--:--][APP] SD2506 init: OK (ret=0)
[+32ms] [2026-10-26 08:28:27][CLK] sys_clock_init: RTC initial time 2026-10-26 08:28:27
[+42ms] [2026-10-26 08:28:27][CLK] sys_clock_init: wall clock base = unix 1793003307, base_tick=139
[+53ms] [2026-10-26 08:28:27][APP] RS485 init: OK (UART4, DE=PA15)
[+63ms] [2026-10-26 08:28:27][NAND] Resetting NAND...
[+84ms] [2026-10-26 08:28:27][NAND] Reading NAND ID...
[+84ms] [2026-10-26 08:28:27][NAND] NAND ID: MID=0xC8, DID=0x52
[+94ms] [2026-10-26 08:28:27][NAND] Scanning bad blocks...
[+229ms] [2026-10-26 08:28:27][NAND] BBT scan: 0 bad blocks found
[+229ms] [2026-10-26 08:28:27][NAND] Enabling ECC...
[+229ms] [2026-10-26 08:28:27][NAND] Unlocking block protection...
[+240ms] [2026-10-26 08:28:27][NAND] Locating BBT pool...
[+240ms] [2026-10-26 08:28:27][NAND] Loading persisted BBT...
[+250ms] [2026-10-26 08:28:27][NAND] NAND init OK
[+250ms] [2026-10-26 08:28:27][APP] NAND init: OK
[+261ms] [2026-10-26 08:28:27][ADC] Init OK
[+261ms] [2026-10-26 08:28:27][APP] ADC init: OK
[+261ms] [2026-10-26 08:28:27][APP] ADC input layer init: OK
[+271ms] [2026-10-26 08:28:27][APP] phasor layer init: OK (inst=5)
[+271ms] [2026-10-26 08:28:27][APP] freq layer init: OK
[+282ms] [2026-10-26 08:28:27][CFG] register: section 'adc_cal' (9 keys)
[+282ms] [2026-10-26 08:28:27][CFG] register: section 'adc_cal_ofs' (8 keys)
[+292ms] [2026-10-26 08:28:27][APP] adc cal init: OK (k_q16 tpafe=10000000 stm32=52800000)
[+302ms] [2026-10-26 08:28:27][CFG] register: section 'run_par' (9 keys)
[+302ms] [2026-10-26 08:28:27][APP] run par init: OK (db9 k/b via adc_cal, evt=1<<8)
[+313ms] [2026-10-26 08:28:27][APP] protection init: OK (set=40 elm=9)
[+323ms] [2026-10-26 08:28:27][APP] SD2506 ID: 2512153425482405
[+323ms] [2026-10-26 08:28:27][APP] SD2506 time: 2026-10-26 08:28:27 (week 1)
[+333ms] [2026-10-26 08:28:27][APP] SD2506 temp: 25 C
[+333ms] [2026-10-26 08:28:27][APP] SD2506 battery: 3.12 V
[+344ms] [2026-10-26 08:28:27][APP] App init OK
[+344ms] [2026-10-26 08:28:27][NAND_FTL] Initializing NAND FTL...
[+365ms] [2026-10-26 08:28:27][PROT] uncalibrated -> protection blocked (UA/UN/IA VALID not all set)
[+450ms] [2026-10-26 08:28:27][NAND_FTL] Map resumed: 14094/97504 sectors used
[+450ms] [2026-10-26 08:28:27][NAND_FTL] Map head=48672 tail=1651 epoch=7
[+461ms] [2026-10-26 08:28:27][NAND_FTL] NAND FTL ready (capacity: 97504 pages = 190 MB)
[+534ms] [2026-10-26 08:28:27] [FS] FATFS mounted
[+607ms] [2026-10-26 08:28:27][SYSLOG] runlog boot entry: power on
[+607ms] [2026-10-26 08:28:27][CFG] register: section 'network' (4 keys)
[+607ms] [2026-10-26 08:28:27][CFG] register: section 'ftp' (3 keys)
[+617ms] [2026-10-26 08:28:27][CFG] load: OK (957B, heap free 16552B)
[+628ms] [2026-10-26 08:28:27][NET_CFG] load: OK ip=192.168.1.100
[+638ms] [2026-10-26 08:28:27] [CFG] prot set loaded (/prot_set.bin)
[+700ms] [2026-10-26 08:28:27][ADC_TASK] first: UA=11428 UB=1082 UC=890 UN=11454 IA=11451 IB=1001 IC=1010 IN=936 T=962
[+710ms] [2026-10-26 08:28:27][ADC_TASK] phasor(first) C01-IA-F1: Re=0 Im=0 rms=0 (x0.1)
[+710ms] [2026-10-26 08:28:27][ADC_TASK] phasor(first) C02-UA-F1: Re=0 Im=0 rms=0 (x0.1)
[+720ms] [2026-10-26 08:28:27][ADC_TASK] phasor(first) C03-UN-F1: Re=0 Im=0 rms=0 (x0.1)
[+731ms] [2026-10-26 08:28:27][ADC_TASK] phasor(first) C04-IA-F2: Re=0 Im=0 rms=0 (x0.1)
[+731ms] [2026-10-26 08:28:27][ADC_TASK] phasor(first) C05-IA-F5: Re=0 Im=0 rms=0 (x0.1)
[+741ms] [2026-10-26 08:28:27][ADC_TASK] freq(first): f=0 valid=0 (x0.01)
[+751ms] [2026-10-26 08:28:27][NET_TASK] === Network Init ===
[+751ms] [2026-10-26 08:28:27][PROT] protect task start: cycle=20ms elm=9
[+751ms] [2026-10-26 08:28:27][NET] ch395f ver=0x4A fun_para=0x08
[+762ms] [2026-10-26 08:28:27][NET_TASK] net init: OK
[+772ms] [2026-10-26 08:28:27][NET_TASK] MAC: 3C:AB:72:53:2B:12
[+772ms] [2026-10-26 08:28:27][NET_TASK] IP_INF: IP=192.168.1.100 GW=192.168.1.1 MASK=255.255.255.0
[+782ms] MISS start snap=0x000001E7 req=0x000001F7 t=886
[+782ms] [2026-10-26 08:28:27][WD] [WD] armed: timeout ~2498ms (worst ~1701ms), miss_tol=800ms
[+824ms] [2026-10-26 08:28:27][UI_TASK] UI task: started, wait net ready
[+2700ms] [2026-10-26 08:28:29][NET_TASK] PHY: 100M FULL
[+2711ms] [2026-10-26 08:28:29][NET_TASK] netTask: started (single-socket mode)
[+2752ms] [2026-10-26 08:28:29] [FTP] using factory default password
[+2783ms] [2026-10-26 08:28:29][TIME_SYNC] time_sync: TCP listen on :8888 (sock=0)
[+2814ms] [2026-10-26 08:28:29][I104] i104: listening on 2404
[+2814ms] MISS start snap=0x000001E7 req=0x000001F7 t=2923
[+2825ms] [PROF] t=2928 uic=0 uip=0 uipn=0 uil=0 uiw=0 uis=0 uisn=0 adc=2477 adcm=2155 adcn=0 gl=0
[+2825ms] [PROF2] t=2928 defd=130939 defs=35408 netp=1994 netn=0 adcp=11 adcq=0 gl=0
[+2835ms] [2026-10-26 08:28:29] [FTP] waiting for connection...
[+2846ms] [2026-10-26 08:28:29][UI_TASK] UI task: net ready, init port
[+2846ms] [2026-10-26 08:28:29][UI_TASK] UI task: ui_port_init enter
[+2856ms] [2026-10-26 08:28:29][UI_PORT] theme: mono dark_bg=true applied
[+2867ms] [2026-10-26 08:28:29][UI_TASK] UI port init: OK
[+2867ms] [2026-10-26 08:28:29][UI_TASK] UI task: ui_menu_init enter
[+2877ms] [2026-10-26 08:28:29][UI_MENU] menu build start: total=15136 free=12692 big=12692
[+2887ms] [2026-10-26 08:28:29][UI_MENU_VIEW] view init: root frame w=66 h=107
[+2887ms] [2026-10-26 08:28:29][UI_MENU] menu build done: total=15020 free=11828 big=11816 used=3192
[+2898ms] [2026-10-26 08:28:29][UI_TASK] UI menu init: OK
[+2908ms] [2026-10-26 08:28:29][UI_STANDBY] standby: ready (idle=60000ms)
[+2908ms] [2026-10-26 08:28:29][UI_TASK] UI standby: ready (idle=60s)
[+2960ms] [2026-10-26 08:28:29][UI_TASK] UI net sink: started (port 7003)
[+4917ms] [PROF] t=5020 uic=176 uip=22044 uipn=1839 uil=11000 uiw=12134 uis=1184 uisn=0 adc=4568 adcm=2155 adcn=0 gl=0
[+4932ms] [PROF2] t=5020 defd=130939 defs=35408 netp=2110 netn=0 adcp=11 adcq=0 gl=0
[+5380ms] [2026-10-26 08:28:32] [STACK] default peak_used=2160/5120 free_min=2960 (57%)
[+5390ms] [2026-10-26 08:28:32] [STACK] net peak_used=1048/3840 free_min=2792 (72%)
[+5401ms] [2026-10-26 08:28:32] [STACK] ftp peak_used=740/4096 free_min=3356 (81%)
[+5401ms] [2026-10-26 08:28:32] [STACK] ui peak_used=4088/16384 free_min=12296 (75%)
[+5411ms] [2026-10-26 08:28:32] [STACK] timesync peak_used=616/2560 free_min=1944 (75%)
[+5421ms] [2026-10-26 08:28:32] [STACK] adc peak_used=1000/2560 free_min=1560 (60%)
[+5421ms] [2026-10-26 08:28:32] [STACK] iec104 peak_used=540/2560 free_min=2020 (78%)
[+5432ms] [2026-10-26 08:28:32] [STACK] protect peak_used=492/2048 free_min=1556 (75%)
[+5442ms] [2026-10-26 08:28:32] [STACK] heap free=15624 min_ever=8464
[+6947ms] [PROF] t=7050 uic=354 uip=73720 uipn=3839 uil=10999 uiw=12134 uis=1184 uisn=0 adc=6598 adcm=2155 adcn=0 gl=0
[+6957ms] [PROF2] t=7050 defd=130939 defs=35408 netp=2110 netn=0 adcp=11 adcq=0 gl=0
[+8960ms] [PROF] t=9068 uic=536 uip=73720 uipn=9838 uil=11000 uiw=12134 uis=1199 uisn=0 adc=8616 adcm=2155 adcn=0 gl=0
[+8970ms] [PROF2] t=9068 defd=130939 defs=35408 netp=2110 netn=0 adcp=11 adcq=0 gl=0
[+10350ms] [2026-10-26 08:28:37][ADC_TASK] frames=10001 idle=0 skip=38 seq=10006 frst_err=0 ring=9963 adc1_rc=0 adc1_err=0 adc1_seq=10004 adc1_gap=40 adc1_hw=0 tgap=1
[+10360ms] [2026-10-26 08:28:37][ADC_TASK] phasor(hb) C01-IA-F1: Re=0 Im=0 rms=0 (x0.1)
[+10370ms] [2026-10-26 08:28:37][ADC_TASK] phasor(hb) C02-UA-F1: Re=0 Im=0 rms=0 (x0.1)
[+10370ms] [2026-10-26 08:28:37][ADC_TASK] phasor(hb) C03-UN-F1: Re=0 Im=0 rms=0 (x0.1)
[+10381ms] [2026-10-26 08:28:37][ADC_TASK] phasor(hb) C04-IA-F2: Re=0 Im=0 rms=0 (x0.1)
[+10391ms] [2026-10-26 08:28:37][ADC_TASK] phasor(hb) C05-IA-F5: Re=0 Im=0 rms=0 (x0.1)
[+10402ms] [2026-10-26 08:28:37][ADC_TASK] freq(hb): f=0 valid=0 (x0.01)
[+10402ms] [2026-10-26 08:28:37][ADC_TASK] cal: ua_mv=1743 un_mv=1747 ia_mv=1747 vl=0x00 cl=0xe6 rail=0x000 edge=0x000
[+10412ms] [2026-10-26 08:28:37][ADC_TASK] meas: seq=50 v=1 ua_mv=0 un_mv=0 ia_mv=0 f=0 fv=0
[+10969ms] [PROF] t=11074 uic=711 uip=75494 uipn=10839 uil=10924 uiw=12134 uis=1199 uisn=0 adc=10621 adcm=2786 adcn=0 gl=0
[+10980ms] [PROF2] t=11074 defd=130939 defs=35408 netp=4013 netn=0 adcp=11 adcq=0 gl=0
[+12983ms] [PROF] t=13092 uic=893 uip=75494 uipn=9839 uil=10999 uiw=12134 uis=1199 uisn=0 adc=12639 adcm=2786 adcn=0 gl=0
[+12993ms] [PROF2] t=13092 defd=130939 defs=35408 netp=4013 netn=0 adcp=11 adcq=0 gl=0
[+15004ms] [PROF] t=15110 uic=1075 uip=75494 uipn=9839 uil=10999 uiw=12134 uis=1199 uisn=0 adc=14657 adcm=2786 adcn=0 gl=0
[+15014ms] [PROF2] t=15110 defd=130939 defs=35408 netp=4013 netn=0 adcp=11 adcq=0 gl=0
[+17027ms] [PROF] t=17128 uic=1257 uip=75494 uipn=9838 uil=11000 uiw=12134 uis=1199 uisn=0 adc=16675 adcm=2786 adcn=0 gl=0
[+17027ms] [PROF2] t=17128 defd=130939 defs=35408 netp=4013 netn=0 adcp=11 adcq=0 gl=0
[+19043ms] [PROF] t=19146 uic=1439 uip=75494 uipn=9839 uil=10998 uiw=12134 uis=1199 uisn=0 adc=18693 adcm=2786 adcn=0 gl=0
[+19054ms] [PROF2] t=19146 defd=130939 defs=35408 netp=4013 netn=0 adcp=11 adcq=0 gl=0
[+20351ms] [2026-10-26 08:28:47][ADC_TASK] frames=20000 idle=0 skip=54 seq=20006 frst_err=0 ring=19946 adc1_rc=0 adc1_err=0 adc1_seq=20004 adc1_gap=57 adc1_hw=0 tgap=2
[+20361ms] [2026-10-26 08:28:47][ADC_TASK] phasor(hb) C01-IA-F1: Re=0 Im=0 rms=0 (x0.1)
[+20372ms] [2026-10-26 08:28:47][ADC_TASK] phasor(hb) C02-UA-F1: Re=0 Im=0 rms=0 (x0.1)
[+20372ms] [2026-10-26 08:28:47][ADC_TASK] phasor(hb) C03-UN-F1: Re=0 Im=0 rms=0 (x0.1)
[+20382ms] [2026-10-26 08:28:47][ADC_TASK] phasor(hb) C04-IA-F2: Re=0 Im=0 rms=0 (x0.1)
[+20392ms] [2026-10-26 08:28:47][ADC_TASK] phasor(hb) C05-IA-F5: Re=0 Im=0 rms=0 (x0.1)
[+20392ms] [2026-10-26 08:28:47][ADC_TASK] freq(hb): f=0 valid=0 (x0.01)
[+20403ms] [2026-10-26 08:28:47][ADC_TASK] cal: ua_mv=1743 un_mv=1747 ia_mv=1747 vl=0x00 cl=0xe6 rail=0x000 edge=0x000
[+20413ms] [2026-10-26 08:28:47][ADC_TASK] meas: seq=100 v=1 ua_mv=0 un_mv=0 ia_mv=0 f=0 fv=0
[+21074ms] [PROF] t=21174 uic=1617 uip=75494 uipn=840 uil=10998 uiw=12134 uis=1199 uisn=584 adc=20720 adcm=2804 adcn=0 gl=0
[+21074ms] [PROF2] t=21174 defd=130939 defs=35408 netp=4029 netn=0 adcp=11 adcq=0 gl=0
[+23084ms] [PROF] t=23192 uic=1798 uip=75494 uipn=9839 uil=10999 uiw=18756 uis=18501 uisn=0 adc=22738 adcm=2804 adcn=0 gl=0
[+23094ms] [PROF2] t=23192 defd=130939 defs=35408 netp=4029 netn=0 adcp=11 adcq=0 gl=0
[+25102ms] [PROF] t=25210 uic=1980 uip=75494 uipn=9838 uil=11000 uiw=18756 uis=18501 uisn=0 adc=24756 adcm=2804 adcn=0 gl=0
[+25112ms] [PROF2] t=25210 defd=130939 defs=35408 netp=4029 netn=0 adcp=11 adcq=0 gl=0
[+27123ms] [PROF] t=27228 uic=2162 uip=75494 uipn=9839 uil=10998 uiw=18756 uis=18501 uisn=0 adc=26774 adcm=2804 adcn=0 gl=0
[+27133ms] [PROF2] t=27228 defd=130939 defs=35408 netp=4029 netn=0 adcp=11 adcq=0 gl=0
[+29146ms] [PROF] t=29246 uic=2344 uip=75494 uipn=9839 uil=11000 uiw=18756 uis=18501 uisn=0 adc=28792 adcm=2804 adcn=0 gl=0
[+29146ms] [PROF2] t=29246 defd=130939 defs=35408 netp=4029 netn=0 adcp=11 adcq=0 gl=0
@@ -0,0 +1,180 @@
# provenance (ASCII header)
# commit/image : same 77779d49 image as 20261026_prod_77779d49_boot_log.txt
# (fw build: Oct 26 2026 16:27:01 suite=NONE dstr=off -- re-read from the wire, identical)
# how produced : THIS BOOT WAS NOT REFLASHED. serial clear (gen=262) -> stlink_flash.py reset
# (-c SWD -Rst -Run, software reset only, zero flash writes) -> wait-idle -> log
# payload = serial-assistant daemon lines, taken verbatim from
# .dsh/skills/serial-assistant/scripts/state/serial.log
# why it matters: task-21 criterion 2 -- "saved value survives reboot" is only meaningful if the reboot
# is a reset, not a reflash. Proof lines to look for in this file:
# [CFG] prot set loaded (/prot_set.bin) <-- blob parsed + checksum pair verified
# no "prot set blob CORRUPT", no "version change", no [ERR], reset cause x1
# [PROT] uncalibrated -> protection blocked ... (uncalibrated interlock still asserted,
# yet the master could still C_SE-write setpoints -- see the iec104 leg file)
# note : the [PROT] line here is byte-interleaved -- that is the defect evidence archived as
# 20261026_prod_console_interleave_evidence.txt, not a capture artifact.
[SYS] 2026-10-26T16:37:08.057+08:00 log cleared (gen=262)
[RX] 2026-10-26T16:37:08.662+08:00 [----/--/-- --:--:--][APP] boot: syslog init OK
[RX] 2026-10-26T16:37:08.662+08:00 [----/--/-- --:--:--][APP] fw build: Oct 26 2026 16:27:01 suite=NONE dstr=off
[RX] 2026-10-26T16:37:08.662+08:00 [----/--/-- --:--:--][APP] reset cause: software (NVIC_SystemReset) (CSR=0x14000000)
[RX] 2026-10-26T16:37:08.662+08:00 PREVRUN seg=2 seq=2156171440 t=487230
[RX] 2026-10-26T16:37:08.662+08:00 TICKRUN uw=28 n=28 duw=4294480094
[RX] 2026-10-26T16:37:08.686+08:00 [----/--/-- --:--:--][RTC] sd2506_init: OK
[RX] 2026-10-26T16:37:08.686+08:00 [----/--/-- --:--:--][APP] SD2506 init: OK (ret=0)
[RX] 2026-10-26T16:37:08.686+08:00 [2026-10-26 08:37:07][CLK] sys_clock_init: RTC initial time 2026-10-26 08:37:07
[RX] 2026-10-26T16:37:08.686+08:00 [2026-10-26 08:37:07][CLK] sys_clock_init: wall clock base = unix 1793003827, base_tick=139
[RX] 2026-10-26T16:37:08.717+08:00 [2026-10-26 08:37:07][APP] RS485 init: OK (UART4, DE=PA15)
[RX] 2026-10-26T16:37:08.717+08:00 [2026-10-26 08:37:07][NAND] Resetting NAND...
[RX] 2026-10-26T16:37:08.733+08:00 [2026-10-26 08:37:07][NAND] Reading NAND ID...
[RX] 2026-10-26T16:37:08.733+08:00 [2026-10-26 08:37:07][NAND] NAND ID: MID=0xC8, DID=0x52
[RX] 2026-10-26T16:37:08.733+08:00 [2026-10-26 08:37:07][NAND] Scanning bad blocks...
[RX] 2026-10-26T16:37:08.867+08:00 [2026-10-26 08:37:07][NAND] BBT scan: 0 bad blocks found
[RX] 2026-10-26T16:37:08.879+08:00 [2026-10-26 08:37:07][NAND] Enabling ECC...
[RX] 2026-10-26T16:37:08.879+08:00 [2026-10-26 08:37:07][NAND] Unlocking block protection...
[RX] 2026-10-26T16:37:08.891+08:00 [2026-10-26 08:37:07][NAND] Locating BBT pool...
[RX] 2026-10-26T16:37:08.891+08:00 [2026-10-26 08:37:07][NAND] Loading persisted BBT...
[RX] 2026-10-26T16:37:08.902+08:00 [2026-10-26 08:37:07][NAND] NAND init OK
[RX] 2026-10-26T16:37:08.902+08:00 [2026-10-26 08:37:07][APP] NAND init: OK
[RX] 2026-10-26T16:37:08.902+08:00 [2026-10-26 08:37:07][ADC] Init OK
[RX] 2026-10-26T16:37:08.902+08:00 [2026-10-26 08:37:07][APP] ADC init: OK
[RX] 2026-10-26T16:37:08.912+08:00 [2026-10-26 08:37:07][APP] ADC input layer init: OK
[RX] 2026-10-26T16:37:08.912+08:00 [2026-10-26 08:37:07][APP] phasor layer init: OK (inst=5)
[RX] 2026-10-26T16:37:08.923+08:00 [2026-10-26 08:37:07][APP] freq layer init: OK
[RX] 2026-10-26T16:37:08.923+08:00 [2026-10-26 08:37:07][CFG] register: section 'adc_cal' (9 keys)
[RX] 2026-10-26T16:37:08.934+08:00 [2026-10-26 08:37:07][CFG] register: section 'adc_cal_ofs' (8 keys)
[RX] 2026-10-26T16:37:08.934+08:00 [2026-10-26 08:37:07][APP] adc cal init: OK (k_q16 tpafe=10000000 stm32=52800000)
[RX] 2026-10-26T16:37:08.946+08:00 [2026-10-26 08:37:07][CFG] register: section 'run_par' (9 keys)
[RX] 2026-10-26T16:37:08.946+08:00 [2026-10-26 08:37:07][APP] run par init: OK (db9 k/b via adc_cal, evt=1<<8)
[RX] 2026-10-26T16:37:08.957+08:00 [2026-10-26 08:37:07][APP] protection init: OK (set=40 elm=9)
[RX] 2026-10-26T16:37:08.967+08:00 [2026-10-26 08:37:07][APP] SD2506 ID: 2512153425482405
[RX] 2026-10-26T16:37:08.967+08:00 [2026-10-26 08:37:07][APP] SD2506 time: 2026-10-26 08:37:08 (week 1)
[RX] 2026-10-26T16:37:08.978+08:00 [2026-10-26 08:37:07][APP] SD2506 temp: 25 C
[RX] 2026-10-26T16:37:08.978+08:00 [2026-10-26 08:37:07][APP] SD2506 battery: 3.12 V
[RX] 2026-10-26T16:37:08.989+08:00 [2026-10-26 08:37:07][APP] App init OK
[RX] 2026-10-26T16:37:08.989+08:00 [2026-10-26 08:37:07][PROT] uncalibrated -> protection [blocked (UA/UN/IA VALI2D not all s0et)
[RX] 2026-10-26T16:37:09.000+08:00 26-10-26 08:37:07][NAND_FTL] Initializing NAND FTL...
[RX] 2026-10-26T16:37:09.093+08:00 [2026-10-26 08:37:07][NAND_FTL] Map resumed: 14094/97504 sectors used
[RX] 2026-10-26T16:37:09.093+08:00 [2026-10-26 08:37:07][NAND_FTL] Map head=48992 tail=1944 epoch=7
[RX] 2026-10-26T16:37:09.104+08:00 [2026-10-26 08:37:07][NAND_FTL] NAND FTL ready (capacity: 97504 pages = 190 MB)
[RX] 2026-10-26T16:37:09.187+08:00 [2026-10-26 08:37:07] [FS] FATFS mounted
[RX] 2026-10-26T16:37:09.260+08:00 [2026-10-26 08:37:07][SYSLOG] runlog boot entry: reboot
[RX] 2026-10-26T16:37:09.260+08:00 [2026-10-26 08:37:07][CFG] register: section 'network' (4 keys)
[RX] 2026-10-26T16:37:09.270+08:00 [2026-10-26 08:37:07][CFG] register: section 'ftp' (3 keys)
[RX] 2026-10-26T16:37:09.281+08:00 [2026-10-26 08:37:07][CFG] load: OK (957B, heap free 16552B)
[RX] 2026-10-26T16:37:09.281+08:00 [2026-10-26 08:37:07][NET_CFG] load: OK ip=192.168.1.100
[RX] 2026-10-26T16:37:09.291+08:00 [2026-10-26 08:37:07] [CFG] prot set loaded (/prot_set.bin)
[RX] 2026-10-26T16:37:09.312+08:00 [2026-10-26 08:37:07][ADC_TASK] first: UA=11428 UB=1070 UC=965 UN=11455 IA=11450 IB=1047 IC=1010 IN=991 T=1012
[RX] 2026-10-26T16:37:09.322+08:00 [2026-10-26 08:37:07][ADC_TASK] phasor(first) C01-IA-F1: Re=0 Im=0 rms=0 (x0.1)
[RX] 2026-10-26T16:37:09.333+08:00 [2026-10-26 08:37:07][ADC_TASK] phasor(first) C02-UA-F1: Re=0 Im=0 rms=0 (x0.1)
[RX] 2026-10-26T16:37:09.333+08:00 [2026-10-26 08:37:07][ADC_TASK] phasor(first) C03-UN-F1: Re=0 Im=0 rms=0 (x0.1)
[RX] 2026-10-26T16:37:09.343+08:00 [2026-10-26 08:37:07][ADC_TASK] phasor(first) C04-IA-F2: Re=0 Im=0 rms=0 (x0.1)
[RX] 2026-10-26T16:37:09.354+08:00 [2026-10-26 08:37:07][ADC_TASK] phasor(first) C05-IA-F5: Re=0 Im=0 rms=0 (x0.1)
[RX] 2026-10-26T16:37:09.354+08:00 [2026-10-26 08:37:07][ADC_TASK] freq(first): f=0 valid=0 (x0.01)
[RX] 2026-10-26T16:37:09.364+08:00 [2026-10-26 08:37:07][NET_TASK] === Network Init ===
[RX] 2026-10-26T16:37:09.364+08:00 [2026-10-26 08:37:07][PROT] protect task start: cycle=20ms elm=9
[RX] 2026-10-26T16:37:09.375+08:00 [2026-10-26 08:37:07][NET] ch395f ver=0x4A fun_para=0x08
[RX] 2026-10-26T16:37:09.375+08:00 [2026-10-26 08:37:07][NET_TASK] net init: OK
[RX] 2026-10-26T16:37:09.385+08:00 [2026-10-26 08:37:07][NET_TASK] MAC: 3C:AB:72:53:2B:12
[RX] 2026-10-26T16:37:09.385+08:00 [2026-10-26 08:37:07][NET_TASK] IP_INF: IP=192.168.1.100 GW=192.168.1.1 MASK=255.255.255.0
[RX] 2026-10-26T16:37:09.396+08:00 MISS start snap=0x000001E7 req=0x000001F7 t=855
[RX] 2026-10-26T16:37:09.406+08:00 [2026-10-26 08:37:07][WD] [WD] armed: timeout ~2498ms (worst ~1701ms), miss_tol=800ms
[RX] 2026-10-26T16:37:09.448+08:00 [2026-10-26 08:37:07][UI_TASK] UI task: started, wait net ready
[RX] 2026-10-26T16:37:11.345+08:00 [2026-10-26 08:37:09][NET_TASK] PHY: 100M FULL
[RX] 2026-10-26T16:37:11.365+08:00 [2026-10-26 08:37:09][NET_TASK] netTask: started (single-socket mode)
[RX] 2026-10-26T16:37:11.396+08:00 [2026-10-26 08:37:09] [FTP] using factory default password
[RX] 2026-10-26T16:37:11.428+08:00 [2026-10-26 08:37:09][TIME_SYNC] time_sync: TCP listen on :8888 (sock=0)
[RX] 2026-10-26T16:37:11.460+08:00 [2026-10-26 08:37:09][I104] i104: listening on 2404
[RX] 2026-10-26T16:37:11.470+08:00 MISS start snap=0x000001E7 req=0x000001F7 t=2926
[RX] 2026-10-26T16:37:11.470+08:00 [PROF] t=2931 uic=0 uip=0 uipn=0 uil=0 uiw=0 uis=0 uisn=0 adc=2481 adcm=2091 adcn=0 gl=0
[RX] 2026-10-26T16:37:11.481+08:00 [PROF2] t=2931 defd=131247 defs=39005 netp=1010 netn=391 adcp=14 adcq=0 gl=0
[RX] 2026-10-26T16:37:11.491+08:00 [2026-10-26 08:37:09] [FTP] waiting for connection...
[RX] 2026-10-26T16:37:11.491+08:00 [2026-10-26 08:37:09][UI_TASK] UI task: net ready, init port
[RX] 2026-10-26T16:37:11.502+08:00 [2026-10-26 08:37:09][UI_TASK] UI task: ui_port_init enter
[RX] 2026-10-26T16:37:11.512+08:00 [2026-10-26 08:37:09][UI_PORT] theme: mono dark_bg=true applied
[RX] 2026-10-26T16:37:11.512+08:00 [2026-10-26 08:37:09][UI_TASK] UI port init: OK
[RX] 2026-10-26T16:37:11.512+08:00 [2026-10-26 08:37:09][UI_TASK] UI task: ui_menu_init enter
[RX] 2026-10-26T16:37:11.523+08:00 [2026-10-26 08:37:09][UI_MENU] menu build start: total=15136 free=12692 big=12692
[RX] 2026-10-26T16:37:11.533+08:00 [2026-10-26 08:37:09][UI_MENU_VIEW] view init: root frame w=66 h=107
[RX] 2026-10-26T16:37:11.544+08:00 [2026-10-26 08:37:09][UI_MENU] menu build done: total=15020 free=11828 big=11816 used=3192
[RX] 2026-10-26T16:37:11.544+08:00 [2026-10-26 08:37:09][UI_TASK] UI menu init: OK
[RX] 2026-10-26T16:37:11.554+08:00 [2026-10-26 08:37:09][UI_STANDBY] standby: ready (idle=60000ms)
[RX] 2026-10-26T16:37:11.564+08:00 [2026-10-26 08:37:09][UI_TASK] UI standby: ready (idle=60s)
[RX] 2026-10-26T16:37:11.606+08:00 [2026-10-26 08:37:09][UI_TASK] UI net sink: started (port 7003)
[RX] 2026-10-26T16:37:13.586+08:00 [PROF] t=5023 uic=176 uip=21980 uipn=2839 uil=10998 uiw=12070 uis=1181 uisn=0 adc=4573 adcm=2091 adcn=0 gl=0
[RX] 2026-10-26T16:37:13.586+08:00 [PROF2] t=5023 defd=131247 defs=39005 netp=1030 netn=0 adcp=14 adcq=0 gl=0
[RX] 2026-10-26T16:37:14.032+08:00 [2026-10-26 08:37:12] [STACK] default peak_used=2160/5120 free_min=2960 (57%)
[RX] 2026-10-26T16:37:14.043+08:00 [2026-10-26 08:37:12] [STACK] net peak_used=1116/3840 free_min=2724 (70%)
[RX] 2026-10-26T16:37:14.043+08:00 [2026-10-26 08:37:12] [STACK] ftp peak_used=740/4096 free_min=3356 (81%)
[RX] 2026-10-26T16:37:14.053+08:00 [2026-10-26 08:37:12] [STACK] ui peak_used=4188/16384 free_min=12196 (74%)
[RX] 2026-10-26T16:37:14.064+08:00 [2026-10-26 08:37:12] [STACK] timesync peak_used=616/2560 free_min=1944 (75%)
[RX] 2026-10-26T16:37:14.064+08:00 [2026-10-26 08:37:12] [STACK] adc peak_used=1000/2560 free_min=1560 (60%)
[RX] 2026-10-26T16:37:14.074+08:00 [2026-10-26 08:37:12] [STACK] iec104 peak_used=540/2560 free_min=2020 (78%)
[RX] 2026-10-26T16:37:14.085+08:00 [2026-10-26 08:37:12] [STACK] protect peak_used=424/2048 free_min=1624 (79%)
[RX] 2026-10-26T16:37:14.096+08:00 [2026-10-26 08:37:12] [STACK] heap free=15624 min_ever=8464
[RX] 2026-10-26T16:37:15.594+08:00 [PROF] t=7054 uic=354 uip=74016 uipn=3778 uil=11060 uiw=12070 uis=1183 uisn=0 adc=6604 adcm=2091 adcn=0 gl=0
[RX] 2026-10-26T16:37:15.604+08:00 [PROF2] t=7054 defd=131247 defs=39005 netp=1030 netn=0 adcp=14 adcq=0 gl=0
[RX] 2026-10-26T16:37:16.063+08:00 [2026-10-26 08:37:14] [STACK] default peak_used=2160/5120 free_min=2960 (57%)
[RX] 2026-10-26T16:37:16.074+08:00 [2026-10-26 08:37:14] [STACK] net peak_used=1116/3840 free_min=2724 (70%)
[RX] 2026-10-26T16:37:16.074+08:00 [2026-10-26 08:37:14] [STACK] ftp peak_used=740/4096 free_min=3356 (81%)
[RX] 2026-10-26T16:37:16.084+08:00 [2026-10-26 08:37:14] [STACK] ui peak_used=4188/16384 free_min=12196 (74%)
[RX] 2026-10-26T16:37:16.095+08:00 [2026-10-26 08:37:14] [STACK] timesync peak_used=616/2560 free_min=1944 (75%)
[RX] 2026-10-26T16:37:16.095+08:00 [2026-10-26 08:37:14] [STACK] adc peak_used=1000/2560 free_min=1560 (60%)
[RX] 2026-10-26T16:37:16.106+08:00 [2026-10-26 08:37:14] [STACK] iec104 peak_used=580/2560 free_min=1980 (77%)
[RX] 2026-10-26T16:37:16.116+08:00 [2026-10-26 08:37:14] [STACK] protect peak_used=424/2048 free_min=1624 (79%)
[RX] 2026-10-26T16:37:16.126+08:00 [2026-10-26 08:37:14] [STACK] heap free=15624 min_ever=8464
[RX] 2026-10-26T16:37:17.631+08:00 [PROF] t=9085 uic=532 uip=74545 uipn=3838 uil=10999 uiw=12070 uis=1198 uisn=0 adc=8635 adcm=2091 adcn=0 gl=0
[RX] 2026-10-26T16:37:17.641+08:00 [PROF2] t=9085 defd=131247 defs=39005 netp=1030 netn=0 adcp=14 adcq=0 gl=0
[RX] 2026-10-26T16:37:18.999+08:00 [2026-10-26 08:37:17][ADC_TASK] frames=10001 idle=0 skip=30 seq=10006 frst_err=0 ring=9971 adc1_rc=0 adc1_err=0 adc1_seq=10004 adc1_gap=32 adc1_hw=0 tgap=1
[RX] 2026-10-26T16:37:19.010+08:00 [2026-10-26 08:37:17][ADC_TASK] phasor(hb) C01-IA-F1: Re=0 Im=0 rms=0 (x0.1)
[RX] 2026-10-26T16:37:19.020+08:00 [2026-10-26 08:37:17][ADC_TASK] phasor(hb) C02-UA-F1: Re=0 Im=0 rms=0 (x0.1)
[RX] 2026-10-26T16:37:19.020+08:00 [2026-10-26 08:37:17][ADC_TASK] phasor(hb) C03-UN-F1: Re=0 Im=0 rms=0 (x0.1)
[RX] 2026-10-26T16:37:19.030+08:00 [2026-10-26 08:37:17][ADC_TASK] phasor(hb) C04-IA-F2: Re=0 Im=0 rms=0 (x0.1)
[RX] 2026-10-26T16:37:19.030+08:00 [2026-10-26 08:37:17][ADC_TASK] phasor(hb) C05-IA-F5: Re=0 Im=0 rms=0 (x0.1)
[RX] 2026-10-26T16:37:19.041+08:00 [2026-10-26 08:37:17][ADC_TASK] freq(hb): f=0 valid=0 (x0.01)
[RX] 2026-10-26T16:37:19.052+08:00 [2026-10-26 08:37:17][ADC_TASK] cal: ua_mv=1743 un_mv=1747 ia_mv=1747 vl=0x00 cl=0xe6 rail=0x000 edge=0x000
[RX] 2026-10-26T16:37:19.062+08:00 [2026-10-26 08:37:17][ADC_TASK] meas: seq=50 v=1 ua_mv=0 un_mv=0 ia_mv=0 f=0 fv=0
[RX] 2026-10-26T16:37:19.670+08:00 [PROF] t=11123 uic=710 uip=79033 uipn=5839 uil=10999 uiw=12070 uis=1198 uisn=0 adc=10672 adcm=2776 adcn=0 gl=0
[RX] 2026-10-26T16:37:19.680+08:00 [PROF2] t=11123 defd=131247 defs=39005 netp=3030 netn=0 adcp=14 adcq=0 gl=0
[RX] 2026-10-26T16:37:21.690+08:00 [PROF] t=13141 uic=892 uip=79033 uipn=9838 uil=10999 uiw=12070 uis=1198 uisn=0 adc=12690 adcm=2776 adcn=0 gl=0
[RX] 2026-10-26T16:37:21.690+08:00 [PROF2] t=13141 defd=131247 defs=39005 netp=3030 netn=0 adcp=14 adcq=0 gl=0
[RX] 2026-10-26T16:37:23.698+08:00 [PROF] t=15159 uic=1074 uip=79033 uipn=9839 uil=10893 uiw=12070 uis=1198 uisn=0 adc=14708 adcm=2776 adcn=0 gl=0
[RX] 2026-10-26T16:37:23.708+08:00 [PROF2] t=15159 defd=131247 defs=39005 netp=3030 netn=0 adcp=14 adcq=0 gl=0
[RX] 2026-10-26T16:37:25.722+08:00 [PROF] t=17177 uic=1256 uip=79033 uipn=9846 uil=10999 uiw=12070 uis=1198 uisn=0 adc=16726 adcm=2776 adcn=0 gl=0
[RX] 2026-10-26T16:37:25.732+08:00 [PROF2] t=17177 defd=131247 defs=39005 netp=3030 netn=0 adcp=14 adcq=0 gl=0
[RX] 2026-10-26T16:37:27.735+08:00 [PROF] t=19195 uic=1438 uip=79033 uipn=9839 uil=10998 uiw=12070 uis=1198 uisn=0 adc=18744 adcm=2776 adcn=0 gl=0
[RX] 2026-10-26T16:37:27.745+08:00 [PROF2] t=19195 defd=131247 defs=39005 netp=3030 netn=0 adcp=14 adcq=0 gl=0
[RX] 2026-10-26T16:37:29.000+08:00 [2026-10-26 08:37:27][ADC_TASK] frames=20000 idle=0 skip=31 seq=20006 frst_err=0 ring=19969 adc1_rc=0 adc1_err=0 adc1_seq=20004 adc1_gap=34 adc1_hw=0 tgap=2
[RX] 2026-10-26T16:37:29.011+08:00 [2026-10-26 08:37:27][ADC_TASK] phasor(hb) C01-IA-F1: Re=0 Im=0 rms=0 (x0.1)
[RX] 2026-10-26T16:37:29.011+08:00 [2026-10-26 08:37:27][ADC_TASK] phasor(hb) C02-UA-F1: Re=0 Im=0 rms=0 (x0.1)
[RX] 2026-10-26T16:37:29.021+08:00 [2026-10-26 08:37:27][ADC_TASK] phasor(hb) C03-UN-F1: Re=0 Im=0 rms=0 (x0.1)
[RX] 2026-10-26T16:37:29.032+08:00 [2026-10-26 08:37:27][ADC_TASK] phasor(hb) C04-IA-F2: Re=0 Im=0 rms=0 (x0.1)
[RX] 2026-10-26T16:37:29.032+08:00 [2026-10-26 08:37:27][ADC_TASK] phasor(hb) C05-IA-F5: Re=0 Im=0 rms=0 (x0.1)
[RX] 2026-10-26T16:37:29.043+08:00 [2026-10-26 08:37:27][ADC_TASK] freq(hb): f=0 valid=0 (x0.01)
[RX] 2026-10-26T16:37:29.053+08:00 [2026-10-26 08:37:27][ADC_TASK] cal: ua_mv=1743 un_mv=1747 ia_mv=1747 vl=0x00 cl=0xe6 rail=0x000 edge=0x000
[RX] 2026-10-26T16:37:29.053+08:00 [2026-10-26 08:37:27][ADC_TASK] meas: seq=100 v=1 ua_mv=0 un_mv=0 ia_mv=0 f=0 fv=0
[RX] 2026-10-26T16:37:29.765+08:00 [PROF] t=21223 uic=1615 uip=79033 uipn=6838 uil=11000 uiw=12070 uis=1198 uisn=0 adc=20771 adcm=2798 adcn=0 gl=0
[RX] 2026-10-26T16:37:29.775+08:00 [PROF2] t=21223 defd=131247 defs=39005 netp=3030 netn=0 adcp=14 adcq=0 gl=0
[RX] 2026-10-26T16:37:31.784+08:00 [PROF] t=23241 uic=1797 uip=79033 uipn=9839 uil=10998 uiw=12070 uis=1199 uisn=0 adc=22789 adcm=2798 adcn=0 gl=0
[RX] 2026-10-26T16:37:31.794+08:00 [PROF2] t=23241 defd=131247 defs=39005 netp=3030 netn=0 adcp=14 adcq=0 gl=0
[RX] 2026-10-26T16:37:33.805+08:00 [PROF] t=25259 uic=1979 uip=79033 uipn=9838 uil=10999 uiw=12070 uis=1199 uisn=0 adc=24807 adcm=2798 adcn=0 gl=0
[RX] 2026-10-26T16:37:33.815+08:00 [PROF2] t=25259 defd=131247 defs=39005 netp=3030 netn=0 adcp=14 adcq=0 gl=0
[RX] 2026-10-26T16:37:35.823+08:00 [PROF] t=27277 uic=2161 uip=79033 uipn=9839 uil=10998 uiw=12070 uis=1199 uisn=0 adc=26825 adcm=2798 adcn=0 gl=0
[RX] 2026-10-26T16:37:35.834+08:00 [PROF2] t=27277 defd=131247 defs=39005 netp=3030 netn=0 adcp=14 adcq=0 gl=0
[RX] 2026-10-26T16:37:37.841+08:00 [PROF] t=29295 uic=2343 uip=79033 uipn=9736 uil=11102 uiw=12070 uis=1199 uisn=0 adc=28843 adcm=2798 adcn=0 gl=0
[RX] 2026-10-26T16:37:37.851+08:00 [PROF2] t=29295 defd=131247 defs=39005 netp=3030 netn=0 adcp=14 adcq=0 gl=0
[RX] 2026-10-26T16:37:38.992+08:00 [2026-10-26 08:37:37][ADC_TASK] frames=29999 idle=0 skip=32 seq=30006 frst_err=0 ring=29967 adc1_rc=0 adc1_err=0 adc1_seq=30004 adc1_gap=36 adc1_hw=0 tgap=3
[RX] 2026-10-26T16:37:39.002+08:00 [2026-10-26 08:37:37][ADC_TASK] phasor(hb) C01-IA-F1: Re=0 Im=0 rms=0 (x0.1)
[RX] 2026-10-26T16:37:39.013+08:00 [2026-10-26 08:37:37][ADC_TASK] phasor(hb) C02-UA-F1: Re=0 Im=0 rms=0 (x0.1)
[RX] 2026-10-26T16:37:39.023+08:00 [2026-10-26 08:37:37][ADC_TASK] phasor(hb) C03-UN-F1: Re=0 Im=0 rms=0 (x0.1)
[RX] 2026-10-26T16:37:39.034+08:00 [2026-10-26 08:37:37][ADC_TASK] phasor(hb) C04-IA-F2: Re=0 Im=0 rms=0 (x0.1)
[RX] 2026-10-26T16:37:39.034+08:00 [2026-10-26 08:37:37][ADC_TASK] phasor(hb) C05-IA-F5: Re=0 Im=0 rms=0 (x0.1)
[RX] 2026-10-26T16:37:39.044+08:00 [2026-10-26 08:37:37][ADC_TASK] freq(hb): f=0 valid=0 (x0.01)
[RX] 2026-10-26T16:37:39.044+08:00 [2026-10-26 08:37:37][ADC_TASK] cal: ua_mv=1743 un_mv=1747 ia_mv=1747 vl=0x00 cl=0xe6 rail=0x000 edge=0x000
[RX] 2026-10-26T16:37:39.055+08:00 [2026-10-26 08:37:37][ADC_TASK] meas: seq=150 v=1 ua_mv=0 un_mv=0 ia_mv=0 f=0 fv=0
[RX] 2026-10-26T16:37:39.866+08:00 [PROF] t=31323 uic=2520 uip=79033 uipn=7839 uil=10998 uiw=12070 uis=1199 uisn=0 adc=30870 adcm=2803 adcn=0 gl=0
[RX] 2026-10-26T16:37:39.876+08:00 [PROF2] t=31323 defd=131247 defs=39005 netp=3030 netn=0 adcp=14 adcq=0 gl=0
[RX] 2026-10-26T16:37:41.884+08:00 [PROF] t=33341 uic=2702 uip=79033 uipn=9838 uil=10999 uiw=12070 uis=1202 uisn=0 adc=32888 adcm=2803 adcn=0 gl=0
[RX] 2026-10-26T16:37:41.895+08:00 [PROF2] t=33341 defd=131247 defs=39005 netp=3030 netn=0 adcp=14 adcq=0 gl=0
[RX] 2026-10-26T16:37:43.905+08:00 [PROF] t=35359 uic=2884 uip=79033 uipn=9839 uil=10998 uiw=12070 uis=1202 uisn=0 adc=34906 adcm=2803 adcn=0 gl=0
[RX] 2026-10-26T16:37:43.915+08:00 [PROF2] t=35359 defd=131247 defs=39005 netp=3030 netn=0 adcp=14 adcq=0 gl=0
@@ -0,0 +1,457 @@
# task-21 bench leg: IEC 60870-5-104 setpoint criteria on the 77779d49 image
# image : fw build: Oct 26 2026 16:27:01 suite=NONE dstr=off (built in a read-only detached
# worktree at 77779d49; the main tree held another lane's in-flight doc edits)
# device : 192.168.1.100:2404 (PC side: wired NIC 192.168.1.2/24, ping 3/3 before the leg)
# tooling : test/host/iec104_master.py imported as a library by a scratch driver kept OUTSIDE the
# repo (D:/Code/DTU Code/_wt/.state/bench21_104.py) -- the repo was not modified.
# addressing : item table read from the HEAD blobs (git show 77779d49:App/algo/prot_set.[ch]),
# not from the working tree. 40 items total = 27 with comm_addr != 0 + 13 local items
# with comm_addr == 0. The 13 local items are structurally NOT addressable by C_SE/C_RD
# and are therefore EXCLUDED from criterion 3 by design (listed in block [A] NOTE line).
# protection : protection is in the "uncalibrated interlock" state (ADC_CAL_F_VALID not set). Per the
# acceptance note on task-21, C_SE writes MUST still be accepted in that state
# (interlock != write-inhibit) -- block [A] SAMPLE checks prove they are.
#
# BLOCK [A] pre-reset run: 27-item C_RD sweep at factory defaults + unmapped-address control
# + 2 out-of-range writes + 3 in-range writes with C_RD read-back.
# The 3 "FAIL sweep_pre_*" lines are EXPECTED and are the persistence evidence: that block
# re-ran after the reset (a driver argv bug -- my scratch file, since fixed), so its sweep
# compared live values against factory defaults and correctly reported the 3 written items
# as still non-default: 0x8858=7.500 / 0x885D=1234.000 / 0x8820=11000.000.
# BLOCK [B] post-reset run (reset only, no reflash): PERSIST 3/3 + YX31 not latched + restore defaults.
# BLOCK [C] final sweep after the restore was saved and the box reset again: 27/27 back at defaults.
#
####################################################################################################
### [A] pre-reset run (also serves as post-reset persistence read)
####################################################################################################
[ 0.028s] TX STARTDT act 68 04 07 00 00 00
[ 0.168s] RX U STARTDT con
-- connected, STARTDT con ok
[ 1.179s] TX I C_IC act (pre) 68 0E 00 00 00 00 64 01 06 00 01 00 00 00 00 14
[ 1.269s] RX I type=100 cot=7 body=00 00 00 14
[ 1.269s] TX S N(R)=1 68 04 01 00 02 00
[ 1.270s] TX S N(R)=2 68 04 01 00 04 00
[ 1.271s] RX I M_SP ioa=15 val=0 iv=0 cot=4 [N(R)=1]
[ 1.271s] RX I M_SP ioa=16 val=0 iv=0 cot=4 [N(R)=1]
[ 1.271s] RX I M_SP ioa=17 val=0 iv=0 cot=4 [N(R)=1]
[ 1.271s] RX I M_SP ioa=18 val=0 iv=0 cot=4 [N(R)=1]
[ 1.271s] RX I M_SP ioa=23 val=0 iv=0 cot=4 [N(R)=1]
[ 1.271s] RX I M_SP ioa=31 val=0 iv=0 cot=4 [N(R)=1]
[ 1.271s] TX S N(R)=3 68 04 01 00 06 00
[ 1.273s] TX S N(R)=4 68 04 01 00 08 00
[ 1.274s] TX S N(R)=5 68 04 01 00 0A 00
[ 1.374s] RX I type=100 cot=8 body=00 00 00 14
[ 1.374s] TX S N(R)=6 68 04 01 00 0C 00
[ 2.387s] CHECK gi_actterm_pre PASS
PASS gi_actterm_pre checks=1
=== SWEEP pre : C_RD over 27 addressable items ===
[ 2.387s] TX C_RD 0x8027 68 0D 02 00 0C 00 66 01 05 00 01 00 27 80 00
[ 2.475s] RX I M_ME_NC ioa=0x8027 val=20.000 qds=0x00 cot=5
[ 2.475s] TX S N(R)=7 68 04 01 00 0E 00
PASS sweep_pre_0x8027 read=20.000 want(default)=20.000 PS_UV_VOL
[ 3.488s] TX C_RD 0x8028 68 0D 04 00 0E 00 66 01 05 00 01 00 28 80 00
[ 3.576s] RX I M_ME_NC ioa=0x8028 val=10000.000 qds=0x00 cot=5
[ 3.576s] TX S N(R)=8 68 04 01 00 10 00
PASS sweep_pre_0x8028 read=10000.000 want(default)=10000.000 PS_UV_DLY
[ 4.590s] TX C_RD 0x8029 68 0D 06 00 10 00 66 01 05 00 01 00 29 80 00
[ 4.677s] RX I M_ME_NC ioa=0x8029 val=120.000 qds=0x00 cot=5
[ 4.677s] TX S N(R)=9 68 04 01 00 12 00
PASS sweep_pre_0x8029 read=120.000 want(default)=120.000 PS_OV_VOL
[ 5.679s] TX C_RD 0x802A 68 0D 08 00 12 00 66 01 05 00 01 00 2A 80 00
[ 5.778s] RX I M_ME_NC ioa=0x802A val=10000.000 qds=0x00 cot=5
[ 5.778s] TX S N(R)=10 68 04 01 00 14 00
PASS sweep_pre_0x802A read=10000.000 want(default)=10000.000 PS_OV_DLY
[ 6.785s] TX C_RD 0x8820 68 0D 0A 00 14 00 66 01 05 00 01 00 20 88 00
[ 6.879s] RX I M_ME_NC ioa=0x8820 val=10000.000 qds=0x00 cot=5
[ 6.879s] TX S N(R)=11 68 04 01 00 16 00
PASS sweep_pre_0x8820 read=10000.000 want(default)=10000.000 PS_PT_PRI
[ 7.883s] TX C_RD 0x8821 68 0D 0C 00 16 00 66 01 05 00 01 00 21 88 00
[ 7.979s] RX I M_ME_NC ioa=0x8821 val=100.000 qds=0x00 cot=5
[ 7.979s] TX S N(R)=12 68 04 01 00 18 00
PASS sweep_pre_0x8821 read=100.000 want(default)=100.000 PS_PT_SEC
[ 8.990s] TX C_RD 0x8825 68 0D 0E 00 18 00 66 01 05 00 01 00 25 88 00
[ 9.080s] RX I M_ME_NC ioa=0x8825 val=100.000 qds=0x00 cot=5
[ 9.080s] TX S N(R)=13 68 04 01 00 1A 00
[ 9.082s] RX S N(R)=8
PASS sweep_pre_0x8825 read=100.000 want(default)=100.000 PS_CT_PRI
[ 10.097s] TX C_RD 0x8826 68 0D 10 00 1A 00 66 01 05 00 01 00 26 88 00
[ 10.183s] RX I M_ME_NC ioa=0x8826 val=5.000 qds=0x00 cot=5
[ 10.183s] TX S N(R)=14 68 04 01 00 1C 00
PASS sweep_pre_0x8826 read=5.000 want(default)=5.000 PS_CT_SEC
[ 11.193s] TX C_RD 0x8856 68 0D 12 00 1C 00 66 01 05 00 01 00 56 88 00
[ 11.284s] RX I M_ME_NC ioa=0x8856 val=1.000 qds=0x00 cot=5
[ 11.284s] TX S N(R)=15 68 04 01 00 1E 00
PASS sweep_pre_0x8856 read=1.000 want(default)=1.000 PS_OC1_EN
[ 12.295s] TX C_RD 0x8857 68 0D 14 00 1E 00 66 01 05 00 01 00 57 88 00
[ 12.385s] RX I M_ME_NC ioa=0x8857 val=1.000 qds=0x00 cot=5
[ 12.385s] TX S N(R)=16 68 04 01 00 20 00
PASS sweep_pre_0x8857 read=1.000 want(default)=1.000 PS_OC1_TRIP
[ 13.390s] TX C_RD 0x8858 68 0D 16 00 20 00 66 01 05 00 01 00 58 88 00
[ 13.486s] RX I M_ME_NC ioa=0x8858 val=10.000 qds=0x00 cot=5
[ 13.486s] TX S N(R)=17 68 04 01 00 22 00
PASS sweep_pre_0x8858 read=10.000 want(default)=10.000 PS_OC1_CUR
[ 14.495s] TX C_RD 0x8859 68 0D 18 00 22 00 66 01 05 00 01 00 59 88 00
[ 14.587s] RX I M_ME_NC ioa=0x8859 val=0.000 qds=0x00 cot=5
[ 14.587s] TX S N(R)=18 68 04 01 00 24 00
PASS sweep_pre_0x8859 read=0.000 want(default)=0.000 PS_OC1_DLY
[ 15.592s] TX C_RD 0x885A 68 0D 1A 00 24 00 66 01 05 00 01 00 5A 88 00
[ 15.688s] RX I M_ME_NC ioa=0x885A val=1.000 qds=0x00 cot=5
[ 15.688s] TX S N(R)=19 68 04 01 00 26 00
PASS sweep_pre_0x885A read=1.000 want(default)=1.000 PS_OC2_EN
[ 16.688s] TX C_RD 0x885B 68 0D 1C 00 26 00 66 01 05 00 01 00 5B 88 00
[ 16.789s] RX I M_ME_NC ioa=0x885B val=1.000 qds=0x00 cot=5
[ 16.789s] TX S N(R)=20 68 04 01 00 28 00
PASS sweep_pre_0x885B read=1.000 want(default)=1.000 PS_OC2_TRIP
[ 17.794s] TX C_RD 0x885C 68 0D 1E 00 28 00 66 01 05 00 01 00 5C 88 00
[ 17.890s] RX I M_ME_NC ioa=0x885C val=5.000 qds=0x00 cot=5
[ 17.890s] TX S N(R)=21 68 04 01 00 2A 00
[ 17.891s] RX S N(R)=16
PASS sweep_pre_0x885C read=5.000 want(default)=5.000 PS_OC2_CUR
[ 18.902s] TX C_RD 0x885D 68 0D 20 00 2A 00 66 01 05 00 01 00 5D 88 00
[ 18.992s] RX I M_ME_NC ioa=0x885D val=500.000 qds=0x00 cot=5
[ 18.992s] TX S N(R)=22 68 04 01 00 2C 00
PASS sweep_pre_0x885D read=500.000 want(default)=500.000 PS_OC2_DLY
[ 20.004s] TX C_RD 0x885E 68 0D 22 00 2C 00 66 01 05 00 01 00 5E 88 00
[ 20.093s] RX I M_ME_NC ioa=0x885E val=1.000 qds=0x00 cot=5
[ 20.093s] TX S N(R)=23 68 04 01 00 2E 00
PASS sweep_pre_0x885E read=1.000 want(default)=1.000 PS_OC3_EN
[ 21.101s] TX C_RD 0x885F 68 0D 24 00 2E 00 66 01 05 00 01 00 5F 88 00
[ 21.193s] RX I M_ME_NC ioa=0x885F val=0.000 qds=0x00 cot=5
[ 21.193s] TX S N(R)=24 68 04 01 00 30 00
PASS sweep_pre_0x885F read=0.000 want(default)=0.000 PS_OC3_TRIP
[ 22.207s] TX C_RD 0x8860 68 0D 26 00 30 00 66 01 05 00 01 00 60 88 00
[ 22.295s] RX I M_ME_NC ioa=0x8860 val=2.000 qds=0x00 cot=5
[ 22.295s] TX S N(R)=25 68 04 01 00 32 00
PASS sweep_pre_0x8860 read=2.000 want(default)=2.000 PS_OC3_CUR
[ 23.305s] TX C_RD 0x8861 68 0D 28 00 32 00 66 01 05 00 01 00 61 88 00
[ 23.394s] RX I M_ME_NC ioa=0x8861 val=3000.000 qds=0x00 cot=5
[ 23.394s] TX S N(R)=26 68 04 01 00 34 00
PASS sweep_pre_0x8861 read=3000.000 want(default)=3000.000 PS_OC3_DLY
[ 24.395s] TX C_RD 0x8871 68 0D 2A 00 34 00 66 01 05 00 01 00 71 88 00
[ 24.496s] RX I M_ME_NC ioa=0x8871 val=1.000 qds=0x00 cot=5
[ 24.496s] TX S N(R)=27 68 04 01 00 36 00
PASS sweep_pre_0x8871 read=1.000 want(default)=1.000 PS_OL_EN
[ 25.501s] TX C_RD 0x8872 68 0D 2C 00 36 00 66 01 05 00 01 00 72 88 00
[ 25.597s] RX I M_ME_NC ioa=0x8872 val=0.000 qds=0x00 cot=5
[ 25.597s] TX S N(R)=28 68 04 01 00 38 00
PASS sweep_pre_0x8872 read=0.000 want(default)=0.000 PS_OL_TRIP
[ 26.599s] TX C_RD 0x8873 68 0D 2E 00 38 00 66 01 05 00 01 00 73 88 00
[ 26.703s] RX I M_ME_NC ioa=0x8873 val=1.000 qds=0x00 cot=5
[ 26.703s] TX S N(R)=29 68 04 01 00 3A 00
[ 26.704s] RX S N(R)=24
PASS sweep_pre_0x8873 read=1.000 want(default)=1.000 PS_OL_CUR
[ 27.709s] TX C_RD 0x8874 68 0D 30 00 3A 00 66 01 05 00 01 00 74 88 00
[ 27.804s] RX I M_ME_NC ioa=0x8874 val=10000.000 qds=0x00 cot=5
[ 27.804s] TX S N(R)=30 68 04 01 00 3C 00
PASS sweep_pre_0x8874 read=10000.000 want(default)=10000.000 PS_OL_DLY
[ 28.817s] TX C_RD 0x888E 68 0D 32 00 3C 00 66 01 05 00 01 00 8E 88 00
[ 28.906s] RX I M_ME_NC ioa=0x888E val=1.000 qds=0x00 cot=5
[ 28.906s] TX S N(R)=31 68 04 01 00 3E 00
PASS sweep_pre_0x888E read=1.000 want(default)=1.000 PS_3U0_EN
[ 29.921s] TX C_RD 0x888F 68 0D 34 00 3E 00 66 01 05 00 01 00 8F 88 00
[ 30.006s] RX I M_ME_NC ioa=0x888F val=15.000 qds=0x00 cot=5
[ 30.006s] TX S N(R)=32 68 04 01 00 40 00
PASS sweep_pre_0x888F read=15.000 want(default)=15.000 PS_3U0_VOL
[ 31.007s] TX C_RD 0x8890 68 0D 36 00 40 00 66 01 05 00 01 00 90 88 00
[ 31.108s] RX I M_ME_NC ioa=0x8890 val=5000.000 qds=0x00 cot=5
[ 31.108s] TX S N(R)=33 68 04 01 00 42 00
PASS sweep_pre_0x8890 read=5000.000 want(default)=5000.000 PS_3U0_DLY
=== UNMAPPED: C_RD on addresses outside the table (13 local items have comm_addr=0) ===
[ 32.112s] TX C_RD 0x8854 68 0D 38 00 42 00 66 01 05 00 01 00 54 88 00
[ 32.208s] RX I type=102 cot=14 body=54 88 00
[ 32.208s] TX S N(R)=34 68 04 01 00 44 00
PASS unmapped_no_answer_0x8854 read=None (expect no response: not in comm table)
[ 35.243s] TX C_RD 0x8855 68 0D 3A 00 44 00 66 01 05 00 01 00 55 88 00
[ 35.310s] RX I type=102 cot=14 body=55 88 00
[ 35.310s] TX S N(R)=35 68 04 01 00 46 00
[ 37.810s] RX S N(R)=30
PASS unmapped_no_answer_0x8855 read=None (expect no response: not in comm table)
[ 38.820s] TX C_RD 0x8862 68 0D 3C 00 46 00 66 01 05 00 01 00 62 88 00
[ 38.911s] RX I type=102 cot=14 body=62 88 00
[ 38.911s] TX S N(R)=36 68 04 01 00 48 00
PASS unmapped_no_answer_0x8862 read=None (expect no response: not in comm table)
NOTE: 13 local items (comm_addr=0) are structurally NOT addressable by C_RD, hence excluded from this criterion: PS_OV_EN,PS_UV_EN,PS_RET_OVER,PS_RET_UNDER,PS_GLOBAL_EN,PS_OF_EN,PS_OF_HZ,PS_OF_DLY,PS_UF_EN,PS_UF_HZ,PS_UF_DLY,PS_RET_OF,PS_RET_UF
=== OOB: out-of-range must be refused, value unchanged ===
[ 41.932s] TX C_RD 0x8858 68 0D 3E 00 48 00 66 01 05 00 01 00 58 88 00
[ 42.012s] RX I M_ME_NC ioa=0x8858 val=10.000 qds=0x00 cot=5
[ 42.012s] TX S N(R)=37 68 04 01 00 4A 00
[ 43.022s] TX C_SE sel 0x8858 = 500.000 68 12 40 00 4A 00 32 01 06 00 01 00 58 88 00 00 00 FA 43 80
[ 43.113s] RX I type=50 cot=7 PN body=58 88 00 00 00 FA 43 80
[ 43.113s] TX S N(R)=38 68 04 01 00 4C 00
[ 44.122s] TX C_SE exe 0x8858 = 500.000 68 12 42 00 4C 00 32 01 06 00 01 00 58 88 00 00 00 FA 43 00
[ 44.214s] RX I type=50 cot=7 PN body=58 88 00 00 00 FA 43 00
[ 44.214s] TX S N(R)=39 68 04 01 00 4E 00
PASS oob_refused_0x8858 sel_any=True sel_pn=True exe_any=True exe_pn=True PS_OC1_CUR (above max 100)
[ 45.218s] TX C_RD 0x8858 68 0D 44 00 4E 00 66 01 05 00 01 00 58 88 00
[ 45.314s] RX I M_ME_NC ioa=0x8858 val=10.000 qds=0x00 cot=5
[ 45.314s] TX S N(R)=40 68 04 01 00 50 00
PASS oob_value_unchanged_0x8858 before=10.000 after=10.000 (refused write must not land)
[ 46.326s] TX C_RD 0x8821 68 0D 46 00 50 00 66 01 05 00 01 00 21 88 00
[ 46.416s] RX I M_ME_NC ioa=0x8821 val=100.000 qds=0x00 cot=5
[ 46.416s] TX S N(R)=41 68 04 01 00 52 00
[ 47.431s] TX C_SE sel 0x8821 = 0.500 68 12 48 00 52 00 32 01 06 00 01 00 21 88 00 00 00 00 3F 80
[ 47.517s] RX I type=50 cot=7 PN body=21 88 00 00 00 00 3F 80
[ 47.517s] TX S N(R)=42 68 04 01 00 54 00
[ 48.521s] TX C_SE exe 0x8821 = 0.500 68 12 4A 00 54 00 32 01 06 00 01 00 21 88 00 00 00 00 3F 00
[ 48.617s] RX I type=50 cot=7 PN body=21 88 00 00 00 00 3F 00
[ 48.617s] TX S N(R)=43 68 04 01 00 56 00
[ 48.619s] RX S N(R)=38
PASS oob_refused_0x8821 sel_any=True sel_pn=True exe_any=True exe_pn=True PS_PT_SEC (below min 1.0)
[ 49.630s] TX C_RD 0x8821 68 0D 4C 00 56 00 66 01 05 00 01 00 21 88 00
[ 49.724s] RX I M_ME_NC ioa=0x8821 val=100.000 qds=0x00 cot=5
[ 49.724s] TX S N(R)=44 68 04 01 00 58 00
PASS oob_value_unchanged_0x8821 before=100.000 after=100.000 (refused write must not land)
=== SAMPLE: 3 in-range C_SE writes then C_RD ===
[ 50.739s] TX C_SE sel 0x8858 = 7.500 68 12 4E 00 58 00 32 01 06 00 01 00 58 88 00 00 00 F0 40 80
[ 50.828s] RX I type=50 cot=7 body=58 88 00 00 00 F0 40 80
[ 50.828s] TX S N(R)=45 68 04 01 00 5A 00
[ 51.835s] TX C_SE exe 0x8858 = 7.500 68 12 50 00 5A 00 32 01 06 00 01 00 58 88 00 00 00 F0 40 00
[ 51.929s] RX I type=50 cot=7 body=58 88 00 00 00 F0 40 00
[ 51.929s] TX S N(R)=46 68 04 01 00 5C 00
PASS se_accept_0x8858 sel=True/False exe=True/False (want actcon, pn=False) PS_OC1_CUR -> 7.500
[ 52.944s] TX C_RD 0x8858 68 0D 52 00 5C 00 66 01 05 00 01 00 58 88 00
[ 53.035s] RX I M_ME_NC ioa=0x8858 val=7.500 qds=0x00 cot=5
[ 53.035s] TX S N(R)=47 68 04 01 00 5E 00
PASS rd_after_se_0x8858 read=7.500 want=7.500
[ 54.040s] TX C_SE sel 0x885D = 1234.000 68 12 54 00 5E 00 32 01 06 00 01 00 5D 88 00 00 40 9A 44 80
[ 54.136s] RX I type=50 cot=7 body=5D 88 00 00 40 9A 44 80
[ 54.136s] TX S N(R)=48 68 04 01 00 60 00
[ 55.138s] TX C_SE exe 0x885D = 1234.000 68 12 56 00 60 00 32 01 06 00 01 00 5D 88 00 00 40 9A 44 00
[ 55.237s] RX I type=50 cot=7 body=5D 88 00 00 40 9A 44 00
[ 55.237s] TX S N(R)=49 68 04 01 00 62 00
PASS se_accept_0x885D sel=True/False exe=True/False (want actcon, pn=False) PS_OC2_DLY -> 1234.000
[ 56.240s] TX C_RD 0x885D 68 0D 58 00 62 00 66 01 05 00 01 00 5D 88 00
[ 56.343s] RX I M_ME_NC ioa=0x885D val=1234.000 qds=0x00 cot=5
[ 56.343s] TX S N(R)=50 68 04 01 00 64 00
PASS rd_after_se_0x885D read=1234.000 want=1234.000
[ 57.347s] TX C_SE sel 0x8820 = 11000.000 68 12 5A 00 64 00 32 01 06 00 01 00 20 88 00 00 E0 2B 46 80
[ 57.444s] RX I type=50 cot=7 body=20 88 00 00 E0 2B 46 80
[ 57.444s] TX S N(R)=51 68 04 01 00 66 00
[ 57.445s] RX S N(R)=46
[ 58.454s] TX C_SE exe 0x8820 = 11000.000 68 12 5C 00 66 00 32 01 06 00 01 00 20 88 00 00 E0 2B 46 00
[ 58.546s] RX I type=50 cot=7 body=20 88 00 00 E0 2B 46 00
[ 58.546s] TX S N(R)=52 68 04 01 00 68 00
PASS se_accept_0x8820 sel=True/False exe=True/False (want actcon, pn=False) PS_PT_PRI -> 11000.000
[ 59.558s] TX C_RD 0x8820 68 0D 5E 00 68 00 66 01 05 00 01 00 20 88 00
[ 59.650s] RX I M_ME_NC ioa=0x8820 val=11000.000 qds=0x00 cot=5
[ 59.650s] TX S N(R)=53 68 04 01 00 6A 00
[ 60.150s] RX I M_ME_NC ioa=0x4011 val=0.000 qds=0x00 cot=1
[ 60.150s] RX I M_ME_NC ioa=0x4012 val=0.000 qds=0x80 cot=1
[ 60.150s] RX I M_ME_NC ioa=0x4013 val=0.000 qds=0x80 cot=1
[ 60.150s] RX I M_ME_NC ioa=0x4014 val=0.000 qds=0x00 cot=1
[ 60.150s] RX I M_ME_NC ioa=0x4015 val=0.000 qds=0x00 cot=1
[ 60.150s] RX I M_ME_NC ioa=0x4016 val=0.000 qds=0x80 cot=1
[ 60.150s] RX I M_ME_NC ioa=0x4017 val=0.000 qds=0x80 cot=1
[ 60.150s] RX I M_ME_NC ioa=0x4018 val=0.000 qds=0x80 cot=1
[ 60.150s] TX S N(R)=54 68 04 01 00 6C 00
[ 60.151s] RX I M_ME_NC ioa=0x401D val=0.000 qds=0x80 cot=1
[ 60.151s] TX S N(R)=55 68 04 01 00 6E 00
PASS rd_after_se_0x8820 read=11000.000 want=11000.000
-- letting dirty setpoints flush to /prot_set.bin (5 s)
==== PRE: 41/41 checks PASS ====
json -> D:\\Code\\DTU Code\\_wt\\.state\\i104_pre.json
####################################################################################################
### [B] post-reset run: persist + YX31 + restore
####################################################################################################
[ 0.026s] TX STARTDT act 68 04 07 00 00 00
[ 0.137s] RX U STARTDT con
-- connected, STARTDT con ok
=== PERSIST after reset: written values must survive ===
[ 1.140s] TX C_RD 0x8858 68 0D 00 00 00 00 66 01 05 00 01 00 58 88 00
[ 1.238s] RX I M_ME_NC ioa=0x8858 val=7.500 qds=0x00 cot=5
[ 1.238s] TX S N(R)=1 68 04 01 00 02 00
PASS persist_0x8858 read=7.500 want=7.500 PS_OC1_CUR
[ 2.252s] TX C_RD 0x885D 68 0D 02 00 02 00 66 01 05 00 01 00 5D 88 00
[ 2.339s] RX I M_ME_NC ioa=0x885D val=1234.000 qds=0x00 cot=5
[ 2.339s] TX S N(R)=2 68 04 01 00 04 00
PASS persist_0x885D read=1234.000 want=1234.000 PS_OC2_DLY
[ 3.343s] TX C_RD 0x8820 68 0D 04 00 04 00 66 01 05 00 01 00 20 88 00
[ 3.439s] RX I M_ME_NC ioa=0x8820 val=11000.000 qds=0x00 cot=5
[ 3.439s] TX S N(R)=3 68 04 01 00 06 00
PASS persist_0x8820 read=11000.000 want=11000.000 PS_PT_PRI
[ 4.447s] TX I C_IC act (persist) 68 0E 06 00 06 00 64 01 06 00 01 00 00 00 00 14
[ 4.540s] RX I type=100 cot=7 body=00 00 00 14
[ 4.540s] TX S N(R)=4 68 04 01 00 08 00
[ 4.541s] TX S N(R)=5 68 04 01 00 0A 00
[ 4.543s] RX I M_SP ioa=15 val=0 iv=0 cot=4 [N(R)=4]
[ 4.543s] RX I M_SP ioa=16 val=0 iv=0 cot=4 [N(R)=4]
[ 4.543s] RX I M_SP ioa=17 val=0 iv=0 cot=4 [N(R)=4]
[ 4.543s] RX I M_SP ioa=18 val=0 iv=0 cot=4 [N(R)=4]
[ 4.543s] RX I M_SP ioa=23 val=0 iv=0 cot=4 [N(R)=4]
[ 4.543s] RX I M_SP ioa=31 val=0 iv=0 cot=4 [N(R)=4]
[ 4.543s] TX S N(R)=6 68 04 01 00 0C 00
[ 4.543s] TX S N(R)=7 68 04 01 00 0E 00
[ 4.544s] TX S N(R)=8 68 04 01 00 10 00
[ 4.645s] RX I type=100 cot=8 body=00 00 00 14
[ 4.645s] TX S N(R)=9 68 04 01 00 12 00
PASS yx31_prot_block_off yx31=(0, False) (L5-1 checksum selfcheck must not latch)
=== RESTORE factory defaults ===
[ 5.649s] TX C_SE sel 0x8858 = 10.000 68 12 08 00 12 00 32 01 06 00 01 00 58 88 00 00 00 20 41 80
[ 5.746s] RX I type=50 cot=7 body=58 88 00 00 00 20 41 80
[ 5.746s] TX S N(R)=10 68 04 01 00 14 00
[ 6.759s] TX C_SE exe 0x8858 = 10.000 68 12 0A 00 14 00 32 01 06 00 01 00 58 88 00 00 00 20 41 00
[ 6.848s] RX I type=50 cot=7 body=58 88 00 00 00 20 41 00
[ 6.848s] TX S N(R)=11 68 04 01 00 16 00
[ 7.863s] TX C_RD 0x8858 68 0D 0C 00 16 00 66 01 05 00 01 00 58 88 00
[ 7.956s] RX I M_ME_NC ioa=0x8858 val=10.000 qds=0x00 cot=5
[ 7.956s] TX S N(R)=12 68 04 01 00 18 00
PASS restore_0x8858 read=10.000 want(default)=10.000
[ 8.959s] TX C_SE sel 0x885D = 500.000 68 12 0E 00 18 00 32 01 06 00 01 00 5D 88 00 00 00 FA 43 80
[ 9.058s] RX I type=50 cot=7 body=5D 88 00 00 00 FA 43 80
[ 9.058s] TX S N(R)=13 68 04 01 00 1A 00
[ 9.058s] RX S N(R)=8
[ 10.068s] TX C_SE exe 0x885D = 500.000 68 12 10 00 1A 00 32 01 06 00 01 00 5D 88 00 00 00 FA 43 00
[ 10.160s] RX I type=50 cot=7 body=5D 88 00 00 00 FA 43 00
[ 10.160s] TX S N(R)=14 68 04 01 00 1C 00
[ 11.160s] TX C_RD 0x885D 68 0D 12 00 1C 00 66 01 05 00 01 00 5D 88 00
[ 11.169s] RX I M_ME_NC ioa=0x885D val=500.000 qds=0x00 cot=5
[ 11.169s] TX S N(R)=15 68 04 01 00 1E 00
PASS restore_0x885D read=500.000 want(default)=500.000
[ 12.178s] TX C_SE sel 0x8820 = 10000.000 68 12 14 00 1E 00 32 01 06 00 01 00 20 88 00 00 40 1C 46 80
[ 12.274s] RX I type=50 cot=7 body=20 88 00 00 40 1C 46 80
[ 12.274s] TX S N(R)=16 68 04 01 00 20 00
[ 13.288s] TX C_SE exe 0x8820 = 10000.000 68 12 16 00 20 00 32 01 06 00 01 00 20 88 00 00 40 1C 46 00
[ 13.375s] RX I type=50 cot=7 body=20 88 00 00 40 1C 46 00
[ 13.375s] TX S N(R)=17 68 04 01 00 22 00
[ 14.378s] TX C_RD 0x8820 68 0D 18 00 22 00 66 01 05 00 01 00 20 88 00
[ 14.481s] RX I M_ME_NC ioa=0x8820 val=10000.000 qds=0x00 cot=5
[ 14.481s] TX S N(R)=18 68 04 01 00 24 00
PASS restore_0x8820 read=10000.000 want(default)=10000.000
PROTOCOL ERROR: ValueError('too many values to unpack (expected 2)')
json -> D:\\Code\\DTU Code\\_wt\\.state\\i104_post.json
####################################################################################################
### [C] final sweep: 27/27 at factory defaults after a second reset
####################################################################################################
[ 0.029s] TX STARTDT act 68 04 07 00 00 00
[ 0.113s] RX U STARTDT con
-- connected, STARTDT con ok
=== SWEEP default : C_RD over 27 addressable items ===
[ 1.118s] TX C_RD 0x8027 68 0D 00 00 00 00 66 01 05 00 01 00 27 80 00
[ 1.213s] RX I M_ME_NC ioa=0x8027 val=20.000 qds=0x00 cot=5
[ 1.213s] TX S N(R)=1 68 04 01 00 02 00
PASS sweep_default_0x8027 read=20.000 want(default)=20.000 PS_UV_VOL
[ 2.215s] TX C_RD 0x8028 68 0D 02 00 02 00 66 01 05 00 01 00 28 80 00
[ 2.221s] RX I M_ME_NC ioa=0x8028 val=10000.000 qds=0x00 cot=5
[ 2.221s] TX S N(R)=2 68 04 01 00 04 00
PASS sweep_default_0x8028 read=10000.000 want(default)=10000.000 PS_UV_DLY
[ 3.225s] TX C_RD 0x8029 68 0D 04 00 04 00 66 01 05 00 01 00 29 80 00
[ 3.321s] RX I M_ME_NC ioa=0x8029 val=120.000 qds=0x00 cot=5
[ 3.321s] TX S N(R)=3 68 04 01 00 06 00
PASS sweep_default_0x8029 read=120.000 want(default)=120.000 PS_OV_VOL
[ 4.329s] TX C_RD 0x802A 68 0D 06 00 06 00 66 01 05 00 01 00 2A 80 00
[ 4.423s] RX I M_ME_NC ioa=0x802A val=10000.000 qds=0x00 cot=5
[ 4.423s] TX S N(R)=4 68 04 01 00 08 00
PASS sweep_default_0x802A read=10000.000 want(default)=10000.000 PS_OV_DLY
[ 5.423s] TX C_RD 0x8820 68 0D 08 00 08 00 66 01 05 00 01 00 20 88 00
[ 5.523s] RX I M_ME_NC ioa=0x8820 val=10000.000 qds=0x00 cot=5
[ 5.523s] TX S N(R)=5 68 04 01 00 0A 00
PASS sweep_default_0x8820 read=10000.000 want(default)=10000.000 PS_PT_PRI
[ 6.526s] TX C_RD 0x8821 68 0D 0A 00 0A 00 66 01 05 00 01 00 21 88 00
[ 6.625s] RX I M_ME_NC ioa=0x8821 val=100.000 qds=0x00 cot=5
[ 6.625s] TX S N(R)=6 68 04 01 00 0C 00
PASS sweep_default_0x8821 read=100.000 want(default)=100.000 PS_PT_SEC
[ 7.625s] TX C_RD 0x8825 68 0D 0C 00 0C 00 66 01 05 00 01 00 25 88 00
[ 7.725s] RX I M_ME_NC ioa=0x8825 val=100.000 qds=0x00 cot=5
[ 7.725s] TX S N(R)=7 68 04 01 00 0E 00
PASS sweep_default_0x8825 read=100.000 want(default)=100.000 PS_CT_PRI
[ 8.727s] TX C_RD 0x8826 68 0D 0E 00 0E 00 66 01 05 00 01 00 26 88 00
[ 8.826s] RX I M_ME_NC ioa=0x8826 val=5.000 qds=0x00 cot=5
[ 8.826s] TX S N(R)=8 68 04 01 00 10 00
[ 8.827s] RX S N(R)=8
PASS sweep_default_0x8826 read=5.000 want(default)=5.000 PS_CT_SEC
[ 9.827s] TX C_RD 0x8856 68 0D 10 00 10 00 66 01 05 00 01 00 56 88 00
[ 9.929s] RX I M_ME_NC ioa=0x8856 val=1.000 qds=0x00 cot=5
[ 9.929s] TX S N(R)=9 68 04 01 00 12 00
PASS sweep_default_0x8856 read=1.000 want(default)=1.000 PS_OC1_EN
[ 10.933s] TX C_RD 0x8857 68 0D 12 00 12 00 66 01 05 00 01 00 57 88 00
[ 11.029s] RX I M_ME_NC ioa=0x8857 val=1.000 qds=0x00 cot=5
[ 11.029s] TX S N(R)=10 68 04 01 00 14 00
PASS sweep_default_0x8857 read=1.000 want(default)=1.000 PS_OC1_TRIP
[ 12.032s] TX C_RD 0x8858 68 0D 14 00 14 00 66 01 05 00 01 00 58 88 00
[ 12.131s] RX I M_ME_NC ioa=0x8858 val=10.000 qds=0x00 cot=5
[ 12.131s] TX S N(R)=11 68 04 01 00 16 00
PASS sweep_default_0x8858 read=10.000 want(default)=10.000 PS_OC1_CUR
[ 13.143s] TX C_RD 0x8859 68 0D 16 00 16 00 66 01 05 00 01 00 59 88 00
[ 13.231s] RX I M_ME_NC ioa=0x8859 val=0.000 qds=0x00 cot=5
[ 13.231s] TX S N(R)=12 68 04 01 00 18 00
PASS sweep_default_0x8859 read=0.000 want(default)=0.000 PS_OC1_DLY
[ 14.232s] TX C_RD 0x885A 68 0D 18 00 18 00 66 01 05 00 01 00 5A 88 00
[ 14.332s] RX I M_ME_NC ioa=0x885A val=1.000 qds=0x00 cot=5
[ 14.332s] TX S N(R)=13 68 04 01 00 1A 00
PASS sweep_default_0x885A read=1.000 want(default)=1.000 PS_OC2_EN
[ 15.334s] TX C_RD 0x885B 68 0D 1A 00 1A 00 66 01 05 00 01 00 5B 88 00
[ 15.433s] RX I M_ME_NC ioa=0x885B val=1.000 qds=0x00 cot=5
[ 15.433s] TX S N(R)=14 68 04 01 00 1C 00
PASS sweep_default_0x885B read=1.000 want(default)=1.000 PS_OC2_TRIP
[ 16.445s] TX C_RD 0x885C 68 0D 1C 00 1C 00 66 01 05 00 01 00 5C 88 00
[ 16.534s] RX I M_ME_NC ioa=0x885C val=5.000 qds=0x00 cot=5
[ 16.534s] TX S N(R)=15 68 04 01 00 1E 00
PASS sweep_default_0x885C read=5.000 want(default)=5.000 PS_OC2_CUR
[ 17.546s] TX C_RD 0x885D 68 0D 1E 00 1E 00 66 01 05 00 01 00 5D 88 00
[ 17.635s] RX I M_ME_NC ioa=0x885D val=500.000 qds=0x00 cot=5
[ 17.635s] TX S N(R)=16 68 04 01 00 20 00
[ 17.637s] RX S N(R)=16
PASS sweep_default_0x885D read=500.000 want(default)=500.000 PS_OC2_DLY
[ 18.642s] TX C_RD 0x885E 68 0D 20 00 20 00 66 01 05 00 01 00 5E 88 00
[ 18.737s] RX I M_ME_NC ioa=0x885E val=1.000 qds=0x00 cot=5
[ 18.737s] TX S N(R)=17 68 04 01 00 22 00
PASS sweep_default_0x885E read=1.000 want(default)=1.000 PS_OC3_EN
[ 19.744s] TX C_RD 0x885F 68 0D 22 00 22 00 66 01 05 00 01 00 5F 88 00
[ 19.838s] RX I M_ME_NC ioa=0x885F val=0.000 qds=0x00 cot=5
[ 19.838s] TX S N(R)=18 68 04 01 00 24 00
PASS sweep_default_0x885F read=0.000 want(default)=0.000 PS_OC3_TRIP
[ 20.850s] TX C_RD 0x8860 68 0D 24 00 24 00 66 01 05 00 01 00 60 88 00
[ 20.940s] RX I M_ME_NC ioa=0x8860 val=2.000 qds=0x00 cot=5
[ 20.940s] TX S N(R)=19 68 04 01 00 26 00
PASS sweep_default_0x8860 read=2.000 want(default)=2.000 PS_OC3_CUR
[ 21.942s] TX C_RD 0x8861 68 0D 26 00 26 00 66 01 05 00 01 00 61 88 00
[ 22.041s] RX I M_ME_NC ioa=0x8861 val=3000.000 qds=0x00 cot=5
[ 22.041s] TX S N(R)=20 68 04 01 00 28 00
PASS sweep_default_0x8861 read=3000.000 want(default)=3000.000 PS_OC3_DLY
[ 23.045s] TX C_RD 0x8871 68 0D 28 00 28 00 66 01 05 00 01 00 71 88 00
[ 23.141s] RX I M_ME_NC ioa=0x8871 val=1.000 qds=0x00 cot=5
[ 23.141s] TX S N(R)=21 68 04 01 00 2A 00
PASS sweep_default_0x8871 read=1.000 want(default)=1.000 PS_OL_EN
[ 24.142s] TX C_RD 0x8872 68 0D 2A 00 2A 00 66 01 05 00 01 00 72 88 00
[ 24.242s] RX I M_ME_NC ioa=0x8872 val=0.000 qds=0x00 cot=5
[ 24.242s] TX S N(R)=22 68 04 01 00 2C 00
PASS sweep_default_0x8872 read=0.000 want(default)=0.000 PS_OL_TRIP
[ 25.243s] TX C_RD 0x8873 68 0D 2C 00 2C 00 66 01 05 00 01 00 73 88 00
[ 25.342s] RX I M_ME_NC ioa=0x8873 val=1.000 qds=0x00 cot=5
[ 25.342s] TX S N(R)=23 68 04 01 00 2E 00
PASS sweep_default_0x8873 read=1.000 want(default)=1.000 PS_OL_CUR
[ 26.352s] TX C_RD 0x8874 68 0D 2E 00 2E 00 66 01 05 00 01 00 74 88 00
[ 26.444s] RX I M_ME_NC ioa=0x8874 val=10000.000 qds=0x00 cot=5
[ 26.444s] TX S N(R)=24 68 04 01 00 30 00
[ 26.445s] RX S N(R)=24
PASS sweep_default_0x8874 read=10000.000 want(default)=10000.000 PS_OL_DLY
[ 27.449s] TX C_RD 0x888E 68 0D 30 00 30 00 66 01 05 00 01 00 8E 88 00
[ 27.546s] RX I M_ME_NC ioa=0x888E val=1.000 qds=0x00 cot=5
[ 27.546s] TX S N(R)=25 68 04 01 00 32 00
PASS sweep_default_0x888E read=1.000 want(default)=1.000 PS_3U0_EN
[ 28.549s] TX C_RD 0x888F 68 0D 32 00 32 00 66 01 05 00 01 00 8F 88 00
[ 28.647s] RX I M_ME_NC ioa=0x888F val=15.000 qds=0x00 cot=5
[ 28.647s] TX S N(R)=26 68 04 01 00 34 00
PASS sweep_default_0x888F read=15.000 want(default)=15.000 PS_3U0_VOL
[ 29.649s] TX C_RD 0x8890 68 0D 34 00 34 00 66 01 05 00 01 00 90 88 00
[ 29.748s] RX I M_ME_NC ioa=0x8890 val=5000.000 qds=0x00 cot=5
[ 29.748s] TX S N(R)=27 68 04 01 00 36 00
PASS sweep_default_0x8890 read=5000.000 want(default)=5000.000 PS_3U0_DLY
==== SWEEP: 27/27 checks PASS ====
json -> D:\\Code\\DTU Code\\_wt\\.state\\i104_sweep.json
@@ -0,0 +1,131 @@
{
"decoded_from": "utf-8",
"note": "task-21 board_ui_nav step trace for view_set (write password 123); re-dumped with ensure_ascii so the archived file is pure ASCII",
"trace": [
{
"key": "l",
"why": "\u56de\u6839\u540e\u8865\u6536\u4e00\u6b21\uff08\u9632\u88ab\u88c1\u5f39\u6846\u6f0f\u8ba1\uff09",
"ink": 3226,
"kind": "MENU",
"page": null,
"cols": [
[
"\u88c5\u7f6e\u4fe1\u606f",
3,
57
]
]
},
{
"key": "d",
"why": "\u5728\u5217 0 \u91cc\u627e \u53c2\u6570\u5b9a\u503c",
"ink": 3182,
"kind": "MENU",
"page": null,
"cols": [
[
"\u5b9e\u65f6\u6570\u636e",
3,
72
]
]
},
{
"key": "d",
"why": "\u5728\u5217 0 \u91cc\u627e \u53c2\u6570\u5b9a\u503c",
"ink": 3170,
"kind": "MENU",
"page": null,
"cols": [
[
"\u53c2\u6570\u5b9a\u503c",
3,
87
]
]
},
{
"key": "e",
"why": "\u8fdb\u5165 \u53c2\u6570\u5b9a\u503c",
"ink": 3888,
"kind": "MENU",
"page": null,
"cols": [
[
"\u53c2\u6570\u5b9a\u503c",
3,
87
],
[
"\u6574\u5b9a",
68,
88
]
]
},
{
"key": "e",
"why": "\u8fdb\u5165 \u6574\u5b9a",
"ink": 4958,
"kind": "MENU",
"page": null,
"cols": [
[
"\u53c2\u6570\u5b9a\u503c",
3,
87
],
[
"\u6574\u5b9a",
68,
88
],
[
"\u53c2\u6570",
109,
89
]
]
},
{
"key": "d",
"why": "\u5728\u5217 2 \u91cc\u627e \u5b9a\u503c",
"ink": 4948,
"kind": "MENU",
"page": null,
"cols": [
[
"\u53c2\u6570\u5b9a\u503c",
3,
87
],
[
"\u6574\u5b9a",
68,
88
],
[
"\u5b9a\u503c",
109,
104
]
]
},
{
"key": "e",
"why": "\u8fdb\u5165 \u5b9a\u503c",
"ink": 512,
"kind": "PWD",
"page": null,
"cols": null
},
{
"key": "e",
"why": "\u8fc7\u67e5\u770b\u5bc6\u7801\uff08\u51fa\u5382 000\uff0c\u76f4\u63a5\u786e\u8ba4\uff09",
"ink": 4373,
"kind": "PAGE",
"page": "view_set",
"cols": null
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -0,0 +1,89 @@
task-21 bench evidence: console output byte-level interleaving (2 writers on one line)
image identity : fw build: Oct 26 2026 16:27:01 suite=NONE dstr=off (built from 77779d49, detached worktree)
source log : .dsh/skills/serial-assistant/scripts/state/serial.log (serial-assistant daemon, COM6@115200)
observed by : bench-runner (task-21), 2026-10-26
mechanism claim : bytes that appear INSIDE a line belong to the HEAD of the NEXT line, which is
correspondingly missing that head. A UART/framing error can corrupt a byte but
can never make a line longer, and the stolen bytes always show up in order at the
front of the neighbour. Therefore two console writers were emitting concurrently.
rate : 2 spliced lines out of 2807 [RX] lines in this log window; both hits are the same
writer pair, and both occurred on boots that were NOT the flash boot:
boot A 16:28 (stlink flash -Rst -Run) -> line CLEAN
boot B 16:37 (reset only, -Rst -Run) -> line SPLICED
boot C 16:44 (reset only, -Rst -Run) -> line SPLICED
i.e. 2 of 3 boots; not reproducible by construction, timing dependent.
pair (constant) : writer mid-line = [PROT] (App/task/protect_task.c, "uncalibrated -> protection
blocked (UA/UN/IA VALID not all set)", DBG_INFO)
victim head = [NAND_FTL] "Initializing NAND FTL..." (defaultTask storage bring-up)
the two run at boot time in different tasks (protectTask AboveNormal vs defaultTask Normal)
==========================================================================================
OCCURRENCE 1 (serial.log byte offsets 3410..3539 and 3540..3628)
==========================================================================================
--- PROT line, verbatim (may contain spliced bytes) ---
[RX] 2026-10-26T16:37:08.989+08:00 [2026-10-26 08:37:07][PROT] uncalibrated -> protection [blocked (UA/UN/IA VALI2D not all s0et)
--- its hexdump (129 bytes @ 3410) ---
00000d52 5b 52 58 5d 20 32 30 32 36 2d 31 30 2d 32 36 54 |[RX] 2026-10-26T|
00000d62 31 36 3a 33 37 3a 30 38 2e 39 38 39 2b 30 38 3a |16:37:08.989+08:|
00000d72 30 30 20 5b 32 30 32 36 2d 31 30 2d 32 36 20 30 |00 [2026-10-26 0|
00000d82 38 3a 33 37 3a 30 37 5d 5b 50 52 4f 54 5d 20 75 |8:37:07][PROT] u|
00000d92 6e 63 61 6c 69 62 72 61 74 65 64 20 2d 3e 20 70 |ncalibrated -> p|
00000da2 72 6f 74 65 63 74 69 6f 6e 20 5b 62 6c 6f 63 6b |rotection [block|
00000db2 65 64 20 28 55 41 2f 55 4e 2f 49 41 20 56 41 4c |ed (UA/UN/IA VAL|
00000dc2 49 32 44 20 6e 6f 74 20 61 6c 6c 20 73 30 65 74 |I2D not all s0et|
00000dd2 29 |)|
--- following line, verbatim (note the missing head) ---
[RX] 2026-10-26T16:37:09.000+08:00 26-10-26 08:37:07][NAND_FTL] Initializing NAND FTL...
--- its hexdump (88 bytes @ 3540) ---
00000dd4 5b 52 58 5d 20 32 30 32 36 2d 31 30 2d 32 36 54 |[RX] 2026-10-26T|
00000de4 31 36 3a 33 37 3a 30 39 2e 30 30 30 2b 30 38 3a |16:37:09.000+08:|
00000df4 30 30 20 32 36 2d 31 30 2d 32 36 20 30 38 3a 33 |00 26-10-26 08:3|
00000e04 37 3a 30 37 5d 5b 4e 41 4e 44 5f 46 54 4c 5d 20 |7:07][NAND_FTL] |
00000e14 49 6e 69 74 69 61 6c 69 7a 69 6e 67 20 4e 41 4e |Initializing NAN|
00000e24 44 20 46 54 4c 2e 2e 2e |D FTL...|
==========================================================================================
OCCURRENCE 2 (serial.log byte offsets 122942..123069 and 123070..123160)
==========================================================================================
--- PROT line, verbatim (may contain spliced bytes) ---
[RX] 2026-10-26T16:44:20.822+08:00 [2026-10-26 08:44:19][PROT] uncalibrated -> protection blocked (UA/UN/IA VALID[ not all set)
--- its hexdump (127 bytes @ 122942) ---
0001e03e 5b 52 58 5d 20 32 30 32 36 2d 31 30 2d 32 36 54 |[RX] 2026-10-26T|
0001e04e 31 36 3a 34 34 3a 32 30 2e 38 32 32 2b 30 38 3a |16:44:20.822+08:|
0001e05e 30 30 20 5b 32 30 32 36 2d 31 30 2d 32 36 20 30 |00 [2026-10-26 0|
0001e06e 38 3a 34 34 3a 31 39 5d 5b 50 52 4f 54 5d 20 75 |8:44:19][PROT] u|
0001e07e 6e 63 61 6c 69 62 72 61 74 65 64 20 2d 3e 20 70 |ncalibrated -> p|
0001e08e 72 6f 74 65 63 74 69 6f 6e 20 62 6c 6f 63 6b 65 |rotection blocke|
0001e09e 64 20 28 55 41 2f 55 4e 2f 49 41 20 56 41 4c 49 |d (UA/UN/IA VALI|
0001e0ae 44 5b 20 6e 6f 74 20 61 6c 6c 20 73 65 74 29 |D[ not all set)|
--- following line, verbatim (note the missing head) ---
[RX] 2026-10-26T16:44:20.822+08:00 2026-10-26 08:44:19][NAND_FTL] Initializing NAND FTL...
--- its hexdump (90 bytes @ 123070) ---
0001e0be 5b 52 58 5d 20 32 30 32 36 2d 31 30 2d 32 36 54 |[RX] 2026-10-26T|
0001e0ce 31 36 3a 34 34 3a 32 30 2e 38 32 32 2b 30 38 3a |16:44:20.822+08:|
0001e0de 30 30 20 32 30 32 36 2d 31 30 2d 32 36 20 30 38 |00 2026-10-26 08|
0001e0ee 3a 34 34 3a 31 39 5d 5b 4e 41 4e 44 5f 46 54 4c |:44:19][NAND_FTL|
0001e0fe 5d 20 49 6e 69 74 69 61 6c 69 7a 69 6e 67 20 4e |] Initializing N|
0001e10e 41 4e 44 20 46 54 4c 2e 2e 2e |AND FTL...|
=== CONTRAST: the same PROT line from boot A (16:28, flash boot) -- CLEAN ===
(from the archived boot log; note identical length minus the 3 spliced bytes)
[+365ms] [2026-10-26 08:28:27][PROT] uncalibrated -> protection blocked (UA/UN/IA VALID not all set)
=== BYTE ACCOUNTING ===
boot B: PROT line gained exactly 3 bytes: '[' , '2' , '0' (in that order, at 3 different insert points)
NAND_FTL line lost exactly its first 3 bytes: '[' , '2' , '0' -> it now starts at "26-10-26 ..."
correct head would be "[2026-10-26 08:37:07]" -> after losing "[20" it reads "26-10-26 08:37:07]"
boot C: PROT line gained 1 byte '[' ; NAND_FTL line lost its leading '[' -> reads "2026-10-26 08:44:19][NAND_FTL]..."
=> gained bytes == lost head, same order. Two writers, no line-level guard.
=== WHY IT MATTERS (not a cosmetic call) ===
- The B6 archive row in this directory (20261012_B6_prod_boot_log_PROTECT_MISS.txt) recorded the same
class: protectTask's start line arrived interleaved ("st[art: cycle=220ms elm=70") and was fixed by
moving that line behind the writer-attach gate. This is the same task (protectTask) emitting an INFO
line on the SAME boot window as the storage bring-up writer.
- Automation impact: an anchor/marker can be silently broken by splicing. Here the surviving text still
matched, but '@test' Required markers and the anchor count are only as reliable as this line.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,141 @@
[
{
"key": "e",
"why": "UNKNOWN 屏第 1 次:按 'e'(退避键序 'esl',可 --backoff-keys 覆盖)",
"ink": 3226,
"kind": "MENU",
"page": null,
"cols": [
[
"装置信息",
3,
57
]
]
},
{
"key": "l",
"why": "回根后补收一次(防被裁弹框漏计)",
"ink": 3226,
"kind": "MENU",
"page": null,
"cols": [
[
"装置信息",
3,
57
]
]
},
{
"key": "d",
"why": "在列 0 里找 参数定值",
"ink": 3182,
"kind": "MENU",
"page": null,
"cols": [
[
"实时数据",
3,
72
]
]
},
{
"key": "d",
"why": "在列 0 里找 参数定值",
"ink": 3170,
"kind": "MENU",
"page": null,
"cols": [
[
"参数定值",
3,
87
]
]
},
{
"key": "e",
"why": "进入 参数定值",
"ink": 3888,
"kind": "MENU",
"page": null,
"cols": [
[
"参数定值",
3,
87
],
[
"整定",
68,
88
]
]
},
{
"key": "e",
"why": "进入 整定",
"ink": 4958,
"kind": "MENU",
"page": null,
"cols": [
[
"参数定值",
3,
87
],
[
"整定",
68,
88
],
[
"参数",
109,
89
]
]
},
{
"key": "d",
"why": "在列 2 里找 定值",
"ink": 4948,
"kind": "MENU",
"page": null,
"cols": [
[
"参数定值",
3,
87
],
[
"整定",
68,
88
],
[
"定值",
109,
104
]
]
},
{
"key": "e",
"why": "进入 定值",
"ink": 512,
"kind": "PWD",
"page": null,
"cols": null
},
{
"key": "e",
"why": "过查看密码(出厂 000,直接确认)",
"ink": 4373,
"kind": "PAGE",
"page": "view_set",
"cols": null
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

@@ -39,6 +39,8 @@
| 2026-10-25 | `prod`(最终主干 `1a624b1`:看门狗 v1.5 按位分账 + F3 推流心跳边界)/ **f3rounds 轮① :7003 relisten_stress 单独 30 min**(11×200=2200 个 connect/close 周期,零拒绝) | **v1.1 判据 PASS**:`task stall`=0;MISS 758 条中 NET 缺席=0(旧 12 条 NET 参与组合消失达成);推流段在途峰值 103 ms / 完成段 183 ms(UI 周期最坏 254 ms)——F2 复合零误报 + F3 阻塞型治愈的定案样本;gz 前置 6 行 ASCII 来源/判据头 | `f3rounds_r1_7003_churn_30min.log.gz` |
| 2026-10-25 | 同固件 / **f3rounds 轮② e02_probe 单独 30 min**(50 个探针掉线窗) | **v1.1 判据 FAIL(如实申报,零放水)**:stall=8 = 3 事件(20:06:05/20:16:54/20:20:30,miss=998-1000/1187-1189/1249-1250),mask=0x1E7 纯 UI 单串×7 + 0x167 UI+I104×1,h= 环全部 ≥8 连窗真申报;**饿死型**(每样本点 uisn=0、mg=217/262、拖点不在推流段而在 feed/port 墙钟)=「reconcile 突发饿死 uiTask」登记行与 E-27/E-28 批 3 的直接立项证据;全窗无 IWDG 复位 | `f3rounds_r2_probe_30min.log.gz` |
| 2026-10-21 | `prod` + **兜底新鲜度门停 TIM3 注入探针**(`task/adc-health-design-doc`,探针已撤)/ 开机 + 45 s | `protect` **620/2048(69%)**、`adc` 与 heap 水位同前;`[ERR][PROT] … blocked (hf=0x00000100)` +5462ms、`recovered` +5757ms、`health:` 0 次、`reset cause:` ×1 | `20261021_probe_stale_gate_boot_log.txt` |
| 2026-10-26 | `prod`(**像来自仓外只读 detached worktree `_wt/bench-21-77779d49` @ 77779d49**;主树当时另有在飞文档改动,这份像**不含**主树未提交内容——别拿本行结论去对主树源码)/ 开机 + 30 s 空载,**@test PASS** | `protect` **492/2048(75%)**、`iec104` 540/2560(78%)、`adc` 1000/2560(60%)、`ui` 4088/16384(75%)、`default` 2160/5120(57%)、heap free=15624 **min_ever=8464**;`protection init: OK (set=40 elm=9)`、锚点 `reset cause:` ×1、**[ERR]=0 / Fault=0**、`net init: OK` **+762 ms**、身份戳 `Oct 26 2026 16:27:01 suite=NONE dstr=off` 从 hex 抠出并与线上相符、`-CmpFile` 回读 No difference found(**排在采日志之后**);⚠ **启动日志行内字节交错缺陷**:protectTask `[PROT]` 与 `[NAND_FTL]` 两个写者并发,3 次启动命中 2 次,hexdump 实证「某行内多出的字节恰等于下一行被吃掉的行首」(串口 framing 错不可能让行变长)→ 已立项派 App/log 属主 | `20261026_prod_77779d49_boot_log.txt` / `20261026_prod_77779d49_boot_log_reset_only.txt` / `20261026_prod_console_interleave_evidence.txt` |
| 2026-10-26 | 同镜像 / **整定页两条实机判据**(主站 `test/host/iec104_master.py` 以库形式复用,驱动脚本留在仓外 `_wt/.state/bench21_104.py`,**未改仓库**;复位只用 `-Rst -Run`,**全程不重烧**) | **C_RD 27/27 可寻址项全中出厂值**(清单取自 `git show 77779d49:App/algo/prot_set.[ch]` 而非工作树;**13 个 comm_addr=0 本地项结构性不可寻址 → 明确不参与本判据**,另以 0x8854/0x8855/0x8862 三个未映射地址实证「不应答」);⚠ **未标定闭锁态下 C_SE 仍允许写**(闭锁 ≠ 禁写):0x8858=7.5 / 0x885D=1234 / 0x8820=11000 三项 sel+exe 均 actcon 且 C_RD 同值 → 仅复位后仍为 7.5/1234/11000,启动日志 `[CFG] prot set loaded (/prot_set.bin)`、CORRUPT=0、version change=0、YX31=OFF(**校验和通过**);越界写 0x8858=500(>100)、0x8821=0.5(<1) 均回 **actcon+PN** 且原值一字未变;还原默认值 → 再复位 → 27/27 回默认(双向持久化,台架留在出厂态)。屏检:整定页(123 修改密码)标题右侧**「未标定」徽标在位**(Glyph 精确命中 x=105/117/129;同页用查看密码进去无徽标 = 设计如此),**LOCK 未见且可证未闭锁**(ENTER 被消费进入数值编辑,而闭锁时 ENTER 一律不消费),全程零提交;强制真 LOCK 需篡改 `/prot_set.bin`,台架禁做 | `20261026_prod_77779d49_iec104_setpoint_leg.txt` / `20261026_prod_77779d49_ui_set_badge_uncal.png`(同名 `.bin` = 原始 3200 B 帧、可 `board_ui_nav.py --classify` 复判,但被 `.gitignore` 的 `*.bin` 全局规则挡住,**入库需 `git add -f`**;顺带一条既存事实:本目录上方 B9 那两行引用的 `20261012_B9_soe*.bin` 同样从未入库,`git ls-files` 查不到)/ `20261026_prod_77779d49_ui_set_view_readonly.png` / `20261026_prod_77779d49_ui_set_edit_enter_accepted.png` / `20261026_prod_77779d49_ui_nav_trace.json` |
> f3rounds 两只是**判读证据存档**(非 `[STACK]` 水位快照):源为 serial-assistant live 日志的
> 字节偏移切片(不 clear、行边界对齐),gz 前置 ASCII 头行携带偏移窗、负载定义与 v1.1 判据
@@ -704,7 +704,7 @@ uint16_t ch395f_link_change_count(void);
| 类别 | 体量 | 为什么不做 | 那它靠什么把关 |
|---|---|---|---|
| 芯片驱动(寄存器/时序) | `ch395f.c`(801)、`gd5f2gq5ue.c`(689)、`sd2506.c`(264)、`tpafe5160.c`(157)、`rs485.c`(79) = **1,990 行** | 把 HAL 与寄存器假到底 ≈ **重写一遍驱动**,测的是自己的桩而不是硬件行为(SPI 分频必须 8、DMA 流、I2C 时序在桩里怎么编都"对") | 板级套件(`board-test-suite` skill)+ `@test` 真机日志 + 4 个 Fault 现场;陷阱逐条登记在 `docs/问题记录/CH395F_Trap_Records.md`、`docs/问题记录/GD5F2GQ5UE_Trap_Records.md`、`docs/问题记录/SD2506_Trap_Records.md` |
| UI 侧 | 17 个 `.c` = **1,942 行** | 与 PC 模拟器是**同一份源码**,行为回归已由 `ui-golden`(菜单 8 状态 + 内容页 18 页像素黄金参考)覆盖;再写单测是第三份重复投资 | `ui-verify` skill 五步 + `docs/UI设计/UI_自动化测试流程.md` |
| UI 侧 | 17 个 `.c` = **1,942 行** | 与 PC 模拟器是**同一份源码**,行为回归已由 `ui-golden`(菜单 8 状态 + 内容页 27 页像素黄金参考)覆盖;再写单测是第三份重复投资 | `ui-verify` skill 五步 + `docs/UI设计/UI_自动化测试流程.md` |
| 任务编排 | `default_task.c`(281)、`adc_task.c`(171)、`net_task.c`(123)、`protect_task.c`(90)、`ui_task.c`(71)、`rs485_task.c`(11) = **747 行** | 每个只有 4~5 个函数,主体是"按顺序调别人";真逻辑在被调模块里(那些已测或在 P1/P2)。给编排层写单测最容易写成"断言我调了几次"的自证 | 真机启动锚点计数 + IWDG `required_mask`(`@test`)+ `docs/架构设计/项目启动顺序管理.md` |
| 内核前装配 | `app_main.c`(135) | 被测对象本身就是"`osKernelStart` 之前不许调内核 API"的时序,主机侧无法忠实复现(PC 上没有那个约束) | 同上 + `cubemx-survival` 审计 |
@@ -771,3 +771,4 @@ P2 5 个 2,226 行、P3 29 个 4,814 行明确不做**(四条对得上:13+5+
未接读端而在台架上无能可见。台架与临时改动全部反向还原,设备回到产品构建并回读校验通过。
详见 §9「第 3 轮」与 `docs/子系统设计/运行与自检记录设计说明.md` §4.4;合回主干后以 `4288658`
回填内存台账,主干 `pre-commit run --all-files` 39 项全绿。 |
| 2026-10-26 | UI 侧「不做」理由里的页数口径修正为 **内容页 27 页**(原写 18,与 `simulator/tests/ref/` 入库的 27 个 `shot_*.bin` 不符)|
@@ -372,8 +372,8 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警
| 层 | 载体 | 抓住什么 |
|---|---|---|
| 文本层 | `check_content_pages.py` 字形模板匹配(50 条,解析字体 cmap / glyph_dsc / bitmap) | 缺字显示空白 / 标签写错等**静默失败**(不依赖 `ref/`,先于像素比对运行、失败即中止防误刷参考) |
| 像素层 | 11 内容页 + 菜单 8 状态,与黄金参考逐字节比对 | 几何 / 样式 / 光标反显 / 整体外观 |
| 文本层 | `check_content_pages.py` 字形模板匹配(242 条、覆盖 24 页,解析字体 cmap / glyph_dsc / bitmap) | 缺字显示空白 / 标签写错等**静默失败**(不依赖 `ref/`,先于像素比对运行、失败即中止防误刷参考) |
| 像素层 | 27 内容页 + 菜单 8 状态,与黄金参考逐字节比对 | 几何 / 样式 / 光标反显 / 整体外观 |
| 池水位层 | `MENU_POOL_BASELINE=5368 B` / `POOL_AFTER_ROUNDTRIP=5448 B` | LVGL 内存池泄漏 / 结构性漂移(量的是 `used`,与 `LV_MEM_SIZE` 总大小无关) |
### 5.3 黄金参考与字体纪律
@@ -641,3 +641,5 @@ CI 就静默停摆(2026-10-11 实测 run 16~21 共 12 个 job 零日志)。
| 2026-09-15 | §3.4 更新:P1-6 修复落地(基础线程策略)——USER CODE 区冗余 netTask 创建删除 + `net_task_func` 入口 `ENABLE_HW_LAYER_TESTS` 自挂起守卫;"代码事实/设计约束"按修复后现状重写(硬件层模式中断断言恢复安全、NET 层/产品仍按"netTask 恒在轮询"设计);彻底方案(`.ioc` 移除)经评估未采用。同日构建验证发现两处**存量**测试固件缺陷(与 P1-6 无关,A/B 对照确证):① `net_test_task.c` `s_rx_buf` 使用先于声明 → NET 层配置自 2026-08(88160a5)起不可编译;② 128KB 静态 RAM 预算溢出(产品镜像 123.6KB,HW 全阶段固件超 8.7KB、NET 固件超 ~28KB)——已立 P1-9/P1-10 跟踪 |
| 2026-09-13 | 重写:对齐现测试架构——统一 `test_config.h` 套件(旧版仅描述 CH395F 单套件与 `ch395fTestTask` 旧入口);新增 @test 自动化闭环、pre-commit 门禁、UI 模拟器五步门禁、主机侧工具约定;新增"测试构建下 netTask"已知问题(旧版"硬件层模式 netTask 不创建"表述与代码不符,改为设计约束并给出重构路径);修正附录路径(旧版 `docs/测试说明/CH395F_Test_Guide.md` 等已移入 `docs/测试说明/`);新增双端注册、内存预算、陷阱沉淀要求 |
| 2026-10-22 | §5.3 黄金参考纪律更新:`ref/*.bin` 入库(新克隆即可跑 UI 像素回归)、`.png` 仍不入库;补充"刷参考会在 `git diff` 显形,须逐页过目并在提交正文说明" |
| 2026-10-26 | 像素层页数口径修正为 **27 内容页**(判据 = `simulator/tests/ref/` 入库的 27 个 `shot_*.bin`;旧写「11」是页面扩充前的数字)。同类陈述同步在 `UI_自动化测试流程.md` 等六处改正 |
| 2026-10-26 | 三层断言表的文本层规模由「50 条」修正为 **242 条、覆盖 24 页**(现值取自 `simulator/tests/check_content_pages.py` 的 TEXTS 实跑计数;三张密码态无文本断言)|
+2 -1
View File
@@ -119,7 +119,7 @@
- **初始化回归**:`App/task/ui_task.c` 已有 `UI port init: OK` / `UI menu init: OK` / `UI net sink: started`
打印(见 `@test` 命令示例);将其加入 `-Required` 即可经串口自动回归 UI 初始化。
- **屏幕内容回归**:由 LVGL 模拟器像素回归覆盖(文本级断言 + 11 页黄金参考 + 池水位门禁,
- **屏幕内容回归**:由 LVGL 模拟器像素回归覆盖(文本级断言 + 27 页黄金参考 + 池水位门禁,
见 `docs/UI设计/UI_自动化测试流程.md`);上机屏幕仍需人工目检(L3 层,见 `测试开发标准流程.md` §1)。
## 扩展用法
@@ -149,3 +149,4 @@
| 2026-10-10 | **第三批:判定可读性 + 时序 + 采集卫生**:① Forbidden 命中逐行打印行号+内容(旧只报条数);② boot_log 每行加 `[+Nms]` daemon 毫秒时基(`--raw-log` 关闭),据此量开机耗时(实测 `net init: OK` = **+537ms**;`--max-boot-ms` 默认 0 只报告);③ `--require-order` 标记先后顺序校验(全中但失序此前不可见);④ `--json` 结构化判定(退出码不变);⑤ `--keep-out`(默认 20)清理 out 目录旧产物(本机曾堆到 31+7 份);⑥ daemon 侧:无换行残行 64KB 封顶 + open/close/clear 会话边界丢弃残行 + 截头按行边界对齐(旧实现截出来的首行是半行)+ `open` 不带 `--dtr/--rts` 时保持现状、显式给则现场应用;⑦ `ports` 回 `configured_present`/`hint`,`status` 回 `rx_age_ms`。selftest 23/23、verify_log 离线回归 17/17、真机闭环 PASS |
| 2026-10-11 | **身份戳覆盖测试宏**(N-31 复测时发现):切 `test/board/test_config.h` 的套件宏后 `app_main.c` 未被重编,测试镜像与产品镜像顶着同一个 `fw build:` 戳 → 戳校验对"两种构建互换"失明(当时只有 `-CmpFile` 回读发现镜像不同)。修复:`app_main.c` `#include "test_config.h"`(头依赖 → 改宏必重编、戳必刷新)+ 戳串追加 ` suite=<TEST_SUITE_NAME> dstr=<on|off>`(`FW_BUILD_VARIANT`,戳内容本身区分构建)。实测:`20:36:11 suite=NONE dstr=off` ↔ `20:36:40 suite=CH395F dstr=on`;`@test` 闭环对新格式 PASS(脚本无需改动,戳按"标记后连续可打印 ASCII"抠取)。失败模式表补一行:戳命中但 `suite=` 与要跑的套件不符 = 烧了产品镜像 |
| 2026-10-22 | **自检台账判据改为「只看本次启动新增」**(修两处叠出来的假绿):① 抓取用的 FTP 登录还写着匿名,而设备自鉴权落地起就没有匿名通道 → 抓取**必然** 530 失败,旧实现又把"抓取失败"和"设备侧无此文件"都返回空、一律按"无记录 → PASS"处理 → `--require-selftest-pass` 从写下那天起没真正看过文件;② 就算抓到了,`/log/selftest.log` 是 512 条环形累计历史,"非空即 FAIL"在任何做过故障注入/长跑的设备上永远不可能 PASS(本仓台架实测 29 条记录全是 2026-10-10 ~ 10-14 留下的)。现在:烧录前取基线、启动后只判新增;抓取失败 / 无数据源 / 基线被环覆盖 **一律 FAIL**;`--selftest-scope all` 保留累计口径用于审计与首次建基线;FTP 凭据 `--ftp-user/--ftp-pass`(默认出厂 admin/admin)。凭据与判据两处都过真机:`this-run` → PASS(无新增)、`all` → FAIL(累计 29 条)、无 `device_ip` → FAIL(无数据源)。 |
| 2026-10-26 | UI 屏幕自动化段的页数口径修正为 **27 页**黄金参考(与 `simulator/tests/ref/` 入库的 27 个 `shot_*.bin` 对齐;旧写 11)|
+20 -6
View File
@@ -1,11 +1,20 @@
# 门禁逐项说明(每种门禁一篇)
> 这一目录回答一个问题:**提交时那 49 个钩子到底在干什么、拦谁、放谁、什么时候会骗人。**
> 这一目录回答一个问题:**提交时那 50 个钩子到底在干什么、拦谁、放谁、什么时候会骗人。**
> 每个门禁一篇,八段固定结构:查什么 / 为什么要它 / 触发面 / 报错怎么读 / 好处 /
> **限制与盲区** / 豁免与基线 / 改门禁本身要注意什么。**最后一节是这套文档存在的理由**——
> 一份只看"拦了什么"的门禁文档,会让人以为绿就是对的。
- 配置事实源:`.pre-commit-config.yaml`(39 个 `- id:`,2026-10-23 清点)。
- 配置事实源:`.pre-commit-config.yaml`。**钩子数的唯一口径 = YAML 解析后的 hook 条目数**,不是文件里的文本行数。
自查(打印声明数;加/删一条钩子后,这个数要与 **AGENTS.md 的「共 N 个钩子」**、**本目录 §2 标题**同时对齐):
`& '.venv\Scripts\python.exe' -c "import yaml; d=yaml.safe_load(open('.pre-commit-config.yaml', encoding='utf-8')); print(sum(len(r['hooks']) for r in d['repos']))"`
→ **50**(2026-10-26:49 → 50,加的是 `generated-fresh`)。
⚠ **别用 `grep -c` 去数配置里的声明行**:该文件里有一条**注释掉的**候选钩子(clang-format,第 63 行),
文本 grep 会多算 1 = 51——「声明 51 / 文档 50 / 实跑 50」这种三个数不同源的争执就是这么来的。真要 grep,
用只认行首缩进的模式:`grep -Ec "^ +- id: " .pre-commit-config.yaml`(= 50)。
实跑侧口径:`scripts/ci_local.py` 的 `[SUMMARY] L0: pre-commit hooks -- passed=A failed=B skipped=C`,
`A+B+C` = **commit 阶段**的钩子数(49),再加 `gitlint` 1 条(它的 commit-msg 阶段声明写在**上游**仓库自带的
钩子清单里,本配置文件里查不到,见 `scripts/ci_local.py` 文件头)→ 49 + 1 = 50 = 声明数,三数同源。
- 通用机制(两阶段、stash 陷阱、`always` 已废、增量拦截三段式、旧 §2.x 查号表):
`docs/代码规范/pre-commit使用说明.md` §2。
- 快速自查(不等提交):`& '.venv\Scripts\python.exe' scripts/ci_local.py`(L0 + L0b + L0.5)。
@@ -16,12 +25,12 @@
|---|---|---|---|
| **改写型** | 不报错,直接改你的文件 | `trailing-whitespace`、`end-of-file-fixer` | 重新 `git add` 被改的文件再提交(它改的是工作区,不是索引) |
| **硬拦截** | 违反即失败,没有存量豁免 | `cppcheck`、`coding-standard`、`doc-links`、`no-mojibake`、`mem-budget`、`ui-golden`、`font-charset`、`lv-conf-sync`、`task-table`、`mem-numbers`、`registration-check`、`cubemx-survival`、`test-suite-switch`、16 个主机单测、官方 5 个、`gitlint` | 只能改代码或改文档;**不要**找豁免开关 |
| **增量拦截** | 规则恒生效,但存量钉在基线/台账里,只许减不许增 | `layer-deps`(3 条,2026-10-25 还清 UI 记录 3 条)、`bsp-err`(38 条)、`id-check`(`scripts/id_known_backlog.json` 台账) | 报错先分清是**新增**还是**存量搬家**:`--verbose` 看归类,`--prune` 看已还清 |
| **增量拦截** | 规则恒生效,但存量钉在基线/台账里,只许减不许增 | `layer-deps`(3 条,2026-10-25 还清 UI 记录 3 条)、`bsp-err`(38 条)、`id-check`(`scripts/id_known_backlog.json` 台账)、`generated-fresh`(5 处 `docs/images/` 配图存量,脚本内 `BASELINE`) | 报错先分清是**新增**还是**存量搬家**:`--verbose` 看归类,`--prune` 看已还清 |
⚠ 增量拦截最容易误会的一点:**绿灯不等于没问题**,只等于"没有比上次更差"。
每个增量拦截门禁的基线数字都写进它自己的文档,读那篇时第一眼看的就是"现在在册多少条"。
## 2. 索引(49 个 hook → 26 篇 + 1 根在 docs/测试说明/)
## 2. 索引(50 个 hook → 26 篇 + 1 根只有本表指针(专篇待补)+ 1 根详解在 docs/测试说明/)
| 门禁文档 | 覆盖的 hook id | 一句话它在查什么 | 改了哪类文件才会跑 | 文档 |
|---|---|---|---|---|
@@ -36,11 +45,12 @@
| [`mem-budget.md`](mem-budget.md) | `mem-budget` | RAM 总量红线:`RW_IRAM1` / `RW_CCM` 超 92% 即拦 | 代码与配置改动 | 已写 |
| [`task-table.md`](task-table.md) | `task-table` | README/AGENTS 里的任务表是否与 `Src/freertos.c` 一致 | `README.md`、`AGENTS.md`、`Src/freertos.c` | 已写 |
| [`registration-check.md`](registration-check.md) | `registration-check` | 新 `.c` 是否双端注册(Keil 工程 + 模拟器 CMake);UI 页面描述符表是否接线 | `.c`、`.uvprojx`、`CMakeLists.txt` | 已写 |
| [`generated-fresh.md`](generated-fresh.md) | `generated-fresh` | 生成器幂等性:把生成器重跑到**系统临时目录**,与工作区逐字节必须一致(不一致 = 生成物比生成器旧) | `tools/**`、UI 源码、`ui_menu_tree.txt`、`ui_cn_chars.txt`、`docs/images/*.svg`、两份注册表 | 已写 |
| [`lv-conf-sync.md`](lv-conf-sync.md) | `lv-conf-sync` | 两份 `lv_conf.h`(MCU / 模拟器)的特性子集是否一致 | 两份 lv_conf 或脚本 | 已写 |
| [`cubemx-survival.md`](cubemx-survival.md) | `cubemx-survival` | CubeMX 重生成会不会把定制静默抹掉(L3 审计兜底) | `.ioc`、`Src/`、`Inc/` | 已写 |
| [`test-suite-switch.md`](test-suite-switch.md) | `test-suite-switch` | 板级套件开关必须互斥;全注释 = 产品构建 | `test/board/test_config.h` | 已写 |
| [`font-charset.md`](font-charset.md) | `font-charset` | UI 用到的汉字是否都在子集字体里(缺字 = 白框) | `Drivers/BSP/UI/**`、字体脚本 | 已写 |
| [`ui-golden.md`](ui-golden.md) | `ui-golden` | UI 像素回归:菜单 8 状态 + 内容页 18 页黄金参考图比对 | UI 源码、模拟器 | 已写 |
| [`ui-golden.md`](ui-golden.md) | `ui-golden` | UI 像素回归:菜单 8 状态 + 内容页 27 页黄金参考图比对 | UI 源码、模拟器 | 已写 |
| [`bom-guard.md`](bom-guard.md) | `bom-guard` | 带 BOM 的文件被编辑工具剥掉 BOM(ARMCC 换解码 → 假语法错) | `Drivers/BSP/UI/**` 等名单内文件 | 已写 |
| [`setview-check.md`](setview-check.md) | `setview-check` | 定值查看行表与 `prot_set` 枚举序 / `g_prot_set_meta` 上下限是否同一份口径 | `tools/setview_check/**`、`ui_w_setview.*` | 已写 |
| (专篇待补,档位与限度正文在 [`../UI设计/UI_自动化测试流程.md`](../UI设计/UI_自动化测试流程.md) §7.3) | `uinav-selftest` | 板端 UI 导航器认屏快检(黄金参考帧分类;完整闭环档=合入前/上板前手工跑,快检不互替) | `scripts/board_ui_nav.py`、`simulator/tests/ref/*.bin` | 指针 |
@@ -66,7 +76,11 @@
## 4. 现状与下一步
- **本目录 23 篇已全部成文**(上表「文档」列 = 覆盖的 hook id,全部可达)。
- **本目录 26 篇已全部成文**(2026-10-26 实测口径:`docs/门禁说明/` 下除 README 外 26 篇,与上表「文档」列
**一一对应**——「有页无行」与「有行无页」两个差集都是空集;一行覆盖多个 hook 的行(`cppcheck`+`test-cppcheck`、
22 个 `*-unit-test`)按行内列举计。上一句写「23 篇」是存量误差:2026-10-25 连加 `bom-guard` /
`setview-check` / `uinav-selftest` 三根、本轮加 `generated-fresh` 都没同步它(本轮前实有 25 篇)。
**加钩子时的三步见下一行**,第 ③ 步的计数与本目录 §2 标题、AGENTS.md 的钩子数必须同一笔改。
- 新增/改动一个门禁时的三步:① 在 `.pre-commit-config.yaml` 落地;② 在本目录**照八段结构
新增或更新一篇**("限制与盲区"一节必须写真实判据的限度,不许留空);③ 在本 README 索引加一行。
- **收敛已完成(2026-10-23)**:`docs/代码规范/pre-commit使用说明.md` 的原 §2「各钩子规则详解」
+175
View File
@@ -0,0 +1,175 @@
# `generated-fresh` — 生成器幂等性(重跑生成物 diff 必须为空)
> 一句话:**生成物与它的生成器不许分家**——改完生成器(或它的输入)没重跑,
> 下一个碰巧重跑的人就会看到一段「不是我改的」diff。本钩子把「重跑一遍看有没有变化」机械化。
> 规范正文(为什么生成物必须与生成器同笔提交)→ `docs/文档规范.md` §4.4。
- 配置:`entry: python scripts/check_generated_fresh.py`,`pass_filenames: false`,
`files:` 见 `.pre-commit-config.yaml` 第 16) 段注释(覆盖生成器 / 输入 / 生成物 / 脚本自身 / 本配置)。
- 数据源:判定表与存量基线**写在脚本里**(`GENS` / `BASELINE`),没有第三方数据文件——
生成器与产物就是一张三行表,做成 JSON 只会多一处会过期的地方。
## 1. 它在查什么
对每个**在册生成器**做同一件事:把声明的输入按相对路径拷进系统临时目录 → 把生成器脚本
也拷进去 → 在临时目录里重跑 → 遍历临时目录里除 `__pycache__` 外的每个文件,与工作区同路径
**逐字节**比对:
| 结果 | 含义 | 级别 |
|---|---|---|
| DIFF | 工作区有、内容不同 → **生成物比生成器旧** | 错误(在基线内则降级 WARN) |
| NEW | 工作区没有该文件 → 重跑会凭空多出文件(漏注册 / 漏提交) | 错误 |
| 生成器 rc≠0 | 生成器自己报错(多半是输入与产物对不上) | 错误 |
| 生成器超时(300 s) | 生成器卡死 | 错误 |
| 环境缺失(只有字体链的**外部工具**:node / lv_font_conv / 系统字体) | 该台机器造不出这份产物 | SKIP(`--strict-env` 升错误) |
| 挑不出能跑字体链的**解释器**(仓库 .venv 没装 fontTools 或已坏) | 判定用不了,不是产物漂了 | 错误(退出码 2,附逐个候选的探针表与修法;**刻意不 SKIP**) |
在册生成器(`GENS`,加一个生成器就在此登记一处):
| 生成器 | 生成物 | 一并比对的副作用 |
|---|---|---|
| `tools/menu_gen/gen_menu.py` | `Drivers/BSP/UI/ui_menu_tab.h`、`Drivers/BSP/UI/ui_menu_tab.c`、`Drivers/BSP/UI/ui_page_registry.h`、`Drivers/BSP/UI/ui_page_registry.c` | 它还会回写 `MDK-ARM/STM32F407-Demo.uvprojx` 与 `simulator/CMakeLists.txt`,两份注册表一起比 |
| `tools/font_gen/extract_cn.py` | `ui_cn_chars.txt` | **唯一不按裸字节判**的一项,见 §7 |
| `tools/font_gen/make_font.py` | `Drivers/BSP/UI/ui_font_cn_12.c` | 需 node + 全局 lv_font_conv + 系统宋体,缺则 SKIP |
| `tools/arch_diagram/gen_*.py`(逐个) | `docs/images/*.svg` | 每个生成器各开一个临时目录,互不污染 |
判据是**逐字节相等**,不依赖文件头注释里写没写「自动生成」。生成器一律用 `__file__` 的三级
父目录定位仓库根,所以拷进临时目录后它只会写临时目录——**仓库一个字节都不写**:不需要还原
现场,也不会 stash 别人在飞的改动(多人共用一棵工作树也能安全跑)。
## 2. 为什么要它
- **实证一(本卡的由来)**:`0f7b5a87`(编号迁移)改了 `tools/menu_gen/gen_menu.py` 的模板
文字却没重跑生成物 → HEAD 里 `Drivers/BSP/UI/ui_menu_tab.h` 与模板不一致,后来是别人重跑
生成器才被顺带纠正(属主确认以生成器输出为准)。
- **实证二(2026-10-26 接线前实测扫出)**:5 张 `docs/images/*.svg` 与各自生成器不一致,
全部是文档挪目录后 `scripts/rename_ref.py` 改写了 `tools/` 里的引用路径、**产物没跟着重跑**
(`docs/images/项目架构总览.svg` / `docs/images/配置文件分层架构.svg` /
`docs/images/RemoDispBus帧格式.svg` / `docs/images/物理分区布局.svg` /
`docs/images/存储栈分层.svg`;逐条差异原因见脚本内 `BASELINE`)。同一天 14 个配图生成器里
9 个是干净的——所以不是「所有配图都别信」,而是**只有改过脚本的那批会漂**,恰好是没人会去
看 diff 的那批。
- 事故形态值得记清楚:**改生成器的人看不到任何 diff**(脚本改动编译、单测、像素回归全过,
因为产物没变),代价全部转嫁给下一个碰巧重跑生成器的人——他要么把这段陌生改动连带提交,
要么 revert 掉别人真正的修复。两种处置都错,而门禁能让他**不必遇到**这个选择。
- **本钩子为什么刻意「只读」**(同一批接线过程中复核到的另一件事,一并写下来,后来人需要知道):
pre-commit 的 run 子命令在**不带 `--all-files` 也不带 `--files`** 时会进入 staged_files_only 上下文
(安装目录里 commands/run.py 的判定就是 stash = not args.all_files and not args.files;进上下文后先
起一个 backup patch,钩子跑完再用 git checkout -- . 加 git apply 还原)。两个后果:
① **假红**——钩子看到的是「没有你未暂存改动」的那一份树:拆笔提交时(工作区还留着下一笔要提的
改动)生成物与它的输入会被拆成两代,任何「重跑比对」类判定都会报出一段本不存在的漂移;
② **危险**——那本质上是一次全树 checkout 回滚,靠补丁恢复,别人在同一棵树上在飞的改动经这一趟
就不再是原样(与本仓「绝不用 git checkout 丢弃未提交工作」是同一条铁律)。
所以本钩子的实现约束是硬性的:**不写仓库、不改受管区、比对基准取工作区**,代价是它自己永远不需要
还原现场,也因此可以在多人共用的工作树上随便跑。通用机制正文 →
`docs/代码规范/pre-commit使用说明.md` §2「stash 陷阱」。
## 3. 触发面
`files:` 覆盖四类(pre-commit 4.x 已移除 `always: true`,没有兜底开关,必须写全):
任何生成器 `tools/` 下全部、生成器的输入(`Drivers/BSP/UI/` 源码、`App/task/ui_task.c` 与
`App/task/ui_task.h`、`ui_menu_tree.txt`、`ui_cn_chars.txt`)、生成物(`docs/images/*.svg` +
两份注册表)、以及 `scripts/check_generated_fresh.py` 与 `.pre-commit-config.yaml` 自身
(不覆盖自身 = 谁改坏这根闸的 `files:` 它就再也不跑且无人察觉,同 `registration-check` 的口径)。
扫描范围恒为工作区全量(`pass_filenames: false`:重跑一遍与「你改了哪个文件」无关)。
## 4. 报错怎么读
```
[FAIL] 生成器幂等性 ... menu-gen
---- 漂移文件 Drivers/BSP/UI/ui_menu_tab.h(生成器 tools/menu_gen/gen_menu.py)
@@ -58 +58 @@
-bool ui_menu_tab_has_children(uint8_t node);
修法:重跑该生成器,然后把生成物与生成器**同一笔**提交
```
- **多人同树时怎么验证接线**(本会话实测口径):带 `--files` 才不触发 stash ——
`& '.venv\Scripts\python.exe' -m pre_commit run generated-fresh --files .pre-commit-config.yaml`;
只核对配置文件本身用 `-m pre_commit validate-config .pre-commit-config.yaml`。**别跑不带 `--files`
也不带 `--all-files` 的裸 run**(会 stash 别人在飞的改动,见 §2 末条)。
- 单独跑:`& '.venv\Scripts\python.exe' scripts/check_generated_fresh.py`
(`--verbose` 打差异行、`--only menu-gen,cn-font` 只跑指定项、`--list` 只列声明、
`--prune` 打基线台账、`--selftest` 自证有牙、`--keep-temp` 保留临时目录排查)。
- **钩子 entry 为什么不能只是裸 python**(2026-10-26 真实提交实测,同日修):本钩子的 entry 与其它本地
钩子一样写 `python scripts/check_generated_fresh.py`,而 pre-commit 在提交环境里把 `python` 解析成了
**机器级**解释器,那次提交日志原文:
`[FAIL] cn-font tools/font_gen/make_font.py 退出码 1:ERROR: No module named fontTools`,
解释器行打印的是 C:/Python314/python.EXE。纯标准库的 `menu-gen` / `cn-chars` 照旧绿——这是一根
**只在字库链上假红**的闸,换任何一台没把 `.venv` 排在 PATH 前面的机器或 runner 都会重犯(同族第二次
发作见 `scripts/ci_local.py` 文件头 2026-10-11 记的「L0 全灭」)。修法**不是**把 entry 写死成 `.venv`
里的路径(那正是 2026-10-11 那次把候选收窄、等于把门禁绑死在一台机器上的错),而是脚本自己按优先级
挑解释器并**逐个探针校验**:显式 `DTU_GEN_PYTHON` > 当前解释器 > 仓库 `.venv` > PATH 上探到的机器级,
且必须真能 `import fontTools` 才算数(装了 pre-commit 不等于装了 fontTools;Microsoft Store 那个
`python` 占位程序连探针都跑不起来)。整轮生成器**统一用同一个**解释器跑——换解释器可能换输出格式,
逐字节比对要的是同一把尺子。一个都挑不出来时打 `[ENV]` + 候选探针表 + 修 var / 重建 `.venv` 的指引,
并**退出码 2**,不静默 SKIP。钩子自身跑在坏解释器上时会先打一行 `[WARN]` 说明改用哪个(别把这条
WARN 读成钩子坏了)。
- `[BASELINE]` = 存量在册(§7),不拦提交;`[TIGHTEN]` = 这条已还清、可以删基线了。
- `[SKIP]` 只可能出现在字体链:那台机器造不出 `Drivers/BSP/UI/ui_font_cn_12.c`,**等于没验**,
别读成「验过了」。
- ⚠ 别拿 `--only` 的输出判断「有没有还清存量」:`[TIGHTEN]` 提示只在未指定 `--only` 时打印。
## 5. 好处
- 把「生成物与生成器同一笔提交」从**口头约定 + 事后靠别人重跑纠正**变成提交期机械判定;
- 顺带把**双端注册表**纳入同一判定:`gen_menu.py` 会把绑定页写进 uvprojx 与 CMakeLists。
漏注册在 `registration-check` 那一头是「文件存在但没注册」,在这一头是「重跑会改注册表」,
两侧独立兜同一件事——不同判据、同一事故。
- `--selftest` 自带两项牙(都不往仓库注入假改动):① 临时副本里改一行模板 → 必须报 DIFF;② 拿一个
「跑得起但看不见第三方库」的假解释器(当前解释器加 `-S` 关掉 site-packages)去要字体链 → 必须给出
可诊断的 `[ENV]` 报错与修法(退出码 2),而不是崩在 ModuleNotFoundError 上。第 ② 项就是上面那次
假红的回归用例。
## 6. 限制与盲区(请当正文读)
1. **只覆盖 `GENS` 里登记过的生成器**。没登记的(`tools/refgraph/`、
`tools/netlist_semantics/`,以及 `scripts/gen_task_table.py` 的受管表区——后者已有
`task-table` 钩子)不在闸内。新增生成器不登记 = 本钩子沉默,**这是它最大的盲区**。
2. 输入靠声明(每项一份 `seed` 清单)。漏声明不会假绿——生成器在临时目录里读不到东西会直接
报错(FAIL),但它以「生成器报错」的形态出现,容易被误读成生成器坏了。
3. 判定基准是**工作区**而不是 git 索引:别人在飞的改动会被当成输入。这是刻意的(工作树才是
事实),但意味着 UI 源码改到一半时本钩子可能报「字库比源码旧」——那是**真**没重跑,只是
还没轮到你重跑;正确处置是跑完生成器再提交,不是跳过。
4. 逐字节相等不校验「生成物内容对不对」:模板本身写错、重跑照抄,本钩子全绿。
5. 字体链 SKIP 见 §4:换机或 runner 没装 lv_font_conv 时,那一项目前是**空的**。
6. 基线只覆盖 `docs/images/` 下的 SVG。业务生成物(菜单表 / 页面注册表 / 字库 / 两份注册表)
**一条基线都没有**——这里没有豁免面可钻。
7. 解释器探针每轮只跑一次并缓存。`DTU_GEN_PYTHON` 指向一个装了 fontTools 但版本古怪的解释器时本钩子
照用它——真因若出在解释器换输出格式,症状是**所有生成物一起漂**,先看输出开头那行解释器再查生成器。
## 7. 豁免与基线(只减不增)
- 存量:脚本内 `BASELINE`,形状 = `(产物路径, 生成器路径, 漂移成因一句话)`,接线当天钉死 **5 条**
(§2 实证二)。还清一条删整条:默认运行会打印 `[TIGHTEN]` 点名「已还清仍在册」,
`--prune` 打印整张台账。同构 `layer-deps` 的 BASELINE 与 `registration-check` 的 KNOWN_GAPS:
**没有** `--write` 一类往基线加条目的开关,加一条要写清理由并过评审。
- **唯一例外判定:`ui_cn_chars.txt`**。按 **font-regen** 约定它 = 源码在用 ∪ `KEEP_EXTRA`
(历史落盘记录用字,逐条理由只在 `scripts/check_font_charset.py` 一份),裸字节比对必然不等。
故该项改判「**txt 恰等于 重跑结果 ∪ KEEP_EXTRA**」,两向都不许差:少一字 = 下次重跑会加进
txt(就是「不是我改的」diff);多一字 = 下次重跑会把它洗掉(字形不可再生,历史
`/log/selftest.log` 那批记录渲染空白且无日志无报错)。清单 importlib 直载,**不留第二份**。
与 `font-charset` 的分工要分清:那条查**白框方向**的覆盖(源码在用 ⊆ txt ⊆ 字体 cmap),
本条查 **txt 与生成链是否同一份**(多一个未登记的字也 FAIL,那条只 WARN)。
- 实测基线(2026-10-26 接线):`menu-gen` 70 个文件逐字节一致 / `cn-chars` 源码在用 165 ∪
KEEP_EXTRA 39 = 183 字、两向无差 / `cn-font` 逐字节一致(59,138 B)/ `docs-svg` 14 个生成器
= 9 一致 + 5 存量在册 → 退出码 0。
## 8. 改这个门禁本身
- 加生成器:`GENS` 加一项(声明 `seed` 输入 + `mkdir` 输出目录)→ **同笔**更新本页 §1 的表与
`docs/文档规范.md` §4.4 的在册表;先跑 `--only <新id> --verbose` 确认干净再合。
- 声明 `seed` 要按**生成器实际读了什么**写,不是按「我觉得它是输入」:`gen_menu.py` 会
`os.listdir(Drivers/BSP/UI)` 找页面实例,所以整个 UI 目录 + 两份注册表必须一起拷。
- 临时目录一律 `tempfile.mkdtemp()`:任何「顺手写回仓库」的改动都不要加——本钩子的全部安全性
来自它不写仓库(写了就要还原,还原就要面对别人在飞的改动)。
- 自测:`--selftest`。探针字符串若在生成器里被改名,自测会直接报「探针已不存在」,不会假过。
## 9. 变更记录
| 日期 | 变更 |
|---|---|
| 2026-10-26 | **同日修假红**:entry 的裸 `python` 在提交环境里被解到机器级解释器(无 fontTools),字体链
一项报 `No module named fontTools`——跑了一次真实提交才暴露。现由脚本按 ci_local 的优先级解析生成器
解释器并逐个探针校验(新增显式变量 `DTU_GEN_PYTHON`),挑不出来打 `[ENV]` + 候选表 + 修法、退出码 2 不
SKIP;`--selftest` 加「假解释器必须可诊断」第二项。判据与基线未变 |
| 2026-10-26 | 首篇逐项文档(task-18 接线当日):实证 = `0f7b5a87` 改模板没重跑 + 接线前实测 5 张 `docs/images/*.svg` 已漂(钉成只减不增基线);在册 4 项 = `gen_menu` / `extract_cn` / `make_font` / `tools/arch_diagram` 全量配图;接线实跑 `menu-gen` 70 文件一致、字库链两向无差、14 个配图生成器 9 干净 5 在册 → PASS |
+3 -3
View File
@@ -1,4 +1,4 @@
# `ui-golden` — UI 像素回归(菜单 8 状态 + 内容页 18 页黄金参考)
# `ui-golden` — UI 像素回归(菜单 8 状态 + 内容页 27 页黄金参考)
> 一句话:改 UI 源码时,把模拟器增量重编 + 两条 golden 回归串成一次提交门禁,**像素对不上就拦**。
> 它是包装器:真正的判据在被包装的两条脚本与参考图里。
@@ -11,7 +11,7 @@
## 1. 它在查什么
- 菜单侧:整棵菜单树的行渲染结果(8 个状态)与参考图逐像素比对;
- 内容页侧:11 个内容页的渲染结果与参考图比对;
- 内容页侧:27 个内容页的渲染结果与参考图比对;
- 参考图 `simulator/tests/ref/*.bin` **就是判据本身**。
## 2. 为什么要它(有牙的实测)
@@ -57,7 +57,7 @@
1. **参考图即判据**:刷新参考图 = 修改判据。评审时必须盯 `simulator/tests/ref/` 的 diff;
一旦"顺手更新",此后所有回归都只在和错的基线比。**门禁无法阻止这件事**,只能靠评审。
2. 只覆盖 8 个菜单状态 + 11 个内容页:未列入的状态(密码错误态、超长文本、缺字渲染、
2. 只覆盖 8 个菜单状态 + 27 个内容页:未列入的状态(密码错误态、超长文本、缺字渲染、
无 SD 卡 / 挂载失败的降级显示)**不会有任何回归**。
3. 跑的是**模拟器那份配置**,不是固件那份 —— 两份 `lv_conf.h` 漂移时,回归结论不能外推到固件
(所以 `lv-conf-sync` 是本门有效性的前提)。
+663
View File
@@ -0,0 +1,663 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r"""check_generated_fresh.py — 生成器幂等性门禁(重跑到临时目录 → 与工作区逐字节一致)。
背景(实证,不是预防性):
0f7b5a87(编号迁移)改了 tools/menu_gen/gen_menu.py 的**模板文字**却没重跑生成物
→ HEAD 里 Drivers/BSP/UI/ui_menu_tab.h 与模板不一致,直到后来有人重跑生成器才被
顺带纠正(属主确认以生成器输出为准)。本钩子接线前实测又扫出 **5 张 docs/images/*.svg**
与各自的生成器不同码移动——全是文档挪目录后 rename_ref.py 改了 tools/ 里的引用路径、
产物没跟着重跑(逐条见 BASELINE)。所以"生成物比生成器旧"是**系统性**缺口,不是偶发。
它的隐蔽之处在于:改生成器的人看不到任何 diff(脚本改动编译与单测全过),下一个
**碰巧**重跑生成器的人却会看到一段"不是我改的"改动 → 要么连带提交、要么 revert 掉真修复。
只读承诺(绝不写仓库):
重跑一律在 tempfile.mkdtemp()(系统临时目录)里进行。三个生成器都用
os.path.dirname(__file__) 的三级父目录定位仓库根,因此把**脚本自身**拷进 temp 后,
它写出的每个文件都落在 temp:工作区一个字节都不动,不需要还原,也不会 stash
别人在飞的改动(同一条钩子因此可以安全地跑在多人共用的工作树上)。
判定口径:
每个生成器声明 seed(按相对路径拷进 temp 的输入)与 mkdir(生成器不会自建、
但需要存在的输出目录)。跑完遍历 temp 里除 __pycache__ 外的每个文件,与工作区
同路径逐字节比对:
DIFF 工作区有、内容不同 → 生成物比生成器旧 → FAIL
NEW 工作区没有该文件 → 重跑会凭空多出文件(漏注册)→ FAIL
rc≠0 生成器自身报错 → 打印 stderr 末行 → FAIL
唯一例外是 ui_cn_chars.txt:按 font-regen 约定它 = 源码在用 ∪ KEEP_EXTRA(刻意保留
字),直接字节比对必然不等,故改判"txt 恰等于 重跑结果 ∪ KEEP_EXTRA",两个方向都不
许差——差一个字,下次重跑就会冒出或洗掉一段"不是我改的"改动。KEEP_EXTRA 用 importlib
直载 scripts/check_font_charset.py,**不抄第二份清单**(两套清单必然漂移,
同 docs/代码规范/pre-commit使用说明.md §5.5 的教训)。
存量基线 BASELINE(只减不增,同构 layer-deps 的 BASELINE / registration-check 的 KNOWN_GAPS):
接线当天(2026-10-26)实测 5 张 SVG 已漂 → 钉进基线判 WARN,避免钩子一上线就天天红。
还清一条就删一条:默认运行会点名"已还清仍在册",--prune 打印每条的出处。
⚠ 基线**只覆盖 docs/images/*.svg**。业务生成物(ui_menu_tab.* / ui_page_registry.* /
ui_font_cn_12.c / 双端注册表)一条基线都没有:漂了就是 FAIL,没有豁免面可钻。
解释器解析(2026-10-26 真实提交里实测到的假红,同日修):
钩子 entry 是裸 python,而 pre-commit 在提交环境里把它解析成了**机器级** C:/Python314——那上面
没装 fontTools,于是那次提交实测报:
[FAIL] cn-font tools/font_gen/make_font.py 退出码 1:ERROR: No module named 'fontTools'
纯标准库的 menu-gen / cn-chars 照旧绿,所以这是一根**只在字库链上假红**的闸:换任何一台没把
.venv 排在 PATH 前面的机器都会重犯(同 scripts/ci_local.py 文件头 2026-10-11 记的 L0 全灭)。
修法:脚本自己按优先级挑解释器(显式 DTU_GEN_PYTHON > 当前解释器 > 仓库 .venv > PATH 探到的
机器级)**且逐个探针校验**(要真能 import fontTools 才算数),整轮生成器统一用挑出来的这一个
跑(换解释器就可能换输出格式,逐字节比对要的是同一把尺子);一个都挑不出来时打 [ENV] + 候选
探针表 + 修 var / 重建 .venv 的指引并**退出码 2**,不静默 SKIP。
环境依赖与 SKIP:
make_font.py 需要 node + 全局 lv_font_conv + Windows 宋体 simsun.ttc 与
Segoe UI Symbol seguisym.ttf(补 ▶ 箭头字形用)。缺任一项 → 该项 SKIP 并打印
[SKIP],默认退出码 0,--strict-env 升级为 FAIL。为什么这里允许 SKIP 而 test-coverage
选 FAIL:字体链的产物在缺工具的机器上根本无法生成,FAIL 只是把人挡在提交门外、
没有任何可执行的修法;SKIP 至少把"这台机器没验过"这件事留在输出里(盲区同时登记在
docs/门禁说明/README.md 的索引行)。
用法(仓库根目录执行,python 一律用 .venv):
& '.venv\Scripts\python.exe' scripts/check_generated_fresh.py
追加开关:--list 只列声明不跑 / --only id[,id] 只跑指定项 / --verbose 打印差异行 /
--strict-env 把 SKIP 当 FAIL / --selftest 自证有牙 / --prune 打印基线台账 /
--keep-temp 保留临时目录供排查(默认删除)
退出码:
0 = 一致(含 SKIP 与存量基线 WARN)
1 = 新漂移 DIFF / NEW / 生成器报错或超时 / 自测证明判定失效
2 = 工具或环境坏了(仓库根定位失败、KEEP_EXTRA 载入失败、--only 写了不存在的 id、
挑不出能跑字体链的解释器——后者会打候选探针表与修法,不静默 SKIP)
——与"生成物漂了"分开,好让人一眼看出该修代码还是该修环境
创建日期:2026-10-26(task-18;实证 = 0f7b5a87 改模板没重跑 + 接线前实测 5 张 SVG 已漂)
"""
import argparse
import difflib
import glob
import importlib.util
import os
import shutil
import subprocess
import sys
import tempfile
import time
try:
sys.stdout.reconfigure(errors="replace") # GBK 控制台打不出 ▶ 等符号
sys.stderr.reconfigure(errors="replace")
except Exception:
pass
REPO = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
PY = sys.executable
RUN_TIMEOUT = 300 # 单生成器上限(秒);字体链实测 < 5 s
KEEP_SRC = "scripts/check_font_charset.py" # KEEP_EXTRA 的唯一事实源(importlib 直载)
# 跑生成器的解释器(argv 前缀)。默认就是跑本钩子这个;main() 里按下面的优先级解析后可能被换掉
# (见 interpreter_candidates)。2026-10-26 假红事故:钩子 entry 是裸 python,pre-commit 在提交
# 环境里把它解析成机器级 C:/Python314,那上面没装 fontTools → 字体链报 No module named fontTools。
RUN_PY = [sys.executable]
# 存量基线:接线当天实测已漂的产物,**只减不增**(还清即删整条,--prune 打印出处)
BASELINE = (
("docs/images/项目架构总览.svg", "tools/arch_diagram/gen_arch_svg.py",
"脚注写 docs/项目架构总览.md,生成器已改成 docs/架构设计/(文档挪目录,产物没重跑)"),
("docs/images/配置文件分层架构.svg", "tools/arch_diagram/gen_cfg_arch_svg.py",
"图内写 App/cfg_file.c,生成器已改成 App/cfg/cfg_file.c(配置引擎挪目录,产物没重跑)"),
("docs/images/RemoDispBus帧格式.svg", "tools/arch_diagram/gen_frame_svg.py",
"脚注写 docs/RemoDispBus协议与接收设计说明.md,生成器已改成 docs/UI设计/(挪目录)"),
("docs/images/物理分区布局.svg", "tools/arch_diagram/gen_partition_svg.py",
"脚注写 docs/STM32F4-Base存储架构说明.md,生成器已改成 docs/架构设计/(挪目录)"),
("docs/images/存储栈分层.svg", "tools/arch_diagram/gen_storage_stack_svg.py",
"同上:存储架构说明挪进 docs/架构设计/,产物脚注没重跑"),
)
GENS = (
{
"id": "menu-gen",
"what": "菜单节点表 + 页面注册表(并回写双端注册表)",
"script": "tools/menu_gen/gen_menu.py",
# gen_menu 会 os.listdir(Drivers/BSP/UI) 找页面实例并按 #page 绑定校验,
# 所以整个 UI 目录 + 两份注册表都是它的输入,必须一起拷进 temp
"seed": ["ui_menu_tree.txt", "Drivers/BSP/UI",
"MDK-ARM/STM32F407-Demo.uvprojx", "simulator/CMakeLists.txt"],
"mkdir": ["Drivers/BSP/UI"],
"mode": "exact",
},
{
"id": "cn-chars",
"what": "UI 字符集清单 ui_cn_chars.txt(字体链第 2 步)",
"script": "tools/font_gen/extract_cn.py",
"seed": ["Drivers/BSP/UI", "App/task/ui_task.c", "App/task/ui_task.h"],
"mkdir": [],
"mode": "chars-union-keepextra",
"artifact": "ui_cn_chars.txt",
},
{
"id": "cn-font",
"what": "中文子集字库 ui_font_cn_12.c(字体链第 3 步)",
"script": "tools/font_gen/make_font.py",
"seed": ["ui_cn_chars.txt"],
"mkdir": ["Drivers/BSP/UI"],
"mode": "exact",
"requires": "font_chain",
"third_party": True, # make_font 要 fontTools(从 simsun.ttc 抽 face0)
},
{
"id": "docs-svg",
"what": "docs/images 精确布局配图(每个生成器各开一个临时目录)",
"per_script": True,
"dir": "tools/arch_diagram",
"pattern": "gen_*.py",
"seed": [],
"mkdir": ["docs/images"],
"mode": "exact",
"has_baseline": True,
},
)
drifting_now = set() # 本轮实测仍在漂的基线条目(用于"可下架"提示)
def ws_path(rel):
"""仓库根相对路径(声明里一律用正斜杠)→ 本机绝对路径。"""
return os.path.join(REPO, *rel.split("/"))
# --- 解释器解析(2026-10-26 假红事故的修法;优先级复刻 scripts/ci_local.py)---------------
# 显式变量 DTU_GEN_PYTHON > 当前解释器 > 仓库 .venv > PATH 上探到的机器级,并且**必须探针校验**
# 候选真的跑得动(装了 pre-commit ≠ 装了 fontTools;Microsoft Store 的 python 占位程序更是跑不起
# 来)。为什么不能只把钩子 entry 写死成 .venv 里的解释器:ci_local.py 文件头记着 2026-10-11 那次
# 「workflow 把候选收窄成 PATH python + 仓库 .venv,runner 上两者都是死路,L0 全灭」——收窄候选
# 等于把门禁绑死在一台机器上,本处保留全部候选 + 逐个探针,就是为了不再犯第三次。
PROBE_CODE = (
"import sys\n"
"try:\n"
" import fontTools\n"
" ft = str(getattr(fontTools, 'version', 'yes'))\n"
"except Exception:\n"
" ft = ''\n"
"print('%d.%d.%d|%s' % (sys.version_info[0], sys.version_info[1], sys.version_info[2], ft))\n"
)
def interpreter_candidates(injected=()):
"""按优先级给出候选解释器 → [(标签, argv 前缀)];injected 供 --selftest 塞假解释器。"""
cands = []
explicit = os.environ.get("DTU_GEN_PYTHON")
if explicit:
cands.append(("显式变量 DTU_GEN_PYTHON", [explicit]))
cands.append(("当前解释器(跑本钩子这个)", [sys.executable]))
for rel, label in ((".venv/Scripts/python.exe", "仓库 .venv(Scripts)"),
(".venv/bin/python", "仓库 .venv(bin)")):
path = ws_path(rel)
if os.path.exists(path):
cands.append((label, [path]))
for name in ("python", "python3"):
found = shutil.which(name)
if found:
cands.append(("PATH 上的 " + name, [found]))
seen, uniq = set(), []
for label, argv in cands:
key = os.path.normcase(os.path.abspath(argv[0])) + "|" + " ".join(argv[1:])
if key in seen:
continue
seen.add(key)
uniq.append((label, argv))
return uniq + list(injected)
_probe_cache = {}
def probe_interpreter(argv):
"""探针 → {"version": "3.12.4", "fonttools": "4.51.0"};这个解释器跑不起来则 None。"""
key = " ".join(argv)
if key in _probe_cache:
return _probe_cache[key]
info = None
try:
r = subprocess.run(list(argv) + ["-c", PROBE_CODE], capture_output=True, text=True,
encoding="utf-8", errors="replace", timeout=120)
if r.returncode == 0 and "|" in (r.stdout or ""):
ver, _, ft = r.stdout.strip().partition("|")
info = {"version": ver.strip(), "fonttools": ft.strip()}
except Exception:
info = None
_probe_cache[key] = info
return info
def pick_interpreter(need_third_party, candidates=None):
"""→ (chosen|None, 候选表)。chosen = 优先级最高且满足需求的那个。"""
table, chosen = [], None
for label, argv in (interpreter_candidates() if candidates is None else candidates):
info = probe_interpreter(argv)
table.append((label, argv, info))
if chosen is None and info and (not need_third_party or info["fonttools"]):
chosen = (label, argv, info)
return chosen, table
def report_no_interpreter(table):
"""挑不出能跑字体链的解释器:逐个候选打印探针结果 + 修 var / 重建 .venv 的指引。退出码 2。"""
print(" [ENV] 没有能跑字体链的解释器(需要第三方库 fontTools)。逐个候选探针结果:")
for label, argv, info in table:
if info is None:
print(" ✗ %-44s %-24s 跑不起来(Microsoft Store 占位程序 / 已删除 / 权限)"
% (argv[0], label))
else:
print(" %s %-44s %-24s %s fontTools=%s"
% ("✓" if info["fonttools"] else "✗", argv[0], label, info["version"],
info["fonttools"] or "无"))
venv = os.path.join(REPO, ".venv", "Scripts", "python.exe")
q = chr(39)
print(" 修法(任一,改完直接重跑本脚本自检):")
print(" 1) 显式指定:PowerShell 里 $env:DTU_GEN_PYTHON = " + q + venv + q)
print(" 2) 重建仓库虚拟环境(口径 AGENTS.md「Python 环境」):")
print(" & <本机任一 python ≥3.10> -m venv .venv ; & " + q + venv + q
+ " -m pip install -r requirements.txt")
print(" ⚠ 这一项刻意**不 SKIP**:解释器或依赖坏了必须响,静默跳过等于造一根会假绿的闸")
print(" (同 test-coverage 缺 mscoverage 必 FAIL 的用户裁定精神)。")
return 2
def load_keep_extra():
"""importlib 直载 check_font_charset.KEEP_EXTRA,杜绝第二份保留字清单。"""
src = ws_path(KEEP_SRC)
if not os.path.exists(src):
raise IOError("找不到 KEEP_EXTRA 事实源: " + KEEP_SRC)
spec = importlib.util.spec_from_file_location("_keep_extra_src", src)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
keep = set(getattr(mod, "KEEP_EXTRA"))
if not keep:
raise ValueError("KEEP_EXTRA 载入后为空,判据不可能成立")
return keep
def env_font_chain():
"""字体链环境自检 → (ok, 缺什么)。"""
missing = []
if not shutil.which("node"):
missing.append("node 不在 PATH")
try:
groot = subprocess.run(["npm", "root", "-g"], capture_output=True,
text=True, timeout=60).stdout.strip()
except Exception:
groot = ""
has_conv = bool(groot) and os.path.exists(
os.path.join(groot, "lv_font_conv", "lv_font_conv.js"))
has_conv = has_conv or bool(shutil.which("lv_font_conv")
or shutil.which("lv_font_conv.cmd"))
if not has_conv:
missing.append("lv_font_conv(npm i -g lv_font_conv)")
windir = os.environ.get("WINDIR") or "C:/Windows"
for fn in ("simsun.ttc", "seguisym.ttf"):
if not os.path.exists(os.path.join(windir, "Fonts", fn)):
missing.append("系统字体 " + fn)
return (not missing), "、".join(missing)
def copy_in(tmp, rel):
s = ws_path(rel)
if not os.path.exists(s):
raise IOError("声明的输入不存在: " + rel)
d = os.path.join(tmp, *rel.split("/"))
os.makedirs(os.path.dirname(d) or tmp, exist_ok=True)
if os.path.isdir(s):
shutil.copytree(s, d, dirs_exist_ok=True,
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
else:
shutil.copy2(s, d)
def run_gen(tmp, script_rel):
"""在临时目录里重跑生成器;解释器 = RUN_PY(解析优先级见 interpreter_candidates)。"""
cmd = list(RUN_PY) + [os.path.join(tmp, *script_rel.split("/"))]
return subprocess.run(cmd, cwd=tmp, capture_output=True, text=True,
encoding="utf-8", errors="replace", timeout=RUN_TIMEOUT)
def tree_compare(tmp):
"""temp 树 vs 工作区 → (DIFF 列表, NEW 列表)。"""
diffs, news = [], []
for root, dirs, files in os.walk(tmp):
dirs[:] = [d for d in dirs if d != "__pycache__"]
for fn in files:
fp = os.path.join(root, fn)
rel = os.path.relpath(fp, tmp).replace(os.sep, "/")
ws = ws_path(rel)
if not os.path.exists(ws):
news.append(rel)
elif open(fp, "rb").read() != open(ws, "rb").read():
diffs.append(rel)
return diffs, news
def preview(rel, tmp, limit=8):
a = open(ws_path(rel), encoding="utf-8", errors="replace").read().splitlines()
b = open(os.path.join(tmp, *rel.split("/")), encoding="utf-8",
errors="replace").read().splitlines()
d = list(difflib.unified_diff(a, b, "工作区 " + rel, "重跑 " + rel, lineterm="", n=0))
for line in d[2:2 + limit]:
print(" " + line[:200])
if len(d) - 2 > limit:
print(" … 其余 %d 行略" % (len(d) - 2 - limit))
def fix_hint(script_rel):
print(" 修法:重跑该生成器 —— \"%s\" %s"
",然后把生成物与生成器**同一笔**提交(口径 docs/文档规范.md §4.4)" % (" ".join(RUN_PY), script_rel))
def check_exact(tmp, script_rel, base_map, verbose):
"""跑生成器 + 整树比对 → (state, msg, 涉及文件)。state ∈ OK/WARN/FAIL/ERR。"""
try:
r = run_gen(tmp, script_rel)
except subprocess.TimeoutExpired:
return "ERR", "%s 超过 %d s 未结束(生成器卡死?)" % (script_rel, RUN_TIMEOUT), []
if r.returncode != 0:
tail = ((r.stderr or "").strip().splitlines() or ["(无 stderr 输出)"])[-1][:180]
return "ERR", "%s 退出码 %s:%s" % (script_rel, r.returncode, tail), []
diffs, news = tree_compare(tmp)
if news:
return "FAIL", "%s 重跑会新增工作区没有的文件: %s" % (
script_rel, ", ".join(sorted(news)[:6])), news
hard = sorted(d for d in diffs if d not in base_map)
held = sorted(d for d in diffs if d in base_map)
if hard:
for d in hard[:3]:
print(" ---- 漂移文件 %s(生成器 %s)" % (d, script_rel))
preview(d, tmp, 6 if verbose else 2)
if not verbose:
print(" (--verbose 打更长的差异行)")
return "FAIL", "%d 个生成物比生成器旧: %s" % (len(hard), ", ".join(hard[:4])), hard
if held:
return "WARN", "%d 个存量基线在册: %s" % (len(held), ", ".join(held)), held
n = sum(len(files) for root, dirs, files in os.walk(tmp)
if "__pycache__" not in root.replace(os.sep, "/").split("/"))
return "OK", "%d 个文件逐字节一致" % n, []
def check_chars(tmp, gen, keep_extra, verbose):
"""ui_cn_chars.txt 专用:txt 必须恰等于 重跑结果 ∪ KEEP_EXTRA。"""
try:
r = run_gen(tmp, gen["script"])
except subprocess.TimeoutExpired:
return "ERR", "extract_cn 超时", []
if r.returncode != 0:
tail = ((r.stderr or "").strip().splitlines() or ["(无 stderr 输出)"])[-1][:180]
return "ERR", "extract_cn 退出码 %s:%s" % (r.returncode, tail), []
gen_txt = os.path.join(tmp, "ui_cn_chars.txt")
ws_txt = ws_path(gen["artifact"])
if not (os.path.exists(gen_txt) and os.path.exists(ws_txt)):
return "ERR", "字符集清单缺失(重跑侧或工作区侧)", []
re_chars = set(open(gen_txt, encoding="utf-8").read().strip())
ws_chars = set(open(ws_txt, encoding="utf-8").read().strip())
expected = re_chars | keep_extra
gap = sorted(expected - ws_chars) # 下次重跑会加进 txt
red = sorted(ws_chars - expected) # 下次重跑会洗掉 txt
detail = "源码在用 %d ∪ KEEP_EXTRA %d = %d,清单 %d" % (
len(re_chars), len(keep_extra), len(expected), len(ws_chars))
if gap or red:
if gap:
print(" [GAP] 清单缺 %d 字(下次重跑会加进来 → 「不是我改的」diff): %s"
% (len(gap), "".join(gap)))
if red:
print(" [REDUNDANT] 清单多 %d 字(下次重跑会洗掉;要么登记进 KEEP_EXTRA"
" 要么连字体一起重跑): %s" % (len(red), "".join(red)))
fix_hint(gen["script"])
return "FAIL", "txt 与生成链不等(%s)" % detail, gap + red
return "OK", detail + ",两向都无差", []
def new_tmp(tag):
return tempfile.mkdtemp(prefix="genfresh_" + tag + "_")
def drop(tmp, keep):
if not keep:
shutil.rmtree(tmp, ignore_errors=True)
def seed_gen(tmp, gen, script_rel):
for rel in gen["seed"]:
copy_in(tmp, rel)
for d in gen.get("mkdir", []):
os.makedirs(os.path.join(tmp, *d.split("/")), exist_ok=True)
copy_in(tmp, script_rel)
def run_one_gen(gen, base_map, args, keep_extra):
"""单项生成器(非 per_script)→ (state, msg)。"""
tmp = new_tmp(gen["id"])
try:
seed_gen(tmp, gen, gen["script"])
if gen["mode"] == "chars-union-keepextra":
state, msg, bad = check_chars(tmp, gen, keep_extra, args.verbose)
else:
state, msg, bad = check_exact(tmp, gen["script"], base_map, args.verbose)
for b in bad:
if b in base_map:
drifting_now.add(b)
return state, msg
except Exception as e:
return "ERR", "%s: %s" % (type(e).__name__, e)
finally:
drop(tmp, args.keep_temp)
def run_family(gen, base_map, args):
"""一个目录里每个生成器各开一个临时目录(互不污染)→ 汇总成一条结果。"""
scripts = sorted(glob.glob(os.path.join(ws_path(gen["dir"]), gen["pattern"])))
if not scripts:
return "ERR", "没找到生成器: " + gen["dir"], 0
ok = warn = 0
bad_msgs = []
for s in scripts:
rel_script = os.path.relpath(s, REPO).replace(os.sep, "/")
tmp = new_tmp(gen["id"])
try:
seed_gen(tmp, gen, rel_script)
state, msg, bad = check_exact(tmp, rel_script, base_map, args.verbose)
if state == "OK":
ok += 1
elif state == "WARN":
warn += 1
for b in bad:
script, why = base_map[b]
drifting_now.add(b)
print(" [BASELINE] %s <- %s:%s" % (b, script, why))
if args.verbose:
preview(b, tmp, 4)
fix_hint(script)
else:
bad_msgs.append("%s: %s" % (os.path.basename(s), msg))
except Exception as e:
bad_msgs.append("%s: %s: %s" % (os.path.basename(s), type(e).__name__, e))
finally:
drop(tmp, args.keep_temp)
if bad_msgs:
return "FAIL", "; ".join(bad_msgs[:3]) + (
"…共 %d 项" % len(bad_msgs) if len(bad_msgs) > 3 else ""), len(scripts)
return "WARN" if warn else "OK", "%d 个生成器:%d 一致 / %d 存量基线" % (
len(scripts), ok, warn), len(scripts)
def selftest(args):
"""两项自证(全在临时目录,不碰仓库):① 改一行模板必须报 DIFF;② 拿一个跑得起但没装
fontTools 的「假解释器」去要字体链,必须拿到可诊断的 [ENV] 报错(退出码 2)而不是崩溃。"""
return _selftest_drift(args) and _selftest_bad_interpreter()
def _selftest_bad_interpreter():
fake = [sys.executable, "-S"] # -S 关掉 site-packages:跑得起,但看不见第三方库
info = probe_interpreter(fake)
if not info or info["fonttools"]:
print(" ② 假解释器探针不合预期(%s)→ 这项跳过(不算失败,本机环境特殊)" % (info,))
return True
chosen, table = pick_interpreter(True, candidates=[
("自测注入:假解释器(-S 关 site-packages)", fake)])
code = 0 if chosen else report_no_interpreter(table)
print(" ② 探针:只要 fontTools 而候选全是假解释器 → chosen=%s、退出码=%s(要求:无 + 2)"
% ("有" if chosen else "无", code))
return chosen is None and code == 2
def _selftest_drift(args):
"""①:把 gen_menu.py 拷进临时目录、改掉一行模板文字 → 期望钩子报 DIFF(证明判定真的在跑)。"""
gen = next(g for g in GENS if g["id"] == "menu-gen")
probe = "bool ui_menu_tab_has_children(uint8_t node);"
tmp = new_tmp("selftest")
try:
seed_gen(tmp, gen, gen["script"])
sp = os.path.join(tmp, *gen["script"].split("/"))
src = open(sp, encoding="utf-8").read()
if probe not in src:
print(" [SELFTEST] 探针字符串已不在脚本里 → 自测本身失效,请更新 selftest()")
return False
open(sp, "w", encoding="utf-8", newline="").write(
src.replace(probe, probe.replace("has_children", "has_children_ZZ"), 1))
state, msg, _ = check_exact(tmp, gen["script"], {}, args.verbose)
print(" 探针:临时副本改 1 行模板 → 钩子判 %s(%s)" % (state, msg[:80]))
return state == "FAIL"
except Exception as e:
print(" [SELFTEST] 异常 %s: %s" % (type(e).__name__, e))
return False
finally:
drop(tmp, False)
def main():
ap = argparse.ArgumentParser(description="生成器幂等性门禁(重跑到临时目录 → 与工作区逐字节一致)")
ap.add_argument("--list", action="store_true", help="只打印生成器声明")
ap.add_argument("--only", default="", help="只跑这些 id(逗号分隔)")
ap.add_argument("--verbose", action="store_true", help="打印差异行")
ap.add_argument("--strict-env", action="store_true", help="环境缺失从 SKIP 升级为 FAIL")
ap.add_argument("--selftest", action="store_true", help="自证有牙(全在临时目录,不碰仓库)")
ap.add_argument("--prune", action="store_true", help="打印基线台账与已还清条目")
ap.add_argument("--keep-temp", action="store_true", help="保留临时目录(默认删除)")
args = ap.parse_args()
if not os.path.exists(ws_path(".pre-commit-config.yaml")):
print("[ENV] 仓库根定位失败: " + REPO)
return 2
want = [x.strip() for x in args.only.split(",") if x.strip()]
if want:
unknown = sorted(set(want) - set(g["id"] for g in GENS))
if unknown:
print("[USAGE] --only 里没有这个 id: %s(可选 %s)"
% (", ".join(unknown), ", ".join(g["id"] for g in GENS)))
return 2
gens = [g for g in GENS if not want or g["id"] in want]
if args.list:
print("=== 生成器幂等性:在册声明 %d 项(判定 = 重跑到临时目录后与工作区逐字节一致)===" % len(GENS))
for g in GENS:
src = g.get("script") or ("%s/%s(逐个)" % (g["dir"], g["pattern"]))
print(" %-10s %s" % (g["id"], g["what"]))
print(" 生成器 %-40s 输入 %s" % (src, ", ".join(g["seed"]) or "(仅脚本自身)"))
print(" 存量基线 %d 条(只减不增,--prune 看台账)" % len(BASELINE))
return 0
print("=== 生成器幂等性门禁 check_generated_fresh.py(重跑生成物 diff 必须为空)===")
print(" 仓库根 %s" % REPO)
print(" 钩子自身跑在 %s / 临时根 %s" % (sys.executable, tempfile.gettempdir()))
# 生成器解释器:整轮统一用同一个(换解释器可能换输出格式,逐字节比对要同一把尺子),
# 且必须探针校验——只有 node/lv_font_conv 都在时字体链才真的需要第三方库。
font_env = env_font_chain() if any(g.get("requires") == "font_chain" for g in gens) else None
need_ft = bool(font_env and font_env[0] and any(g.get("third_party") for g in gens))
chosen, table = pick_interpreter(need_ft)
if chosen is None:
return report_no_interpreter(table)
RUN_PY[:] = chosen[1]
print(" 生成器解释器:%s(%s,%s%s)" % (
" ".join(chosen[1]), chosen[0], chosen[2]["version"],
",fontTools " + chosen[2]["fonttools"] if chosen[2]["fonttools"] else ""))
if need_ft:
own = probe_interpreter([sys.executable])
if own is None or not own["fonttools"]:
print(" [WARN] 钩子 entry 是裸 python,被解析到了没有 fontTools 的解释器(%s)→"
" 生成器已改用上面那个。原因与实测证据 docs/门禁说明/generated-fresh.md §4"
% (sys.executable))
keep_extra = None
if any(g["mode"] == "chars-union-keepextra" for g in gens):
try:
keep_extra = load_keep_extra()
except Exception as e:
print("[ENV] 载入 KEEP_EXTRA 失败(%s: %s)→ 字符集那一项目前无法判定" % (type(e).__name__, e))
return 2
n_ok = n_warn = n_fail = n_skip = 0
fails = []
if args.selftest:
print("\n--- 自证有牙(临时副本里改模板,不碰仓库)---")
if selftest(args):
print(" [SELFTEST] PASS:改模板必被点名 + 坏解释器给出可诊断错误 → 判定与自检都活着")
n_ok += 1
else:
print(" [SELFTEST] FAIL:两项自测有没过 → 别信这根闸,先修钩子")
n_fail += 1
fails.append(("selftest", "模板漂移探针或假解释器诊断有一项没过"))
for gen in gens:
t0 = time.time()
if gen.get("requires") == "font_chain":
ok, why = font_env if font_env else env_font_chain()
if not ok:
if args.strict_env:
print(" [FAIL] %-9s 环境缺失:%s" % (gen["id"], why))
n_fail += 1
fails.append((gen["id"], "缺 " + why))
else:
print(" [SKIP] %-9s 环境缺失:%s → 本台未验(--strict-env 可升级为 FAIL)"
% (gen["id"], why))
n_skip += 1
continue
base_map = {}
if gen.get("has_baseline") and (not want or gen["id"] in want):
base_map = dict((rel, (script, why)) for rel, script, why in BASELINE)
if gen.get("per_script"):
state, msg, _n = run_family(gen, base_map, args)
else:
state, msg = run_one_gen(gen, base_map, args, keep_extra)
print(" [%s] %-9s %5.1fs %s" % (
{"OK": "OK ", "WARN": "WARN", "FAIL": "FAIL", "ERR": "FAIL"}[state],
gen["id"], time.time() - t0, msg))
if state == "OK":
n_ok += 1
elif state == "WARN":
n_warn += 1
else:
n_fail += 1
fails.append((gen["id"], msg))
cleared = [rel for rel, _s, _w in BASELINE if rel not in drifting_now]
if cleared and not want:
print(" [TIGHTEN] 基线 %d/%d 条本轮实测已还清,可下架(--prune 打出台账): %s"
% (len(cleared), len(BASELINE), ", ".join(os.path.basename(x) for x in cleared)))
if args.prune:
print("\n--- 存量基线台账(只减不增)---")
for rel, script, why in BASELINE:
print(" %-34s <- %-40s %s%s" % (rel, script, why,
"" if rel in drifting_now else " 【已还清,可删这一行】"))
print("汇总:%d 项 / 一致 %d / 新漂移或报错 %d / SKIP %d / 存量基线在册 %d 处(走 WARN 不拦提交)"
% (len(gens) + (1 if args.selftest else 0), n_ok, n_fail, n_skip, len(drifting_now)))
if n_fail:
print("结论: FAIL —— 生成物与它的生成器不同码移动。修法:重跑对应生成器,"
"产物与生成器**同一笔**提交(docs/文档规范.md §4.4)")
for gid, msg in fails:
print(" - %s: %s" % (gid, msg[:160]))
return 1
print("结论: PASS —— 在册生成器重跑后与工作区逐字节一致;存量基线在册 %d 处走 WARN 不拦提交%s"
% (len(drifting_now),
";SKIP %d 项 = 这台机器**没验过**那部分(不是验过了)" % n_skip if n_skip else ""))
return 0
if __name__ == "__main__":
sys.exit(main())