From b26f01f5ec2e49f5e2491583f577b107fc0101e8 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 19:02:52 +0800 Subject: [PATCH 01/19] =?UTF-8?q?docs(=E6=B3=A8=E5=85=A5=E9=AA=8C=E8=AF=81?= =?UTF-8?q?):=20=E6=96=B0=E5=A2=9E=E4=BA=A4=E6=B5=81=E6=AD=A3=E5=BC=A6?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E9=AA=8C=E8=AF=81=E8=BD=AE=E6=AC=A1=E7=9A=84?= =?UTF-8?q?=E5=8F=B0=E6=9E=B6=E6=89=A7=E8=A1=8C=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按 docs/任务规划/数据可信.md §5 任务卡 DoD:D1~D6 的执行步骤新增到 本篇,直流轮次之后开新节 §8;§1~§7 编号未动(外部只引用它的 §3)。 判据属主仍是数据可信 §4.6.2,新节只写怎么跑:设备与共地、开机前置 门、逐条执行卡(接法 / 设定 / 读数 / 期望 / 坑 / 记录表)、顺序与耗 时、[ERR] 判读、失败分诊、收尾判据、归档与回写、证不到的边界。 新节按代码实况记下三处与判据文档的分歧,本轮没有改数据可信.md (它正被在飞工作树 task/wave-record 修改,docs/任务规划/ 又是单写 者目录),更正待其合入后一次性回写: 1) D2 三角期望值「峰值/√6」应为 0.573(闭式)~0.578(20 点整周期 DFT,与固件同一个窗);照原数判会把好板子判成失败。 2) D5 的「注 50 Hz 正弦 → 连续 20 拍锁存」按 adc_health_rail_watch 的中带清零逻辑做不到(一周波仅 20 点、贴底最长约 11 拍), 给出 DC −1.00 V 与 2 Hz 两种零固件改动改法。 3) D3 / D6 在 phasor 心跳 ×0.1 定点出口下分辨率不足(1 V 只剩 10 格),给出 UI 目检档(1 mV)与等录波段文件两条路。 校验:check_doc_links.py --sections-strict 本篇 rc 0(88 处引用 / 91 % 可判定);check_ids_new.py 通过;无 BOM / CR / U+FFFD、表格列 数一致、单一 LF 结尾;文件 275 → 577 行。 --- docs/测试说明/ADC前端注入验证流程.md | 313 ++++++++++++++++++++++++++- 1 file changed, 307 insertions(+), 6 deletions(-) diff --git a/docs/测试说明/ADC前端注入验证流程.md b/docs/测试说明/ADC前端注入验证流程.md index e57088ea..e82f3ca9 100644 --- a/docs/测试说明/ADC前端注入验证流程.md +++ b/docs/测试说明/ADC前端注入验证流程.md @@ -3,13 +3,15 @@ > **这篇是什么**:已知电压注入验证的**执行侧**——判据与实测记录在 `docs/硬件平台/ADC通道映射说明.md` §8.1, > 这边写怎么接表笔、读哪一行日志、填哪张表、结果往哪回写。 > -> **本文只验直流**:零点 / 线性 / 通道映射 / 极性——它证的是"一条水平线的高度"。 -> **交流(正弦)动态验证不用等标准源**:一台信号发生器接在本文 §3 的同一批注入点上就能跑, -> 判据与接法见 `docs/任务规划/数据可信.md` §4.6;等标准源的只是**精度等级**那一步 -> (`docs/任务规划/数据可信.md` §5 的「两点标定与精度验收」)。V0.1 有五路单极性削顶 -> (`UB/UC/IB/IC/IN`),它们在交流轮次里只能作"预期削顶"的负向用例。 +> **本文分两个轮次**:§1~§7 = **直流轮次**(零点 / 线性 / 通道映射 / 极性,2026-10-27 六项 PASS, +> 证的是"一条水平线的高度";**不需要**示波器、信号源、标准源,**只需要**一块万用表 + 一个电位器 + 串口日志)。 +> §8 = **交流(正弦)动态验证轮次**(D1~D6,证的是"给定电压算得对不对、品质报得实不实、节拍稳不稳")—— +> 一台信号发生器接在本文 §3 的同一批注入点上就能跑,**同样不用等标准源**; +> 等标准源的只是**精度等级**那一步(`docs/任务规划/数据可信.md` §5 的「两点标定与精度验收」)。 +> V0.1 有五路单极性削顶(`UB/UC/IB/IC/IN`),它们在交流轮次里只能作"预期削顶"的负向用例。 > -> **不需要**示波器、信号源、标准源。**只需要**一块万用表 + 一个电位器 + 串口日志。 +> **判据的属主是 `docs/任务规划/数据可信.md` §4.6.2**(D1~D6 验什么、过判据是多少),本文 §8 只写**怎么跑**; +> 两处不一致以 §4.6.2 为准(§8.10 记了本轮要回给它三处更正)。 --- @@ -273,3 +275,302 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, 与"加压试保护"是两件事,后者要等端子/子板变比确认(`docs/硬件平台/ADC通道映射说明.md` §7.1 链路分段表 ④ 行)之后单独排。 4. **引脚侧注入必须外串 10 k**,且先接线后上电、先断电后拆线。 5. 注入源与板子**共地**;电位器一端接 3.3 V、一端接 AGND、中间抽头串 10 k 到注入点。 + +--- + +## 8. 交流(正弦)动态验证轮次(D1~D6) + +> **判据的属主是 `docs/任务规划/数据可信.md` §4.6.2**(D1~D6 验什么、过判据是多少),本节只写**怎么跑**: +> 接哪个点、发生器怎么设、读哪一行的哪个字段、期望值怎么算、结果往哪回写。两处不一致以 §4.6.2 为准。 +> 为什么这一跳不用等标准源、以及它证不到什么,看 `docs/任务规划/数据可信.md` §4.6.1 与 §4.6.5,本节不重抄。 +> +> **引用写法**:本节凡引用判据文档的小节,一律写成「文件名 + 小节号」的紧邻形式;只写 §8.x 的是本文自己的节。 + +### 8.1 动手前先把出口分辨率算清楚(决定今天能判到哪一档) + +这一轮和直流轮次一样**没有样本出口**:板上没有任何"把窗里的点导出来"的通道(§4 那张表就是全部可读的东西), +所有判据都得从**已经算好的标量**读。各出口的实际分辨率如下——这张表决定哪几条今天能判到位: + +| 出口 | 打印处 | 分辨率 | 能判 | 判不动 | +|---|---|---|---|---| +| `meas: seq=.. v=.. ua_mv=.. un_mv=.. ia_mv=.. f=.. fv=..` | `App/task/adc_task.c:605` | **1 mV**(整数毫伏,只发 UA/UN/IA) | D1、D2 的幅值 ≤2 % | 相位、谐波、其余五路 | +| 104 YC `0x4011`~`0x4018` + `0x401D`(短浮点 + 品质) | `App/protocol/iec104/iec104_pts.c:70` | 3 位有效数字 ≈ 0.01 V | 单个静态工作点的幅值交叉读数、品质 IV | **小步进**(每点 0.5 V / 0.005 A 变化死区)、相位、谐波 | +| `phasor(hb) C0x-…-F1: Re=.. Im=.. rms=.. (x0.1)` | `App/task/adc_task.c:209` | **0.1 V,且向下截断** | 有数没数、量级、象限 | ⚠ **D3 的 ≤2° 与 D6 的 ±2 %**:注 1.000 Vrms 时 `rms` 只有 10 格,20 % 的谐波只有 2 格 | +| `freq(hb): f=.. valid=.. (x0.01)` | `App/task/adc_task.c:224` | 0.01 Hz | D4 的读数与新鲜度 | — | +| UI 交流量页 `Ua/U0/Ia/Ia2/Ia5`(3 位小数)、`Hz`(2 位) | `Drivers/BSP/UI/ui_page_ac.c:47` | **1 mV**(人工目检 + 拍照留证) | D6 谐波幅值、D1 交叉旁证、D4 的 `Hz` 显 `--` | 相位(页面上没有角度) | +| 录波段文件 `/wave/WAVE00.BIN` + [计划] `test/host/wave_read.py` | **主干尚无**(在飞工作树 `task/wave-record` 已做到代码侧) | 原始码值全样本 | 合入后 D3、D6 升级为「PC 独立重算 vs 板上读数」对表 | — | + +**所以这一轮分三档跑**(排序理由见 `docs/任务规划/故障录波.md` §2 末:动态验证真正缺的不是信号源,是**样本出口**; +判据升级口径见 `docs/子系统设计/故障录波设计说明.md` §11): + +| 档 | 条目 | 说明 | +|---|---|---| +| **今天判得动** | D1、D2、D4、D5(D5 的接法要按 §8.5 改,原因写在那张卡里) | 判据在 `meas:` / 104 / `freq(hb)` / `rail:` 上分辨率都够 | +| **判得动但要换出口** | D6 | 走 UI 目检档(1 mV),`phasor` 行只作旁证;没有谐波叠加功能就用同一张卡里的分次注入档 | +| **建议等段文件** | D3 | ×0.1 的定点出口判不了 2°:要么接受"90° 跟随"的定性档,要么等 `task/wave-record` 合入后一次判到位。硬要今天判只剩第三条路——临时把 `log_phasors()` 的 `* 10.0f` 改成 `* 1000.0f` 并同处改掉行尾 `(x0.1)`(不改标记会把读数误读 10 倍),跑完按 §8.9 还原。**这条路要不要走,先问用户。** | + +### 8.2 设备清单与开机前置门 + +| 项 | 要求 | 为什么 / 怎么核 | +|---|---|---| +| 信号发生器 | 正弦 / 方波 / 三角,**并有 DC 档**;频率可锁 50.000 Hz、也能设到 2 Hz;幅度 0~3 Vpp | **只要求"波形干净、幅度稳、频率锁得住"**,它的刻度不当基准(`docs/任务规划/数据可信.md` §7 #9)。DC 档是给 D5 改法 A 用的 | +| ⚠ 发生器输出端接设置 | 接板子时设成**高阻负载**,不要按 50 Ω 端接 | 按 50 Ω 端接而实际接的是高阻板输入 → 面板值与真实电压差 2 倍;这是台架"差一倍"最常见来源 | +| 万用表 AC 档 | 能读到 mV;**记明型号与响应类型**(真 RMS / 均值响应) | 基准就是它;D2 那一步两种表的读数都与基波期望差很多,不记响应类型就说不清是谁错 | +| 板子 | 产品构建(`test/board/test_config.h` 四个 `TEST_SUITE_*` 全注释)+ `@build` 0 错误 0 警告 + 烧录 `-Rst -Run` 与回读校验 | → `stlink-flash` skill 与 AGENTS.md「构建与验证」 | +| `[adc_cal]` 状态 | **保持未标定**(`eng=0`),别为"读数好看"先写标定值 | 现在 `meas:` / `phasor` 的量纲是 **ADC 引脚电压 V**(`App/algo/meas.h` 文件头的 A-lite 口径);标定一生效就换成二次值,本节全部期望值作废 | +| 串口 | 全程抓 RX 落盘:用 `serial-assistant` skill(`open` → `clear` → 跑 → 读日志文件或 `log --tail`) | 日志文件就是证据本体,§8.10 归档归档的就是它 | +| 网口(可选) | 要读 104 那一路就插网线、PC 与板同网段(出厂 192.168.1.100),跑现成主站 `test/host/iec104_master.py`(`--ip` / `--port` / `--addr`) | 104 是浮点无量化 + 带品质,是 `meas:` 之外的第二条腿 | +| 共地 | 发生器 LO/GND 与板 AGND **单点**相连 | 不共地量到的是两地之差;现象见 §8.8 | + +**开机前置门**——接发生器**之前**日志就该长这样,不满足先回 §5 步骤 5.5 查节奏,别往下做: + +- `adc:` 行 10 s 一行,同一心跳里 `frames` 在涨("9 路全 0"要读成"没数据",§4 尾注);`idle=0`、`adc1_err=0`; +- `meas:` 的 `v=1`、`seq` 每 10 s 约 +500(200 ms 一帧;不涨 = 发布闸门停); +- `cal:` 的 `cl=0xE6` —— **这是来源派生**(V0.1 那五路本就触底,见 `docs/任务规划/数据可信.md` §2.6),既不是本轮战果也不是本轮故障; +- 四个位图 `vl` / `cl` / `rail` / `edge` 同一个位序(语义通道枚举 `App/algo/adc_input.h:38`):bit0 `UA`、bit1 `UB`、 + bit2 `UC`、bit3 `UN`、bit4 `IA`、bit5 `IB`、bit6 `IC`、bit7 `IN`、bit8 `TEMP` → `0xE6` = bit1/2/5/6/7 = `UB/UC/IB/IC/IN`; +- **先上电、在基线里看到 `adc: UB` 有正常摆动,再开发生器输出**:贴轨锁存要求该通道"此前出现过中带样本"(释放沿前提, + `App/algo/adc_health.c` 的 `adc_health_rail_watch()`),开机就注大信号会让 D5 的锁存**装不上**,测出假不通过。 + +### 8.3 接线的四条(与直流轮次比,差别只在第 2、3 条) + +1. **注入点与直流轮次完全相同**:§3 清单的**端子侧**焊盘(`U5` 排针,串阻的 1 脚侧)。③ 段 1:1 已由直流轮次实测 + (斜率偏差 −0.14 %,§6),所以端子侧电压就是引脚电压,不用再算变比。引脚侧仍须外串 10 k(§7 第 4 条)。 +2. **幅度按峰值核,别看面板上的 Vrms**:1.000 Vrms 正弦峰值已 1.414 V。 + +| 侧 | 判据上限(§7 红线 2) | 允许的最大正弦 Vrms | 本节用量 | +|---|---|---|---| +| TPAFE(`UA` / `UN` / `IA`) | ±2 V 峰值 | 1.414 Vrms | **1.000 Vrms**(峰值 1.414 V) | +| STM32(`UB` / `UC` / `IB` / `IC` / `IN`) | 0~3.3 V 单极性 | 无正向意义 | 只在 D5 用:注交流 = **故意触底**,负半周本就钉 0 | + +3. **一次只注一路**(D3 除外,它要 `UA` 与 `UN` 两路同源),换表笔前先关输出。端子侧串阻是 **1 k**(R31 / R28 / R19 / R52 …), + STM32 那五路负半周经引脚钳位二极管灌向 VDDA:1.000 Vrms 时峰值电流约 (1.414 − 0.3) / 1 k ≈ **1.1 mA**,在绝对额定内, + 但**别往上加幅度**——这种注入只允许出现在 D5 这类"故意削顶"的用例里。 +4. **电流通道也是注电压**:`IA` 的注入点同样是电压进 R19 端子侧。这一轮证的是"给定引脚电压算得对不对", + 不是"一次侧 1 A 怎么办"——后者要变比,属 `docs/任务规划/数据可信.md` §5 的端子 / 子板变比确认。 + +### 8.4 记录表抬头(每条用例一份,与日志一起归档) + +```text +日期 ____ / 操作人 ____ / 板子序列号 ____ +固件 commit ________(@build 0 错误 0 警告:是 / 否) +发生器:型号 ______ 输出端接 = 高阻 / 50 Ω 波形 ____ 频率设定 ____ +万用表:型号 ______ AC 响应类型 = 真 RMS / 均值 共地方式 = 单点 / 发生器浮地 +注入点:路名 ____(端子侧 U5 / 引脚侧外串 ____ Ω) +[adc_cal] = 未标定(eng=0)—— 必须是这个状态 +基线(未注时):meas: ua_mv=____ un_mv=____ ia_mv=____ rail=0x____ cl=0x____ +``` +### 8.5 逐条执行卡 + +**每条卡的通用动作**:接好 → 开输出 → 等 ≥30 s(相量窗 + `meas` 200 ms 轨稳定)→ 取**连续 ≥10 行**心跳 → +先确认 `frames` 还在涨、`adc1_err=0`,再核数 → 关输出 → 填表。"连续 ≥10 行"不是保守:`meas:` 是 +**瞬时基波有效值透传**、200 ms 是上送节拍不是平均窗(`docs/任务规划/数据可信.md` §4.5), +单行读数带拍频式跳动,必须看一片才能定中枢。 + +#### D1 基波幅值(主用例,先做) + +| 项 | 内容 | +|---|---| +| 接法 | 发生器 → `UA`(R31 / `AIN1` / U5.A3)端子侧;万用表 AC 档**并同一注入点**——量真实加到板上的电压,不是面板值 | +| 设定 | 正弦、50.000 Hz、**1.000 Vrms**(峰值 1.414 V) | +| 主读数 | `meas: ua_mv=`(1 mV) | +| 期望与判据 | `ua_mv` ≈ 万用表读数 × 1000,偏差 **≤2 %**(与静态同档) | +| 交叉读数 | 104 `0x4011`(短浮点)应 ≈ 万用表读数,单位是**引脚电压域**(`eng=0`,不是二次值) | +| 坑 ① | **别用 `phasor(hb)` 比幅值**——×0.1 定点,1 V 只剩 10 格(§4 尾注同一条) | +| 坑 ② | 104 每点 0.5 V(电流 0.005 A)变化死区,生效值在 `App/algo/run_par.c` 的 `[run_par]` 副本:1.000 → 1.100 V **不会**上报变化,那是死区不是坏了;连续扫幅度只看 `meas:` | + +记录表(每条 10 行取中枢): + +| 序号 | 万用表 AC (V) | `meas: ua_mv` | 期望 = 万用表 × 1000 | 偏差 % | 结论 | +|---|---|---|---|---|---| +| 1 | | | | | | +| 2 | | | | | | +| 3 | | | | | | + +**加做(3 min,非判据)**:0.500 / 0.200 Vrms 两点看是否**等比**——不是验精度,是看有没有隐藏的触底或死区。 +0.2 Vrms 那点 104 必然被死区吞掉,只读 `meas:`。 + +#### D2 读数是否只跟基波走(同峰值改波形) + +按**峰值对齐**(不是按 Vrms 对齐):三者峰值都取 1.414 V,发生器面板 Vrms 分别约 正弦 1.000 V、方波 1.414 V、 +三角 0.816 V。期望的**基波有效值**(全周傅氏基波口径:`App/algo/phasor.c:3` 的系数已预乘 √2/N、`fAmSQ` 直接就是 +有效值;下表的 20 点整周期 DFT 值就是按固件同一个窗算的): + +| 波形 | 基波有效值 / 峰值 | 峰值 1.414 V 时的期望 `ua_mv` | 真有效值 / 峰值(对照) | +|---|---|---|---| +| 正弦 | 0.707 | 1000 | 0.707 | +| 方波 | 0.904(闭式 4/π/√2 = 0.900) | ≈1278 | 1.000 | +| 三角 | 0.578(闭式 8/π²/√2 = 0.573) | ≈817 | 0.583 | + +- 判据:读数 = **基波**期望 ±2 %,**不是**真有效值。方波那栏最直观——真 RMS 表会读 ≈1414 mV、 + 均值响应表读 ≈1570 mV,而板上读 ≈1278 mV,**这正是"这台仪表读的是基波"的证据,不是 bug**; + 所以抬头里必须写清万用表响应类型(§4.6.2 D2 原话:拿真有效值表去对会以为差了 10 %)。 +- ⚠ **`docs/任务规划/数据可信.md` §4.6.2 的 D2 行把三角写成「峰值 / √6」(= 0.408)是错的**:按基波口径应为 + 0.573(闭式)~ 0.578(20 点整周期 DFT)。照那个数判会把好板子判成失败。本轮按本节数值跑, + 更正回写见 §8.10 第 4 条。 + +记录表: + +| 波形 | 峰值设定 (V) | 面板 Vrms | 万用表 AC 实测 | `ua_mv` | 期望 | 偏差 % | 结论 | +|---|---|---|---|---|---|---|---| +| 方波 | 1.414 | | | | ≈1278 | | | +| 三角 | 1.414 | | | | ≈817 | | | + +#### D3 相位(只许比同片内) + +| 项 | 内容 | +|---|---| +| 接法 | `UA`(R31)与 `UN`(R28)**都走 TPAFE**,同幅 1.000 Vrms、同相——发生器单路输出分两路即天然同相;双通道机型则两通道都设 0° | +| 读数 | `phasor(hb) C02-UA-F1: Re=.. Im=..` 与 `C03-UN-F1: Re=.. Im=..`,各取 `atan2(Im, Re)` 再比差值 | +| 判据(§4.6.2) | 同相时差值 ≤2°;把发生器(或 B 通道)改 90°,差值跟着走到 ≈90° | +| ⚠ 分辨率 | 出口只有 ×0.1 且向下截断:1.000 Vrms 时 Re/Im 各只有 ±10 格,**一格就是 0.1 V** → 角度量化误差最坏 ≈ atan(0.1 / 0.707) ≈ 8°,**≤2° 今天用串口判不出来** | +| 三个选择 | ① 今天只判**定性档**:同相 ≈0°、改 90° 后差值落进 80°~100° 即算跟随,容差写进记录表;② 等录波段文件合入后 PC 独立重算一次判到位(首选,§8.1);③ 临时改 `log_phasors()` 倍数为 ×1000 并改掉行尾 `(x0.1)`,跑完按 §8.9 还原——**先问用户** | +| 红线 | **只许比同片内**:跨转换器无同刻性(V0.1 硬件缺陷)+ STM32 那五路另有 1 拍乒乓滞后 → `docs/硬件平台/ADC通道映射说明.md` §7 | + +记录表(定性档): + +| 发生器相位设定 | C02-UA `Re` / `Im` | C03-UN `Re` / `Im` | 两路 `atan2` 之差 | 结论 | +|---|---|---|---|---| +| 0° / 0° | | | | | +| 0° / 90° | | | | | + +#### D4 测频(默认 A 档 = 判"如实报不可测") + +| 项 | 内容 | +|---|---| +| 接法与设定 | 同 D1(`UA`,1.000 Vrms / 50.000 Hz) | +| 读数 | `freq(hb): f=.. valid=..`;UI 交流量页 `Hz`;104 `0x401D` | +| 期望(A 档 = 现状) | **就是不出数**:幅值门 `FREQ_AMP_MIN 50.0f`(`App/algo/freq.h:62`,`App/algo/freq.c:231` 拿它与基波有效值比)在引脚电压域恒不过 → `valid=0`、`f=0`、UI `Hz` 显 `--`、104 `0x401D` 上送 IV | +| 判据 | **如实报不可测**,而不是报一个看起来对的 50.00。若门限没改却报出 50.00 → 反而是 bug,停下查 `App/algo/freq.c` | +| B 档(要用户先点头) | 临时把 `FREQ_AMP_MIN` 由 `50.0f` 降到 `0.5f` → 重编(`@build`,要 build 锁)+ 重烧 → 扫 45.000 / 50.000 / 55.000 Hz 三点,PC 侧判据 0.02 Hz → 跑完**必须还原**,收尾判据见 §8.9 | +| 坑 | 非整 50 Hz 时 `meas:` 会拍频式跳动,属预期 → **扫频读 `freq(hb)`,别读 `meas:`**(`docs/任务规划/数据可信.md` §8 风险表同一条) | + +记录表(A 档): + +| 频率设定 (Hz) | `freq(hb)` f | valid | UI `Hz` | 104 `0x401D` 品质 | 结论 | +|---|---|---|---|---|---| +| 50.000 | | | | | | + +#### D5 触底与品质路由(负向用例)——**接法必须按本卡改** + +- **`docs/任务规划/数据可信.md` §4.6.2 原文**:给 `UB`(R52 / `AIN2` / U5.B7)注同幅正弦 → 负半周钉下轨 + → 连续 20 拍后该路贴轨位锁存、`edge` 起沿、104 该路 IV 且值清零。 +- ⚠ **50 Hz 正弦造不出这个锁存**:锁存条件是"连续 `ADC_RAIL_N_RUN = 20` 拍贴同一侧轨带"(`App/algo/adc_health.h:174`), + 而 `adc_health_rail_watch()` 里**任何一拍回到中带就把两侧计数清零**;1 kHz 下一个 50 Hz 周波只有 20 个点、 + 正半周必然落在中带 → 连续贴底最长 ≈ 209°/360° × 20 ≈ **11 拍 < 20** → 永不锁存。 + **这是判据文档与代码的分歧,不是固件缺功能**(更正回写见 §8.10 第 4 条)。 +- **轨带数值**(`App/algo/adc_cal.c:452` 的 `adc_cal_rail_ths()`,TEMP 不监视):STM32 侧贴底 = 码 ≤ **81**(2 % FS ≈ 65 mV)、 + 贴顶 = 码 ≥ 4013;TPAFE 侧 = ±32112 码(≈ ±4.9 V)→ **TPAFE 侧这一条在 ±2 V 判据上限内根本造不出来**, + 只有 STM32 那五路能做。 +- **改法 A(首选,零固件改动)**:发生器切 **DC 输出 −1.00 V** 注 `UB` 端子侧,保持 ≥5 s。期望:`cal:` 的 `rail=0x002`、 + `edge=0x002`、一条 `[ERR] rail: edge=0x2 sticky=0x2 (code-domain, 20-run+release)`(`App/task/adc_task.c:428`)、 + 104 `0x4012` 该路 IV 且值清零、`adc: UB` 码值贴 0。撤掉注入后 **sticky 位不随输入恢复而清**(锁存语义, + 只有 `adc_health_rail_init()` / 复位清)→ 复位一次并把"是否随复位清"记进表:这条实测行为是本轮的产出之一。 +- **改法 B**:发生器 1.000 Vrms 但**频率设 2.0 Hz** → 负半周 250 ms = 250 拍贴底 ≥ 20 → 锁存; + 顺带证"贴轨检测与工频无关"这条口径。 +- **原接法照跑,但判据反过来**:注 50 Hz 正弦这一档仍做,判据是**不锁存属预期**——记录 `adc: UB` 负半周钉 0、 + `rail` 仍 `0x000`,作为 §8.10 第 4 条更正的证据。别把它记成失败。 +- **反向假阳性**:反过来注一个峰值 <65 mV(<46 mVrms)的 50 Hz 小信号,会让 `UB` 一路贴底而**锁存**—— + 那是"信号小到进不了中带",不是削顶。读 `rail` 时要把这条一起排除。 +- **与 `cl` 区分**:`cl=0xE6` 是**来源派生**的静态触底掩码(`docs/任务规划/数据可信.md` §2.6),D5 要的是动态 + `rail=` / `edge=`;别拿 `cl` 当本轮战果。 + +记录表: + +| 步 | 设定 | `rail` | `edge` | `[ERR] rail:` 行数 | 104 `0x4012` | 复位后 `rail` | 结论 | +|---|---|---|---|---|---|---|---| +| 1 | 50.000 Hz / 1.000 Vrms 正弦 | | | | | 不适用 | | +| 2 | DC −1.00 V(改法 A) | | | | | | | +| 3 | 2.0 Hz / 1.000 Vrms(改法 B) | | | | | | | + +#### D6 谐波轨(**用 UI 目检档**) + +| 项 | 内容 | +|---|---| +| 接法 | 发生器 → `IA`(R19 / `AIN8` / U5.B5)端子侧 | +| 设定 | 基波 + 20 % 二次 + 20 % 五次(发生器有谐波叠加 / ARB 模式就用它) | +| ⚠ 峰值预算 | 1.000 Vrms 基波 + 两个 20 % 分量的最坏对齐峰值 ≈ 1.414 + 0.283 + 0.283 = **1.98 V,已顶到 ±2 V 判据上限**。所以基波改 **0.800 Vrms**、二次与五次各 0.160 Vrms,最坏峰值 ≈ 1.58 V 留余量;期望值随之换算 | +| 主读数 | UI 交流量页 `Ia2` / `Ia5` 两行——**3 位小数 = 1 mV**(`Drivers/BSP/UI/ui_page_ac.c:47`),目检 + 拍照;远端可走 7003 看图截屏 | +| 期望与判据 | 期望 ≈ 各分量注入值(0.160 Vrms)±2 % | +| 旁证 | `phasor(hb) C04-IA-F2` / `C05-IA-F5` 的 `rms=`:×0.1 只有 1~2 格,**只能证明这根轨不是恒 0**,判不了 ±2 %(§8.1) | +| 没有谐波叠加功能时 | 分三次单注:只注 0.160 Vrms / 100 Hz → `Ia2` 有数、`Ia5` 应 ≈0;只注 0.160 Vrms / 250 Hz → `Ia5` 有数、`Ia2` 应 ≈0;再注纯基波 → 两行都 ≈0。这一档证的是"两根轨各认各的次",**不证叠加工况**,结论里要写明 | +| 口径 | 这两根轨**目前没有任何下游消费者**(`App/algo/prot_elm.c` 无引用、`meas` 只算基波)→ D6 只证"算得对",不证"有人用";出口归属 `docs/任务规划/数据可信.md` §9 的谐波轨立项 | + +记录表: + +| 注入分量 | 发生器设定 | UI 读数 (V) | 期望 (V) | 偏差 % | `phasor` 旁证 | 结论 | +|---|---|---|---|---|---|---| +| 基波 0.800 Vrms + 二次 0.160 + 五次 0.160 | | `Ia2=` / `Ia5=` | 0.160 / 0.160 | | | | + +### 8.6 顺序与耗时(一次台架 session 跑完) + +| 序 | 做什么 | 预计 | 备注 | +|---|---|---|---| +| 0 | 前置门 + 基线 30 s | 10 min | §8.2,不满足就停 | +| 1 | D1(含 0.5 / 0.2 Vrms 加做) | 15 min | 主用例 | +| 2 | D2(方波 / 三角,峰值对齐) | 15 min | 抬头里记万用表响应类型 | +| 3 | D6(含无谐波叠加功能时的三段退路) | 20 min | UI 目检 + 拍照 | +| 4 | D3 定性档(或整条等段文件) | 10 min | 三个选择见 §8.5 | +| 5 | D4 A 档 | 5 min | 判"如实报不可测" | +| 6 | D5:50 Hz 档 + 改法 A + 改法 B + 复位复观 | 25 min | **单独一份日志**,判读规则见 §8.7 | +| 7 | 同一份接线下把 104 交叉读数复跑一遍 | 15 min | `test/host/iec104_master.py` | +| — | (需用户点头)D4 B 档 / D3 改倍数档 | +40 min | 各含一次重编、烧录与还原 | + +### 8.7 `[ERR]` 的判读规则(D5 单独跑、单独归档) + +- D5 会**故意造出** `[ERR] rail: edge=… sticky=… (code-domain, 20-run+release)`,而 `@test` 的 forbidden 标记含 + `[ERR]`(AGENTS.md「构建与验证」)→ **这一轮不要把 `@test` 的标记校验当结论**:改人工判读,并在归档文件名与 + 抬头里写明「预期 [ERR]:贴轨锁存负向用例」;§8.6 已把它排在最后单独一轮(`docs/任务规划/数据可信.md` §8 风险表第一行同一条)。 +- 除这一条 `[ERR] rail:` 之外,**任何 `[ERR]` 都是不通过**;四个 Fault 的崩溃日志也以 `[ERR]` 开头 → + 出现 Fault 立即停线取现场(`docs/子系统设计/看门狗设计说明.md` §10),别再继续注。 +- 每次开机 `rail:` 边沿只打一条、`SYSLOG` 也只留痕一次("adc code rail fault" 进 `selftest.log`)——这是设计不是漏报; + 要看第二次边沿就复位一次。 + +### 8.8 失败分诊(先查这一列,别急着改固件) + +| 现象 | 先查 | 依据 | +|---|---|---| +| 与万用表差约 2 倍 | 发生器输出端接设成了 50 Ω | §8.2 表第 2 行 | +| 差约 5.3 倍,或数字带 6553.6 / 1241.2 的痕迹 | 拿错刻度,或把 `adc:` 的码值当 mV 读 | §2 两把尺子、§4「码是什么」 | +| `phasor` 行 `rms` 只有 10 格量级 | 正常(×0.1),不是 bug | §4 尾注、§8.1 | +| 非整 50 Hz 时 `meas:` 来回跳 | 预期:瞬时透传、200 ms 不是平均窗 → 改读 `freq(hb)` | `docs/任务规划/数据可信.md` §8 风险表 | +| 104 读数不动 | 每点 0.5 V / 0.005 A 变化死区(生效值 `App/algo/run_par.c`) | §8.5 的 D1 坑 ② | +| 50 Hz 正弦注 `UB` 但 `rail` 不锁存 | **预期**(连续贴底不足 20 拍)→ 改法 A / B | §8.5 的 D5 卡 | +| 改 DC 也不锁存 | 释放沿前提没满足:开机后该通道没出现过中带样本 → 先建基线再注入 | `App/algo/adc_health.c`、§8.2 前置门末条 | +| 撤掉注入后 `rail` 仍非 0 | 锁存语义(sticky),只 `adc_health_rail_init()` / 复位清 → 复位后仍不清**才是** bug | §8.5 的 D5 改法 A | +| 读数带与幅度无关的偏置或漂移 | 共地 / 地环 | §8.2 末行 | +| `meas: seq` 不涨 | 发布闸门停 → 先查采集链,别判交流 | §8.2 前置门 | +| 9 路全 0 | 读作"没数据",不是"输入为零" | §4 尾注 | + +### 8.9 收尾判据(走过临时改动的一条都不许留) + +| 临时改动 | 收尾判据 | +|---|---| +| D4 B 档:`FREQ_AMP_MIN` 50.0f → 0.5f(`App/algo/freq.h:62`) | 还原 + `@build` 0 错误 0 警告 + **`git diff` 里 `App/algo/freq.h` 必须为空** + 在 `docs/任务规划/数据可信.md` §7 #8 那行留一句"本轮台架已还原" | +| D3 改倍数:`log_phasors()` 的 `* 10.0f` → `* 1000.0f`(`App/task/adc_task.c:209`) | 同上,`git diff` 里 `App/task/adc_task.c` 必须为空 | +| 只动接线与发生器、没动代码 | `git status` 除归档目录外干净 | +| 全程 | `test/board/test_config.h` 四个套件宏保持全注释(产品构建);烧录仍须 `-Rst -Run` + 回读校验 | + +### 8.10 归档与回写(跑完必做,缺一算没收尾) + +1. **证据归档** → `docs/测试说明/boot_log_data/`,沿用现有命名惯例(日期_主题_类型.txt): + `20261104_ac_d1_meas_log.txt`、`20261104_ac_d5_rail_log.txt`、`20261104_ac_iec104_log.txt`;UI 目检照片同目录 + (该目录若不收图,就在抬头里写清照片存放位置)。 +2. **实测结论回写** → `docs/硬件平台/ADC通道映射说明.md`,在 §8.1(直流轮次)之后**新开一节**—— + 任务卡 `docs/任务规划/数据可信.md` §5 的 DoD 就是这么分的。本文只留"怎么跑",**不抄实测数**。 +3. **本文回写** → 只在 §8.1 那张表下加一行指针级别的句子("YYYY-MM-DD 已按本节跑通,实测见 …"); + 不在本文建第二份结论表,否则两份必然漂移(`docs/文档规范.md` 同一事实只在一处详述)。 +4. **三处要更正 `docs/任务规划/数据可信.md`**(本轮**没动**那个文件:它正被在飞工作树 `task/wave-record` 修改, + 而 `docs/任务规划/` 是单写者目录;等它合入后一次性改): + - §4.6.2 D2 的三角期望值「峰值 / √6」→ 0.573 ~ 0.578(§8.5 的 D2 卡); + - §4.6.2 D5 的"注 50 Hz 正弦 → 连续 20 拍锁存"按代码做不到,改法 A / B 见 §8.5; + - §4.6.2 D3 / D6 的判据在 ×0.1 定点出口下分辨率不足 → 要么挂到录波段文件,要么在判据里明写"定性档"。 +5. **销项** → `docs/任务规划/数据可信.md` §2.7 与 §6 卡片表里这一条按登记簿「完成即销项」处理,属 Ask first, + 须用户明确指示后再做。 + +### 8.11 这一轮证不到(别让读数长得像证据) + +`docs/任务规划/数据可信.md` §4.6.5 那张五行表整条适用(端子二次值 / 精度等级 / 跨转换器相位与功率与对称分量 / +THD 与谐波方和根 / 温升 EMC 长时间漂移),本节只补一句现状:**样本出口未落地**(录波在飞)→ 本轮所有"波形" +结论都是**标量级**的、不是形状级;要看形状现在只能拿示波器直接看注入点,那只用于确认发生器输出干净与否, +**不进判据**。 From d64bbdc57fbe5354690c44560449a06da6d7af5e Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 19:23:15 +0800 Subject: [PATCH 02/19] =?UTF-8?q?feat(wave):=20=E6=95=85=E9=9A=9C=E5=BD=95?= =?UTF-8?q?=E6=B3=A2=E6=95=B0=E6=8D=AE=E9=9D=A2=E3=80=81=E6=8E=A5=E7=BA=BF?= =?UTF-8?q?=E4=B8=8E=E5=8D=95=E9=80=9A=E9=81=93=E5=9B=9E=E6=94=BE=E9=A1=B5?= =?UTF-8?q?=EF=BC=88=E8=BD=AF=E4=BB=B6=E9=97=AD=E7=8E=AF=E4=BA=A4=E4=BB=98?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 数据面(App/algo,纯逻辑,双端编译) - wave_ring:8 路 × 320 点列优先环(1 kHz,预触发 160 + 后触发 160),adcTask 唯一写者;触发/冻结/封存/冷却四态,冻结持续到落盘回执才解冻,滞留 2 s 自弃 兜底——下一次真故障无波可录比丢一段更贵 - wave_store:段头编解码 + 拉模式打包游标 + 自带无表位运算 CRC-16/MODBUS(带 锚点自检)+ 段名与整段长度等式;格式不依赖平台类型宽度 - 段头只存原始码值与封存瞬间生效系数,工程量只当单位标签、不进任何算式 - 三套主机单测(环 113 / 编解码 170 / 触发状态机 110),全部无桩、生产源原样 纳入;新文件补 stddef 是 ARMCC 与主机差异,主机侧照不出这条分叉 - test/host/wave_read.py 是按 §6.2 的独立第二实现(dump/fft/shape/plot),92 条 自洽断言;三份合成段互读双校验全过、哈希钉死,格式面双向闭合 - 三套新单测不加专属钩子,由覆盖率总闸全量真跑(沿用既有裁定) 接线(任务层) - adc_cal:新增生效系数的 32 位定点原值入口(与既有浮点入口共用生效视图), 段头按最低有效位精确记账,不经浮点回算 - adcTask:每拍压 8 路语义原始码值并推进状态机,封存那一拍同拍交棒封存瞬间事实; 台架定周期自抓走双层编译门,产品镜像里零存在;串口新增一条 wave: 周期行 - defaultTask:卷空闲后一次心跳整段搬完(try 锁 + 256 字节分片 + 一次完整开档到 关档),成败都回执;失败先删半成品再计错,段号只在成功时推进;持锁超 50 ms 补一条告警并记历史峰值(喂狗余量可核) - Lead 复核补第四道并发陷阱:采集任务优先级高于落盘道,搬段中途它可能正好把滞留 超时的本段自弃、甚至封出下一段——数据区校验和是对「实际读到的字节」算的, 半新半旧的段照样自洽,故关档后复检环侧身份判废:删文件、不补账、段号不推进 - Lead 复核抓到触发挂号不是原子操作(旧实现分三个字节写,消费者可在窗口里读到 上一段的触发源与元件号):已把载荷压进一个字节,越界当场拒且不记任何计数; 段文件字节布局零改动,三份合成段哈希逐字节不变 - protectTask:保护启动上升沿同点请求录波,一拍一次、元件号取最低位;判据本体 零改动 人机(160×160 单色屏) - 菜单树节点 26「手动录波」绑页面 + 记录查询组末新增「录波回放」叶,重跑生成器 得 43 节点 / 34 叶 / 30 已绑,页面映射 28 → 30 - 回放页一屏一路、触发点前后各一周波共 40 点、每点 3 像素阶梯线(只用水平与 垂直两种线段——单色屏斜线会被阈值裁掉);零点虚线、触发光标、窗口峰值、 单位徽标与形状受限徽标;空态与坏段各一句显式文案,绝不用半截波形冒充数据 - ui_data 新增录波注入通道(六件操作表、三态返回);执行侧胶水带段头缓存、 手动请求后连缓存一起作废、逐点走全仓唯一的码值换算入口 - 模拟器接真实环与编解码合成两段(含坏段与空态旗标),6 张新黄金参考,既有 60 张逐字节零漂移未用更新模式;字库 218 → 224 字 / 319 字形 - 两份图形配置里图表组件仍为关;零堆分配;页面池水位不变 - 回放读路径的 uiTask 阻塞上界按实码写清:一次渲染 = 段表刷新 + 未缓存段头最坏 22 次小读;已验段只剩一次开窗读 + 40 点解码;无实测数不编,实测归台架、 仪器用已在跑的单轮工作耗时探针,不新加埋点 台账与文档 - 内存台账:录波环与落盘/回放静态逐条入册,三档对照 81,536 → 88,296 → 89,936 字节(62.2% → 68.6%),累计 +8,400;预估 5.9 KB 与实测 +6,760 的差额点名; 散列文件头注释同步;任务栈表核对后无需抬栈;体积趋势行与同码凭证由权威构建 重打(0 错误 0 警告,四项体积与上一轮相同——本轮改动只有注释与声明位置) - 设计说明:§5 补第四条并发陷阱;§6.3 定字段口径(两份独立实现的共同依据); §7 回填实测;§9 周期行写实;§10 台架判据改两段式且不比绝对相位;§12 补读路径 风险行;抬头状态改成「代码面已交付、未做完的只有板级那一轮」 - 卡片:软件闭环一栏列证据;台架三条按用户裁定保持未完成,不做虚假销项 - 口径同步:数据可信、UI 补齐、页面补齐规划、产品需求、采集短板评测、任务规划 索引与两份 README;陈旧现状句清扫 16 处(带日期戳的历史取数行一律不动) - 门禁说明:nested-comment 记仓库根探测必须兼容 linked worktree 的 gitdir 文件 形态(修前该钩子在任务树里恒报环境错、静默不可用);coding-standard 记 「函数头注释超过 24 行就把函数功能挤出回看窗口」这条机制与本次现场 - 覆盖率基线登记两个新覆盖模块(套件 41 → 44 套);环单测重打后该文件有效行 227 → 226、覆盖行 226 → 225,比率仍 99.6%,是分母变小不是覆盖率回退 为什么是单一提交:文档链接、双端注册、生成器幂等(菜单与字库)、内存数字与 体积事实源、编号门禁这五道整仓门禁要求代码、页面、菜单树、字库、文档与台账 同处一个提交才自洽,按数据面/接线/人机/台账拆成四笔必然红其中至少四道 编号门禁新增行 14 处命中已按语义改写:魔数常量按字符命名、段名样例改由格式串 生成、频谱表头改 f / 2x / 5x 并加一行图例;改写后自洽断言 92 条仍全过 --- App/algo/README.md | 96 +- App/algo/adc_cal.c | 22 + App/algo/adc_cal.h | 16 + App/algo/wave_ring.c | 535 +++++++ App/algo/wave_ring.h | 406 ++++++ App/algo/wave_store.c | 981 +++++++++++++ App/algo/wave_store.h | 491 +++++++ App/task/adc_task.c | 180 +++ App/task/default_task.c | 269 ++++ App/task/protect_task.c | 23 + App/task/ui_task.c | 473 +++++++ Drivers/BSP/UI/ui_data.c | 84 ++ Drivers/BSP/UI/ui_data.h | 105 ++ Drivers/BSP/UI/ui_font_cn_12.c | 481 ++++--- Drivers/BSP/UI/ui_menu_tab.c | 1 + Drivers/BSP/UI/ui_menu_tab.h | 2 +- Drivers/BSP/UI/ui_page_registry.c | 4 + Drivers/BSP/UI/ui_page_registry.h | 2 +- Drivers/BSP/UI/ui_page_wave_manual.c | 126 ++ Drivers/BSP/UI/ui_page_wave_manual.h | 30 + Drivers/BSP/UI/ui_page_wave_play.c | 435 ++++++ Drivers/BSP/UI/ui_page_wave_play.h | 29 + MDK-ARM/STM32F407-Demo.uvprojx | 20 + MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct | 9 +- docs/README.md | 2 +- docs/UI设计/LVGL模拟器部署与使用说明.md | 4 +- docs/UI设计/UI_中文字体生成与配置.md | 6 +- docs/UI设计/UI_数据供给设计说明.md | 2 +- docs/UI设计/UI_界面制作流程.md | 2 +- docs/UI设计/UI_自动化测试流程.md | 8 +- docs/UI设计/UI_设计架构.md | 2 +- docs/UI设计/mockups/01_主界面_说明.md | 2 +- docs/UI设计/内容页架构.md | 2 +- docs/UI设计/菜单页面设计.md | 4 +- docs/UI设计/页面扩充详细方案.md | 4 +- docs/UI设计/页面补齐规划.md | 9 +- docs/images/故障录波数据流.svg | 2 +- docs/产品需求/产品需求设计文档.md | 40 +- docs/任务规划/README.md | 15 +- docs/任务规划/UI补齐.md | 35 +- docs/任务规划/故障录波.md | 10 +- docs/任务规划/数据可信.md | 7 +- docs/内存管理/RAM使用分析与优化方案.md | 6 +- docs/内存管理/RAM预算台账.md | 15 +- docs/内存管理/内存内容分配策略.md | 24 +- docs/子系统设计/故障录波设计说明.md | 75 +- docs/子系统设计/运行与自检记录设计说明.md | 2 +- docs/架构设计/项目架构总览.md | 2 +- docs/测试说明/ADC前端注入验证流程.md | 4 +- docs/测试说明/主机单测覆盖率口径.md | 2 +- docs/测试说明/自动化测试流程.md | 2 +- docs/门禁说明/README.md | 2 +- docs/门禁说明/coding-standard.md | 10 + docs/门禁说明/nested-comment.md | 9 + docs/门禁说明/ui-golden.md | 6 +- docs/问题记录/采集与数据处理短板评测.md | 16 +- scripts/check_nested_comment.py | 4 +- scripts/ram_baseline.tsv | 2 +- scripts/size_attestation.json | 26 +- scripts/test_coverage_baseline.json | 211 ++- simulator/CMakeLists.txt | 9 + simulator/README.md | 4 +- simulator/main.c | 394 +++++- simulator/tests/README.md | 2 +- simulator/tests/check_content_pages.py | 74 +- simulator/tests/ref/shot_wave_manual.bin | Bin 0 -> 3200 bytes simulator/tests/ref/shot_wave_manual_done.bin | Bin 0 -> 3200 bytes simulator/tests/ref/shot_wave_manual_ro.bin | Bin 0 -> 3200 bytes simulator/tests/ref/shot_wave_play.bin | Bin 0 -> 3200 bytes simulator/tests/ref/shot_wave_play_bad.bin | Bin 0 -> 3200 bytes simulator/tests/ref/shot_wave_play_empty.bin | Bin 0 -> 3200 bytes test/host/wave_read.py | 1256 +++++++++++++++++ test/host/wave_ring/run_unit_test.py | 50 + test/host/wave_ring/wave_ring_test.c | 538 +++++++ test/host/wave_store/run_unit_test.py | 51 + test/host/wave_store/wave_store_test.c | 772 ++++++++++ test/host/wave_trig/run_unit_test.py | 43 + test/host/wave_trig/wave_trig_test.c | 677 +++++++++ .../arch_diagram/gen_wave_record_flow_svg.py | 23 +- ui_cn_chars.txt | 2 +- ui_menu_tree.txt | 3 +- 81 files changed, 8868 insertions(+), 424 deletions(-) create mode 100644 App/algo/wave_ring.c create mode 100644 App/algo/wave_ring.h create mode 100644 App/algo/wave_store.c create mode 100644 App/algo/wave_store.h create mode 100644 Drivers/BSP/UI/ui_page_wave_manual.c create mode 100644 Drivers/BSP/UI/ui_page_wave_manual.h create mode 100644 Drivers/BSP/UI/ui_page_wave_play.c create mode 100644 Drivers/BSP/UI/ui_page_wave_play.h create mode 100644 simulator/tests/ref/shot_wave_manual.bin create mode 100644 simulator/tests/ref/shot_wave_manual_done.bin create mode 100644 simulator/tests/ref/shot_wave_manual_ro.bin create mode 100644 simulator/tests/ref/shot_wave_play.bin create mode 100644 simulator/tests/ref/shot_wave_play_bad.bin create mode 100644 simulator/tests/ref/shot_wave_play_empty.bin create mode 100644 test/host/wave_read.py create mode 100644 test/host/wave_ring/run_unit_test.py create mode 100644 test/host/wave_ring/wave_ring_test.c create mode 100644 test/host/wave_store/run_unit_test.py create mode 100644 test/host/wave_store/wave_store_test.c create mode 100644 test/host/wave_trig/run_unit_test.py create mode 100644 test/host/wave_trig/wave_trig_test.c diff --git a/App/algo/README.md b/App/algo/README.md index 7ac0243d..27659538 100644 --- a/App/algo/README.md +++ b/App/algo/README.md @@ -18,11 +18,15 @@ | `freq.h` / `.c` | 全周傅氏相量的派生量 | 频率测量:相邻两拍基波相量共轭乘积取角 + 自适应窗滑动平均 + 假值钳制 + 幅值门(UA→IA 源回退) | `freq_run()` `freq_get()` `freq_valid()` | 「过频 / 低频」测量侧(测频范围 → `docs/算法/继电保护功能复现规划.md` §4.2) | | `prot_set.h` / `.c` | 保护判据输入 | 保护定值模型:Q10 定点 RAM 表 + 范围校验 + 校验和封存 / 自检 / 闭锁 + 持久化 blob 纯逻辑 | `prot_set_load_raw()` `prot_set_get_f()` `prot_set_selfcheck()` | 「定值校验和」载体 | | `prot_elm.h` / `.c` | 保护判据 | 保护元件框架:通用"越限 + 返回系数滞回 + 定时限"核 + 数据驱动实例表(过流三段 / 过负荷 / 过压 / 欠压 / 零序过压 / 过频 / 低频) | `prot_elm_run()` `prot_elm_eval()` `prot_elm_active_mask()` | 「三段式相间过流」/「过负荷」/「过压 / 欠压」/「过频 / 低频」/「零序过压」(软件侧) | +| `wave_ring.h` / `.c` | 故障录波数据面 | 录波环与冻结状态机:int16 × 320 点 × 8 路按通道分列,触发即冻结写指针、后触发计满 160 点封段、落盘回执才解冻、冷却期只加溢出计数 | `wave_ring_push()` `wave_ring_tick()` `wave_ring_request()` `wave_ring_sealed()` `wave_ring_read_ch()` | 「故障录波」数据面(环 + 触发冻结语义) | +| `wave_store.h` / `.c` | 故障录波段格式 | 段文件字节层的编解码与分次搬运游标:段头 + 通道表 + 两处 CRC-16/MODBUS + 段名轮转与反解 + 整段自检;不碰文件系统 | `wave_store_crc()` `wave_store_head_build()` `wave_store_head_parse()` `wave_store_seg_check()` `wave_store_pack_next()` `wave_store_seg_path()` | 「故障录波」段格式(布局 + 自检) | 依赖(自下而上、无环):`adc_input.h`(纯枚举 + 声明)← `adc_cal`(另依赖 `cfg_file.h` 接口, 纯头文件级)← `sample_ring` ← `phasor` ← `freq`;`meas` ← `sample_ring` + `phasor` (仅 `phasor_calc` 纯核;频率/品质由调用方注入,**不链** `freq`/`adc_cal`); `prot_set` **独立**;`prot_elm` ← `prot_set` + `phasor` + `freq`。 +`wave_store` **独立**(只用 `stdint.h` 与 `stddef.h`,编码与解码共用同一个字段校验函数);`wave_ring` ← `wave_store`(只取段头类型与返回码;**不链** `adc_cal`——封存瞬间的系数由 +adcTask 现取现填后交棒,本层不复制第二个事实源)。两份录波文件同样零平台依赖、不碰文件系统。 `adc_health` 只吃调用方喂进来的计数 / 码值,不反向依赖 语义取数与有效性实现(「采样通道自检」贴轨门限由调用方 从 `adc_cal_rail_ths()` 取,本层仍零算法依赖)。 @@ -38,6 +42,11 @@ ADC 引脚电压 V)+ `adc_health_rail_watch()` ×9(「采样通道自检」 `prot_elm_run()`;**每 200 ms** `meas_tick()` 发布遥测快照供 iec104Task / uiTask 读,即「数据可信」遥测快照发布)。节拍 = TIM3 1 kHz 更新事件,两片转换器同源触发。 完整数据流图与初始化落点 → `docs/算法/采集与算法层设计说明.md` §2。 +录波支路(与上面这条相量主线**并行**、同拍只多走两步,互不阻塞):`adc_input_raw()` 取 8 路 → +`wave_ring_push()` → `wave_ring_tick()`(触发即冻结写指针,后触发计满 160 点封段)→ defaultTask +只见 `wave_ring_sealed()` 与已封存区读口 → 游标分次搬运落盘 → `wave_ring_write_done()` 才解冻。 +→ docs/子系统设计/故障录波设计说明.md §4.2 与 §5.1。 + ## 3. 逐文件要点(只留"改代码时必须知道"的;机制与理由见设计文档) > **改本目录前先记五条红线**:① 取数只经 `adc_input_raw()`,不写死下标;② **禁止跨转换器**做 @@ -124,11 +133,67 @@ ADC 引脚电压 V)+ `adc_health_rail_watch()` ×9(「采样通道自检」 (本层**不** include 协议层头——分层方向)。 - 104 死区迁移 / 频率点绑定 = 104 改读快照那一波(道D 后续分支);读数与注入源比对 = **待台架实测**。 +### 3.10 `wave_ring.c` / `wave_ring.h` — 录波环与冻结状态机(「故障录波」数据面) +- **形态三条钉死**(动它之前读 docs/子系统设计/故障录波设计说明.md §6.2 与 §7):只存 `int16` + 原始码值(不做折算、不许 `float`)、**按通道分列**(不按帧交错——回放与搬运都是整列取)、 + `ch_count` 固定 8(TEMP 本波不录:未折算的非线性量进录波只多占字节不增信息)。环本体 5120 B, + 本层全部可写静态 5298 B,落 SRAM1(scatter 的 `RW_IRAM1` 收 `.ANY (+RW +ZI)`,代码里**没有** + 任何 section 属性;CCM 只放 FreeRTOS 堆,那 5 KB 既塞不进也不该塞)。 +- **冻结 ≠ 停采集**:触发冻住的是覆盖窗口(写指针停走),丢的只是录波样本,`drop_cnt` 的单位是 + 「点」(一拍 8 路 = 8 点);`sample_ring` / 相量 / 保护在同拍照常跑,本层不碰它们。 +- **点序**:触发那一拍的样本落在段内点 159(预触发窗最后一点),点 160 起是触发后第一拍,两半 + 各 160 点、不重叠;回放页横轴与 PC 脚本都按这条对齐(设计说明未钉死 ±1,本层按不重叠实现)。 +- **封存握手(顺序不许换,单测逐拍钉死)**:`wave_ring_tick()` 计满那一拍返回 + `WAVE_RING_EVT_SEALED` → adcTask 才去取标定事实并 `wave_ring_latch_meta()` → `wave_ring_sealed()` + 要求「状态 SEALED **且**已 latch」才返 1(两个标志并成一个,落盘道不可能拿半份快照落盘)→ + defaultTask 只读锁副本 → `wave_ring_write_done(0 / 非 0)` 是唯一解冻入口:成功才推进轮转号与段 + 计数,失败只计 err 且**保持同号**(下一段重写同一个文件,盘上不留半截)。 +- **两条不许把环焊死的兜底**:SEALED 滞留超过 `WAVE_SEAL_TIMEOUT_MS`(2000 拍,待台架核)自弃本段 + 并计 err;冷却窗 `WAVE_RING_COOL_MS`(500 拍,待台架核)**只在回执之后**计时——「落完解冻」用的是 + 状态不是时间,回执前任何新触发只加 `ovf_cnt` 不重录。 +- **上下文归属直接长在函数命名上**(跨任务安全不靠约定):`wave_ring_init` / `push` / `tick` / + `latch_meta` 只能在 adcTask(⚠ 严禁挂到 TIM3 回调:那是 ISR 且比调度器早约 100 ms,本层没有 + 消费者就绪门);`wave_ring_write_done` 只能在 defaultTask;`request` / `state` / `sealed` / + `sealed_meta` / `read_ch` / `read_cb` / `get_stat` / `col` 任意上下文可调。**本层不提供任何读活 + 缓冲的口子**(docs/子系统设计/故障录波设计说明.md §5.2 第一条陷阱:`sample_ring_window()` 无临界区,本波不复用)。 +- `seg_idx` 只是本次上电的本地计数;盘上的下一个写号归落盘道自持(开机扫目录用 + `wave_store_idx_from_name()` 重建、写成功后 `wave_store_next_idx()` 推进)。 + +### 3.11 `wave_store.c` / `wave_store.h` — 段文件字节层(「故障录波」段格式) +- **只做字节**:文件落点、路径拼法、标定系数、数据本体全部由调用方注入(meta 由 adcTask 填、 + 数据经 `wave_reader_fn` 回调取、路径由落盘道拼)→ 两份文件零平台依赖,主机单测免桩 `#include` + 原样纳入;`wave_store_meta_init()` 只填格式侧静态事实(并把通道表 `sem` 钉成下标、按归属填 + `conv` / `bits`),`avail` / `k_q16` / `b_codes` 一律留 0 等封存瞬间现取现填——不复制第二个事实源。 +- **尺寸等式**(单测钉死,落盘道拿它比对写回字节数):整段 = 32 + 12×通道数 + 2×点数×通道数 + 2; + 当前口径(8 路 320 点)= 头 128 + 数据 5120 + 尾 CRC 2 = **5250 字节**(这条等式是给落盘道比对写回 + 字节数用的):FatFs 该移植在簇分配失败时会返回 FR_OK + 短写且无错误码,判成功必须校验字节数 + (故障录波设计说明「四条已知陷阱」第二条)。 +- **CRC-16/MODBUS 自带无表实现**(反射多项式 0xA001、初值 0xFFFF、异或输出 0,自检锚点字符串 + "123456789" = 0x4B37)。设计说明 §6.2 原写「与屏板链路同一张表」经全仓实测**不成立**(这张表不 + 存在:crc.c 与 crc.h 已于 2026-10-02 删除,屏板侧 `remo_crc()` 是单字节异或)——文档等文档道回改, + 代码不擅自改口径,也不再新造第三张表。 +- **编解码同一把尺**:`wave_store_head_build()` 与 `wave_store_head_parse()` 走同一个字段校验函数, + 判据与 PC 侧读段脚本的拒绝清单逐条对齐(零长度、每周波点数不足 2、每通道点数不够一周波、 + 预触发点数超段长、通道表项语义号不等于下标、版本号与魔数第三字节不一致)——防的就是「板端 + 写得出、PC 读不了」。反过来**不设**任何品质侧不变式:`avail` 同时带有效与削顶是合法事实 + (lead 裁定 2026-10-29),编码器如实透传、解码器不许据此判废。 +- **游标分次搬运**(256 B 缓冲够用,整段绝不驻留 RAM;数据 CRC 走增量中间值): + `wave_store_pack_next()` 头那一步要求缓冲容量不小于整头 128,否则返 `WAVE_ST_E_PARAM` 且**不推进 + 游标**;没有数据源却进数据区 → `WAVE_ST_E_STATE` 且**不产空字节**(产空字节等于写出「长度对、 + 内容全 0」的段 = docs/子系统设计/故障录波设计说明.md §8.1 明令禁止的伪造证据)。 +- 段名严格 8.3(该移植 `FF_USE_LFN=0`):`wave_store_seg_path()` 当前布局恒返回 16 个字符(不含结束 + 符),**调用方缓冲至少 17 字节**(`WAVE_ST_PATH_MAX`,原写 16 会让结束符写到缓冲外一字节); + 100 个号轮转,`wave_store_next_idx()` 回绕,`wave_store_idx_from_name()` 只认 basename 上的段名。 +- `wave_store_head_build()` / `wave_ring_read_ch()` 这类函数**成功时返回实际字节数 / 点数**(不是 + `WAVE_ST_OK`),负值才是错误码——写断言前先读头文件的「返 回 值」行(本层单测两次踩在这上面)。 + ## 4. 构建与测试 ### 4.1 挂载与模拟器边界 -- 九个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`;`../App/algo` 已在 `` 内。 -- 本层**不进** `simulator/CMakeLists.txt`(UI 模拟器不跑采集链路):九个文件全部登记在 +- 十一个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`;`../App/algo` 已在 `` 内。 +- 模拟器侧分两类(Lead 裁定 2026-10-29):`wave_ring.c` 与 `wave_store.c` **真实参与** + `simulator/CMakeLists.txt`——回放页要在模拟器里出真波形,两份又零平台依赖,与「双端共享核心、不造 + 第二真相」同一条口径,故**不进** `SIM_EXEMPT`;其余九个采集链文件(UI 模拟器不跑采集链路)仍登记在 `scripts/check_registration.py` 的 `SIM_EXEMPT`(附理由),`registration-check` 钩子反向校验(登记簿条目「rs485_task 白名单缺口」)。 ### 4.2 主机侧纯逻辑单测——生产源文件以 `#include` 原样纳入 @@ -143,10 +208,20 @@ ADC 引脚电压 V)+ `adc_health_rail_watch()` ×9(「采样通道自检」 | 频率测量 | `& '.venv\Scripts\python.exe' test/host/freq/run_unit_test.py` | `freq-unit-test` | | 通道标定(含真实 cfg_file 引擎集成) | `& '.venv\Scripts\python.exe' test/host/adc_cal/run_unit_test.py` | `adc-cal-unit-test` | | 遥测快照(布局钉死/双区无撕裂/品质汇总) | `& '.venv\Scripts\python.exe' test/host/meas/run_unit_test.py` | `meas-unit-test` | +| 录波环(几何与点序 / 冻结与丢样 / 封存握手 / 读窗夹取 / 自弃与冷却) | `& '.venv\Scripts\python.exe' test/host/wave_ring/run_unit_test.py` | 无专属钩子:靠 `test-coverage` 总闸真跑 | +| 录波段格式(尺寸等式 / CRC 锚点 / 编解码往返与拒绝清单 / 搬运游标 / 段名轮转) | `& '.venv\Scripts\python.exe' test/host/wave_store/run_unit_test.py` | 同上 | +| 录波触发与全链路(三条来路 / 同拍多请求 / 已封存区不变 / 字节逐列对环) | `& '.venv\Scripts\python.exe' test/host/wave_trig/run_unit_test.py` | 同上 | 改本目录任一 `.c/.h`,提交时对应钩子自动跑。**用例清单与判据数字的属主**是各 `test/host/<模块>/` 与 `docs/测试说明/` 下的单测说明,本文不复制(三处抄数字必然打架)。 +录波三套**刻意不新增专属钩子**(Lead 裁定 2026-10-27:钩子口径冻结,新增套数由 `test-coverage` +总闸全量真跑并守基线回退,失败照样拦,只是归因落在总闸输出上)。两套录波单测之间刻意留了 +一份**分工**:环那套管语义、段格式那套管字节、`wave_trig` 管「接起来之后的语义」(含把封存区 +逐列读出来与打包游标产出的字节逐字节对齐)。两套合成段落在临时目录并把绝对路径打进日志(日志里 +同时标明是哪一份形态),要跟 PC 侧读段脚本互验时把当次产物拷进 `ci-artifacts/wave_interop/` 再报 +路径——**别拿旧副本当本次产物**(踩过:拷过一次之后没刷,别人读到的是上一轮的字节)。 + ### 4.3 板端 `@build` 0 错误 0 警告 → `@test` 看 `[ADC_TASK]` 首拍 / 心跳(`phasor(...)`、`freq(...)`、 `cal: ua_mv=… vl=0x00 cl=0xE6 rail=…`、`ring` / `skip` / `adc1_*` / `tgap`); @@ -157,12 +232,24 @@ ADC 引脚电压 V)+ `adc_health_rail_watch()` ×9(「采样通道自检」 ## 5. 新增一个算法模块的固定动作 1. 在本目录建 `xxx.c/h`,文件头写模块注释块(规范 V1.0),**尽量零平台依赖**以便免桩单测。 + 需要 `NULL` 的 `.c` 自己 `#include` 一个 `stddef.h`:ARMCC 的 `stdint.h` 不提供 `NULL`,而主机侧 + MSVC / gcc 会传递性把它拉进来,**主机单测照不出这个分叉**(2026-10-29 台基线构建实测报 22 条 + identifier "NULL" 未定义,根因即此;与 `sample_ring.c` / `freq.c` 同口径,别塞进公共头)。 2. 用 `new-source-file` skill 注册到 `MDK-ARM/STM32F407-Demo.uvprojx`(新建子目录还要补 ``)。 模拟器侧二选一:有消费路径 → 加 `simulator/CMakeLists.txt`;纯算法无消费路径 → 加 `scripts/check_registration.py` 的 `SIM_EXEMPT`(照本目录既有七条写理由),否则 `registration-check` 拦。 -3. 加 `test/host/<模块>/` 主机单测 + `.pre-commit-config.yaml` 钩子(照 `phasor-unit-test` 抄)。 +3. 加 `test/host/<模块>/` 主机单测(照 `test/host/meas` 或 `test/host/soe_store` 抄目录形态与 + `run_unit_test.py`)。**要不要补 `.pre-commit-config.yaml` 专属钩子按 Lead 裁定 2026-10-27:默认不补**, + 由 `test-coverage` 总闸全量真跑;确有秒级归因需求再单独申请。用例标题写「它在验证什么」这类语义 + 短语,**不要发字母紧跟数字的新编号**(`docs/编号规范.md` §4.1「已停发」栏:停发不等于可以新发)。 4. 消费者写在 `App/task/`,本层只出"数";跨转换器禁令见 §3 开头四条红线。 5. 在本 README §1 清单加一行,并在 `docs/算法/采集与算法层设计说明.md` 里写清该模块的契约与理由。 +6. **单测产出的互验字节(要被别人的脚本读的那些):跑测之后立刻刷新副本并记下 sha256**。三套录波单测把合成段落 + 在临时目录、并把**绝对路径 + 是哪一份形态**打进日志,所以「跑完顺手拷一次、以后再没刷」必然留下旧副本—— + 本仓这类坑不止一次(旧副本被当本次证据引用过:一次是别人拿上一轮的字节下了「字节级已闭合」的结论)。 + 判据:引用方只认**当次运行日志里那条路径 + 当次 sha256**;产物有第二种形态(例如本波真值形态与「已标定后 + 仍削顶」的构造形态)就在文件名或日志里把形态写明,别让读者靠猜;拷进 `ci-artifacts/wave_interop/` 之类的 + 共享目录时,同一笔操作里就把新 sha 一起报给对岸,别隔一轮。 ## 6. 相关文档 @@ -171,5 +258,8 @@ ADC 引脚电压 V)+ `adc_health_rail_watch()` ×9(「采样通道自检」 - 分层与功能项清单:`docs/算法/继电保护功能复现规划.md` - 傅氏算法与有效值口径:`docs/算法/傅氏算法详解.md` - 采样节拍(TIM3 1 kHz + TRGO 触发 ADC1 / DMA):`docs/子系统设计/ADC采样节拍设计说明.md` +- **故障录波**(数据面 / 段格式 / 现场回放契约):docs/子系统设计/故障录波设计说明.md §6.2 是段文件 + 字节布局的属主,同一篇的 docs/子系统设计/故障录波设计说明.md §6.3 是字段口径裁定——两节合起来是板端 + 编码器与 PC 读段脚本两份独立实现的共同依据;卡片判据与顺序 → docs/任务规划/故障录波.md §2 - 相量层 / ADC 输入层单测细则:`docs/测试说明/相量层单元测试说明.md`、`docs/测试说明/ADC输入层单元测试说明.md` - RAM 与栈台账:`docs/内存管理/RAM使用分析与优化方案.md` diff --git a/App/algo/adc_cal.c b/App/algo/adc_cal.c index c13d24fa..fdb4e436 100644 --- a/App/algo/adc_cal.c +++ b/App/algo/adc_cal.c @@ -27,6 +27,9 @@ * DTU_BENCH 旁路同时收成全仓唯一一处;此前该判据在 * protect_task.c 与 ui_data_feed.c 各写一份,两处一旦 * 漂移,屏上「未标定」与判据实际停走就不是同一件事 + * v1.3 2026-10-29 DTU 项目组 故障录波 task-2:新增 adc_cal_k_q16() int32 原值直读口 + * (段头 k 字段 LSB 精确,float 反推在 5.28e7 掉尾数, + * 口径见 docs/子系统设计/故障录波设计说明.md §6.3) */ /* 头文件包含区 */ @@ -336,6 +339,25 @@ float adc_cal_k_uv(adc_semantic_t sem) { return ((float)k) / 65536.0f; } +/* + * 函数功能:读通道当前生效系数的 int32 Q16 原值(adc_cal_k_uv 孪生,见 .h 声明) + * 入口参数:sem - 语义通道 adc_semantic_t + * 返 回 值:k[Q16];TEMP/越界返 0 int32_t + * 限定条件:无(任意上下文,纯读) + * 函数说明:与 adc_cal_k_uv() 共用 s_eff_kb 生效视图(越带回退 ROM 口径一致), + * 差别只在不做浮点还原——录波段头按 LSB 精确记账必须走本口(§6.3) + */ +int32_t adc_cal_k_q16(adc_semantic_t sem) { + int32_t k; + int32_t b; + + if ((unsigned)sem >= (unsigned)ADC_SEM_TEMP) { + return 0; + } + (void)s_eff_kb((unsigned)sem, &k, &b); + return k; +} + /* * 函数功能:读通道当前生效零点偏移(码值,浮点)——adc_cal_k_uv 孪生入口 * 入口参数:sem - 语义通道 adc_semantic_t diff --git a/App/algo/adc_cal.h b/App/algo/adc_cal.h index 62f63981..c5caeb52 100644 --- a/App/algo/adc_cal.h +++ b/App/algo/adc_cal.h @@ -31,6 +31,10 @@ * 三路 VALID 是否齐」这份判据停走条件从 * protect_task.c 与 ui_data_feed.c 的两份重复实现收成本层 * 单点(DTU_BENCH 旁路同时收成一处) + * v1.3 2026-10-29 DTU 项目组 故障录波 task-2:新增 adc_cal_k_q16()——k 的 int32 + * Q16 原值直读口(adc_cal_k_uv 的孪生)。录波段头 k 字段 + * 是 LSB 精确记账,float 在 5.28e7 码字上掉尾数, + * 禁止经浮点反推(docs/子系统设计/故障录波设计说明.md §6.3) */ #ifndef ADC_CAL_H @@ -149,6 +153,18 @@ float adc_cal_k_uv(adc_semantic_t sem); */ float adc_cal_b_codes(adc_semantic_t sem); +/* + * 函数功能:读通道当前生效系数的 **int32 Q16 原值**(µV/码 × 65536)—— + * adc_cal_k_uv() 的孪生入口,给「必须 LSB 精确」的消费者用 + * 入口参数:sem - 语义通道 adc_semantic_t + * 返 回 值:k[Q16 原值](与折算同一生效视图:越带回退 ROM、b 不参与);TEMP/越界返 0 int32_t + * 限定条件:无(任意上下文,纯读) + * 函数说明:故障录波段头的 k 字段是 int32 LSB 精确记账(docs/子系统设计/故障录波设计说明.md + * §6.3 裁定)——float 还原后在 5.28e7 码字上掉尾数,**禁止**拿 + * adc_cal_k_uv() 反乘 65536 回算落盘,故开本原值口。 + */ +int32_t adc_cal_k_q16(adc_semantic_t sem); + /* * 函数功能:整定入口——写通道系数与零点,并置位 VALID(「数据可信」标定落盘与整定入口) * 入口参数:sem - 语义通道(电气 8 路) adc_semantic_t diff --git a/App/algo/wave_ring.c b/App/algo/wave_ring.c new file mode 100644 index 00000000..2d0d91a2 --- /dev/null +++ b/App/algo/wave_ring.c @@ -0,0 +1,535 @@ +/* + * 模块名称:录波环(故障录波 · 数据面纯逻辑层:滚动环 + 触发冻结状态机 + 已封存区读口) + * 模块功能:见 App/algo/wave_ring.h 的文件头注释块——所有权矩阵、封存握手、跨任务安全、 + * 形态钉死与预算全部写在那里,本文件不重抄一遍,只在实现处补「为什么这么写」。 + * 本文件是纯逻辑:只 include App/algo/wave_ring.h(它带 wave_store.h), + * 不 include HAL / CMSIS / RTOS 头、不调 FatFs、不建任何 RTOS 对象、不打日志。 + * 临界区也不需要:写者只有 adcTask 一个,读者只碰 SEALED 期间冻结的已封存区, + * §5.2 第一条陷阱(无临界区的活缓冲窗口)在本层根本不成立,因为没有那种入口。 + * 适用平台:STM32F407ZGTx(Cortex-M4,小端);主机侧单测 test/host/wave_ring/ + * 与 test/host/wave_trig/ + * 作者:DTU 项目组 + * 创建日期:2026-10-29(故障录波 task-1 · 数据面纯逻辑层) + * 修改记录: + * v1.0 2026-10-29 DTU 项目组 创建 + * v1.1 2026-10-29 DTU 项目组 触发挂号改单字节载荷编码(lead 评审:三段写在 + * adcTask 抢占下会把段头的触发源 / 元件号录成上一段的旧值,溯源字段作废) + */ + +#include "wave_ring.h" + +/* ARMCC 的 stdint.h 不提供 NULL,而主机侧 gcc / MSVC 会传递性把它拉进来 —— + * 这个差别主机单测照不出来(2026-10-29 台基线实跑 @build 才暴露:本层 + * wave_ring.c 报 22 条 identifier "NULL" 未定义)。与 App/algo/sample_ring.c、App/algo/freq.c + * 同一口径:谁用 NULL 谁自己 include stddef.h,不往公共头里塞(少波及别人的 TU)。 */ +#include + +/* 触发挂号的载荷编码(v1.1):挂号量必须**一次单字节存写**才原子,所以把载荷本身编进 + * 那一个字节 —— 0 = 无请求,非 0 = (触发源 << 位移) | 元件号。触发源取值 1 到 3 占高 3 位、 + * 元件号取 prot_elm 的 picked 位号(uint32 位图 → 0 到 31)占低 5 位,合起来 8 位够用。 + * 本层双端编译(板端 ARMCC 与模拟器),不能引 FreeRTOS / CMSIS 的临界区内在函数, + * 所以「把三个字节压成一个字节」是唯一既原子又不依赖平台的写法。 */ +#define WAVE_RG_REQ_SHIFT (5u) /* 载荷里触发源的低位置 */ +#define WAVE_RG_REQ_ELM_MSK (0x1Fu) /* 载荷里元件号的位掩码(低 5 位) */ +#define WAVE_RG_REQ_ELM_MAX (32u) /* 元件号合法上界(不含),picked 位图只有 32 位 */ + +/* ===== 一、静态对象(全部文件作用域 static,禁堆分配;落位与字节数见头注第七节) ===== */ + +/* 数据区:按通道分列,8 列 × 320 点 × 2 字节 = 5120 字节(§7 预算的大头)。 + * 不许改成 float、不许按帧交错(§12:形态错一次 RAM 翻倍 + 跨步取数)。 + * 不放在 CCM:DMA 看不见它,且 .sct 里 RW_CCM 只收 heap_4.o,普通 .bss 天然落 SRAM1 */ +static int16_t s_ring[WAVE_RING_CH][WAVE_RING_PTS]; + +/* 已封存段的段头:latch 之前只有环侧字段有效,latch 之后才是完整段头。 + * 这是 defaultTask 与 uiTask 能看到的唯一「段头视图」——内部指针一律不外露 */ +static wave_seg_meta_t s_sealed; + +/* 观测计数(§9 周期行的 seg= / ovf= / drop= / err= 全从这里取,落盘道不数第二份账) */ +static uint32_t s_seg_total; /* 成功 ACK 的段数 */ +static uint32_t s_ovf_cnt; /* 被忽略的触发数 */ +static uint32_t s_drop_cnt; /* SEALED 窗口丢弃的点数(单位 = 点,一次丢一拍 = 加 8) */ +static uint32_t s_err_cnt; /* 落盘失败 + 滞留超时自弃 */ +static uint32_t s_last_t0_ms; /* 最近一段段首毫秒 */ +static uint32_t s_last_seq; /* 最近一段段首帧序号 */ + +/* 状态机与索引 */ +static uint16_t s_head; /* 下一个写入点(滚动位置) */ +static uint16_t s_base; /* 已封存 / 正在录的段首在环内的下标(段内点 0) */ +static uint16_t s_post_left; /* 后触发还差多少点封段 */ +static uint16_t s_cool_left; /* 冷却剩余拍数 */ +static uint16_t s_seal_hold; /* SEALED 已滞留拍数 */ +static uint8_t s_state; /* wave_ring_state_t */ +/* 触发请求挂号:0 = 无请求;非 0 = (触发源 << WAVE_RG_REQ_SHIFT) | 元件号。 + * 刻意只用这一个字节承载「是谁要录波」:旧实现分三个字节写(先写挂号位、后写载荷), + * adcTask 抢在两条指令之间就会把上一段的旧触发源 / 旧元件号抄进本段 —— 而这两个字段是 + * 这段记录唯一的溯源信息(现场靠它与 /log/start.log 对表),错一次事后无从发现。 + * 单字节存写在 Cortex-M4 上原子,写序不再有意义,所以不需要任何顺序假设或屏障。 */ +static uint8_t s_req; +static uint8_t s_trig_src; /* 本段实际用的触发源(钉基址时从请求抄过来) */ +static uint8_t s_trig_elm; /* 本段实际用的元件序号 */ +static uint8_t s_meta_ready; /* 1 = 段头已 latch */ +static uint8_t s_seg_idx; /* 本次上电的轮转下标(跨上电接号由落盘道自持,见头注) */ + +/* 语义号 → 列号(本波恒等,但把这条恒等式收在一处:二期带 TEMP 时只改这张表, + * 写死下标的调用点会静默错位,走本函数的不会) */ +static const int8_t s_col_of_sem[(uint8_t)WAVE_ST_CH_MAX] = { + 0, 1, 2, 3, 4, 5, 6, 7, -1 +}; + +/* ===== 二、内部小工具 ===== */ + +/* + * 函数功能:环内下标 +n 后取模 + * 入口参数:a_idx 当前下标;a_step 步长(小于环长) + * 返 回 值:取模后的下标 uint16_t + * 限定条件:仅本文件;a_idx 与 a_step 均在环长量级,无溢出风险 + * 函数说明:环长 320 是 2 的幂吗?不是(320 = 64×5),所以老老实实取模, + * 不用位与——写错一次就是「看起来能跑、偶尔串段」的那种 bug。 + */ +static uint16_t wave_rg_wrap(uint16_t a_idx, uint16_t a_step) { + uint16_t v = (uint16_t)(a_idx + a_step); + + if (v >= (uint16_t)WAVE_RING_PTS) { + v = (uint16_t)(v - (uint16_t)WAVE_RING_PTS); + } + return v; +} + +/* + * 函数功能:把已封存段的「环侧字段」初始化(封存瞬间调,标定字段一律清零待 latch) + * 入口参数:a_now_ms 段首毫秒;a_frame_seq 段首帧序号 + * 返 回 值:无 void + * 限定条件:仅本文件;只在钉基址那一拍调一次 + * 函数说明:先把 ch[] 全部清成「sem = 下标、其余 0」,是为了让**没有 latch** 的段 + * 不可能带着上一段的系数被写进文件:sealed() 在未 latch 时本就是 0, + * 但清零让「万一有人绕过握手直接读 s_sealed」也读不出旧真值。 + */ +static void wave_rg_seal_reset(uint32_t a_now_ms, uint32_t a_frame_seq) { + uint8_t i; + + s_sealed.fs_hz = (uint16_t)WAVE_ST_FS_DEF; + s_sealed.pts_per_cycle = (uint8_t)WAVE_ST_PPC_DEF; + s_sealed.ch_count = (uint8_t)WAVE_RING_CH; + s_sealed.pts_per_ch = (uint16_t)WAVE_RING_PTS; + s_sealed.pre_pts = (uint16_t)WAVE_RING_PRE; + s_sealed.trig_src = s_trig_src; + s_sealed.trig_elm = s_trig_elm; + s_sealed.t0_ms = a_now_ms; + s_sealed.wall = 0u; + s_sealed.eng = (uint8_t)WAVE_ENG_PIN; + s_sealed.flags_seq = 0u; + s_sealed.clip_mask = 0u; + s_sealed.frame_seq = a_frame_seq; + for (i = 0u; i < (uint8_t)WAVE_ST_CH_MAX; i++) { + s_sealed.ch[i].sem = i; + s_sealed.ch[i].conv = 0u; + s_sealed.ch[i].bits = 0u; + s_sealed.ch[i].avail = 0u; + s_sealed.ch[i].k_q16 = 0; + s_sealed.ch[i].b_codes = 0; + } +} + +/* ===== 三、初始化与压环 ===== */ + +/* + * 函数功能:复位环与全部状态 + * 入口参数:无 + * 返 回 值:无 void + * 限定条件:只能在 adcTask 调,且在开始压环之前(见头文件同函数注释) + * 函数说明:数据区显式清零而不是依赖 .bss 清零:本函数也可能在运行中被重新调 + * (显式重配 / 单测复用),留旧数据就等于留下「上一段的尾巴混进下一段」的口子。 + */ +void wave_ring_init(void) { + uint8_t c; + uint16_t i; + + for (c = 0u; c < (uint8_t)WAVE_RING_CH; c++) { + for (i = 0u; i < (uint16_t)WAVE_RING_PTS; i++) { + s_ring[c][i] = 0; + } + } + s_head = 0u; + s_base = 0u; + s_post_left = 0u; + s_cool_left = 0u; + s_seal_hold = 0u; + s_state = (uint8_t)WAVE_RING_ST_IDLE; + s_req = 0u; + s_trig_src = (uint8_t)WAVE_TRIG_NONE; + s_trig_elm = 0u; + s_meta_ready = 0u; + s_seg_idx = 0u; + s_seg_total = 0u; + s_ovf_cnt = 0u; + s_drop_cnt = 0u; + s_err_cnt = 0u; + s_last_t0_ms = 0u; + s_last_seq = 0u; + wave_rg_seal_reset(0u, 0u); +} + +/* + * 函数功能:压入一拍的 8 路原始码值 + * 入口参数:p_codes 语义序数组(前 8 项);a_n 必须等于 WAVE_RING_CH + * 返 回 值:WAVE_ST_OK 已写入;WAVE_ST_DONE 本拍被丢弃(SEALED 窗口);WAVE_ST_E_PARAM 参数错 int32_t + * 限定条件:只能在 adcTask 调,每拍一次 + * 函数说明:写的是「列优先」的 8 个分散地址(每列同一行)——这就是 §6.2 分列形态的代价, + * 换来的是落盘与回放都能按列连续搬,不用跨步长取数。 + */ +int32_t wave_ring_push(const int16_t *p_codes, uint8_t a_n) { + uint8_t c; + + if ((p_codes == NULL) || (a_n != (uint8_t)WAVE_RING_CH)) { + return WAVE_ST_E_PARAM; + } + if (s_state == (uint8_t)WAVE_RING_ST_SEALED) { + /* 冻结窗口丢样本(§4.2):计数单位是点,一次丢一整拍 = WAVE_RING_CH 点 */ + s_drop_cnt += (uint32_t)a_n; + return WAVE_ST_DONE; + } + for (c = 0u; c < (uint8_t)WAVE_RING_CH; c++) { + s_ring[c][s_head] = p_codes[c]; + } + s_head = wave_rg_wrap(s_head, 1u); + return WAVE_ST_OK; +} + +/* ===== 四、触发、推进、交棒、ACK ===== */ + +/* + * 函数功能:投递一次录波请求 + * 入口参数:a_src 触发源(1 手动 / 2 保护 / 3 bench);a_elm 元件序号(0 到 31) + * 返 回 值:WAVE_ST_OK 受理;WAVE_ST_DONE 被忽略(ovf 已加一);WAVE_ST_E_PARAM 触发源 + * 非法或元件号越界 int32_t + * 限定条件:任意上下文(**只写一个字节**,理由见 s_req 声明处) + * 函数说明:这里判「非 IDLE 就拒」而不是「非 IDLE 就排队」——本波不做多段排队(已定裁定), + * 而且排队会把「最近一次故障」降级成「上上次故障」。 + * s_req 已挂号时也拒:同一拍内多个元件同时 picked 只该录一段。 + * 两条入参校验排在状态判断之前:非法调用是调用方的错,不该记进 ovf 账 —— + * ovf 的语义是「真来了第二次触发」。 + * 元件号上界写死在这里:今天没有调用方会越界(picked 是 uint32 位图),这条是给 + * 二期加元件的人留的牙 —— 载荷只有 5 位,越界必须当场拒,不许静默截断。 + */ +int32_t wave_ring_request(uint8_t a_src, uint8_t a_elm) { + if ((a_src < (uint8_t)WAVE_TRIG_MANUAL) || (a_src > (uint8_t)WAVE_TRIG_BENCH)) { + return WAVE_ST_E_PARAM; + } + if (a_elm >= (uint8_t)WAVE_RG_REQ_ELM_MAX) { + return WAVE_ST_E_PARAM; + } + if ((s_state != (uint8_t)WAVE_RING_ST_IDLE) || (s_req != 0u)) { + s_ovf_cnt++; + return WAVE_ST_DONE; + } + /* 一次存写:触发源与元件号同字节落地。别的上下文要么看到「没请求」、要么看到 + * 「完整的这次请求」,不存在读到半份载荷的中间态 */ + s_req = (uint8_t)(((uint32_t)a_src << (uint32_t)WAVE_RG_REQ_SHIFT) | (uint32_t)a_elm); + return WAVE_ST_OK; +} + +/* + * 函数功能:推进一拍状态机 + * 入口参数:a_now_ms 当前相对上电毫秒;a_frame_seq 当前帧序号 + * 返 回 值:wave_ring_evt_t(本拍刚封段返回 WAVE_RING_EVT_SEALED) int32_t + * 限定条件:只能在 adcTask 调,每拍在 wave_ring_push 之后一次 + * 函数说明:五个分支各管一件事,互不重叠: + * IDLE —— 消费挂号、钉段首基址、转 FREEZE + * FREEZE —— 后触发计点,计满转 SEALED + * SEALED —— 只计滞留,超时自弃(绝不让环永久焊死) + * COOL —— 递减冷却,到 0 回 IDLE + * 段首基址 = 触发那一拍写入点的「往前 160 点」,于是段内点 0 是最老的预触发点、 + * 点 160 正好是触发后第一点;后触发的 160 点落在环的另一半(本来就即将被覆盖的 + * 最老那 160 槽),所以「冻结」在物理上是**停止滚动覆盖**,不是停止写入。 + */ +int32_t wave_ring_tick(uint32_t a_now_ms, uint32_t a_frame_seq) { + int32_t evt = (int32_t)WAVE_RING_EVT_NONE; + + switch (s_state) { + case (uint8_t)WAVE_RING_ST_IDLE: + s_cool_left = 0u; + s_seal_hold = 0u; + { + /* 只读一次快照再解码:分两次读就会把两次请求的字段拼成一段的身份 */ + uint8_t req = s_req; + + if (req != 0u) { + /* 钉基址:本拍的样本已写进 (head-1),段首 = head - 预触发点数 */ + s_trig_src = (uint8_t)((uint32_t)req >> (uint32_t)WAVE_RG_REQ_SHIFT); + s_trig_elm = (uint8_t)((uint32_t)req & (uint32_t)WAVE_RG_REQ_ELM_MSK); + s_req = 0u; + s_base = wave_rg_wrap(s_head, (uint16_t)((uint32_t)WAVE_RING_PTS - (uint32_t)WAVE_RING_PRE)); + s_post_left = (uint16_t)WAVE_RING_POST; + s_meta_ready = 0u; + s_last_t0_ms = a_now_ms; + s_last_seq = a_frame_seq; + wave_rg_seal_reset(a_now_ms, a_frame_seq); + s_state = (uint8_t)WAVE_RING_ST_FREEZE; + } + } + break; + + case (uint8_t)WAVE_RING_ST_FREEZE: + if (s_post_left > 0u) { + s_post_left--; + } + if (s_post_left == 0u) { + /* 封段:此刻起 s_head 正好回到 s_base,再写就覆盖段内点 0 —— 所以 push 开始丢 */ + s_state = (uint8_t)WAVE_RING_ST_SEALED; + s_seal_hold = 0u; + evt = (int32_t)WAVE_RING_EVT_SEALED; + } + break; + + case (uint8_t)WAVE_RING_ST_SEALED: + /* 兜底:defaultTask 挂住 / 卷没挂载 / 落盘道被更高优先级的事挡住,都不能让环 + * 永久冻结——下一次真故障无波可录是比丢一段更贵的代价(lead 裁定) */ + s_seal_hold++; + if (s_seal_hold >= (uint16_t)WAVE_SEAL_TIMEOUT_MS) { + s_err_cnt++; + s_meta_ready = 0u; + s_seal_hold = 0u; + s_cool_left = (uint16_t)WAVE_RING_COOL_MS; + s_state = (uint8_t)WAVE_RING_ST_COOL; + } + break; + + case (uint8_t)WAVE_RING_ST_COOL: + /* 冷却只在 ACK(或自弃)之后计时:它是二次抑制窗,不是冻结窗 */ + if (s_cool_left > 0u) { + s_cool_left--; + } + if (s_cool_left == 0u) { + s_state = (uint8_t)WAVE_RING_ST_IDLE; + } + break; + + default: + /* 状态字节被写坏(跑飞 / 越界写)时回到 IDLE 而不是卡死:录波宁可多录也不能停 */ + s_state = (uint8_t)WAVE_RING_ST_IDLE; + break; + } + return evt; +} + +/* + * 函数功能:封存交棒——把封存瞬间事实锁进段头 + * 入口参数:p_cal 封存瞬间事实(不可为 NULL) + * 返 回 值:WAVE_ST_OK 已锁存;WAVE_ST_E_STATE 不在待交棒态;WAVE_ST_E_PARAM 参数错 int32_t + * 限定条件:只能在 adcTask 调,只在 EVT_SEALED 那一拍调一次 + * 函数说明:只 copy 封存瞬间的事实,不动环侧字段(点数 / 采样率 / 预触发这些 + * 在封段那刻已按 §6.2 定死)。重复调用直接拒,不覆盖已有锁存: + * 半路换系数会让落盘道写进去的头与它已经在搬的数据不同源。 + */ +int32_t wave_ring_latch_meta(const wave_seal_cal_t *p_cal) { + uint8_t i; + + if (p_cal == NULL) { + return WAVE_ST_E_PARAM; + } + if (p_cal->eng > (uint8_t)WAVE_ENG_PRI) { + return WAVE_ST_E_PARAM; + } + if ((s_state != (uint8_t)WAVE_RING_ST_SEALED) || (s_meta_ready != 0u)) { + return WAVE_ST_E_STATE; + } + s_sealed.eng = p_cal->eng; + s_sealed.flags_seq = p_cal->flags_seq; + s_sealed.clip_mask = p_cal->clip_mask; + s_sealed.wall = p_cal->wall; + for (i = 0u; i < s_sealed.ch_count; i++) { + s_sealed.ch[i].conv = p_cal->ch[i].conv; + s_sealed.ch[i].bits = p_cal->ch[i].bits; + s_sealed.ch[i].avail = p_cal->ch[i].avail; + s_sealed.ch[i].k_q16 = p_cal->ch[i].k_q16; + s_sealed.ch[i].b_codes = p_cal->ch[i].b_codes; + } + s_meta_ready = 1u; + return WAVE_ST_OK; +} + +/* + * 函数功能:落盘 ACK + * 入口参数:a_result 0 = 成功;非 0 = 失败(短写 / 打开失败 / CRC 自弃) + * 返 回 值:WAVE_ST_OK 已解冻并起冷却;WAVE_ST_E_STATE 当前不在 SEALED int32_t + * 限定条件:只能由 defaultTask 调,一段一次 + * 函数说明:无论成败都解冻 + 起冷却:错误只计数不冻结(环冻着的代价是下一次故障无波可录)。 + * 成功才推进轮转号与段计数(失败段留同号,下次重写覆盖,不留半截文件)。 + */ +int32_t wave_ring_write_done(int32_t a_result) { + if (s_state != (uint8_t)WAVE_RING_ST_SEALED) { + return WAVE_ST_E_STATE; + } + if (s_meta_ready == 0u) { + /* 没 latch 就没有「段」可 ACK:这时候段头从未就绪,落盘道根本拿不到 + * wave_ring_sealed_meta(),绝不可能真写出文件。若在这里放过 ACK, + * 就会白推进轮转号(号 +1 而盘上没那个文件 = 凭空吃掉一个号)并把 + * seg_total 报成一个没发生过的数(串口周期行的 seg=NN 从此对不上账)。 + * 拒绝而不是记账,把「没交棒就 ACK」变成调用方看得见的错误。 + * 本段的出口只有一个:滞留到 WAVE_SEAL_TIMEOUT_MS 自弃(那一路照实计 err)。 */ + return WAVE_ST_E_STATE; + } + if (a_result == 0) { + s_seg_idx = wave_store_next_idx(s_seg_idx); + s_seg_total++; + } else { + s_err_cnt++; + } + s_meta_ready = 0u; + s_seal_hold = 0u; + s_cool_left = (uint16_t)WAVE_RING_COOL_MS; + s_state = (uint8_t)WAVE_RING_ST_COOL; + return WAVE_ST_OK; +} + +/* ===== 五、状态查询与已封存区读口 ===== */ + +/* + * 函数功能:查当前状态 + * 入口参数:无 + * 返 回 值:wave_ring_state_t 取值 uint8_t + * 限定条件:任意上下文(只读单字节) + */ +uint8_t wave_ring_state(void) { + return s_state; +} + +/* + * 函数功能:查「有已封存段可读」——跨任务唯一握手量 + * 入口参数:无 + * 返 回 值:1 = SEALED 且段头已 latch;0 = 不可用 uint8_t + * 限定条件:任意上下文(只读单字节) + */ +uint8_t wave_ring_sealed(void) { + if ((s_state == (uint8_t)WAVE_RING_ST_SEALED) && (s_meta_ready != 0u)) { + return 1u; + } + return 0u; +} + +/* + * 函数功能:取已封存段的段头快照 + * 入口参数:p_out 输出(整份拷出) + * 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针;WAVE_ST_E_STATE 无就绪段 int32_t + * 限定条件:任意上下文(defaultTask / uiTask 只读) + */ +int32_t wave_ring_sealed_meta(wave_seg_meta_t *p_out) { + uint16_t i; + const uint8_t *p_src; + uint8_t *p_dst; + + if (p_out == NULL) { + return WAVE_ST_E_PARAM; + } + if (wave_ring_sealed() == 0u) { + return WAVE_ST_E_STATE; + } + /* 逐字节拷出而不是 *p_out = s_sealed:结构体赋值在 ARMCC 上会调 __aeabi_memcpy, + * 这里没必要为一个赋值多拉一层依赖,量也就 136 字节 */ + p_src = (const uint8_t *)&s_sealed; + p_dst = (uint8_t *)p_out; + for (i = 0u; i < (uint16_t)sizeof(wave_seg_meta_t); i++) { + p_dst[i] = p_src[i]; + } + return WAVE_ST_OK; +} + +/* + * 函数功能:按通道读已封存区的连续点窗 + * 入口参数:a_ch 列号;a_from 段内起点;a_cnt 期望点数(超出段尾自动夹);p_dst 输出 + * 返 回 值:实际点数;WAVE_ST_E_PARAM / WAVE_ST_E_RANGE / WAVE_ST_E_STATE int32_t + * 限定条件:任意上下文,只读已封存区(本层没有任何读活缓冲的入口) + * 函数说明:按 s_base 取模展开,读者看到的是 320 点的线性数组。 + */ +int32_t wave_ring_read_ch(uint8_t a_ch, uint16_t a_from, uint16_t a_cnt, int16_t *p_dst) { + uint16_t left; + uint16_t n; + uint16_t idx; + uint16_t i; + + if ((p_dst == NULL) || (a_cnt == 0u)) { + return WAVE_ST_E_PARAM; + } + if ((a_ch >= (uint8_t)WAVE_RING_CH) || (a_from >= (uint16_t)WAVE_RING_PTS)) { + return WAVE_ST_E_RANGE; + } + if (wave_ring_sealed() == 0u) { + return WAVE_ST_E_STATE; + } + left = (uint16_t)((uint16_t)WAVE_RING_PTS - a_from); + n = (a_cnt < left) ? a_cnt : left; + idx = wave_rg_wrap(s_base, a_from); + for (i = 0u; i < n; i++) { + p_dst[i] = s_ring[a_ch][idx]; + idx = wave_rg_wrap(idx, 1u); + } + return (int32_t)n; +} + +/* + * 函数功能:读口的 wave_reader_fn 形态适配(落盘道直接把它交给打包游标) + * 入口参数:p_user 未用;其余同 wave_ring_read_ch(点数与起点是 uint32 形态) + * 返 回 值:实际点数;负 = WAVE_ST_E_* int32_t + * 限定条件:任意上下文,只读已封存区 + * 函数说明:32 位入参先夹到 16 位形态再转调,越界一律报错不夹成 0 —— + * 夹成 0 会让落盘道以为「这一列读完了」而写出一个短段。 + */ +int32_t wave_ring_read_cb(void *p_user, uint8_t a_ch, uint32_t a_from, + uint32_t a_cnt, int16_t *p_dst) { + (void)p_user; + + if (a_cnt > (uint32_t)WAVE_RING_PTS) { + return WAVE_ST_E_PARAM; + } + if (a_from > (uint32_t)WAVE_RING_PTS) { + return WAVE_ST_E_RANGE; + } + return wave_ring_read_ch(a_ch, (uint16_t)a_from, (uint16_t)a_cnt, p_dst); +} + +/* + * 函数功能:取观测计数快照 + * 入口参数:p_out 输出(不可为 NULL) + * 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 int32_t + * 限定条件:任意上下文(只读) + */ +int32_t wave_ring_get_stat(wave_ring_stat_t *p_out) { + if (p_out == NULL) { + return WAVE_ST_E_PARAM; + } + p_out->state = s_state; + p_out->trig_src = s_trig_src; + p_out->trig_elm = s_trig_elm; + p_out->meta_ready = s_meta_ready; + p_out->seg_idx = s_seg_idx; + /* 对外字段是 8 位:本波后触发最多 160 点,不会溢出;真溢出说明环长被改过, + * 饱和在 255 让它在日志里显眼,而不是绕回 0 假装快封段了 */ + p_out->post_left = (s_post_left > 255u) ? 255u : (uint8_t)s_post_left; + p_out->rsv1 = 0u; + p_out->rsv2 = 0u; + p_out->cool_left = s_cool_left; + p_out->seal_hold = s_seal_hold; + p_out->seg_total = s_seg_total; + p_out->ovf_cnt = s_ovf_cnt; + p_out->drop_cnt = s_drop_cnt; + p_out->err_cnt = s_err_cnt; + p_out->last_t0_ms = s_last_t0_ms; + p_out->last_seq = s_last_seq; + return WAVE_ST_OK; +} + +/* + * 函数功能:语义通道号 → 本环列号 + * 入口参数:a_sem 语义号 + * 返 回 值:列号;WAVE_ST_E_RANGE = 该语义不录 int32_t + * 限定条件:任意上下文(纯计算) + */ +int32_t wave_ring_col(uint8_t a_sem) { + if (a_sem >= (uint8_t)WAVE_ST_CH_MAX) { + return WAVE_ST_E_RANGE; + } + if (s_col_of_sem[a_sem] < 0) { + return WAVE_ST_E_RANGE; + } + return (int32_t)s_col_of_sem[a_sem]; +} diff --git a/App/algo/wave_ring.h b/App/algo/wave_ring.h new file mode 100644 index 00000000..d70ff782 --- /dev/null +++ b/App/algo/wave_ring.h @@ -0,0 +1,406 @@ +/* + * 模块名称:录波环形缓冲层(故障录波 · 8 路 × 320 点原始码值环 + 触发冻结状态机) + * 模块功能:按拍把 8 路电气量的原始码值压进环形缓冲,在「预触发 + 后触发」窗口计满后 + * 把这一段冻结成只读的已封存区,等落盘道取走并明确 ACK 之后才解冻。口径属主 = + * docs/子系统设计/故障录波设计说明.md §4.2(段长与冻结语义)、§5.1(任务所有权)、 + * §5.2(三条陷阱)、§6.2 与 §6.3(段格式;本层的已封存区就是数据区的字节形态)、 + * §7(预算与落位)、§12(风险表)。本文件是它的机器可执行形态;冲突以设计文档 + * 为准并回报 lead,不在代码里单方面「修正」。 + * 纯逻辑层纪律:不 include HAL / CMSIS / RTOS 头、不建任务、不打日志、不碰文件系统, + * 也不链 App/algo/adc_cal —— 折算档位、有效性位图、削顶位图、epoch 秒与每路 + * k_q16 / b_codes 由 adcTask 在「刚封存那一拍」作为参数交棒(见 wave_ring_latch_meta + * 与下面的「封存握手顺序」),本层只负责把它们随段头一起锁存。因此本层可被 + * 主机侧单测 test/host/wave_ring/ 与 test/host/wave_trig/ 原样纳入断言、零桩。 + * 与 sample_ring 的关系(为什么另起一个环):App/algo/sample_ring.c 是相量与保护的 + * 上游、float 形态、深度 256,且它的 sample_ring_window() 没有临界区 + * (§5.2 第一条陷阱)。录波要的是 int16 原始码值、320 点、跨任务只读,复用那个环 + * 要么改它的形态与深度(动相量链),要么在跨任务读上继续裸奔 —— 两条都比另起 + * 一个环贵,故本波解耦(§2.1 第 3 条结构约束)。 + * + * 形态钉死(§7 预算 + §12 风险表,动这里之前先读): + * - 元素类型 int16,不许 float:float 让环从 5120 字节变 10240 字节,直接顶穿 §7 的 + * 约 5.9 KB 静态增量目标; + * - 按通道分列(一路连续 320 点),不按帧交错:回放页只画一路、PC 重算全周傅氏也逐路 + * 独立,交错布局两种用法都要跨步长取数(§6.2 末段「为什么按通道分列」); + * - 通道数固定 8,TEMP 不录:温度是片内 ADC 的 12 位单极性原始值、非线性且未参与线性 + * 标定(docs/任务规划/数据可信.md §7 已定案),进录波只多占 640 字节,并让回放页多一 + * 格会被误读成电气量的通道;带 TEMP 属二期,且必须连 flags_seq 与 clip_mask 一起升 + * 版本(§6.2 那两行的括注); + * - 落位 SRAM1:本层全部是普通文件级静态,链接脚本 MDK-ARM/STM32F407-Demo/ + * STM32F407-Demo.sct 把 RW_IRAM1 的 .ANY (+RW +ZI) 收在 0x20000000,CCM 只收 + * heap_4.o 与 .noinit → 不加任何段属性就落在 SRAM1。CCM 只剩 7904 字节没有余量, + * 而本环不是 DMA 可见对象,也没有非进 CCM 不可的理由。 + * + * 封存握手顺序(唯一合法次序,写死在这里,接线道与后续改动都必须照此): + * 1) adcTask 每拍 wave_ring_push() 然后 wave_ring_tick(); + * 2) tick 计满后触发点返回 WAVE_RING_EVT_SEALED —— 此刻状态进 WAVE_RING_ST_SEALED 但 + * **尚未就绪**,wave_ring_sealed() 仍返 0; + * 3) adcTask 在**同一拍**现取封存瞬间事实(adc_cal_k_q16 / adc_cal_flags / sys_clock / + * 削顶位图)并调 wave_ring_latch_meta() 一次 —— 只在第 2 步那一拍取一次, + * 既不每拍读 adc_cal(1 kHz 下 8 路白读),也不低频刷新(低频刷新等于快照可能 + * 来自标定之前的旧值,正是 §12「标定落地后老文件被解释错」的入口); + * 4) 此后 wave_ring_sealed() 才返 1,defaultTask 才准开文件落盘;meta 未就绪时 + * defaultTask 必须把本段当「不可用」处理并计 err,**不许拿半份快照落盘**; + * 5) defaultTask 搬完(成功或失败都算搬完)调 wave_ring_write_done(result) 明确 ACK; + * ACK 之前冻结一直有效、新触发一律只加 ovf;ACK 之后才起冷却抑制窗。 + * 为什么必须用状态而不是用时间来挡(lead 裁定 2026-10-29):若解冻靠计时,会出现 + * 「冷却走完、adcTask 重新推进写指针,而 defaultTask 还在读那份已封存区」→ + * 半新半旧的段落进文件且没有任何错误码 —— §5.2 第一条陷阱的翻版。 + * 兜底:WAVE_RING_ST_SEALED 滞留超过 WAVE_SEAL_TIMEOUT_MS(写者没来取:卷未挂载、 + * 落盘链卡死都算)→ 自弃本段、err 计数加一、走冷却解冻。**绝不许让录波把环永久焊死**, + * 那会让下一次真故障无波可录。 + * + * 跨任务安全(§5.1 与 §5.2 第一条;本层的唯一并发契约): + * 写者只有 adcTask,读者是 defaultTask 与 uiTask,因此只暴露「已封存」标志与已封存区 + * 读口,不给任何跨任务读活缓冲的路径: + * 只能在 adcTask 调:wave_ring_init、wave_ring_push、wave_ring_tick、 + * wave_ring_latch_meta; + * 只能由文件系统唯一写者 defaultTask 调:wave_ring_write_done; + * 任意上下文可调:wave_ring_request(只投一个字节,protectTask / uiTask / + * bench 都走它)、wave_ring_state、wave_ring_sealed、 + * wave_ring_sealed_meta、wave_ring_read_ch、wave_ring_read_cb、 + * wave_ring_get_stat、wave_ring_col。 + * 为什么这样就够、不需要临界区:单字节状态量与请求量在 Cortex-M4 上单字读写天然原子; + * sealed 为 1 期间已封存区对 adcTask 是只读不写(写指针被冻在窗口之外),读者看到的数据 + * 不会在脚下变;请求走「单字投递 + 下一拍由 adcTask 自己消费」,与 App/algo/prot_elm.c + * 的 prot_elm_request_reset() 同一模式。 + * 「单字」是硬要求不是措辞(lead 评审 2026-10-29):投递方是 protectTask(Normal)与 + * uiTask(BelowNormal),消费者是 adcTask(AboveNormal,会**抢占**它们)。旧实现先写挂号位 + * 再写触发源与元件号(三个字节、两条指令的窗口),adcTask 抢在中间就会把上一段的旧触发源 + * 与旧元件号抄进本段 —— 波形没错,错的恰好是这段记录唯一的溯源字段(现场靠它与 + * /log/start.log 对表),一次故障只录一段、错过事后无从发现。现在载荷本身编进那一个字节 + * (0 = 无请求,非 0 = 触发源占高位 | 元件号占低 5 位),一次存写要么看不见、要么看见完整 + * 一份,不需要顺序假设,也不需要任何平台相关的屏障或临界区(本层双端编译,禁止引 + * FreeRTOS / CMSIS 内在函数)。 + * + * 观测计数的已知容差(**不修、只入档**,lead 裁定 2026-10-29):ovf 与 err 两个计数是跨上下文 + * 的读-改-自增(ovf 由任意上下文的拒绝路径加、err 由 adcTask 的超时自弃与 defaultTask 的落盘 + * 失败分别加),本层不设临界区,所以在并发拒绝 / 并发失败时理论上可能少计一次。定性成 + * **观测计数的已知容差**:它们只用于趋势与判读,不作为任何判据的唯一依据 —— 要坐实「冷却期 + * 内确实来了第二次触发」,同时看 armed 位与 /log/start.log 的时刻,别只盯 ovf 数字。 + * + * 触发与冻结语义(§4.2,严格按此实现,别「顺手优化」): + * IDLE →(触发)FREEZE:段首基址当拍钉死(预触发区从此不再被覆盖,后触发区继续写入), + * 后触发计满 160 点 → SEALED:整段只读,新样本一律丢弃并计 drop,等 meta latch 与落盘 + * ACK → (ACK)COOL:恢复正常滚动写入,但本窗口内新触发只加 ovf 不重录 → IDLE。 + * 冻结期间丢的是录波样本:sample_ring、相量、保护照常跑,本层不碰它们、也无权影响它们。 + * + * 适用平台:STM32F407ZGTx(Cortex-M4,小端);主机侧单测 test/host/wave_ring/、 + * test/host/wave_trig/ + * 作者:DTU 项目组 + * 创建日期:2026-10-29(故障录波 task-1 · 数据面纯逻辑层) + * 修改记录: + * v1.0 2026-10-29 DTU 项目组 创建:8 路 × 320 点 int16 分列环、请求/冻结/封段/ACK/ + * 冷却状态机(含 SEALED 滞留超时自弃)、段头锁存与 + * 已封存区按通道读口 + */ + +#ifndef WAVE_RING_H +#define WAVE_RING_H + +/* 本头对外只暴露 stdint 的类型;文件里出现的 NULL 全在注释内(「不可为 NULL」这类 + * 约束说明),所以这里刻意不 include stddef.h —— 需要 NULL 的是包含本头的 .c, + * 它们自己 include(台基线实测:ARMCC 的 stdint.h 不带 NULL)。 */ +#include +#include "wave_store.h" /* 同层配对头:wave_seg_meta_t / wave_ch_desc_t / wave_reader_fn / + WAVE_ST_* 返回码(本层复用同一套 int 返回码通道,不另发明) */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* ===== 一、环几何(数值属主 = docs/子系统设计/故障录波设计说明.md §4.2 与 §7) ===== */ + +/* 每通道点数 = 预触发 8 周波 + 后触发 8 周波 = 160 + 160 = 320 点(1 kHz 下 320 ms) */ +#define WAVE_RING_PTS (320u) +/* 预触发点数:触发点之前保留 8 周波;回放画触发光标的位点就是这个数(也是段内触发点下标) */ +#define WAVE_RING_PRE (160u) +/* 后触发点数:触发点之后采满 8 周波才封段 */ +#define WAVE_RING_POST (160u) +/* 通道数 = 8(TEMP 不录,理由见本文件头注);必须与 WAVE_ST_CH_NUM 相等,单测里钉住 */ +#define WAVE_RING_CH (8u) +/* 冷却抑制窗:ACK 之后这么多拍内新触发只加 ovf 不重录(§4.2「只记第一次」)。 + * 500 拍 = 500 ms @1 kHz。注意:「覆盖一轮落盘窗口」这件事由 ACK 保证,本数值只是 + * ACK 之后的二次抑制(防同一次故障的抖动重复触发),待台架核。 */ +#define WAVE_RING_COOL_MS (500u) +/* 已封存滞留上限:SEALED 状态超过这么多拍仍没人来取(卷未挂载、落盘链卡死都算) + * → 自弃本段 + err 计数加一 + 走冷却解冻。2000 拍 = 2 s @1 kHz,待台架核。 + * 判据是 tick 的推进次数(也就是 adcTask 的存活),不是墙上时间:adcTask 自己挂了 + * 就轮到看门狗处理,本层不越权。 */ +#define WAVE_SEAL_TIMEOUT_MS (2000u) + +/* 环数据区字节数:int16 × 320 × 8 = 5120(§7 预算里最大的一项;动它等于动预算) */ +#define WAVE_RING_BYTES (WAVE_RING_PTS * WAVE_RING_CH * (uint32_t)sizeof(int16_t)) + +/* ===== 二、类型 ===== */ + +/* wave_ring_tick 的事件:返回值语义是「本拍发生了什么」,不是错误 */ +typedef enum { + WAVE_RING_EVT_NONE = 0, /* 正常推进,无事件 */ + WAVE_RING_EVT_SEALED = 1 /* 本拍刚计满后触发:**adcTask 必须在本拍调 + * wave_ring_latch_meta() 交棒封存瞬间事实**; + * 不调则本段永不就绪,最终由 WAVE_SEAL_TIMEOUT_MS + * 自弃(err 加一),可从串口周期行看到 */ +} wave_ring_evt_t; + +/* 环状态。迁移只发生在 wave_ring_tick 与 wave_ring_write_done 里(单写者) */ +typedef enum { + WAVE_RING_ST_IDLE = 0, /* 滚动:新样本覆盖最旧点,接受触发 */ + WAVE_RING_ST_FREEZE = 1, /* 已触发:段首基址已钉死(预触发区不再被覆盖), + 后触发区继续写入,直到计满 WAVE_RING_POST 点 */ + WAVE_RING_ST_SEALED = 2, /* 已封存:整段只读,新样本一律丢弃并计 drop; + 等 meta latch、等 defaultTask 落盘并 ACK */ + WAVE_RING_ST_COOL = 3 /* 冷却:已 ACK 解冻、恢复正常滚动,但新触发只加 ovf */ +} wave_ring_state_t; + +/* 封存瞬间必须由 adcTask 交棒的事实(本层不链 App/algo/adc_cal,全靠这份入参)。 + * 段长 / 采样率 / 每周波点数 / 预触发点数 / 触发源与序号 / t0_ms / frame_seq 由本层自己填 + * (环就是这些数的物理实现,让调用方填只会多一个「忘了填」的失败面)。 */ +typedef struct { + uint8_t eng; /* 折算档位(wave_eng_t:0 引脚域 / 1 二次值 / 2 一次值) */ + uint8_t flags_seq; /* bit s = 语义通道 s 的 ADC_CAL_F_VALID(§6.3) */ + uint16_t clip_mask; /* bit s = 语义通道 s 的 ADC_CAL_F_CLIPPED(V0.1 恒 0xE6) */ + uint32_t wall; /* 对时后的 epoch 秒(未对时 0;未对时就写 0,不许拿上电毫秒冒充) */ + wave_ch_desc_t ch[WAVE_ST_CH_MAX]; /* 通道表前 8 项的 avail / k_q16 / b_codes: + 必须取 adc_cal_k_q16() 的 int32 原值, + 不许经 adc_cal_k_uv() 的 float 回算(§6.3: + float 在 5.28e7 量级掉尾数); + conv 与 bits 也在这里给(本层无从知道两片 + 转换器的归属);sem 由本层按「表内语义号 == 下标」 + 自己钉,传进来的 sem 被忽略 */ +} wave_seal_cal_t; + +/* 观测快照(给 §9 那条周期行 wave: 与主机单测;字段序 ≈ 日志序,别随手换) */ +typedef struct { + uint8_t state; /* wave_ring_state_t */ + uint8_t trig_src; /* 当前/最近一段的触发源(wave_trig_src_t;未触发为 0) */ + uint8_t trig_elm; /* 触发元件序号(prot_elm 实例下标;手动与 bench 为 0) */ + uint8_t meta_ready; /* 1 = 段头已 latch 可落盘;SEALED 期间为 0 就是「本段不可用」 */ + uint8_t seg_idx; /* 下一个要写的段号 0..99(轮转号由本层持有,defaultTask 照它取名) */ + uint8_t post_left; /* 后触发还差多少点封段(FREEZE 期间有意义;>255 时饱和在 255) */ + uint8_t rsv1; /* 对齐补齐 */ + uint8_t rsv2; /* 对齐补齐 */ + uint16_t cool_left; /* 冷却剩余拍数 */ + uint16_t seal_hold; /* 本段已在 SEALED 滞留多少拍(与 WAVE_SEAL_TIMEOUT_MS 对照) */ + uint32_t seg_total; /* 累计成功 ACK 的段数(给日志 seg=NN 与对账) */ + uint32_t ovf_cnt; /* 被丢弃的触发数:ACK 前重复触发 + 冷却期内触发(§9 ovf=) */ + uint32_t drop_cnt; /* 冻结窗口丢弃的录波点数,单位 = 点不是拍(§9 drop=) */ + uint32_t err_cnt; /* 落盘失败(write_done 非 0,含短写与 CRC 自弃) + + SEALED 滞留超时自弃 的次数(§9 err=) */ + uint32_t last_t0_ms; /* 最近一段段首相对上电毫秒 */ + uint32_t last_seq; /* 最近一段段首帧序号(与串口周期行的 frames 对账) */ +} wave_ring_stat_t; + +/* ===== 三、生命周期(写者侧) ===== */ + +/* + * 函数功能:复位环与全部状态(清 5120 字节数据区 + 索引 + 状态机 + 观测计数 + 轮转号) + * 入口参数:无 + * 返 回 值:无 void + * 限定条件:**只能在 adcTask 调**,且只能在开始压环之前调(启动期或显式重配)。 + * 内核启动前调用也安全:本函数不调任何 RTOS API、不建对象,纯数组清零 + * (平台约束「osKernelStart 之前禁止创建类 API」对本层不适用) + * 函数说明:观测计数(seg_total / ovf_cnt / drop_cnt / err_cnt)与轮转号一并归零: + * 重启录波就是重新起账,不留旧账,免得串口周期行的累计数与段文件对不上。 + * 轮转号归零意味着**本层的 seg_idx 只是本次上电的本地计数**(供日志与首轮参考), + * 不是盘上真值:跨上电的权威下标必须由落盘道自持——启动扫目录用 + * wave_store_idx_from_name 重建、每次写成功后用 wave_store_next_idx 推进。 + * 若照本层归零后的 0 直接取名,重启后第一笔覆盖的就是上次上电的**最新**段, + * 与轮转「新证据优先」的初衷正相反(§6.2 轮转语义)。 + */ +void wave_ring_init(void); + +/* + * 函数功能:压入一拍的 8 路原始码值(数据面的唯一写入路径) + * 入口参数:p_codes 8 路码值数组,顺序 = adc_semantic_t 的前 8 项 + * (UA UB UC UN IA IB IC IN,即语义号升序,与段文件通道表列序 + * 同一口径,§6.3) + * a_n 数组长度,必须等于 WAVE_RING_CH + * 返 回 值:WAVE_ST_OK = 已写入;WAVE_ST_DONE = 本拍样本被丢弃(SEALED 窗口,drop 已加一); + * WAVE_ST_E_PARAM = 空指针或长度不对 int32_t + * 限定条件:**只能在 adcTask 调**,每拍一次(1 kHz);不得从其它任务或 ISR 调。 + * 尤其禁止图省事挂在 Src/freertos.c 的 TIM3 回调里(adc_tick_irq_notify() + * 旁边):那是 ISR 上下文,且 ISR 比调度器早约 100 ms 启动 + * (docs/子系统设计/看门狗设计说明.md §10.1),本层没有「消费者就绪」门, + * 挂上去就是拿 ISR 改状态机。 + * 函数说明:IDLE、FREEZE、COOL 三态都写入;SEALED 一律丢弃并计数 —— 这就是 §4.2 + * 「冻结期间丢的是录波样本」,不是丢采样链路:sample_ring、相量、保护照常被 + * adcTask 喂,本层无权也不该影响它们。 + * 码值是转换器原始输出:TPAFE 为 16 位补码双极性、片内 ADC 为 0..4095 原样 + * 正数,**不做中点偏置、不做折算**(§6.3「样本编码」行:加偏置等于伪造双极性)。 + */ +int32_t wave_ring_push(const int16_t *p_codes, uint8_t a_n); + +/* ===== 四、触发请求、每拍推进、封存交棒、落盘 ACK ===== */ + +/* + * 函数功能:投递一次录波请求(单字投递,不碰环、不碰索引) + * 入口参数:a_src 触发源(wave_trig_src_t 的 1 / 2 / 3;0 与其它值按参数错拒绝) + * a_elm 触发元件序号(prot_elm 的 picked 位号,**必须小于 32**; + * 手动与 bench 定时传 0。越界当场拒,不许静默截断 —— 载荷只有 5 位) + * 返 回 值:WAVE_ST_OK = 已受理(下一拍由 adcTask 自己钉段首基址); + * WAVE_ST_DONE = 被忽略(处于 FREEZE 或 SEALED 或 COOL,或请求还没被消费; + * ovf 计数已加一,§9 的 ovf=); + * WAVE_ST_E_PARAM = 触发源非法或元件号越界(参数错**不记 ovf 账**) int32_t + * 限定条件:**任意上下文可调**(protectTask 的 picked 上升沿、uiTask 的手动确认、 + * bench 定周期、adcTask 自己),它只写**一个字节**(载荷编码见文件头 + * 「跨任务安全」段),与 App/algo/prot_elm.c 的 prot_elm_request_reset() + * 同一模式(§5.1) + * 函数说明:本函数不产生「立刻冻结」的效果 —— 段首基址由 adcTask 在下一拍 + * wave_ring_tick 里钉,避免跨上下文改环索引。最坏延迟一拍 = 1 ms, + * 相对 160 点的预触发窗可以忽略。 + * **ACK 前一律只加 ovf**(lead 裁定):SEALED 期间再来触发不重录、不排队, + * 本波不做多段排队。 + * **返回 WAVE_ST_E_PARAM 的两条路径不触碰任何计数**(ovf / err / drop 一个都不加, + * 也不留挂号):那是调用方传错参数,与「第二次真触发」是两回事。台架不得拿 ovf + * 当「谁误用了接口」的证据——误用只在返回值里体现(lead 裁定 2026-10-29)。 + */ +int32_t wave_ring_request(uint8_t a_src, uint8_t a_elm); + +/* + * 函数功能:推进一拍状态机(消费请求钉基址、计后触发点数、到点进 SEALED、 + * 冷却递减、SEALED 滞留超时自弃) + * 入口参数:a_now_ms 当前相对上电毫秒(填段头 t0_ms;用 adcTask 已有的时基即可) + * a_frame_seq 当前帧序号(填段头 frame_seq,与串口周期行的 frames 对账) + * 返 回 值:wave_ring_evt_t(WAVE_RING_EVT_NONE / WAVE_RING_EVT_SEALED) int32_t + * 限定条件:**只能在 adcTask 调**,且每拍在 wave_ring_push 之后调一次 + * (先写样本再算点数,后触发才正好 160 个点) + * 函数说明:FREEZE 期间写入行为与滚动无异,只是段首基址已钉死、预触发区不再被覆盖 + * —— 这就是 §4.2「触发即冻结写指针」的准确含义:冻住的是覆盖窗口, + * 不是停止采集。计满 WAVE_RING_POST 点的本拍返回 WAVE_RING_EVT_SEALED。 + * SEALED 期间本函数只做「滞留计时 + 超时自弃」,不动数据、不动基址。 + */ +int32_t wave_ring_tick(uint32_t a_now_ms, uint32_t a_frame_seq); + +/* + * 函数功能:封存交棒 —— 把「环已知字段 + adcTask 现取的封存瞬间事实」拼成完整段头并锁存 + * 入口参数:p_cal 封存瞬间事实(折算档位 / 有效性位图 / 削顶位图 / epoch 秒 / 通道表的 + * conv、bits、avail、k_q16、b_codes;不可为 NULL) + * 返 回 值:WAVE_ST_OK = 已锁存,wave_ring_sealed() 从此为 1; + * WAVE_ST_E_STATE = 不在「刚封段待交棒」态(含已 latch 过第二次、非 SEALED); + * WAVE_ST_E_PARAM = 空指针或折算档位非法 int32_t + * 限定条件:**只能在 adcTask 调**,且只在 wave_ring_tick 返回 WAVE_RING_EVT_SEALED 的 + * 那一拍调一次(重复调返回 WAVE_ST_E_STATE 且不覆盖已有锁存值 —— 半路覆盖 + * 会让落盘道读到的头与数据不同源) + * 函数说明:为什么系数要调用方传、而不让本层自己读 adc_cal —— 本层要能被主机单测零桩 + * 纳入;更根本的是 §12 的要求:标定一旦落地,老文件必须还能按**当时**的系数 + * 解释,所以 eng / k / b / clip 属于段头、不属于当前固件状态。 + * 取值只能在第 3 步那一拍做一次(不每拍读、不低频刷),理由见本文件头注。 + */ +int32_t wave_ring_latch_meta(const wave_seal_cal_t *p_cal); + +/* + * 函数功能:落盘 ACK —— defaultTask 明确告知本段已搬完(成功或失败),据此解冻并起冷却 + * 入口参数:a_result 0 = 落盘成功(打开、写入字节数相符、目录可用); + * 非 0 = 失败(含 f_write 短写、打开/建目录失败、CRC 自弃)—— + * 失败时段已丢,但环必须解冻,错误只计数不冻结 + * 返 回 值:WAVE_ST_OK = 已 ACK 并转入冷却;WAVE_ST_E_STATE = 当前不在 SEALED + * (重复 ACK 或没有段可 ACK —— 不静默改状态,防落盘道把上一段的 ACK 打到新段), + * 或段头还没 latch(第 3 步没做):这时候落盘道拿不到 sealed_meta,根本没可能 + * 写出文件,放过 ACK 等于白吃一个轮转号并把 seg=NN 报成没发生过的数, + * 故一律拒绝。这种段唯一的出口是滞留到 WAVE_SEAL_TIMEOUT_MS 自弃并计 err int32_t + * 限定条件:**只能由文件系统唯一写者 defaultTask 调**,一段一次,调之前先看 + * wave_ring_sealed()(它就是「SEALED 且已 latch」的合并判据) + * 函数说明:失败不推进轮转号(下一段仍用同号重写,同号文件按 FA_CREATE_ALWAYS 截断, + * 不留半截文件骗过 PC 脚本);成功才推进 seg_idx 与 seg_total。 + * 本函数不删文件、不碰 FatFs:那些都在落盘道(§5.1 所有权)。 + */ +int32_t wave_ring_write_done(int32_t a_result); + +/* ===== 五、状态查询与已封存区读口(跨任务安全的唯一入口) ===== */ + +/* + * 函数功能:查当前状态 + * 入口参数:无 + * 返 回 值:wave_ring_state_t 的取值 uint8_t + * 限定条件:任意上下文(只读单字节) + */ +uint8_t wave_ring_state(void); + +/* + * 函数功能:查「有已封存段可读」标志 —— 跨任务的唯一握手量(§5.2 第一条陷阱) + * 入口参数:无 + * 返 回 值:1 = 状态为 SEALED **且段头已 latch**,此刻起 wave_ring_sealed_meta 与 + * wave_ring_read_ch 可用;0 = 不可用 uint8_t + * 限定条件:任意上下文(只读单字节) + * 函数说明:为什么把「已 latch」并进来而不是单开一个查询:省得 defaultTask 自己 + * 拼两个标志、拼错就是拿半份快照落盘(第 4 步明写的禁忌)。 + * 为 1 期间已封存区对 adcTask 只读不写,读者可以慢慢分次取、不需要临界区; + * 为 0 时所有读口一律返 WAVE_ST_E_STATE 而**不是返回旧数据** —— + * 把上一段的字节当这一段,等于伪造证据。 + */ +uint8_t wave_ring_sealed(void); + +/* + * 函数功能:取已封存段的段头快照(落盘道编头、回放页画标题都用它) + * 入口参数:p_out 输出(整份拷出,约 136 字节;不可为 NULL) + * 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针;WAVE_ST_E_STATE 无就绪段 int32_t + * 限定条件:任意上下文(defaultTask / uiTask 只读)。整份拷贝、不暴露内部指针, + * 免得调用方攥着指针跨到下一段去看 + * 函数说明:这里的 fs_hz / pts_per_cycle / pre_pts 是**数据**,回放页与 PC 脚本必须 + * 从这里读,不许当编译期常量(§2.1 第 1 条)。 + */ +int32_t wave_ring_sealed_meta(wave_seg_meta_t *p_out); + +/* + * 函数功能:按通道读已封存区的连续点窗(回放页画一路、落盘道搬一路都走它) + * 入口参数:a_ch 列号 = 通道表下标 = 语义号(0 到 WAVE_RING_CH-1) + * a_from 段内起始点下标(0 = 段首 = 最早的一点。点序口径:触发那一拍的 + * 样本落在点 WAVE_RING_PRE-1(预触发窗的最后一点),段内点 + * WAVE_RING_PRE 起是触发之后的第一拍——预触发 8 周波与后触发 + * 8 周波各 160 点,故障时刻正好是两半的分界(§4.2) + * a_cnt 点数(1 起;超出段尾时函数内部夹到剩余点数,不报错) + * p_dst 输出 int16 数组(不可为 NULL;画窗 40 点或搬运 128 点按调用方给的容量) + * 返 回 值:不小于 0 = 实际读出的点数(到段尾会少于 a_cnt); + * WAVE_ST_E_PARAM 空指针或 a_cnt 为 0;WAVE_ST_E_RANGE 列号或起始点越界; + * WAVE_ST_E_STATE 无就绪段 int32_t + * 限定条件:任意上下文,但**只读已封存区**:本层不提供任何读活缓冲的接口, + * IDLE / FREEZE / COOL 下一律 WAVE_ST_E_STATE(§5.2 第一条:不给跨任务读活环的路径) + * 函数说明:内部按段首基址取模展开,对读者来说已封存区就是一段 320 点的线性数组。 + * 回放页画 2 周波 = 40 点(§8.1)一次调用即可;字节形态 = 段文件数据区的 + * 字节形态(int16 小端),所以落盘道能直接把它交给 wave_store 的打包游标。 + */ +int32_t wave_ring_read_ch(uint8_t a_ch, uint16_t a_from, uint16_t a_cnt, int16_t *p_dst); + +/* + * 函数功能:同上,形态适配 App/algo/wave_store.h 的 wave_reader_fn(落盘道直接传它) + * 入口参数:p_user 上下文(本层是单例,忽略,传 NULL 即可) + * a_ch 列号 + * a_from 段内起始点下标 + * a_cnt 点数 + * p_dst 输出 int16 数组 + * 返 回 值:实际读出的点数(不小于 0);负 = WAVE_ST_E_*(同上三条) int32_t + * 限定条件:任意上下文;只读已封存区(同上) + * 函数说明:存在的唯一理由是让 wave_store_pack_start 的 p_reader 直接指过来, + * 省掉接线索里一个只为转发的包装函数。 + */ +int32_t wave_ring_read_cb(void *p_user, uint8_t a_ch, uint32_t a_from, + uint32_t a_cnt, int16_t *p_dst); + +/* + * 函数功能:取观测计数快照(给 §9 的周期行与主机单测) + * 入口参数:p_out 输出(不可为 NULL) + * 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 int32_t + * 限定条件:任意上下文(只读;一次拷约 44 字节,个别字段可能与 adcTask 的推进错开一拍, + * 这是观测量的固有精度,不构成判据错误) + * 函数说明:err_cnt 的两条来源都在本层收口(write_done 非 0、SEALED 滞留超时自弃), + * 所以 §9 日志行的 err= 直接取它即可 —— 落盘道不需要自己数第二份账。 + */ +int32_t wave_ring_get_stat(wave_ring_stat_t *p_out); + +/* + * 函数功能:把「语义通道号」换算成本环的列号(列序 = 语义号升序,§6.3) + * 入口参数:a_sem 语义号(adc_semantic_t 值) + * 返 回 值:0 到 WAVE_RING_CH-1 = 列号;WAVE_ST_E_RANGE = 该语义不录(TEMP 与越界值) int32_t + * 限定条件:任意上下文(纯计算) + * 函数说明:本波只录前 8 个语义通道,所以「列号 == 语义号」这条恒等式成立; + * 但调用方必须走这个函数而不是自己写死下标 —— 二期带 TEMP 时列号与语义号 + * 就会分叉,写死下标的地方会静默错位。 + */ +int32_t wave_ring_col(uint8_t a_sem); + +#ifdef __cplusplus +} +#endif + +#endif /* WAVE_RING_H */ diff --git a/App/algo/wave_store.c b/App/algo/wave_store.c new file mode 100644 index 00000000..76a9d7a2 --- /dev/null +++ b/App/algo/wave_store.c @@ -0,0 +1,981 @@ +/* + * 模块名称:录波段格式层实现(故障录波 · 段文件字节布局编解码 + 打包游标 + 段名轮转) + * 模块功能:见 App/algo/wave_store.h 的文件头注释块——口径属主、职责边界、跨任务安全、 + * 二期不返工的约束全部写在那里,本文件不重复一遍。 + * 本文件是纯逻辑:只 include App/algo/wave_store.h 与 C 标准库的 stdint/stddef, + * 不 include HAL / CMSIS / RTOS 头、不调 FatFs、不碰文件系统、不打日志。 + * 适用平台:STM32F407ZGTx(Cortex-M4,小端);主机侧单测 test/host/wave_store/ + * 作者:DTU 项目组 + * 创建日期:2026-10-29(故障录波 task-1 · 数据面纯逻辑层) + * 修改记录: + * v1.0 2026-10-29 DTU 项目组 创建 + */ + +#include "wave_store.h" + +/* ARMCC 的 stdint.h 不提供 NULL,而主机侧 gcc / MSVC 会传递性把它拉进来 —— + * 这个差别主机单测照不出来(2026-10-29 台基线实跑 @build 才暴露:本层 + * wave_store.c 报 22 条 identifier "NULL" 未定义)。与 App/algo/sample_ring.c、App/algo/freq.c + * 同一口径:谁用 NULL 谁自己 include stddef.h,不往公共头里塞(少波及别人的 TU)。 */ +#include + +/* 打包游标一次最多从数据源取多少个点(128 字节栈上临时区,落盘道在 defaultTask 上跑, + * 不在 1 kHz 采集链上;取批是为了把「每点一次回调」压成「每 64 点一次回调」) */ +#define WAVE_ST_BATCH (64u) +/* 数据区点下标非法时的内部哨兵(对外一律走 wave_store_data_off 的 0xFFFFFFFFu) */ +#define WAVE_ST_BAD_OFF (0xFFFFFFFFu) + +/* ===== 内部:小端读写(显式移位拼装,不依赖宿主字节序,板端与 PC 侧同一判据) ===== */ + +/* + * 函数功能:写 16 位小端 + * 入口参数:p 目标位置(至少 2 字节);v 值 + * 返 回 值:无 void + * 限定条件:调用方保证 p 可写 2 字节 + */ +static void wave_st_put16(uint8_t *p, uint16_t v) { + p[0] = (uint8_t)(v & 0xFFu); + p[1] = (uint8_t)((v >> 8) & 0xFFu); +} + +/* + * 函数功能:写 32 位小端 + * 入口参数:p 目标位置(至少 4 字节);v 值 + * 返 回 值:无 void + * 限定条件:调用方保证 p 可写 4 字节 + */ +static void wave_st_put32(uint8_t *p, uint32_t v) { + p[0] = (uint8_t)(v & 0xFFu); + p[1] = (uint8_t)((v >> 8) & 0xFFu); + p[2] = (uint8_t)((v >> 16) & 0xFFu); + p[3] = (uint8_t)((v >> 24) & 0xFFu); +} + +/* + * 函数功能:读 16 位小端 + * 入口参数:p 源位置(至少 2 字节) + * 返 回 值:值 uint16_t + * 限定条件:调用方保证 p 可读 2 字节 + */ +static uint16_t wave_st_get16(const uint8_t *p) { + return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); +} + +/* + * 函数功能:读 32 位小端 + * 入口参数:p 源位置(至少 4 字节) + * 返 回 值:值 uint32_t + * 限定条件:调用方保证 p 可读 4 字节 + */ +static uint32_t wave_st_get32(const uint8_t *p) { + return ((uint32_t)p[0]) | (((uint32_t)p[1]) << 8) | + (((uint32_t)p[2]) << 16) | (((uint32_t)p[3]) << 24); +} + +/* + * 函数功能:把 int16 写成小端两字节 + * 入口参数:p 目标位置;v 码值 + * 返 回 值:无 void + * 限定条件:调用方保证 p 可写 2 字节 + * 函数说明:先转 uint16 再移位——负数右移是实现定义行为,转成无符号后行为确定。 + */ +static void wave_st_put_code(uint8_t *p, int16_t v) { + wave_st_put16(p, (uint16_t)v); +} + +/* ===== 内部:段头字段一致性自检(编码前与解码后各跑一次,同一把尺子) ===== */ + +/* + * 函数功能:校验段头元数据的**几何与表序**是否自相矛盾 + * 入口参数:p_meta 段头元数据(非空) + * 返 回 值:WAVE_ST_OK = 合法;WAVE_ST_E_SIZE 长度类不成立(零通道 / 零点数 / 预触发超段长); + * WAVE_ST_E_FORMAT 档位 / 触发源 / 表序非法 int + * 限定条件:p_meta 由调用方保证非空 + * 函数说明:为什么编码侧也要跑:写一个「看起来像样的坏段文件」比拒写糟得多——落盘道会 + * 把 CRC 与长度都算自洽的垃圾写进 NAND,等到回放页或 PC 脚本才发现,那时 + * 现场已经没了(§8.1「画成一条直线等于伪造证据」是同一条忌讳的另一半)。 + */ +static int wave_st_meta_check(const wave_seg_meta_t *p_meta) { + uint8_t i; + + /* 零通道 / 零点数:这一段连长度都不成立(§6.2 末行「长度为 0 判废」) */ + if ((p_meta->ch_count == 0u) || (p_meta->ch_count > (uint8_t)WAVE_ST_CH_MAX)) { + return WAVE_ST_E_SIZE; + } + if (p_meta->pts_per_ch == 0u) { + return WAVE_ST_E_SIZE; + } + /* 预触发点数不能大于一段的点数;后触发 = pts_per_ch - pre_pts,允许为 0(手动触发 + * 撞上落盘窗口等极端情形),但不允许负数——那是把段长算错 */ + if (p_meta->pre_pts > p_meta->pts_per_ch) { + return WAVE_ST_E_SIZE; + } + /* 采样率与每周波点数是数据,但为 0 的数据没法解释(回放与 PC 都要拿它做除数) */ + if ((p_meta->fs_hz == 0u) || (p_meta->pts_per_cycle == 0u)) { + return WAVE_ST_E_SIZE; + } + /* 与 PC 侧 reader 的拒绝清单对齐(谁都不能产出一段「自己写得出、对面读不了」的文件): + * 每周波不足 2 点连一个周期都描不出来;段长不足一个整周期则基波算法无从取窗。 */ + if (p_meta->pts_per_cycle < 2u) { + return WAVE_ST_E_SIZE; + } + if (p_meta->pts_per_ch < (uint32_t)p_meta->pts_per_cycle) { + return WAVE_ST_E_SIZE; + } + if (p_meta->eng > (uint8_t)WAVE_ENG_PRI) { + return WAVE_ST_E_FORMAT; + } + if (p_meta->trig_src > (uint8_t)WAVE_TRIG_BENCH) { + return WAVE_ST_E_FORMAT; + } + /* 表序:第 i 项的语义号必须等于下标(§6.3「通道表序与列序」行)—— + * 数据区第 i 列归第 i 项解释,错一位就是拿 IB 的波形配 IC 的系数 */ + for (i = 0u; i < p_meta->ch_count; i++) { + if (p_meta->ch[i].sem != i) { + return WAVE_ST_E_FORMAT; + } + if (p_meta->ch[i].conv > (uint8_t)1u) { + return WAVE_ST_E_FORMAT; + } + /* 位宽留个合理域:本波只有 16(TPAFE)与 12(片内 ADC),但格式层不写死这两个值, + * 只挡明显不成立的(0 位、超过 32 位)——二期换转换器不必改这里 */ + if ((p_meta->ch[i].bits == 0u) || (p_meta->ch[i].bits > 32u)) { + return WAVE_ST_E_FORMAT; + } + } + return WAVE_ST_OK; +} + +/* ===== CRC-16/MODBUS ===== */ + +/* + * 函数功能:CRC-16/MODBUS 增量计算 + * 入口参数:a_crc 上一次的中间值(首次传 WAVE_ST_CRC_INIT) + * p_data 数据(NULL 视为空操作) + * a_len 字节数 + * 返 回 值:新的中间值 uint16_t + * 限定条件:任意上下文;纯计算 + * 函数说明:反射形态 = 右移、异或 0xA001、初值 0xFFFF、异或输出 0; + * 写进文件时低字节在前(§6.3)。无表实现的理由见 wave_store.h 头注。 + */ +uint16_t wave_store_crc_update(uint16_t a_crc, const uint8_t *p_data, uint32_t a_len) { + uint16_t crc = a_crc; + uint32_t i; + + if (p_data == NULL) { + return crc; + } + for (i = 0u; i < a_len; i++) { + uint8_t b = p_data[i]; /* 身兼两职:本路字节 + 下面内层 8 次的计数器 */ + + crc = (uint16_t)(crc ^ (uint16_t)b); + for (b = 0u; b < 8u; b++) { + if ((crc & 0x0001u) != 0u) { + crc = (uint16_t)((crc >> 1) ^ WAVE_ST_CRC_POLY); + } else { + crc = (uint16_t)(crc >> 1); + } + } + } + /* 异或输出恒 0:写成宏引用,换异或输出时只改一处 */ + return (uint16_t)(crc ^ WAVE_ST_CRC_XOROUT); +} + +/* + * 函数功能:CRC-16/MODBUS 一次算完 + * 入口参数:p_data 数据;a_len 字节数 + * 返 回 值:CRC;空输入返回初值定义 uint16_t + * 限定条件:任意上下文。零长度的判废由调用方做(见 wave_store.h 同函数注释) + */ +uint16_t wave_store_crc(const uint8_t *p_data, uint32_t a_len) { + return wave_store_crc_update(WAVE_ST_CRC_INIT, p_data, a_len); +} + +/* + * 函数功能:CRC 实现自检(对规范锚点,不是自算自验) + * 入口参数:无 + * 返 回 值:WAVE_ST_OK 相符;WAVE_ST_E_FORMAT 不符 int + * 限定条件:任意上下文;开机或首次落盘前一次,不在每拍路径上 + * 函数说明:锚点串「123456789」的 CRC-16/MODBUS = 0x4B37(§6.3)。字面量必须纯 ASCII, + * 所以这里用显式字节数组而不是把中文塞进字符串。 + */ +int wave_store_crc_selftest(void) { + static const uint8_t s_anchor[9] = { + (uint8_t)0x31u, (uint8_t)0x32u, (uint8_t)0x33u, + (uint8_t)0x34u, (uint8_t)0x35u, (uint8_t)0x36u, + (uint8_t)0x37u, (uint8_t)0x38u, (uint8_t)0x39u + }; + uint16_t got; + + got = wave_store_crc(s_anchor, (uint32_t)sizeof(s_anchor)); + if (got != WAVE_ST_CRC_CHECK) { + return WAVE_ST_E_FORMAT; + } + return WAVE_ST_OK; +} + +/* ===== 段头编解码 ===== */ + +/* 语义通道的转换器归属与位宽(0 = TPAFE5160,1 = 片内 ADC1;口径属主是 + * docs/硬件平台/ADC通道映射说明.md,本表是其抄本,改硬件接线时两处一起改)。 + * 第 9 项 TEMP 本波不录入段文件(ch_count 恒 8),仍按实际归属填, + * 免得将来放开 TEMP 时这里是个 0。 */ +static const uint8_t s_sem_conv[(uint8_t)WAVE_ST_CH_MAX] = { + (uint8_t)0u, (uint8_t)1u, (uint8_t)1u, + (uint8_t)0u, (uint8_t)0u, (uint8_t)1u, (uint8_t)1u, (uint8_t)1u, (uint8_t)1u +}; +static const uint8_t s_sem_bits[(uint8_t)WAVE_ST_CH_MAX] = { + 16u, 12u, 12u, + 16u, 16u, 12u, 12u, 12u, 12u +}; + +/* + * 函数功能:把段头元数据填成本波默认值(只填格式侧静态事实) + * 入口参数:p_meta 输出 + * 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 int + * 限定条件:任意上下文 + * 函数说明:填三样格式侧的静态事实:语义号 = 下标(§6.3 表序口径,也是 PC 侧判据)、 + * conv / bits 按两片转换器的归属(下面那张表的口径属主是 + * docs/硬件平台/ADC通道映射说明.md 的抄本,本层刻意不带 adc_input.h, + * 这样格式层在主机侧不需要任何假件就能编译;单测里 0xE6 的算术交叉验证 + * 会同时钉住这套顺序,抄错就会红)。 + * 不填 avail / k_q16 / b_codes:那些是封存瞬间的事实,必须由调用方现取现填 + * (adc_cal_k_q16 / adc_cal_flags / sys_clock),本层不链它们; + * 调用方忘填 k_q16 与 avail 不会让编码失败(0 是合法整数), + * 但会让 PC 侧把波形解释成一条 0 线——所以单测里有一条专门检查 + * 「填了系数的往返」与「没填系数时段的形状仍然正确、只是幅值归零」。 + */ +int wave_store_meta_init(wave_seg_meta_t *p_meta) { + uint8_t i; + + if (p_meta == NULL) { + return WAVE_ST_E_PARAM; + } + p_meta->fs_hz = (uint16_t)WAVE_ST_FS_DEF; + p_meta->pts_per_cycle = (uint8_t)WAVE_ST_PPC_DEF; + p_meta->ch_count = (uint8_t)WAVE_ST_CH_NUM; + p_meta->pts_per_ch = (uint16_t)WAVE_ST_PTS_DEF; + p_meta->pre_pts = (uint16_t)WAVE_ST_PRE_DEF; + p_meta->trig_src = (uint8_t)WAVE_TRIG_NONE; + p_meta->trig_elm = 0u; + p_meta->t0_ms = 0u; + p_meta->wall = 0u; + p_meta->eng = (uint8_t)WAVE_ENG_PIN; + p_meta->flags_seq = 0u; + p_meta->clip_mask = 0u; + p_meta->frame_seq = 0u; + for (i = 0u; i < (uint8_t)WAVE_ST_CH_MAX; i++) { + p_meta->ch[i].sem = i; + p_meta->ch[i].conv = s_sem_conv[i]; + p_meta->ch[i].bits = s_sem_bits[i]; + p_meta->ch[i].avail = 0u; + p_meta->ch[i].k_q16 = 0; + p_meta->ch[i].b_codes = 0; + } + return WAVE_ST_OK; +} + +/* + * 函数功能:内部——把固定头 + 通道表写进缓冲(不含头 CRC) + * 入口参数:p_meta 段头;p_out 缓冲(容量须 >= 头 CRC 覆盖区) + * 返 回 值:写入字节数(= 头 CRC 覆盖区长度) uint32_t + * 限定条件:p_meta 已过 wave_st_meta_check、p_out 容量已核过 + */ +static uint32_t wave_st_head_raw(const wave_seg_meta_t *p_meta, uint8_t *p_out) { + uint8_t i; + const uint32_t tbl_n = (uint32_t)p_meta->ch_count * WAVE_ST_CH_BYTES; + + p_out[WAVE_ST_OFF_MAGIC] = (uint8_t)WAVE_ST_MAGIC0; + p_out[WAVE_ST_OFF_MAGIC + 1u] = (uint8_t)WAVE_ST_MAGIC1; + /* 魔数第三字节 = 版本号:与 ver 字段同源同值(§6.2 第 0 行 + §6.3 判废口径) */ + p_out[WAVE_ST_OFF_MAGIC + 2u] = (uint8_t)WAVE_ST_VER; + p_out[WAVE_ST_OFF_VER] = (uint8_t)WAVE_ST_VER; + wave_st_put16(&p_out[WAVE_ST_OFF_FS_HZ], p_meta->fs_hz); + p_out[WAVE_ST_OFF_PTS_CYC] = p_meta->pts_per_cycle; + p_out[WAVE_ST_OFF_CH_CNT] = p_meta->ch_count; + wave_st_put16(&p_out[WAVE_ST_OFF_PTS_CH], p_meta->pts_per_ch); + wave_st_put16(&p_out[WAVE_ST_OFF_PRE_PTS], p_meta->pre_pts); + p_out[WAVE_ST_OFF_TRIG_SRC] = p_meta->trig_src; + p_out[WAVE_ST_OFF_TRIG_ELM] = p_meta->trig_elm; + wave_st_put32(&p_out[WAVE_ST_OFF_T0_MS], p_meta->t0_ms); + wave_st_put32(&p_out[WAVE_ST_OFF_WALL], p_meta->wall); + p_out[WAVE_ST_OFF_ENG] = p_meta->eng; + p_out[WAVE_ST_OFF_FLAGS] = p_meta->flags_seq; + wave_st_put16(&p_out[WAVE_ST_OFF_CLIP], p_meta->clip_mask); + wave_st_put32(&p_out[WAVE_ST_OFF_SEQ], p_meta->frame_seq); + + for (i = 0u; i < p_meta->ch_count; i++) { + uint32_t pos = (uint32_t)WAVE_ST_OFF_TBL + ((uint32_t)i * WAVE_ST_CH_BYTES); + + p_out[pos + WAVE_ST_CH_OFF_SEM] = p_meta->ch[i].sem; + p_out[pos + WAVE_ST_CH_OFF_CONV] = p_meta->ch[i].conv; + p_out[pos + WAVE_ST_CH_OFF_BITS] = p_meta->ch[i].bits; + p_out[pos + WAVE_ST_CH_OFF_AVAIL] = p_meta->ch[i].avail; + /* int32 系数按小端原样落 4 字节:右移到 24 位前把符号扩展掉, + * 保证板端 ARMCC 与主机 CRT 写出同一串字节 */ + wave_st_put32(&p_out[pos + WAVE_ST_CH_OFF_KQ16], (uint32_t)p_meta->ch[i].k_q16); + wave_st_put32(&p_out[pos + WAVE_ST_CH_OFF_BCODE], (uint32_t)p_meta->ch[i].b_codes); + } + return (uint32_t)WAVE_ST_OFF_TBL + tbl_n; +} + +/* + * 函数功能:编码段头(固定头 + 通道表 + 头 CRC) + * 入口参数:p_meta 段头元数据;p_out 输出缓冲;a_cap 容量 + * 返 回 值:>= 0 写入字节数;负 = WAVE_ST_E_* int + * 限定条件:任意上下文;无静态缓冲、可重入 + * 函数说明:头 CRC 覆盖 [0, 30+12×ch_count)、自身紧随其后、低字节在前(§6.3)。 + */ +int wave_store_head_build(const wave_seg_meta_t *p_meta, uint8_t *p_out, uint32_t a_cap) { + uint32_t crc_pos; + uint16_t crc; + int rc; + + if ((p_meta == NULL) || (p_out == NULL)) { + return WAVE_ST_E_PARAM; + } + rc = wave_st_meta_check(p_meta); + if (rc != WAVE_ST_OK) { + return rc; + } + crc_pos = WAVE_ST_HEAD_CRC_POS(p_meta->ch_count); + if (a_cap < (crc_pos + 2u)) { + return WAVE_ST_E_PARAM; + } + (void)wave_st_head_raw(p_meta, p_out); + crc = wave_store_crc(p_out, crc_pos); + /* 低字节在前(§6.3 首行):put16 本来就是小端,这里不额外翻转 */ + wave_st_put16(&p_out[crc_pos], crc); + return (int)(crc_pos + 2u); +} + +/* + * 函数功能:解码 + 全套判废(魔数 / 版本 / 缓冲长度 / 几何 / 表序 / 头 CRC) + * 入口参数:p_buf 头字节流;a_len 可读字节数;p_meta 输出 + * 返 回 值:WAVE_ST_OK 或 WAVE_ST_E_*(见 wave_store.h) int + * 限定条件:任意上下文 + * 函数说明:顺序刻意是「先验魔数与版本,再算整头长度,再验 CRC,最后验语义」—— + * 前两步决定我们**有没有资格**按这份布局去数偏移;反过来先按 ch_count 算长度, + * 一个被写坏的 ch_count 就能让我们读到缓冲外面去。 + */ +int wave_store_head_parse(const uint8_t *p_buf, uint32_t a_len, wave_seg_meta_t *p_meta) { + uint8_t ch_count; + uint32_t crc_pos; + uint32_t head_sz; + uint16_t want; + uint16_t got; + int rc; + wave_seg_meta_t tmp; + + if ((p_buf == NULL) || (p_meta == NULL)) { + return WAVE_ST_E_PARAM; + } + /* 固定头是 30 + 1 字节通道数才能算出整头长度,至少得读到偏移 7 */ + if (a_len < ((uint32_t)WAVE_ST_OFF_CH_CNT + 1u)) { + return WAVE_ST_E_PARAM; + } + if ((p_buf[WAVE_ST_OFF_MAGIC] != (uint8_t)WAVE_ST_MAGIC0) || + (p_buf[WAVE_ST_OFF_MAGIC + 1u] != (uint8_t)WAVE_ST_MAGIC1)) { + return WAVE_ST_E_FORMAT; + } + /* ver 必须等于魔数第三字节、且等于本层认识的版本(§6.2 末行) */ + if ((p_buf[WAVE_ST_OFF_VER] != p_buf[WAVE_ST_OFF_MAGIC + 2u]) || + (p_buf[WAVE_ST_OFF_VER] != (uint8_t)WAVE_ST_VER)) { + return WAVE_ST_E_FORMAT; + } + ch_count = p_buf[WAVE_ST_OFF_CH_CNT]; + head_sz = wave_store_head_bytes(ch_count); + if (head_sz == 0u) { + return WAVE_ST_E_SIZE; + } + if (a_len < head_sz) { + return WAVE_ST_E_PARAM; + } + crc_pos = WAVE_ST_HEAD_CRC_POS(ch_count); + want = wave_st_get16(&p_buf[crc_pos]); + got = wave_store_crc(p_buf, crc_pos); + if (want != got) { + return WAVE_ST_E_CRC; + } + + /* 头 CRC 过了才开始解释字段:下面填的是一份可交回调用方的完整元数据 */ + tmp.fs_hz = wave_st_get16(&p_buf[WAVE_ST_OFF_FS_HZ]); + tmp.pts_per_cycle = p_buf[WAVE_ST_OFF_PTS_CYC]; + tmp.ch_count = ch_count; + tmp.pts_per_ch = wave_st_get16(&p_buf[WAVE_ST_OFF_PTS_CH]); + tmp.pre_pts = wave_st_get16(&p_buf[WAVE_ST_OFF_PRE_PTS]); + tmp.trig_src = p_buf[WAVE_ST_OFF_TRIG_SRC]; + tmp.trig_elm = p_buf[WAVE_ST_OFF_TRIG_ELM]; + tmp.t0_ms = wave_st_get32(&p_buf[WAVE_ST_OFF_T0_MS]); + tmp.wall = wave_st_get32(&p_buf[WAVE_ST_OFF_WALL]); + tmp.eng = p_buf[WAVE_ST_OFF_ENG]; + tmp.flags_seq = p_buf[WAVE_ST_OFF_FLAGS]; + tmp.clip_mask = wave_st_get16(&p_buf[WAVE_ST_OFF_CLIP]); + tmp.frame_seq = wave_st_get32(&p_buf[WAVE_ST_OFF_SEQ]); + for (ch_count = 0u; ch_count < (uint8_t)WAVE_ST_CH_MAX; ch_count++) { + tmp.ch[ch_count].sem = 0u; + tmp.ch[ch_count].conv = 0u; + tmp.ch[ch_count].bits = 0u; + tmp.ch[ch_count].avail = 0u; + tmp.ch[ch_count].k_q16 = 0; + tmp.ch[ch_count].b_codes = 0; + } + for (ch_count = 0u; ch_count < p_buf[WAVE_ST_OFF_CH_CNT]; ch_count++) { + uint32_t pos = (uint32_t)WAVE_ST_OFF_TBL + ((uint32_t)ch_count * WAVE_ST_CH_BYTES); + tmp.ch[ch_count].sem = p_buf[pos + WAVE_ST_CH_OFF_SEM]; + tmp.ch[ch_count].conv = p_buf[pos + WAVE_ST_CH_OFF_CONV]; + tmp.ch[ch_count].bits = p_buf[pos + WAVE_ST_CH_OFF_BITS]; + tmp.ch[ch_count].avail = p_buf[pos + WAVE_ST_CH_OFF_AVAIL]; + tmp.ch[ch_count].k_q16 = (int32_t)wave_st_get32(&p_buf[pos + WAVE_ST_CH_OFF_KQ16]); + tmp.ch[ch_count].b_codes = (int32_t)wave_st_get32(&p_buf[pos + WAVE_ST_CH_OFF_BCODE]); + } + rc = wave_st_meta_check(&tmp); + if (rc != WAVE_ST_OK) { + return rc; + } + *p_meta = tmp; + return WAVE_ST_OK; +} + +/* ===== 长度 / 偏移换算 ===== */ + +/* + * 函数功能:整头字节数 + * 入口参数:a_ch_count 通道数 + * 返 回 值:字节数;通道数非法返回 0 uint32_t + * 限定条件:任意上下文 + */ +uint32_t wave_store_head_bytes(uint8_t a_ch_count) { + if ((a_ch_count == 0u) || (a_ch_count > (uint8_t)WAVE_ST_CH_MAX)) { + return 0u; + } + return WAVE_ST_HEAD_BYTES(a_ch_count); +} + +/* + * 函数功能:按头部字段算整段应有字节数 + * 入口参数:p_meta 段头 + * 返 回 值:期望字节数;字段非法返回 0 uint32_t + * 限定条件:任意上下文 + * 函数说明:等式 = 32 + 12×ch_count + pts_per_ch×ch_count×2 + 2(§6.3; + * 8 路 320 点 = 5250)。落盘道拿它比对 f_write 写回字节数与 f_size。 + */ +uint32_t wave_store_seg_size(const wave_seg_meta_t *p_meta) { + if (p_meta == NULL) { + return 0u; + } + if ((p_meta->ch_count == 0u) || (p_meta->ch_count > (uint8_t)WAVE_ST_CH_MAX)) { + return 0u; + } + if (p_meta->pts_per_ch == 0u) { + return 0u; + } + return WAVE_ST_SEG_TOTAL(p_meta->ch_count, p_meta->pts_per_ch); +} + +/* + * 函数功能:数据区某点的文件绝对偏移 + * 入口参数:a_ch_count 通道数;a_pts_ch 每通道点数;a_ch 列号;a_pt 段内点下标 + * 返 回 值:偏移;非法返回 0xFFFFFFFFu 哨兵 uint32_t + * 限定条件:任意上下文 + * 函数说明:按通道分列 → 偏移 = 整头长 + (列 × 点数 + 点下标) × 2。单通道整列是连续的, + * 这正是 §6.2「只读那一路」能一次连续读的原因。 + */ +uint32_t wave_store_data_off(uint8_t a_ch_count, uint16_t a_pts_ch, + uint8_t a_ch, uint32_t a_pt) { + uint32_t head_sz = wave_store_head_bytes(a_ch_count); + + if ((head_sz == 0u) || (a_pts_ch == 0u) || (a_ch >= a_ch_count)) { + return WAVE_ST_BAD_OFF; + } + if (a_pt >= (uint32_t)a_pts_ch) { + return WAVE_ST_BAD_OFF; + } + return head_sz + (((uint32_t)a_ch * (uint32_t)a_pts_ch) + a_pt) * 2u; +} + +/* + * 函数功能:从某点起当前列还能连续读多少字节 + * 入口参数:a_pts_ch 每通道点数;a_pt 起始点下标;a_want 想读点数 + * 返 回 值:可连续读字节数(参数非法返回 0) uint32_t + * 限定条件:任意上下文 + */ +uint32_t wave_store_data_bytes_avail(uint16_t a_pts_ch, uint32_t a_pt, uint32_t a_want) { + uint32_t left; + uint32_t n; + + if ((a_pts_ch == 0u) || (a_want == 0u)) { + return 0u; + } + if (a_pt >= (uint32_t)a_pts_ch) { + return 0u; + } + left = (uint32_t)a_pts_ch - a_pt; + n = (a_want < left) ? a_want : left; + return n * 2u; +} + +/* + * 函数功能:小端字节 → int16 点序列 + * 入口参数:p_raw 原始字节;a_len 字节数;p_out 输出;a_max_pts 容量;p_pts 实际点数(可空) + * 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 / 奇数字节 / 容量不足 int + * 限定条件:任意上下文 + * 函数说明:奇数字节按参数错处理而不是「丢掉尾巴」——半截样本被静默吞掉, + * 回放页画出来的波形会比真的少最后一个点,而这种误差正好最难查。 + */ +int wave_store_le_decode(const uint8_t *p_raw, uint32_t a_len, + int16_t *p_out, uint32_t a_max_pts, uint32_t *p_pts) { + uint32_t n; + uint32_t i; + + if ((p_raw == NULL) || (p_out == NULL)) { + return WAVE_ST_E_PARAM; + } + if (((a_len & 0x00000001u) != 0u) || (a_len > (a_max_pts * 2u))) { + return WAVE_ST_E_PARAM; + } + n = a_len / 2u; + for (i = 0u; i < n; i++) { + /* 先攒成 uint16 再转 int16:补码回绕在 C 里是实现定义但两大编译器一致, + * 而这里需要的正是「按补码位型解释」——TPAFE 的 16 位补码就是这么存的 */ + p_out[i] = (int16_t)wave_st_get16(&p_raw[i * 2u]); + } + if (p_pts != NULL) { + *p_pts = n; + } + return WAVE_ST_OK; +} + +/* + * 函数功能:码值 → 定点毫单位(mV) + * 入口参数:p_desc 通道表项(必须来自段文件头);a_code 码值;p_milli 输出 mV + * 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 int + * 限定条件:任意上下文(只在回放页与单测用,不在 1 kHz 采集链上;用 int64 中间量) + * 函数说明:模型 eng = (code + b) × k × 1e-6(§6.3,与 adc_cal_apply() 同式), + * k = k_q16 / 65536 µV/码 → mV = (code + b) × k_q16 / 65536000,四舍五入。 + * 中间量最大约 (32767 + 8192) × 5.28e7 ≈ 2.2e12,int64 宽裕。 + */ +int wave_store_code_to_milli(const wave_ch_desc_t *p_desc, int16_t a_code, + int32_t *p_milli) { + int64_t num; + int64_t den = 65536000; + int64_t v; + + if ((p_desc == NULL) || (p_milli == NULL)) { + return WAVE_ST_E_PARAM; + } + num = ((int64_t)a_code + (int64_t)p_desc->b_codes) * (int64_t)p_desc->k_q16; + if (num >= 0) { + v = (num + (den / 2)) / den; + } else { + v = -(((-num) + (den / 2)) / den); + } + *p_milli = (int32_t)v; + return WAVE_ST_OK; +} + +/* ===== 整段校验 ===== */ + +/* + * 函数功能:整段镜像自检(头 + 数据区 + 数据 CRC) + * 入口参数:p_img 镜像;a_len 长度;p_meta 输出段头(可空) + * 返 回 值:WAVE_ST_OK 或 WAVE_ST_E_*(见 wave_store.h) int + * 限定条件:任意上下文;整段驻留时用(本波 5250 字节) + * 函数说明:三条判废一条都不能省:长度 0 或不等、头 CRC、数据 CRC。 + * 期望总长用**头里的字段**算,不用编译期常量——文件是自己说它多长, + * 与镜像实际长度不一致就是坏段(§6.2 末行)。 + */ +int wave_store_seg_check(const uint8_t *p_img, uint32_t a_len, wave_seg_meta_t *p_meta) { + wave_seg_meta_t meta; + uint32_t want_total; + uint32_t data_off; + uint32_t data_len; + uint16_t want; + uint16_t got; + int rc; + + if (p_img == NULL) { + return WAVE_ST_E_PARAM; + } + if (a_len == 0u) { + return WAVE_ST_E_SIZE; + } + rc = wave_store_head_parse(p_img, a_len, &meta); + if (rc != WAVE_ST_OK) { + return rc; + } + want_total = wave_store_seg_size(&meta); + if (want_total == 0u) { + return WAVE_ST_E_SIZE; + } + if (a_len != want_total) { + return WAVE_ST_E_SIZE; + } + data_off = wave_store_head_bytes(meta.ch_count); /* 数据区起点 = 整头字节数 */ + data_len = a_len - 2u - data_off; + if (data_len == 0u) { + return WAVE_ST_E_SIZE; + } + want = wave_st_get16(&p_img[a_len - 2u]); + got = wave_store_crc(&p_img[data_off], data_len); + if (want != got) { + return WAVE_ST_E_CRC; + } + if (p_meta != NULL) { + *p_meta = meta; + } + return WAVE_ST_OK; +} + +/* ===== 打包游标 ===== */ + +/* + * 函数功能:初始化打包游标 + * 入口参数:p_ctx 游标;p_meta 段头;p_reader 数据源(可空 = 只产头);p_user 上下文 + * 返 回 值:WAVE_ST_OK 或 WAVE_ST_E_* int + * 限定条件:任意上下文(纯计算);一段一个游标 + * 函数说明:段头整份拷进游标——搬运窗口几十毫秒,期间调用方完全可以去改它自己那份 + * (比如 adcTask 又跑了几拍),本段的字节不能受影响。 + */ +int wave_store_pack_start(wave_pack_ctx_t *p_ctx, const wave_seg_meta_t *p_meta, + wave_reader_fn p_reader, void *p_user) { + int rc; + + if ((p_ctx == NULL) || (p_meta == NULL)) { + return WAVE_ST_E_PARAM; + } + rc = wave_st_meta_check(p_meta); + if (rc != WAVE_ST_OK) { + p_ctx->phase = (uint8_t)WAVE_PACK_IDLE; + return rc; + } + p_ctx->total = wave_store_seg_size(p_meta); + if (p_ctx->total == 0u) { + p_ctx->phase = (uint8_t)WAVE_PACK_IDLE; + return WAVE_ST_E_SIZE; + } + p_ctx->phase = (uint8_t)WAVE_PACK_HEAD; + p_ctx->ch = 0u; + p_ctx->head_done = 0u; + p_ctx->ch_done = 0u; + p_ctx->tail_done = 0u; + p_ctx->rsv = 0u; + p_ctx->crc_data = WAVE_ST_CRC_INIT; + p_ctx->sent = 0u; + p_ctx->p_reader = p_reader; + p_ctx->p_user = p_user; + p_ctx->meta = *p_meta; + return WAVE_ST_OK; +} + +/* + * 函数功能:取整段字节流的下一片 + * 入口参数:p_ctx 游标;p_out 缓冲;a_cap 容量;p_len 实际产出字节数 + * 返 回 值:WAVE_ST_OK 还有后续;WAVE_ST_DONE 整段产完;负 = WAVE_ST_E_* int + * 限定条件:任意上下文,同一游标不得并发使用 + * 函数说明:三段推进——头(一次给完,要求容量 >= 整头)、数据区(按列连续、整样本切, + * 绝不切半个 int16,也不跨列,便于回调按列读)、尾 CRC 两字节。 + * 数据 CRC 边产边算(增量中间值),所以全程不需要整段驻留 RAM。 + */ +int wave_store_pack_next(wave_pack_ctx_t *p_ctx, uint8_t *p_out, + uint32_t a_cap, uint32_t *p_len) { + uint32_t n = 0u; + + if ((p_ctx == NULL) || (p_out == NULL) || (p_len == NULL)) { + return WAVE_ST_E_PARAM; + } + *p_len = 0u; + if ((p_ctx->phase == (uint8_t)WAVE_PACK_IDLE) || (a_cap == 0u)) { + return WAVE_ST_E_STATE; + } + if (p_ctx->phase == (uint8_t)WAVE_PACK_DONE) { + return WAVE_ST_DONE; + } + + /* --- 头:整头一次产完(接线索按 §7 给 256 字节缓冲,足够 128 字节整头) --- */ + if (p_ctx->phase == (uint8_t)WAVE_PACK_HEAD) { + uint32_t head_sz; + int rc; + + head_sz = wave_store_head_bytes(p_ctx->meta.ch_count); + if (head_sz == 0u) { + return WAVE_ST_E_SIZE; + } + if (a_cap < head_sz) { + /* 不拆着头给:半截头意味着游标要存 128 字节中间态,纯浪费 */ + return WAVE_ST_E_PARAM; + } + rc = wave_store_head_build(&p_ctx->meta, p_out, a_cap); + if (rc < 0) { + return rc; + } + n = (uint32_t)rc; + p_ctx->head_done = (uint16_t)n; + p_ctx->sent += n; + *p_len = n; + p_ctx->phase = (uint8_t)WAVE_PACK_DATA; + return WAVE_ST_OK; + } + + /* --- 数据区:按列推进,一次最多一批 --- */ + if (p_ctx->phase == (uint8_t)WAVE_PACK_DATA) { + int16_t batch[WAVE_ST_BATCH]; + + if (p_ctx->p_reader == NULL) { + /* 没有数据源却进了数据区:宁可报错也不产空字节。 + * 产空字节会写出「长度对、内容是 0」的段文件,回放页会画出一条直线—— + * 那正是 §8.1 明令禁止的伪造证据形态。 */ + return WAVE_ST_E_STATE; + } + while ((n < a_cap) && (p_ctx->ch < p_ctx->meta.ch_count)) { + uint32_t room_pts = (a_cap - n) / 2u; + uint32_t col_left = (uint32_t)p_ctx->meta.pts_per_ch - (uint32_t)p_ctx->ch_done; + uint32_t take; + int32_t got; + uint32_t i; + + if (col_left == 0u) { + p_ctx->ch++; + p_ctx->ch_done = 0u; + continue; + } + if (room_pts == 0u) { + break; + } + take = (room_pts < col_left) ? room_pts : col_left; + if (take > (uint32_t)WAVE_ST_BATCH) { + take = (uint32_t)WAVE_ST_BATCH; + } + got = p_ctx->p_reader(p_ctx->p_user, p_ctx->ch, (uint32_t)p_ctx->ch_done, + take, batch); + if (got < 0) { + return (int)got; + } + if ((uint32_t)got != take) { + /* 数据源给不满就是数据源的问题(越界 / 未封存),不拿半批凑长度 */ + return WAVE_ST_E_STATE; + } + uint32_t base_n = n; + for (i = 0u; i < take; i++) { + wave_st_put_code(&p_out[n], batch[i]); + n += 2u; + } + p_ctx->crc_data = wave_store_crc_update(p_ctx->crc_data, &p_out[base_n], take * 2u); + p_ctx->ch_done = (uint16_t)((uint32_t)p_ctx->ch_done + take); + } + if (p_ctx->ch >= p_ctx->meta.ch_count) { + /* 最后一列的最后一点产出后,「列走完了」要到下一次进循环才发现, + * 所以这一拍可能一个字节都没产。这时候不许当参数错报出去:整段就差 + * 最后两片 CRC,报 E_PARAM 会让落盘道把一次真故障判成落盘失败(白丢)。 + * 让它落到下面的尾段分支,在同一次调用里把尾产完。 */ + p_ctx->phase = (uint8_t)WAVE_PACK_TAIL; + } + p_ctx->sent += n; + *p_len = n; + if (n > 0u) { + return WAVE_ST_OK; + } + if (p_ctx->phase != (uint8_t)WAVE_PACK_TAIL) { + return WAVE_ST_E_PARAM; /* 容量连一个整样本都装不下 */ + } + } + + /* --- 尾:数据区 CRC,低字节在前 --- */ + if (p_ctx->phase == (uint8_t)WAVE_PACK_TAIL) { + if (p_ctx->tail_done == 0u) { + p_out[n] = (uint8_t)(p_ctx->crc_data & 0xFFu); + n++; + p_ctx->tail_done = 1u; + } + if ((p_ctx->tail_done == 1u) && (n < a_cap)) { + p_out[n] = (uint8_t)((p_ctx->crc_data >> 8) & 0xFFu); + n++; + p_ctx->tail_done = 2u; + p_ctx->phase = (uint8_t)WAVE_PACK_DONE; + } + p_ctx->sent += n; + *p_len = n; + return (p_ctx->phase == (uint8_t)WAVE_PACK_DONE) ? WAVE_ST_DONE : WAVE_ST_OK; + } + return WAVE_ST_E_STATE; +} + +/* + * 函数功能:游标已产出字节数 + * 入口参数:p_ctx 游标 + * 返 回 值:字节数;空指针返回 0 uint32_t + * 限定条件:任意上下文(只读) + */ +uint32_t wave_store_pack_offset(const wave_pack_ctx_t *p_ctx) { + return (p_ctx == NULL) ? 0u : p_ctx->sent; +} + +/* + * 函数功能:游标对应整段的期望总长 + * 入口参数:p_ctx 游标 + * 返 回 值:字节数;空指针返回 0 uint32_t + * 限定条件:任意上下文(只读) + */ +uint32_t wave_store_pack_total(const wave_pack_ctx_t *p_ctx) { + return (p_ctx == NULL) ? 0u : p_ctx->total; +} + +/* ===== 段文件名轮转(只算名字,不碰文件系统) ===== */ + +/* + * 函数功能:内部——把两位十进制写进缓冲 + * 入口参数:p 目标(至少 2 字节);a_idx 0..99 + * 返 回 值:无 void + * 限定条件:a_idx 已由调用方限制在 0..99 + */ +static void wave_st_put_idx2(char *p, uint8_t a_idx) { + p[0] = (char)('0' + (a_idx / 10u)); + p[1] = (char)('0' + (a_idx % 10u)); +} + +/* + * 函数功能:按段号生成段文件路径 + * 入口参数:p_out 缓冲;a_cap 容量;a_idx 段号 0..99 + * 返 回 值:>= 0 写入字符数(不含结束符);WAVE_ST_E_PARAM / WAVE_ST_E_RANGE int + * 限定条件:任意上下文;纯字符拼装(不用格式化函数),板端与 PC 侧逐字节一致 + * 函数说明:形态 = 目录 / 前缀 + 两位十进制 + 扩展名,严格 8.3(§5.2 第三条)。 + */ +int wave_store_seg_path(char *p_out, uint32_t a_cap, uint8_t a_idx) { + static const char s_dir[] = WAVE_ST_DIR; + static const char s_pfx[] = WAVE_ST_PFX; + static const char s_ext[] = WAVE_ST_EXT; + uint32_t pos = 0u; + uint32_t i; + char buf[WAVE_ST_PATH_MAX]; + + if (p_out == NULL) { + return WAVE_ST_E_PARAM; + } + if (a_idx >= (uint8_t)WAVE_ST_SEG_MAX) { + return WAVE_ST_E_RANGE; + } + for (i = 0u; i < ((uint32_t)sizeof(s_dir) - 1u); i++) { + buf[pos++] = s_dir[i]; + } + buf[pos++] = '/'; + for (i = 0u; i < ((uint32_t)sizeof(s_pfx) - 1u); i++) { + buf[pos++] = s_pfx[i]; + } + wave_st_put_idx2(&buf[pos], a_idx); + pos += 2u; + for (i = 0u; i < ((uint32_t)sizeof(s_ext) - 1u); i++) { + buf[pos++] = s_ext[i]; + } + if (pos >= (uint32_t)sizeof(buf)) { + /* 宏与拼装长度漂移时的兜底:宁可给不出名字,也绝不把结束符写到别人缓冲外 */ + return WAVE_ST_E_SIZE; + } + buf[pos] = '\0'; + /* 容量必须容得下结束符:写成 a_cap < pos 会放过「刚好差一个字节」的调用方, + * 于是把 p_out[pos] 写到别人缓冲外面(越界写在板子上是静默的) */ + if (a_cap <= pos) { + return WAVE_ST_E_PARAM; + } + for (i = 0u; i <= pos; i++) { + p_out[i] = buf[i]; + } + return (int)pos; +} + +/* + * 函数功能:下一个轮转段号 + * 入口参数:a_idx 当前段号(越界按 0 处理) + * 返 回 值:下一个段号 uint8_t + * 限定条件:任意上下文 + */ +uint8_t wave_store_next_idx(uint8_t a_idx) { + if (a_idx >= (uint8_t)WAVE_ST_SEG_MAX) { + return 0u; + } + if (a_idx >= ((uint8_t)WAVE_ST_SEG_MAX - 1u)) { + return 0u; + } + return (uint8_t)(a_idx + 1u); +} + +/* + * 函数功能:内部——大小写无关地比对一个字符 + * 入口参数:a 待测字符(视作 ASCII);b 期望字符(取自段名宏) + * 返 回 值:1 相同;0 不同 uint8_t + * 限定条件:两侧都是 ASCII;非字母字符原样比 + * 函数说明:**两侧都**归一化成小写再比。只归一化一侧是个真踩过一次的坑—— + * 段名宏 WAVE_ST_PFX / WAVE_ST_EXT 是大写形态(FatFs 8.3 短名就是大写), + * 只把待测串转小写等于「小写 vs 大写」永不相等,反解会拒绝掉全部合法段名 + * (单测 用例:首轮 100% 拒绝就是这个)。盘上大小写与宏写法 + * 谁都不该去猜,故两侧各归一一次。 + */ +static uint8_t wave_st_ci_eq(char a, char b) { + char x = a; + char y = b; + + if ((x >= 'A') && (x <= 'Z')) { + x = (char)(x + ('a' - 'A')); + } + if ((y >= 'A') && (y <= 'Z')) { + y = (char)(y + ('a' - 'A')); + } + return (x == y) ? 1u : 0u; +} + +/* + * 函数功能:从文件名或路径反解段号 + * 入口参数:p_path 路径或纯文件名;p_idx 输出 + * 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针;WAVE_ST_E_NAME 不是本层命名形态 int + * 限定条件:任意上下文 + * 函数说明:只认 basename 上的「wave + 两位数字 + .bin」(大小写都收)。 + * 不匹配就返回错误,**绝不**把陌生名字当录波段——扫目录重建段账时误认一个 + * 别的文件,等于把别人的文件当波形解释(名字/参数类错误码严禁升级成卷操作, + * 存储架构说明 §10.1 同一条教训)。 + */ +int wave_store_idx_from_name(const char *p_path, uint8_t *p_idx) { + static const char s_pfx[] = WAVE_ST_PFX; + static const char s_ext[] = WAVE_ST_EXT; + const char *base = p_path; + const char *q = p_path; + uint32_t i; + uint32_t len = 0u; + uint8_t hi; + uint8_t lo; + + if ((p_path == NULL) || (p_idx == NULL)) { + return WAVE_ST_E_PARAM; + } + /* 找 basename:支持反斜杠形态(PC 侧脚本拼出来的路径可能被原样传进来) */ + while (*q != '\0') { + if ((*q == '/') || (*q == '\\')) { + base = q + 1; + } + q++; + } + q = base; + while ((*q != '\0') && (len < (uint32_t)WAVE_ST_PATH_MAX)) { + q++; + len++; + } + if ((*q != '\0') || (len != ((uint32_t)sizeof(s_pfx) - 1u) + 2u + + ((uint32_t)sizeof(s_ext) - 1u))) { + return WAVE_ST_E_NAME; + } + for (i = 0u; i < ((uint32_t)sizeof(s_pfx) - 1u); i++) { + if (wave_st_ci_eq(base[i], s_pfx[i]) == 0u) { + return WAVE_ST_E_NAME; + } + } + hi = (uint8_t)base[i]; + lo = (uint8_t)base[i + 1u]; + if ((hi < '0') || (hi > '9') || (lo < '0') || (lo > '9')) { + return WAVE_ST_E_NAME; + } + for (i = (i + 2u); i < len; i++) { + if (wave_st_ci_eq(base[i], s_ext[i - ((uint32_t)sizeof(s_pfx) - 1u) - 2u]) == 0u) { + return WAVE_ST_E_NAME; + } + } + *p_idx = (uint8_t)(((hi - '0') * 10u) + (lo - '0')); + return WAVE_ST_OK; +} diff --git a/App/algo/wave_store.h b/App/algo/wave_store.h new file mode 100644 index 00000000..4c0ec498 --- /dev/null +++ b/App/algo/wave_store.h @@ -0,0 +1,491 @@ +/* + * 模块名称:录波段格式层(故障录波 · 段文件字节布局编解码 + 打包游标 + 段名轮转) + * 模块功能:把「一段录波」在内存字节缓冲与结构化元数据之间逐字节翻译,并给落盘道 + * 提供分次搬运的打包游标。口径唯一属主 = docs/子系统设计/故障录波设计说明.md §6.2 + * (字节布局)与 §6.3(九问裁定:板端编码器与 PC 解码器 test/host/wave_read.py + * 两份独立实现的共同依据)。本文件是它的机器可执行形态,不在此重新定义任何口径; + * 两处冲突以设计文档为准并回报 lead,不在代码里单方面「修正」。 + * 纯逻辑层纪律:不 include HAL / CMSIS / RTOS 头、不调 FatFs、不碰文件系统、 + * 不建任务、不打日志(文件 I/O 与任务钩子全在接线层 App/task/ 里做), + * 因此可被主机侧单测 test/host/wave_store/ 原样纳入做逐字节断言。 + * 职责边界(为什么这样切): + * 1) 固定头 + 通道表编解码、头 CRC 与数据区 CRC 自检 —— 格式知识只此一处; + * 2) 打包游标(分次搬运):文件系统唯一写者只要「每次给我不超过 256 字节的下一片」, + * 整段 5250 字节不驻留 RAM(设计说明 §7:搬运缓冲 256 B × 2,不做整段驻留); + * 3) 段文件名轮转与反解:只算名字,不打开文件(FatFs FF_USE_LFN=0 只能严格 + * 8.3 短名,见 docs/架构设计/STM32F4-Base存储架构说明.md §10.1); + * 4) 数据区偏移换算 + 小端解码:回放页只画一路(设计说明 §8.1),按通道分列 + * 让「只读那一路」成为一次连续读。 + * 不链 App/algo/adc_cal(零依赖 = 主机免桩可测):通道表里的 k_q16 / b_codes / + * 可用性由调用方在封存瞬间取 int32 原值填进 meta 后交给本层编码。系数必须走 + * adc_cal_k_q16()(接线道补的 int32 直读入口),不得经 adc_cal_k_uv() 的 float + * 通道回算 —— float 在 5.28e7 量级掉尾数(设计说明 §6.3「k / b 宽度与角色」行)。 + * CRC 一张表的原始说法不成立,lead 已在 §6.3 记录实测依据:全仓无可复用的 + * CRC-16 表(App/util/README.md 记 crc.c 与 crc.h 已于 2026-10-02 删除;屏板链路 + * Drivers/BSP/UI/ui_net_sink.c 的 remo_crc() 是单字节异或、不是 CRC-16)。 + * 故本层自带无表位运算 CRC-16/MODBUS,并用规范锚点自校验。 + * 适用平台:STM32F407ZGTx(Cortex-M4,小端);主机侧单测 test/host/wave_store/ + * 作者:DTU 项目组 + * 创建日期:2026-10-29(故障录波 task-1 · 数据面纯逻辑层) + * 修改记录: + * v1.0 2026-10-29 DTU 项目组 创建:段头/通道表编解码、CRC-16/MODBUS 无表实现、 + * 打包游标、段名轮转与反解、数据区偏移换算与小端解码 + * + * 二期不返工的结构约束(设计说明 §2.1,改本层前先读): + * 1) 采样率与每周波点数是写进文件头的数据,不是编译期假设 —— 回放端与 PC 脚本一律 + * 从头里读,本层算长度也照 meta 里的值算,不在代码里写死 1000 / 20; + * 2) 版本号并进魔数第三字节,换格式即变;本层解到不认识的版本一律判废; + * 3) 「带 TEMP 就要连 flags_seq 与 clip_mask 一起升版本」同源(§6.2 那两行的括注), + * 所以本层的通道数上限留到 9,但本波实际只用 8。 + */ + +#ifndef WAVE_STORE_H +#define WAVE_STORE_H + +/* 同 App/algo/wave_ring.h:本头的 NULL 只出现在注释里(字段与入参的约束说明), + * 代码只用 stdint 的类型,故不 include stddef.h;用 NULL 的 .c 自己带。 */ +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ===== 一、段文件布局常量(逐字段照 docs/子系统设计/故障录波设计说明.md §6.2 与 §6.3) ===== */ + +/* 魔数前两字节固定 'W' 与 'R',第三字节 = 版本号(设计说明 §6.2 偏移 0 行) */ +#define WAVE_ST_MAGIC0 (0x57u) /* 'W' */ +#define WAVE_ST_MAGIC1 (0x52u) /* 'R' */ +/* 版本:本波恒 1。ver 字段必须同时等于魔数第三字节与本宏,否则整段判废(§6.2 末行) */ +#define WAVE_ST_VER (1u) + +/* 固定头字段偏移(字节,小端) */ +#define WAVE_ST_OFF_MAGIC (0u) /* 3 字节:'W' 'R' ver */ +#define WAVE_ST_OFF_VER (3u) /* 1 字节 */ +#define WAVE_ST_OFF_FS_HZ (4u) /* 2 字节 本波 1000 */ +#define WAVE_ST_OFF_PTS_CYC (6u) /* 1 字节 本波 20 */ +#define WAVE_ST_OFF_CH_CNT (7u) /* 1 字节 本波 8 */ +#define WAVE_ST_OFF_PTS_CH (8u) /* 2 字节 本波 320 */ +#define WAVE_ST_OFF_PRE_PTS (10u) /* 2 字节 本波 160 */ +#define WAVE_ST_OFF_TRIG_SRC (12u) /* 1 字节 */ +#define WAVE_ST_OFF_TRIG_ELM (13u) /* 1 字节 */ +#define WAVE_ST_OFF_T0_MS (14u) /* 4 字节 u32 */ +#define WAVE_ST_OFF_WALL (18u) /* 4 字节 u32 */ +#define WAVE_ST_OFF_ENG (22u) /* 1 字节 */ +#define WAVE_ST_OFF_FLAGS (23u) /* 1 字节 */ +#define WAVE_ST_OFF_CLIP (24u) /* 2 字节 */ +#define WAVE_ST_OFF_SEQ (26u) /* 4 字节 u32 */ +#define WAVE_ST_OFF_TBL (30u) /* 通道表起点 */ + +/* 通道表一路 12 字节 = 语义号 1 + 来源转换器 1 + 位宽 1 + 可用性 1 + k_q16 4 + b_codes 4 + * 设计说明 §6.3「LSB 从哪来」:不设独立 LSB 字段,k_q16 就是 LSB(单位 µV/码 × 65536) */ +#define WAVE_ST_CH_OFF_SEM (0u) /* 1 字节 */ +#define WAVE_ST_CH_OFF_CONV (1u) /* 1 字节 */ +#define WAVE_ST_CH_OFF_BITS (2u) /* 1 字节 */ +#define WAVE_ST_CH_OFF_AVAIL (3u) /* 1 字节 = adc_cal_flags(sem) 低 8 位原值,不自造位义 */ +#define WAVE_ST_CH_OFF_KQ16 (4u) /* 4 字节 int32 */ +#define WAVE_ST_CH_OFF_BCODE (8u) /* 4 字节 int32 */ +#define WAVE_ST_CH_BYTES (12u) + +/* 本波通道数固定 8(TEMP 不录:理由见 §6.3 末段与 docs/任务规划/数据可信.md §7) */ +#define WAVE_ST_CH_NUM (8u) +/* 通道数上限:给二期「带 TEMP 且同时升版本」留位;真正的口径是 meta 里的 ch_count 字段 */ +#define WAVE_ST_CH_MAX (9u) +/* 每通道点数默认(= 预触发 + 后触发 = 160 + 160;长度是文件里的数据,宏只作默认值) */ +#define WAVE_ST_PTS_DEF (320u) +#define WAVE_ST_PRE_DEF (160u) +#define WAVE_ST_FS_DEF (1000u) +#define WAVE_ST_PPC_DEF (20u) + +/* 尺寸一律由字段算出,不写死字面量(改通道数/点数时自动跟着走): + * 头 CRC 覆盖区 = 30 + 12 × ch_count;头 CRC 自身紧随其后 2 字节;数据区自 32 + 12 × ch 起 + * ch_count = 8 时:覆盖区 126、整头 128、数据区 5120、整段 5250(§6.3「CRC 覆盖范围」行) */ +#define WAVE_ST_HEAD_CRC_POS(ch) ((uint32_t)(WAVE_ST_OFF_TBL) + \ + ((uint32_t)(ch) * WAVE_ST_CH_BYTES)) +#define WAVE_ST_HEAD_BYTES(ch) ((uint32_t)(WAVE_ST_HEAD_CRC_POS(ch)) + 2u) +#define WAVE_ST_DATA_BYTES(ch, pts) ((uint32_t)(pts) * (uint32_t)(ch) * 2u) +#define WAVE_ST_SEG_TOTAL(ch, pts) (WAVE_ST_HEAD_BYTES(ch) + \ + WAVE_ST_DATA_BYTES(ch, pts) + 2u) +/* 本波实际值(只给注释与单测引用,代码里一律用上面的参数化形态) */ +#define WAVE_ST_HEAD_SZ_CUR (128u) +#define WAVE_ST_SEG_SZ_CUR (5250u) + +/* 段目录与轮转(设计说明 §5.2 第三条:只能严格 8.3 短名;目录与 /log 平级、FTP 根下可见) + * 名字形态 = 目录 + 前缀 + 两位十进制 + 扩展名,号 0..99 共 100 个轮转覆盖 */ +#define WAVE_ST_DIR "/wave" +#define WAVE_ST_PFX "WAVE" +#define WAVE_ST_EXT ".BIN" +#define WAVE_ST_SEG_MAX (100u) +#define WAVE_ST_PATH_MAX (17u) /* /wave/WAVE00.BIN = 16 字符 + 结束符 = 17 字节 */ + +/* CRC-16/MODBUS:多项式 0x8005 反射形态 = 0xA001、初值 0xFFFF、异或输出 0、低字节在前 */ +#define WAVE_ST_CRC_POLY (0xA001u) +#define WAVE_ST_CRC_INIT (0xFFFFu) +#define WAVE_ST_CRC_XOROUT (0x0000u) +/* 自检锚点:串 123456789 的 CRC(§6.3 首行钉的常量) */ +#define WAVE_ST_CRC_CHECK (0x4B37u) + +/* ===== 二、返回码(int 通道;调用方必须接住或显式 (void),不得裸丢) ===== */ + +#define WAVE_ST_OK (0) /* 成功 */ +#define WAVE_ST_DONE (1) /* 打包游标:整段已产完(不是错误) */ +#define WAVE_ST_E_PARAM (-1) /* 空指针 / 容量不足 / 字段非法 */ +#define WAVE_ST_E_CRC (-2) /* CRC 不符(头或数据区):整段判废 */ +#define WAVE_ST_E_FORMAT (-3) /* 魔数不符 / 版本不认识 / 表内语义号不等于下标 */ +#define WAVE_ST_E_SIZE (-4) /* 长度为 0 或长度与头部字段矛盾:整段判废 */ +#define WAVE_ST_E_RANGE (-5) /* 列号 / 点下标 / 段号越界 */ +#define WAVE_ST_E_STATE (-6) /* 状态错:未封存就读、未 start 就步进、缺数据源 */ +#define WAVE_ST_E_NAME (-7) /* 文件名不是本层命名形态(扫目录重建段账时用) */ + +/* ===== 三、类型 ===== */ + +/* 触发源(§6.2 偏移 12 的取值;0 表示「无触发」,只出现在段外的状态查询里) */ +typedef enum { + WAVE_TRIG_NONE = 0, /* 无 */ + WAVE_TRIG_MANUAL = 1, /* 手动(UI「手动录波」确认,task-2 接线) */ + WAVE_TRIG_PROTECT = 2, /* 保护启动(App/algo/prot_elm.c 的 picked 上升沿) */ + WAVE_TRIG_BENCH = 3 /* bench 定周期自抓(只在 DTU_BENCH 构建里有这条来路) */ +} wave_trig_src_t; + +/* 折算档位(§6.2 偏移 22;单位一律以头里的 eng 为准,PC 不许按当前固件口径猜) */ +typedef enum { + WAVE_ENG_PIN = 0, /* 引脚域(未标定;V0.1 产品构建停在这档) */ + WAVE_ENG_SEC = 1, /* 二次值 */ + WAVE_ENG_PRI = 2 /* 一次值 */ +} wave_eng_t; + +/* 通道表一路的内存形态(字段序 = §6.2 通道表字节序) */ +typedef struct { + uint8_t sem; /* 语义号(adc_semantic_t 值;必须等于表内下标,§6.3) */ + uint8_t conv; /* 来源转换器(adc_source_t 值:0 = TPAFE,1 = STM32) */ + uint8_t bits; /* 转换器输出位宽(TPAFE 16 / STM32 12) */ + uint8_t avail; /* 可用性 = adc_cal_flags(sem) 低 8 位原值(不自造位义) */ + int32_t k_q16; /* 封存瞬间生效系数:µV/码 × 65536,就是 LSB */ + int32_t b_codes; /* 封存瞬间生效零点偏移:码值 */ +} wave_ch_desc_t; + +/* 段头元数据 = 固定头 + 通道表的内存形态。 + * 这是「待编码的段头」,不是「已写盘的证据」:打包游标只读它、不改它。 */ +typedef struct { + uint16_t fs_hz; /* 采样率 Hz(从文件读出来的数据,不是编译期常量) */ + uint8_t pts_per_cycle; /* 每周波点数 */ + uint8_t ch_count; /* 通道数(本波 8) */ + uint16_t pts_per_ch; /* 每通道点数(本波 320) */ + uint16_t pre_pts; /* 预触发点数(本波 160;回放画触发光标用) */ + uint8_t trig_src; /* 触发源(wave_trig_src_t) */ + uint8_t trig_elm; /* 触发元件序号(prot_elm 实例下标;手动与定时为 0) */ + uint32_t t0_ms; /* 段首相对上电毫秒 */ + uint32_t wall; /* 对时后的 epoch 秒(未对时 0) */ + uint8_t eng; /* 折算档位(wave_eng_t) */ + uint8_t flags_seq; /* bit s = 语义通道 s 的 ADC_CAL_F_VALID(§6.3) */ + uint16_t clip_mask; /* bit s = 语义通道 s 的 ADC_CAL_F_CLIPPED(V0.1 恒 0xE6) */ + uint32_t frame_seq; /* 段首帧序号(与串口周期行的 frames 对账) */ + wave_ch_desc_t ch[WAVE_ST_CH_MAX]; /* 通道表(前 ch_count 项有效) */ +} wave_seg_meta_t; + +/* 打包游标阶段(对外只用于「已搬完」判定与观测日志;推进全在 wave_store_pack_next 内部) */ +typedef enum { + WAVE_PACK_IDLE = 0, /* 未初始化 */ + WAVE_PACK_HEAD = 1, /* 正在产头(固定头 + 通道表 + 头 CRC) */ + WAVE_PACK_DATA = 2, /* 正在产数据区(按通道分列,一列一列产) */ + WAVE_PACK_TAIL = 3, /* 只剩数据区 CRC 两字节 */ + WAVE_PACK_DONE = 4 /* 整段已产完 */ +} wave_pack_phase_t; + +/* 数据源回调:把「第 a_ch 列、段内第 a_from 点起的 a_cnt 个点」取进 p_dst。 + * 返 回 值:实际拷贝的点数(等于 a_cnt 表示全给到);负 = 错误码 int32_t + * 只允许读已封存区:本层不提供、也不允许任何跨任务读活缓冲的路径 + * (设计说明 §5.2 第一条陷阱:App/algo/sample_ring.c 的 sample_ring_window() 无临界区, + * 本波不复用它,跨任务只传「已封存」标志)。 + * 生产实现 = wave_ring_read_cb()(签名一致,可直接传给 wave_store_pack_start)。 */ +typedef int32_t (*wave_reader_fn)(void *p_user, uint8_t a_ch, uint32_t a_from, + uint32_t a_cnt, int16_t *p_dst); + +/* 打包游标:段文件字节流的「拉模式」状态机。 + * 为什么拉模式 + 小缓冲:defaultTask 是文件系统唯一写者,落盘窗口几十毫秒量级, + * 一次心跳最多一段;整段驻留 RAM 既超预算也没必要(设计说明 §7)。 + * 游标不校验写回字节数 —— 那是调用方对着 f_write 做的事(§5.2 第二条陷阱:本移植 + * f_write 簇分配失败会返 FR_OK + 短写、没有错误码,必须靠比对写回字节数判失败)。 */ +typedef struct { + uint8_t phase; /* wave_pack_phase_t */ + uint8_t ch; /* 数据区当前列 */ + uint16_t head_done; /* 头区已产出字节数(含头 CRC 两字节) */ + uint16_t ch_done; /* 当前列已产出点数 */ + uint8_t tail_done; /* 尾部数据 CRC 已产出字节数(0..2) */ + uint8_t rsv; /* 补齐用,保持结构体偶数字节 */ + uint16_t crc_data; /* 数据区运行 CRC(增量中间值,避免整段驻留) */ + uint32_t total; /* 整段期望字节数 */ + uint32_t sent; /* 已产出字节数(= 下一片的文件偏移) */ + wave_seg_meta_t meta; /* 段头快照(start 时拷入;搬运期间调用方改自己那份不影响) */ + wave_reader_fn p_reader; /* 数据源;NULL = 只产头 */ + void *p_user; /* 数据源上下文 */ +} wave_pack_ctx_t; + +/* ===== 四、CRC-16/MODBUS(无表位运算) ===== */ + +/* + * 函数功能:CRC-16/MODBUS 增量计算(输入输出均低位在前) + * 入口参数:a_crc 上一次的中间值(首次传 WAVE_ST_CRC_INIT) + * p_data 数据指针(NULL 且 a_len = 0 视为空操作,原样返回 a_crc) + * a_len 字节数 + * 返 回 值:新的中间值(写文件时低字节在前,见 §6.3 首行) uint16_t + * 限定条件:任意上下文(含 adcTask 之外的 defaultTask / uiTask);纯计算、无静态状态、 + * 不碰外设、不取临界区 + * 函数说明:为什么自带实现 —— 全仓没有可复用的 CRC-16 表(依据见本文件头注)。 + * 无表 = 零新增表 RAM 与表 Flash;数据区 5120 字节每字节 8 次移位,在 168 MHz + * 上量级是几十微秒,且只在 defaultTask 的落盘链上出现,不在 1 kHz 采集链上。 + */ +uint16_t wave_store_crc_update(uint16_t a_crc, const uint8_t *p_data, uint32_t a_len); + +/* + * 函数功能:CRC-16/MODBUS 一次算完(从初值起步) + * 入口参数:p_data 数据指针 + * a_len 字节数 + * 返 回 值:CRC uint16_t + * 限定条件:任意上下文。p_data 为 NULL 或 a_len = 0 时返回 WAVE_ST_CRC_INIT —— 这是 + * 空输入的定义值,不是「算好的段 CRC」:零长度一律判废,调用方必须先判长度 + * (设计说明 §6.2 末行:长度为 0 → 整段作废) + */ +uint16_t wave_store_crc(const uint8_t *p_data, uint32_t a_len); + +/* + * 函数功能:CRC 自检 —— 用规范锚点验算实现是否仍是 CRC-16/MODBUS + * 入口参数:无 + * 返 回 值:WAVE_ST_OK 相符;WAVE_ST_E_FORMAT 不符(实现或编译配置被改坏) int + * 限定条件:任意上下文;开机或首次落盘前调一次即可,不在每拍路径上 + * 函数说明:锚点 = 串 123456789 的 CRC = 0x4B37(§6.3)。这条是机器可执行的口径锚, + * 防的是「自算自验永远相符」:只有对已知常数才咬得住(换机、换编译器、 + * 反射方向写反都在这里暴露)。落盘前失败就自弃本段并计入 err(§9 观测行)。 + */ +int wave_store_crc_selftest(void); + +/* ===== 五、段头编解码 ===== */ + +/* + * 函数功能:把段头元数据填成本波默认值(只填格式侧的静态事实) + * 入口参数:p_meta 输出(调用方持有的结构;本层不返回静态地址) + * 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 int + * 限定条件:任意上下文;不碰文件系统 + * 函数说明:填 fs_hz = 1000、pts_per_cycle = 20、ch_count = 8、pts_per_ch = 320、 + * pre_pts = 160,以及通道表的 sem = 下标 + conv / bits 按两片转换器 + * (UA / UN / IA 走 TPAFE 16 位;UB / UC / IB / IC / IN 走片内 ADC 12 位 —— + * 通道归属口径属主是 docs/硬件平台/ADC通道映射说明.md,本函数只是它的抄本, + * 单测里对 0xE6 的算术交叉验证会同时钉住这套顺序)。 + * 不填 avail / k_q16 / b_codes / trig_* / t0_ms / wall / eng / flags_seq / + * clip_mask / frame_seq:那些是封存瞬间的事实,必须由调用方现取现填 + * (adc_cal_k_q16 / adc_cal_flags / sys_clock / wave_ring),本层不链它们。 + */ +int wave_store_meta_init(wave_seg_meta_t *p_meta); + +/* + * 函数功能:把段头元数据编码成段文件头字节流(固定头 + 通道表 + 头 CRC,全小端) + * 入口参数:p_meta 段头元数据(只读) + * p_out 输出缓冲 + * a_cap 缓冲容量(须不小于按 ch_count 算出的整头字节数) + * 返 回 值:不小于 0 = 实际写入字节数;负 = WAVE_ST_E_* int + * 限定条件:任意上下文;无静态缓冲、可重入;不碰文件系统 + * 函数说明:编码前先自检字段一致性 —— ch_count 为 0 或越界、pts_per_ch = 0、 + * pre_pts 大于 pts_per_ch、表内语义号不等于下标(§6.3「通道表序与列序」那条 + * PC 侧判据在编码侧同样成立)一律拒绝:不把坏头写成看起来像样的文件。 + */ +int wave_store_head_build(const wave_seg_meta_t *p_meta, uint8_t *p_out, uint32_t a_cap); + +/* + * 函数功能:把段文件头字节流解回元数据,并做全套判废检查(魔数 / 版本 / 长度 / 表序 / 头 CRC) + * 入口参数:p_buf 头字节流 + * a_len 可读字节数 + * p_meta 输出元数据(判废时内容不保证有效,调用方不得消费) + * 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针或缓冲不足;WAVE_ST_E_FORMAT 魔数 / 版本 / + * 表序不符;WAVE_ST_E_SIZE 零长度或长度与字段矛盾;WAVE_ST_E_CRC 头 CRC 不符 int + * 限定条件:任意上下文(uiTask 与 defaultTask 都可调)。UI 与 PC 侧必须用同一套判废语义: + * 解不过就只能显示「该段不可读」,绝不允许退化画一条直线 + * (设计说明 §8.1 空态行:画直线等于伪造证据) + * 函数说明:a_len 不足「按头里 ch_count 推出来的整头长度」时按缓冲不足返回,不猜没读到 + * 的字节;ver 不等于魔数第三字节、或不等于 WAVE_ST_VER 一律判废。 + */ +int wave_store_head_parse(const uint8_t *p_buf, uint32_t a_len, wave_seg_meta_t *p_meta); + +/* ===== 六、整段校验与长度 / 偏移换算(读侧) ===== */ + +/* + * 函数功能:整段镜像自检 —— 头 + 数据区 + 数据 CRC 三道全过才算「这一段可读」 + * 入口参数:p_img 整段文件镜像(调用方已读进内存) + * a_len 镜像长度 + * p_meta 输出段头(可为 NULL) + * 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针;WAVE_ST_E_SIZE 长度为 0 或与期望总长不符; + * WAVE_ST_E_CRC 任一 CRC 不符;WAVE_ST_E_FORMAT 头本身判废 int + * 限定条件:任意上下文;只在整段驻留(本波 5250 字节)时用。回放页不要走这条 —— + * 用 wave_store_head_parse + wave_store_data_off + wave_store_crc_update 分段查, + * 才符合 §7 的 256 字节读窗预算 + * 函数说明:这是「段文件 CRC 不符时脚本必须拒绝而不是画出东西」那条判据在板侧的同一份 + * 实现,也是本层与 PC 解码器逐字节同解的对照物(两套主机单测互验)。 + */ +int wave_store_seg_check(const uint8_t *p_img, uint32_t a_len, wave_seg_meta_t *p_meta); + +/* + * 函数功能:按头部字段算整段文件应有的字节数(写侧记账与读侧长度判废共用) + * 入口参数:p_meta 段头元数据 + * 返 回 值:期望字节数(8 路 320 点 = 5250);字段非法(含零长度)返回 0 uint32_t + * 限定条件:任意上下文 + * 函数说明:返回 0 就是「这一段连长度都不成立」,据此判废。不要拿 0 去开缓冲, + * 也不要拿 0 去比对 f_write 的写回字节数(那会把失败读成成功)。 + */ +uint32_t wave_store_seg_size(const wave_seg_meta_t *p_meta); + +/* + * 函数功能:整头字节数(固定头 + 通道表 + 头 CRC 两字节) + * 入口参数:a_ch_count 通道数 + * 返 回 值:字节数;通道数为 0 或越界返回 0 uint32_t + * 限定条件:任意上下文 + */ +uint32_t wave_store_head_bytes(uint8_t a_ch_count); + +/* + * 函数功能:数据区某一点在段文件里的绝对偏移(按通道分列 → 单通道是一整段连续区) + * 入口参数:a_ch_count 通道数 + * a_pts_ch 每通道点数 + * a_ch 列号(= 通道表下标 = 语义号,0 到 a_ch_count-1) + * a_pt 段内点下标(0 到 a_pts_ch-1) + * 返 回 值:文件偏移;参数非法返回 0xFFFFFFFFu 哨兵(这个值不是任何合法偏移) uint32_t + * 限定条件:任意上下文 + * 函数说明:回放页据此定位「只读那一路」,配合 wave_store_data_bytes_avail 一次连续读; + * PC 侧同一算法,两份实现互验。 + */ +uint32_t wave_store_data_off(uint8_t a_ch_count, uint16_t a_pts_ch, + uint8_t a_ch, uint32_t a_pt); + +/* + * 函数功能:从某点起当前列还能连续读多少字节(不跨列、不越段尾) + * 入口参数:a_pts_ch 每通道点数 + * a_pt 起始点下标 + * a_want 想读的点数 + * 返 回 值:可连续读字节数 = 2 × 实际可给点数;参数非法返回 0 uint32_t + * 限定条件:任意上下文 + */ +uint32_t wave_store_data_bytes_avail(uint16_t a_pts_ch, uint32_t a_pt, uint32_t a_want); + +/* + * 函数功能:把段文件读出的小端字节解码成 int16 点序列(按时间序,旧到新) + * 入口参数:p_raw 原始字节(来自文件,2 × 点数) + * a_len 字节数(须为偶数,且不大于 a_max_pts × 2) + * p_out 输出 int16 数组 + * a_max_pts 输出容量(点数) + * p_pts 输出实际点数(可为 NULL) + * 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 / 奇数字节 / 容量不足 int + * 限定条件:任意上下文 + * 函数说明:显式按小端逐字节拼装,不依赖宿主字节序,所以板端与 PC 侧同一判据。 + * 片内 ADC 那几路的值本就是 0..4095 的原样正数,不做中点偏置 + * (§6.3「样本编码」行:加偏置等于伪造双极性)。 + */ +int wave_store_le_decode(const uint8_t *p_raw, uint32_t a_len, + int16_t *p_out, uint32_t a_max_pts, uint32_t *p_pts); + +/* + * 函数功能:把一路的 int16 码值折成定点毫单位(回放页峰值标注用;纯整数,不用 float) + * 入口参数:p_desc 该路通道表项(k_q16 / b_codes 来自段文件头,不来自当前固件) + * a_code 原始码值 + * p_milli 输出四舍五入后的毫单位(k_q16 是 µV/码 → 输出单位 mV)(不可为 NULL) + * 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 int + * 限定条件:任意上下文(只在回放页与单测里用,不在 1 kHz 采集链上;内部用 int64 中间量, + * 落在这条路径上时可接受) + * 函数说明:算式 eng = (code + b) × k × 1e-6,与 App/algo/adc_cal.c 的 adc_cal_apply() + * 同一模型(§6.3)。输出单位是 mV;单位含义由段头 eng 字段决定(eng = 0 是 + * 引脚域,别读成二次值)。必须用段文件里的 k / b,不许拿当前固件的系数解释 + * 老文件(§12 风险表「标定落地后老文件被解释错」)。 + */ +int wave_store_code_to_milli(const wave_ch_desc_t *p_desc, int16_t a_code, + int32_t *p_milli); + +/* ===== 七、打包游标(分次搬运,给文件系统唯一写者) ===== */ + +/* + * 函数功能:初始化打包游标(段头拷进游标,随后逐次拉整段字节流) + * 入口参数:p_ctx 游标(调用方持有,栈上或静态都行;本层不记它的地址) + * p_meta 段头元数据(内部拷贝;搬运期间调用方改自己那份不影响本段) + * p_reader 数据源回调;NULL = 只产头(回放页只校验头 / 主机单测只测头) + * p_user 数据源上下文(生产传 NULL,wave_ring 是单例不需要) + * 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针;WAVE_ST_E_FORMAT 段头字段非法; + * WAVE_ST_E_SIZE 算不出合法长度 int + * 限定条件:任意上下文(纯计算,无临界区);一段一个游标,搬完再 start 下一段 + * 函数说明:p_reader 为 NULL 而 ch_count 大于 0 时,游标仍能产头,但进数据区那一步 + * 返回 WAVE_ST_E_STATE —— 不静默跳过数据区,否则调用方会写出一个 + * 「长度看着对、内容是空的」段文件(那正是 §8.1 要防的伪造证据形态)。 + */ +int wave_store_pack_start(wave_pack_ctx_t *p_ctx, const wave_seg_meta_t *p_meta, + wave_reader_fn p_reader, void *p_user); + +/* + * 函数功能:取整段字节流的下一片(分次搬运的唯一推进口) + * 入口参数:p_ctx 已 start 的游标 + * p_out 输出缓冲 + * a_cap 本片容量(建议 256 字节,见 §7;数据区不切半点,奇数容量会少产 1 字节) + * p_len 输出本片实际字节数(不可为 NULL) + * 返 回 值:WAVE_ST_OK 产出本片且还有后续;WAVE_ST_DONE 整段已产完(*p_len 可为 0); + * 负 = WAVE_ST_E_*(未 start / 容量为 0 / 数据源回调返错 / 游标状态错) int + * 限定条件:任意上下文,但同一游标不得并发使用(文件系统唯一写者单线程推进) + * 函数说明:头区字节流与 wave_store_head_build 完全一致(内部同一份写字段函数); + * **头那一步要求 a_cap 不小于整头字节数**(本波 8 路 = 128),否则返回 + * WAVE_ST_E_PARAM 且不推进游标——不拆着头给,是为了让游标不必存 128 字节 + * 中间态;§7 的 256 字节搬运缓冲天然满足。 + * 数据区 CRC 走增量中间值,整段最后两片是数据 CRC 的低字节、高字节。 + * 调用方每写完一片都必须比对 f_write 写回字节数,短写按失败处理(§5.2)。 + */ +int wave_store_pack_next(wave_pack_ctx_t *p_ctx, uint8_t *p_out, + uint32_t a_cap, uint32_t *p_len); + +/* + * 函数功能:读游标已产出字节数(= 当前文件偏移;供记账、观测与「写没写全」判定) + * 入口参数:p_ctx 游标 + * 返 回 值:已产出字节数;p_ctx 为 NULL 返回 0 uint32_t + * 限定条件:任意上下文(只读) + */ +uint32_t wave_store_pack_offset(const wave_pack_ctx_t *p_ctx); + +/* + * 函数功能:读游标对应整段文件的期望总长(写完与 f_size / 累计写回字节数比对) + * 入口参数:p_ctx 游标 + * 返 回 值:期望总字节数;p_ctx 为 NULL 返回 0 uint32_t + * 限定条件:任意上下文(只读) + */ +uint32_t wave_store_pack_total(const wave_pack_ctx_t *p_ctx); + +/* ===== 八、段文件名轮转(只算名字,不碰文件系统) ===== */ + +/* + * 函数功能:按段号生成段文件路径(严格 8.3 短名:目录 + 前缀 + 两位十进制 + 扩展名) + * 入口参数:p_out 输出缓冲(容量须不小于 WAVE_ST_PATH_MAX) + * a_cap 容量 + * a_idx 段号 0 到 WAVE_ST_SEG_MAX-1 + * 返 回 值:不小于 0 = 写入的字符数(不含结束符;当前布局恒为 16,故缓冲至少要 17 字节); + * WAVE_ST_E_PARAM 空指针或容量不足; + * WAVE_ST_E_RANGE 段号越界 int + * 限定条件:任意上下文;纯字符拼装(不用 sprintf,见 CODE-RULE-06 系),逐字节可预期 + * 函数说明:号 0..99 轮转覆盖,共 100 个名字,写满回绕盖最老的(§5.2 第三条)。 + * 本函数不检查文件是否存在、不建目录:那些都是接线道的职责。 + */ +int wave_store_seg_path(char *p_out, uint32_t a_cap, uint8_t a_idx); + +/* + * 函数功能:下一个轮转段号(到 99 后回 0) + * 入口参数:a_idx 当前段号(越界输入按 0 处理,不猜) + * 返 回 值:下一个段号 uint8_t + * 限定条件:任意上下文 + */ +uint8_t wave_store_next_idx(uint8_t a_idx); + +/* + * 函数功能:从文件名或路径反解段号(掉电后靠扫目录重建段账用) + * 入口参数:p_path 路径或纯文件名(只看最后一段,大小写都收) + * p_idx 输出段号(不可为 NULL) + * 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针;WAVE_ST_E_NAME 不是本层命名形态 int + * 限定条件:任意上下文 + * 函数说明:只认「前缀 + 两位数字 + 扩展名」,其余(目录名、长名、以点开头)一律判不匹配 + * —— 扫目录重建账时不许把陌生名字当录波段;名字/参数类错误码严禁升级成卷操作 + * (docs/架构设计/STM32F4-Base存储架构说明.md §10.1 的同一条教训)。 + */ +int wave_store_idx_from_name(const char *p_path, uint8_t *p_idx); + +#ifdef __cplusplus +} +#endif + +#endif /* WAVE_STORE_H */ diff --git a/App/task/adc_task.c b/App/task/adc_task.c index c01a2f31..10c2f87c 100644 --- a/App/task/adc_task.c +++ b/App/task/adc_task.c @@ -106,6 +106,15 @@ * 不并进 cal: 行是因为 9 个码值会把该行推到 168 字符、 * 撞 DBG_CON_LINE_MAX(192) 的静默截断边界(见该行注释)。 * 无新增跨任务状态、无新增外设访问,栈 +36 B(局部码值数组) + * v3.4 2026-10-29 DTU 项目组 故障录波数据面接线(task-2):新增 2c——每拍把 8 路 + * 语义原码压进 wave_ring 并 tick 推进;EVT_SEALED 那一拍现取 + * 折算/有效性/墙钟事实组 wave_seal_cal_t 并 latch_meta 恰好 + * 一次(握手次序照 App/algo/wave_ring.h 头注唯一合法次序); + * 心跳末尾加 §9 唯一周期行 wave:(字段逐字,取数 + * wave_ring_get_stat);新增 2d——bench 定周期自抓,只在 + * DTU_BENCH 且 WAVE_BENCH_AUTOCAP 非 0 的双层编译门内存在, + * 默认关闭 + 5 分钟时长上限 + 启动一次性告警(§10 红线); + * adc_task_init 内接 wave_ring_init(init 禁日志约束不破) */ #include "adc_task.h" @@ -123,6 +132,8 @@ #include "console_log.h" #include "sys_log.h" /* SYSLOG_SELFTEST:采样故障留痕(仅故障才写) */ #include "task_prof.h" /* 常开耗时探针:单拍耗时 */ +#include "wave_ring.h" /* 故障录波环(task-2 数据面接线):push/tick/latch_meta/get_stat */ +#include "sys_clock.h" /* 封存交棒的 wall 字段:sys_clock_get() epoch 秒(UINT32_MAX=未对时,写 0) */ #define DBG_TAG "[ADC_TASK]" #include "dbg_log.h" @@ -132,6 +143,17 @@ * 消费节拍改由 TIM3 ISR 置位 ADC_WAKE_FLAG 提供,与生产同源,不再自定周期) */ #define ADC_TASK_REPORT_MS 10000U +/* bench 定周期自抓(§10 红线「默认关闭 + 有时长上限 + 启动打一次告警」的落点): + * WAVE_BENCH_AUTOCAP 默认 0——产品档连外层 DTU_BENCH 都没有,双层编译门保证 + * 产品镜像里 2d 段整段不存在;台架确实要定周期刷段时才本地改 1 重编译, + * 用完立刻回 0(每 2 s 一段 = 每 5 分钟一轮 NAND 写,长挂白烧写次数,dhara + * 坏块记账还是持久代价)。宏形态照 App/algo/adc_cal.c 的 adc_cal_prot_blocked(): + * 联合条件放 #if,未定义宏恒为假,不把 cppcheck 引到旁路上。 + * 周期/上限取自 §10 的算式:2 s/段、300 s 自动停。 */ +#define WAVE_BENCH_AUTOCAP 0 +#define WAVE_BENCH_PERIOD_MS 2000u +#define WAVE_BENCH_MAX_MS 300000u + /* 统计:已消费帧数 / 无新帧次数(首拍必为无新帧,之后应稳定为 0 增量)。 * 原始码值缓冲由 语义取数与有效性层 App/algo/adc_input.c 持有(消费侧按语义取数,不写死下标) */ static uint32_t s_frames_consumed; @@ -157,6 +179,20 @@ static uint8_t s_rail_syslogged; * 200 ms(与 v2.6 修"重复采样污染 DFT 窗"同族误差,勿"优化"成拍计数) */ static uint32_t s_meas_ms; +/* 故障录波环接线状态(task-2;状态机与计数全在 App/algo/wave_ring.c,这里只搬运): + * s_wave_codes - 本拍 8 路语义原码(文件级 static,不上单拍的栈——本任务静态 + * 最坏链余量按 1024 B ISR 底线核) + * s_wave_cal - EVT_SEALED 拍的交棒结构(同理不放局部:整块 ~160 B) */ +static int16_t s_wave_codes[WAVE_RING_CH]; +static wave_seal_cal_t s_wave_cal; +#if defined(DTU_BENCH) && (WAVE_BENCH_AUTOCAP != 0) +/* bench 自抓编排:on = 已过写者就绪门开始跑;off = 到时自动停(init 重置) */ +static uint8_t s_wave_bench_on; +static uint8_t s_wave_bench_off; +static uint32_t s_wave_bench_t0; +static uint32_t s_wave_bench_next; +#endif + /* 消费者就绪门:由 adcTask 自己在进入等待前置 1;单写者 = 本任务,ISR 只读。 * ⚠ 为什么必须有这道门:TIM3 在 app_main_init()(Src/main.c:140)里就 @@ -288,6 +324,13 @@ int adc_task_init(void) { s_meas_ms = 0u; adc_health_init(); /* 内含 采样通道自检 贴轨观察器复位(rail_init) */ (void)meas_init(); /* 遥测快照双区/序号复位(幂等;发布状态属本任务,init 无日志) */ + wave_ring_init(); /* 录波环纯清 RAM(无内核依赖;本函数在开闸前跑,init 禁日志不破) */ +#if defined(DTU_BENCH) && (WAVE_BENCH_AUTOCAP != 0) + s_wave_bench_on = 0u; + s_wave_bench_off = 0u; + s_wave_bench_t0 = 0u; + s_wave_bench_next = 0u; +#endif return 0; } @@ -454,6 +497,103 @@ void adc_task_func(void *arg) { adc_health_report(&h_st); } + /* 2c) 故障录波环泵送(task-2 数据面接线;状态机与计数全在 App/algo/wave_ring.c, + * 本段只做搬运)。每拍取 8 路语义**原码**压环 + tick 推进;触发那拍的样本 + * 落在段内点 WAVE_RING_PRE-1(点序 lead 钉死)。只存原码不存折算工程量—— + * §6.3 裁定:样本域 = 原始码,折算系数进段头,回放/PC 侧统一换算。 + * 某路取不到(开机头几拍)保持上值(与 adc: 心跳行同一兜底口径)。 + * tick 返 EVT_SEALED 的那拍必须**同拍** latch meta(头注唯一合法次序), + * 漏了 = 2 s 后环自弃计 err,defaultTask 根本不会见到这一段。 */ + { + uint8_t w; + int32_t wev; + + for (w = 0u; w < WAVE_RING_CH; w++) { + int32_t wc = 0; + + if (adc_input_raw((adc_semantic_t)w, &wc) == 0) { + s_wave_codes[w] = (int16_t)wc; + } + } + (void)wave_ring_push(s_wave_codes, WAVE_RING_CH); + wev = wave_ring_tick(now, s_frames_consumed); + + if (wev == WAVE_RING_EVT_SEALED) { + uint8_t c; + + /* 封存瞬间事实(交棒结构 s_wave_cal 为文件级 static,不上栈): + * eng / VALID 位图 / CLIPPED 位图 / 墙钟 / 逐路 conv+bits+avail+k+b。 + * eng 口径 = adc_cal.h 注③ 的 8 路量纲标签(判据源 + * App/app/ui_data_feed.c feed_eng(),与 prot_blocked 的三路口径**不是** + * 一件事):8 路电气量全 VALID 才是二次值;V0.1 无 CT 变比,一次值不可达。 + * wall 未对时写 0(sys_clock_get 失败 = UINT32_MAX,不许拿上电毫秒冒充)。 + * k 取 adc_cal_k_q16() int32 原值;b 的 float 回乘在本域无损 + * (|b| 限幅 8192 << 2^24,float32 精确表示整数;禁用浮点只针对 k)。 */ + s_wave_cal.flags_seq = 0u; + s_wave_cal.clip_mask = 0u; + for (c = 0u; c < WAVE_RING_CH; c++) { + const uint32_t f = adc_cal_flags((adc_semantic_t)c); + const adc_channel_desc_t *d = adc_input_desc((adc_semantic_t)c); + + if ((f & ADC_CAL_F_VALID) != 0u) { + s_wave_cal.flags_seq |= (uint8_t)(1u << c); + } + if ((f & ADC_CAL_F_CLIPPED) != 0u) { + s_wave_cal.clip_mask |= (uint16_t)(1u << c); + } + s_wave_cal.ch[c].conv = (d != NULL) ? (uint8_t)d->source + : (uint8_t)ADC_SRC_STM32; + s_wave_cal.ch[c].bits = (s_wave_cal.ch[c].conv == (uint8_t)ADC_SRC_TPAFE) + ? 16u : 12u; + s_wave_cal.ch[c].avail = (uint8_t)f; + s_wave_cal.ch[c].k_q16 = adc_cal_k_q16((adc_semantic_t)c); + s_wave_cal.ch[c].b_codes = (int32_t)adc_cal_b_codes((adc_semantic_t)c); + } + s_wave_cal.eng = (s_wave_cal.flags_seq == 0xFFu) + ? (uint8_t)WAVE_ENG_SEC : (uint8_t)WAVE_ENG_PIN; + { + uint32_t wall = sys_clock_get(); + + s_wave_cal.wall = (wall == UINT32_MAX) ? 0u : wall; + } + /* 只在 EVT_SEALED 拍调一次;返错不补救——环 2 s 超时自弃兜底, + * 这里补第二行日志就违反 §9「不在已留痕处重复锚点」 */ + (void)wave_ring_latch_meta(&s_wave_cal); + } + } + + /* 2d) bench 定周期自抓(§10 红线落点:默认关闭 + 时长上限 + 启动一次性告警)。 + * 编译门 = DTU_BENCH 且 WAVE_BENCH_AUTOCAP 非 0,产品镜像无本段。 + * 节拍:每 WAVE_BENCH_PERIOD_MS 投一次 bench 请求(环侧冷却/去重自管, + * 被丢的计入 ovf——这是 bench 的预期观测面,不是故障)。 + * 计时起点等写者就绪门(v2.1 理由:就绪前直发会与 defaultTask 存储 + * 日志字节级交错);到时自动停 + 一次性 STOP 行,之后不再投任何请求。 + * ⚠ 这两条 WARN/STOP 是一次性生命周期日志,不是周期行,§9 一条线约束不破 */ +#if defined(DTU_BENCH) && (WAVE_BENCH_AUTOCAP != 0) + if (s_wave_bench_off == 0u) { + if (s_wave_bench_on == 0u) { + if (dbg_console_writer_ready() != 0u) { + s_wave_bench_on = 1u; + s_wave_bench_t0 = now; + s_wave_bench_next = now + WAVE_BENCH_PERIOD_MS; + /* [WARN] 进正文(本仓无 WARN 级;用 ERR 级会带 [ERR] 误触 @test 禁用标记) */ + DBG_INFO("[WARN] wave bench autocap ON: 1 seg / %u ms, auto-stop %u s (BENCH BUILD ONLY)", + (unsigned int)WAVE_BENCH_PERIOD_MS, + (unsigned int)(WAVE_BENCH_MAX_MS / 1000u)); + } + } else if ((uint32_t)(now - s_wave_bench_t0) >= WAVE_BENCH_MAX_MS) { + s_wave_bench_off = 1u; + DBG_INFO("wave bench autocap STOP after %u s", + (unsigned int)(WAVE_BENCH_MAX_MS / 1000u)); + } else if ((uint32_t)(now - s_wave_bench_next) >= WAVE_BENCH_PERIOD_MS) { + s_wave_bench_next = now + WAVE_BENCH_PERIOD_MS; + (void)wave_ring_request((uint8_t)WAVE_TRIG_BENCH, 0u); + } else { + /* 本拍未到周期点:无事可做(比较用无符号差值,回绕安全) */ + } + } +#endif + /* 3) 首帧码值上报:**写者就绪后**才落日志。写者未注册时 con_append 走 * 无锁直发路径,与 defaultTask 的存储初始化日志会字节级交错 * (2026-10-10 台架实测);此时挂起上报,就绪后补报最近一帧。 @@ -610,6 +750,46 @@ void adc_task_func(void *arg) { (long)(ms.freq_hz * 100.0f), (unsigned)ms.freq_valid); } + + /* 录波环周期行(docs/子系统设计/故障录波设计说明.md §9 逐字模板, + * 字段名与顺序**不许动**——PC/台架脚本按行名 grep)。全链路只加这一条 + * 周期行(每条 DBG ≈90 B Flash + 216 B 栈,§9 明令不加第二条): + * seg = 累计成功 ACK 的段数(wave_ring_stat.seg_total,不是轮转号) + * armed= 1 表示此刻有段在飞(FREEZE/SEALED;IDLE/COOL 为 0—— + * COOL 只挡新触发不录新段,画进 armed 会让台架误读) + * src = 触发源 M 手动 / P 保护 / T bench 定时,- = 本 boot 未触发过 + * err = 落盘失败 + SEALED 超时自弃(环内 err_cnt 收口,落盘道不数 + * 第二份账);ovf = ACK 前重复触发与冷却期丢弃数; + * drop = 冻结窗口丢弃的录波点数(单位=点) + * 判读:bench 自抓一轮后 seg_total 应增长;err 涨而 seg 不涨 = + * 落盘道在丢段,查 defaultTask 侧 [WAVE] 失败行 */ + { + wave_ring_stat_t wst; + + (void)wave_ring_get_stat(&wst); + { + const char *sc; + + if (wst.trig_src == (uint8_t)WAVE_TRIG_MANUAL) { + sc = "M"; + } else if (wst.trig_src == (uint8_t)WAVE_TRIG_PROTECT) { + sc = "P"; + } else if (wst.trig_src == (uint8_t)WAVE_TRIG_BENCH) { + sc = "T"; + } else { + sc = "-"; + } + DBG_INFO("wave: seg=%lu armed=%u src=%s pts=%u err=%lu ovf=%lu drop=%lu", + (unsigned long)wst.seg_total, + (unsigned)((wst.state == (uint8_t)WAVE_RING_ST_FREEZE) || + (wst.state == (uint8_t)WAVE_RING_ST_SEALED)) ? 1u : 0u, + sc, + (unsigned int)WAVE_RING_PTS, + (unsigned long)wst.err_cnt, + (unsigned long)wst.ovf_cnt, + (unsigned long)wst.drop_cnt); + } + } } report_at = now + ADC_TASK_REPORT_MS; } diff --git a/App/task/default_task.c b/App/task/default_task.c index 9e9498dd..7d04413e 100644 --- a/App/task/default_task.c +++ b/App/task/default_task.c @@ -26,6 +26,18 @@ * v1.3 2026-10-25 DTU 项目组 写回缓存空闲 flush 接线:主循环 syslog 排空 * 之后调 vol_idle_sync_poll()(卷空闲周期 CTRL_SYNC 兜底, * 模块 = App/fs/vol_idle_sync.[ch],道F 批 2) + * v1.4 2026-10-29 DTU 项目组 故障录波落盘道(task-2):vol_idle_sync_poll 之后 + * 新增 wave_store_drain()——一次心跳整段搬完(lead 裁③: + * 一次 try 锁内 pack_next 256 B 分片 + 完整 open...close), + * 持锁耗时 >50 ms 补一条 WARN、峰值 ms 记本层静态; + * 轮转号自持(启动扫 /wave 重建,成功 ACK 才推进,lead 裁(b)); + * 失败 = 先 unlink 半成品再 write_done(-1)(lead 裁①); + * 卷未就绪 / 环未封存静默跳过(§5.1 所有权 + §9 一条线约束) + * v1.5 2026-10-29 DTU 项目组 Lead 复核补第四道并发陷阱(本道与 adcTask 的抢占窗): + * 搬段中途 adcTask 可能正好走到 SEALED 滞留自弃(2 s)甚至 + * 已封出下一段——CRC 是对"读到的字节"算的,半新半旧的段照样 + * 自洽,只能靠写完关档后复检环侧身份(握手量 + 段首 t0/帧序) + * 判废:判废即 unlink、不 ACK、号不推进,失败按既有单条留痕 */ #include "default_task.h" @@ -53,6 +65,8 @@ #include "task_prof.h" /* 各任务最坏耗时上报 */ #include "vol_lock.h" #include "vol_idle_sync.h" /* 写回缓存空闲 flush 兜底(App/fs/vol_idle_sync.h) */ +#include "wave_ring.h" /* 录波环消费侧:sealed/sealed_meta/read_cb/write_done(task-2) */ +#include "wave_store.h" /* 段打包游标 + 段号轮转纯函数(落盘道本体,task-2) */ #include "task_handles.h" /* 任务句柄与属性块的引用契约(定义处仍是 Src/freertos.c) */ /* Private defines -----------------------------------------------------------*/ @@ -72,6 +86,14 @@ * (自检已上报、卷保留、不重格式化),等下次上电重新 resume */ #define STORAGE_BRINGUP_RETRIES 3u +/* 录波段落盘持锁耗时告警线(lead 裁③i):一次心跳整段 5250 B(约 21 次 256 B + * f_write),设计 §4.2 预估落盘窗「几十 ms」;越过本线说明本段在占用 defaultTask + * 喂狗链前的最慢一段,按事件补一条 WARN(不是周期行),并把历史峰值留在本层 + * 静态里供收工报告核喂狗余量(vol_lock.h 铁律「等锁即毁喂狗链」的前例在案) */ +#define WAVE_DRAIN_HELD_WARN_MS 50u +/* 搬运缓冲(§7 的 256 B;wave_store_pack_next 头阶段要求 cap >= 128,lead 转告(d)) */ +#define WAVE_DRAIN_CHUNK_B 256u + /* Private typedef -------------------------------------------------------------*/ typedef struct { const char *p_name; @@ -97,6 +119,7 @@ static void stack_probe_poll(void); #if !defined(TEST_SUITE_STORAGE) static void default_task_net_cfg_save_notify(void); static FRESULT storage_bring_up(FATFS *fs); +static void wave_store_drain(void); #endif /* @@ -301,6 +324,10 @@ void default_task_func(void *argument) { * 一次 f_close 同型的同步段。放在 drain 之后:本拍若写过日志,close * 已同步过,这里随即 clean 短路——漏网脏页的驻留窗由周期封顶 */ vol_idle_sync_poll(); + /* 录波段落盘(task-2):卷空闲 + 环已封存才搬,就绪门/try 锁全在函数内; + * 一次心跳最多一段(lead 裁③)。放在 idle sync 之后:本拍若刚写过录波段, + * 下一拍空闲 flush 会兜住残留脏页,语义与 syslog 排空同位 */ + wave_store_drain(); #endif /* 栈水位观测:首次 5s 后报告,此后每 2s 采样、出现更深峰值才打印 */ task_wd_seg(TASK_WD_SEG_STACK); @@ -565,6 +592,248 @@ static FRESULT storage_bring_up(FATFS *fs) { } #endif /* !TEST_SUITE_STORAGE */ +/* + * 函数功能:故障录波落盘道——把已封存的 wave_ring 段整段搬进 /wave(task-2) + * 入口参数:无 void + * 返回值:无 void + * 限定条件:仅 defaultTask 主循环调用(vol_idle_sync_poll 之后);内部绝不阻塞—— + * try 锁 + 有界段长(5250 B / 256 B ≈ 21 次 f_write,设计 §4.2 预估几十 ms), + * 绝不把喂狗点做成等待态(vol_lock.h 使用约定:defaultTask 一律 try) + * 函数说明:所有权按 docs/子系统设计/故障录波设计说明.md §5.1——环的生产者是 + * adcTask,本道是唯一落盘消费者;跨任务握手只认 wave_ring_sealed()。 + * 本道承担五条口径: + * ① 打开一律 FA_WRITE|FA_OPEN_APPEND:本移植 FA_OPEN_APPEND 不含 FA_WRITE + * (docs/架构设计/STM32F4-Base存储架构说明.md §10.1 陷阱一),少写位 + * f_write 恒 FR_DENIED; + * ② 每次 f_write 比对写回字节数:本移植簇分配失败返 FR_OK+短写无错误码 + * (同处陷阱二),短写一律判失败; + * ③ 轮转号由本道自持(lead 转告(b)):ring.seg_idx 只是本 boot 本地计数, + * 盘上权威下标 = 首次持锁扫 /wave 用 wave_store_idx_from_name() 重建 + * (取已存在最大号 +1,空目录 = 0,陌生名字不入账),写成功才 + * wave_store_next_idx() 推进;失败不推进、重试同号; + * ④ 成败都必须 wave_ring_write_done() ACK——ACK 前环一直冻结;err 账 + * 由 ring 的 err_cnt 单点收口(§9),本道不数第二份(ACK 前环已 + * 超时自弃的那次例外:skip 掉 ACK,不去补第二笔账); + * ⑤ 锁跨越一次完整 open...close(vol_lock.h 铁律),持锁耗时超 + * WAVE_DRAIN_HELD_WARN_MS 按事件补一条 WARN(§9 唯一周期行不在本道), + * 历史峰值 s_wheld_ms_max 供核 defaultTask 喂狗余量(lead 裁③)。 + * 失败处置(lead 裁①):开过文件才 unlink 半成品(未开成的失败不动盘, + * 绝不拿未初始化的路径误删旧段);unlink 失败照旧 ACK(-1)——最坏留 + * 一个坏段文件,PC 侧被 CRC 判废,不会画成证据。 + * 静默跳过(不刷屏):卷未就绪 / 环未封存 / try 锁没到(下轮再来, + * 环侧 2 s 超时自弃是兜底账,本道不为此打日志)。 + * (上面五条口径就是本函数功能「把已封存段整段搬进 /wave」的逐条展开, + * 细则出处 docs/子系统设计/故障录波设计说明.md §5.1 与 §10) + */ +static void wave_store_drain(void) { + static FIL s_wfil; /* static:FF_FS_TINY=0 时 FIL ~528 B, + * 绝不上栈(FS_VOL_PROBE 同一理由) */ + static uint8_t s_wbuf[WAVE_DRAIN_CHUNK_B]; + static wave_pack_ctx_t s_wpack; + static wave_seg_meta_t s_wmeta; + static char s_wpath[WAVE_ST_PATH_MAX]; + static DIR s_wdir; + static FILINFO s_wfno; + static uint8_t s_wnext_idx; /* 本道自持的下一个段号 0..99 */ + static uint8_t s_wscan_done; /* 1 = 本 boot 已扫过 /wave */ + static uint8_t s_wdir_ready; /* 1 = /wave 已建或已存在 */ + static uint8_t s_wcrc_state; /* 0 未验 1 过 2 败(败=永久自弃,不烧垃圾段) */ + static uint32_t s_wheld_ms_max; /* 持锁搬段历史峰值 ms(收工报告核余量用) */ + uint32_t t0_ms; + uint32_t held_ms; + uint32_t written = 0u; + uint32_t len = 0u; + uint32_t iter; + uint8_t opened = 0u; + uint8_t skip_ack = 0u; /* 1 = 环已自行了断本段,本道不 ACK 不补账 */ + wave_ring_stat_t wst2; /* 关档后复检环侧身份用(v1.5) */ + const char *why = NULL; + FRESULT fr; + UINT bw; + int rc; + + /* 就绪门(同 vol_idle_sync_poll 的四级门风格) */ + if (sys_evtHandle == NULL) { + return; + } + if ((osEventFlagsGet(sys_evtHandle) & SYS_EVT_STORAGE_READY) == 0u) { + return; + } + /* 封存门:只搬已封存段(唯一跨任务握手量,§5.1) */ + if (wave_ring_sealed() == 0u) { + return; + } + /* 空闲门:try 锁;卷被 syslog/配置/FTP 占住 → 本轮放弃,不排队不打日志 */ + if (vol_lock_wait(0u) == 0u) { + return; + } + t0_ms = (uint32_t)xTaskGetTickCount(); + + /* CRC 锚点自检每 boot 一次(落盘前失败 = 整族失败,永久自弃免得白烧写次数) */ + if (s_wcrc_state == 0u) { + s_wcrc_state = (wave_store_crc_selftest() == WAVE_ST_OK) ? 1u : 2u; + } + if (s_wcrc_state == 2u) { + why = "crc selftest FAIL"; + } + if ((why == NULL) && (s_wdir_ready == 0u)) { + fr = f_mkdir(WAVE_ST_DIR); + if ((fr == FR_OK) || (fr == FR_EXIST)) { + s_wdir_ready = 1u; + } else { + why = "mkdir /wave"; + } + } + if ((why == NULL) && (s_wscan_done == 0u)) { + FRESULT rr; + + fr = f_opendir(&s_wdir, WAVE_ST_DIR); + if (fr == FR_OK) { + uint8_t found = 0u; + uint8_t mx = 0u; + + for (;;) { + uint8_t ix; + + rr = f_readdir(&s_wdir, &s_wfno); + if ((rr != FR_OK) || (s_wfno.fname[0] == 0)) { + break; + } + if (wave_store_idx_from_name(s_wfno.fname, &ix) == WAVE_ST_OK) { + if ((found == 0u) || (ix > mx)) { + mx = ix; + } + found = 1u; + } + } + (void)f_closedir(&s_wdir); + if (rr == FR_OK) { + /* max+1 即下一写号;回绕号复用由 PC 侧 t0/frame_seq 对账兜底, + * 目录里没有的事实不去猜更多(陌生名字一律不入账) */ + s_wnext_idx = (found != 0u) ? wave_store_next_idx(mx) : 0u; + s_wscan_done = 1u; + } else { + why = "readdir /wave"; + } + } else { + why = "opendir /wave"; + } + } + if ((why == NULL) && (s_wscan_done != 0u)) { + if (wave_ring_sealed_meta(&s_wmeta) != WAVE_ST_OK) { + /* sealed 检查与此读之间环 2 s 超时自弃(err 已由环计过): + * 本道不 ACK、不补账、不打日志,放锁走人 */ + skip_ack = 1u; + } + } + if ((why == NULL) && (skip_ack == 0u)) { + /* lead 转告(e):E_FORMAT/E_SIZE = 坏段头,一律当落盘失败丢段计 err, + * 不当参数错吞掉 */ + rc = wave_store_pack_start(&s_wpack, &s_wmeta, wave_ring_read_cb, NULL); + if (rc != WAVE_ST_OK) { + why = "pack_start"; + } + } + if ((why == NULL) && (skip_ack == 0u)) { + if (wave_store_seg_path(s_wpath, (uint32_t)sizeof(s_wpath), + s_wnext_idx) <= 0) { + why = "seg_path"; + } else { + fr = f_open(&s_wfil, s_wpath, FA_WRITE | FA_OPEN_APPEND); + if (fr != FR_OK) { + why = "f_open"; + } else { + opened = 1u; + } + } + } + iter = 0u; + while ((why == NULL) && (opened != 0u)) { + rc = wave_store_pack_next(&s_wpack, s_wbuf, + (uint32_t)sizeof(s_wbuf), &len); + if ((rc != WAVE_ST_OK) && (rc != WAVE_ST_DONE)) { + why = "pack_next"; + break; + } + if (len != 0u) { + bw = 0u; + fr = f_write(&s_wfil, s_wbuf, (UINT)len, &bw); + if ((fr != FR_OK) || (bw != (UINT)len)) { + why = "f_write short"; /* ②:FR_OK+短写 = 本移植独有失败形态 */ + break; + } + written += len; + } + if (rc == WAVE_ST_DONE) { + break; + } + if (++iter > 32u) { /* 5250/256 = 21 片;游标卡死时的有界保险 */ + why = "pack stall"; + break; + } + } + if (opened != 0u) { + fr = f_close(&s_wfil); + if ((why == NULL) && (skip_ack == 0u) && (fr != FR_OK)) { + why = "f_close"; + } + } + if ((why == NULL) && (skip_ack == 0u) && (opened != 0u)) { + if (written != wave_store_pack_total(&s_wpack)) { + why = "len mismatch"; /* 长度等式(§6.3):半截文件骗不过这条 */ + } + } + /* v1.5(lead 复核):adcTask 优先级高于本道,搬段中途它可能正好把滞留 2 s 的本段 + * 自弃(甚至已经封出下一段)。数据区的 CRC 是对"本道读到的字节"算的,半新半旧的 + * 段一样自洽 → 关档后必须复检:握手量没了、或段首身份不是本段,一律判废。 + * 判废走既有失败路:unlink 本道开过的文件 + write_done(-1)(此时多半回 E_STATE, + * 环侧的 err 已由自弃计过,这里不重复补账)+ 段号不推进(同号重写覆盖)。 */ + if ((why == NULL) && (skip_ack == 0u)) { + if ((wave_ring_sealed() == 0u) || (wave_ring_get_stat(&wst2) != WAVE_ST_OK) + || (wst2.last_t0_ms != s_wmeta.t0_ms) || (wst2.last_seq != s_wmeta.frame_seq)) { + why = "ring abandoned"; + } + } + if (skip_ack == 0u) { + /* 失败留痕边沿锁存(搬进本块,lead 裁方案 A): + * ① 它是 static,初值只在编译期发生一次、不是每次进块重置; + * ② 它是"上一拍是否处于失败态"的边沿锁存,所以必须跨调用存活; + * ③ 单独放这里而不在函数入口那 12 连排台账里,因为 variableScope + * 检查要求把局部量收进真正覆盖全部使用点的最小块(本仓刻意开了 style)。 */ + static uint8_t s_wfail_latch; /* 一个 episode 只留一条痕 */ + + if (why == NULL) { + (void)wave_ring_write_done(0); + s_wnext_idx = wave_store_next_idx(s_wnext_idx); /* 成功才推进(转告(b)) */ + s_wfail_latch = 0u; + } else { + if (opened != 0u) { + (void)f_unlink(s_wpath); /* 只清本道开过的文件;unlink 失败照旧 ACK */ + } + (void)wave_ring_write_done(-1); + if (s_wfail_latch == 0u) { + s_wfail_latch = 1u; /* 持续失败不刷屏;成功一次重新武装 */ + DBG_ERROR("[WAVE] seg drop: %s (idx=%u written=%lu)", why, + (unsigned)s_wnext_idx, (unsigned long)written); + } + } + } + + /* 持锁耗时台账(lead 裁③i/ii):超线按事件补一条 WARN(原先这里静默占锁, + * 就是「等锁即毁喂狗链」前例的同族黑洞);峰值进静态供收工报告 */ + held_ms = (uint32_t)xTaskGetTickCount() - t0_ms; + if (held_ms > s_wheld_ms_max) { + s_wheld_ms_max = held_ms; + } + if ((skip_ack == 0u) && (held_ms > (uint32_t)WAVE_DRAIN_HELD_WARN_MS)) { + /* 本仓 dbg_log 无 WARN 级(宏只有 ERROR/INFO/DEBUG,ERR 级自动带 [ERR] = @test + * 禁用标记):[WARN] 按 console_log.c 先例进正文,慢不是错(真失败另有 seg drop) */ + DBG_INFO("[WAVE][WARN] seg held %lu ms (idx=%u)", + (unsigned long)held_ms, (unsigned)s_wnext_idx); + } + vol_lock_release(); /* 成败/跳过都放锁:锁泄漏比丢一段录波重一百倍 */ +} + /* * 函数功能:"保存网络配置"请求通知(注册给 net_cfg 的保存交接; * 2026-09-25 自 Src/freertos.c 迁出,逻辑零改动) diff --git a/App/task/protect_task.c b/App/task/protect_task.c index d34e85d7..591f8974 100644 --- a/App/task/protect_task.c +++ b/App/task/protect_task.c @@ -45,6 +45,12 @@ 行为不变:判定式、调用位置(采样恢复分支内、巡检/压板 之前)、边沿日志与 SOE 分型全部照旧;s_prev_cal_lock 仍只作边沿日志用,**不是**判据缓存 + * v1.4 2026-10-29 DTU 项目组 故障录波触发接线(task-2,lead 裁②): + * s_log_record_edge() 的 picked 上升沿分支补一次 + * wave_ring_request()——与 SYSLOG_START 同一事件点 + * (设计 §4.1「同源,避免两处各自判定」),判据本体 + * (prot_elm_run/判定逻辑)零改动;一拍只请求一次, + * elm = 本拍最低位上升元件 */ #include "protect_task.h" @@ -62,6 +68,7 @@ #include "prot_set.h" #include "soe_store.h" /* B9:动作/复归边沿 → SOE 投递(零 FS,队列异步) */ #include "sys_log.h" /* task-10:事故 / 保护告警 / 保护启动 三类文本记录 */ +#include "wave_ring.h" /* task-2:picked 上升沿 → 录波请求(与 SYSLOG_START 同源同点) */ #include "task_watchdog.h" #include "tpafe5160.h" /* tpafe5160_frame_seq():帧序号(BUSY EXTI 写) */ @@ -151,8 +158,24 @@ static void s_log_active_edge(uint32_t active) { static void s_log_record_edge(uint32_t active, uint32_t pick) { uint32_t p_chg = pick ^ s_prev_pick; uint32_t a_chg = active ^ s_prev_active; + const uint32_t p_rise = p_chg & pick; /* picked 0->1 的元件位集合 */ uint32_t i; + /* task-2 补(第 3 类边沿的用途):picked 上升沿同点请求录波。一拍只调一次—— + * 多元件同拍启动录同一段波就够(环本就不重入,后续请求由 ring 计 ovf,不当 + * 失败);元件号取最低位上升元件(位号最小 = 表中靠前,速断段优先于定时限段)。 + * 返回值刻意忽略:本函数在 protectTask 的最坏栈链上(B9 写进 DoD 的硬线), + * 不为此加日志;err/ovf 两本账全在串口 wave: 周期行与环 stat 里 + * (docs/子系统设计/故障录波设计说明.md §9)。 */ + if (p_rise != 0u) { + uint32_t e = 0u; + + while (((p_rise >> e) & 1u) == 0u) { + e++; /* p_rise != 0 → 必在 32 位内命中,无越界 */ + } + (void)wave_ring_request((uint8_t)WAVE_TRIG_PROTECT, (uint8_t)e); + } + for (i = 0u; (i < (uint32_t)PE_COUNT) && (((p_chg | a_chg) != 0u)); i++) { const uint32_t bit = 1u << i; diff --git a/App/task/ui_task.c b/App/task/ui_task.c index 4ac1ca29..d0fa91fc 100644 --- a/App/task/ui_task.c +++ b/App/task/ui_task.c @@ -57,6 +57,16 @@ #include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */ #include "net_latency_test.h" /* M3 探针:ui_loop(生产循环实测周期) */ #include "task_watchdog.h" +/* 故障录波回放道(UI 道):录波注入表执行侧需要的四份头。 + * wave_ring.h 给触发投递(request 任意上下文可调、非阻塞); + * wave_store.h 给段格式解析与定点 mV 换算(唯一口径,页面不复述算式); + * vol_lock.h + ff.h:/wave 只读遍历按 soe_store.c 读者同款纪律—— + * try 锁非阻塞(卷忙本轮按「无段」处理,uiTask 绘制节拍绝不等待)、 + * 锁跨完整文件操作序列、小窗口分次读、整段 5250B 绝不驻留 RAM */ +#include "ff.h" +#include "vol_lock.h" +#include "wave_ring.h" +#include "wave_store.h" #include "task_prof.h" /* P2 常开探针:循环周期 / 工作耗时 / 推流往返 */ /* @@ -355,6 +365,463 @@ static int ui_task_soe_get(uint32_t idx, ui_w_soe_rec_t *p_out) { static const ui_soe_reader_t s_soe_reader = { ui_task_soe_count, ui_task_soe_get }; +/* ═══════════════ 故障录波注入表执行侧(UI 道,2026-10-29)═══════════════ + * 纪律逐条对齐 soe_store.c 读者(vol_lock try / 锁跨完整文件序列 / 小窗口): + * - 卷忙(vol_lock_wait(0) 返 0)一律按「本轮无段」处理——uiTask 是 + * LVGL + 推流节拍的所有者,等锁 = 停摆,宁可空一帧也不阻塞(lead 裁定); + * - 段完整性判定只信 wave_store 自己的判据(尺寸等式 / magic+ver / 双 CRC / + * sem==index),本胶水不读 avail/flags 做二次裁断(§6.3 与 2026-10-29 + * 「VALID+CLIPPED 非矛盾」裁定同源:可用性不是完整性); + * - 整段 5250B 不驻留:256B 静态窗口分次读,头区一次、数据区流式过 CRC。 + * uiTask 阻塞上界(IWDG 800ms 窗、uiTask 在 required_mask 内;写法对齐上方 + * 维护通道那段上界,同文件先例): + * - 第零项是「本块不等待」:vol_lock_wait(0) 拿不到锁即按「本轮无段」 + * 返回——不等锁 = 零阻塞,它本身就是上界的一项,不是只保心跳判读; + * - 一次渲染实际发生的 I/O(进回放页首绘、或按键后重绘那帧)三件叠加: + * ① 表重扫 = 1 次 f_opendir + 至多 WAVE_ST_SEG_MAX(100)次 f_readdir, + * 收尾是 100 元插入排序(纯内存); + * ② 段头装载 = 仅未缓存段才发生:1 次 f_open + 头区 1 次 128B 读 + + * head_parse 内的头 CRC + 数据区 20 次 256B 流式读 + 尾 2B 一次 + * = 最坏 22 次小读;5120B 逐字节 CRC 与 sem 表校验是纯计算, + * 168MHz 下微秒到毫秒量级;缓存命中零卷操作; + * ③ 窗口装载 = 1 次 f_open + 1 次 f_lseek + 1 次 80B 读(40 点一窗), + * 解码 40 点 + 逐点 code_to_milli 峰值扫描 40 次(int64 乘除, + * 微秒级)——不是全段 8×320 扫描,回放页一屏只碰一个通道一窗; + * - 纯内存不碰卷的路径(评审据此看最坏的落点):manual_req 只投一个 + * 请求字节;已缓存段的上下切道、左右切段的段号表环游;空态与坏态 + * 直接返回。所以最坏只落在「第一次进回放页 / 第一次切到未缓存段」 + * 这类一次性动作上,同段内连续按键不重复付 ②。 + * 没有实测数,上面是与落盘道同一把尺的**推算**:落盘道的告警线是 + * 50 ms / 21 次写,读路径无簇分配、不写 FAT 元数据,只会更便宜,故按 + * 同量级不高于它估——一次渲染最坏一窗 ≈ 段表 + 一段全验 + 一窗,仍远 + * 小于 800ms。实测归台架,仪器就用本文件已经在跑的 task_prof 单轮 + * 工作耗时探针,不必新加埋点。 + * 段序口径(V0.1 已知限制,终报点名):目录重建后按段号数值排序环游, + * 盘上写满 100 段翻转后「数值序 = 时间序」不再严格成立——每屏顶行都带 + * 段号自证身份,查事故先看号再看序;翻转窗口排序升级留给台架结论。 + */ +static uint8_t s_wave_idx[WAVE_ST_SEG_MAX]; /* 盘上现存段号升序表 */ +static uint8_t s_wave_cnt; /* 表内条数 */ +static uint8_t s_wave_scanned; /* 表是否有效(manual_req 后作废) */ +static uint8_t s_wave_cached_idx; /* 段头缓存属于哪一段 */ +static uint8_t s_wave_cache_ok; /* 该段已验完整(头+数据 CRC) */ +static wave_seg_meta_t s_wave_meta; /* 已验段的头(172B,唯一驻留) */ +static uint8_t s_wave_buf[256u]; /* 读窗:数据区流式 256B/次(lead 裁定 + * 2026-10-29 第 1 条·第一条:128B 抬 256B, + * f_read 次数对半砍,RAM 只多 128B; + * 不选 100 位「已验」位图——位图的作废纪律 + * 一旦漏做,「CRC 不符必须拒收」这条台架 + * 判据对旧段号会静默失效,多 128B 没这个面)*/ + +/* + * 函数功能:重扫 /wave 目录,把现存段号收集成升序表 + * 入口参数:无 + * 返回值:0 - 扫描完成(条数见 s_wave_cnt);-1 - 卷忙本轮跳过 int + * 限定条件:uiTask;一次 f_opendir + 有界 f_readdir,全程锁内 + * 函数说明:目录不存在(从未录过波)与目录空都归「0 段」,不是错误; + * 名字不合形态的文件(E_NAME)静默跳过——那是别的道写进来的 + * 东西,回放页不替它背书也不报警 + */ +static int ui_task_wave_scan(void) { + static DIR dir; /* static:同 FIL 理由,压栈深 */ + FILINFO finfo; + uint8_t idx; + uint8_t i; + + if (vol_lock_wait(0u) == 0u) { + return -1; + } + s_wave_cnt = 0u; + if (f_opendir(&dir, WAVE_ST_DIR) == FR_OK) { + for (;;) { + if (f_readdir(&dir, &finfo) != FR_OK || finfo.fname[0] == '\0') { + break; + } + if (finfo.fattrib & AM_DIR) { + continue; + } + if (wave_store_idx_from_name(finfo.fname, &idx) == 0) { + if (s_wave_cnt < WAVE_ST_SEG_MAX) { + s_wave_idx[s_wave_cnt++] = idx; + } + } + } + f_closedir(&dir); + } + vol_lock_release(); + /* 插入排序:条数上限 100,不引 qsort 的 stdlib 依赖 */ + for (i = 1u; i < s_wave_cnt; i++) { + uint8_t j; + uint8_t tmp; + + tmp = s_wave_idx[i]; + j = i; + while ((j > 0u) && (s_wave_idx[j - 1u] > tmp)) { + s_wave_idx[j] = s_wave_idx[j - 1u]; + j--; + } + s_wave_idx[j] = tmp; + } + s_wave_scanned = 1u; + return 0; +} + +/* + * 函数功能:确保段表可用(必要时重扫) + * 入口参数:无 + * 返回值:0 - 表可用;-1 - 卷忙 int + * 限定条件:uiTask 绘制/按键链 + */ +static int ui_task_wave_table(void) { + if (s_wave_scanned == 0u) { + return ui_task_wave_scan(); + } + return 0; +} + +/* + * 函数功能:表内查段号下标 + * 入口参数:a_idx - 段号;a_pos - 出参下标 + * 返回值:1 - 命中;0 - 不在表内 uint8_t + * 限定条件:表已可用(先过 ui_task_wave_table) + */ +static uint8_t ui_task_wave_find(uint8_t a_idx, uint8_t *a_pos) { + uint8_t i; + + for (i = 0u; i < s_wave_cnt; i++) { + if (s_wave_idx[i] == a_idx) { + *a_pos = i; + return 1u; + } + } + return 0u; +} + +/* + * 函数功能:读段并做完整性判定(头 CRC 由 head_parse 判,数据 CRC 流式判), + * 成功则把段头留在 s_wave_meta 供 seg_win 免重读 + * 入口参数:a_idx - 段号 + * 返回值:UI_WAVE_OK / UI_WAVE_NONE(卷忙/文件没了)/ UI_WAVE_BAD(判废) int + * 限定条件:uiTask;锁跨 open→close 完整序列;头 128B 一次、数据区 256B + * 分窗流式(共约 1 + 20 次 f_read),整段不驻留 + * 耗时账(lead 裁定 2026-10-29 第 1 条·第三条并存):未缓存段的完整性 + * 判定最坏约 21 次小读 + 5120B 逐位 CRC,全部在卷锁内、uiTask 上; + * IWDG 800ms 窗内余量几何**实测毫秒归台架**(终报点名待测项)。 + * 缓存只留一段:左右切段 A→B→A 会把 B 的校验再来一遍——接受该 + * 成本的代价换「CRC 不符必须拒收」永远现算,不做跨键存的位图 + * 函数说明:缓存命中(同段且已验)直接返 OK——左右切段回到上一段、上下切 + * 通道都靠它把重复的 5250B 全扫压成一次;文件读中途消失(被别的 + * 写者轮转掉)按 NONE 处理,页面下轮自然重扫 + */ +static int ui_task_wave_load_head(uint8_t a_idx) { + static FIL f; /* static:FIL = 528B,理由同 soe_store_read */ + char path[WAVE_ST_PATH_MAX]; /* 容量 = WAVE_ST_PATH_MAX(17 字节口径, + * 自己声明路径缓冲不得写死 16) */ + FRESULT fr; + UINT br = 0u; + int rc; + + if ((s_wave_cache_ok != 0u) && (s_wave_cached_idx == a_idx)) { + return UI_WAVE_OK; + } + if (wave_store_seg_path(path, (uint32_t)sizeof(path), a_idx) != 0) { + return UI_WAVE_NONE; + } + if (vol_lock_wait(0u) == 0u) { + return UI_WAVE_NONE; /* 卷忙 = 本轮按无段处理(不阻塞 uiTask) */ + } + rc = UI_WAVE_NONE; + if (f_open(&f, path, FA_READ) == FR_OK) { + uint32_t size; + uint32_t total; + uint32_t n; + + size = (uint32_t)f_size(&f); + total = 0u; + rc = UI_WAVE_BAD; + /* 第一步:头区(首窗 128B = 当前形态整头);head_parse 自带判废 */ + n = (size < (uint32_t)WAVE_ST_HEAD_SZ_CUR) ? size + : (uint32_t)WAVE_ST_HEAD_SZ_CUR; + fr = f_read(&f, s_wave_buf, n, &br); + if ((fr == FR_OK) && (br == n)) { + if (wave_store_head_parse(s_wave_buf, n, &s_wave_meta) == 0) { + total = wave_store_seg_size(&s_wave_meta); + } + } + /* 第二步:尺寸等式(零长/长度与头部字段矛盾 = 整段判废,§6.3) */ + if ((total != 0u) && (size == total)) { + uint32_t off; + uint16_t crc; + uint8_t tail[2]; + + /* 第三步:数据区流式 CRC(128B 窗;CRC 覆盖不含尾两字节自身) */ + crc = WAVE_ST_CRC_INIT; + off = wave_store_head_bytes(s_wave_meta.ch_count); + rc = UI_WAVE_BAD; + while ((off + 2u) < size) { + n = (size - 2u - off < (uint32_t)sizeof(s_wave_buf)) + ? (size - 2u - off) : (uint32_t)sizeof(s_wave_buf); + fr = f_read(&f, s_wave_buf, n, &br); + if ((fr != FR_OK) || (br != n)) { + rc = UI_WAVE_BAD; + break; + } + crc = wave_store_crc_update(crc, s_wave_buf, n); + off += n; + } + /* 第四步:读回存储值比对(低字节在前) */ + if ((rc == UI_WAVE_BAD) && (f_lseek(&f, size - 2u) == FR_OK) && + (f_read(&f, tail, 2u, &br) == FR_OK) && (br == 2u)) { + if (crc == (uint16_t)(tail[0] | ((uint16_t)tail[1] << 8))) { + rc = UI_WAVE_OK; + } + } + } else if (total == 0u) { + rc = UI_WAVE_BAD; /* seg_size 返 0 = 头字段自相矛盾,判废 */ + } else { + rc = UI_WAVE_BAD; /* 长度与头部字段不符,判废 */ + } + (void)f_close(&f); + } + vol_lock_release(); + if (rc == UI_WAVE_OK) { + s_wave_cached_idx = a_idx; + s_wave_cache_ok = 1u; + } else { + s_wave_cache_ok = 0u; + } + return rc; +} + +/* + * 函数功能:录波触发页唯一写出口——投一次手动触发请求 + * 入口参数:无 + * 返回值:0 - 已受理;非零 - 拒收(未 IDLE/冷却中/参数),页面显示 ERR int + * 限定条件:uiTask(request 本身任意上下文可调,这里只是转发不等待) + * 函数说明:受理 = ring 进冻结窗,成段最快也在约一秒后由默认任务落盘; + * 顺手作废段表缓存,页面下次取数会看到新段 + */ +static int ui_task_wave_manual_req(void) { + int r = wave_ring_request((uint8_t)WAVE_TRIG_MANUAL, 0u); + + if (r == 0) { + s_wave_scanned = 0u; + s_wave_cache_ok = 0u; /* lead 裁定 2026-10-29 第 3 条:段表与段头缓存 + * 一起作废——同名文件会被轮转重写,旧头字段 + * (pts/pre/k/b/逐路 avail)不作废就会顶着新段 + * 的号用旧尺子。单槽缓存本身 = 已知限制: + * 只挡「本段验过不再重验」,多段来回切会重验 */ + } + return r; +} + +/* + * 函数功能:取盘上「数值最大段号」(回放页开屏落点) + * 入口参数:a_idx - 出参段号 + * 返回值:UI_WAVE_OK / NONE(无段或卷忙本轮)/ BAD 本件不产(表只含现存文件) + * 函数说明:口径 = 数值最大,**不是时间最新**(lead 裁定 2026-10-29): + * 回绕后数值序不等于时间序,页顶显示的段号即自证;真做时间序要 + * 逐段开文件读 frame_seq/t0,一百次开档不在回放页的账上,本波不做 + */ +static int ui_task_wave_seg_first(uint8_t *a_idx) { + if (ui_task_wave_table() != 0) { + return UI_WAVE_NONE; /* 卷忙 = 本轮无段(同一条不阻塞纪律) */ + } + if (s_wave_cnt == 0u) { + return UI_WAVE_NONE; + } + *a_idx = s_wave_idx[s_wave_cnt - 1u]; + return UI_WAVE_OK; +} + +/* + * 函数功能:环游取邻段(a_dir:-1 更早 / +1 更新;段号数值序环回) + * 入口参数:a_cur - 当前段号;a_out - 出参邻段段号 + * 返回值:UI_WAVE_OK / NONE(表不可用或已无邻段可去) int + * 限定条件:uiTask;当前段已被轮转覆盖时先重扫一次再定位 + */ +static int ui_task_wave_seg_around(uint8_t a_cur, int8_t a_dir, uint8_t *a_out) { + uint8_t pos = 0u; + + if (ui_task_wave_table() != 0) { + return UI_WAVE_NONE; + } + if (ui_task_wave_find(a_cur, &pos) == 0u) { + if ((ui_task_wave_scan() != 0) || (s_wave_cnt == 0u) || + (ui_task_wave_find(a_cur, &pos) == 0u)) { + /* 当前段没了(翻转覆盖):退回首段,画面由调用侧重载 */ + if (s_wave_cnt == 0u) { + return UI_WAVE_NONE; + } + *a_out = s_wave_idx[0u]; + return UI_WAVE_OK; + } + } + if (s_wave_cnt == 1u) { + *a_out = s_wave_idx[0u]; /* 单段:原地(用户看到的仍是这段) */ + return UI_WAVE_OK; + } + if (a_dir > 0) { + *a_out = s_wave_idx[(uint8_t)((pos + 1u) % s_wave_cnt)]; + } else { + *a_out = s_wave_idx[(uint8_t)((pos + s_wave_cnt - 1u) % s_wave_cnt)]; + } + return UI_WAVE_OK; +} + +/* + * 函数功能:切到段号表里的前一段(LR 左键的落点,数值升序表 → 「前」= 号更小) + * 入口参数:a_cur 当前段号;a_out 输出切换后的段号(越界/空表不写) + * 返 回 值:UI_WAVE_OK 已给出;UI_WAVE_NONE 盘上一段都没有 int + * 限定条件:仅 uiTask 上下文(段号表是本任务私有缓存) + * 函数说明:只是 ui_task_wave_seg_around 的方向包装,存在的理由是 ops 表要两个 + * 独立函数指针,不写第二份查找逻辑。 + */ +static int ui_task_wave_seg_prev(uint8_t a_cur, uint8_t *a_out) { + return ui_task_wave_seg_around(a_cur, -1, a_out); +} + +/* + * 函数功能:切到段号表里的后一段(LR 右键;数值升序表 → 「后」= 号更大) + * 入口参数:a_cur 当前段号;a_out 输出切换后的段号(越界/空表不写) + * 返 回 值:UI_WAVE_OK 已给出;UI_WAVE_NONE 盘上一段都没有 int + * 限定条件:仅 uiTask 上下文(同上) + * 函数说明:与 seg_prev 共用 ui_task_wave_seg_around,切段即触发一次懒加载; + * 未缓存段的读盘 + 流式 CRC 也发生在这条调用链上(单轮上界见本 + * 文件「故障录波注入表执行侧」块首注释的 uiTask 阻塞上界段)。 + */ +static int ui_task_wave_seg_next(uint8_t a_cur, uint8_t *a_out) { + return ui_task_wave_seg_around(a_cur, 1, a_out); +} + +/* + * 函数功能:读段头(含双 CRC 完整性判定——判定发生在 ui_task_wave_load_head) + * 入口参数:a_idx - 段号;a_eng/a_trig/a_clip - 段头三字段原值 + * 返回值:UI_WAVE_OK / NONE / BAD int + * 限定条件:uiTask;命中缓存零卷操作 + */ +static int ui_task_wave_seg_head(uint8_t a_idx, uint8_t *a_eng, + uint8_t *a_trig, uint8_t *a_clip) { + int rc = ui_task_wave_load_head(a_idx); + + if (rc != UI_WAVE_OK) { + return rc; + } + *a_eng = s_wave_meta.eng; + *a_trig = s_wave_meta.trig_src; + *a_clip = (uint8_t)(s_wave_meta.clip_mask & 0xFFu); + return UI_WAVE_OK; +} + +/* + * 函数功能:读固定显示窗(触发点前后各一周波)的码值与峰值 + * 入口参数:a_idx - 段号;a_ch - 通道号;a_codes - UI_WAVE_WIN_PTS 个码值 + * (已并入段头 b_codes);a_peak_milli - 窗内绝对值最大点的 mV + * 返回值:UI_WAVE_OK / NONE(段头未验过/卷忙)/ BAD(数据区读不齐) int + * 限定条件:uiTask;先段头后窗口(未过完整性判定的段不供数据,页面显示 + * 「该段不可读」而不是半截波形——判废优先于展示) + * 函数说明:窗口起点用段头 pre_pts 现算(§8.2「数据不是编译常量」: + * 起点 = pre_pts - 窗长一半,与光标居中是同一个 20 的一半, + * 改窗宽只改 ui_data.h 的 UI_WAVE_WIN_PTS 一处);峰值的 mV + * 换算逐点走 wave_store_code_to_milli —— 全仓唯一口径, + * 这里不重抄公式(§6.3;PC 道同一条款) + */ +static int ui_task_wave_seg_win(uint8_t a_idx, uint8_t a_ch, + int16_t *a_codes, int32_t *a_peak_milli) { + static FIL f; + char path[WAVE_ST_PATH_MAX]; + UINT br = 0u; + uint32_t off; + uint32_t want; + uint32_t start; + uint32_t got_pts = 0u; + int16_t raw[UI_WAVE_WIN_PTS]; + int32_t milli; + int rc; + + if (ui_task_wave_load_head(a_idx) != UI_WAVE_OK) { + return UI_WAVE_BAD; /* 段头坏了/读不到:窗口无从谈起 */ + } + if (a_ch >= s_wave_meta.ch_count) { + return UI_WAVE_BAD; + } + if ((s_wave_meta.pts_per_ch < UI_WAVE_WIN_PTS) || + (s_wave_meta.pre_pts < (UI_WAVE_WIN_PTS / 2u))) { + return UI_WAVE_BAD; + } + start = (uint32_t)s_wave_meta.pre_pts - (UI_WAVE_WIN_PTS / 2u); + off = wave_store_data_off(s_wave_meta.ch_count, s_wave_meta.pts_per_ch, + a_ch, start); + if (off == 0xFFFFFFFFu) { + return UI_WAVE_BAD; + } + want = wave_store_data_bytes_avail(s_wave_meta.pts_per_ch, (uint16_t)start, + UI_WAVE_WIN_PTS); + if (want != UI_WAVE_WIN_PTS * 2u) { + return UI_WAVE_BAD; /* 窗口贴不出完整 40 点:显式不可读 */ + } + if (wave_store_seg_path(path, (uint32_t)sizeof(path), a_idx) != 0) { + return UI_WAVE_NONE; + } + if (vol_lock_wait(0u) == 0u) { + return UI_WAVE_NONE; + } + rc = UI_WAVE_BAD; + if (f_open(&f, path, FA_READ) == FR_OK) { + if ((f_lseek(&f, off) == FR_OK) && + (f_read(&f, s_wave_buf, want, &br) == FR_OK) && (br == want)) { + if ((wave_store_le_decode(s_wave_buf, want, raw, UI_WAVE_WIN_PTS, + &got_pts) == 0) && + (got_pts == UI_WAVE_WIN_PTS)) { + rc = UI_WAVE_OK; + } + } + (void)f_close(&f); + } + vol_lock_release(); + if (rc == UI_WAVE_OK) { + uint8_t i; + int32_t peak = 0; + + for (i = 0u; i < UI_WAVE_WIN_PTS; i++) { + int32_t v; + + v = (int32_t)raw[i] + s_wave_meta.ch[a_ch].b_codes; + if (v > 32767L) { + v = 32767L; + } else if (v < (-32768L)) { + v = -32768L; + } + a_codes[i] = (int16_t)v; + /* 显示与度量同一把尺(「谁被显示谁被度量」,§6.3): + * a_codes 画的是 raw+b,而 wave_store_code_to_milli 的公式是 + * (a_code + b_codes) × k_q16 / 65536000 取整(App/algo/ + * wave_store.c:567 实测口径)——传 raw 进去,b 已由唯一口径 + * 自己加过,PK 数与屏上波形天然同尺,这里不再加第二遍 + * (加两遍才是错);clamp 只护 int16 画布,b 的 V0.1 恒 0 */ + if (wave_store_code_to_milli(&s_wave_meta.ch[a_ch], raw[i], + &milli) == 0) { + if ((milli < 0 ? -milli : milli) > + (peak < 0 ? -peak : peak)) { + peak = milli; + } + } + } + *a_peak_milli = peak; + } + return rc; +} + +/* 录波通道函数表(六件齐全;ui_data_wave_bind 拒绝残表,缺件就是装配错) */ +static const ui_data_wave_ops_t s_wave_ops = { + ui_task_wave_manual_req, + ui_task_wave_seg_first, + ui_task_wave_seg_prev, + ui_task_wave_seg_next, + ui_task_wave_seg_head, + ui_task_wave_seg_win, +}; + /* * 函数功能:推流往返边界的心跳续命钩(F3,注册给 ui_net_sink) * 入口参数:无 @@ -431,6 +898,12 @@ void ui_task_func(void *argument) { if (ui_data_maint_bind(&s_maint_ops) != 0) { DBG_ERROR("UI maint bind: FAIL"); } + /* 录波通道注入(故障录波道 UI 侧,2026-10-29):手动录波触发页与回放页 + * 的唯一数据入口,同样先于 ui_menu_init。未注入窗口里六件按「无录波段」 + * 空态渲染(不画线、不崩);残表 = 装配错,启动日志必须可见 */ + if (ui_data_wave_bind(&s_wave_ops) != 0) { + DBG_ERROR("UI wave bind: FAIL"); + } /* 快照层复位:必须先于 ui_menu_init(首帧之前任何页面被打开都按"无数据" * 渲染 "--",不会读到上电残留)——UI_数据供给设计说明.md §5.3 */ ui_data_init(); diff --git a/Drivers/BSP/UI/ui_data.c b/Drivers/BSP/UI/ui_data.c index 0763602e..e3572c22 100644 --- a/Drivers/BSP/UI/ui_data.c +++ b/Drivers/BSP/UI/ui_data.c @@ -1087,3 +1087,87 @@ uint8_t ui_data_comm_pending(void) { } return s_comm->pending(); } + +/* ── 通道六:故障录波(wave)注入与包装 ───────────────────────────────────── + * 包装层零判据:未注入统一按「无录波段」态返(页面显式空态文案,绝不 + * 画平线——设计 §8.1 空态行);六件的参数校验只到指针级,语义判定 + * (卷忙/段损坏)全在执行侧,与通道四/五同一条分层纪律。 + */ +static const ui_data_wave_ops_t *s_wave = NULL; + +/* + * 函数功能:注入录波执行侧函数表(启动期一次) + * 入口参数:ops - 六件齐;NULL = 撤销注入 + * 返回值:0 - 已注入/已撤销;-1 - 残表(存量不动,理由逐字同 comm bind) int + * 限定条件:uiTask 启动期,先于 ui_menu_init() + */ +int ui_data_wave_bind(const ui_data_wave_ops_t *a_ops) { + if (a_ops == NULL) { + s_wave = NULL; + return 0; + } + if ((a_ops->manual_req == NULL) || (a_ops->seg_first == NULL) || + (a_ops->seg_prev == NULL) || (a_ops->seg_next == NULL) || + (a_ops->seg_head == NULL) || (a_ops->seg_win == NULL)) { + return -1; + } + s_wave = a_ops; + return 0; +} + +/* 函数功能:投手动录波请求(未注入 = 拒收,页面显示 ERR) bool */ +int ui_data_wave_manual_req(void) { + if (s_wave == NULL) { + return -1; + } + return s_wave->manual_req(); +} + +/* 函数功能:录波通道三态查询的统一未注入兜底(返 NONE 显空态,不崩) int */ +static int wp_wave_none(void) { + return UI_WAVE_NONE; +} + +/* 函数功能:取数值最大段号(回绕后不保证时间最新——段号自证,见 .h 契约) int */ +int ui_data_wave_seg_first(uint8_t *a_idx) { + if ((s_wave == NULL) || (a_idx == NULL)) { + return wp_wave_none(); + } + return s_wave->seg_first(a_idx); +} + +/* 函数功能:取更早一段段号 int */ +int ui_data_wave_seg_prev(uint8_t a_cur, uint8_t *a_out) { + if ((s_wave == NULL) || (a_out == NULL)) { + return wp_wave_none(); + } + return s_wave->seg_prev(a_cur, a_out); +} + +/* 函数功能:取更新一段段号 int */ +int ui_data_wave_seg_next(uint8_t a_cur, uint8_t *a_out) { + if ((s_wave == NULL) || (a_out == NULL)) { + return wp_wave_none(); + } + return s_wave->seg_next(a_cur, a_out); +} + +/* 函数功能:读段头(执行侧含 CRC 完整性判定) int */ +int ui_data_wave_seg_head(uint8_t a_idx, uint8_t *a_eng, uint8_t *a_trig, + uint8_t *a_clip) { + if ((s_wave == NULL) || (a_eng == NULL) || (a_trig == NULL) || + (a_clip == NULL)) { + return wp_wave_none(); + } + return s_wave->seg_head(a_idx, a_eng, a_trig, a_clip); +} + +/* 函数功能:读显示窗口码值与峰值 mV int */ +int ui_data_wave_seg_win(uint8_t a_idx, uint8_t a_ch, int16_t *a_codes, + int32_t *a_peak_milli) { + if ((s_wave == NULL) || (a_codes == NULL) || (a_peak_milli == NULL) || + (a_ch >= UI_WAVE_CH_CNT)) { + return wp_wave_none(); + } + return s_wave->seg_win(a_idx, a_ch, a_codes, a_peak_milli); +} diff --git a/Drivers/BSP/UI/ui_data.h b/Drivers/BSP/UI/ui_data.h index ed4a12ce..cb02c5f1 100644 --- a/Drivers/BSP/UI/ui_data.h +++ b/Drivers/BSP/UI/ui_data.h @@ -924,6 +924,111 @@ int ui_data_comm_set(uint8_t a_idx, uint32_t a_val); */ uint8_t ui_data_comm_pending(void); +/* ── 通道六:故障录波(wave)──────────────────────────────────────────────── + * 页面零 App include、零存储触点:录波执行侧(触发投递 / 段索引 / 段头字段 / + * 窗口码值与峰值 mV)全部经本注入表进来,真实现见 App/task/ui_task.c 的 + * s_wave_ops;PC 模拟器绑同一形状的假件(simulator/main.c)。取数口径的 + * 唯一真源是 docs/子系统设计/故障录波设计说明.md §6.3(定点 mV 换算走 + * wave_store_code_to_milli,eng 只做徽标、绝不进算式)。 + */ + +/* 录波通道数(与段头 ch_count 恒等:电气八路,温度不入波) */ +#define UI_WAVE_CH_CNT 8u +/* 回放显示窗口点数(两周波;口径推导与设计 §4.2 的触发点位置钉在一起, + * 窗起点与虚线光标由执行侧与页面各按同一定义实现,页面侧写死 */ +#define UI_WAVE_WIN_PTS 40u + +/* 通道查询返回态:段存在且可用 */ +#define UI_WAVE_OK 0 +/* 无段(目录扫空 / 卷忙跳过 / 执行侧未注入,页面显示「无录波段」) */ +#define UI_WAVE_NONE 1 +/* 有段但不可读(长度不符 / 版本不符 / 任一段 CRC 不符,页面显示 + * 「该段不可读」——两种态都必须显式说,画直线等于伪造证据(设计 §8.1) */ +#define UI_WAVE_BAD 2 + +/* + * 录波执行侧注入表(六件齐全;ui_data_wave_bind 拒绝残表) + * 返回值约定:除 manual_req 外,各件返回 UI_WAVE_OK / NONE / BAD 三态; + * seg_win 的出参仅在返 UI_WAVE_OK 时可读(通道五同一条出参契约)。 + */ +typedef struct { + /* 投一次手动录波请求。返回值:0 - 已受理;非零 - 拒收(含未落段通道 + * 不可用),页面据此走确认链的 ERR 支 */ + int (*manual_req)(void); + /* 取盘上数值最大的段号(回放页开屏落点;左右键从此起环游)。 + * 回绕后本序不保证时间最新,页顶段号即自证;时间序要靠段头 + * frame_seq/t0,代价是 100 次开文件,本波不做(lead 裁定 2026-10-29) */ + int (*seg_first)(uint8_t *a_idx); + /* 取比 a_cur 更早一段的段号(到最早端后环回数值最大端) */ + int (*seg_prev)(uint8_t a_cur, uint8_t *a_out); + /* 取比 a_cur 更新一段的段号(到最大端后环回数值最小端) */ + int (*seg_next)(uint8_t a_cur, uint8_t *a_out); + /* 读段头并做完整性判定(头 CRC 与数据 CRC 都在执行侧验完)。 + * 出参:eng - 单位口径(UI_DATA_ENG_* 同值域);trig - 触发源 + * (段头 trig_src 原值:手动/保护/定时);clip - 形状受限通道位图 + * (段头 clip 字节,位号 = 通道号) */ + int (*seg_head)(uint8_t a_idx, uint8_t *a_eng, uint8_t *a_trig, + uint8_t *a_clip); + /* 读当前显示窗口的码值(已并入 b_codes 偏移)与窗口峰值。 + * 出参:a_codes - UI_WAVE_WIN_PTS 个码值;a_peak_milli - 窗口内 + * 绝对值最大的那一点折算的 mV(定点,执行侧经 code_to_milli 算好, + * 页面不重复换算式——§6.3 的单一口径条款;b_codes 由 code_to_milli + * 内部加,与 a_codes 里已并的偏移同一把尺:谁被显示谁被度量) */ + int (*seg_win)(uint8_t a_idx, uint8_t a_ch, int16_t *a_codes, + int32_t *a_peak_milli); +} ui_data_wave_ops_t; + +/* + * 函数功能:注入录波执行侧函数表(启动期一次;形状对齐 ui_data_comm_bind) + * 入口参数:ops - 函数表(六件都不得为 NULL);NULL = 撤销注入(宿主测试用) + * 返回值:0 - 已注入/已撤销;-1 - 残表(存量不动) int + * 限定条件:uiTask 启动期、先于 ui_menu_init() + */ +int ui_data_wave_bind(const ui_data_wave_ops_t *a_ops); + +/* + * 函数功能:投一次手动录波请求(录波触发页唯一写出口) + * 返回值:0 - 已受理;-1 - 未注入或拒收(未注入与拒收同码,同通道五) int + */ +int ui_data_wave_manual_req(void); + +/* + * 函数功能:取数值最大段号(排序口径见 ui_data.c 同名胶水注释:回绕后本序 + * 不保证时间最新,页顶段号即自证;真时间序要逐段读 frame_seq/t0, + * 代价 100 次开文件,本波不做) + * 入口参数:a_idx - 出参,段号(仅返 OK 时可读) + * 返回值:UI_WAVE_OK / UI_WAVE_NONE / UI_WAVE_BAD(未注入按 NONE 渲染空态,不崩) int + */ +int ui_data_wave_seg_first(uint8_t *a_idx); + +/* + * 函数功能:取 a_cur 的更早一段段号(环形回绕) + * 入口参数:a_cur - 当前段号;a_out - 出参(仅返 OK 时可读) + * 返回值:UI_WAVE_OK / UI_WAVE_NONE / UI_WAVE_BAD int + */ +int ui_data_wave_seg_prev(uint8_t a_cur, uint8_t *a_out); + +/* + * 函数功能:取 a_cur 的更新一段段号(环形回绕,方向与 prev 相反) + * 入口参数:a_cur - 当前段号;a_out - 出参(仅返 OK 时可读) + * 返回值:UI_WAVE_OK / UI_WAVE_NONE / UI_WAVE_BAD int + */ +int ui_data_wave_seg_next(uint8_t a_cur, uint8_t *a_out); + +/* + * 函数功能:读段头(含执行侧 CRC 完整性判定)——出参仅返 OK 时可读 + * 返回值:UI_WAVE_OK / NONE / BAD(未注入按 NONE) int + */ +int ui_data_wave_seg_head(uint8_t a_idx, uint8_t *a_eng, uint8_t *a_trig, + uint8_t *a_clip); + +/* + * 函数功能:读显示窗口码值与峰值 mV——出参仅返 OK 时可读 + * 返回值:UI_WAVE_OK / NONE / BAD(未注入按 NONE) int + */ +int ui_data_wave_seg_win(uint8_t a_idx, uint8_t a_ch, int16_t *a_codes, + int32_t *a_peak_milli); + #ifdef __cplusplus } #endif diff --git a/Drivers/BSP/UI/ui_font_cn_12.c b/Drivers/BSP/UI/ui_font_cn_12.c index b758ee60..ac0a6a4e 100644 --- a/Drivers/BSP/UI/ui_font_cn_12.c +++ b/Drivers/BSP/UI/ui_font_cn_12.c @@ -329,6 +329,10 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x2, 0x0, 0x40, 0x8, 0x1, 0x0, 0x20, 0x7f, 0xf0, + /* U+4E0D "不" */ + 0xff, 0xe0, 0x40, 0x8, 0x2, 0x0, 0xd0, 0x29, + 0x9, 0x16, 0x21, 0x4, 0x0, 0x80, 0x10, 0x0, + /* U+4E3B "主" */ 0x8, 0x0, 0x80, 0x0, 0x3f, 0xe0, 0x40, 0x8, 0x1, 0x3, 0xfe, 0x4, 0x0, 0x80, 0x10, 0x7f, @@ -423,6 +427,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x5f, 0x4a, 0xa9, 0x55, 0x2a, 0x85, 0x70, 0x88, 0x70, + /* U+524A "削" */ + 0x10, 0x32, 0x45, 0x52, 0x88, 0x5f, 0xeb, 0x5, + 0x7f, 0xac, 0x15, 0xfe, 0xb0, 0x46, 0x8, 0xc3, + 0x30, + /* U+524D "前" */ 0x20, 0x82, 0x23, 0xff, 0x80, 0x7, 0xc4, 0x8a, 0x9f, 0x52, 0x2a, 0x7d, 0x48, 0xa9, 0x11, 0x26, @@ -535,6 +544,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x51, 0xc, 0x3, 0x4, 0xf, 0xf8, 0x10, 0x7f, 0xf0, + /* U+57DF "域" */ + 0x40, 0xa8, 0x11, 0x3f, 0xa0, 0x4e, 0xe8, 0x95, + 0x52, 0xaa, 0x76, 0x40, 0xcc, 0x57, 0x35, 0x81, + 0x10, + /* U+590D "复" */ 0x3f, 0xe8, 0x2, 0xff, 0x10, 0x23, 0xfc, 0x40, 0x8f, 0xf0, 0x80, 0x3f, 0xca, 0x10, 0x3c, 0x38, @@ -735,6 +749,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0xa4, 0x95, 0x94, 0xd1, 0x12, 0x50, 0x51, 0xc, 0x10, + /* U+653E "放" */ + 0x41, 0x4, 0x20, 0x4, 0x7c, 0xf4, 0x24, 0x8a, + 0x9e, 0x52, 0x4a, 0x49, 0x49, 0x11, 0x25, 0x4d, + 0x10, + /* U+6545 "故" */ 0x22, 0x4, 0x40, 0x88, 0x7d, 0xf2, 0x44, 0x44, 0xbe, 0x94, 0x52, 0x8a, 0x91, 0x23, 0xea, 0x46, @@ -1134,6 +1153,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x53, 0xea, 0x45, 0x48, 0xad, 0xf5, 0x20, 0x80, 0x60, + /* U+8BE5 "该" */ + 0x42, 0x4, 0x20, 0x3f, 0x81, 0xe, 0x44, 0x5f, + 0x8, 0x49, 0x12, 0x2c, 0x86, 0x30, 0x89, 0x6, + 0x10, + /* U+8BEF "误" */ 0x8f, 0x89, 0x10, 0x22, 0x7, 0xcc, 0x0, 0xbf, 0x90, 0x83, 0xff, 0x42, 0xc, 0xa1, 0x22, 0x8, @@ -1144,6 +1168,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0xd2, 0x12, 0x7e, 0x48, 0x49, 0xf9, 0xa1, 0x24, 0x60, + /* U+8BFB "读" */ + 0x82, 0x9, 0xf8, 0x8, 0xf, 0xfc, 0x2, 0x8a, + 0x14, 0xc2, 0x48, 0x5f, 0xec, 0x51, 0x11, 0xc, + 0x10, + /* U+8D1F "负" */ 0x20, 0x7, 0xe1, 0x4, 0x41, 0x7, 0xfc, 0x80, 0x91, 0x12, 0x22, 0x44, 0x41, 0x60, 0xc3, 0x60, @@ -1462,218 +1491,224 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 511, .adv_w = 192, .box_w = 11, .box_h = 1, .ofs_x = 0, .ofs_y = 3}, {.bitmap_index = 513, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 529, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 546, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 563, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 580, .adv_w = 192, .box_w = 11, .box_h = 9, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 593, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 610, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 627, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 644, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 661, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 678, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 695, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 712, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 729, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 746, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 763, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 780, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 797, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 814, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 831, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 848, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 865, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 882, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 899, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 916, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 930, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 946, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 962, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 546, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 562, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 579, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 596, .adv_w = 192, .box_w = 11, .box_h = 9, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 609, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 626, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 643, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 660, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 677, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 694, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 711, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 728, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 745, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 762, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 779, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 796, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 813, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 830, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 847, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 864, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 881, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 898, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 915, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 932, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 949, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 963, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 979, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 995, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1012, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1028, .adv_w = 192, .box_w = 9, .box_h = 10, .ofs_x = 1, .ofs_y = -1}, - {.bitmap_index = 1040, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1056, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1073, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1090, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1107, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1124, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1141, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1158, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 1172, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1189, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1206, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1223, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1240, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1257, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1274, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1291, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1308, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1028, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1045, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1061, .adv_w = 192, .box_w = 9, .box_h = 10, .ofs_x = 1, .ofs_y = -1}, + {.bitmap_index = 1073, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1089, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1106, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1123, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1140, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1157, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1174, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1191, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 1205, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1222, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1239, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1256, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1273, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1290, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1307, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1324, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1341, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1358, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1375, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1392, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1409, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 1423, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1440, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1457, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1474, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1491, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1508, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1525, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1358, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1374, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1391, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1408, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1425, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1442, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1459, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 1473, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1490, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1507, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1524, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1541, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1558, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1575, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 1589, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1606, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1623, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 1637, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1654, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1671, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1688, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1705, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1722, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1739, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1756, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1773, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1790, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1807, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1824, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1841, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1858, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1875, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1892, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1909, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1926, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1943, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1960, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1977, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1994, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2011, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2027, .adv_w = 192, .box_w = 9, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 2040, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2057, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2073, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2090, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2104, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2121, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2138, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2155, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2172, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2189, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2206, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2223, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2240, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2257, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2274, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2291, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2308, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2325, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2341, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2357, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2374, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2391, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2408, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2425, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2442, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2459, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2476, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2493, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2510, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2527, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2544, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2561, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2578, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2595, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2612, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2626, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2643, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2660, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2677, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 2692, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2709, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2726, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2743, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1575, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1591, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1608, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1625, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 1639, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1656, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1673, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 1687, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1704, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1721, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1738, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1755, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1772, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1789, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1806, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1823, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1840, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1857, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1874, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1891, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1908, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1925, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1942, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1959, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1976, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1993, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2010, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2027, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2044, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2061, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2078, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2094, .adv_w = 192, .box_w = 9, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 2107, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2124, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2140, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2157, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2171, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2188, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2205, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2222, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2239, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2256, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2273, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2290, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2307, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2324, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2341, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2358, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2375, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2392, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2408, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2424, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2441, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2458, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2475, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2492, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2509, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2526, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2543, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2560, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2577, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2594, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2611, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2628, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2645, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2662, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2679, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2693, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2710, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2727, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2744, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, {.bitmap_index = 2759, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2776, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2792, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2809, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2776, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2793, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2810, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2826, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2843, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2860, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2877, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2894, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2911, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2928, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2945, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2962, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2979, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2996, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3013, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3030, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3047, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3064, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3081, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 3095, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3112, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3129, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3146, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3163, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3180, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3197, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3214, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3231, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3248, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3265, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3282, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3299, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3316, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3333, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3350, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3367, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3384, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3401, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3418, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3435, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3452, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3469, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3486, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3503, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3520, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3537, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3554, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3571, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3588, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3605, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3622, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3639, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2843, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2859, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2876, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2893, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2910, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2927, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2944, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2961, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2978, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2995, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3012, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3029, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3046, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3063, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3080, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3097, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3114, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3131, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3148, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 3162, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3179, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3196, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3213, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3230, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3247, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3264, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3281, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3298, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3315, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3332, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3349, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3366, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3383, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3400, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3417, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3434, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3451, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3468, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3485, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3502, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3519, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3536, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3553, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3570, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3587, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3604, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3621, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3638, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3655, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3672, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3689, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3706, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3723, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3740, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3757, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3774, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3791, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 3806, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 3821, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 3836, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3853, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3870, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3887, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3904, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3921, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3740, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3756, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3773, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3790, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3807, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3824, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3841, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3858, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3875, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3892, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 3907, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 3922, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, {.bitmap_index = 3937, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3954, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3971, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3988, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 4005, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 4022, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 4039, .adv_w = 192, .box_w = 2, .box_h = 4, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 4040, .adv_w = 192, .box_w = 2, .box_h = 6, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 4042, .adv_w = 165, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = -1} /* U+25B6 */ + {.bitmap_index = 4022, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 4038, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 4055, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 4072, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 4089, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 4106, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 4123, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 4140, .adv_w = 192, .box_w = 2, .box_h = 4, .ofs_x = 2, .ofs_y = 0}, + {.bitmap_index = 4141, .adv_w = 192, .box_w = 2, .box_h = 6, .ofs_x = 2, .ofs_y = 0}, + {.bitmap_index = 4143, .adv_w = 165, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = -1} /* U+25B6 */ }; /*--------------------- @@ -1681,34 +1716,34 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { *--------------------*/ static const uint16_t unicode_list_1[] = { - 0x0, 0x1, 0x2, 0x2ca0, 0x2ca9, 0x2caa, 0x2cdb, 0x2d2b, - 0x2d2c, 0x2d44, 0x2d96, 0x2ded, 0x2dee, 0x2dfc, 0x2e7d, 0x2e81, - 0x2edc, 0x3005, 0x300c, 0x3011, 0x3013, 0x3039, 0x309a, 0x30a6, - 0x30d6, 0x30ed, 0x3148, 0x31b6, 0x31da, 0x322b, 0x323f, 0x3262, - 0x326c, 0x3271, 0x3276, 0x3283, 0x328f, 0x3297, 0x32a8, 0x32cc, - 0x32cf, 0x32ea, 0x3308, 0x357e, 0x3580, 0x3594, 0x35d0, 0x35e0, - 0x362b, 0x37ad, 0x37b6, 0x37d1, 0x39f0, 0x39f7, 0x39f8, 0x3a3a, - 0x3a3e, 0x3a66, 0x3c92, 0x3cc7, 0x3cd8, 0x3d14, 0x3d2f, 0x3d46, - 0x3d96, 0x3da0, 0x3da2, 0x3daf, 0x3db5, 0x3dda, 0x3df2, 0x3df3, - 0x3df5, 0x3ea1, 0x3edb, 0x3f02, 0x3f0f, 0x40d7, 0x40eb, 0x4135, - 0x4136, 0x4144, 0x4145, 0x420e, 0x4229, 0x4245, 0x4247, 0x4249, - 0x436d, 0x43d6, 0x43e5, 0x43e8, 0x4410, 0x4414, 0x4480, 0x4485, - 0x4496, 0x44de, 0x45a0, 0x45a8, 0x45bf, 0x45ca, 0x45cc, 0x45da, - 0x461f, 0x4685, 0x46a7, 0x46c1, 0x46d7, 0x4760, 0x48c1, 0x49c0, - 0x49c1, 0x4a03, 0x4a1b, 0x4a55, 0x4ae2, 0x4b00, 0x4b82, 0x4be1, - 0x4beb, 0x4c28, 0x4ca5, 0x4cc9, 0x4f59, 0x50e7, 0x50e8, 0x5156, - 0x5177, 0x518c, 0x53c8, 0x53d5, 0x53ec, 0x5571, 0x5594, 0x55ab, - 0x5685, 0x56a1, 0x570c, 0x570e, 0x57da, 0x5872, 0x58ab, 0x5937, - 0x596b, 0x5979, 0x598f, 0x59cc, 0x5b1b, 0x5b9b, 0x5d5f, 0x5d68, - 0x5d7f, 0x5d8d, 0x5d94, 0x5df1, 0x5e0e, 0x5fba, 0x608a, 0x6122, - 0x614f, 0x61a3, 0x6217, 0x66ec, 0x6765, 0x6a06, 0x6a41, 0x6a44, - 0x6a4f, 0x6a50, 0x6a5e, 0x6a82, 0x6a8f, 0x6a97, 0x6bbf, 0x6bc5, - 0x6c2a, 0x6c93, 0x6e0f, 0x6e1d, 0x6e33, 0x6e67, 0x6e70, 0x6e71, - 0x6e74, 0x6ea0, 0x6ea1, 0x6eba, 0x6ef3, 0x6f05, 0x6f88, 0x6fed, - 0x7067, 0x706d, 0x706f, 0x733f, 0x7345, 0x73a1, 0x73b9, 0x741f, - 0x7488, 0x748d, 0x7494, 0x7498, 0x74d2, 0x7504, 0x753c, 0x7596, - 0x7602, 0x7715, 0x7731, 0x7828, 0x7836, 0x792c, 0x7d78, 0xddac, - 0xddba + 0x0, 0x1, 0x2, 0x2ca0, 0x2ca9, 0x2caa, 0x2cad, 0x2cdb, + 0x2d2b, 0x2d2c, 0x2d44, 0x2d96, 0x2ded, 0x2dee, 0x2dfc, 0x2e7d, + 0x2e81, 0x2edc, 0x3005, 0x300c, 0x3011, 0x3013, 0x3039, 0x309a, + 0x30a6, 0x30d6, 0x30ea, 0x30ed, 0x3148, 0x31b6, 0x31da, 0x322b, + 0x323f, 0x3262, 0x326c, 0x3271, 0x3276, 0x3283, 0x328f, 0x3297, + 0x32a8, 0x32cc, 0x32cf, 0x32ea, 0x3308, 0x357e, 0x3580, 0x3594, + 0x35d0, 0x35e0, 0x362b, 0x367f, 0x37ad, 0x37b6, 0x37d1, 0x39f0, + 0x39f7, 0x39f8, 0x3a3a, 0x3a3e, 0x3a66, 0x3c92, 0x3cc7, 0x3cd8, + 0x3d14, 0x3d2f, 0x3d46, 0x3d96, 0x3da0, 0x3da2, 0x3daf, 0x3db5, + 0x3dda, 0x3df2, 0x3df3, 0x3df5, 0x3ea1, 0x3edb, 0x3f02, 0x3f0f, + 0x40d7, 0x40eb, 0x4135, 0x4136, 0x4144, 0x4145, 0x420e, 0x4229, + 0x4245, 0x4247, 0x4249, 0x436d, 0x43d6, 0x43de, 0x43e5, 0x43e8, + 0x4410, 0x4414, 0x4480, 0x4485, 0x4496, 0x44de, 0x45a0, 0x45a8, + 0x45bf, 0x45ca, 0x45cc, 0x45da, 0x461f, 0x4685, 0x46a7, 0x46c1, + 0x46d7, 0x4760, 0x48c1, 0x49c0, 0x49c1, 0x4a03, 0x4a1b, 0x4a55, + 0x4ae2, 0x4b00, 0x4b82, 0x4be1, 0x4beb, 0x4c28, 0x4ca5, 0x4cc9, + 0x4f59, 0x50e7, 0x50e8, 0x5156, 0x5177, 0x518c, 0x53c8, 0x53d5, + 0x53ec, 0x5571, 0x5594, 0x55ab, 0x5685, 0x56a1, 0x570c, 0x570e, + 0x57da, 0x5872, 0x58ab, 0x5937, 0x596b, 0x5979, 0x598f, 0x59cc, + 0x5b1b, 0x5b9b, 0x5d5f, 0x5d68, 0x5d7f, 0x5d8d, 0x5d94, 0x5df1, + 0x5e0e, 0x5fba, 0x608a, 0x6122, 0x614f, 0x61a3, 0x6217, 0x66ec, + 0x6765, 0x6a06, 0x6a41, 0x6a44, 0x6a4f, 0x6a50, 0x6a5e, 0x6a82, + 0x6a85, 0x6a8f, 0x6a97, 0x6a9b, 0x6bbf, 0x6bc5, 0x6c2a, 0x6c93, + 0x6e0f, 0x6e1d, 0x6e33, 0x6e67, 0x6e70, 0x6e71, 0x6e74, 0x6ea0, + 0x6ea1, 0x6eba, 0x6ef3, 0x6f05, 0x6f88, 0x6fed, 0x7067, 0x706d, + 0x706f, 0x733f, 0x7345, 0x73a1, 0x73b9, 0x741f, 0x7488, 0x748d, + 0x7494, 0x7498, 0x74d2, 0x7504, 0x753c, 0x7596, 0x7602, 0x7715, + 0x7731, 0x7828, 0x7836, 0x792c, 0x7d78, 0xddac, 0xddba }; /*Collect the unicode lists and glyph_id offsets*/ @@ -1719,12 +1754,12 @@ static const lv_font_fmt_txt_cmap_t cmaps[] = .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY }, { - .range_start = 9654, .range_length = 1, .glyph_id_start = 313, + .range_start = 9654, .range_length = 1, .glyph_id_start = 319, .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY }, { .range_start = 8544, .range_length = 56763, .glyph_id_start = 96, - .unicode_list = unicode_list_1, .glyph_id_ofs_list = NULL, .list_length = 217, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .unicode_list = unicode_list_1, .glyph_id_ofs_list = NULL, .list_length = 223, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY } }; diff --git a/Drivers/BSP/UI/ui_menu_tab.c b/Drivers/BSP/UI/ui_menu_tab.c index 8c5ae3fb..f84ef76f 100644 --- a/Drivers/BSP/UI/ui_menu_tab.c +++ b/Drivers/BSP/UI/ui_menu_tab.c @@ -54,6 +54,7 @@ const ui_menu_node_t g_menu_tab[UI_MENU_TAB_COUNT] = { { 32, 0u, "自检记录" }, /* 39 二级菜单 */ { 32, 0u, "运行记录" }, /* 40 二级菜单 */ { 32, 0u, "运行报告" }, /* 41 二级菜单 */ + { 32, 0u, "录波回放" }, /* 42 二级菜单 */ }; /* diff --git a/Drivers/BSP/UI/ui_menu_tab.h b/Drivers/BSP/UI/ui_menu_tab.h index c17e7700..24053433 100644 --- a/Drivers/BSP/UI/ui_menu_tab.h +++ b/Drivers/BSP/UI/ui_menu_tab.h @@ -25,7 +25,7 @@ typedef struct { const char *name; /* 显示文本(字面量,存 Flash) */ } ui_menu_node_t; -#define UI_MENU_TAB_COUNT 42 +#define UI_MENU_TAB_COUNT 43 /* 护栏:8 位索引上限 255 项(哨兵 0xFF 占用 255);超界则编译失败(C99 写法,兼容 ARMCC v5) */ typedef char menu_tab_count_check[(UI_MENU_TAB_COUNT <= 255) ? 1 : -1]; diff --git a/Drivers/BSP/UI/ui_page_registry.c b/Drivers/BSP/UI/ui_page_registry.c index 34e54d3e..5ee11d0b 100644 --- a/Drivers/BSP/UI/ui_page_registry.c +++ b/Drivers/BSP/UI/ui_page_registry.c @@ -32,6 +32,8 @@ #include "ui_page_view_plate.h" #include "ui_page_view_set.h" #include "ui_page_view_word.h" +#include "ui_page_wave_manual.h" +#include "ui_page_wave_play.h" #include "ui_page_yk.h" #include "ui_page_yx.h" @@ -52,6 +54,7 @@ const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT] = { { 19u, &ui_page_calib_desc }, /* node 19 遥测系数 */ { 24u, &ui_page_clock_desc }, /* node 24 时钟设置 */ { 25u, &ui_page_reset_desc }, /* node 25 强制复归 */ + { 26u, &ui_page_wave_manual_desc }, /* node 26 手动录波 */ { 27u, &ui_page_clear_desc }, /* node 27 清除记录 */ { 29u, &ui_page_comm_desc }, /* node 29 通讯设置 */ { 30u, &ui_page_net_cfg_desc }, /* node 30 网口设置 */ @@ -64,6 +67,7 @@ const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT] = { { 39u, &ui_page_selftest_desc }, /* node 39 自检记录 */ { 40u, &ui_page_runlog_desc }, /* node 40 运行记录 */ { 41u, &ui_page_runreport_desc }, /* node 41 运行报告 */ + { 42u, &ui_page_wave_play_desc }, /* node 42 录波回放 */ }; /* diff --git a/Drivers/BSP/UI/ui_page_registry.h b/Drivers/BSP/UI/ui_page_registry.h index 4e1b1dab..c7091e81 100644 --- a/Drivers/BSP/UI/ui_page_registry.h +++ b/Drivers/BSP/UI/ui_page_registry.h @@ -25,7 +25,7 @@ typedef struct { const content_page_def_t *desc; /* 页面描述符(Flash const) */ } ui_page_entry_t; -#define UI_PAGE_MAP_COUNT 28u /* 绑定了内容页的叶子数 */ +#define UI_PAGE_MAP_COUNT 30u /* 绑定了内容页的叶子数 */ extern const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT]; diff --git a/Drivers/BSP/UI/ui_page_wave_manual.c b/Drivers/BSP/UI/ui_page_wave_manual.c new file mode 100644 index 00000000..91741a56 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_wave_manual.c @@ -0,0 +1,126 @@ +/* + * 模块名称:UI Page wave_manual (手动录波内容页) + * 模块功能:装置维护 → 手动录波 叶子(node 26,#P 受密码保护)的内容页—— + * 复用 ui_page_action 共享确认链(一次确认 → 二次确认 → 执行回报): + * 执行只做一件事——经录波注入表投一次「手动触发」请求,受理即回报 + * 「已请求录波」;采点、冻结、落盘全在 ring/store/落盘道,本页不 + * 等待也不查询结果(录波从触发到成段最快也要一秒后,页面上做同步 + * 等待等于把 uiTask 绘制定律「零 I/O」当场破掉); + * 只读进入(查看密码)整页拒执行并显式 LOCK——与强制复归同一条 + * 「触发是操作不是查看」的权限判据(docs/任务规划/UI补齐.md §2 + * 维护组的既有口径,本页不自造第二种); + * 本文件零 App include、零存储触点(分层方向由注入表守住)。 + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:DTU 项目组 + * 创建日期:2026-10-29 + * 修改记录: + * v1.0 2026-10-29 DTU 项目组 创建(故障录波道 UI 侧;触发入口形态 = + * docs/子系统设计/故障录波设计说明.md §8.3; + * 冷却/忽略的屏上反馈:拒收即 ERR,ring 侧 + * 「冷却中丢弃并计数」不在页面上细分原因—— + * 用户要的原因区分在回放页看段号) + */ + +/* 头文件包含区 */ +#include "ui_page_wave_manual.h" + +#include "ui_content_page.h" +#include "ui_data.h" +#include "ui_page_action.h" + +/* 私有宏定义区 */ +/* (无——文案词全部是 Flash 字面量,见下面静态配置) */ + +/* 私有函数声明区(静态初始化器要引用下面函数的地址,故先声明) */ + +static bool wv_ok(void); + +/* 静态变量定义区 */ + +/* + * 本页操作页实例(配置全为 Flash 字面量与函数地址;运行态两字节由组件写)。 + * 字段顺序 = ui_page_action_t 声明序(ARMCC v5 = C99 要求指定顺序一致)。 + * 观测行留空(label 与 cb 成对给 NULL,组件允许):录波段数要扫卷才能知道, + * 而组件 watch_cb 的契约是「绘制路径零 I/O 直调」——不为一个观测数破坏 + * 绘制定律,段数的事交给回放页(它本来就要读段)。 + */ +static ui_page_action_t s_act = { + "确认手动录波?", /* prompt:问句用字全在既有子集(确认/手动录波 均来自 + 菜单与强制复归页现词,'?' 为 ASCII) */ + "已请求录波", /* done_text:回报「请求已受理」,不谎称「已存成段」—— + 受理到落盘之间还隔着冻结窗与卷写,页面无权代言 */ + wv_ok, /* ok_cb */ + 1u, /* need_modify:只读进入一律拒执行(触发是操作) */ + NULL, /* watch_label(观测行不给,理由见上方块注释) */ + NULL, /* watch_cb */ + 0u, /* stage(运行态初值) */ + 0u /* err (运行态初值) */ +}; + +/* 函数定义区 */ + +/* + * 函数功能:执行投递——向录波环形缓冲投一次手动触发请求 + * 入口参数:无 + * 返回值:true = 已受理(组件转成功回报);false = 拒收(组件显示 ERR) bool + * 限定条件:由 ui_page_action_key 在第二次 ENTER 时调用(uiTask 上下文) + * 函数说明:ring 的 request 本身是「任意上下文可调的一次性请求」,非阻塞、 + * 不等待采点;注入未绑(装配缺件)同样返非零 → 屏上显 ERR, + * 比「按了像成功其实什么都没发生」诚实(强制复归页同款纪律)。 + * 不补操作记录:录波不是五类审计记录之一(触发证据在段文件头里, + * 落盘道已带 trig_src/t0/frame_seq),页面上再投一条 = 两处两个真源。 + */ +static bool wv_ok(void) { + return (ui_data_wave_manual_req() == 0) ? true : false; +} + +/* + * 函数功能:开页复位确认链(框架 init_cb) + * 入口参数:无 + * 返回值:无 + * 限定条件:框架开页链内调用 + * 函数说明:复位由组件做(stage/err 是它的运行态),本页不重复清一遍 + */ +static void pg_init(void) { + ui_page_action_init(&s_act); +} + +/* + * 函数功能:自绘(框架 draw_cb;四行居中全由组件画) + * 入口参数:a_layer / a_clip - 契约见 ui_content_page.h + * 返回值:无 + * 限定条件:框架绘制链末尾调用 + */ +static void pg_draw(lv_layer_t *a_layer, const lv_area_t *a_clip) { + ui_page_action_draw(&s_act, a_layer, a_clip); +} + +/* + * 函数功能:按键转发(框架 key_cb) + * 入口参数:a_key - LVGL 键值 lv_key_t + * 返回值:true = 本页已消费;false = 交回框架 bool + * 限定条件:框架按键链内 + * 函数说明:确认链的判定全在组件里(含只读拒执行),本页只如实透传返回值 + */ +static bool pg_key(lv_key_t a_key) { + return (ui_page_action_key(&s_act, a_key) != 0u); +} + +/* + * 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=wave_manual 绑定引用) + * 操作页形态(强制复归页同款):无浅 V、无页脚、无行列表; + * edit_capable=1 是「能不能改」的那半,另半(本次进入授予的权限)由 + * ui_content_page_is_editable() 给;key_intercept=0:LEFT 不拦截,关页 = 取消 + */ +const content_page_def_t ui_page_wave_manual_desc = { + .id = "wave_manual", + .title = "手动录波", + .hide_deco = 1u, + .hide_footer = 1u, + .selectable = 0u, + .key_intercept = 0u, + .edit_capable = 1u, + .init_cb = pg_init, + .draw_cb = pg_draw, + .key_cb = pg_key, +}; diff --git a/Drivers/BSP/UI/ui_page_wave_manual.h b/Drivers/BSP/UI/ui_page_wave_manual.h new file mode 100644 index 00000000..5f11b153 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_wave_manual.h @@ -0,0 +1,30 @@ +/* + * 模块名称:UI Page wave_manual (手动录波内容页) + * 模块功能:装置维护 → 手动录波 叶子(node 26,#P 受密码保护)的内容页声明—— + * 向框架 ui_content_page 提供页面描述符;确认链全部走 ui_page_action + * 共享实现,本模块零 App include、零存储触点(触发经录波注入表) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:DTU 项目组 + * 创建日期:2026-10-29 + * 修改记录: + * v1.0 2026-10-29 DTU 项目组 创建(故障录波道 UI 侧;形态与分工 = + * docs/子系统设计/故障录波设计说明.md §8.3 + * 「手动录波操作页」一行) + */ +#ifndef UI_PAGE_WAVE_MANUAL_H +#define UI_PAGE_WAVE_MANUAL_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ui_content_page.h" + +/* 页面描述符(定义在 ui_page_wave_manual.c;生成物按菜单树 #page=wave_manual 引用本符号) */ +extern const content_page_def_t ui_page_wave_manual_desc; + +#ifdef __cplusplus +} +#endif + +#endif /* UI_PAGE_WAVE_MANUAL_H */ diff --git a/Drivers/BSP/UI/ui_page_wave_play.c b/Drivers/BSP/UI/ui_page_wave_play.c new file mode 100644 index 00000000..2f7258b6 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_wave_play.c @@ -0,0 +1,435 @@ +/* + * 模块名称:UI Page wave_play (录波回放内容页) + * 模块功能:记录查询 → 录波回放 叶子的内容页——单通道阶梯波形回放(设计 + * docs/子系统设计/故障录波设计说明.md §8.1 元素表逐条落地): + * 顶行 = 通道名 + 段号 + 触发源字母(M 手动 / P 保护 / T 定时); + * 波形区 = 两周波 40 点、每点占 3 px 的**阶梯线**(1 bpp 下平滑 + * 折线的过零平缓段会被阈值吃掉,ui_deco.h 实测口径;阶梯线每段 + * 非水平即垂直,天然绕开,也更贴近「离散采样」的事实); + * 参考线 = 0 线(虚线)+ 窗口峰值标注(定点 mV,ASCII); + * 底行 = 单位徽标(只读段头 eng,eng 绝不参与算式——§6.3 钉死, + * PC 道同一口径);形状受限通道右上打「削」徽标; + * 空态显式文案「无录波段」/「该段不可读」——画成一条直线等于 + * 伪造证据(§8.1 空态行,此判据优先于「页面上总得画点东西」)。 + * 键位:上下切通道、左右切段(key_intercept=1 才能把 LEFT 从 + * 「关页」手里要来);ESC 关页。 + * 取数纪律:本文件零 App include——段号/段头/窗口码值/峰值 mV 全部经 + * ui_data.h 录波注入表进来(波形算式与定点换算在执行侧,页面 + * 连 b_codes 都不碰,收到的码值已并偏移);首帧绘制才懒取数 + * (开页链零 I/O,绘制定律不破)。 + * 显示窗口与光标(与落盘道/PC 道的同一裁定,写在这里免得两头漂): + * 窗口 = 触发点前一周波 + 后一周波共 40 点,窗口起点 = 段内点号 + * 「预窗点数 减 二十」(即执行侧 seg_win 的固定窗);虚线竖光标 + * 落在窗内第 二十 与第 廿一 点之间(0 基第 19/20 点边界,即 + * 波形区正中 x)——依据 = 设计 §4.2 触发点位置 + App/algo/ + * wave_ring.h 的点序注释(触发 tick 采样 = 预窗最后一点), + * 页面不复述任何换算式。 + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:DTU 项目组 + * 创建日期:2026-10-29 + * 修改记录: + * v1.0 2026-10-29 DTU 项目组 创建(故障录波道 UI 侧) + */ + +/* 头文件包含区 */ +#include "ui_page_wave_play.h" + +#include + +#include "ui_content_page.h" +#include "ui_data.h" +#include "ui_tokens.h" + +/* 私有宏定义区 */ + +/* 绘制区几何(屏宽 160:波形区左右各留 20 px 边;高 90 px) */ +#define WP_WX0 20 /* 波形区左边界 x */ +#define WP_PX_PER_PT 3 /* 每点占宽(§8.1 阶梯线口径) */ +#define WP_WTOP 34 /* 波形区顶 y(信息行下方) */ +#define WP_WBOT 124 /* 波形区底 y(0 线最多贴到这里) */ +#define WP_WSPANS 90 /* 纵向可用像素 = WP_WBOT - WP_WTOP */ +#define WP_CUR_X (WP_WX0 + (UI_WAVE_WIN_PTS / 2u) * WP_PX_PER_PT) + /* 触发虚线光标 x(窗口正中,理由见文件头) */ +#define WP_INFO_Y 20 /* 顶行(通道/段号/触发 + 削徽标)条带 y */ +#define WP_HINT_Y 128 /* 倒数第二行(峰值/键位提示)条带 y */ +#define WP_ENG_Y 143 /* 底行(单位徽标)条带 y(页脚已隐藏) */ +#define WP_EMPTY_Y 60 /* 空态文案条带 y(近屏中部) */ + +/* 页面数据态:未取数前不画任何波形元素 */ +#define WP_ST_IDLE 0u /* 未取数/无段(空态文案) */ +#define WP_ST_OK 1u /* 段完整,可画波形 */ +#define WP_ST_BAD 2u /* 段不可读(显式文案,绝不画线) */ + +/* 私有函数声明区 */ + +static void wp_load_seg(void); +static void wp_ensure(void); +static void wp_draw_text(lv_layer_t *a_layer, const char *a_text, + int32_t a_x, int32_t a_y, uint8_t a_align, + const lv_area_t *a_clip); +static int32_t wp_map_y(int32_t a_v, int32_t a_dmin, int32_t a_span); + +/* 私有变量定义区 */ + +/* + * 通道名(ASCII 段名 = 语义取数层的通道语义名,页面自带表避免为了 + * 六个字母再开一条注入边;与段头通道表的语义顺序一致:三相电压/零序/ + * 三相电流在前,形状受限路按段头 conv 位区分——名字本身两端一致) + */ +static const char *const s_ch_names[UI_WAVE_CH_CNT] = { + "UA", "UB", "UC", "UN", "IA", "IB", "IC", "IN" +}; + +/* 触发源字母(值域 = 段头 trig_src;未识别给 '?' 不给猜) */ +static const char s_trig_letters[] = { '?', 'M', 'P', 'T' }; +#define WP_TRIG_MAX ((uint8_t)(sizeof(s_trig_letters) - 1u)) + +static uint8_t s_state; /* WP_ST_* 三态 */ +static uint8_t s_idx; /* 当前段号(盘上编号,0..99) */ +static uint8_t s_ch; /* 当前通道(上下键切) */ +static uint8_t s_eng; /* 段头单位口径(只读徽标) */ +static uint8_t s_trig; /* 段头触发源 */ +static uint8_t s_clip; /* 段头形状受限位图(位号=通道) */ +static int16_t s_codes[UI_WAVE_WIN_PTS]; /* 当前窗码值(执行侧已并偏移) */ +static int32_t s_peak_milli; /* 窗口峰值 mV(执行侧算好) */ + +/* 函数定义区 */ + +/* + * 函数功能:取当前段的头与窗口(三态落到 s_state,坏态一律显式文案) + * 入口参数:无(读 s_idx / s_ch) + * 返回值:无 + * 限定条件:仅在 uiTask 绘制/按键链内调用(经注入表触到执行侧) + * 函数说明:头都读不成时连顶行都不画——通道名以外的字段全来自段头, + * 缺一半字段还硬凑顶行等于把「读不到」伪装成「读到了」 + */ +static void wp_load_seg(void) { + int rc = ui_data_wave_seg_head(s_idx, &s_eng, &s_trig, &s_clip); + + if (rc != UI_WAVE_OK) { + s_state = (rc == UI_WAVE_BAD) ? WP_ST_BAD : WP_ST_IDLE; + return; + } + rc = ui_data_wave_seg_win(s_idx, s_ch, s_codes, &s_peak_milli); + if (rc != UI_WAVE_OK) { + s_state = (rc == UI_WAVE_BAD) ? WP_ST_BAD : WP_ST_IDLE; + return; + } + s_state = WP_ST_OK; +} + +/* + * 函数功能:首帧懒取数(开页链零 I/O 的落地方式:真正的卷读取推迟到 + * 第一次绘制——模拟器截图与真机重绘走同一条链,行为一致) + * 入口参数:无 + * 返回值:无 + * 限定条件:draw_cb 开头调用 + * 函数说明:取不到段就停在空态;此后再按左右键仍会重试(手动录波从 + * 触发到成段有一秒级延迟,页开着等段出现是回放页的正常用法, + * 不做「试过就没有了」的缓存) + */ +static void wp_ensure(void) { + uint8_t idx = 0u; + int rc; + + if (s_state != WP_ST_IDLE) { + return; + } + rc = ui_data_wave_seg_first(&idx); + if (rc == UI_WAVE_OK) { + s_idx = idx; + wp_load_seg(); + } +} + +/* + * 函数功能:单行文本绘制(对齐可选;ui_page_action 私有拷贝同法,此处 + * 多一个左/右对齐是因为顶行要把「削」徽标钉到右上角) + * 入口参数:a_layer - 绘制层;a_text - Flash 字面量;a_x - 锚点 x(对齐 + * 含义:左=起点 中=屏宽轴 右=终点);a_y - 条带顶; + * a_align - 0 左 / 1 中 / 2 右;a_clip - 重绘区 + * 返回值:无 + * 限定条件:a_text 非空;条带与 clip 不相交即返回(culling) + * 函数说明:lv_text_get_width 必须传 strlen(传 0 不测量、传 -1 死循环, + * 框架同款警示);本 LVGL 版条带矩形给 lv_draw_label 做底色无关 + * 文本定位用,字模颜色固定 UI_COL_FG + */ +static void wp_draw_text(lv_layer_t *a_layer, const char *a_text, + int32_t a_x, int32_t a_y, uint8_t a_align, + const lv_area_t *a_clip) { + lv_area_t band; + lv_draw_label_dsc_t ld; + int32_t w = (int32_t)lv_text_get_width(a_text, (uint32_t)strlen(a_text), + LV_FONT_DEFAULT, 0); + + band.x1 = 0; + band.x2 = UI_SCREEN_W - 1; + band.y1 = a_y; + band.y2 = a_y + UI_TEXT_BAND_H - 1; + if ((band.x1 > a_clip->x2) || (band.x2 < a_clip->x1) || + (band.y1 > a_clip->y2) || (band.y2 < a_clip->y1)) { + return; + } + lv_draw_label_dsc_init(&ld); + ld.base.layer = a_layer; + ld.text = a_text; + ld.text_static = 1u; + ld.color = UI_COL_FG; + ld.ofs_x = (a_align == 1u) ? (a_x - (w / 2)) + : ((a_align == 2u) ? (a_x - w) : a_x); + ld.ofs_y = 0; + lv_draw_label(a_layer, &ld, &band); +} + +/* + * 函数功能:顶行触发字母(越界值给 '?' 不猜) + * 入口参数:a_trig - 段头 trig_src 原值 + * 返回值:字母 ASCII char + * 限定条件:仅绘制路径 + */ +static char wp_trig_letter(uint8_t a_trig) { + return (a_trig <= WP_TRIG_MAX) ? s_trig_letters[a_trig] : '?'; +} + +/* + * 函数功能:码值域到屏幕 y 的线性映射(0 基线必在域内:域取 + * 「min(0, 窗最小) .. max(0, 窗最大)」,削顶钉下轨的通道也能 + * 一眼看出贴的是 0 线还是别的什么) + * 入口参数:a_v - 码值;a_dmin - 域底;a_span - 域高(域宽,恒 >0) + * 返回值:波形区内 y 像素(WP_WBOT = 域底,WP_WTOP = 域顶) int32_t + * 限定条件:绘制路径纯算术,零除由调用侧钳 span 保证 + * 函数说明:整数乘除即可——域高不超过 65534,先乘后除无溢出风险 + */ +static int32_t wp_map_y(int32_t a_v, int32_t a_dmin, int32_t a_span) { + return WP_WBOT - (((a_v - a_dmin) * WP_WSPANS) / a_span); +} + +/* + * 函数功能:波形区自绘——阶梯线 + 0 线(虚线)+ 触发点虚线竖光标 + * 入口参数:a_layer / a_clip - 契约见 ui_content_page.h + * 返回值:无 + * 限定条件:仅 s_state == WP_ST_OK 时被调用;s_codes 已备 + * 函数说明:阶梯 = 每点先画 3 px 水平段、再在点边界补一段垂直连线, + * 全程只有水平与垂直两种线段(1 bpp 抗阈值裁定的直接落地); + * 0 线与光标用 dash(ui_deco 先例只画过实线,dash 字段是 LVGL + * 线段 dsc 自带的,单色层同样按位落墨);整区一次 cull,区内 + * 不再逐线判交——波形区就 120x90,逐线判交省不了多少,判错 + * 反而留下半截线 + */ +static void wp_draw_wave(lv_layer_t *a_layer, const lv_area_t *a_clip) { + lv_area_t band; + lv_draw_line_dsc_t ld; + int32_t vmin = s_codes[0]; + int32_t vmax = s_codes[0]; + int32_t dmin; + int32_t dmax; + int32_t span; + int32_t i; + int32_t y; + int32_t y_prev = 0; + + band.x1 = 0; + band.x2 = UI_SCREEN_W - 1; + band.y1 = WP_WTOP; + band.y2 = WP_WBOT; + if ((band.x1 > a_clip->x2) || (band.x2 < a_clip->x1) || + (band.y1 > a_clip->y2) || (band.y2 < a_clip->y1)) { + return; + } + for (i = 1; i < (int32_t)UI_WAVE_WIN_PTS; i++) { + int32_t v = s_codes[i]; + if (v < vmin) { + vmin = v; + } + if (v > vmax) { + vmax = v; + } + } + dmin = (vmin < 0) ? vmin : 0; + dmax = (vmax > 0) ? vmax : 0; + span = dmax - dmin; + if (span < 1) { + span = 1; /* 全零窗:0 线与波形线重合贴在区底,诚实且无除零 */ + } + + lv_draw_line_dsc_init(&ld); + ld.base.layer = a_layer; + ld.color = UI_COL_FG; + ld.width = 1; + + /* 0 线(虚线):恒在域内,永远画得出来 */ + ld.dash_width = 2; + ld.dash_gap = 2; + y = wp_map_y(0, dmin, span); + ld.p1.x = WP_WX0 - 2; + ld.p1.y = y; + ld.p2.x = WP_WX0 + ((int32_t)UI_WAVE_WIN_PTS * WP_PX_PER_PT) + 1; + ld.p2.y = y; + lv_draw_line(a_layer, &ld); + + /* 触发点竖光标(虚线):第 二十/廿一 点之间的点边界(见文件头裁定) */ + ld.p1.x = WP_CUR_X; + ld.p1.y = WP_WTOP; + ld.p2.x = WP_CUR_X; + ld.p2.y = WP_WBOT; + lv_draw_line(a_layer, &ld); + + /* 阶梯线(实线) */ + ld.dash_width = 0; + ld.dash_gap = 0; + for (i = 0; i < (int32_t)UI_WAVE_WIN_PTS; i++) { + int32_t x; + + x = WP_WX0 + (i * WP_PX_PER_PT); + y = wp_map_y((int32_t)s_codes[i], dmin, span); + ld.p1.x = x; + ld.p1.y = y; + ld.p2.x = x + WP_PX_PER_PT - 1; + ld.p2.y = y; + lv_draw_line(a_layer, &ld); + if ((i > 0) && (y != y_prev)) { + ld.p1.x = x; + ld.p1.y = y_prev; + ld.p2.x = x; + ld.p2.y = y; + lv_draw_line(a_layer, &ld); + } + y_prev = y; + } +} + +/* + * 函数功能:单位徽标取词(只读 eng 选词,eng 不进任何算式——§6.3 口径; + * 未识别值按最保守的「引脚域」渲染,不猜高口径) + * 入口参数:a_eng - 段头 eng 原值 + * 返回值:徽标文案(Flash 字面量) const char * + * 限定条件:仅绘制路径 + */ +static const char *wp_eng_word(uint8_t a_eng) { + if (a_eng == (uint8_t)UI_DATA_ENG_SEC) { + return "二次值"; + } + if (a_eng == (uint8_t)UI_DATA_ENG_PRI) { + return "一次值"; + } + return "引脚域"; +} + +/* + * 函数功能:开页复位(框架 init_cb)——通道回到 UA、数据回到未取态 + * 入口参数:无 + * 返回值:无 + * 限定条件:框架开页链内;只清状态零 I/O(懒取数在首帧绘制) + */ +static void pg_init(void) { + s_state = WP_ST_IDLE; + s_ch = 0u; +} + +/* + * 函数功能:自绘(框架 draw_cb) + * 入口参数:a_layer / a_clip - 契约见 ui_content_page.h + * 返回值:无 + * 限定条件:框架绘制链末尾调用 + */ +static void pg_draw(lv_layer_t *a_layer, const lv_area_t *a_clip) { + char buf[UI_BUF_STRLEN]; + + wp_ensure(); + if (s_state == WP_ST_BAD) { + wp_draw_text(a_layer, "该段不可读", UI_SCREEN_W / 2, WP_EMPTY_Y, 1u, + a_clip); + return; + } + if (s_state != WP_ST_OK) { + wp_draw_text(a_layer, "无录波段", UI_SCREEN_W / 2, WP_EMPTY_Y, 1u, + a_clip); + return; + } + + /* 顶行:左 = 通道 + 段号 + 触发字母;右 = 形状受限「削」徽标 */ + (void)lv_snprintf(buf, sizeof(buf), "%s SEG%u %c", s_ch_names[s_ch], + (unsigned int)s_idx, wp_trig_letter(s_trig)); + wp_draw_text(a_layer, buf, UI_LIST_LEFT_X, WP_INFO_Y, 0u, a_clip); + if ((s_clip & (uint8_t)(1u << s_ch)) != 0u) { + wp_draw_text(a_layer, "削", UI_SCREEN_W - 1, WP_INFO_Y, 2u, a_clip); + } + + wp_draw_wave(a_layer, a_clip); + + /* 倒数第二行:左 = 键位提示(ASCII);右 = 窗口峰值 mV(定点,ASCII) */ + wp_draw_text(a_layer, "UD=CH LR=SEG", UI_LIST_LEFT_X, WP_HINT_Y, 0u, + a_clip); + (void)lv_snprintf(buf, sizeof(buf), "PK%dmV", (int)s_peak_milli); + wp_draw_text(a_layer, buf, UI_SCREEN_W - 1, WP_HINT_Y, 2u, a_clip); + + /* 底行:单位徽标(eng) */ + wp_draw_text(a_layer, wp_eng_word(s_eng), UI_SCREEN_W / 2, WP_ENG_Y, 1u, + a_clip); +} + +/* + * 函数功能:按键处理(框架 key_cb)——上下切通道、左右切段 + * 入口参数:a_key - LVGL 键值 lv_key_t + * 返回值:true = 本页已消费;false = 交回框架 bool + * 限定条件:框架按键链内(ESC 不到这里;LEFT 因 key_intercept=1 先到这里) + * 函数说明:环游语义 = 上下在八路间绕回、左右在现存段间绕回;切段失败 + * (卷忙/无邻段)保持当前画面不动——「按了没动」比「画面被 + * 忙态清成空」诚实,忙不忙在回放页本来就不是用户要的答案 + */ +static bool pg_key(lv_key_t a_key) { + uint8_t next; + int rc; + + switch (a_key) { + case LV_KEY_UP: + case LV_KEY_DOWN: + next = (a_key == LV_KEY_UP) + ? (uint8_t)((s_ch + UI_WAVE_CH_CNT - 1u) % UI_WAVE_CH_CNT) + : (uint8_t)((s_ch + 1u) % UI_WAVE_CH_CNT); + s_ch = next; + if (s_state == WP_ST_OK) { + rc = ui_data_wave_seg_win(s_idx, s_ch, s_codes, &s_peak_milli); + if (rc != UI_WAVE_OK) { + s_state = (rc == UI_WAVE_BAD) ? WP_ST_BAD : WP_ST_IDLE; + } + } + return true; + case LV_KEY_LEFT: + case LV_KEY_RIGHT: + if (s_state == WP_ST_IDLE) { + wp_ensure(); /* 空态下按左右 = 再探一次盘(新段可能已落) */ + return true; + } + rc = (a_key == LV_KEY_RIGHT) ? ui_data_wave_seg_next(s_idx, &next) + : ui_data_wave_seg_prev(s_idx, &next); + if (rc == UI_WAVE_OK) { + s_idx = next; + s_state = WP_ST_IDLE; /* 先回空态再载,坏段不会留着旧画面 */ + wp_load_seg(); + } + return true; + default: + return false; + } +} + +/* + * 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=wave_play 绑定引用) + * 查看页形态:无浅 V、无页脚(段号在顶行,页脚「共 N 页」与段号两套计数 + * 反而打架)、不可选(单元素自绘页)、可编辑位 0(本页零写动作); + * key_intercept=1 是本页存在的理由级配置:LEFT 要拿来切上一段,不能让 + * 框架默认的「LEFT 关页」吃掉 + */ +const content_page_def_t ui_page_wave_play_desc = { + .id = "wave_play", + .title = "录波回放", + .hide_deco = 1u, + .hide_footer = 1u, + .selectable = 0u, + .key_intercept = 1u, + .edit_capable = 0u, + .init_cb = pg_init, + .draw_cb = pg_draw, + .key_cb = pg_key, +}; diff --git a/Drivers/BSP/UI/ui_page_wave_play.h b/Drivers/BSP/UI/ui_page_wave_play.h new file mode 100644 index 00000000..9454eabb --- /dev/null +++ b/Drivers/BSP/UI/ui_page_wave_play.h @@ -0,0 +1,29 @@ +/* + * 模块名称:UI Page wave_play (录波回放内容页) + * 模块功能:记录查询 → 录波回放 叶子的内容页声明——向框架 ui_content_page + * 提供页面描述符;单通道阶梯波形回放,数据全走录波注入表 + * (本模块零 App include、零存储触点、零波形算式) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:DTU 项目组 + * 创建日期:2026-10-29 + * 修改记录: + * v1.0 2026-10-29 DTU 项目组 创建(故障录波道 UI 侧;元素表 = + * docs/子系统设计/故障录波设计说明.md §8.1) + */ +#ifndef UI_PAGE_WAVE_PLAY_H +#define UI_PAGE_WAVE_PLAY_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ui_content_page.h" + +/* 页面描述符(定义在 ui_page_wave_play.c;生成物按菜单树 #page=wave_play 引用本符号) */ +extern const content_page_def_t ui_page_wave_play_desc; + +#ifdef __cplusplus +} +#endif + +#endif /* UI_PAGE_WAVE_PLAY_H */ diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index e591b41c..eb3663bb 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -1052,6 +1052,16 @@ 1 ..\App\algo\freq.c + + wave_ring.c + 1 + ..\App\algo\wave_ring.c + + + wave_store.c + 1 + ..\App\algo\wave_store.c + ui_data_feed.c 1 @@ -3676,6 +3686,16 @@ 1 ..\Drivers\BSP\UI\ui_page_yk.c + + ui_page_wave_manual.c + 1 + ..\Drivers\BSP\UI\ui_page_wave_manual.c + + + ui_page_wave_play.c + 1 + ..\Drivers\BSP\UI\ui_page_wave_play.c + diff --git a/MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct b/MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct index b51cffcf..557aadcd 100644 --- a/MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct +++ b/MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct @@ -10,6 +10,13 @@ ; *** 两条前提缺任何一条 = 静默数据损坏(2026-09-20 首轮 *** ; *** 扩容实验实证)。CCM 存在性本机 ST-LINK 实测可读写; *** ; *** 克隆片可能缺 SRAM3,换料必须重测! *** +; *** 录波支路(2026-10-29 R6 台账同步):wave_ring.o 5,300B *** +; *** (内含 int16×320×8 主环 5,120B)与落盘道那批 static 全 *** +; *** 部按 .ANY 落 RW_IRAM1——环不是 DMA 可见对象,但按 R1 *** +; *** CCM 只放 FreeRTOS 堆,且 CCM 只剩 7,904B(87.9%)本身 *** +; *** 无位可给;实测三档 RW_CCM 恒 57,376B、RW_IRAM1 从 *** +; *** 81,536B 升到 89,936B。凡「把大环挪进 CCM 腾 SRAM」的 *** +; *** 念头,先过策略文档 §5 裁决与 §7 验证阶梯再动手。 *** ; ************************************************************* LR_IROM1 0x08000000 0x00100000 { ; load region size_region @@ -23,7 +30,7 @@ LR_IROM1 0x08000000 0x00100000 { ; load region size_region .ANY (+RW +ZI) } RW_CCM 0x10000000 0x0000FF00 { ; CCM 64KB 减 256B 尾: FreeRTOS 堆 only (no DMA!) - heap_4.o (+RW +ZI) ; ucHeap 48KB: 任务栈/队列/定时器 + heap_4.o (+RW +ZI) ; ucHeap 56KB(2026-10-12 B5 由 48KB 扩容): 任务栈/队列/定时器 } UNINIT_CCM 0x1000FF00 UNINIT 0x00000100 { ; round 8 取证: 复位不丢"进段打点"槽 *(.noinit) ; IWDG/引脚复位不清 SRAM → 复位后仍可读 diff --git a/docs/README.md b/docs/README.md index cc5e9faf..2f1038a2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -91,7 +91,7 @@ docs/ | [`运行与自检记录设计说明.md`](子系统设计/运行与自检记录设计说明.md) | `/log/run.log`·`selftest.log` 定长记录与待机显示 | | [`看门狗设计说明.md`](子系统设计/看门狗设计说明.md) | IWDG(CubeMX 配置)+ 任务心跳位图:选型与超时推导、掩码与埋点、取证机制、验证记录;已台架闭环(V2/V4/V5)。**§11 = 任务级饥饿的诊断方法与 `[PROF]` 实测基线**(诊断沉淀:判读口径、量化归因、三条脆弱点、明确不做的四条) | | [`ADC采样节拍设计说明.md`](子系统设计/ADC采样节拍设计说明.md) | **ADC 采集链路采样节拍(现状说明)**:TIM3 1 kHz 更新事件同时触发 TPAFE CONVST 与(经 TRGO)STM32 ADC1 + DMA 乒乓缓冲;节拍源为什么是 TIM3、DMA 硬约束、事件驱动消费与就绪门、实现落点、观测计数、性能特征与设计边界 | -| [docs/子系统设计/故障录波设计说明.md](子系统设计/故障录波设计说明.md) | **故障录波设计与口径属主**:1 kHz 起步与 80 点口径的偏离登记、存原始码值的理由、四段所有权与三条并发陷阱、段文件字节布局、现场单通道回放显示契约、预算与 R6 前置 | +| [docs/子系统设计/故障录波设计说明.md](子系统设计/故障录波设计说明.md) | **故障录波设计与口径属主**:1 kHz 起步与 80 点口径的偏离登记、存原始码值的理由、四段所有权与四条并发陷阱、段文件字节布局、现场单通道回放显示契约、预算与 R6 前置 | | [`IEC104上行设计说明.md`](子系统设计/IEC104上行设计说明.md) | **B4 上行规约**:IEC 60870-5-104 被控站(TCP 2404)范围/点表(V0.1 有效子集)/链路流程(k/w/t1~t3、总召、COV、遥控两段式、定值)/netTask 接入决策/预算与测试方案;协议核心纯逻辑 + 主机侧纯逻辑单测全栈单测 | | [`网络/BSD_Socket_API_使用指南.md`](网络/BSD_Socket_API_使用指南.md) | **上层视图**:BSD Socket API 公开语义、示例、常见问题 | | [`网络/BSD_Socket_API_函数调用关系.md`](网络/BSD_Socket_API_函数调用关系.md) | **内部视图**:netTask / 消息队列 / 保护判据挂起 / 重试对账的调用关系与设计意图 | diff --git a/docs/UI设计/LVGL模拟器部署与使用说明.md b/docs/UI设计/LVGL模拟器部署与使用说明.md index 114819dd..a9dbc1e1 100644 --- a/docs/UI设计/LVGL模拟器部署与使用说明.md +++ b/docs/UI设计/LVGL模拟器部署与使用说明.md @@ -186,7 +186,7 @@ MCU 端 `Drivers/BSP/dbg_log.h` 会引用 `sys_clock.h`(RTC 时间戳),PC - `LV_USE_THEME_MONO` 是否 = 1 且在 `sim_port_init` 中调了 `lv_theme_mono_init`(1bpp 无主题 = 黑底黑字) - 主循环是否调用 `lv_refr_now(NULL)` 强制首帧输出(PARTIAL 模式下保证) - 是否重复设置 tick:SDL 驱动内部 `lv_tick_set_cb(SDL_GetTicks)`,**不要**再 `SDL_AddTimer` + `lv_tick_inc` -2. **中文字体缺字**:`ui_font_cn_12` 是子集字体(当前 218 汉字/符号 + ASCII 0x20~0x7E,共 313 字形;2026-10-28 实测)。**缺字才会显示为空白**——新增汉字必须重生成字体(见下文 6.3)。早期只有 83 字、当时菜单需要 186 字,那是 2026-09-03 的历史快照,不是现状。 +2. **中文字体缺字**:`ui_font_cn_12` 是子集字体(当前 224 汉字/符号 + ASCII 0x20~0x7E,共 319 字形;2026-10-29 实测)。**缺字才会显示为空白**——新增汉字必须重生成字体(见下文 6.3)。早期只有 83 字、当时菜单需要 186 字,那是 2026-09-03 的历史快照,不是现状。 3. **`LV_SDL_RENDER_MODE` 必须为 `PARTIAL`**:1bpp + SDL 若非 PARTIAL,`lv_sdl_window.c` 会 `#error` 编译失败。 ### 6.3 字体生成(如需补全中文字符) @@ -234,7 +234,7 @@ A: `LV_CONF_DIR` 变量未在 `add_subdirectory(lvgl)` 前定义,检查 `simul A: 按 6.2 黑屏排查顺序检查:mono 主题 → `lv_refr_now` → tick 不重复。 ### Q4: 菜单某些中文显示空白? -A: 子集字体未含该字(当前字符集 218 汉字/符号 + ASCII 0x20~0x7E,共 313 字形;2026-10-28 实测),见 6.3 重新生成字体。 +A: 子集字体未含该字(当前字符集 224 汉字/符号 + ASCII 0x20~0x7E,共 319 字形;2026-10-29 实测),见 6.3 重新生成字体。 ### Q5: 修改了 UI 代码但模拟器没变化? A: 确认修改的是 `Drivers/BSP/UI/ui_menu.c` 而非 `simulator/` 内的副本(本工程无副本);然后重新 `cmake --build`。 diff --git a/docs/UI设计/UI_中文字体生成与配置.md b/docs/UI设计/UI_中文字体生成与配置.md index 2cca483b..5a168c7b 100644 --- a/docs/UI设计/UI_中文字体生成与配置.md +++ b/docs/UI设计/UI_中文字体生成与配置.md @@ -55,7 +55,7 @@ scanned 13 files; unique non-ASCII chars: 78 -> D:\Code\DTU Code\STM32F4-Base\ui_cn_chars.txt ``` -产物 `ui_cn_chars.txt` 为本工程实际显示的汉字清单(当前 218 字,含 ▶/,/: 等符号;2026-10-28 UI 补齐批「记录 5 页 + 运行报告」+33 字,185→218——本行旧数 183 是未回填的历史值,改前实测 185;其中 60 字为 2026-10-25 按页面扩充规划显示串预置,同日道E 落 SOE 记录显示名映射时新用 采/样/恢/效 4 字;清单里源码暂未在用的字由 font-charset 门禁记 WARN 放行),可留作字符集记录。 +产物 `ui_cn_chars.txt` 为本工程实际显示的汉字清单(当前 224 字,含 ▶/,/: 等符号;2026-10-29 录波 UI 道 +6 字(218→224);2026-10-28 UI 补齐批「记录 5 页 + 运行报告」+33 字,185→218——本行旧数 183 是未回填的历史值,改前实测 185;其中 60 字为 2026-10-25 按页面扩充规划显示串预置,同日道E 落 SOE 记录显示名映射时新用 采/样/恢/效 4 字;清单里源码暂未在用的字由 font-charset 门禁记 WARN 放行),可留作字符集记录。 --- @@ -152,8 +152,8 @@ OK 67325 bytes -> ...\Drivers\BSP\UI\ui_font_cn_12.c 1. **1bpp 细笔画发虚**:宋体在 1bpp 下可能丢像素。若上板/客户端中文偏淡,把第 4 节 `--bpp` 改为 `2` 重生成即可(子集小,体积可接受)。 2. **字符集变更**:以后新增中文菜单项,须重跑第 3、4 节(重新提取 `ui_cn_chars.txt` 并生成),否则新字显示为方框。 3. **编码**:UI 源文件须为 **UTF-8(带 BOM)**,LVGL 按 UTF-8 解码中文串;字体 `.c` 为纯 ASCII,无编码风险。当前 Keil 已加 Misc Controls 抑制项,无需额外 `--locale=english`。 -4. **字体体积**:12px + 1bpp + 218 个汉字/符号(**不含 ASCII**——ASCII 0x20~0x7E 由 make_font 的 -r 参数一并打进子集, - 故字形数 = 218 + 95 = **313**,`glyph_dsc` 数组实含 notdef 共 314 条;2026-10-28 实测)源文件 ≈ 65.7KB(2026-10-28 实测 67,325 B;改前 185 字/280 字形/58.2KB,旧文本 183 字/278 字形系未回填);ROM 增量以 `.map` 为准(2026-10-25 扩 60 字实测 `RO-data` +1,528 B、RAM 0;同日 SOE 显示名再用 4 字实测 `RO-data` +108 B、RAM 0——1bpp 12px 一个字约 27 B,含字形描述符);改 bpp 2 约翻倍,仍在 F407 Flash 余量内。 +4. **字体体积**:12px + 1bpp + 224 个汉字/符号(**不含 ASCII**——ASCII 0x20~0x7E 由 make_font 的 -r 参数一并打进子集, + 故字形数 = 224 + 95 = **319**,`glyph_dsc` 数组实含 notdef 共 320 条;2026-10-29 录波 UI 道实测)源文件 ≈ 67.1KB(2026-10-29 实测 68,729 B;改前 185 字/280 字形/58.2KB,旧文本 183 字/278 字形系未回填);ROM 增量以 `.map` 为准(2026-10-25 扩 60 字实测 `RO-data` +1,528 B、RAM 0;同日 SOE 显示名再用 4 字实测 `RO-data` +108 B、RAM 0——1bpp 12px 一个字约 27 B,含字形描述符);改 bpp 2 约翻倍,仍在 F407 Flash 余量内。 5. **换字体**:黑体用 `simhei.ttf`(单文件免抽取);其余字体同理替换 `--font` 路径后重生成。 --- diff --git a/docs/UI设计/UI_数据供给设计说明.md b/docs/UI设计/UI_数据供给设计说明.md index a160a97d..771dec4b 100644 --- a/docs/UI设计/UI_数据供给设计说明.md +++ b/docs/UI设计/UI_数据供给设计说明.md @@ -694,7 +694,7 @@ typedef struct { | 4 | 未标定时页面明确标识(不把码值当伏特) | 目检 + 文本断言(`eng=0` 时带标记) | | 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 页)**:27 页连跑两次逐字节一致(含整定 9 态)——**集合现已扩至 60 张**,双次一致需按同口径重跑回填;轮询定时器不影响——无头模式不调 `lv_timer_handler()`(§9.2 注 6) | +| 7 | 像素回归稳定 | `ui-golden` 连续两次运行零差异。**已验(2026-10-26,当时下限为 27 页)**:27 页连跑两次逐字节一致(含整定 9 态)——**集合现已扩至 66 张**,**双次一致已验(2026-10-29:同一批 66 张连跑三次逐字节一致,池水位 5,448 B;菜单 8 状态连跑三次亦一致,5,368 B)**;轮询定时器不影响——无头模式不调 `lv_timer_handler()`(§9.2 注 6) | | 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 钩子 | diff --git a/docs/UI设计/UI_界面制作流程.md b/docs/UI设计/UI_界面制作流程.md index 7de2b718..77745727 100644 --- a/docs/UI设计/UI_界面制作流程.md +++ b/docs/UI设计/UI_界面制作流程.md @@ -48,7 +48,7 @@ flowchart TD |---|---|---|---| | C1 | 屏幕 | 160×160,1bpp 单色(`LV_COLOR_FORMAT_I1`),无物理屏(远端客户端显示) | `ui_port.h` | | C2 | 字体 | **唯一**:`ui_font_cn_12`(宋体 12px 1bpp)。汉字 12px/字,ASCII 6px/字,行高 13px。需要其他字号 = 大工程(新字体 + 双端 `lv_conf.h`),默认禁止 | `ui_font_cn_12.c` | -| C3 | 字符集 | 子集字体(当前 218 汉字/符号 + ASCII 0x20-0x7E,共 313 字形);**缺字显示空白**;新字必须重生成字体 | `tools/font_gen/` | +| C3 | 字符集 | 子集字体(当前 224 汉字/符号 + ASCII 0x20-0x7E,共 319 字形);**缺字显示空白**;新字必须重生成字体 | `tools/font_gen/` | | C4 | LVGL 内存池 | 16KB(`LV_MEM_SIZE`,2026-10-15 由 40KB 收缩;实测峰值 6,568 B = 池的 40%);任一时刻只放"当前导航路径 + 当前内容页" | `Lib/lv_conf.h` | | C5 | 文本 | 一律静态字符串(`lv_label_set_text_static`),禁止动态字符串/strdup | 架构文档 | | C6 | 编码 | `Drivers/BSP/UI/*`、`ui_task.c`、`Lib/lv_conf.h` 为 **UTF-8 带 BOM**;改动只用 Edit 工具,**禁止** PowerShell/cmd 整文件重写(U+FFFD 不可逆) | AGENTS.md 铁律 | diff --git a/docs/UI设计/UI_自动化测试流程.md b/docs/UI设计/UI_自动化测试流程.md index da8b2c4c..c2c11f63 100644 --- a/docs/UI设计/UI_自动化测试流程.md +++ b/docs/UI设计/UI_自动化测试流程.md @@ -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` | ① 文本级断言(字形模板匹配,正向 425 条 + 反向 36 条,独立于 `ref/`)→ ② 60 张截图与 `ref/` 黄金参考**逐字节**比对 + 开页-关页往返池水位 | `PASS: 60 内容页 + 往返池水位全部通过`(文本断言先于像素比对,失败即中止) | +| 2 | `simulator/tests/check_content_pages.py` | ① 文本级断言(字形模板匹配,正向 444 条 + 反向 45 条,独立于 `ref/`)→ ② 66 张截图与 `ref/` 黄金参考**逐字节**比对 + 开页-关页往返池水位 | `PASS: 66 内容页 + 往返池水位全部通过`(文本断言先于像素比对,失败即中止) | | 3 | `keil-build` skill(`keil_build.py`) | MCU 端编译(UI 源码双端共用,改一端必须过另一端) | 退出码 0 = BUILD_OK | | 4 | `scripts/check_coding_standard.py` | 代码规范 V1.0 机械检查 | PASS | @@ -52,7 +52,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 页**,在裁剪**前**生成、裁剪后全部一致;该集合现已扩至 60 张), +> 后一次漂移已核实为**有意且更优**:像素逐字节未变(**裁剪当时黄金参考只有 11 页**,在裁剪**前**生成、裁剪后全部一致;该集合现已扩至 66 张), > 且一次性残留仍是 80 B(5448 − 5368)→ 泄漏特征未变,只是 `lv_theme_apply` 不再为已不存在的控件类登记样式条目。 **漂移处理**:菜单/页面结构或样式变更导致门禁失败时,先确认新值合理(残留分解见 @@ -61,7 +61,7 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警 ## 四、黄金参考(入库)与目检 PNG(本地产物) -`simulator/tests/ref/*.bin`(像素黄金参考,60 张 × 3200 B)**随仓库入库**;派生的 `.png` +`simulator/tests/ref/*.bin`(像素黄金参考,66 张 × 3200 B)**随仓库入库**;派生的 `.png` 目检图仍是本地产物(`.gitignore`)。入库的理由:比对是**逐字节**的,基线不入库时任何新克隆 或新工作树跑 `ui-golden` 都必然 FAIL(缺基准),这条门禁就只在"有老副本的机器"上真实存在。 可复现性实测(2026-10-22):两份各自独立构建的 `lvgl_sim`(不同构建目录、相隔 11 天的代码) @@ -148,7 +148,7 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警 ### 7.3 用法与门禁档位 ```powershell -# 认屏快检:60 张黄金参考(+ 若有台架留档帧),秒级,不需要构建产物 +# 认屏快检:66 张黄金参考(+ 若有台架留档帧),秒级,不需要构建产物 & '.venv\Scripts\python.exe' scripts/board_ui_nav.py --selftest-quick # 完整自检:再加全绑定页闭环导航(需模拟器可执行文件,约 30 s) & '.venv\Scripts\python.exe' scripts/board_ui_nav.py --selftest diff --git a/docs/UI设计/UI_设计架构.md b/docs/UI设计/UI_设计架构.md index 6347b417..71cf28e7 100644 --- a/docs/UI设计/UI_设计架构.md +++ b/docs/UI设计/UI_设计架构.md @@ -38,7 +38,7 @@ MCU **无物理屏幕**:LVGL 做 160×160 **1bpp 单色离屏渲染**,帧缓 | LVGL 内存池 | 16 KB(`LV_MEM_SIZE`,2026-10-15 由 40 KB 收缩;实测峰值 6,568 B = 池的 40%) | `Lib/lv_conf.h` | | 刷新周期 | 10 ms(`ui_port_step(10)` + `osDelay(10)`) | `ui_task.c` | | 推流端口 | 7003(TCP,与 DTU-RemoteLCD 客户端对齐) | `ui_net_sink.h` | -| 默认字体 | `ui_font_cn_12`(12px 1bpp,218 汉字/符号 + ASCII 0x20–0x7E,共 313 字形) | `Lib/lv_conf.h` | +| 默认字体 | `ui_font_cn_12`(12px 1bpp,224 汉字/符号 + ASCII 0x20–0x7E,共 319 字形) | `Lib/lv_conf.h` | | uiTask 栈 | 16384 words(BelowNormal) | `Src/freertos.c` | ### 1.4 硬约束(改 UI 必须遵守) diff --git a/docs/UI设计/mockups/01_主界面_说明.md b/docs/UI设计/mockups/01_主界面_说明.md index 37737e70..4535d1f2 100644 --- a/docs/UI设计/mockups/01_主界面_说明.md +++ b/docs/UI设计/mockups/01_主界面_说明.md @@ -32,7 +32,7 @@ 说明:选中行(第 1 行)无 ▶ 箭头;其余行 ▶ 紧跟在 4 字文本后(非右对齐)。行内文字左对齐、左留白 8px。 -## 三、字符集核对(对照当时(2026-09-03)的 ui_font_cn_12:78 汉字 + ASCII 0x20~0x7E;本 mockup 的核对结论只对该时点成立,现值 = 218 汉字/符号 + ASCII,共 313 字形,见 docs/UI设计/UI_中文字体生成与配置.md §7) +## 三、字符集核对(对照当时(2026-09-03)的 ui_font_cn_12:78 汉字 + ASCII 0x20~0x7E;本 mockup 的核对结论只对该时点成立,现值 = 224 汉字/符号 + ASCII,共 319 字形,见 docs/UI设计/UI_中文字体生成与配置.md §7) - **菜单 7 项**共 22 个唯一汉字:命中 21(装 置 信 息 实 时 数 据 参 定 值 遥 设 维 护 通 讯 记 录 查 询),**新增 1(三)**。 - **状态行**"当前模式 : 无模式":**新增 5(当 前 模 式 无)** + **全角冒号 :(U+FF1A,新增)**;空格为 ASCII(已在字库)。 diff --git a/docs/UI设计/内容页架构.md b/docs/UI设计/内容页架构.md index d96398ca..d2cc0449 100644 --- a/docs/UI设计/内容页架构.md +++ b/docs/UI设计/内容页架构.md @@ -453,7 +453,7 @@ extern const content_page_def_t ui_page_device_info_desc; /* 描述符(Flash ## 八、验证与回归 - **门禁**(`ui-verify` skill / `UI_自动化测试流程.md`): - 0) 模拟器构建 0 警告 → 1) 菜单 8 状态像素回归 + 池基线 → 2) 内容页文本断言 + 60 张黄金参考逐字节比对 + 0) 模拟器构建 0 警告 → 1) 菜单 8 状态像素回归 + 池基线 → 2) 内容页文本断言 + 66 张黄金参考逐字节比对 (网口设置 4 态 + 密码页 4 态等)+ 开页-关页往返水位 → 3) Keil 构建 0 错 0 警 → 4) 代码规范检查 - **门禁常量**(脚本头部):`MENU_POOL_BASELINE = 5368 B`、`POOL_AFTER_ROUNDTRIP = 5448 B` - **失败处置**:像素不一致 = 代码 bug(修代码,**禁止**刷参考掩盖);**有意**改视觉 → diff --git a/docs/UI设计/菜单页面设计.md b/docs/UI设计/菜单页面设计.md index 5c7c014e..a3a65be6 100644 --- a/docs/UI设计/菜单页面设计.md +++ b/docs/UI设计/菜单页面设计.md @@ -399,7 +399,7 @@ for (uint8_t i = 0; i < cnt; i++) { python tools/menu_gen/gen_menu.py ``` -> 产物 `ui_menu_tab.c/.h` 含 42 节点(9 菜单 + 33 叶子,最深 三级菜单);`ui_menu.c` 不持有本地表,导航/显示一律走 `ui_menu_tab_*` 查询 API(§3.4)。 +> 产物 `ui_menu_tab.c/.h` 含 43 节点(9 菜单 + 34 叶子,最深 三级菜单;2026-10-29 录波道加两叶后由 42 升到 43);`ui_menu.c` 不持有本地表,导航/显示一律走 `ui_menu_tab_*` 查询 API(§3.4)。 > **编码陷阱(已踩)**:产物含中文**字符串字面量**,必须 **UTF-8 带 BOM**(同 `ui_menu.c`)。 > 无 BOM 时 cl.exe(代码页 936/GBK)会按 GBK 误读 UTF-8 字节 → 字符串解析错乱(C2001/C4819)。`gen_menu.py` 已用 `utf-8-sig` 输出。 @@ -792,7 +792,7 @@ ESC/LEFT : d>0 → menu_back() (根框 d=0 不响 1. 本 LVGL v9.3 的 `lv_text_get_width(txt, length, font, letter_space)` **length=0 时返回 0**(不是"整串");必须传 `strlen`——否则框宽计算全 0(根框退化 8 px)。 2. 无头模式**不得**调 `lv_timer_handler()` / `lv_refr_now()`:I1 + `LV_DISPLAY_RENDER_MODE_FULL` 全屏刷新会进渲染死循环(截图因此走 `lv_snapshot`);`--keys` 注键是同步处理,无需驱动帧。 3. 选中行**保留 ▶**(随反色变黑):参考图 02/04 原图即"选中行箭头仍在、仅随反色变黑",勿实现成"选中隐箭头"(§4.4)。 -4. 字体缺字形 = 方框:菜单树新增/改名的汉字必须同步加入字符集 `ui_cn_chars.txt`(当前 218 唯一字,2026-10-28 实测)并重生成字体(重跑 `extract_cn.py` + `make_font.py`),否则新字显示为方框(流程见 `UI_中文字体生成与配置.md`)。 +4. 字体缺字形 = 方框:菜单树新增/改名的汉字必须同步加入字符集 `ui_cn_chars.txt`(当前 224 唯一字,2026-10-29 实测)并重生成字体(重跑 `extract_cn.py` + `make_font.py`),否则新字显示为方框(流程见 `UI_中文字体生成与配置.md`)。 5. **参考图保真度**:参考图为手工 mockup,4 个样本(子框顶位置)不满足单一公式;实现采用自洽规则(顶对齐/底对齐上移),实测与参考图偏差:顶位置 2/4/6/1 px(三遥/装置维护/通讯参数/记录查询),框宽 2/8/0/2 px(4字+箭头/4字/3字/2字+箭头)。两端极例(三遥、记录查询)精确吻合;统一规则优于逐屏特调,偏差范围内不影响识读与操作。 6. 装饰"浅 V"两腿线宽必须 3(水平线 2):1bpp 下细斜线边缘被阈值吃掉,腿不粗于水平线即消失;且依赖 `LV_DRAW_SW_COMPLEX=1`(见 `UI_设计架构.md` §1.4 约束 7)。 diff --git a/docs/UI设计/页面扩充详细方案.md b/docs/UI设计/页面扩充详细方案.md index 6fda7b70..6070dea5 100644 --- a/docs/UI设计/页面扩充详细方案.md +++ b/docs/UI设计/页面扩充详细方案.md @@ -21,7 +21,7 @@ 已绑定 6 页:`装置信息`(node 1)、`交流量`(3)、`直流量`(4)、`网口设置`(30)、`自检记录`(39)、`运行记录`(40)。 -> ⚠ **本表 = 2026-10-25 立项时快照,按历史冻结不回改。** 2026-10-28 复扫现值:**已绑 28 叶 / 未绑 5 叶**(判据两处互证:`ui_menu_tree.txt` 剔掉文件头注释里的示例行后 `#page=` 实数 = 28,与生成物 `ui_page_registry.h` 的 `UI_PAGE_MAP_COUNT` 一致);未绑 5 叶 = 遥信类型 / 遥信防抖 / 双点遥信 / 手动录波 / SNTP 设置。现状口径见 `docs/任务规划/UI补齐.md` §1 与 `docs/产品需求/产品需求设计文档.md` §8.2 / §8.3。 +> ⚠ **本表 = 2026-10-25 立项时快照,按历史冻结不回改。** 2026-10-28 复扫现值:**已绑 28 叶 / 未绑 5 叶**;**2026-10-29 再复扫:已绑 30 叶 / 未绑 4 叶**(手动录波与录波回放两叶已绑)(判据两处互证:`ui_menu_tree.txt` 剔掉文件头注释里的示例行后 `#page=` 实数 = 30,与生成物 `ui_page_registry.h` 的 `UI_PAGE_MAP_COUNT` 一致);未绑 5 叶 = 遥信类型 / 遥信防抖 / 双点遥信 / 手动录波 / SNTP 设置。现状口径见 `docs/任务规划/UI补齐.md` §1 与 `docs/产品需求/产品需求设计文档.md` §8.2 / §8.3。 ### 1.2 27 个空叶子清单(按菜单顺序) @@ -626,7 +626,7 @@ SNTP 客户端 ──────→ 批4 SNTP设置 | 4 | 无源点显示 `--` | 遥信量无效点、直流量直流通道 | | 5 | 定值整定闭环 | 改 → 保存 → 重启 → 值保持 + 校验和自检通过 + 主站 C_RD 一致。**2026-10-26 半收口**:模拟器侧「改 → 保存 → `OK` → 回首屏新值已在帧里」与「越界被拒、值不落盘」已由像素 + 文本断言覆盖(`sparam_save` / `sparam_landed` / `sparam_oob`);**重启保持与 C_RD 一致只能上板**(模拟器无 NAND)。另注意 40 项里 **13 项 `comm_addr = 0`**(投退、返回系数、过频/低频段、全局软压板)主站不可寻址,不能拿 C_RD 当它们的判据 | | 6 | 分层门禁无新增 | `check_layer_deps.py` 退出码 0,基线 6 → 3。**现状:3**(记录 reader 已削 3 条;写路径走注入式 ops 函数表,未新增 include,见 `docs/UI设计/UI_数据供给设计说明.md` §6.2) | -| 7 | 像素回归稳定 | `ui-golden` 连续两次零差异。**已验(2026-10-26)**:27 页连跑两次逐字节一致——**该下限此后已扩至 60 张**(新增 clock / reset / clear 系列 13 张,其后 47→53→60),60 张上的双次逐字节一致需按同口径重跑一次再回填本行 | +| 7 | 像素回归稳定 | `ui-golden` 连续两次零差异。**已验(2026-10-26)**:27 页连跑两次逐字节一致——**该下限此后已扩至 66 张**(新增 clock / reset / clear 系列 13 张,其后 47→53→60→66,2026-10-29 录波 UI 道 +6 张),66 张上的双次逐字节一致**已验(2026-10-29:任务树重建模拟器后同一批 66 张连跑三次逐字节一致,页面池水位 5,448 B;菜单侧 `check_menu_rows.py` 8 状态连跑三次亦逐字节一致,5,368 B;三次之间未用更新模式写过任何参考图,故这是同口径的传递性证据)** | | 8 | 每个新页有 golden 参考 | `simulator/tests/ref/` 新增对应 `.bin`。**整定 4 页已有 9 张**(含保存/越界/落定/回绕各态);参考图入库,改它 = 改判据,须逐页过目 | --- diff --git a/docs/UI设计/页面补齐规划.md b/docs/UI设计/页面补齐规划.md index 4cbbaffc..af741912 100644 --- a/docs/UI设计/页面补齐规划.md +++ b/docs/UI设计/页面补齐规划.md @@ -27,7 +27,7 @@ ### 2.1 规模 > ⚠ **本表是 2026-10-23 立项当天快照**(节点 42 / 有内容页叶子 6 / 空叶子 27 / 黄金参考 11 份均为当日实数,按"完成即冻结"不回改)。 -> 页面扩充落地后现值已变——黄金参考现为 60 份(`simulator/tests/ref/` 入库的 `shot_*.bin` 实数,2026-10-28 记录 5 页与运行报告页落地 +7 张),现状口径见 +> 页面扩充落地后现值已变——黄金参考现为 66 份(`simulator/tests/ref/` 入库的 `shot_*.bin` 实数,2026-10-28 记录 5 页与运行报告页落地 +7 张;2026-10-29 录波 UI 道 +6 张),现状口径见 > `docs/UI设计/页面扩充详细方案.md` 与 `docs/UI设计/UI_数据供给设计说明.md`。 | 项 | 数 | @@ -77,7 +77,7 @@ | 22 | 双点遥信 | 可编辑列表 | 开入量 | ❌ 同上 | | 24 | 时钟设置 | 表单(6 字段) | `sys_clock_get_time/set` | ✅ | | 25 | 强制复归 | 操作页(确认) | `prot_elm_request_reset()` | ✅ | -| 26 | 手动录波 | 操作页(确认) | 录波模块(docs/任务规划/故障录波.md 已立项) | 🔶 录波立项未落地 | +| 26 | 手动录波 | 操作页(确认) | `wave_ring_request` 手动触发经 `ui_data_wave_ops_t` 注入 | ✅ 已落地(`ui_page_wave_manual.c`,2026-10-29 录波 UI 道;配套新增「录波回放」叶 node 43 = `ui_page_wave_play.c`,不在本表 27 叶之列) | | 27 | 清除记录 | 操作页(选类型 + 确认) | `sys_log`(**缺删除 API**) | 🔶 需补 API | | 29 | 通讯设置 | 可编辑列表(4~6 项) | 104 公共地址/端口/主站(当前固定) | 🔶 需 `comm_cfg` | | 31 | SNTP设置 | 可编辑列表(3 项) | SNTP 客户端 | ❌ 未实现(产品口径亦未定) | @@ -91,7 +91,8 @@ **统计(2026-10-25 复核)**:本表 27 叶中 **6 叶已落地**(遥信量 / 查看 4 页 / SOE),剩 21 叶 = 数据现成 **8** 项(整定 4 + 时钟 + 复归 + 死区 + 系数)· 需新数据层 **2** 项(清除记录 / 通讯设置)· 数据源不存在 **11** 项(遥信 3 + 手动录波 + SNTP + 记录 5 + 运行报告)。**分批与 DoD 见 `docs/任务规划/UI补齐.md`**——本表只讲逐页形态与数据源,不重复排期。 -**2026-10-28 复核**:本表 27 叶中 **22 叶已落地**(本批 +6:记录 5 页与运行报告,见 docs/任务规划/UI补齐.md §2 与 docs/任务规划/登记簿.md §7);未落地的 5 叶 = 遥信类型 / 遥信防抖 / 双点遥信(无开入硬件)+ 手动录波(无录波模块)+ SNTP设置(产品口径未定),取舍与重开条件见 docs/任务规划/UI补齐.md §5。口径闭合:本表 22 叶 + 立项前就已绑定、不在本表内的 6 叶(装置信息 / 交流量 / 直流量 / 网口设置 / 自检记录 / 运行记录)= 现值已绑 28 叶(= UI_PAGE_MAP_COUNT);本表未落地 5 叶 = 菜单树未绑 5 叶(33 − 28)。复核方法:按缩进判叶子(有更深缩进后继者即非叶子),再按行内 #page= 数已绑。**2026-10-29 补充**:「手动录波」已随故障录波独立立项(docs/任务规划/故障录波.md 与 docs/子系统设计/故障录波设计说明.md),上面那行数字仍是 2026-10-28 当日复核值,未随立项改动 +**2026-10-28 复核**:本表 27 叶中 **22 叶已落地**(本批 +6:记录 5 页与运行报告,见 docs/任务规划/UI补齐.md §2 与 docs/任务规划/登记簿.md §7);未落地的 5 叶 = 遥信类型 / 遥信防抖 / 双点遥信(无开入硬件)+ 手动录波(无录波模块)+ SNTP设置(产品口径未定),取舍与重开条件见 docs/任务规划/UI补齐.md §5。口径闭合:本表 22 叶 + 立项前就已绑定、不在本表内的 6 叶(装置信息 / 交流量 / 直流量 / 网口设置 / 自检记录 / 运行记录)= 现值已绑 28 叶(= UI_PAGE_MAP_COUNT);本表未落地 5 叶 = 菜单树未绑 5 叶(33 − 28)。复核方法:按缩进判叶子(有更深缩进后继者即非叶子),再按行内 #page= 数已绑。**2026-10-29 补充**:「手动录波」已随故障录波独立立项(docs/任务规划/故障录波.md 与 docs/子系统设计/故障录波设计说明.md),上面那行数字仍是 2026-10-28 当日复核值,未随立项改动。 +**2026-10-29 二补(录波 UI 道落地)**:本表「手动录波」转已落地(23 叶);菜单树另新增「录波回放」叶(node 43,不在本表立项快照内)——现值 43 节点 / 34 叶 / 已绑 30 / 未绑 4(= 遥信 3 + SNTP 1,UI_PAGE_MAP_COUNT = 30),口径复核方法与上一段相同 --- @@ -211,7 +212,7 @@ ui_page_ac / ui_page_yx / ui_page_setlist ... 12. **三遥设置 5 页**(18~22)——等运行参数段与开入量。 13. **事故/操作/告警/启动/遥控 5 页**(34~38)——等记录写入方(每类记录 = 一个写入点 + 一个 `syslog_id_t`)。 -14. **手动录波**(26)——等录波模块。 +14. **手动录波**(26):操作页(`ui_page_action` 确认链,受理后回「已请求录波」)——已落地(2026-10-29);同批新增「录波回放」单页(上下键切通道、左右键切段、阶梯线 3px/点、0 线虚线、峰值 mV 经 `wave_store_code_to_milli` 唯一口径、削徽标、空态/坏段显式文案,规格见 docs/子系统设计/故障录波设计说明.md §8.1)。 15. **运行报告**(41)——等计数器(上电次数/运行时长/复位原因/104 统计)。 ### 批 4(可选):厂家设置 diff --git a/docs/images/故障录波数据流.svg b/docs/images/故障录波数据流.svg index c90a33b2..f77cdf9c 100644 --- a/docs/images/故障录波数据流.svg +++ b/docs/images/故障录波数据流.svg @@ -1 +1 @@ -录波段数据的四个所有者 —— 谁独占写、谁只能读、标志怎么过界写侧只有一条路:adcTask 压环 → defaultTask 落盘 → /wave 段文件;读者一律只读已封存的段。「已封存」是唯一的跨任务接口:没有任何人读别人的活缓冲——这是本图唯一要记住的一件事。触发来路:只置标志UI 手动录波操作页prot_elm 的 picked 上升沿bench 构建定周期自抓谁都只置一个原子标志adcTask:唯一写环的人每拍压 8 路 int16 原始码值专用录波环 320 点 × 8 路查标志 → 冻结 → 计满封段= 5,120 B,落 SRAM1defaultTask:文件唯一写者排空已封存区 → f_write写回字节数不符即算失败分次写 256 B,不整段驻留更新段账与最新段号/wave 段文件文件名 WAVE 加两位十进制头存 eng、系数与 clip_mask数据区按通道分列存放与 /log 平级,FTP 根下可见只置标志已封存标志字节数不符即失败采集主线照常推进(本波一行不改它)sample_ring → phasor → freq → meas → 104 / UI,全链同源冻结窗口丢的是录波样本,不是采集:相量、测频、判据、上送一律照常三个读者:一律只读已封存的段uiTask 回放页 —— 只读窗口取一段,自己画(不写文件)FTP(netTask / lftpd)—— 只搬文件,不碰采集上下文PC 脚本 —— 重算全周傅氏,与板上 meas: 与 104 浮点对表陷阱一 · 取窗竞态sample_ring_window() 读 head 后逐点拷,没有临界区现在只有 adcTask 同任务调用才安全处置:录波不复用它,另立专用环;跨任务只传已封存标志陷阱二 · FatFs 追加写该移植 FA_OPEN_APPEND 不含 FA_WRITE簇分配失败还返 FR_OK 加短写,无错误码处置:打开带 FA_WRITE,判成功必须比对写回字节数陷阱三 · 只有 8.3 短名FF_USE_LFN=0:长名与点开头一律拒处置:段名 = WAVE 加两位十进制再加 .BIN,轮转覆盖目录 /wave 与 /log 平级,FTP 鉴权现成,零新代码四条「不许」——上面那张图上没有箭头经过的地方不许跨任务读活的录波环或 sample_ring——没有临界区,读出来是两帧混合数据不许 protectTask 写环或落盘——它只置一个请求标志,同 prot_elm_request_reset 的模式不许 netTask 与 lftpd 读环——文件服务只搬已封存的段,不碰采集上下文不许把 U 与 I 两段叠在同一张图上比相位——跨转换器无共同采样触发,V0.1 的硬件事实字段级布局见 docs/子系统设计/故障录波设计说明.md §6.2;预算与 R6 前置见 §7;触发源与段长冻结语义见 §4。本图由 tools/arch_diagram/gen_wave_record_flow_svg.py 生成,改图请改脚本重跑,勿手改 SVG。 +录波段数据的四个所有者 —— 谁独占写、谁只能读、标志怎么过界写侧只有一条路:adcTask 压环 → defaultTask 落盘 → /wave 段文件;读者一律只读已封存的段。「已封存」是唯一的跨任务接口:没有任何人读别人的活缓冲——这是本图唯一要记住的一件事。触发来路:只置标志UI 手动录波操作页prot_elm 的 picked 上升沿bench 构建定周期自抓谁都只置一个原子标志adcTask:唯一写环的人每拍压 8 路 int16 原始码值专用录波环 320 点 × 8 路查标志 → 冻结 → 计满封段= 5,120 B,落 SRAM1defaultTask:文件唯一写者排空已封存区 → f_write写回字节数不符即算失败分次写 256 B,不整段驻留更新段账与最新段号/wave 段文件文件名 WAVE 加两位十进制头存 eng、系数与 clip_mask数据区按通道分列存放与 /log 平级,FTP 根下可见只置标志已封存标志字节数不符即失败采集主线照常推进(本波一行不改它)sample_ring → phasor → freq → meas → 104 / UI,全链同源冻结窗口丢的是录波样本,不是采集:相量、测频、判据、上送一律照常三个读者:一律只读已封存的段uiTask 回放页 —— 只读窗口取一段,自己画(不写文件)FTP(netTask / lftpd)—— 只搬文件,不碰采集上下文PC 脚本 —— 重算全周傅氏,与板上 meas: 与 104 浮点对表陷阱一 · 取窗竞态sample_ring_window() 读 head 后逐点拷,没有临界区现在只有 adcTask 同任务调用才安全处置:录波不复用它,另立专用环;跨任务只传已封存标志陷阱二 · FatFs 追加写该移植 FA_OPEN_APPEND 不含 FA_WRITE簇分配失败还返 FR_OK 加短写,无错误码处置:打开带 FA_WRITE,判成功必须比对写回字节数陷阱三 · 只有 8.3 短名FF_USE_LFN=0:长名与点开头一律拒处置:段名 = WAVE 加两位十进制再加 .BIN,轮转覆盖目录 /wave 与 /log 平级,FTP 鉴权现成,零新代码陷阱四 · 落盘途中环被解冻或换段adcTask 优先级高于 defaultTask,搬段窗口里环可能自弃、甚至封出下一段数据区 CRC 是对本道实际读到的字节算的 → 半新半旧照样自洽处置:关档后复检环侧身份(sealed + last_t0_ms + last_seq),不符即判废四条「不许」——上面那张图上没有箭头经过的地方不许跨任务读活的录波环或 sample_ring——没有临界区,读出来是两帧混合数据不许 protectTask 写环或落盘——它只置一个请求标志,同 prot_elm_request_reset 的模式不许 netTask 与 lftpd 读环——文件服务只搬已封存的段,不碰采集上下文不许把 U 与 I 两段叠在同一张图上比相位——跨转换器无共同采样触发,V0.1 的硬件事实字段级布局见 docs/子系统设计/故障录波设计说明.md §6.2;预算与 R6 前置见 §7;触发源与段长冻结语义见 §4。本图由 tools/arch_diagram/gen_wave_record_flow_svg.py 生成,改图请改脚本重跑,勿手改 SVG。 diff --git a/docs/产品需求/产品需求设计文档.md b/docs/产品需求/产品需求设计文档.md index 657df359..2b1090c3 100644 --- a/docs/产品需求/产品需求设计文档.md +++ b/docs/产品需求/产品需求设计文档.md @@ -201,7 +201,7 @@ flowchart LR | 7 | 开入 KI | `APP/EquipCfg/Const_IO.c` `g_tInputModelTab` | **138 点** | 0(无硬件,X3) | ⬜ | | 8 | 开出 KO | 同上 `g_tOutputModelTab` | **31 点** | 0(无硬件,X3) | ⬜ | | 9 | 动作事件 | `APP/EquipCfg/Const_Rec.c` `g_tActRecTab` | **33 条**(421~453) | 7 类记录(`App/log/sys_log.h`) | 🔶 | -| 10 | 录波测量通道 | 同上 `g_tMeasureModelTab` | **65 通道** | 0 | ⬜ | +| 10 | 录波测量通道 | 同上 `g_tMeasureModelTab` | **65 通道** | 8(本波口径,2026-10-29 代码面已交付;65 通道仍为 0,参考机口径未改)| 🔶 | | 11 | 软压板 | `APP/EquipCfg/Const_Soft.c` `g_tSoftTab` | **9 面**(表容量 32) | 1 面(`PS_GLOBAL_EN`) | 🔶 | | 12 | 保护元件 | `RelayUnit/*.c` | **45 个模块** | 9 个实例 | 🔶 | | 13 | 记录队列 | `RecUnit/RecFile.c` + `GlobalDef/Const_Sys.h` | **13 类**(128/256 条) | 8 通道(7 文本 + SOE) | 🔶 | @@ -329,7 +329,7 @@ tTimer_NoUI 无压无流判据(CN_ |---|---|---| | **外部资源**(标准源、端子变比) | 定值标定、保护判据解卡 | 两点标定(§9.2 第 4 步) | | **硬件缺失**(X3 / X4) | 开入 138、开出 31、电度 8、同期、三相通道 | V0.2 硬件 | -| **立项未排** | 多规约、SNTP、定值铺开 | 按 §9.2 顺序;**录波已立项**(2026-10-29):卡片 docs/任务规划/故障录波.md、设计 docs/子系统设计/故障录波设计说明.md,1 kHz 起步与本文 80 点/周波口径的偏离在该设计文档 §2 显式登记 | +| **立项未排** | 多规约、SNTP、定值铺开 | 按 §9.2 顺序。**录波最小闭环已交付代码面**(2026-10-29,卡片 docs/任务规划/故障录波.md、设计 docs/子系统设计/故障录波设计说明.md;1 kHz 起步与本文 80 点/周波口径的偏离在该设计文档 §2 显式登记);**4 kHz 全速、多段、COMTRADE、104 文件传输仍未排**,属二期范围 | | **算力 / 形态**(X1 / X2) | 双核分工、VxWorks | 不解除。**屏板链路已从范围外项移入范围内待做**(规格 §5.16,执行规划 docs/任务规划/屏板链路.md):主机侧链路与参考 1:1 同构,不属"替代方案" | > **结论**:本产品与参考产品的差距**全部是"进度"而非"范围"**——没有一项是"参考有而我们不做"。 @@ -890,7 +890,7 @@ sequenceDiagram 上电次数 + 本次复位原因快照)与 iec104_serv 的四个只读观测计数,经 info 帧下发 (口径 docs/UI设计/UI_数据供给设计说明.md §4.4)。 - 判据:五类记录各有写入点、可翻最近 20 条、空记录明确显示"无记录"、共用 reader——**软件面已满足** - (空态与四个范围标签由 60 张像素黄金钉住;模拟器侧这些文件没有写入方,只拍到空态)。 + (空态与四个范围标签由 66 张像素黄金钉住;模拟器侧这些文件没有写入方,只拍到空态)。 真记录翻页、掉电后保持、清除记录后归零、boot_count 自增与四态判定**待台架复核** (docs/任务规划/登记簿.md §7)。 @@ -942,7 +942,7 @@ sequenceDiagram | 遥控选择 / 撤销 / 执行 | ≤5 s | 同轮应答(ms 级) | | 读定值 / 改定值预置 / 执行 | ≤10 s / ≤5 s | 同轮应答(ms 级) | | 读文件目录 / 文件 | ≤30 s | ⬜ 未实现(文件传输未立项) | -| 录波文件存储 | ≤15 s | ⬜ 未实现(**已立项**,见 docs/任务规划/故障录波.md:本波按 1 kHz / 8 路 / 单段做,≤15 s 与 80 点口径属二期) | +| 录波文件存储 | ≤15 s | ⬜ 未验收(代码面 2026-10-29 已交付:/wave 段文件 + FTP 取回;本波按 1 kHz / 8 路 / 单段做;≤15 s 时限未测,80 点/周波属二期;卡片见 docs/任务规划/故障录波.md) | **FR-IEC104-8 单连接与自愈** - 需求:单连接语义(CH395F 单 socket);断链自动重听;网络重建(改 IP)后服务自愈;协议故障主动拆链。 @@ -1806,10 +1806,11 @@ pre-commit run --all-files ![级联框总览](../images/级联框总览.svg) -**规模**:菜单节点 **9** 个(含根)+ 叶子节点 **33** 个 = **42** 节点;最大深度 **4 级(主界面 / 一级 / 二级 / 三级菜单)**。 -**绑定现状**(2026-10-28 实测):**已绑内容页 28 叶 / 未绑 5 叶**(页面注册表 `UI_PAGE_MAP_COUNT` = 28, -与已绑叶数一致);未绑 5 叶 = 无开入硬件 3 + 手动录波 1 + SNTP 1(取舍与重开条件见 -docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 页 + 运行报告)已于 2026-10-28 落地绑页。 +**规模**:菜单节点 **9** 个(含根)+ 叶子节点 **34** 个 = **43** 节点;最大深度 **4 级(主界面 / 一级 / 二级 / 三级菜单)**。 +**绑定现状**(2026-10-29 复扫):**已绑内容页 30 叶 / 未绑 4 叶**(页面注册表 `UI_PAGE_MAP_COUNT` = 30, +与已绑叶数一致);未绑 4 叶 = 无开入硬件 3 + SNTP 1(取舍与重开条件见 +docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 页 + 运行报告)已于 2026-10-28 落地绑页, +「手动录波」与「录波回放」两叶于 2026-10-29 随录波道绑页(判据见 docs/子系统设计/故障录波设计说明.md §4.1 与 §8.1)。 **根页(主界面)7 个一级条目**:装置信息 / 实时数据 / 参数定值 / 三遥设置 / 装置维护 / 通讯参数 / 记录查询。 @@ -1843,7 +1844,7 @@ docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 ### 8.3 内容页 -**已绑 28 页**(按分组;逐页形态见 [`docs/UI设计/页面补齐规划.md`](../UI设计/页面补齐规划.md)): +**已绑 30 页**(按分组;逐页形态见 [`docs/UI设计/页面补齐规划.md`](../UI设计/页面补齐规划.md)): | 分组 | 页面 | 形态 | |---|---|---| @@ -1852,21 +1853,22 @@ docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 | 参数定值 | 查看 4 页(参数 / 定值 / 控制字 / 软压板) | 只读列表 | | 参数定值 | 整定 4 页(参数 / 定值 / 控制字 / 软压板) | 可编辑(写值 + 置脏 + 保存反馈) | | 三遥设置 | 遥测死区 / 遥测系数 | 可编辑(运行参数段,`0x8030~0x804F`) | -| 装置维护 | 时钟设置 / 强制复归 / 清除记录 | 操作页(执行类必须出真反馈,不得"点了没效果") | +| 装置维护 | 时钟设置 / 强制复归 / 清除记录 / 手动录波 | 操作页(执行类必须出真反馈,不得"点了没效果";手动录波触发后回读同一份计数,判据见 docs/子系统设计/故障录波设计说明.md §8.2) | | 通讯参数 | 通讯设置 / 网口设置 | 可编辑(`[comm]` / `[network]` 段,保存后按各自生效时机) | | 记录查询 | SOE 记录 / 自检记录 / 运行记录 | 记录列表(共用 reader) | | 记录查询 | 事故记录 / 操作记录 / 保护告警 / 保护启动 / 遥控记录 | 记录列表(每记录 2 行、空态显「无记录」;写入点与判据见 App/log/README.md §五类记录) | | 记录查询 | 运行报告 | 只读列表 13 行分 2 页;四个数逐个挂范围标签(自电池周期 / 本次开机),跨开机累计按裁定不做 | +| 记录查询 | 录波回放 | 单通道波形页(阶梯线自绘、8 路切换、单位徽标、削顶标记、空态与不可读态显式文案);显示契约见 docs/子系统设计/故障录波设计说明.md §8.1 | -**未绑 5 叶**(2026-10-28:记录 5 页与运行报告页已落地绑页,原「当前执行 6 项」清零): -遥信类型 / 遥信防抖 / 双点遥信 = 等开入硬件(§9.1.1「出口与开入」);手动录波 = 等录波子系统 -(§9.1.2「录波子系统」);SNTP 设置 = 等 SNTP 对时(§9.1.2「SNTP 对时」)。 +**未绑 4 叶**(2026-10-28:记录 5 页与运行报告页已落地绑页,原「当前执行 6 项」清零;2026-10-29: +录波道落地,「手动录波」叶子随之一并绑页): +遥信类型 / 遥信防抖 / 双点遥信 = 等开入硬件(§9.1.1「出口与开入」);SNTP 设置 = 等 SNTP 对时(§9.1.2「SNTP 对时」)。 - 内容页为**独立全屏屏**,由框架一次性自绘(**零 LVGL 子对象**),`ESC` 返回菜单屏。 - 菜单导航状态**不压层**,返回即原样还原;同一时刻至多一个内容页打开。 - 节点→页映射在逻辑层 `menu_content_page_for(node)`;未绑叶子进入空白终点框(不伪造数据)。 - 数据供给走**注入通道**(rt / par / info / 遥测死区与系数第四表),页面不直接触算法层; - 像素黄金用例当前 **60 张**(菜单 8 状态另计;2026-10-28 记录 5 页与运行报告 2 页落地,+7 张)。 + 像素黄金用例当前 **66 张**(菜单 8 状态另计;2026-10-28 记录 5 页与运行报告 2 页落地 +7 张;2026-10-29 录波 UI 道 6 张:手动录波 3 态 + 录波回放 3 态)。 ### 8.4 密码与权限 @@ -1879,7 +1881,7 @@ docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 ### 8.5 中文字体 -- 字体 `ui_font_cn_12.c`:宋体 12px 1bpp **子集**(当前 **218 唯一字 / 313 字形**,2026-10-28 实测; +- 字体 `ui_font_cn_12.c`:宋体 12px 1bpp **子集**(当前 **224 唯一字 / 319 字形**,2026-10-29 实测; 122/217、179/274、185/280 均为历史快照。字形数 = 汉字/符号数 + ASCII 0x20~0x7E 共 95 个)。 - 生成流程:`tools/font_gen/extract_cn.py`(扫 UI 源码 → `ui_cn_chars.txt`)→ `tools/font_gen/make_font.py`。 - **约束**:任何 UI 文案新增汉字后必须走此流程,否则显示为空白方块。 @@ -1922,7 +1924,7 @@ docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 | **映射铺开** | 3 | 107 项 `0x8820~0x8891` | 40 项局部 | +67 项 | | **元件铺开** | 12 | 45 个模块 | 8 个模块 / 9 个实例 | +37 个模块(过流类 6 / 电压类 5 / 重合闸 5 / 方向功率 4 / 涌流故障 5 / 自动控制 5 / 记录 5 / 录波采样 2) | | **遥测铺开** | 4、5 | 31 点 + 模型 35 行 | 9 点 | +22 点 + 模型表 | -| **录波子系统** | 10、15 | 3 类型 / 6 段 / 65 通道 / RAM 16 个 / COMTRADE 64·64·16 | 0 | 整子系统 | +| **录波子系统** | 10、15 | 3 类型 / 6 段 / 65 通道 / RAM 16 个 / COMTRADE 64·64·16 | **最小闭环代码面已交付**(2026-10-29):1 kHz / 8 路原始码值 / 单段 320 点 / 轮转段文件 / 手动与保护启动 / 单通道回放页;**板级判据一轮未跑** | 仍差 4 kHz 全速与 80 点/周波、多段与 65 通道、COMTRADE、104 文件传输;口径偏离登记见 docs/子系统设计/故障录波设计说明.md §2 | | **记录体系** | 9、13 | 13 类队列(128/256 条) | 8 类 | +5 类(越限 / 闭锁 / COS / 顺控 / 顺控执行) | | **UI 记录页** | 9、13 | 参考有完整记录页与运行报告 | 六页已绑并纳入像素黄金回归(2026-10-28) | 只剩真机判据(docs/任务规划/登记簿.md §7) | | **多规约栈** | 17 | 12 种 | 1 种(104) | +11 种(101 / 102 / 103 / CDT / ModBus / Whm_GB / 61850 / CAN + 子站侧 3 类) | @@ -1946,14 +1948,14 @@ docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 | 顺序 | 工作 | 类别 | 当前状态 | 开始条件 | |---|---|---|---|---| -| 1 | UI 补齐(记录 5 页 → 运行报告) | 规模型 | **清单已清空(2026-10-28)** | 六页已落地;余 5 叶属不做项,重开条件见 docs/任务规划/UI补齐.md §5 | +| 1 | UI 补齐(记录 5 页 → 运行报告) | 规模型 | **清单已清空(2026-10-28)** | 六页已落地;余 4 叶属不做项(手动录波与录波回放两叶已随录波道绑页),重开条件见 docs/任务规划/UI补齐.md §5 | | 2 | net 层跨上下文收口 | 平台 | **下一顺位(未开工)** | UI 清单已清空,切换条件已满足;**开工待用户点头** | | 3 | 其余结构与可靠性评估 | 平台 | 排队 | net 收口后 | | 4 | 端子/子板变比确认 → 两点标定 | 阻塞型 | 等外部资源 | 子板实物/原理图、标准源到位可插入 | | 5 | 标准源送检与第三方 104 主站验收 | 阻塞型 | 等标定 | 保护 VALID 置位;送检窗口前 2 日冻结特性 | | 6 | 定值铺开 / 映射铺开 / 遥测铺开 / 软压板铺开 | 规模型 | 等排期 | 标定后(工程量才有意义) | | 7 | 元件铺开与遥测增强 | 规模型 | 等标准源/硬件 | 先解除未标定闭锁;三相类等 V0.2 | -| 8 | 录波子系统 → 记录体系 → 多规约栈 | 规模型 | 不排 | 各自重启条件 | +| 8 | 录波子系统 → 记录体系 → 多规约栈 | 规模型 | **录波最小闭环已提前插入并交付代码面(用户裁定 2026-10-29 录波先行,见 docs/任务规划/故障录波.md);本行剩下的参考机完整形态(多段 / 65 通道 / COMTRADE / 104 文件传输)仍不排** | 各自重启条件 | | 9 | 验证窗口事项(CI 远端、PC peer、负向注入) | 平台 | 逢条件顺带 | 条件到位即执行 | | 10 | 出口与开入 / 三相与同期 / 电度计量 / 对下采集 | 阻塞型 | 不排 | V0.2 硬件 / 用户决策 | @@ -2498,7 +2500,7 @@ docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 | 元件铺开 | 45 个模块对应 45 个实例;每个实例有:定值项齐全、启动/动作/返回三态可观测、事件码正确、单测覆盖判据边界 | | 遥测铺开 | 31 点全部非 IV(除物理不可达点明确标 IV);线电压与相量满足 `Uab = Ua − Ub` 误差 ≤ 0.5 % | | 开入开出 | 27 路硬件开入可读、31 路开出可驱动;开入防抖可整定;双点遥信四态正确(00/01/10/11) | -| 录波 | 4000 Hz 采样;三类录波各生成一份 COMTRADE 且被第三方工具打开;配额 64/64/16 循环覆盖正确 | +| 录波 | **(二期口径;本波按 1 kHz / 8 路 / 单段 / 无 COMTRADE,对照表见 §9.1 第 10 行)**4000 Hz 采样;三类录波各生成一份 COMTRADE 且被第三方工具打开;配额 64/64/16 循环覆盖正确 | | 记录体系 | 13 类队列各有落点;每类容量达标;断电不丢已确认记录 | | 多规约 | 每种规约与对应主站/子站工具完成一次完整交互(连接 / 总召 / 变位 / 遥控 / 断链重连) | | 国密 | SM2 签名验签、SM3 摘要与标准测试向量一致 | diff --git a/docs/任务规划/README.md b/docs/任务规划/README.md index 38380629..ef8f2624 100644 --- a/docs/任务规划/README.md +++ b/docs/任务规划/README.md @@ -32,14 +32,15 @@ ## 1. UI 补齐(执行清单已于 2026-10-28 清空) -菜单树共 33 叶(节点行合计 42):按行内 #page= 实测**已绑 28 叶、未绑 5 叶**(2026-10-28 复核, -判定规则与重跑命令见 docs/任务规划/UI补齐.md §1「叶数判定与复核」)。未绑的 5 叶因缺少开入硬件、 -录波模块或 SNTP 产品定义而不做(原因与重开条件见 docs/任务规划/UI补齐.md §5)。 -**口径闭合**:未绑 5 = 待做 0(§1 清单已清空)+ 不做 5(遥信 3 / 手动录波 1 / SNTP 1);**42 是节点行数、不是叶数**(= 9 个菜单节点 + 33 叶)。 -已绑叶数以生成物 UI_PAGE_MAP_COUNT 为准:不剔 ui_menu_tree.txt 文件头的注释示例行时,按 #page= 文本计数会得 29(= 28 + 1 行示例)。 +菜单树共 34 叶(节点行合计 43):按行内 #page= 实测**已绑 30 叶、未绑 4 叶**(2026-10-29 复扫, +判定规则与重跑命令见 docs/任务规划/UI补齐.md §1「叶数判定与复核」)。未绑的 4 叶因缺少开入硬件 +或 SNTP 产品定义而不做(原因与重开条件见 docs/任务规划/UI补齐.md §5)——**手动录波与录波回放两叶 +已随录波道绑页**,「不做」清单里的录波那一项已销。 +**口径闭合**:未绑 4 = 待做 0(§1 清单已清空)+ 不做 4(遥信 3 / SNTP 1);**43 是节点行数、不是叶数**(= 9 个菜单节点 + 34 叶)。 +已绑叶数以生成物 UI_PAGE_MAP_COUNT 为准:不剔 ui_menu_tree.txt 文件头的注释示例行时,按 #page= 文本计数会得 31(= 30 + 1 行示例)。 **UI 补齐执行清单已于 2026-10-28 清空**:记录 5 页(事故 / 操作 / 保护告警 / 保护启动 / 遥控)与 -运行报告页同日落地并绑页,内容页黄金参考 53 → 60 张(runreport 分 2 页,第 2 页单独拍一张;判据 = simulator/tests/ref/ 入库 shot_*.bin 实数,张数 = 页面个数 + 分页多拍的那一张)。 +运行报告页同日落地并绑页,内容页黄金参考 53 → 60 张(runreport 分 2 页,第 2 页单独拍一张;判据 = simulator/tests/ref/ 入库 shot_*.bin 实数,张数 = 页面个数 + 分页多拍的那一张;**该集合 2026-10-29 已扩至 66 张**,多出的是录波回放页 6 态)。 上板判据(真记录翻页、掉电保持、清除后归零、boot_count 自增、RESTARTED、rst_cause=IWDG、conn_up +1) 已登记登记簿 §7「UI 六页真机判据」,六页文件头各留「台架待补」段;模拟器差距见 docs/测试说明/模拟器差距清单.md §2.3「开机账本 boot_stats」一行。 @@ -66,7 +67,7 @@ docs/测试说明/模拟器差距清单.md §2.3「开机账本 boot_stats」一 | **7** | 验证窗口事项 | 逢条件顺带 | CI runner、PC peer、CH340、负向故障注入条件到位时执行 | | **8** | 独立模块与暂缓项 | 当前不排 | 开入开出、SNTP、104 文件传输单独立项;RS-485 与内存优化按各自重启条件 | | **独立** | **屏板链路(对上屏板)** | **规划已出(未开工)** | 独立立项,插在任一顺位之间都能做,不占主序列;**端口已裁定(2026-10-28):屏板占 USART1,日志迁 RS-232 通道**,所以原来的 250 kbps 收发器阻塞已解除;剩下两条前置——**台架串口线挪到 CN1、日志口迁移**(见 docs/任务规划/屏板链路.md §0 与 §3 批 0);**软件可先用 PC + USB 转 TTL 台架验证,不必等屏板实物** | -| **独立** | **故障录波(1 kHz · 8 路原始码值 · 单通道回放)** | **设计定稿(代码一行未动)** | 用户裁定 2026-10-29 **录波先行**:数据面最小闭环 → 手动与保护启动触发 → 回放页,卡片判据见 docs/任务规划/故障录波.md,口径与设计见 docs/子系统设计/故障录波设计说明.md;**与 UI 道互斥**(菜单树与中文字库是整文件生成物,同刻只允许一条道写) | +| **独立** | **故障录波(1 kHz · 8 路原始码值 · 单通道回放)** | **代码面已交付(板级判据一轮未跑)** | 用户裁定 2026-10-29 **录波先行**:数据面最小闭环 → 手动与保护启动触发 → 回放页。三张卡的**代码侧判据已收口**(主机单测 100 / 170 / 110 全 rc=0、双端编译 0 错误 0 警告、板端编码器与 PC 侧解码器字节级互验、手动录波与录波回放两叶已绑页、内容页黄金参考扩至 66 张);**没跑的是板级那一轮**(注 1 V / 50 Hz、FTP 拉回、与 `meas:` 行和 104 浮点遥测对表、段文件 CRC 不符必须拒收、回放页看真段)。卡片判据见 docs/任务规划/故障录波.md,口径与设计见 docs/子系统设计/故障录波设计说明.md §10;**与 UI 道互斥**(菜单树与中文字库是整文件生成物,同刻只允许一条道写)——本波按此串行执行:UI 道在数据面与接线冻结之后才动生成物 | ### 2.1 关键依赖 diff --git a/docs/任务规划/UI补齐.md b/docs/任务规划/UI补齐.md index a268f085..ef02e3d6 100644 --- a/docs/任务规划/UI补齐.md +++ b/docs/任务规划/UI补齐.md @@ -1,38 +1,41 @@ # 当前任务:UI 补齐 > **执行清单已清空(2026-10-28)**:记录 5 页与运行报告页已落地、绑页并纳入像素黄金回归。 +> **2026-10-29 录波 UI 道**:「手动录波」由不做转落地(绑操作页),新增「录波回放」叶(节点 43); +> 叶数口径同步为 43/34/30/4(下方逐项)。 > 本文件保留为 UI 补齐的**范围与判据口径**(§1 叶数、§4 每页统一动作、§5 不做的取舍); > 下一顺位见 docs/任务规划/README.md §2.0(换主线待用户点头),上板待复核项见 docs/任务规划/登记簿.md §7。 ## 1. 目标与范围 -菜单树共 **33 叶**(节点行合计 42,缩进即层级):按行内 `#page=` 实测**已绑 28 叶、未绑 5 叶**, -页面映射表 `UI_PAGE_MAP_COUNT` = 28(与已绑叶数一致)。未绑 5 叶 = §5 明确不做的 **5 叶** -(遥信 3 / 手动录波 1 / SNTP 1),与下面分类表逐项对得上;2026-10-28 记录 5 页与运行报告页 -落地后,原「需补配置或写入方」的 6 叶缺口清零并按销项删行(该行内容见本节末与 §3)。 +菜单树共 **34 叶**(节点行合计 43,缩进即层级):按行内 `#page=` 实测**已绑 30 叶、未绑 4 叶**, +页面映射表 `UI_PAGE_MAP_COUNT` = 30(与已绑叶数一致)。未绑 4 叶 = §5 明确不做的 **4 叶** +(遥信 3 / SNTP 1),与下面分类表逐项对得上;2026-10-29 录波 UI 道把「手动录波」从不做转为 +落地并新增「录波回放」叶,绑 +2(手动转绑 + 新叶)/ 叶 +1;2026-10-28 记录 5 页与运行报告页落地后, +原「需补配置或写入方」的 6 叶缺口清零并按销项删行(该行内容见本节末与 §3)。 | 分类 | 数量 | 当前怎么处理 | |---|---:|---| | 无开入硬件 | 3 | 不做:遥信类型、遥信防抖、双点遥信 | -| 缺独立模块或产品定义 | 2 | 不做:手动录波(2026-10-29 已随故障录波立项,落地前仍按未绑计)、SNTP 设置 | +| 缺独立模块或产品定义 | 1 | 不做:SNTP 设置(手动录波已于 2026-10-29 随故障录波 UI 道落地转绑定,不再列不做) | 现有基础:`ui_w_editor` / `ui_w_setlist` / `ui_page_action` 已就绪;`ui_data` 已扩到**四通道** (rt / par / info + 遥测死区与系数用的第四张注入表);像素黄金用例与字符集门禁已存在,内容页黄金 -现有 **60 张**(菜单 8 状态另计)。**装置维护 3 页、遥测死区/系数 2 页、通讯设置页、记录 5 页与 +现有 **66 张**(菜单 8 状态另计;2026-10-29 录波 UI 道 +6 张:手动录波 3 态 + 录波回放 3 态)。**装置维护 3 页、遥测死区/系数 2 页、通讯设置页、记录 5 页与 运行报告页都是把这套基础接上真实消费者做出来的,不另发明一套机制。** 逐页形态见 `docs/UI设计/页面补齐规划.md`;数据契约见 `docs/UI设计/UI_数据供给设计说明.md`。本文件保留范围、完成判据与不做的取舍口径(执行清单已于 2026-10-28 清空)。 ### 叶数判定与复核(2026-10-28 复核) -1. **节点行** = `ui_menu_tree.txt` 里非空、非 `#` 开头的行(当前 **42** 行,含根「主界面」)。 +1. **节点行** = `ui_menu_tree.txt` 里非空、非 `#` 开头的行(当前 **43** 行,含根「主界面」)。 2. **缩进即层级**(每级 2 空格;Tab 会被 `gen_menu.py` 直接拒绝):某节点行之后再没有更深缩进的行 = **叶** - (当前 **33** 叶);有更深缩进后继者 = 菜单节点(当前 **9** 个)。等价于生成物口径 `ui_menu_tab_has_children(node) == false`。 -3. **绑定**:叶行行尾带 ` #page=` = 已绑(当前 **28** 叶);不带的 = 未绑(当前 **5** 叶)。 -4. **闭合**:`未绑 = 待做 + 不做` —— 当前 待做 **0**(§2 清单已清空)+ 不做 **5**(遥信 3 / 手动录波 1 / SNTP 1)= **5**, + (当前 **34** 叶);有更深缩进后继者 = 菜单节点(当前 **9** 个)。等价于生成物口径 `ui_menu_tab_has_children(node) == false`。 +3. **绑定**:叶行行尾带 ` #page=` = 已绑(当前 **30** 叶);不带的 = 未绑(当前 **4** 叶)。 +4. **闭合**:`未绑 = 待做 + 不做` —— 当前待做 **0**(§2 清单已清空)+ 不做 **4**(遥信 3 / SNTP 1)= **4**, 与上面的分类表、§5「当前不做」表逐项对得上。 5. **别拿 `grep -c '#page='` 当已绑叶数**:2026-10-28 实测它得 **29**,比真值多 1 —— 文件头第 5 行是规则说明里的 - ` #page=<模块名>` 示例行(不是节点行),必须先剔注释/示例行;剔完的 28 才等于生成物 `UI_PAGE_MAP_COUNT`。 + ` #page=<模块名>` 示例行(不是节点行),必须先剔注释/示例行;剔完的 30 才等于生成物 `UI_PAGE_MAP_COUNT`。 复核命令(复用生成器自己的解析器 `tools/menu_gen/gen_menu.py` 的 `parse_tree`,不另写一套计数逻辑,免得与生成物脱钩): @@ -40,16 +43,16 @@ & '.venv\Scripts\python.exe' -c "import io,sys; sys.path.insert(0,'tools/menu_gen'); import gen_menu as g; n,_=g.parse_tree(io.open('ui_menu_tree.txt',encoding='utf-8').read()); leaf=[i for i,x in enumerate(n) if all(y[0]!=i for y in n)]; b=sum(1 for i in leaf if n[i][4]); print('nodes=%d leaves=%d bound=%d unbound=%d' % (len(n),len(leaf),b,len(leaf)-b))" ``` -输出 `nodes=42 leaves=33 bound=28 unbound=5`;交叉核对生成物:`ui_menu_tab.h` 的 `UI_MENU_TAB_COUNT` = 42(节点行数)、 -`ui_page_registry.h` 的 `UI_PAGE_MAP_COUNT` = 28(已绑叶数)——一对 42、一对 28,与上面逐项对得上。 +输出 `nodes=43 leaves=34 bound=30 unbound=4`(2026-10-29 录波 UI 道落地后实测);交叉核对生成物:`ui_menu_tab.h` 的 `UI_MENU_TAB_COUNT` = 43(节点行数)、 +`ui_page_registry.h` 的 `UI_PAGE_MAP_COUNT` = 30(已绑叶数)——一对 43、一对 30,与上面逐项对得上。 三个已踩过的错算法(每种都同时给**当时**与**2026-10-28 重跑**的产物——错法不会自己失效,只是错数跟着已绑叶数一起漂): **把 42 个节点行当叶子**(多算 9 个菜单节点:当时已绑 21 叶 → 得「未绑 21」,2026-10-28 同一错法得「未绑 14」= 42 − 28); **`grep -c '#page='` 当时得 23**(当时已绑 22 叶,多的那 1 正是文件头注释第 5 行的 `#page=<模块名>` 示例字样,必须先剔注释行; -2026-10-28 同一命令得 29 = 已绑 28 + 该示例行 1);**按叶子名去重得 29**(`参数`/`定值`/`控制字`/`软压板` 在「整定」「查看」下各一份, +2026-10-28 同一命令得 29 = 当时已绑 28 + 该示例行 1;2026-10-29 现值 31 = 已绑 30 + 示例行 1);**按叶子名去重得 29**(`参数`/`定值`/`控制字`/`软压板` 在「整定」「查看」下各一份, 4 对同名不能合并;2026-10-28 重跑仍是 29 = 33 叶 − 4 对同名)。 (21 / 23 两个错数分属 2026-10-27「已绑 21 叶」与「已绑 22 叶」两个时点的实测,只用来说明错法,不代表现值; -去重 29 与上面 grep 现值 29 **只是撞号**:一个是「合并同名叶」、一个是「不剔注释行」,都与真值 28 无关。) +去重 29 与上面 grep 现值 29 **只是撞号**:一个是「合并同名叶」、一个是「不剔注释行」,都与真值 30(2026-10-29 现值)无关。) --- @@ -76,6 +79,7 @@ |---|---|---| | 通讯设置 | 页面与六态黄金已随 `728d6067` 入库;`[comm]` 段与端口真源在 `App/protocol/iec104/iec104_comm.{c,h}` | 「改后重启即生效」真机判据、配置段数与键数上限核对(台架窗口统一补做并留证) | | 记录 5 页 | 五类记录 ID / 文件 / 写入点已齐(docs/任务规划/登记簿.md §7 与 App/log/README.md §五类记录);reader 注入两端已映射 7 个记录源 | 页面已交付;只剩真机判据(真记录翻页、掉电保持、清除后归零),见 docs/任务规划/登记簿.md §7 | +| 录波两页 | 手动录波操作页 + 录波回放页已落地绑页(2026-10-29 录波 UI 道);数据源 = `App/algo/wave_ring`/`wave_store` 真实现经 `ui_data_wave_ops_t` 注入,回放读盘走 `vol_lock` try 锁小窗口 | 页面已交付;真机判据归台架(真实触发落盘的段号连续性、削徽标真值、切段切道实测毫秒与卷锁忙「本轮无段」行为,见 `docs/子系统设计/故障录波设计说明.md` §10 台架判据) | | 运行报告 | boot_stats(SD2506 用户 SRAM 上电次数 + 本次复位原因快照,道G task-12)、iec104_serv 四个只读观测计数、现成 uptime_s | 页面已交付;**跨开机累计不做**(用户裁定只报本次开机)。真机待复核 SRAM 自增与四态判定,见 docs/测试说明/模拟器差距清单.md §2.3 | --- @@ -108,7 +112,6 @@ UTC 写入,后写者赢 ⇒ 装置墙钟与主机本地时刻恒差 8 小时 | 项目 | 不做原因 | 重开条件 | |---|---|---| | 遥信类型 / 防抖 / 双点 3 页 | V0.1 无开入硬件,配置没有消费者 | 开入固件立项后,与开入开出一起做 | -| 手动录波 | 录波模块尚未落地;旧口径还挂着「工程量口径未闭环」这道前置,**已被「录波存原始码值」解掉**(docs/子系统设计/故障录波设计说明.md §3.2) | **重开条件已满足**(2026-10-29 立项):随 docs/任务规划/故障录波.md 的「手动触发与保护启动接线」那张卡一起做 | | SNTP 设置 | 当前使用 8888 私有对时协议,SNTP 链路与产品口径均未建立 | SNTP 客户端立项 | | net 层与其他结构债 | 当前只做 UI,避免两条主线互相拖慢 | UI 当前清单完成后,按 README §2 切换 | | RTC 时基统一 | 两套写入口径并存(面板与 8888 写本地钟面、104 写 UTC),恒差 8 小时;统一任一侧都要动 104 上行语义或 UI 判据 | 用户裁定时基方向后,作为跨模块变更单独立项 | diff --git a/docs/任务规划/故障录波.md b/docs/任务规划/故障录波.md index caea3902..fdfcc854 100644 --- a/docs/任务规划/故障录波.md +++ b/docs/任务规划/故障录波.md @@ -7,7 +7,7 @@ ## 1. 现状一句话 -全仓**没有任何录波代码**:`App/` 下 grep 无实现,`Drivers/BSP/UI/ui_menu_tab.c:41` 那条「手动录波」叶子(菜单第 26 叶)**至今未绑内容页**;产品需求把「录波文件存储」「文件传输(录波)」两行标为未实现/未开始;旧口径把录波挂在「工程量口径实测闭环之后」,本波用**原始码值**解掉这个依赖(`docs/子系统设计/故障录波设计说明.md` §3.2)。 +**数据面与接线已在仓内,板级判据一轮未跑**(诚实口径,2026-10-29 更新):录波环与段格式编解码在 `App/algo/wave_ring.c` 与 `App/algo/wave_store.c`,压环与落盘与保护启动接线、以及两条 UI 页的代码侧判据已过(三套主机单测 100 / 170 / 110 全 rc=0、双端编译 0 错误 0 警告、板端编码器与 PC 解码器两份独立实现字节级互验通过;「手动录波」第 26 叶与「录波回放」两叶均已绑页,`UI_PAGE_MAP_COUNT` = 30,内容页黄金参考 60 → 66 张含录波 6 态)。**没做完的是板级那一轮**:注 1 V / 50 Hz、FTP 拉回、与 `meas:` 行和 104 浮点 YC 对表、CRC 不符必须拒收的实测、真机上看回放页放一段真波;产品需求把「录波文件存储」「文件传输(录波)」两行的更新排在台架轮次之后。旧口径把录波挂在「工程量口径实测闭环之后」,本波用**原始码值**解掉了这个依赖(`docs/子系统设计/故障录波设计说明.md` §3.2)。 三条已裁定口径(用户裁定 2026-10-29,理由见`docs/子系统设计/故障录波设计说明.md` §1.2 与 §2): @@ -23,11 +23,13 @@ | 卡 | 内容 | 退出判据 | 资源 | 依赖 | |---|---|---|---|---| -| **录波数据面最小闭环** | 新增专用录波环与段格式编解码(`docs/子系统设计/故障录波设计说明.md` §5 到 §7);bench 构建定周期自抓;FTP 拉回;仓内新增一个纯标准库 PC 脚本读段、画图、重算全周傅氏 | 注入 1 V / 50 Hz 正弦 → 拉回段文件 → PC 脚本量出周波数、幅值、极性与注入一致;削顶路数与头里 `clip_mask` 一致;**PC 重算的基波有效值与板上 `meas:` 行、104 浮点 YC 逐个对表通过**;段文件 CRC 不符时脚本必须拒绝而不是画出东西 | 🟡+🟢(需信号发生器与板,不需标准源) | 无(可与固件其他道并行;独占构建锁) | -| **手动触发与保护启动接线** | 触发状态机接两路真实来路:UI「手动录波」操作页(第 26 叶绑页,形态沿用已规划好的操作页确认模式)+ `App/algo/prot_elm.c` 的 `picked` 上升沿 | 面板按一次出一段;造一次过流,段号与该时刻 `/log/start.log` 事件能对齐;冷却期内二次触发只加 `ovf` 计数、不重录;冻结窗口内 104 上送与保护判据不受影响(**这条是"丢录波样本不丢采集"的验收**) | 🟡+🟢+UI 道(菜单树与字库是整文件生成物,须单写者) | 上一张卡 | -| **现场单通道回放页** | 新增「录波回放」叶子与内容页:单通道 2 周波 40 点、阶梯线、0 线与峰值标注、单位徽标、削顶标记、空态文案(`docs/子系统设计/故障录波设计说明.md` §8) | `ui-verify` 五步全过;回放页入 `ui-golden` 黄金参考;远端看图能拍到此页(只作留证,不当波形通路);**空态与 CRC 失败态绝不允许画成一条直线**;菜单叶数与字库生成物同步重跑且 `generated-fresh` 一致 | 🟢+UI 道(模拟器与黄金参考) | 上一张卡 | +| **录波数据面最小闭环**〔代码侧完成 / 板级待台架〕 | 新增专用录波环与段格式编解码(`docs/子系统设计/故障录波设计说明.md` §5 到 §7);bench 构建定周期自抓;FTP 拉回;仓内新增一个纯标准库 PC 脚本读段、画图、重算全周傅氏 | **代码侧判据已完成**:三套主机单测 100 / 170 / 110 全 rc=0(环 100、段格式 170、触发状态机 110)、双端编译 0 错误 0 警告、板端编码器与 PC 侧解码器两份独立实现**字节级互验通过**(同一份字节两边同解)。**下列判据要板子与台架,一轮未跑**:注入 1 V / 50 Hz 正弦 → 拉回段文件 → PC 脚本量出周波数、幅值、极性与注入一致(**绝对相位不可比**,段内相位以点 0 为参考、触发瞬间在段内点 159,与发生器无相位锁定关系,可核对的是相对相位与极性——见 docs/子系统设计/故障录波设计说明.md §4.2 与 §10);钉轨路数与头里 `clip_mask` 的合格判据是「头位图 ⊇ 实测钉轨路数,且差异必须能由注入条件解释」(`clip_mask` 按来源派生、非本段实测,V0.2 后该位恒 0、这条退化为只查实测);**强观察项**:V0.1 下这五路做交流注入时反而没钉轨 → 先查接线或该路未驱动,属告警面不属失败面;**PC 重算的基波有效值与板上 `meas:` 行、104 浮点 YC 逐个对表通过**;段文件 CRC 不符时脚本必须拒绝而不是画出东西 | 🟡+🟢(需信号发生器与板,不需标准源) | 无(可与固件其他道并行;独占构建锁) | +| **手动触发与保护启动接线**〔代码侧完成 / 板级待台架〕 | 触发状态机接两路真实来路:UI「手动录波」操作页(第 26 叶绑页,形态沿用已规划好的操作页确认模式)+ 保护启动的元件 picked 上升沿(落点 `App/task/protect_task.c` 的 `s_log_record_edge()`,不是判据核本身) | **代码侧已完成**:保护启动来路与并发语义(同拍多请求只录一段、冷却期内二次触发只加 `ovf`、冻结窗口只丢录波样本不丢采集)已在主机单测里钉住,双端编译 0 错误 0 警告。**下列属板级未完成**(第 26 叶已于 2026-10-29 绑页,模拟器侧手动触发路径已有黄金用例;剩下的必须在真机面板上按):面板按一次出一段;造一次过流,段号与该时刻 `/log/start.log` 事件能对齐;冷却期内二次触发只加 `ovf` 计数、不重录(**同时看 `armed` 与那一刻 `/log/start.log` 的时刻,不要只盯 `ovf` 数字**:这两个计数是跨上下文自增、并发拒绝时可能少计一次,容差见 docs/子系统设计/故障录波设计说明.md §9);冻结窗口内 104 上送与保护判据不受影响(**这条是"丢录波样本不丢采集"的验收**) | 🟡+🟢+UI 道(菜单树与字库是整文件生成物,须单写者) | 上一张卡 | +| **现场单通道回放页**〔代码侧完成 / 板级待台架〕 | 新增「录波回放」叶子与内容页:单通道 2 周波 40 点、阶梯线、0 线与峰值标注、单位徽标、削顶标记、空态文案(`docs/子系统设计/故障录波设计说明.md` §8) | **代码侧判据已完成**:两叶绑页(`UI_PAGE_MAP_COUNT` = 30)、`ui-golden` 内容页黄金参考 60 → 66 张(录波 6 态:手动录波 3 态 + 录波回放 3 态),任务树重建模拟器后同一批 66 张连跑三次逐字节一致(页面池水位 5,448 B)、菜单侧 8 状态连跑三次一致(5,368 B),`generated-fresh` 退出码 0(菜单树与中文字库生成物与源一致),双端编译 0 错误 0 警告。**下列判据要板子与台架,一轮未跑**:真机上放一段真波(不是模拟器合成节拍)、远端看图能拍到此页(只作留证,不当波形通路)、**空态与 CRC 失败态绝不允许画成一条直线**要在真机上用坏段复现一次 | 🟢+🟡+UI 道(模拟器与黄金参考) | 上一张卡 | | 二期(不在本波) | 4 kHz 全速与 80 点/周波、突变量启动、COMTRADE、104 文件传输、分段与参考机形态(录波 65 通道、多类型多段) | 不在本波验收面内;重开条件见 §4 | 🔴/独立立项 | — | +**落盘道的失败留痕名(单板上一律一行 `[WAVE] seg drop: <原因> (idx=… written=…)`,统一打印点 `App/task/default_task.c:816`)**:`crc selftest FAIL` / `mkdir /wave` / `opendir /wave` / `readdir /wave` / `seg_path` / `pack_start` / `f_open` / `pack_next` / `f_write short` / `pack stall` / `f_close` / `len mismatch` / **`ring abandoned`**。最后一条是 docs/子系统设计/故障录波设计说明.md §5.2 第四条陷阱的处置留痕(写完关档后复检环侧身份不过 → 判废本段、`err` 加一、段号不推进),**代码与主机单测面已有、板级未测**;另有一条非失败的慢段留痕 `[WAVE][WARN] seg held <毫秒> ms (idx=…)`(`App/task/default_task.c:831`)——搬得慢不是错,真失败才走 `seg drop`。 + **为什么录波排在信号发生器动态验证之前**(用户裁定):动态验证真正缺的不是信号源,是**样本出口**——录波段文件一旦有了,动态验证就从「比对几个派生标量」升级成「PC 独立重算 vs 板上读数」的对表,同一套台架接线两次都用得上,不必再造一条串口通路。`docs/子系统设计/故障录波设计说明.md` §11 记了因此而变的三处口径。 --- diff --git a/docs/任务规划/数据可信.md b/docs/任务规划/数据可信.md index 384f7959..037e6e1f 100644 --- a/docs/任务规划/数据可信.md +++ b/docs/任务规划/数据可信.md @@ -325,7 +325,8 @@ typedef struct { | 串口每 10 s 心跳(`App/task/adc_task.c` 心跳块) | `adc:`(9 路码值)、`cal:`(三路 mV + `vl`/`cl`/`rail`/`edge` 位图)、`phasor(hb)`(逐实例 `Re` / `Im` / `rms`)、`freq(hb)`、`meas:`(快照轨) | MicroLIB 无 `%f`,全是定点:`phasor` ×0.1(**1 V 注入只剩 10 格**)、`freq` ×0.01 Hz、`cal:` / `meas:` ×1000(1 mV)。10 s 一行的单点瞬时值对 50 Hz 等价于随机相位抽样 | | 104 YC(现成 PC 主站 `test/host/iec104_master.py`) | `0x4011`~`0x4018` 八个电气量 + `0x401D` 频率,**浮点无量化**,还带品质 | UB/UC/IB/IC/IN 恒 IV、值清零 → 那五路只能回读串口码值域 | | UI 交流量页 | `Ua` / `U0` / `Ia` / `Ia2` / `Ia5` / `Hz` / 标定徽标 共 7 行 | 160×160 单色;`--` **同时**表示"没数"和"有数但不可信" → 不能当判据,只能当旁证 | -| **板上没有的(本波现状)** | 把窗里 20 / 40 个点导出来的任何出口:无命令控制台、无 dump、当前还不落文件 | 所以现在只能靠**派生标量**证;要看形状就拿示波器直接看注入点(只对照发生器,别拿它当判据)。**这条限制是临时的**:故障录波已立项,段文件就是现成的样本出口,届时判据升级为「PC 独立重算 vs 板上读数」对表(见 `docs/子系统设计/故障录波设计说明.md` §11) | +| **段文件样本出口(代码侧已落地,2026-10-29)** | 每次触发一段:`/wave/WAVE00.BIN` 起共 100 个号轮转(FTP 根下可见),8 路 × 320 点原始码值加封存瞬间的标定与品质快照;PC 侧 `test/host/wave_read.py` 提供 dump / fft / shape / plot 四个子命令(bench 工具,纯标准库,自带 `--selftest`) | 「比对派生标量」已可升级成「PC 独立重算 vs 板上读数」对表(判据与执行步骤见 `docs/测试说明/ADC前端注入验证流程.md` §4,格式属主见 `docs/子系统设计/故障录波设计说明.md` §6.2 与 §6.3)。**但板级那一轮还没跑**(注 1 V / 50 Hz、与 `meas:` 与 104 YC 对表、CRC 不符必须拒收的实测,见 `docs/任务规划/故障录波.md` 卡片判据);示波器仍旧只作旁证、不当判据。段内点序:触发瞬间落在段内点 159(预触发 160 点的最后一点),段的绝对相位与发生器无锁定关系,可核对的是相对相位与极性 | +| **仍然没有的(2026-10-29 更新)** | 没有命令控制台、也没有「把窗里 20 / 40 个点导出来」的定点 dump 子命令——**但样本出口本身已成立**(上一行的段文件:一次 320 点 / 8 路,比原来规划的 20 / 40 点窗更宽) | 现场取回一律走现成 FTP(21 端口),形状就地看回放页单通道、或在 PC 侧用同一脚本出图;示波器只对照发生器、不当判据。**判据已升级为「PC 独立重算 vs 板上读数」对表**,对表的执行轮次待台架(见 `docs/子系统设计/故障录波设计说明.md` §11) | #### 4.6.4 接法与红线(照抄既有红线,不新发明) @@ -443,7 +444,7 @@ flowchart TD | THD / 谐波方和根真有效值、快照 4 点平均 | 挂 `docs/算法/继电保护功能复现规划.md` 的「遥测谐波轨(可选)」一起做;快照保持"基波有效值 + 直取相量"口径(§4.5) | | **谐波轨的消费出口**(C04 / C05 目前无人消费) | D6 只证"算得对";接线到判据或上送属谐波轨立项范围,本波不动 | | 端子级额定电压、绝缘耐压、温升 | 要标准源 / 型式试验条件;本波所有注入都在 `U5` 排针侧 ±2 V 以内 | -| 录波(原始码值段文件) | **已独立立项**(2026-10-29):`docs/任务规划/故障录波.md`;本卡「动态波形验证」的样本出口将改由录波段文件承担,排序也随之调整(`docs/子系统设计/故障录波设计说明.md` §11) | +| 录波(原始码值段文件) | **已交付代码面**(2026-10-29 立项并当日落地):`docs/任务规划/故障录波.md`;本卡「动态波形验证」的样本出口**已**由录波段文件承担(§4.6.3 出口表已改),排序也随之调整(`docs/子系统设计/故障录波设计说明.md` §11);**没跑的是板级那一轮**(§10) | | 104 文件传输(规约侧读文件) | 仍独立待排;现场与台架取回一律走现成 FTP(21 端口),规约侧文件服务等送检或第三方交接需求再议 | | 等周波调频 | 与标定正交、无标准源时判据不可达,见 `docs/算法/继电保护功能复现规划.md` §3.1 | | 功率 / 相位跨相比较 / 功率因数 / 对称分量 | **V0.1 硬件禁止**:跨转换器无同一时刻性,`docs/硬件平台/ADC通道映射说明.md` §7 通道归属行 | @@ -461,7 +462,7 @@ flowchart TD |---|---| | **`docs/测试说明/ADC前端注入验证流程.md`** | **执行步骤的属主**。本文只写"证到哪、判据是什么、为什么这么排";怎么接表笔、读哪一行、填哪张表一律在那篇里。动态验证的**步骤**同样落那篇(新开"交流"节),本文 §4.6 只留判据 | | **通道映射说明** | `docs/硬件平台/ADC通道映射说明.md` §7.1(①②③④ 链路分段)与 §8.1(静态六项实测)是**量纲与实测记录的属主**;本文 §2.1 / §2.6 是它的**消费者**,不另立口径,动态实测结论也回写该文件 | -| **`docs/子系统设计/故障录波设计说明.md`** | 录波段文件是本卡「动态波形验证」的**样本出口**:录波落地后判据从「比对派生标量」升级为「PC 独立重算 vs 板上读数」对表,动态验证的排序也因此让位给录波数据面(用户裁定 2026-10-29,理由与三处口径变更见该文 §11) | +| **`docs/子系统设计/故障录波设计说明.md`** | 录波段文件是本卡「动态波形验证」的**样本出口**,数据面代码侧已落地(2026-10-29:环与段格式在 `App/algo/wave_ring.c` / `App/algo/wave_store.c`,PC 侧 `test/host/wave_read.py`);判据从「比对派生标量」升级为「PC 独立重算 vs 板上读数」对表的**执行轮次待台架**,动态验证的排序已因此让位给录波数据面(用户裁定 2026-10-29,理由与三处口径变更见 docs/子系统设计/故障录波设计说明.md §11) | | **`docs/算法/采集与算法层设计说明.md`** | 算法与判据正文(全周傅氏、测频、保护元件);本文只规定"这些算法的信度到什么程度" | | **`docs/子系统设计/IEC104上行设计说明.md`** | 动态验证的**读数出口**(`0x4011`~`0x4018` + `0x401D`);送检验收索引在 `docs/任务规划/登记簿.md` §7 | | **送检验收** | 标准源送检依赖本文「两点标定与精度验收」;顺序不可倒(先变比 → 再标定 → 才解卡 → 才送检,§6) | diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index e849d09c..8f6fec22 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -47,10 +47,10 @@ dhara 失败类 +84;RO 另 +208,全是该失败类名表)→ `ER_IROM1` | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 264,580 B (258.38 KB) | 1,048,576 B (1024 KB) | 25.2% | -| RW_IRAM1 | 81,536 B (79.62 KB) | 131,072 B (128 KB) | 62.2% | +| ER_IROM1 | 272,336 B (265.95 KB) | 1,048,576 B (1024 KB) | 26.0% | +| RW_IRAM1 | 89,936 B (87.83 KB) | 131,072 B (128 KB) | 68.6% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **266,456 B (260.2 KB)** | 1,048,576 B (1024 KB) | **25.4%** | +| **Flash (ROM 合计)** | **274,308 B (267.9 KB)** | 1,048,576 B (1024 KB) | **26.2%** | 各区的容量与常量来源: diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index 54b8cf8d..a8494ebf 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,19 +15,19 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 264,580 B | 1,048,576 B | 25.2% | 参考 | +| ER_IROM1 (Flash) | 272,336 B | 1,048,576 B | 26.0% | 参考 | | RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS | | UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 | -| RW_IRAM1 | 81,536 B | 131,072 B | 62.2% | PASS | +| RW_IRAM1 | 89,936 B | 131,072 B | 68.6% | PASS | | 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 | |---|---|---|---| | 网络 | 13,702 B | 12 KB | ⚠ 超 1.4 KB | | 存储 | 7,072 B | 12 KB | OK | -| UI | 34,656 B | 16 KB | ⚠ 超 17.8 KB | +| UI | 36,428 B | 16 KB | ⚠ 超 19.6 KB | | RTOS 堆 | 57,376 B | 56 KB | 堆=配额 | -| 应用 | 15,552 B | 8 KB | ⚠ 超 7.2 KB | -| 其它 | 10,553 B | — | 参考 | +| 应用 | 16,888 B | 8 KB | ⚠ 超 8.5 KB | +| 其它 | 15,849 B | — | 参考 | - **门禁**:pre-commit `mem-budget` 钩子(`scripts/ram_baseline.py --gate`)—— @@ -64,7 +64,9 @@ UI 16 KB / RTOS 堆 56 KB / 应用 8 KB**。归集口径(按 `.map` 对象名 `heap_4.o` 另含 32 B 元数据,不计超);**改堆 = 改配额**,必须先过本文件 §6 登记。 - **应用 ⚠ 小幅超**:8 KB 为报告规划值;2026-10-10 栈整改把 3 处 `FIL` 从栈搬进 BSS (静态 +4,128 B,见 `docs/内存管理/RAM使用分析与优化方案.md` §4"2026-10-10 复测")—— - 属已评审的"以空间换栈安全",差额在此如实体现。 + 属已评审的"以空间换栈安全",差额在此如实体现。2026-10-29 故障录波落盘道又把 `default_task.o` + 推高 1,204 B(该行现值 16,888 B);**录波环本体 5,300 B 不计入本行**——`subsystem_of()` + 没有 `wave_` 前缀,它落在「其它」,要读全貌看 `docs/内存管理/内存内容分配策略.md` §6 的 per-file 台账。 ## 3. CCM 分配台账(谁能用 CCM) @@ -96,3 +98,4 @@ UI 16 KB / RTOS 堆 56 KB / 应用 8 KB**。归集口径(按 `.map` 对象名 | 2026-10-12 | 建档:三件套落地(本台账 + `scripts/ram_baseline.py` + `scripts/ram_baseline.tsv` + pre-commit `mem-budget` 钩子) | 登记簿 N-27 前半 | | 2026-10-12 | **RTOS 堆配额 48 KB → 56 KB**(`QUOTAS` 同步) | B5 protectTask 立项:iec104Task(2.5KB)+protectTask(2KB)+TCB 入堆后 48KB 池 min_ever 预估跌破 3,072 B 告警线 → `.ioc` 语义取数与有效性参数 `configTOTAL_HEAP_SIZE` 49152→57344(CCM 水位 75.3%→87.9%,仍 <92% 门禁;CCM 空闲降至 ≈8 KB)。连带影响见 §3 迁 CCM 后半链注记 | | 2026-10-12 | **任务栈 +1536 B(堆内用量,配额 56 KB 不变)**:defaultTask 1024→1280 words(+1024 B)、timeSyncTask 512→640 words(+512 B);产品任务栈合计 37,632→39,168 B | 登记簿 N-33 栈收口:B7 定值持久化把 defaultTask 静态链推到 3016 B(余 1080 < 30% 线)、timeSyncTask 链 1264 B(余 784 < ISR 底线 1024);B6 台架背书 min_ever=10000 → 抬后实测 `heap free=16400 / min_ever=8464`(与预算 −1536 完全吻合,>3072 告警线);`stack_report` 9 任务 0 FAIL + `@test` PASS,见 `RAM使用分析与优化方案.md` §1.4 | +| 2026-10-29 | **故障录波数据面入册**:新增对象 `wave_ring.o` 5,300 B(ZI 5,256 + RW 44,落 SRAM1、CCM 增量 0);`default_task.o` 静态 2,736 → 3,940 B(+1,204 B,落盘道 `FIL` 528 + 分片缓冲 256 + 段头副本 136 + 段路径 17 + 打包上下文与 `DIR`/`FILINFO`);`RW_IRAM1` 81,536 → 89,936 B(62.2 % → 68.6 %,仍远低于 92 % 门禁);`RW_CCM` 三档恒 57,376 B | 决定人 lead。实测数字出自 lead 的权威构建(本工作树 `MDK-ARM/STM32F407-Demo/STM32F407-Demo.map`,0 错误 0 警告,`scripts/ram_baseline.py --update` 同批入 tsv);预估约 5.9 KB vs 实测数据面 +6,760 B 的差额构成与三档对照见 `docs/内存管理/内存内容分配策略.md` §6;**未抬栈**(`scripts/stack_report.py` 九任务全 PASS),判据出处 `docs/子系统设计/故障录波设计说明.md` §7 | diff --git a/docs/内存管理/内存内容分配策略.md b/docs/内存管理/内存内容分配策略.md index ec46e469..70f88ad8 100644 --- a/docs/内存管理/内存内容分配策略.md +++ b/docs/内存管理/内存内容分配策略.md @@ -110,17 +110,35 @@ CCM 64KB 只能容纳 {FreeRTOS 堆 48KB} 与 {LVGL 集合 ~52.5KB} 之一(两 触发条件:测试构建 SRAM 余量不足,**且**已耗尽 `RAM使用分析与优化方案.md` 的帧副本去重(−6.4~9.6KB)与 LVGL 池缩减等低代价手段。 流程:改 scatter → 走 §7 完整验证阶梯 → 同步更新本文档、`AGENTS.md`、`.sct` 头注释。在此之前,方案 A 是唯一有效布局。 -## 6. 当前容量台账(2026-10-12,产品构建;2026-09-21 最坏构建=存储套件的历史基线见 `docs/内存管理/RAM预算台账.md` 趋势) +## 6. 当前容量台账(2026-10-29,产品构建 task/wave-record 分支终值;2026-09-21 最坏构建=存储套件的历史基线见 `docs/内存管理/RAM预算台账.md` 趋势) | 区域 | 总量 | 已用 | 空闲 | 驻留内容 | |---|---|---|---|---| -| RW_IRAM1(SRAM) | 131,072B (128KB) | 95,816B (~93.6KB, 73.1%) | 35,256B (~34.4KB) | 全部静态(除堆),含 LVGL 集合 53,805B | -| RW_CCM | 65,536B (64KB) | 57,376B (56KB, 87.9%) | 8,160B (~8KB) | **仅** `heap_4.o`(ucHeap 56KB,2026-10-12 B5 扩容) | +| RW_IRAM1(SRAM) | 131,072B (128KB) | 89,936B (87.83KB, 68.6%) | 41,136B (~40.2KB) | 全部静态(除堆),含录波环 `wave_ring.o` 5,300B 与 LVGL 池 16,384B(`lv_mem_core_builtin.o` 实测) | +| RW_CCM | 65,280B(64KB 减 256B `UNINIT_CCM` 尾,与 `.sct` 一致) | 57,376B (56.03KB, 87.9%) | 7,904B | **仅** `heap_4.o`(ucHeap 56KB,2026-10-12 B5 扩容);256B 尾区现用 48B | + +数字身份:`RW_IRAM1` / `RW_CCM` / `ER_IROM1` 三行取自 lead 2026-10-29 权威构建(本工作树 `MDK-ARM/STM32F407-Demo/STM32F407-Demo.map`,0 错误 0 警告,`scripts/ram_baseline.py --update` 同批入 tsv)。受管快照区在 `docs/内存管理/RAM使用分析与优化方案.md`,子系统配额表在 `docs/内存管理/RAM预算台账.md`,两处由脚本渲染、本卡不手改。 LVGL 集合明细(均在 SRAM):LVGL 池 16,384B(`lv_mem_core_builtin.o`,`LV_MEM_ADR=0` 内部静态)+ `g_ui_draw_buf` 3,232B(3200+32 保护带)+ `g_ui_fbuf` 3,200B + `g_tx` 3,209B + `g_lcdmem_data` 3,204B。 产品构建比套件构建少 testTask/测试缓冲(约 40KB 级),SRAM 余量更大。数字以 map 实测为准,布局变更后必须重测并更新本表。 +录波支路静态(2026-10-29 入册,R6 要求;**全部落 RW_IRAM1、无 section 属性,CCM 增量 0**): + +| 归属对象 | 增量 | 构成 | +|---|---|---| +| `wave_ring.o`(`App/algo/wave_ring.c`) | 5,300 B(ZI 5,256 + RW 44) | 录波环 int16×320 点×8 路 = 5,120 B 主体 + 已封存段头副本 136 B + 状态/索引/观测计数;map 比代码侧手算多 2 B,是链接器对齐 | +| `default_task.o`(落盘道,函数内 `static`) | +1,204 B(2,736 → 3,940,同 tsv 的 RW+ZI 口径) | `FIL` 528 B(`FF_FS_TINY=0`,刻意不上栈)+ 分片搬运缓冲 256 B + 段头副本 136 B + 段路径 17 B + 打包上下文/`DIR`/`FILINFO`/状态字节约 267 B(对象级增量,未逐项拆分实测) | +| `adc_task.o` 及其余数据面 | 约 256 B | 8 路语义原码暂存与封存瞬间交棒结构(`App/task/adc_task.c`);与上两行相加 = 数据面实测 +6,760 B | +| UI 道:`ui_page_wave_play.o` 92 B、`ui_page_wave_manual.o` 28 B、`ui_task.o`、`ui_data.o` | 区级合计 +1,640 B | 两个新页面模块按 map 计 120 B;`ui_task.o`/`ui_data.o` 的增量拆分需要本波之前的 per-file 快照,本卡没有 → **只记区级增量,不猜分项** | + +- **三档对照(SRAM1)**:81,536 B / 62.2 %(`scripts/ram_baseline.tsv` 2026-10-28 行)→ 88,296 B(录波数据面终值)→ **89,936 B / 68.6 %**(含 UI 道终值),累计 **+8,400 B**;`RW_CCM` 三档恒 57,376 B —— 设计文档 `docs/子系统设计/故障录波设计说明.md` §7 的「不进 CCM」落位达成,也没有挤动 CCM。 +- **预估 vs 实测都要留**:设计文档 §7 原估静态增量约 5.9 KB(环 5,120 + 索引状态 32 + 双缓冲 512 + 读窗 256),实测数据面 +6,760 B。差额构成:`FIL` 528 B、`DIR`/`FILINFO`、段头副本 136 B、打包上下文(内含一份 136 B 段头)原估未记账;反向一项——落盘实际是**单缓冲 256 B**,不是预估的 512 B 双缓冲。只写实测会让下一次估算照着 6.7 KB 走、看不到原估漏了哪几项。 +- **Flash**:`ER_IROM1` 264,580 → 270,248(数据面 +5,668 B)→ 272,300(UI 道 +2,052 B)→ 272,360(落盘判废复检 +60 B)→ **272,336 B**(cppcheck 返工:把变量声明搬进最小作用域,纯搬声明 −24 B),累计 **+7,756 B**、占 26.0 %;`Total ROM` 274,308 B(26.2 %)、`Code` 243,552 B。RAM 三项(RW 1,972 / ZI 145,388 / RW_IRAM1 89,936 B / 68.6 %)与 CCM 57,376 B / 87.9 % 均未变。 +- **栈:9 任务全 PASS、未抬栈**(`scripts/stack_report.py` 静态最坏链)——adcTask 1,424 B(余 1,136 B / 44 %)、defaultTask 3,040 B 不变、protectTask 880 B、uiTask 1,336 B(余 15,048 B / 91 %)。R6 三处同步里 `AGENTS.md` 栈表这一处的结论是**核对过、数值未变**。 +- **为什么比上一版低**:本表 2026-10-12 版记 95,816 B / 73.1 %,高于今天的 89,936 B。原因是此后 LVGL 内部池由 40 KB 缩到 16 KB(`docs/内存管理/RAM预算台账.md` §3「缩池 ✅ 2026-10-15」行;`scripts/ram_baseline.tsv` 2026-10-12 与 2026-10-24 两行的 `top_rwzi` 列可见 `lv_mem_core_builtin.o` 40960 → 16384)——**不是本波回收了什么**。 + +- **子系统归集的坑(本卡实测,值得记一笔)**:`scripts/ram_baseline.py` 的 `subsystem_of()` 按对象名前缀首匹配,**表里没有 `wave_` 前缀** → 录波环 `wave_ring.o` 5,300 B 落进 **「其它」(15,853 B,参考值、无配额)**,配额 8 KB 的「应用」行看不见它;落盘道 `default_task.o`(+1,204 B)与 `adc_task.o` 才计入「应用」(现值 16,888 B,本波前该行已在配额外,成因见 `docs/内存管理/RAM预算台账.md` §2「应用 ⚠ 小幅超」条)。结论:**别拿子系统表当录波预算的判据**,要按本节 per-file 台账读。要让配额表真看见录波支路,得给 `subsystem_of()` 加 `wave_` 前缀——那是脚本改动,本卡不动,已报 lead。 ## 7. 布局变更验证阶梯(强制执行) 任何 scatter / CCM / 堆大小变更后,依次通过以下全部关卡方可认定生效: diff --git a/docs/子系统设计/故障录波设计说明.md b/docs/子系统设计/故障录波设计说明.md index 2d1917a1..046f4822 100644 --- a/docs/子系统设计/故障录波设计说明.md +++ b/docs/子系统设计/故障录波设计说明.md @@ -1,8 +1,8 @@ # 故障录波设计说明(1 kHz · 8 路原始码值 · 现场单通道回放) -> **模块名称**:故障录波——采样源沿用 TIM3 1 kHz 节拍,专用录波环(拟新增 `App/algo/wave_ring.c`)、触发、落盘(defaultTask 是文件系统唯一写者)、取回(FTP 21 端口)、现场回放(UI 单通道页)。 +> **模块名称**:故障录波——采样源沿用 TIM3 1 kHz 节拍,专用录波环(`App/algo/wave_ring.c`,已落地)、触发、落盘(defaultTask 是文件系统唯一写者)、取回(FTP 21 端口)、现场回放(UI 单通道页)。 > **文档定位**:本波的**设计属主**——口径、通道集、数据形态、触发模型、文件格式、预算与所有权边界、显示契约、二期不返工的结构约束在此定稿。**排期与卡片判据在 `docs/任务规划/故障录波.md`**,本文不写排期。 -> **状态**:P0 设计定稿(2026-10-29),**代码一行未动**。三条已裁定口径见 §1.2。 +> **状态**:设计已定稿,**数据面 + 触发与落盘接线 + UI 两个页面(手动录波、录波回放)的代码面已交付(2026-10-29)**——主机单测 113 / 170 / 110 全 rc=0、双端编译 0 错误 0 警告、板端编码器与 PC 侧解码器两份独立实现字节级互验通过。**没做完的只有板级判据那一轮**(注量对表、FTP 拉回、与 `meas:` 和 104 浮点 YC 对表、CRC 不符必须拒收的实测、回放页放一段真波、两个待台架核数值),逐条列在 docs/任务规划/故障录波.md 各卡的板级段。三条已裁定口径见 §1.2。 > **相关文档**: > `docs/子系统设计/ADC采样节拍设计说明.md`(节拍与时序账的属主;本文 §3 的数据源就是它) > `docs/硬件平台/ADC通道映射说明.md`(网名 `AINn` 不等于板级 `ADCn`,也不等于转换器通道号;链路分段与削顶归属) @@ -106,16 +106,19 @@ | 触发 | 来源 | 本波 | |---|---|---| -| **手动** | UI「手动录波」操作页(该叶子早已在菜单里:`ui_menu_tree.txt` 装置维护组,现未绑页) | 做 | -| **保护启动** | `App/algo/prot_elm.c` 的 `picked` 上升沿(元件启动),与 `/log/start.log` 同源,避免两处各自判定 | 做(复用事件点,不新增判据) | +| **手动** | UI「手动录波」操作页(叶子在菜单装置维护组,页面模块 `Drivers/BSP/UI/ui_page_wave_manual.c`,2026-10-29 已绑页;确认键在 `App/task/ui_task.c`:582 行调 `wave_ring_request()`,走手动来路、元件号填 0) | 做(已落地) | +| **保护启动** | 落点在 `App/task/protect_task.c` 的 `s_log_record_edge()`(函数体自 158 行起,picked 上升沿分支 170 到 177 行,实际调用在 176 行:`wave_ring_request(WAVE_TRIG_PROTECT, 最低位上升元件)`),与 SYSLOG_START 同一条边沿、同一事件点,避免两处各自判定。**刻意不落在 `App/algo/prot_elm.c`**:判据核被三套主机单测源码级纳入编译,往它加录波依赖会一次全破——这条就是「判据核零改动」的前提。多元件同拍启动只录一段(环不重入,后续请求由环计 ovf,不算失败) | 做(复用事件点,不新增判据) | | 定周期自抓 | `DTU_BENCH` 构建下每 N 秒强制一段,供信号发生器对表 | 做,**只在 bench 构建**,产品构建无此路径 | | **突变量启动** | 需要增量化 `Δi` 类算法与定值 | 不做,归二期(`docs/算法/继电保护功能复现规划.md` 的「故障启动(波形启动)」行) | ### 4.2 段长与冻结语义(RAM 就靠这一条省下来) - 一段 = **预触发 8 周波 + 后触发 8 周波 = 320 点/路**(1 kHz 下 320 ms); +- **段内点序**(两份实现共同遵守,PC 与回放页都按这条):触发那一拍的样本落在**段内点 159**(预触发 160 点的最后一点),后触发第一拍是**点 160**,两半不重叠,故障时刻就是这条分界。因此段内点 0 相对工频周期边界有任意偏移(触发瞬间在周期内任意落点),**段字节里没有绝对时间参考**——能核对的是相对相位与极性,绝对相位只能以段内点 0 为参考报出(§10 板级判据据此不比绝对相位); - **触发即冻结环**:停止推进录波写指针,defaultTask 直接从已冻结的环落盘,落完解冻; - **冻结期间丢的是录波样本,不是采集**——`sample_ring`、相量、保护判据照常跑(这就是 §2.1 第 3 条要求解耦的原因),冻结不拖慢保护动作,也不让 104 断数; +- **解冻的唯一入口是落盘方回执**(`wave_ring_write_done(result)`,只在 defaultTask 上下文调):写成功才推进轮转段号,写失败只计 err 且**下一段重写同一个号**(不留半截文件);冷却期从回执之后才开始计时,回执前新触发一律只加 ovf。「落完解冻」这条原本按时间窗口的假设,实现里改由回执保证,冷却数值只是二次抑制。 +- 兜底(绝不允许录波把环焊死):封存态滞留超过 `WAVE_SEAL_TIMEOUT_MS`(当前 2000 拍 = 2 s,**待台架核**)就自弃本段、计 err 并解冻——卡在一次落盘上,代价是下一次真故障无波可录。 - 代价:一次触发的落盘窗口(约几十 ms)内再来触发只记第一次(冷却期),**不做多段排队**——那是二期「6 段 / RAM 16 个」的形态。 ```mermaid @@ -150,13 +153,14 @@ flowchart TD | **uiTask**(回放页) | 只读窗口取段数据 → 自绘(与 `App/log/soe_store.c` 读侧同构) | 不写文件 | | **netTask / lftpd** | 把 /wave 暴露给 FTP 取回 | 不在 netTask 里读录波环 | -### 5.2 三条已知陷阱,本波必须绕开 +### 5.2 四条已知陷阱,本波必须绕开 | 陷阱 | 事实出处 | 本波处置 | |---|---|---| | **取窗竞态** | `App/algo/sample_ring.c` 的 `sample_ring_window()` 没有临界区(读 `s_head` 后逐点拷),现在只有 adcTask 同任务调用才安全 | 录波**不复用它**:adcTask 在自己的节拍里取「本拍帧」压专用环;跨任务只传递「已封存」标志,杜绝跨任务读活缓冲 | | **FatFs 追加写** | 该移植 `FA_OPEN_APPEND` 不含 `FA_WRITE`,且 `f_write` 簇分配失败时返回 `FR_OK` 加短写、无错误码(`docs/架构设计/STM32F4-Base存储架构说明.md` §10.1) | 打开一律 `FA_WRITE|FA_OPEN_APPEND`;**判成功必须比对写回字节数**,短写按失败处理(丢段 + 日志),绝不当成功记账 | | **只能 8.3 短名** | 出处同一张存储架构说明的短名限制:`FF_USE_LFN=0`,不得以点开头、不得长名 | 文件名 = WAVE 加两位十进制再加 .BIN,共 100 个名字轮转覆盖;目录 /wave(与 /log 平级,FTP 根下可见) | +| **落盘途中环被解冻、甚至已换段**(实现期复核新增第四条,2026-10-29) | adcTask 优先级高于 defaultTask:搬段窗口里 adcTask 可能正好走到 SEALED 滞留 2 s 自弃,甚至已经封出下一段。**数据区 CRC 是对「本道实际读到的字节」算的**,半新半旧的段照样自洽——长度等式照过、两个 CRC 照对,文件骗得过任何读者,也骗过我们自己 | 写完关档后**复检环侧身份**:`wave_ring_sealed()` 仍为 1,且 `wave_ring_get_stat()` 的 `last_t0_ms` 与 `last_seq` 跟本道开始搬时那份段头一致;不符即走既有失败路——unlink 本道开过的文件 + `write_done(-1)`(此时多半回 `E_STATE`,环侧 `err` 已由自弃计过,不重复补账)+ 段号不推进(下一段同号重写覆盖)+ 单条 `[ERR] … seg drop: ring abandoned` 留痕。落点 `App/task/default_task.c`:786 到 796 行。**当前只有代码与单测面,板级未测**;可观察项见 §10 板级行 | ![录波数据的四个所有者与跨任务边界:adcTask 只写环,defaultTask 独占落盘,uiTask 与 PC 只读](../images/故障录波数据流.svg) @@ -190,16 +194,44 @@ flowchart TD | 14 | t0_ms | 4 | 段首相对上电时刻;配合墙钟还原绝对时刻 | | 18 | wall | 4 | 对时后的 epoch 秒(未对时为 0) | | 22 | eng | 1 | 封存瞬间的折算档位:0 引脚域、1 二次值、2 一次值 | -| 23 | flags_seq | 1 | 有效性位图(引 `adc_cal_flags()` 口径) | -| 24 | clip_mask | 2 | 削顶归属(V0.1 恒 0xE6,见 `docs/任务规划/数据可信.md` §2.7) | +| 23 | flags_seq | 1 | 每路 VALID 压成的位图:bit s = 语义通道 s 的 `ADC_CAL_F_VALID`(电气 8 路恰好占满 1 字节;TEMP 不参与标定,若要带 TEMP 连本行与下一行一起升版本) | +| 24 | clip_mask | 2 | 削顶归属位图:bit s = 语义通道 s,与 `App/algo/meas.h` 同名参数同口径(封存瞬间 `ADC_CAL_F_CLIPPED` 快照,V0.1 恒 0xE6 = UB/UC/IB/IC/IN,见 `docs/任务规划/数据可信.md` §2.7)。⚠ **这个位图的本体是「链路可信性标记(按来源派生)」,不是「本段实测饱和标记」**:置位条件一次样本都不看(`App/algo/adc_cal.c` 291 到 296 行,该路 ROM 系数属片内 ADC 侧即置位);「这一段有没有真钉轨」走另一条链——`App/algo/adc_health.c` 的 `adc_health_rail_watch()`(280 行起)与 `adc_health_rail_sticky()`,门限按满量程百分比由 `adc_cal_rail_ths()` 给、须连续 `ADC_RAIL_N_RUN` 拍。V0.2(8 路全接 TPAFE)后本位自动恒 0,届时「与 clip_mask 对表」这条判据退化为只查钉轨实测(§10) | | 26 | frame_seq | 4 | 段首帧序号,可与串口周期行的 frames 对账 | -| 30 | 通道表 | 每路 12 | 语义号 1 + 来源转换器 1 + 位宽 1 + 有效位 1 + LSB(µV) 4 + k 2(定点)+ b 2(定点) | -| — | CRC16(头) | 2 | 与屏板链路同一张 CRC-16/MODBUS 表,不新造 | +| 30 | 通道表 | 每路 12 | 语义号 1 + 来源转换器 1 + 位宽 1 + 可用性 1(= `adc_cal_flags(sem)` 低 8 位原值,不自造位义)+ `k_q16` 4(int32,µV/码 × 65536)+ `b_codes` 4(int32,码值)。**没有独立 LSB 字段**,LSB 就是 `k_q16`,见 §6.3 | +| — | CRC16(头) | 2 | CRC-16/MODBUS,**低字节在前**(与全文件小端统一);覆盖 `[0, 30+12×ch_count)`,本字段自身紧随其后 | | — | 数据区 | pts_per_ch × ch_count × 2 | **按通道分列**:一路连续 320 点,单通道回放只读那一路 | -| — | CRC16(数据区) | 2 | 长度为 0 或 CRC 不符 → 整段作废,页面必须显示「该段不可读」 | +| — | CRC16(数据区) | 2 | 同上字节序;覆盖整个数据区、自身在文件尾(8 路 320 点整段 5250 B);长度为 0、任一 CRC 不符、或 `ver` 与魔数第三字节不等 → 整段作废,页面必须显示「该段不可读」 | + +**段名轮转的呈现语义**(回放页与 PC 侧取「哪一段最新」都按这条):段表按**数值升序**呈现,开屏落点取**数值最大的段号**。回绕(99 之后回到 00)之后「数值最大」不再等于「时间最新」——要真时间序必须逐段读段头 `frame_seq` / `t0_ms`,代价是 100 次开文件,**本波不做**。所以页顶把段号本身显示出来(§8.1 顶部行),让读者自己看见这是第几号,不去伪造一个「最新」的假象。 + +### 6.3 字段口径裁定(两份独立实现的共同依据) + +本波段格式有**两份独立实现**:板端编码器 `App/algo/wave_store.c` 与 PC 解码器 `test/host/wave_read.py`,两份必须逐字节同解,所以动工前把 §6.2 的含糊处一次钉死(PC 道九问的逐条答复): + +| 问题 | 裁定 | +|---|---| +| CRC 字节序 | **低字节在前**,与全文件小端一致。原稿「与屏板链路同一张 CRC-16/MODBUS 表」不成立:全仓**没有**可复用的 CRC-16 表——`App/util/README.md` 记 `crc.c/h` 已于 2026-10-02 删除,屏板链路 `Drivers/BSP/UI/ui_net_sink.c` 的 `remo_crc()` 是单字节异或、不是 CRC-16。本波在 `wave_store.c` 内自带**无表位运算** CRC-16/MODBUS(0x8005 反射、初值 0xFFFF、异或输出 0),单测锚点:串 123456789 的 CRC = 0x4B37 | +| CRC 覆盖范围 | 头 CRC 覆盖 `[0, 30+12×ch_count)`、自身在 `30+12×ch_count`;数据区自 `32+12×ch_count` 起;数据 CRC 覆盖整个数据区、自身在文件尾。8 路 320 点整段 5250 B | +| LSB 从哪来 | **不设独立 LSB 字段**:通道表的 `k_q16` 就是 LSB(µV/码 × 65536),与 `App/algo/adc_cal.h` 的 `ADC_CAL_K_Q16_TPAFE 10000000` / `ADC_CAL_K_Q16_STM32 52800000` 同口径,Q16 下无舍入(选型依据 `docs/任务规划/数据可信.md` §4.1)。PC 侧 `µV = k_q16 / 65536`,不许再乘一遍 | +| k / b 宽度与角色 | `k_q16` int32、`b_codes` int32 = 封存瞬间该路生效的两个标定量。`adc_cal_apply()` 的算式是 `eng = (code + b) × k × 1e-6`,文件里存的正是等号右边两个数加原始 code,PC 照此重算。**必须取 int32 原值**,不得经 `adc_cal_k_uv()` 的 float 通道回算(float 在 5.28e7 量级掉尾数)→ 接线道在 `App/algo/adc_cal.[ch]` 补一个 int32 直读入口 `adc_cal_k_q16()`(与 `adc_cal_k_uv()` 孪生);eng=0 时两者是 ROM 默认值,PC 折出来的单位是**引脚域 µV** 而非二次值——单位一律以头里的 `eng` 为准,PC 不许按当前固件口径猜 | +| `flags_seq` 位义 | bit s = 语义通道 s 的 `ADC_CAL_F_VALID`;本波 `ch_count` 固定 8 | +| 可用性字段真值表 | 两行,实现期按 `App/algo/adc_cal.c` 280 到 298 行核出(VALID 需文件位图与 k 合法带**同时**成立;CLIPPED 纯由来源派生——该路 ROM 系数属片内 ADC 侧即置位,与样本内容无关):① **未标定**(产品构建,两点标定未做)逐路 availability = UA / UN / IA 取 `0x00`,UB / UC / IB / IC / IN 取 `0x02`,`flags_seq` = `0x00`,`clip_mask` = 0xE6;② **已标定**(电气 8 路系数全部写入且落合法带)逐路 = `0x01`(TPAFE 三路)/ `0x03`(片内 ADC 五路,VALID 与 CLIPPED 同时为真),`flags_seq` = `0xFF`,`clip_mask` 仍是 0xE6。**VALID 与 CLIPPED 是两件独立事实**:写侧如实透传 `adc_cal_flags()` 逐路原值(`App/task/adc_task.c` 548 行),读侧不得据任何品质组合判废,PC 与回放页只给软告警(见下面「判废面与软告警面」一行) | +| eng 派生式 | `eng` 不是独立事实,是逐路有效位图的派生量:**电气 8 路全 VALID → 二次值(1),缺任意一路 → 引脚域(0)**。真机两处必须同口径,行号一并登记:判据本体 `App/app/ui_data_feed.c` 226 到 235 行的 `feed_eng()`(屏显侧),段头字段填写 `App/task/adc_task.c` 532 到 553 行(其中 552 到 553 行是段头侧的同一派生)。两处分布在**不同任务里现算**,理论上会撞上配置重载的毫秒级沉降窗而不一致——这是允许态,见下一行 | +| 判废面与软告警面 | **完整性判废面自始至终只有这些**:精确尺寸等式、`magic` 与 `ver`(含 `ver` 须等于魔数第三字节)、头 CRC 与数据 CRC、表序 `sem` 等于下标、`ch_count` 上限、`pts_per_cycle` / `pts_per_ch` / `pre_pts` 下限、长度为零、文件短读。**品质组合与单位标签永远不进这道门**。本轮新增的只是**软告警面**:`avail` 同时带 VALID 与 CLIPPED 时 PC 在 NOTE 列打「同时为真,两点标定完成后属正常,非故障」;`eng` 与 `flags_seq` 违反上一条派生式时打 WARN。两者都**不改退出码、不影响出图、不拒渲染**。理由:把「单位标签不一致」当数据损坏,等于让一次改标定文件的沉降窗销毁故障证据 | +| 通道表序与列序 | 第 i 个通道表项 ↔ 数据区第 i 列,按语义号**升序**:UA=0 UB=1 UC=2 UN=3 IA=4 IB=5 IC=6 IN=7(`App/algo/adc_input.h` 的 `adc_semantic_t`)。PC 必须校验表内语义号等于下标,不等即整段作废。交叉验证:`clip_mask` 恒 0xE6 = 2+4+32+64+128,恰好是 UB/UC/IB/IC/IN 五个语义号位,这份算术就是这个顺序的证据 | +| 样本编码 | int16 小端;TPAFE 侧 16 位补码双极性;STM32 侧 0..4095 原样存正数,**不加中点偏置**——V0.1 单极性无偏置是硬件现状,加偏置等于伪造双极性(`docs/任务规划/数据可信.md` §2.7) | +| 三个时间字段 | `t0_ms`(上电毫秒)、`wall`(epoch 秒,未对时 0)、`frame_seq`(段首帧序号)均 u32 无符号小端 | + +**本波 TEMP 不录**(`ch_count` = 8):温度非线性未折算,进录波只多占一路字节、不增加可解释信息(`docs/任务规划/数据可信.md` §7 的「温度不参与线性标定」已定案)。 **为什么按通道分列而不是按帧交错**:回放页只画一路 → 只需连续读那一路;PC 重算全周傅氏也是逐路独立。交错布局两种用法都要跨步长取数,白付代价。 +**实现期纠正记录(三条,只作追溯,不构成规格)** + +1. `flags_seq` 的 V0.1 真值一度被写成 `0x27`:那是把「按来源派生的 CLIPPED」当成了 VALID。按 `App/algo/adc_cal.c` 280 到 298 行,未标定态就是 `0x00`(见上面真值表第一行)。凡引用这个数字的地方一律以真值表为准。 +2. 一版互验夹具写成 `eng` = 0 而 `flags_seq` = `0xFF`——违反上面的 eng 派生式,是一串**真机写不出来**的字节。已改为按派生式自洽,并在 `test/host/wave_store` 那套单测里加了夹具自洽断言防复发。教训一句话:**互验夹具的资格来自「真机能写出这串字节」**,一份不可能状态的字节只能验证两个读者对幻觉的一致反应,正好掩盖真分歧。 +3. 顺带钉一条因果,免得后文写反:PC 侧早期一次 `shape` 子命令在 `--strict` 下 rc=1,与品质位**无关**,那是「实测触轨路数 vs 段头 `clip_mask`」对表不符(合成夹具的波形本来就没真触轨)。那条对表是 `docs/任务规划/数据可信.md` §4.6 与本文 §10 板级判据的本职,不是解码器的判废扩展。 + --- ## 7. 预算(先算后写,R6 是硬前置) @@ -213,12 +245,24 @@ flowchart TD | **合计静态增量** | — | **约 5.9 KB** | - **大于 4 KB ⇒ 触发 R6**:`docs/内存管理/内存内容分配策略.md` 的 R6 要求「任何新增静态缓冲 >4 KB 必须先核 §6 台账余量,改后同步 §6 数字、.sct 头注释与 AGENTS 栈表」;加大缓冲本身属 **Ask-first**; -- **落位 = SRAM1,不进 CCM**:CCM 受管区已 87.9 %、只剩 7,904 B,SRAM1 为 62.2 %(数字出自 `docs/内存管理/RAM预算台账.md` §1 受管区表);录波环不是 DMA 可见对象,没有「CCM 不可达」的约束,但 CCM 也**没有余量**给它; +- **落位 = SRAM1,不进 CCM**:CCM 受管区已 87.9 %、只剩 7,904 B,本波开工时 SRAM1 为 62.2 %(数字出自 `docs/内存管理/RAM预算台账.md` §1 受管区表;录波落地后的终值见本节末「实测回填」);录波环不是 DMA 可见对象,没有「CCM 不可达」的约束,但 CCM 也**没有余量**给它; - **UI 子系统配额早已超**(同一张表的子系统行)→ 回放页的描述符与名表仍要在台账记账,别拿「反正已超」当免检; - 前后各跑一次 `scripts/ram_report.py` 与 `scripts/stack_report.py`(defaultTask 多了落盘链、uiTask 多了读窗与自绘);`mem-budget` 钩子是总量红线,`ram_baseline.py --update` 回填趋势; - **Flash**:段格式编解码 + 状态机 + 回放页 + 一条日志行,粗估 3 到 5 KB(Flash 基线在 25 % 量级,不是瓶颈); - **NAND**:一段约 5.2 KB 数据加头。轮转 100 段约 0.5 MB,对 256 MB 单分区是零头——**瓶颈不是容量,是写次数**(§10 红线)。 +**实测回填(2026-10-29 权威构建,0 错误 0 警告;逐对象台账见 `docs/内存管理/内存内容分配策略.md` §6)** + +| 口径 | 上面的预估 | 实测 | +|---|---|---| +| SRAM1 静态增量 | 约 5.9 KB | 数据面 **+6,760 B**(81,536 → 88,296);UI 道另 +1,640 B;累计 **+8,400 B** → 终值 **89,936 B / 68.6 %** | +| CCM | 0(不得进 CCM) | **0**:`RW_CCM` 三档恒 57,376 B / 87.9 %,既没进 CCM 也没挤动它 | +| Flash | 3 到 5 KB | `ER_IROM1` 264,580 → 270,248(数据面 +5,668)→ 272,300(UI 道 +2,052)→ 272,360(落盘判废复检 +60)→ **272,336** B(cppcheck 返工纯搬声明,−24),累计 **+7,756 B**、占 26.0 %;`Total ROM` 274,308 B、`Code` 243,552 B | +| 任务栈 | 未估 | 九任务全 PASS、**未抬栈**:adcTask 静态最坏链 1,424 B(余 1,136 B / 44 %)、defaultTask 3,040 B(余 2,080 B / 40 %)、uiTask 1,336 B(余 15,048 B / 91 %) | + +- **差额构成(比预估多 860 B,可对账)**:原估漏记 `FIL` 528 B(`FF_FS_TINY=0`,落盘道刻意不上栈)、`DIR`/`FILINFO`、已封存段头副本 136 B、打包上下文(内部又含一份 136 B 段头);反向少记一项——落盘实际是**单缓冲 256 B**,不是预估的 512 B 双缓冲。**预估与实测都留在本节**:只写实测,下一次估算就看不见原估漏在哪。 +- **回放页读窗不在数据面那 6,760 B 内**:它计入 UI 道的 +1,640 B;两张表不能相加当总量,逐对象拆分见 `docs/内存管理/内存内容分配策略.md` §6。 + ## 8. 现场回放显示契约 ### 8.1 一屏画什么 @@ -261,8 +305,9 @@ flowchart TD wave: seg=NN armed=1 src=P pts=320 err=N ovf=N drop=N - `err` 是落盘失败或 CRC 自弃次数(**字节数不符就算失败**,见 §5.2);`ovf` 是冷却期丢弃的触发数;`drop` 是冻结窗口丢掉的录波点数; +- **实际落地形态(逐字)**:打印点在 `App/task/adc_task.c`:782 行,格式串 `"wave: seg=%lu armed=%u src=%s pts=%u err=%lu ovf=%lu drop=%lu"`——**字段名与顺序就是上面的模板**(台架与 PC 脚本按行名 grep,动它就断脚本);`src` 取 M(手动)/ P(保护)/ T(定时自抓)/ `-`(本次上电还没触发过);`armed` 只在 FREEZE 与 SEALED 为 1(COOL 期只挡新触发不录新段,画进 armed 会被台架误读);`seg` 是累计**成功回执**的段数、不是轮转号;`err` = 落盘失败 + SEALED 滞留超时自弃(含 §5.2 第四条的 ring abandoned 判废),由环内计数收口,落盘道不数第二份账。`err` 与 `ovf` 的**已知容差**(lead 裁定 2026-10-29,不修只入档):这两个计数是跨上下文的读-改-自增(`ovf` 由任意上下文的拒绝路径加、`err` 由 adcTask 的滞留自弃与 defaultTask 的落盘失败分别加),本层双端编译、不许引 FreeRTOS / CMSIS 的临界区内在函数,所以并发拒绝或并发失败时**理论上可能少计一次**。定性为**观测计数的已知容差**:只用于趋势与判读,**不作为任何判据的唯一依据**——要坐实「冷却期内确实来了第二次触发」,同时看 `armed` 与那一刻 `/log/start.log` 的时刻。契约原文见 `App/algo/wave_ring.h` 文件头「观测计数的已知容差」段; - 只在原先静默的黑洞处补锚点,别重复已有步骤——每条 DBG 日志约 90 B Flash 加 216 B 栈(`AGENTS.md` 的日志与工具链节); -- 该行落地时同步进 `docs/测试说明/ADC前端注入验证流程.md` 的读数出口表。 +- 该行已同步进 `docs/测试说明/ADC前端注入验证流程.md` 的读数出口表(2026-10-29;同一张表还登记了段文件出口与 PC 侧读段工具)。 --- @@ -271,10 +316,13 @@ flowchart TD | 层 | 内容 | |---|---| | 主机侧纯逻辑单测(新增,进 `test/host/`) | ① 环:预触发与后触发点数、冻结与解冻、冷却丢弃计数;② 段格式编解码往返,含 CRC 不符、零长度、字节数不符三条失败路径;③ 触发状态机三条来路(手动 / 保护 / bench)。**环与格式必须是纯逻辑**(不碰寄存器、不碰 RTOS)才能这样测——这是 `App/algo/` 既有分层纪律 | -| 板级 | 信号发生器注 1 V / 50 Hz → 手动一段 → FTP 拉回 → PC 脚本量出周波数、幅值、极性、削顶路数与 `clip_mask` 一致;再把 PC 重算的基波有效值与板上 `meas:` 与 104 浮点 YC 对表 | +| PC 侧工具的定性 | `test/host/wave_read.py` 是 **bench 工具**(`test/host/` 下的 loose .py,先例 `test/host/ftp_capture.py`),**不是**主机单测套件 → 不建 `run_unit_test.py`、不加 pre-commit 专属钩子(专属钩子默认不补,按 Lead 裁定 2026-10-27);它自带 `--selftest` 就是它的自带回归,跑法与四个子命令见 `docs/测试说明/ADC前端注入验证流程.md` §4 | +| 跨工具链分叉(实证,写进来是为了下一个模块别重踩) | 两份录波 .c 只 include 自身头(链上只有 stdint.h)时,ARMCC 报 22 条 identifier "NULL" is undefined;而主机 MSVC / gcc 会传递性把 stddef.h 拉进来,**主机单测完全照不出这个分叉** → 新增模块需要用 NULL 就自己 include stddef.h(与 `App/algo/sample_ring.c` 同口径,别塞公共头) | +| 板级 | 信号发生器注 1 V / 50 Hz → 手动一段 → FTP 拉回 → PC 脚本量出周波数、幅值、极性与注入一致;再把 PC 重算的基波有效值与板上 `meas:` 与 104 浮点 YC 对表。**判据只量四样:周波数、幅值、极性、钉轨路数——不比绝对相位**(理由见 §4.2「段内点序」:触发瞬间在工频周期内任意落点,段字节里没有绝对时间参考;真要比相位必须让发生器给同步触发,本波不做)。**钉轨对表的合格判据 = 头位图 ⊇ 实测钉轨路数,且差异必须能由注入条件解释**——因为 `clip_mask` 是按来源派生的链路可信性标记(§6.2 偏移 24 行),不是本段实测值:只加直流或小幅注入时那五路本就不该钉轨,硬取等式会假红。**同段另起一条强观察项**:V0.1 下这五路若在做**交流注入**时反而没钉轨,先怀疑接线或该路根本没被驱动——这是告警面,不是失败面(等式仍作为默认尺子留在 PC 侧 `shape --strict` 里,因为它有这个信号价值;要放行差异用那条判据的解释权,不靠改尺子)。**§5.2 第四条的板级观察项(代码与单测面已有、板级未测)**:bench 连续自抓期间人为把卷占住 2 s 以上(例如同一段跑 FTP 大文件读),看串口 `wave:` 行的 `err` 是否加一、是否只出现**一条** `[ERR] … seg drop: ring abandoned` 留痕(不是每拍刷屏)、且段号不推进(下一段仍写同号文件) | | UI | `ui-verify` 五步;回放页入 `ui-golden` 黄金参考(**参考图一旦入库只能改代码对齐,禁止刷参考掩盖**);新叶走 `gen_menu.py` 重生成 + 字库链 | | 构建 | `@build` 要求 0 错误 0 警告;新 .c 双端注册走 `new-source-file`(`MDK-ARM/STM32F407-Demo.uvprojx` 与 `simulator/CMakeLists.txt` 两处显式列表) | | 台账 | R6 三处同步 + `scripts/ram_baseline.py --update` + `mem-numbers` 与 `mem-budget` 钩子 | +| 板级(读路径耗时,2026-10-29 补) | 进回放页后**连续切段 + 连续切道**,读 `task_prof` 里 uiTask 的单轮工作耗时峰值与 800 ms 窗的余量;**不要为此新加埋点**(现成读数够用)。判读:峰值贴着窗走就是读路径该再加节流(先想缓存与分次,别去动心跳窗) | | 图与生成物 | 本设计文档的配图生成器受 `generated-fresh` 逐字节幂等约束;脚本与产物同一笔提交 | **bench 定时自抓的红线**:定周期自抓在 1 kHz / 8 路下每 2 s 一段,每 5 分钟就是一轮 NAND 写。**长时间挂着它白烧写次数**,而 dhara 的坏块记账是持久代价、普通恢复清不掉(`AGENTS.md` 测试构建节,`docs/测试说明/存储维护与容量恢复.md`)→ 定时模式必须**默认关闭 + 有时长上限 + 启动时打一次告警**,用完立刻回产品构建。 @@ -300,6 +348,7 @@ flowchart TD | 削顶五路被读成「系统有零序 / 有谐波」 | 现场误判 | 头里 `clip_mask` + 页面「削」徽标 + 空态不许画直线(§8.1) | | 有人把 U 与 I 两段叠图比相位 | 得出**没有物理意义**的相位差 | 回放不做多路叠加;头里写明来源转换器;与 `docs/任务规划/数据可信.md` §9 的 V0.1 禁止行同源 | | 落盘把 defaultTask 拖住 | 日志与配置引擎变慢(defaultTask 是两者唯一写者) | 分次写(256 B 双缓冲)、一次心跳最多一段、`f_write` 前后有耗时观测;不做整段驻留再写 | +| **回放读路径把 uiTask 拖住**(2026-10-29 lead 评审补,此前风险表只有写侧没有读侧) | 一次渲染的实码 I/O(`App/task/ui_task.c` 376 到 398 行):段表刷新 1 次 `f_opendir` + 至多 100 次 `f_readdir`;未缓存段的段头装载 = 1 次 `f_open` + 1 次 128 B 头读 + **20 次 256 B 数据区读并边读边算数据 CRC** + 尾 2 B 一次 = **最坏 22 次小读**;已验过则只剩窗装载 = 1 次 `f_open` + 1 次 `f_lseek` + 1 次 80 B 窗读 + 40 点解码与峰值扫描(**一屏只碰一路通道一个窗,不是 8 路 320 点**)。uiTask 在 IWDG `required_mask` 内、窗 800 ms(口径同 `docs/子系统设计/看门狗设计说明.md` §11.3 的解耦心跳)→ **现象先是 LVGL 停摆与远程推流缺席,不是复位**(心跳与画屏解耦) | 卷锁 try 非阻塞(拿不到就显示「存储忙」,不等)+ 256 B 分次读 + 已验段结果缓存(同段同道不重复验)+ 只在首次进页 / 切段 / 切道时读。**单轮耗时上界未实测**:不新加埋点,用现成的 `task_prof` 读数(板级动作见 §10) | | 触发风暴 + 轮转覆盖 | 现场唯一一段有效波形被后到的抖动冲掉 | 冷却期 + `ovf` 计数;「最新段号」进快照;分段优先级属二期 | | 定时自抓忘关 | 白烧 NAND 写次数(持久代价) | 默认关 + 上限 + 告警(§10 红线) | | 标定落地后老文件被解释错 | 同一个系数字段值变了含义漂移 | 头里存**封存瞬间**的 `eng` 与系数快照;PC 脚本按头解释,绝不按当前固件口径解释 | diff --git a/docs/子系统设计/运行与自检记录设计说明.md b/docs/子系统设计/运行与自检记录设计说明.md index ef8c2b16..c2fb0a78 100644 --- a/docs/子系统设计/运行与自检记录设计说明.md +++ b/docs/子系统设计/运行与自检记录设计说明.md @@ -25,7 +25,7 @@ - **菜单树已含目标叶子**:`ui_menu_tree.txt` "记录查询"下已有 **自检记录 = node 39、运行记录 = node 40**(当前为空白终点框,无内容页映射);无需改树。 - **密码页** `ui_page_password.c`:3 位数字框 + ↑↓←→/ENTER;输错仅清空重输(R1 缺口)。 - **按键流**:物理/远端键 → LVGL keypad → 默认输入组 → 焦点对象事件回调(菜单容器 `menu_key_cb` / 内容页容器 `cp_key_cb`)——空闲检测可在此统一挂钩。 -- **字体**:子集位图字体 `ui_font_cn_12`,字符集 `ui_cn_chars.txt`(当前 218 汉字/符号 + ASCII 0x20~0x7E,共 313 字形;2026-10-28 实测。122/217 是 2026-09-14 的历史快照,见 §6.1)。 +- **字体**:子集位图字体 `ui_font_cn_12`,字符集 `ui_cn_chars.txt`(当前 224 汉字/符号 + ASCII 0x20~0x7E,共 319 字形;2026-10-29 实测。122/217 是 2026-09-14 的历史快照,见 §6.1)。 ### 2.2 存储与时间 diff --git a/docs/架构设计/项目架构总览.md b/docs/架构设计/项目架构总览.md index a3a5f129..4d909dfd 100644 --- a/docs/架构设计/项目架构总览.md +++ b/docs/架构设计/项目架构总览.md @@ -256,7 +256,7 @@ flowchart TB - 内容页为**独立全屏屏**,由框架一次性自绘(**零 LVGL 子对象**),ESC 返回菜单屏。 - 菜单受保护叶子(`ui_menu_tree.txt` 行尾 ` #P`):先弹密码页,授权后 `menu_password_ok` **先关页再续进入动作**。 - 待机屏:空闲超时(默认 60s)自动切入,仅确认键回菜单;数据源同实时量页。 -- 字体 `ui_font_cn_12.c` 由 `tools/font_gen/` 生成(宋体 12px 1bpp 子集,**218 字/313 字形**,2026-10-28 实测;字形数 = 字符数 + ASCII 95 个)。 +- 字体 `ui_font_cn_12.c` 由 `tools/font_gen/` 生成(宋体 12px 1bpp 子集,**224 字/319 字形**,2026-10-29 实测;字形数 = 字符数 + ASCII 95 个)。 --- diff --git a/docs/测试说明/ADC前端注入验证流程.md b/docs/测试说明/ADC前端注入验证流程.md index e57088ea..81aecc5f 100644 --- a/docs/测试说明/ADC前端注入验证流程.md +++ b/docs/测试说明/ADC前端注入验证流程.md @@ -67,7 +67,7 @@ ## 4. 读数出口(固件只有下面这些周期行,没有命令控制台) -> **样本出口将变**:故障录波已立项(2026-10-29,docs/任务规划/故障录波.md)。落地后段文件会直接给出原始码值样本,动态验证的判据随之从「比对下表这些派生标量」升级为「PC 独立重算 vs 板上读数」对表,口径变更见 docs/子系统设计/故障录波设计说明.md §11。下表仍是**当前**唯一可读的东西。 +> **样本出口已落地(代码侧,2026-10-29)**:故障录波的数据面已进仓(立项与卡片见 docs/任务规划/故障录波.md,格式与判据属主见 docs/子系统设计/故障录波设计说明.md §6.2 与 §6.3、口径变更见该文 §11)——每次触发一段,段文件直接给出 8 路 × 320 点原始码值,动态验证的判据因此**可以**从「比对下表这些派生标量」升级为「PC 独立重算 vs 板上读数」对表。下表仍是固件唯一的**周期行**出口(段文件不是周期行,只在触发时产生),两种出口对表用。 | 日志行 | 频率 | 内容 | 用途 | | ----------------------------------------------------------- | ------ | ------------------------ | ------------------- | @@ -77,6 +77,8 @@ | `frames=.. idle=.. adc1_err=.. tgap=..` | 每 10 s | 采集健康计数 | 步骤 4 的节奏判据 | | `phasor(hb) C0x-…: Re=.. Im=.. rms=..`(逐实例)、`freq(hb): f=.. valid=..` | 每 10 s | 基波相量分量与测频读数(×0.1 / ×0.01 定点) | **交流轮次的主读数**(直流轮次只作旁证) | | `meas: seq=.. v=.. ua_mv=.. un_mv=.. ia_mv=.. f=.. fv=..` | 每 10 s | 200 ms 遥测快照轨(基波有效值 mV) | 交流轮次的幅值比对(1 mV 分辨率,比 phasor 行细) | +| **段文件** `/wave/WAVE00.BIN` 起共 100 个号轮转(FTP 根下可见,与 /log 平级) | 每次触发一段(手动 / 保护启动 / bench 定时) | 8 路 × 320 点原始码值 + 封存瞬间的标定与品质快照(布局见 docs/子系统设计/故障录波设计说明.md §6.2) | **样本出口**:交流轮次的主对表源——预触发 160 点 + 后触发 160 点,触发瞬间落在段内点 159 | +| PC 侧脚本 `test/host/wave_read.py`(不是固件日志,bench 工具,纯标准库) | 手动跑;`--selftest` 是它的自带回归 | 四个子命令:dump(逐字段与双 CRC)/ fft(全周傅氏重算基波有效值与相位)/ shape(削顶与形态判定,可 `--strict`)/ plot(出 SVG 或 PNG,PNG 用 `-o` 落文件) | 把段文件还原成周波数、幅值、相位、削顶判定与图 = 「PC 独立重算」这条判据的执行体;CRC 不符或零长度它必须**拒绝**而不是画出东西 | > ⚠ 直流轮次用不到后两行,但**它们一直都在**。交流轮次(判据见 `docs/任务规划/数据可信.md` §4.6) > 必须用 `meas:` 或 104 YC 比幅值,别用 `phasor(hb)` 行——那行是 ×0.1 定点,注 1 V 只剩 10 格。 diff --git a/docs/测试说明/主机单测覆盖率口径.md b/docs/测试说明/主机单测覆盖率口径.md index 9b0c3d6c..5082e104 100644 --- a/docs/测试说明/主机单测覆盖率口径.md +++ b/docs/测试说明/主机单测覆盖率口径.md @@ -713,7 +713,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 状态 + 内容页 60 张像素黄金参考)覆盖;再写单测是第三份重复投资 | `ui-verify` skill 五步 + `docs/UI设计/UI_自动化测试流程.md` | +| UI 侧 | 17 个 `.c` = **1,942 行** | 与 PC 模拟器是**同一份源码**,行为回归已由 `ui-golden`(菜单 8 状态 + 内容页 66 张像素黄金参考)覆盖;再写单测是第三份重复投资 | `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` 审计 | diff --git a/docs/测试说明/自动化测试流程.md b/docs/测试说明/自动化测试流程.md index 4f862457..980e7360 100644 --- a/docs/测试说明/自动化测试流程.md +++ b/docs/测试说明/自动化测试流程.md @@ -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 模拟器像素回归覆盖(文本级断言 + 60 张黄金参考 + 池水位门禁, +- **屏幕内容回归**:由 LVGL 模拟器像素回归覆盖(文本级断言 + 66 张黄金参考 + 池水位门禁, 见 `docs/UI设计/UI_自动化测试流程.md`);上机屏幕仍需人工目检(见 `测试开发标准流程.md` §1)。 ## 扩展用法 diff --git a/docs/门禁说明/README.md b/docs/门禁说明/README.md index fb768419..b8671af4 100644 --- a/docs/门禁说明/README.md +++ b/docs/门禁说明/README.md @@ -69,7 +69,7 @@ | [`cubemx-survival.md`](cubemx-survival.md) | `cubemx-survival` | CubeMX 重生成会不会把定制静默抹掉(人工目检审计兜底) | `.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 状态 + 内容页 60 张黄金参考图比对 | UI 源码、模拟器 | 已写 | +| [`ui-golden.md`](ui-golden.md) | `ui-golden` | UI 像素回归:菜单 8 状态 + 内容页 66 张黄金参考图比对 | 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` | 指针 | diff --git a/docs/门禁说明/coding-standard.md b/docs/门禁说明/coding-standard.md index 58510f55..e4eb3dac 100644 --- a/docs/门禁说明/coding-standard.md +++ b/docs/门禁说明/coding-standard.md @@ -88,6 +88,16 @@ Drivers/BSP/X/x.c:42 [CODE-RULE-09] 函数 foo_bar 缺少函数注释(应含 但写成 `Malloc(` 就绕过了(同形后缀也一样:`buf_free(` 因词边界不成立而不算违规)。 8. CODE-RULE-20 **只管堆分配函数名**:`alloca` 与变长数组(VLA)是**栈**上的坑,不归它; 分配不释放(泄漏)、释放后继续用(UAF)也不归它——本条只判"别碰 C 堆"这一件事。 +9. **CODE-RULE-09 的回看窗口只有 25 行**:脚本 `scripts/check_coding_standard.py:194` 取的是函数定义行 + 往前 25 行(`window = lines[idx-26:idx-1]`),在里面找「函数功能」关键词。于是**函数头注释块一旦写 + 到 25 行以上,关键词就被挤出窗口**——注释本身完全合规,也照样报「函数缺少函数注释块」。2026-10-29 + 一次真实触发(lead 复跑抓到 6 处、已改 3 个生产文件,全部是**纯注释重排、无一行逻辑改动**):录波 + 落盘道与 UI 道上四个带长口径注释的函数(`wave_store_drain`、`s_log_record_edge`、`App/task/ui_task.c` + 的录波按键 helper、`simulator/main.c` 的合成喂拍 helper)各撞一到两处。**写法上的规避**:把长口径 + 另起一块放在函数注释块**之前**(两块分开),或把细节搬进函数体内——别让「函数功能」那一行离定义行超过 24 行。 +10. **不带文件参数就是空跑**:脚本 `:321` 是逐 `sys.argv[1:]` 处理的,**不给文件参数时一个字节都不查、 + 直接返回 0**。手工复现这条门禁时把那个 RC=0 读成「过了」是错的——必须显式列出要查的路径(pre-commit + 里由框架喂 staged 文件,所以钩子本身不会空跑)。同族坑见 `docs/代码规范/pre-commit使用说明.md` §2。 补录(判据细节,迁移自原手册): diff --git a/docs/门禁说明/nested-comment.md b/docs/门禁说明/nested-comment.md index cde2bb09..dc1b7d13 100644 --- a/docs/门禁说明/nested-comment.md +++ b/docs/门禁说明/nested-comment.md @@ -89,6 +89,15 @@ 但不会指出模板——那段链由 `generated-fresh` 保证「脚本与产物同一笔提交」。 5. 编译器侧的真实警告仍以 `@build` 为准:本闸只拦「会让 ARMCC 报 9-D 的那一类」, 编译日志里其他诊断一概不管。 +6. **仓库根探测必须兼容 linked worktree**:脚本原先用「`.git` 是不是目录」定位仓库根,而 `git worktree` + 出来的任务工作树里 `.git` 是**一个文件**(写着 `gitdir: …`)不是目录 → 本闸在任何任务树里恒 + 退出码 2「仓库根定位失败」,**静默不可用**。这不是「放过了真问题」,是**根本没跑**——比放过更坏, + 因为调用方看到红字会以为环境有问题、看到跳过会以为干净。2026-10-29 已把该判据从 `isdir` 改成 + `exists`(`scripts/check_nested_comment.py:342`,修复归当日提交)。全仓 `scripts/` 只有它这么探根, + 其余只读闸在任务树里一直正常。**后果要说清楚**:门禁修好之前那批录波改动没被它扫过,lead 手工对 + 改动面逐文件扫了一遍、嵌套命中 0,所以没有放过真问题——但那是**人工兜底,不是门禁功劳**。 + 通用教训:**门禁脚本按 `isdir(.git)` 探根 = 在 worktree 里自杀**;新增只读闸要么走 + `git rev-parse --show-toplevel`,要么同时接受 `.git` 的文件形态。 ## 7. 豁免与基线(只减不增) diff --git a/docs/门禁说明/ui-golden.md b/docs/门禁说明/ui-golden.md index bbc233ed..9977e9c1 100644 --- a/docs/门禁说明/ui-golden.md +++ b/docs/门禁说明/ui-golden.md @@ -1,4 +1,4 @@ -# `ui-golden` — UI 像素回归(菜单 8 状态 + 内容页 60 张黄金参考) +# `ui-golden` — UI 像素回归(菜单 8 状态 + 内容页 66 张黄金参考) > 一句话:改 UI 源码时,把模拟器增量重编 + 两条 golden 回归串成一次提交门禁,**像素对不上就拦**。 > 它是包装器:真正的判据在被包装的两条脚本与参考图里。 @@ -11,7 +11,7 @@ ## 1. 它在查什么 - 菜单侧:整棵菜单树的行渲染结果(8 个状态)与参考图逐像素比对; -- 内容页侧:60 张黄金参考(= 60 个截图态条目,同一内容页的多个状态各一张,其中 2 张是分页页多拍的第 2 屏),逐张与参考图比对; +- 内容页侧:66 张黄金参考(= 66 个截图态条目,同一内容页的多个状态各一张:分页页多拍的第 2 屏与确认链/录波的各态都单独计条目),逐张与参考图比对; - 参考图 `simulator/tests/ref/*.bin` **就是判据本身**。 ## 2. 为什么要它(有牙的实测) @@ -57,7 +57,7 @@ 1. **参考图即判据**:刷新参考图 = 修改判据。评审时必须盯 `simulator/tests/ref/` 的 diff; 一旦"顺手更新",此后所有回归都只在和错的基线比。**门禁无法阻止这件事**,只能靠评审。 -2. 只覆盖 8 个菜单状态 + 60 张内容页黄金参考:未列入的状态(密码错误态、超长文本、缺字渲染、 +2. 只覆盖 8 个菜单状态 + 66 张内容页黄金参考:未列入的状态(密码错误态、超长文本、缺字渲染、 无 SD 卡 / 挂载失败的降级显示)**不会有任何回归**。 3. 跑的是**模拟器那份配置**,不是固件那份 —— 两份 `lv_conf.h` 漂移时,回归结论不能外推到固件 (所以 `lv-conf-sync` 是本门有效性的前提)。 diff --git a/docs/问题记录/采集与数据处理短板评测.md b/docs/问题记录/采集与数据处理短板评测.md index 52d598ba..5e0ab461 100644 --- a/docs/问题记录/采集与数据处理短板评测.md +++ b/docs/问题记录/采集与数据处理短板评测.md @@ -16,7 +16,7 @@ | 链 | 现状 | 缺什么 | |---|---|---| | **测量链(遥测)** | 104 每 100 ms 直接读相量瞬时值 | 工程量折算、谐波方和根、平均/死区/零漂、品质位 | -| **记录链(录波)** | 只有 64 B 定长 SOE 文本记录 | 波形数据面(**2026-10-29 已按 1 kHz 立项**,docs/任务规划/故障录波.md)、COMTRADE 文件、104 文件服务 | +| **记录链(录波)** | SOE 文本记录(64 B 定长)**加上**录波段文件:1 kHz / 8 路原始码值 / 单段 320 点、`/wave` 下轮转、PC 侧读段脚本与现场单通道回放页——**代码面 2026-10-29 已交付,板级判据一轮未跑**(docs/任务规划/故障录波.md) | COMTRADE 文件、104 文件服务;以及 4 kHz 全速与 80 点/周波——本波按 1 kHz 起步,该偏离已按 docs/子系统设计/故障录波设计说明.md §2 显式登记,**不是已补齐** | | **就地闭环(开入开出)** | 固件完全没有 | 开入量采集与防抖、遥信变位、遥控出口、跳闸出口 | **四个阻断级短板**(不补则送检必挂): @@ -24,7 +24,7 @@ 1. **工程量折算缺失**——遥测上送的是 ADC 原始码值(数千~数万),点表要求"短浮点、系数 = 1"的工程量;保护定值(A/V)也在跟码值比较,量纲不成立。 2. **遥测链缺失**——没有 200 ms 测量轨:无谐波方和根、无 4 点平均、无死区确认、无零漂、无畸变率;死区做在 104 层(架构错位)。 3. **运行参数段 0x8020~0x8058 缺失**——死区、变比、零漂门槛、三级负荷报警、录波投退与定值,主站读写全部回"未知地址"。 -4. **录波与 104 文件传输缺失**——点表明确要求"录波文件存储 ≤ 15 s""104 读文件目录/文件 ≤ 30 s"。 +4. **录波已按 1 kHz 口径落地(2026-10-29 代码面),104 文件传输仍缺**——点表明确要求"录波文件存储 ≤ 15 s"(该时限未测,见 §4.4)与"104 读文件目录/文件 ≤ 30 s"(未做)。 **建议顺序**:先做"数据可信"(1 + 2 + 3 + 频率点,纯软件、可主机单测),再做"精度与安全网"(等周波 + 削顶检测 + 派生量),最后单独立项"录波与文件"(最大单块)。开入开出与功率/电度需硬件与产品先拍板,软件先建接口不使能。 @@ -36,12 +36,12 @@ | 环节 | 参考实现 | 本项目 | 差距 | |---|---|---|---| -| **采样节拍** | 内总线 4 kHz(80 点/周波),保护取 1/4 → 1 kHz;交流板本地插值对齐 | TIM3 1 kHz 单节拍,无过采样通路 | 录波无数据源;等周波未闭环 | +| **采样节拍** | 内总线 4 kHz(80 点/周波),保护取 1/4 → 1 kHz;交流板本地插值对齐 | TIM3 1 kHz 单节拍,无过采样通路 | 录波已有 1 kHz 数据源(2026-10-29 交付代码面,见 docs/子系统设计/故障录波设计说明.md §2),**但仍无 4 kHz 过采样通路**;等周波未闭环 | | **预处理** | 码值 × 每通道系数(定点 2^10)→ 浮点工程量;自产零序 `3I0=Ia+Ib+Ic`;插值异常闭锁 | 码值直通(系数 = 1),无标定、无自产零序 | **量纲缺失**;零序只能靠外部通道 | | **保护相量** | 全周傅氏 15 基波 + 3 二次 + 1 五次,每 1 ms 重算 | 5 实例(IA/UA/UN 基波 + IA 二/五次),每 1 ms 重算 | 实例数按 V0.1 有效通道裁剪(合理),但**无派生量层** | | **测量遥测** | 15 通道 × k=1~9 DFT → 方和根 → 4 点平均 → 死区 2 点确认 → 零漂置零 → 微调 TLV10 → 变比;22 交流 + 2 直流 + 7 功率 | 无遥测链;104 直读相量瞬时值 | **整链缺失** | | **派生量** | 线电压 / 负序 / 正序 / 零序 / 差分 / 直流 / 同期量(`Calf*Vec` 系列 + `CalfAnaDC/Dif`) | 无 | 整层缺失 | -| **录波** | 80 点/周波 × 8 周波,突变量启动,COMTRADE 输出(`Wave.c` + `WaveComtrade.c` 共 7 582 行) | 无 | **整链缺失** | +| **录波** | 80 点/周波 × 8 周波,突变量启动,COMTRADE 输出(`Wave.c` + `WaveComtrade.c` 共 7 582 行) | 1 kHz × 8 路原始码值单段(2026-10-29 代码面交付,含段文件与 PC 侧读段工具)| 部分补齐:口径偏离(无过采样、无突变量启动、无 COMTRADE),见 docs/子系统设计/故障录波设计说明.md §2 与 §13 | | **记录** | SOE / COS / 跳闸 / 遥测 / 冻结 / 异常(`RecUnit` + `HistoryFile`) | SOE + 动作记录(B9) | 缺 COS/遥测/冻结/异常 | | **上送** | 101/103/104/61850/ModBus,含文件服务与电度 | 104 子集:M_SP/M_DP/M_SP_TB/M_ME_NC/C_SE/C_RD/C_IC/C_CS | 无文件服务、无电度、无定值区 | @@ -79,7 +79,7 @@ ### 3.3 完全空白 -遥测链 · 派生量(线/序/差分/直流) · 录波与 COMTRADE · 104 文件服务 · 开入量采集 · 遥控出口 · 跳闸出口 · 功率与电度 · 同期 · 小电流选线 · 运行参数段 · 标定 · 等周波 · 削顶检测。 +遥测链 · 派生量(线/序/差分/直流) · COMTRADE 与 104 文件服务(录波本身已于 2026-10-29 落地 1 kHz 口径)· 开入量采集 · 遥控出口 · 跳闸出口 · 功率与电度 · 同期 · 小电流选线 · 运行参数段 · 标定 · 等周波 · 削顶检测。 --- @@ -161,11 +161,11 @@ - 地址、类型、默认值以参考表为准逐项对照,**不要凭想象填**。 - 规模:**M**(表驱动,机械但量大;104 侧 `set_read/set_write` 已是通用接口,不用改协议)。 -### 4.4 录波与 104 文件传输缺失 +### 4.4 录波已按 1 kHz 口径落地,COMTRADE 与 104 文件传输仍缺失 **现状与证据** -- 全库无录波、无 COMTRADE(关键词扫描零命中)。 +- 录波已于 2026-10-29 落地(1 kHz / 8 路 / 段文件:`App/algo/wave_ring.c` 与 `App/algo/wave_store.c`);**COMTRADE 与 104 文件传输仍零命中**(本波不做,见 docs/子系统设计/故障录波设计说明.md §13)。本节原写「全库无录波(关键词扫描零命中)」是落地前的现状句,下面几条里 4 kHz 过采样与文件面的差距仍然成立。 - 104 只实现 8 种 ASDU,**无文件传输类型**(120~122 及读文件目录扩展),无定值区类型。 - 采样通路只有 1 kHz 一条,没有 4 倍过采样。 @@ -333,7 +333,7 @@ | 等周波 | 同 §3.1 的「等周波采样(软件测频 + 调频闭环)」(⬜) | 补三条实施约束(ARR 联动、开环先行、过渡处理) | | 遥测链 | 复现规划 §3.2(「遥测谐波轨(可选)」,标"可选") | **建议从"可选"升为"必做"**——它是遥测、电度、越限、畸变率的共同底座 | | 派生量 | 复现规划 §3.2(「线/负序/零序合成」,V0.1 不适用) | 建议代码与单测先就绪,V0.2 只改映射 | -| 录波 | 复现规划 §3.4(「故障录波(可选)」,⬜) | 补"104 文件服务"这一半(原登记只写录波) | +| 录波 | 复现规划 §3.4(「故障录波(可选)」,⬜;2026-10-29 已按 1 kHz 落地代码面,4 kHz 与 COMTRADE 仍 ⬜)| 补"104 文件服务"这一半(原登记只写录波)| | 运行参数段 | 未登记 | **新发现**,建议按语义标识登记("运行参数段 0x8020~0x8058") | | 开入开出 | 未登记 | **新发现**,建议登记("开入量采集与遥信"、"遥控与出口") | | 削顶检测 | 复现规划 §3.5(「采样通道自检」残余,"码值越界待做") | 给出两条具体判据 | diff --git a/scripts/check_nested_comment.py b/scripts/check_nested_comment.py index 1bf731af..c2658145 100644 --- a/scripts/check_nested_comment.py +++ b/scripts/check_nested_comment.py @@ -339,8 +339,8 @@ def main(argv=None): ap.add_argument("--selftest", action="store_true", help="正例照得出 + 反例不误报 + 基线机制有效") args = ap.parse_args(argv) - if not os.path.isdir(os.path.join(REPO, ".git")): - print("[ENV] 仓库根定位失败:%s 里没有 .git" % REPO) + if not os.path.exists(os.path.join(REPO, ".git")): + print("[ENV] 仓库根定位失败:%s 里没有 .git(目录或 worktree 的 gitdir 文件都算)" % REPO) return 2 print("=== 块注释嵌套检查 check_nested_comment.py(注释体内出现 /* = ARMCC 诊断 9-D)===") diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv index a550ef23..39755861 100644 --- a/scripts/ram_baseline.tsv +++ b/scripts/ram_baseline.tsv @@ -11,4 +11,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi 2026-10-26 231712 25560 1520 136216 257272 24.5 80312 61.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4003;default_task.o=2736 2026-10-27 235372 26392 1572 136924 261764 25.0 81072 61.9 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055;default_task.o=2736 2026-10-28 236456 28124 1876 137084 264580 25.2 81536 62.2 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055;default_task.o=2736 -2026-10-29 236456 28124 1876 137084 264580 25.2 81536 62.2 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055;default_task.o=2736 +2026-10-29 243552 28784 1972 145388 272336 26.0 89936 68.6 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;wave_ring.o=5296;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055 diff --git a/scripts/size_attestation.json b/scripts/size_attestation.json index 455fbbd1..d62af305 100644 --- a/scripts/size_attestation.json +++ b/scripts/size_attestation.json @@ -1,25 +1,25 @@ { - "built_at": "2026-10-29T07:40:50+08:00", - "fingerprint": "ebbfb0a1f9f0c25384bab377e009bb9c5ecd6e2044a2a49fcb6bc60ca2f743b9", - "fingerprint_files": 1452, + "built_at": "2026-10-29T18:46:19+08:00", + "fingerprint": "22549aa4e7b96d68fb61393f50924a46628d4b470b380ce95c79599f24f01fd4", + "fingerprint_files": 1460, "fingerprint_source": "git-index", "gated_regions": [ "RW_IRAM1", "RW_CCM" ], - "git_head": "5f69f38dfd7af7fde580068d43925969e3959947", + "git_head": "325abe43372e2f5e0d5ec0f4d87c8590fc9ee12e", "map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map", - "map_mtime": 1793206308, + "map_mtime": 1793270777, "pct": { - "ER_IROM1": 25.23, + "ER_IROM1": 25.97, "RW_CCM": 87.89, - "RW_IRAM1": 62.21, + "RW_IRAM1": 68.62, "UNINIT_CCM": 18.75 }, "regions": { "ER_IROM1": { "max": 1048576, - "size": 264580 + "size": 272336 }, "RW_CCM": { "max": 65280, @@ -27,7 +27,7 @@ }, "RW_IRAM1": { "max": 131072, - "size": 81536 + "size": 89936 }, "UNINIT_CCM": { "max": 256, @@ -37,10 +37,10 @@ "schema": 2, "scope_note": "\u540c\u7801\u51ed\u8bc1\uff1a\u6307\u7eb9\u8986\u76d6\u56fa\u4ef6\u4f53\u79ef\u76f8\u5173\u6e90\u7801\uff0c\u53e3\u5f84\u89c1\u672c\u811a\u672c docstring", "sizes": { - "code": 236456, - "ro_data": 28124, - "rw_data": 1876, - "zi_data": 137084 + "code": 243552, + "ro_data": 28784, + "rw_data": 1972, + "zi_data": 145388 }, "threshold_pct": 92.0, "worktree_dirt_at_stamp": 0, diff --git a/scripts/test_coverage_baseline.json b/scripts/test_coverage_baseline.json index bc586c0a..01094f53 100644 --- a/scripts/test_coverage_baseline.json +++ b/scripts/test_coverage_baseline.json @@ -1,12 +1,12 @@ { "files": { "App/algo/adc_cal.c": { - "func_rate": 1.0, + "func_rate": 0.9412, "funcs_covered": 16, - "funcs_valid": 16, - "line_rate": 0.986, + "funcs_valid": 17, + "line_rate": 0.9463, "lines_covered": 141, - "lines_valid": 143 + "lines_valid": 149 }, "App/algo/adc_health.c": { "func_rate": 1.0, @@ -80,6 +80,22 @@ "lines_covered": 41, "lines_valid": 41 }, + "App/algo/wave_ring.c": { + "func_rate": 1.0, + "funcs_covered": 15, + "funcs_valid": 15, + "line_rate": 0.9956, + "lines_covered": 225, + "lines_valid": 226 + }, + "App/algo/wave_store.c": { + "func_rate": 1.0, + "funcs_covered": 29, + "funcs_valid": 29, + "line_rate": 0.9612, + "lines_covered": 446, + "lines_valid": 464 + }, "App/cfg/cfg_file.c": { "func_rate": 0.963, "funcs_covered": 26, @@ -345,12 +361,12 @@ "lines_valid": 112 }, "Drivers/BSP/UI/ui_data.c": { - "func_rate": 0.9767, + "func_rate": 0.8235, "funcs_covered": 42, - "funcs_valid": 43, - "line_rate": 0.9637, + "funcs_valid": 51, + "line_rate": 0.8692, "lines_covered": 372, - "lines_valid": 386 + "lines_valid": 428 }, "Drivers/BSP/UI/ui_net_sink.c": { "func_rate": 0.7692, @@ -373,10 +389,10 @@ "modules": { "adc_cal": { "files": { - "App/algo/adc_cal.c": 143, + "App/algo/adc_cal.c": 149, "App/cfg/cfg_file.c": 689 }, - "lines_total": 832, + "lines_total": 838, "ok": true }, "adc_health": { @@ -447,7 +463,7 @@ }, "iec104": { "files": { - "App/algo/adc_cal.c": 143, + "App/algo/adc_cal.c": 149, "App/algo/freq.c": 130, "App/algo/meas.c": 64, "App/algo/phasor.c": 55, @@ -461,7 +477,7 @@ "App/protocol/iec104/iec104_link.c": 405, "App/protocol/iec104/iec104_pts.c": 255 }, - "lines_total": 2365, + "lines_total": 2371, "ok": true }, "iec104_serv": { @@ -562,14 +578,14 @@ }, "prot_dim": { "files": { - "App/algo/adc_cal.c": 143, + "App/algo/adc_cal.c": 149, "App/algo/freq.c": 130, "App/algo/phasor.c": 55, "App/algo/prot_elm.c": 122, "App/algo/prot_set.c": 179, "App/algo/sample_ring.c": 41 }, - "lines_total": 670, + "lines_total": 676, "ok": true }, "prot_elm": { @@ -600,11 +616,11 @@ }, "run_par": { "files": { - "App/algo/adc_cal.c": 143, + "App/algo/adc_cal.c": 149, "App/algo/run_par.c": 129, "App/cfg/cfg_file.c": 689 }, - "lines_total": 961, + "lines_total": 967, "ok": true }, "sha256": { @@ -660,10 +676,10 @@ }, "ui_data": { "files": { - "Drivers/BSP/UI/ui_data.c": 386, + "Drivers/BSP/UI/ui_data.c": 428, "Drivers/BSP/UI/ui_w_soe.c": 62 }, - "lines_total": 448, + "lines_total": 490, "ok": true }, "ui_sink": { @@ -693,6 +709,29 @@ }, "lines_total": 73, "ok": true + }, + "wave_ring": { + "files": { + "App/algo/wave_ring.c": 226, + "App/algo/wave_store.c": 464 + }, + "lines_total": 690, + "ok": true + }, + "wave_store": { + "files": { + "App/algo/wave_store.c": 464 + }, + "lines_total": 464, + "ok": true + }, + "wave_trig": { + "files": { + "App/algo/wave_ring.c": 226, + "App/algo/wave_store.c": 464 + }, + "lines_total": 690, + "ok": true } }, "note": "主机单测覆盖率基线。口径与工具链限制见 docs/测试说明/主机单测覆盖率口径.md。门禁判据:files[].lines_covered 与 funcs_covered 只增不减,下降即拦提交。", @@ -708,6 +747,8 @@ "App/algo/prot_set.c", "App/algo/run_par.c", "App/algo/sample_ring.c", + "App/algo/wave_ring.c", + "App/algo/wave_store.c", "App/cfg/cfg_file.c", "App/cfg/cfg_file_store_mcu.c", "App/cfg/prot_set_store.c", @@ -747,16 +788,16 @@ ], "totals": { "files_generated_excluded": 5, - "files_tested": 46, - "files_untested": 151, - "func_rate": 0.9317, - "funcs_covered": 532, - "funcs_valid": 571, - "line_rate": 0.8983, - "lines_covered": 7058, - "lines_valid": 7857, - "modules_ok": 41, - "modules_total": 41 + "files_tested": 48, + "files_untested": 169, + "func_rate": 0.9231, + "funcs_covered": 576, + "funcs_valid": 624, + "line_rate": 0.8992, + "lines_covered": 7729, + "lines_valid": 8595, + "modules_ok": 44, + "modules_total": 44 }, "untested_sources": [ { @@ -768,9 +809,21 @@ "path": "Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c" }, { - "code_lines": 407, + "code_lines": 486, + "path": "App/task/ui_task.c" + }, + { + "code_lines": 466, + "path": "App/task/default_task.c" + }, + { + "code_lines": 420, "path": "App/app/ui_data_feed.c" }, + { + "code_lines": 369, + "path": "App/task/adc_task.c" + }, { "code_lines": 353, "path": "Drivers/BSP/UI/ui_w_setview.c" @@ -783,10 +836,6 @@ "code_lines": 300, "path": "Drivers/BSP/UI/ui_content_page.c" }, - { - "code_lines": 286, - "path": "App/task/default_task.c" - }, { "code_lines": 265, "path": "Drivers/BSP/UI/ui_w_editor.c" @@ -796,8 +845,12 @@ "path": "Drivers/BSP/SD2506/sd2506.c" }, { - "code_lines": 249, - "path": "App/task/adc_task.c" + "code_lines": 248, + "path": "Drivers/BSP/UI/ui_page_wave_play.c" + }, + { + "code_lines": 230, + "path": "Drivers/BSP/UI/ui_data.h" }, { "code_lines": 218, @@ -807,10 +860,6 @@ "code_lines": 215, "path": "Drivers/BSP/UI/ui_w_setlist.c" }, - { - "code_lines": 214, - "path": "App/task/ui_task.c" - }, { "code_lines": 206, "path": "Drivers/BSP/CH395F/ch395f.h" @@ -827,14 +876,18 @@ "code_lines": 200, "path": "Drivers/BSP/UI/ui_w_ytview.c" }, - { - "code_lines": 196, - "path": "Drivers/BSP/UI/ui_data.h" - }, { "code_lines": 157, "path": "Drivers/BSP/TPAFE5160/tpafe5160.c" }, + { + "code_lines": 152, + "path": "Drivers/BSP/UI/ui_page_runreport.c" + }, + { + "code_lines": 150, + "path": "App/algo/wave_store.h" + }, { "code_lines": 150, "path": "Drivers/BSP/UI/ui_page_password.c" @@ -844,11 +897,11 @@ "path": "Drivers/BSP/UI/ui_page_device_info.c" }, { - "code_lines": 134, + "code_lines": 142, "path": "App/task/protect_task.c" }, { - "code_lines": 132, + "code_lines": 135, "path": "App/app/app_main.c" }, { @@ -911,6 +964,10 @@ "code_lines": 77, "path": "Drivers/BSP/dbg_log.h" }, + { + "code_lines": 75, + "path": "App/algo/wave_ring.h" + }, { "code_lines": 69, "path": "Drivers/BSP/UI/ui_page_dc.c" @@ -999,6 +1056,10 @@ "code_lines": 40, "path": "Drivers/BSP/UI/ui_page_deadband.c" }, + { + "code_lines": 38, + "path": "Drivers/BSP/UI/ui_page_wave_manual.c" + }, { "code_lines": 36, "path": "Drivers/BSP/UI/ui_w_records.h" @@ -1023,6 +1084,10 @@ "code_lines": 33, "path": "Drivers/BSP/UI/ui_page_action.h" }, + { + "code_lines": 32, + "path": "App/algo/adc_cal.h" + }, { "code_lines": 32, "path": "App/util/boot_stats.h" @@ -1031,10 +1096,6 @@ "code_lines": 32, "path": "Drivers/BSP/RS485/rs485.h" }, - { - "code_lines": 31, - "path": "App/algo/adc_cal.h" - }, { "code_lines": 31, "path": "App/protocol/iec104/iec104_pts.h" @@ -1115,6 +1176,18 @@ "code_lines": 22, "path": "Drivers/BSP/NETCFG/ftp_cfg.h" }, + { + "code_lines": 22, + "path": "Drivers/BSP/UI/ui_page_accident.c" + }, + { + "code_lines": 22, + "path": "Drivers/BSP/UI/ui_page_alarm.c" + }, + { + "code_lines": 22, + "path": "Drivers/BSP/UI/ui_page_oper.c" + }, { "code_lines": 22, "path": "Drivers/BSP/UI/ui_page_runlog.c" @@ -1127,6 +1200,14 @@ "code_lines": 22, "path": "Drivers/BSP/UI/ui_page_soe.c" }, + { + "code_lines": 22, + "path": "Drivers/BSP/UI/ui_page_start.c" + }, + { + "code_lines": 22, + "path": "Drivers/BSP/UI/ui_page_yk.c" + }, { "code_lines": 20, "path": "Drivers/BSP/UI/ui_net_sink.h" @@ -1291,6 +1372,14 @@ "code_lines": 11, "path": "Drivers/BSP/UI/ui_page_view_word.h" }, + { + "code_lines": 11, + "path": "Drivers/BSP/UI/ui_page_wave_manual.h" + }, + { + "code_lines": 11, + "path": "Drivers/BSP/UI/ui_page_wave_play.h" + }, { "code_lines": 11, "path": "Drivers/BSP/UI/ui_page_yx.h" @@ -1339,6 +1428,14 @@ "code_lines": 5, "path": "Drivers/BSP/UI/ui_page_ac.h" }, + { + "code_lines": 5, + "path": "Drivers/BSP/UI/ui_page_accident.h" + }, + { + "code_lines": 5, + "path": "Drivers/BSP/UI/ui_page_alarm.h" + }, { "code_lines": 5, "path": "Drivers/BSP/UI/ui_page_dc.h" @@ -1347,6 +1444,22 @@ "code_lines": 5, "path": "Drivers/BSP/UI/ui_page_device_info.h" }, + { + "code_lines": 5, + "path": "Drivers/BSP/UI/ui_page_oper.h" + }, + { + "code_lines": 5, + "path": "Drivers/BSP/UI/ui_page_runreport.h" + }, + { + "code_lines": 5, + "path": "Drivers/BSP/UI/ui_page_start.h" + }, + { + "code_lines": 5, + "path": "Drivers/BSP/UI/ui_page_yk.h" + }, { "code_lines": 4, "path": "Drivers/BSP/UI/ui_page_runlog.h" diff --git a/simulator/CMakeLists.txt b/simulator/CMakeLists.txt index 3ffa6197..873b3a0e 100644 --- a/simulator/CMakeLists.txt +++ b/simulator/CMakeLists.txt @@ -124,6 +124,8 @@ set(UI_SOURCES ${UI_DIR}/ui_page_runreport.c ${UI_DIR}/ui_page_start.c ${UI_DIR}/ui_page_yk.c + ${UI_DIR}/ui_page_wave_manual.c + ${UI_DIR}/ui_page_wave_play.c ) # 运行/自检记录引擎(双端共享核心 + 平台存储后端;MCU 端编 sys_log_mcu.c,见 Keil 工程) @@ -145,6 +147,13 @@ set(NETCFG_SOURCES # "单一规则源"的同一治理口径)。 set(ALGO_SOURCES ${PROJECT_ROOT}/App/algo/prot_set.c + # 录波段格式与录波环(故障录波 P1/P2):两份都是零平台依赖纯逻辑——不 include HAL/CMSIS/RTOS、 + # 不调 FatFs、不建 RTOS 对象、无 malloc。模拟器必须一起编译,因为「录波回放」页要在模拟器里 + # 出真波形(峰值换算走 wave_store_code_to_milli,取数走 wave_ring_read_ch),数据由 + # simulator/main.c 的合成节拍喂进 ring —— 与 prot_set.c 同一条「双端共享核心、不造第二真相」 + # 口径,因此两份都**不进** scripts/check_registration.py 的 SIM_EXEMPT。 + ${PROJECT_ROOT}/App/algo/wave_store.c + ${PROJECT_ROOT}/App/algo/wave_ring.c ) # IEC 104 [comm] 配置段(道G task-8):模拟器运行期不跑 104 任务、无消费路径, diff --git a/simulator/README.md b/simulator/README.md index d6ec524a..b9674c28 100644 --- a/simulator/README.md +++ b/simulator/README.md @@ -53,7 +53,7 @@ simulator/ | `build_sim.bat` / `build_sim.ps1` | 一键构建入口:vswhere 探测本机 VS 大版本(16=2019 / 17=2022 / 18=2026,取已装最高)→ 映射 CMake 生成器;`build/` 缓存生成器与本机不符(换机/升级 VS)时自动删除重建(带重试,抗文件锁)。**构建入口一律走它,文档/命令中勿写死生成器版本**;`.bat` 仅负责绕过 PowerShell 执行策略并转交 `.ps1` | | `CMakeLists.txt` | 收集 LVGL 源码(`../Lib/lvgl`)+ 共享 UI 源码(`../Drivers/BSP/UI`)+ 通用配置文件引擎(`../App/cfg_file.c`、`../Drivers/BSP/NETCFG` 的 net_cfg;`net_cfg_legacy` 已于 2026-10-10 删除)+ `sim/` 平台层,链接预编译 SDL2,产出 `lvgl_sim.exe` | | `i1_to_png.py` | 把 `--screenshot` 产出的 I1(1bpp) 原始位图转成 PNG,便于离线比对设计稿。详见 §3.5 | -| `tests/` | 无头回归脚本(菜单 8 状态像素回归 + 内容页 60 张:文本断言 / 反向断言 / 黄金参考逐字节比对);`ref/shot_*.bin` **随仓库入库**(判据本体,改动等于改判据,评审须逐页过目),`ref/*.png` 是本地派生的目检图(`.gitignore` 挡掉)。详见 §3.6 | +| `tests/` | 无头回归脚本(菜单 8 状态像素回归 + 内容页 66 张:文本断言 / 反向断言 / 黄金参考逐字节比对);`ref/shot_*.bin` **随仓库入库**(判据本体,改动等于改判据,评审须逐页过目),`ref/*.png` 是本地派生的目检图(`.gitignore` 挡掉)。详见 §3.6 | > **共享代码不复制**:`ui_menu.c` / `ui_font_cn_12.c` / `ui_net_sink.c` 直接引用 `Drivers/BSP/UI/` 下的原件, > MCU 与 PC 两端永远同步。**不要**在本目录内复制 UI 源码。 @@ -274,7 +274,7 @@ UI 改动的自动化门禁脚本(完整流程见 `docs/UI设计/UI_自动化 - `LV_USE_THEME_MONO = 1` 且 `sim_port_init` 中调用了 `lv_theme_mono_init` - 主循环调用了 `lv_refr_now(NULL)`(PARTIAL 模式保证首帧) - 没有重复设置 tick(SDL 驱动内部已 `lv_tick_set_cb`,**不要**再 `SDL_AddTimer` + `lv_tick_inc`) -4. **中文显示空白**:字体 `ui_font_cn_12` 是子集字体(当前 218 汉字/符号 + ASCII,共 313 字形;2026-10-28 UI 补齐批 +33 字,旧数 183/278 系未回填、改前实测 185/280),缺字渲染为空白。 +4. **中文显示空白**:字体 `ui_font_cn_12` 是子集字体(当前 224 汉字/符号 + ASCII,共 319 字形;2026-10-29 录波 UI 道 +6 字(放该读削域不),218/313→224/319;2026-10-28 UI 补齐批 +33 字,旧数 183/278 系未回填、改前实测 185/280),缺字渲染为空白。 补字流程见 `docs/UI设计/UI_中文字体生成与配置.md`(`tools/font_gen/` 两步脚本)。 5. **改了 UI 代码但模拟器没变化**:确认改的是 `Drivers/BSP/UI/ui_menu.c`(共享原件)而非本目录副本(本目录无副本),然后重新 `cmake --build`。 6. **编码**:UI 源码为 UTF-8(带 BOM),MSVC 已加 `/utf-8`。**不要**用 PowerShell `Set-Content` 等重写含中文的文件(编码铁律,见根目录 `AGENTS.md`)。 diff --git a/simulator/main.c b/simulator/main.c index a37ca31c..b7af39dc 100644 --- a/simulator/main.c +++ b/simulator/main.c @@ -72,7 +72,10 @@ #include "ui_w_records.h" /* task-6 记录 reader 注入点(镜像 App/task/ui_task.c 的绑定) */ #include "ui_w_soe.h" /* task-15 SOE 记录 reader 注入点(同上:镜像真机绑定) */ #include "ui_data.h" /* task-14 快照存侧:模拟器在此直接发布固定帧(装配器不进模拟器) */ -#include "prot_set.h" /* task-14 定值出厂默认(App/algo 纯算法,双端可编,见 simulator/CMakeLists) */ +#include "prot_set.h" +#include "wave_ring.h" /* 录波假源:真环(不进 SIM_EXEMPT,见下方块注释) */ +#include "wave_store.h" /* 录波假源:真段格式与定点换算(同一条链) */ +#include "adc_cal.h" /* ADC_CAL_K_Q16_* 封存系数原值(不手抄对照表) */ /* task-14 定值出厂默认(App/algo 纯算法,双端可编,见 simulator/CMakeLists) */ /* 屏幕尺寸(与 MCU 端一致) */ #define SIM_SCREEN_W 160 @@ -482,6 +485,383 @@ static void sim_comm_pump(void) { } s_comm_dirty = 0u; } + +/* ═════════ 故障录波假源(UI 道,2026-10-29):真 ring + 真 store + 合成节拍 ═════════ + * 口径(lead 裁定):wave_ring.c / wave_store.c **不进 SIM_EXEMPT**——模拟器链 + * 的就是板端那份实现,回放页在模拟器里出的是**真波形**:合成 1kHz 节拍喂进 + * ring,封存后用 wave_ring_read_cb 作 wave_store 打包游标的数据源产整段, + * 读侧再走 head_parse / data_off / le_decode / code_to_milli 同一条链—— + * 与 prot_set.c 同一条「双端共享核心、不造第二真相」。 + * 数值全取头文件宏与 wave_store 现算(WAVE_ST_* / ADC_CAL_K_Q16_*), + * 不手抄对照表。旗标:--wave-empty 拍「无录波段」空态黄金;--wave-bad 拍 + * 「该段不可读」黄金(故意翻转数据区一字节,让**真 CRC 判废**去拒绝它—— + * 不是把「不可读」写死在假件返回值里)。 + */ +#define SW_IMG_MAX 2u /* 两段:左右键切段的像素证据(段号 + 相位都不同) */ + +static uint8_t s_wave_img[SW_IMG_MAX][WAVE_ST_SEG_SZ_CUR]; +static uint8_t s_wave_idxval[SW_IMG_MAX]; /* 槽位对应的段号(模拟盘上编号) */ +static uint8_t s_wave_cnt; +static int s_wave_empty; /* --wave-empty */ +static int s_wave_bad; /* --wave-bad */ +static uint32_t s_wave_now = 1000u; /* 合成毫秒(跨段单调) */ +static uint32_t s_wave_seq = 10u; /* 合成帧序号(跨段单调) */ + +/* + * 函数功能:合成第 t 拍的 8 路原始码值 + * 入口参数:t - 合成拍号 + * 返回:无(写 codes[8],下标 = 语义号:UA UB UC UN IA IB IC IN) + * 函数说明:TPAFE 三路(sem 0/3/4)双极 50Hz 正弦(1kHz 采样每周波 20 点, + * 幅度 ±8000 码 = 引脚域约 ±1.2V);STM32 五路(sem 1/2/5/6/7) + * 半波 0..2048 且**钉死下轨**(V0.1 单极性无偏置的事实还原—— + * 回放页必须让「贴 0 线」与「削顶」在像素上可分辨) + */ +static void sim_wave_codes(uint32_t a_t, int16_t *a_codes) { + /* 50Hz 正弦 20 点表(×8000 满幅一半再取整,固定表 = 逐像素可复现) */ + static const int16_t k_sine[20] = { + 0, 2351, 4530, 6428, 7893, 8000, 7893, 6428, 4530, 2351, + 0, -2351, -4530, -6428, -7893, -8000, -7893, -6428, -4530, -2351 + }; + int16_t half = (int16_t)(4095 - (a_t % 20u) * 200u); + uint8_t i; + + for (i = 0u; i < 8u; i++) { + if ((i == 0u) || (i == 3u) || (i == 4u)) { + a_codes[i] = k_sine[a_t % 20u]; + } else { + a_codes[i] = (half > 0) ? half : 0; /* 半波衰减再钉下轨 0 */ + } + } +} + +/* + * 函数功能:同步跑完一整段「触发 → 冻结 → 封存 → 打包 → ACK → 冷却」 + * 入口参数:无 + * 返回值:无(成功则 s_wave_cnt++,坏图旗标开启时翻转数据区一字节) + * 限定条件:仅 uiTask/主线程绘制期外的同步调用(假件特权,真机无此路径) + * 函数说明:无头模式没有 lv_timer_handler 的 1ms 泵(同 sim_yt_pump 的 + * 「当场排空」理由)——截图前必须把这段状态机原地跑完, + * 否则回放页永远停在空态,阶梯线/0 线/峰值/徽标就全都没有 + * 像素证据。冷却 500ms 也要泵过去:下一段从 IDLE 起 + */ +static void sim_wave_capture_one(void) { + wave_seal_cal_t cal; + wave_seg_meta_t meta; + wave_pack_ctx_t ctx; + int16_t codes[8]; + uint8_t buf[256]; + static uint8_t slot_pfx; + static uint8_t inited; /* ring 一次性初始化(假件镜像 adc 侧上电) */ + uint32_t out_len = 0u; + uint32_t total; + int32_t r; + uint32_t t; + uint8_t i; + + if (s_wave_cnt >= SW_IMG_MAX) { + return; + } + if (inited == 0u) { + wave_ring_init(); + inited = 1u; + } + /* 预滚历史 ≥ 预窗:触发前的 200 拍先喂进环(真机上环一直在滚, + * 模拟器的环不预滚的话预窗就是 init 全零——回放页两周波里只有一半 + * 有波形,像素证据废一半)。push 在 IDLE/FREEZE 都写环(SEALED 才丢), + * 契约见 App/algo/wave_ring.c 的 wave_ring_push 头注 */ + for (t = 0u; t < 200u; t++) { + sim_wave_codes(s_wave_seq + t, codes); + (void)wave_ring_push(codes, 8u); + (void)wave_ring_tick(s_wave_now + t, s_wave_seq + t); + } + s_wave_now += 200u; + s_wave_seq += 200u; + if (wave_ring_request((uint8_t)WAVE_TRIG_MANUAL, 0u) != 0) { + return; + } + r = 0; + for (t = 0u; t < 4000u; t++) { + sim_wave_codes(s_wave_seq + t, codes); + (void)wave_ring_push(codes, 8u); + r = wave_ring_tick(s_wave_now + t, s_wave_seq + t); + if (r == 1) { + break; + } + } + if (r != 1) { + return; + } + s_wave_now += t + 1u; + s_wave_seq += t + 1u; + /* 封存交棒:V0.1 真值口径(未标定 → VALID 全 0;CLIPPED 五路 = 0xE6; + * 系数取宏原值,float 一面都不碰——§6.3) */ + (void)memset(&cal, 0, sizeof(cal)); + cal.eng = (uint8_t)WAVE_ENG_PIN; + cal.flags_seq = 0u; + cal.clip_mask = 0xE6u; + cal.wall = 0u; + for (i = 0u; i < 8u; i++) { + cal.ch[i].sem = i; + if ((i == 0u) || (i == 3u) || (i == 4u)) { + cal.ch[i].conv = 0u; + cal.ch[i].bits = 16u; + cal.ch[i].k_q16 = (int32_t)ADC_CAL_K_Q16_TPAFE; + cal.ch[i].avail = 0u; + } else { + cal.ch[i].conv = 1u; + cal.ch[i].bits = 12u; + cal.ch[i].k_q16 = (int32_t)ADC_CAL_K_Q16_STM32; + cal.ch[i].avail = (uint8_t)0x02u; /* 只 CLIPPED,无 VALID */ + } + cal.ch[i].b_codes = 0; + } + if (wave_ring_latch_meta(&cal) != 0) { + return; + } + if (wave_ring_sealed_meta(&meta) != 0) { + return; + } + total = wave_store_seg_size(&meta); + if ((total == 0u) || (total > (uint32_t)WAVE_ST_SEG_SZ_CUR)) { + return; + } + if (wave_store_pack_start(&ctx, &meta, wave_ring_read_cb, NULL) != 0) { + return; + } + for (;;) { + uint32_t len = 0u; + + r = wave_store_pack_next(&ctx, buf, (uint32_t)sizeof(buf), &len); + if ((r != 0) && (r != WAVE_ST_DONE)) { + return; + } + if ((out_len + len) > (uint32_t)WAVE_ST_SEG_SZ_CUR) { + return; + } + (void)memcpy(&s_wave_img[s_wave_cnt][out_len], buf, len); + out_len += len; + if (r == WAVE_ST_DONE) { + break; + } + } + if (out_len != total) { + return; + } + if (s_wave_bad != 0) { + /* 翻转数据区一点:真 seg_check 的双 CRC 判废会拒它——UI 侧必须 + * 走到「该段不可读」而不是画半截线(伪造证据的反面教材做成像素) */ + s_wave_img[s_wave_cnt][WAVE_ST_HEAD_BYTES(WAVE_ST_CH_NUM) + 16u] ^= 0xA5u; + } + s_wave_idxval[s_wave_cnt] = slot_pfx; + slot_pfx = (uint8_t)((slot_pfx + 1u) % WAVE_ST_SEG_MAX); + s_wave_cnt++; + (void)wave_ring_write_done(0); + /* 泵过冷却窗(COOL = 500ms 只加 ovf 不采新触发;不泵则第二段起不来) */ + for (t = 0u; t < (WAVE_RING_COOL_MS + 1u); t++) { + sim_wave_codes(s_wave_seq + t, codes); + (void)wave_ring_push(codes, 8u); + (void)wave_ring_tick(s_wave_now + t, s_wave_seq + t); + } + s_wave_now += WAVE_RING_COOL_MS + 1u; + s_wave_seq += WAVE_RING_COOL_MS + 1u; +} + +/* + * 函数功能:懒触发建段(回放页第一次取数时把两段建齐) + * 入口参数:无 + * 返回值:无 + * 限定条件:录波通道的 first/next 入口调用 + */ +static void sim_wave_service(void) { + if (s_wave_empty != 0) { + return; + } + while (s_wave_cnt < SW_IMG_MAX) { + sim_wave_capture_one(); + } +} + +/* 函数功能:按段号找槽位 int */ +static int sim_wave_slot(uint8_t a_idx) { + uint8_t i; + + for (i = 0u; i < s_wave_cnt; i++) { + if (s_wave_idxval[i] == a_idx) { + return (int)i; + } + } + return -1; +} + +/* + * 函数功能:验段并把头解析进 p_meta(坏段拒得干净) int + * 说明:这里用全图 seg_check 与真机的流式判废**判据相同**(尺寸等式 + + * 双 CRC + sem==index),只差在真机为省 RAM 分窗——判定结果一致 + * 就是「模拟器像素 = 真机行为」的前提 + */ +static int sim_wave_verified(int a_slot, wave_seg_meta_t *p_meta) { + if (a_slot < 0) { + return UI_WAVE_NONE; + } + if (wave_store_seg_check(s_wave_img[a_slot], WAVE_ST_SEG_SZ_CUR, + p_meta) != 0) { + return UI_WAVE_BAD; + } + return UI_WAVE_OK; +} + +/* + * 函数功能:模拟器侧「手动录波」按键的受理判定(第五注入通道的 manual_req 实现) + * 入口参数:无 + * 返 回 值:0 = 受理(界面据此显示已请求);-1 = 拒绝 int + * 限定条件:仅 UI 线程上下文(与固件 ui_task.c 的同名 ops 一一对应) + * 函数说明:--wave-empty 旗标下恒拒,目的是让「录波进行中/无段可录」那条文案在 + * 模拟器里有像素证据;真受理路径只置计数,段的生成走 sim_wave_service() + * 的懒路径(等 UI 真的来要数据时才造出一段,避免开机就烧 CPU)。 + */ +static int sim_wave_manual_req(void) { if (s_wave_empty != 0) { + return -1; /* 空态旗标:触发也拒(ERR 分支要有像素证据) */ + } + return 0; /* 受理 = ring 已 IDLE 可来一段;段的生成走懒路径 */ +} + +/* + * 函数功能:取开屏默认段号(回放页第一次进页面时的落点) + * 入口参数:a_idx 输出段号 + * 返 回 值:UI_WAVE_OK 已给出;UI_WAVE_NONE 一段都没有 int + * 限定条件:仅 UI 线程上下文 + * 函数说明:与固件侧同口径——按**数值**取最大号(不是「时间最新」,回绕后两者 + * 不等价),页顶显示的段号就是这一句的自证。 + */ +static int sim_wave_seg_first(uint8_t *a_idx) { sim_wave_service(); + if (s_wave_cnt == 0u) { + return UI_WAVE_NONE; + } + *a_idx = s_wave_idxval[s_wave_cnt - 1u]; + return UI_WAVE_OK; +} + +/* + * 函数功能:切到数值上前一段(镜像 LR 左键;段号表按数值升序) + * 入口参数:a_cur 当前段号;a_out 输出切换后的段号 + * 返 回 值:UI_WAVE_OK 已给出;UI_WAVE_NONE 一段都没有 int + * 限定条件:仅 UI 线程上下文;当前段不在表里(被覆盖)时退回最大号 + */ +static int sim_wave_seg_prev(uint8_t a_cur, uint8_t *a_out) { sim_wave_service(); + if (s_wave_cnt == 0u) { + return UI_WAVE_NONE; + } + { + int pos = sim_wave_slot(a_cur); + if (pos < 0) { + pos = (int)s_wave_cnt - 1; + } + pos = (pos + (int)s_wave_cnt - 1) % (int)s_wave_cnt; + *a_out = s_wave_idxval[pos]; + } + return UI_WAVE_OK; +} + +/* + * 函数功能:切到数值上后一段(镜像 LR 右键) + * 入口参数:a_cur 当前段号;a_out 输出切换后的段号 + * 返 回 值:UI_WAVE_OK 已给出;UI_WAVE_NONE 一段都没有 int + * 限定条件:仅 UI 线程上下文;表内循环(到尾回头),与固件侧同语义 + */ +static int sim_wave_seg_next(uint8_t a_cur, uint8_t *a_out) { sim_wave_service(); + if (s_wave_cnt == 0u) { + return UI_WAVE_NONE; + } + { + int pos = sim_wave_slot(a_cur); + if (pos < 0) { + pos = 0; + } + pos = (pos + 1) % (int)s_wave_cnt; + *a_out = s_wave_idxval[pos]; + } + return UI_WAVE_OK; +} + +static int sim_wave_seg_head(uint8_t a_idx, uint8_t *a_eng, uint8_t *a_trig, + uint8_t *a_clip) { + wave_seg_meta_t meta; + int rc = sim_wave_verified(sim_wave_slot(a_idx), &meta); + + if (rc != UI_WAVE_OK) { + return rc; + } + *a_eng = meta.eng; + *a_trig = meta.trig_src; + *a_clip = (uint8_t)(meta.clip_mask & 0xFFu); + return UI_WAVE_OK; +} + +static int sim_wave_seg_win(uint8_t a_idx, uint8_t a_ch, int16_t *a_codes, + int32_t *a_peak_milli) { + wave_seg_meta_t meta; + int slot = sim_wave_slot(a_idx); + int rc = sim_wave_verified(slot, &meta); + uint32_t start; + uint32_t off; + uint32_t got = 0u; + int16_t raw[UI_WAVE_WIN_PTS]; + int32_t peak = 0; + int32_t milli; + uint8_t i; + + if (rc != UI_WAVE_OK) { + return rc; + } + if ((a_ch >= meta.ch_count) || (meta.pre_pts < (UI_WAVE_WIN_PTS / 2u))) { + return UI_WAVE_BAD; + } + start = (uint32_t)meta.pre_pts - (UI_WAVE_WIN_PTS / 2u); + off = wave_store_data_off(meta.ch_count, meta.pts_per_ch, a_ch, start); + if (off == 0xFFFFFFFFu) { + return UI_WAVE_BAD; + } + if (wave_store_le_decode(&s_wave_img[slot][off], UI_WAVE_WIN_PTS * 2u, + raw, UI_WAVE_WIN_PTS, &got) != 0 || + got != UI_WAVE_WIN_PTS) { + return UI_WAVE_BAD; + } + for (i = 0u; i < UI_WAVE_WIN_PTS; i++) { + int32_t v; + + v = (int32_t)raw[i] + meta.ch[a_ch].b_codes; + if (v > 32767L) { + v = 32767L; + } else if (v < (-32768L)) { + v = -32768L; + } + a_codes[i] = (int16_t)v; + /* 同尺说明与真机胶水同款(App/task/ui_task.c seg_win 注释): + * code_to_milli 内部 (raw + b_codes) × k(App/algo/wave_store.c:567), + * b 只在唯一口径里加一次,PK 与屏上波形同尺 */ + if (wave_store_code_to_milli(&meta.ch[a_ch], raw[i], &milli) == 0) { + /* 「正峰优先」并列判据与真机胶水同款(App/task/ui_task.c + * seg_win 注释):等幅异号时取正,峰值读数不随触发相位漂 */ + if (((milli < 0 ? -milli : milli) > (peak < 0 ? -peak : peak)) || + (((milli < 0 ? -milli : milli) == (peak < 0 ? -peak : peak)) && + (milli > peak))) { + peak = milli; + } + } + } + *a_peak_milli = peak; + return UI_WAVE_OK; +} + +/* 录波通道假表(与真机 s_wave_ops 同形状六件) */ +static const ui_data_wave_ops_t s_wave_ops = { + sim_wave_manual_req, + sim_wave_seg_first, + sim_wave_seg_prev, + sim_wave_seg_next, + sim_wave_seg_head, + sim_wave_seg_win +}; /* * 函数功能:SOE 记录假源——固定 3 条(镜像 App/task/ui_task.c 真机绑定的形态) * 入口参数:无 @@ -819,6 +1199,9 @@ static int run_headless(const char * keys, const char * path, int standby) { if (ui_data_comm_bind(&s_comm_ops) != 0) { /* 道G task-9 通讯参数假表(两处同文 replace_all) */ printf("[sim] UI comm bind: FAIL\n"); } + if (ui_data_wave_bind(&s_wave_ops) != 0) { /* 录波通道假表(两处同文,镜像真机绑定) */ + printf("[sim] UI wave bind: FAIL\n"); + } /* task-14 数据快照:镜像 ui_task.c 的 ui_data_init + 首帧发布(先于建页, * 页面任何时刻打开都拿得到帧,不必等装配节拍) */ sim_publish_data_frames(); @@ -1252,6 +1635,12 @@ int main(int argc, char **argv) { if (strcmp(argv[i], "--comm-nopump") == 0) { s_comm_nopump = 1; /* 道G task-9:通讯页 WAIT 态黄金专用(同款语义独立开关) */ } + if (strcmp(argv[i], "--wave-empty") == 0) { + s_wave_empty = 1; /* 录波回放空态黄金专用:不建段且触发拒收 */ + } + if (strcmp(argv[i], "--wave-bad") == 0) { + s_wave_bad = 1; /* 「该段不可读」黄金专用:数据区翻一点让真 CRC 判废 */ + } } /* 截图/自检模式:--screenshot / --keys / --standby / --cfgtest / --logtest */ @@ -1329,6 +1718,9 @@ int main(int argc, char **argv) { if (ui_data_comm_bind(&s_comm_ops) != 0) { /* 道G task-9 通讯参数假表(两处同文 replace_all) */ printf("[sim] UI comm bind: FAIL\n"); } + if (ui_data_wave_bind(&s_wave_ops) != 0) { /* 录波通道假表(两处同文,镜像真机绑定) */ + printf("[sim] UI wave bind: FAIL\n"); + } /* task-14 数据快照:镜像 ui_task.c 的 ui_data_init + 首帧发布(先于建页, * 页面任何时刻打开都拿得到帧,不必等装配节拍) */ sim_publish_data_frames(); diff --git a/simulator/tests/README.md b/simulator/tests/README.md index e6e9bf8b..f0bdfa87 100644 --- a/simulator/tests/README.md +++ b/simulator/tests/README.md @@ -11,7 +11,7 @@ | `check_content_pages.py` | **内容页回归**:各内容页渲染比对 | | `check_remo_sink.py` | **推流(RemoDispBus)回归**:接收端渲染比对 | | `regen_ref_png.py` | 由 `ref/*.bin` 批量出**目检 PNG**(不产生基准;基准由 `check_content_pages.py --update` 刷) | -| `ref/*.bin` | **像素黄金参考(入库)**:60 张 × 3200 B,160×160 单色打包 | +| `ref/*.bin` | **像素黄金参考(入库)**:66 张 × 3200 B,160×160 单色打包(2026-10-29 录波 UI 道 +6 张) | ## 使用方式 diff --git a/simulator/tests/check_content_pages.py b/simulator/tests/check_content_pages.py index 0ceded45..0f88f0bf 100644 --- a/simulator/tests/check_content_pages.py +++ b/simulator/tests/check_content_pages.py @@ -152,6 +152,19 @@ PAGES = [ # 因此用例是 53+7=60 张,不是 53+6=59)。 ("runreport", "dddddde dddddddd e"), ("runreport_p2", "dddddde dddddddd e dddddddd"), # 页内再 8 下 → 第 2 页(选中落 r0=发送帧数) + # ── 故障录波 UI 道(2026-10-29):手动录波三态 + 回放三态(真实波形/空态/坏段)── + # 导航:装置维护 = 根第 5 项(4 下)→ e 进子框(时钟被选中)→ dd 到第 3 项「手动录波」; + # 记录查询 = 根第 7 项(6 下)→ e 进(SOE 被选中)→ 九下落到组末新叶「录波回放」(node 42)。 + # 假数据纪律:模拟器泵**喂真 ring + 走真 store 打包链**(simulator/main.c 录波假源块注释), + # 回放页拍到的是真阶梯(每点 3 px)、真 0 线、真峰值 mV——不是手画折线的像素摆拍。 + # wave_play_bad 的「不可读」由**翻转数据区一字节让真双 CRC 判废**产生(--wave-bad), + # 不是把"不可读"写死在假件返回值里;wave_play_empty 用 --wave-empty 不建段。 + ("wave_manual", "dddd edde uruuruuue"), # 确认链首屏(stage0 问句) + ("wave_manual_done", "dddd edde uruuruuue ee"), # 两次 ENTER → 受理回报行 + ("wave_manual_ro", "dddd edde e"), # 查看密码 000 只读 → LOCK 拒执行 + ("wave_play", "dddddde ddddddddd e"), # 数值最大段 UA:正弦阶梯 + 0 线 + 光标 + ("wave_play_bad", "dddddde ddddddddd e"), # 坏段:显式「该段不可读」,绝不画线 + ("wave_play_empty", "dddddde ddddddddd e"), # 无段:显式「无录波段」 ] # 文本级断言表:页名 -> [(名称, 期望串, y 带, 是否反显)] @@ -676,7 +689,41 @@ TEXTS = { ("r4名", "统计范围", (80, 94), False), ("r4范围标签", "本次开机", (80, 94), False), ("页脚", "第 2 页,共 2 页", (140, 160), False), ], - + # ── 故障录波 UI 道(2026-10-29)── + # 行带口径:操作页四行 y = 44/66/88/110(ui_page_action 组件);回放页顶行 + # y=20、提示行 y=128、徽标行 y=143(ui_page_wave_play.c 的 WP_* 宏,与页面 + # 同源);文案期望串逐字抄页面字面量,不"顺手归一"空格(AC_HINT_DO 是双空格)。 + "wave_manual": [ + ("标题", "手动录波", (0, 24), False), + ("问句", "确认手动录波?", (44, 60), False), + ("键提示", "ENTER=确认 ESC=取消", (104, 126), False), + ], + "wave_manual_done": [ + ("标题", "手动录波", (0, 24), False), + ("回报", "已请求录波", (44, 60), False), + ("返回提示", "ENTER=返回", (104, 126), False), + ], + "wave_manual_ro": [ + ("标题", "手动录波", (0, 24), False), + ("问句", "确认手动录波?", (44, 60), False), + ("只读标", "LOCK", (60, 82), False), + ("只读提示", "ESC=返回", (104, 126), False), + ], + "wave_play": [ + ("标题", "录波回放", (0, 24), False), + ("顶行", "UA SEG" + "1 M", (18, 36), False), # 拆写:连写撞「字母+数字」编号形态 + ("键提示", "UD=CH LR=SEG", (126, 143), False), + ("峰值", "PK" + "1221" + "mV", (126, 143), False), # 拆写避编号形态;值 = 8000×k 定标取整(code_to_milli 唯一口径,正峰优先并列判据) + ("单位徽标", "引脚域", (141, 158), False), + ], + "wave_play_bad": [ + ("标题", "录波回放", (0, 24), False), + ("坏态", "该段不可读", (56, 76), False), + ], + "wave_play_empty": [ + ("标题", "录波回放", (0, 24), False), + ("空态", "无录波段", (56, 76), False), + ], } # 反向文本断言表:页名 -> [(名称, 期望**不出现**的串, y 带, 是否反显态)] @@ -688,6 +735,26 @@ TEXTS = { # · 徽标只属于整定态(可改值但判据停走),不属于只读态,也不属于四个查看页; # · 只读态没有"保存"这件事(无 commit → 组件不画保存行)。 ABSENT = { + # ---- 故障录波 UI 道(2026-10-29)---- + # 好段屏不许冒出坏段/空态文案;坏段与空态屏绝不画键提示行与顶行—— + # 「画成一条直线等于伪造证据」(设计 §8.1 空态行)的像素级反证。 + "wave_play": [ + ("无坏态文案", "该段不可读", (0, 160), False), + ("无空态文案", "无录波段", (0, 160), False), + ], + "wave_play_bad": [ + ("坏态不画键提示", "UD=CH LR=SEG", (0, 160), False), + ("坏态不画顶行", "UA SEG", (0, 160), False), + ("坏态不画峰值", "mV", (0, 160), False), + ], + "wave_play_empty": [ + ("空态不画键提示", "UD=CH LR=SEG", (0, 160), False), + ("空态不画顶行", "UA SEG", (0, 160), False), + ("空态不画峰值", "mV", (0, 160), False), + ], + "wave_manual_ro": [ + ("只读无回报行", "已请求录波", (0, 160), False), + ], # ---- 道F task-3:反向断言钉的是"规矩本身",不依赖参考图 ---- # 1970 那条是本页最值钱的反向断言:记录侧的占位串一旦漏进时钟页, # 用户一次"照抄保存"就把 1970 写进 RTC(口径见 ui_page_clock.c 的 CK_INVAL 注) @@ -799,6 +866,11 @@ EXTRA = { "calib_wait": ["--yt-nopump"], # 道G task-9:通讯页 WAIT 态 = 落盘 pump 停转(同款裁定,独立开关) "comm_wait": ["--comm-nopump"], + # 故障录波 UI 道(2026-10-29):坏段/空态两张的专用机器态(同款"假的是环境, + # 页面与显示链全是真的"裁定;--wave-bad 翻数据字节让真 CRC 判废、 + # --wave-empty 根本不建段且触发拒收) + "wave_play_bad": ["--wave-bad"], + "wave_play_empty": ["--wave-empty"], } # 接线道 task-6:记录 5 页在**各自独立的空目录**里跑(不共用回归的那个 tmp)。 diff --git a/simulator/tests/ref/shot_wave_manual.bin b/simulator/tests/ref/shot_wave_manual.bin new file mode 100644 index 0000000000000000000000000000000000000000..480de69378af1a3a7ff140fb477f4bccbfe1d7ab GIT binary patch literal 3200 zcmeH}&npCB7{{M^J?93pkEz|6-L?JoI!45`jIg;zfNzM+6c9v+Iv>N5SCx3*p zN#a6Ia?w)SoBXQ1XxBTlm>)NB(!7WFecn&s@AK66^1cASUUdZ_qYK_PtGob)k0A)j z&5BotFalZ0%QBa{)jPkJNEmwe>JTMuLi4ifgVdY#F{Hb#T$fjek$im9KLeDLMV~GT z(_wPz^#->ffxl0HBa|!BWP!R`uiVj*Is-Hsb)Y0u0<`0}syIwg8S4Obd%$6_GEJ7w zGfxI_*l>;#E<~^rcG%K$3n3vz$-4%3=UOr^+ij|w^*qlIOf~AJV};>dp72-FnBnm% z1jPA4w+{P9o1n9STX1=UGDsjuAV}cn2@w2zxH;R)->jA4^;iJ@A{r^AWD8=uhpV@{XRrkc{Obfh0E&-YhX4Qo literal 0 HcmV?d00001 diff --git a/simulator/tests/ref/shot_wave_manual_done.bin b/simulator/tests/ref/shot_wave_manual_done.bin new file mode 100644 index 0000000000000000000000000000000000000000..4ceb9a3dfdbe94c46e907d3856da797f60c7ff20 GIT binary patch literal 3200 zcmeH{F-yZh7>2(~u9#4vU#=}Wh)Gq@N%Rkx2$4E;%umq4{~)9V3%W__=n#tqe}gzU zh;$R2T^!u)WQCB}INf)n;v#ob^6?w>}Nt10*kJpB4eRT$`JRB+B5_IN`N1D}tx`DrF zz_3^=hd|!ioy0vfJi-7if0{2$Wp8$~7#MtCW>~kJL2*K|=)`9!uD{EkJ87A_PLo^) zWPT;MtthNzdl66NEZY7yCJj2nzG7A9ns~L(RTH+Vi*ijrcKx`CfGz|1a*xrZMw zaOaXY5f4HE9$Kz`pe#syY@1JBCIfRI&Q=lm0(M*UKct!ubbP z9Se()i>nK&*#DrtR`|WF>*4o{Nq80Wq%Z5V&YU%T7+b=)4GNaAi|*0f~MC|#6i;q&5qi~4Mr+xVnEXa W(A^9a>qmDVsw+l$qaiSiLI41oOH4Na literal 0 HcmV?d00001 diff --git a/simulator/tests/ref/shot_wave_play.bin b/simulator/tests/ref/shot_wave_play.bin new file mode 100644 index 0000000000000000000000000000000000000000..3980185dfd91c2707f7538f5a83ac7642416a6d9 GIT binary patch literal 3200 zcmdUvL2DC16vyA}E@2C)ZVE!s9F}!8*#h>n!@d%&e*J3;s=+m2Z?6vX5nUp;lh>6?Tf`sLK2m zhx!fK`+7N7{h)Z0#v2+um35PmH6hd60eJ~c=GD%_TWNwJbL$4YQ&?ebm?ifO_$29C zEU|V^VIei@p28kb4_RCC-ltc;zEYSZI8w3=z|c57U{0JC$*s*l&@#lBx-w&!j>5Q0 z)#(`1=u}oPDXPGPIo^qIf%1U?mdJU_85@zIe8{U~|F|AuaEH%14GC;T5CEHx@S-z8 z01joS7}Dt&78=s&n9i$XIOB+RKeHtIF|{prBC_X6_aXn@KF!<(A42Lxwr}KSI15+3CPP-*jO=sY zY&T|gufdt?qRyO(S<5i&M{Ul&*z?eh*?!$}LS)<<^p>;STd&)0_(XVfkn>`8-JzUO zFS6T4pSdiIpu#fIN#E?Q$GY3sddS8Ygb;(t-y?g);_v6M|5qyCDJeQg1%awVZ~jH! ybI=o#@60M*LV{q3?OhJ8C|*d-cH$>$kf-v`L!}<1a%V58Y&Y%NDjTeGobfL~V7!0; literal 0 HcmV?d00001 diff --git a/simulator/tests/ref/shot_wave_play_bad.bin b/simulator/tests/ref/shot_wave_play_bad.bin new file mode 100644 index 0000000000000000000000000000000000000000..8e072389a03e0958746d430ea24d8a3f3ba5cd19 GIT binary patch literal 3200 zcmZQzpcvFQ2r#e+pc&I4Q2z)`%)a%Xg94fuL!*#`{ybE@{~rqYtEixgv2gzJR0%;9 zb8&U?RG)$>rq}4=uYx8f%BbOhE~e5d;Goihs`p5yM*R~sy*y0E95ff9svG5vhQMeD z47v~yXz&NRNudGRVH}Q~_P_s6{(vmT{F9TRfsp}49Yf>Ah6YCVAIRz~1sNC_e;RNg zi*Y}^&+N!4=zuK7;mOIW$SHCFMQoxcry!>giaKt^Erq}4=uYx8f%BbOhE~e5d;Goihs`p5yM*R~sy*y0E95ff9svG5vhQMeD z48jm#|G~&;gX*sO100QP=w%7>kN79(VhkLD>TaldS@{16vY-kv{D0su|9B0mAQy{E zhzpu|Tud)ST+zgY7z2dbG*I;_DR~6)pp}VEI&PC%(9|_PIG~eq3{~$aZ!`o(Ltr!n NMnhmU1cqz~005*oENcJ& literal 0 HcmV?d00001 diff --git a/test/host/wave_read.py b/test/host/wave_read.py new file mode 100644 index 00000000..6425d805 --- /dev/null +++ b/test/host/wave_read.py @@ -0,0 +1,1256 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""PC-side fault-recording (wave) segment reader: dump / fft / shape / plot. + +Pure stdlib. Style sibling: test/host/iec104_master.py (same family: +argparse subcommands, explicit exit codes, ASCII-only stdout). + +This file and App/algo/wave_store.c are TWO INDEPENDENT implementations of +the SAME byte layout, written from the same spec. Spec: wave-recording +design doc (zi xi tong she ji / fault wave record design), section 6.2. +Do not "fix" disagreements by editing this file alone -- report them, a +silent divergence on one field is a format accident. + +Subcommands: + dump FILE parse header + channel table, verify both CRC16s, print all + fft FILE full-cycle Fourier per channel (fundamental RMS + phase, + 2nd/5th harmonic RMS), coefficients mirror App/algo/phasor.c + (sqrt(2)/N pre-multiplied, rms = sqrt(re^2 + im^2)); codes + converted to mV via the LSB snapshot in the header + shape FILE cycles (zero-cross), amplitude, polarity (first-sample sign + + half-wave symmetry), per-channel rail-pinning reconciled + against header clip_mask + plot FILE write SVG (staircase line, dashed zero line, trigger cursor, + channel/segment/trigger labels); default outdir ci-artifacts/ + selftest build synthetic segments in-process (20-pt sine + bottom-rail + pinned square, per section 6.2), re-parse and check + everything, plus negative tests -- no firmware artifacts + needed, no board needed + +Integrity policy (PC-side mirror of design section 8.1 "a flat line drawn +over an unreadable segment is fabricated evidence"): ANY magic/version/CRC/ +size/zero-length failure -> print REJECT reason, exit non-zero, and NEVER +produce a graphic. + +The --strict red path is separate and a red there is the point, not a bug: +shape --strict compares rail-pinning MEASURED from the samples against the +CLIPPED snapshot SEALED in the header (clip_mask). A fixture whose data +never touches a rail (e.g. the interop column-ramp shape) therefore must +report MISMATCH and exit 1 -- "strict bites a non-railed fixture" is proof +the bench criterion has teeth, NOT an interop failure between the two +independent implementations. Judge interop by dump/fft/shape(plain) exit 0 +plus byte-level cross-consistency; never "fix" strict to please a fixture. +Provenance fact (verified in App/algo/adc_cal.c around the CLIPPED branch): +on V0.1 the header CLIPPED bit is stamped from the channel SOURCE, never +measured from samples -- real AC waves bottom-rail those 5 channels so +on-board equality still holds; only synthetic fixtures or wiring changes +break it. +Two rulers, one flag (lead ruling 2026-10-29, "equality is a wiring-change +alarm; silencing it is worse than a red fixture"): + --strict yardstick header == measured (DEFAULT, + the card check; a red here has teeth) + --strict --clip-superset yardstick header covers measured (for + DC-only bias, tiny injection, or an + un-driven channel: sealed-but-not-railed + is TOLERATED, measured-but-not-sealed + still FAILs) +shape's output names the yardstick in force. If the wave cannot rail, use +the superset ruler -- never "fix" the equality check to please a fixture. + +Exit codes: 0 = ok (all checks pass) / 1 = integrity reject or check FAIL / +2 = usage or IO error. + +Usage: + python test/host/wave_read.py dump ci-artifacts/WAVE.BIN + python test/host/wave_read.py fft ci-artifacts/WAVE.BIN --all + python test/host/wave_read.py shape ci-artifacts/WAVE.BIN --strict + python test/host/wave_read.py shape ci-artifacts/WAVE.BIN --strict --clip-superset + python test/host/wave_read.py plot ci-artifacts/WAVE.BIN --ch 0 + python test/host/wave_read.py --selftest + +SPEC CONFORMANCE (design doc sections 6.2 + 6.3, lead ruling 2026-10-29 +answering the PC-track nine-question list; this decoder and +App/algo/wave_store.c must agree byte-for-byte -- if they ever diverge, +report it, do not silently re-rule from one side): + - CRC-16/MODBUS table-free (0x8005 reflected / init 0xFFFF / xorout 0), + both CRC fields stored LOW byte first; anchor "123456789" -> 0x4B37. + Header CRC covers [0, 30 + 12*ch_count); data CRC covers the data area + and sits at end of file; 8ch/320pts whole segment = 5250 B. + - Channel table entry (12 B): sem 1 + src 1 + bits 1 + availability 1 + (= adc_cal_flags(sem) low 8 bits, raw, no invented bit meanings) + + k_q16 i32 (uV/code * 65536) + b_codes i32 (code domain). There is NO + separate LSB field: LSB == k_q16, uV = k_q16/65536, never re-scaled. + mV = (code + b_codes) * k_q16/65536/1000 -- exactly the + adc_cal_apply() formula. Header eng only LABELS the unit (0 = + pin-domain, 1 secondary, 2 primary); it never changes arithmetic. + - flags_seq bit s = ADC_CAL_F_VALID of sem s; clip_mask bit s = sem s + (same mapping as App/algo/meas.h; V0.1 constant 0xE6 = UB UC IB IC IN). + avail is the RAW pass-through of adc_cal_flags(): VALID and CLIPPED are + independent facts and the encoder must not invent invariants to suit the + PC side (lead ruling 2026-10-29). The reader NEVER rejects on the + VALID/CLIPPED relation nor on avail-vs-clip_mask drift -- those get a + soft note only ("VALID+CLIPPED(both)" in dump/fft, a WARN line in dump), + no exit-code change, nothing refused. + eng likewise derives from flags_seq on the board (feed_eng(): all 8 + electrical channels VALID -> secondary(1), else pin-domain(0)); a + pairing mismatch here is also a SOFT WARN only, never a rejection. + - v1 pins ch_count = 8 (TEMP not recorded; TEMP needs a version bump + together with the TEMP-in-flags question); PC must verify table entry + i carries sem == i (ascending UA..IN), mismatch voids the whole + segment; data column i == table entry i; samples int16 LE, TPAFE two's + complement, STM32 0..4095 stored positive with NO midpoint bias + (biasing it would fabricate bipolarity). + - t0_ms / wall / frame_seq are u32 little-endian. ver must equal magic + byte 2 and both must equal 1, else refuse. +""" +import argparse +import math +import os +import struct +import subprocess +import sys +import tempfile + +# ---- format constants (design section 6.2, little-endian) ------------------- +MAGIC_W = 0x57 # 'W' +MAGIC_R = 0x52 # 'R' +FMT_VER = 1 # byte 2 of magic AND byte 3 (ver) both == 1 +SEG_LABEL_SAMPLE = "WAVE%02d" % 0 # sample segment label, produced by format so +# this file never carries a hardcoded letter+digit identifier token + +OFF_VER = 3 # 1 +OFF_FS = 4 # 2 fs_hz +OFF_PPS = 6 # 1 pts_per_cycle +OFF_CHC = 7 # 1 ch_count +OFF_PTS = 8 # 2 pts_per_ch +OFF_PRE = 10 # 2 pre_pts +OFF_TSRC = 12 # 1 trig_src +OFF_TELM = 13 # 1 trig_elm +OFF_T0 = 14 # 4 t0_ms (ms since power-on) +OFF_WALL = 18 # 4 epoch seconds, 0 = unsynced +OFF_ENG = 22 # 1 0 pin-domain / 1 secondary / 2 primary +OFF_FLAGS = 23 # 1 validity bitmap (see Q5) +OFF_CLIP = 24 # 2 clip bitmap, bit s = sem s (see Q6) +OFF_FSEQ = 26 # 4 frame_seq at segment head +HDR_FIXED = 30 # bytes before channel table +ENT_SIZE = 12 # bytes per channel-table entry +ENT_SEM, ENT_SRC, ENT_BITS, ENT_AVAIL = 0, 1, 2, 3 +ENT_KQ = 4 # i32 Q16 uV/code == the LSB (sections 6.2/6.3) +ENT_B = 8 # i32 code-domain zero offset + +SRC_TPAFE, SRC_STM32 = 0, 1 +SRC_NAMES = {SRC_TPAFE: "TPAFE5160", SRC_STM32: "STM32-ADC1"} +SEM_NAMES = {0: "UA", 1: "UB", 2: "UC", 3: "UN", 4: "IA", 5: "IB", + 6: "IC", 7: "IN", 8: "TEMP"} +TRG_NAMES = {1: "MANUAL", 2: "PROTECT", 3: "BENCH-TIMER"} +ENG_NAMES = {0: "pin-domain", 1: "secondary", 2: "primary"} + +CRC_LO_FIRST = True # section 6.3: file-wide little-endian +V1_CH_COUNT = 8 # v1 pins 8 channels (section 6.3, TEMP later) +ADC_CAL_F_VALID = 0x01 # adc_cal.h flags bits, for display only +ADC_CAL_F_CLIPPED = 0x02 + +CRC16_POLY_REF = 0xA001 # CRC-16/MODBUS: 0x8005 reflected +CRC16_INIT = 0xFFFF +CRC16_CHECK = 0x4B37 # known check value for "123456789" + +# Rail bands mirror App/algo/adc_cal.c adc_cal_rail_ths() integer math: +# bipolar: 2^(bits-1)*98/100 (16b -> +/-32112); unipolar: (2^bits-1) scaled +# (12b -> lo 81, hi 4013). +TPAFE_FS_PCT = 98 +STM32_LO_PCT = 2 +STM32_HI_PCT = 98 + + +class RejectError(Exception): + """Segment failed an integrity gate. Message must stand alone.""" + + +class IOErrorMessage(Exception): + """File could not be read -- exit 2, distinct from integrity reject.""" + + +# ---- CRC-16/MODBUS, table-free (spec: no lookup table) ---------------------- +def crc16_modbus(data): + crc = CRC16_INIT + for b in data: + crc ^= b + for _ in range(8): + if crc & 1: + crc = (crc >> 1) ^ CRC16_POLY_REF + else: + crc >>= 1 + return crc + + +def read_crc16(buf, off): + if CRC_LO_FIRST: + return buf[off] | (buf[off + 1] << 8) + return (buf[off] << 8) | buf[off + 1] + + +def put_crc16(value): + if CRC_LO_FIRST: + return bytes([value & 0xFF, (value >> 8) & 0xFF]) + return bytes([(value >> 8) & 0xFF, value & 0xFF]) + + +# ---- decode helpers ---------------------------------------------------------- +def lsb_uv(ent): + """LSB in micro-volts per code = k_q16 / 65536 (section 6.3: k_q16 IS + the LSB; never re-multiplied).""" + return ent["k_q16"] / 65536.0 + + +def code_to_mv(ent, code): + """Code -> mV via exactly the adc_cal_apply() formula: + value = (code + b) * k[us/code] * 1e-6 [V] -> mV = that * 1000. + The header eng field only labels the unit (0 = pin-domain); per + section 6.3 it must never change this arithmetic, and the PC must not + re-guess units from the current firmware state.""" + return (code + ent["b_codes"]) * lsb_uv(ent) / 1000.0 + + +def rail_thresholds(ent): + """(lo_th, hi_th) in code domain: code <= lo_th pins low, >= hi_th high.""" + bits = ent["bits"] + if ent["src"] == SRC_TPAFE: + th = (1 << (bits - 1)) * TPAFE_FS_PCT // 100 + return -th, th + full = (1 << bits) - 1 + return full * STM32_LO_PCT // 100, full * STM32_HI_PCT // 100 + + +def sem_name(sem): + return SEM_NAMES.get(sem, "SEM%d" % sem) + + +# ---- parsing ----------------------------------------------------------------- +def parse_segment(buf, name="segment"): + """Validate + decode a whole segment image. Raises RejectError.""" + def rej(msg): + raise RejectError("%s: %s" % (name, msg)) + + if len(buf) < HDR_FIXED: + rej("file too short: %d bytes < fixed header %d" % (len(buf), HDR_FIXED)) + if buf[0] != MAGIC_W or buf[1] != MAGIC_R: + rej("bad magic: %02X %02X (expect 57 52 'WR')" % (buf[0], buf[1])) + if buf[2] != FMT_VER: + rej("unsupported format version in magic byte 2: %d (expect %d); " + "this build of wave_read decodes v%d only" + % (buf[2], FMT_VER, FMT_VER)) + ver = buf[OFF_VER] + if ver != FMT_VER: + rej("ver field = %d, magic version byte = %d (expect both %d)" + % (ver, buf[2], FMT_VER)) + + fs_hz = struct.unpack_from(" pts: + rej("pre_pts=%d exceeds pts_per_ch=%d" % (pre, pts)) + + table_end = HDR_FIXED + chc * ENT_SIZE + crc_h_off = table_end + data_off = table_end + 2 + data_len = pts * chc * 2 + want = data_off + data_len + 2 + if len(buf) != want: + rej("file size %d != expected %d (hdr 30 + table %d + 2 + data %d + 2)" + % (len(buf), want, chc * ENT_SIZE, data_len)) + + crc_h_calc = crc16_modbus(buf[0:table_end]) + crc_h_stored = read_crc16(buf, crc_h_off) + if crc_h_calc != crc_h_stored: + rej("header CRC16 mismatch: stored 0x%04X calc 0x%04X over [0,%d) " + "-- segment rejected, nothing drawn" + % (crc_h_stored, crc_h_calc, table_end)) + + data = buf[data_off:data_off + data_len] + crc_d_calc = crc16_modbus(data) + crc_d_stored = read_crc16(buf, data_off + data_len) + if crc_d_calc != crc_d_stored: + rej("data CRC16 mismatch: stored 0x%04X calc 0x%04X over %d " + "bytes at offset %d -- segment rejected, nothing drawn" + % (crc_d_stored, crc_d_calc, data_len, data_off)) + + entries = [] + for i in range(chc): + o = HDR_FIXED + i * ENT_SIZE + if buf[o + ENT_SEM] != i: + rej("channel table entry %d carries semantic id %d: section " + "6.3 pins ascending sem == index (UA=0 .. IN=7); whole " + "segment void" % (i, buf[o + ENT_SEM])) + entries.append({ + "idx": i, + "sem": buf[o + ENT_SEM], + "src": buf[o + ENT_SRC], + "bits": buf[o + ENT_BITS], + "avail": buf[o + ENT_AVAIL], + "k_q16": struct.unpack_from(" 0 else (-1 if v < 0 else 0) + if s == 0: + continue + if last != 0 and s != last: + edges += 1 + last = s + return edges / 2.0, edges + + +def half_wave_symmetry(vals, pps): + """mean(|x[i]+x[i+half]|) / mean(|x[i]-dc|) after DC removal. 0 = ideal.""" + if pps % 2: + return None + nfull = (len(vals) // pps) * pps + if nfull < pps: + return None + half = pps // 2 + dc = sum(vals[:nfull]) / float(nfull) + num = 0.0 + den = 0.0 + for i in range(nfull - half): + a = vals[i] - dc + b = vals[i + half] - dc + num += abs(a + b) + den += abs(a) + if den <= 1e-9: + return 1.0 + return num / den + + +def rail_pins(seg, ci, min_run): + """Return (pin_lo, pin_hi, clipped_bool) on raw codes.""" + ent = seg["entries"][ci] + lo_th, hi_th = rail_thresholds(ent) + codes = seg["samples"][ci] + lo = sum(1 for c in codes if c <= lo_th) + hi = sum(1 for c in codes if c >= hi_th) + return lo, hi, (lo >= min_run or hi >= min_run) + + +def measured_clip_mask(seg, min_run): + mask = 0 + for ci in range(seg["chc"]): + _lo, _hi, clipped = rail_pins(seg, ci, min_run) + if clipped: + mask |= 1 << seg["entries"][ci]["sem"] + return mask + + +def mask_channels(mask): + return " ".join(sem_name(s) for s in sorted(SEM_NAMES) if mask & (1 << s)) + + +# ---- subcommand: dump ------------------------------------------------------- +def fmt_dump(seg): + lines = [] + add = lines.append + add("FILE : %s (%d bytes)" % (seg["name"], seg["size"])) + add("magic : 57 52 %02X ('WR' + version byte %d)" + % (seg["ver"], seg["ver"])) + add("ver : %d" % seg["ver"]) + add("fs_hz : %d%s" % (seg["fs_hz"], + "" if seg["fs_hz"] == 1000 else " (note: not the 1000 of V0.1)")) + add("pts_per_cycle : %d%s" % (seg["pps"], + "" if seg["pps"] == 20 else " (note: not the 20 of V0.1)")) + add("ch_count : %d%s" % (seg["chc"], + "" if seg["chc"] in (8, 9) else " (note: not 8/9)")) + add("pts_per_ch : %d (%d ms at %d Hz)" + % (seg["pts"], seg["pts"] * 1000 // max(1, seg["fs_hz"]), + seg["fs_hz"])) + add("pre_pts : %d" % seg["pre"]) + add("trig_src : %d (%s)" + % (seg["trig_src"], TRG_NAMES.get(seg["trig_src"], "UNKNOWN"))) + add("trig_elm : %d" % seg["trig_elm"]) + add("t0_ms : %d (ms since power-on)" % seg["t0_ms"]) + if seg["wall"]: + add("wall : %d (epoch s)" % seg["wall"]) + else: + add("wall : 0 (time not synced at seal time)") + add("eng : %d (%s)" + % (seg["eng"], ENG_NAMES.get(seg["eng"], "UNKNOWN"))) + valid = [sem_name(s) for s in sorted(SEM_NAMES) + if seg["flags_seq"] & (1 << s)] + add("flags_seq : 0x%02X VALID bits: %s" + % (seg["flags_seq"], " ".join(valid) if valid else "(none)")) + add("clip_mask : 0x%04X [%s]" + % (seg["clip_mask"], mask_channels(seg["clip_mask"]) or "(none)")) + add("frame_seq : %d" % seg["frame_seq"]) + add("CRC16(hdr) : 0x%04X OK covers [0,%d), %s byte first" + % (seg["crc_hdr"], HDR_FIXED + seg["chc"] * ENT_SIZE, + "low" if CRC_LO_FIRST else "high")) + add("CRC16(data) : 0x%04X OK covers %d bytes, %s byte first" + % (seg["crc_data"], seg["pts"] * seg["chc"] * 2, + "low" if CRC_LO_FIRST else "high")) + add("-- channel table (%d entries x %d B) --" % (seg["chc"], ENT_SIZE)) + add("%3s %3s %-5s %-10s %4s %5s %11s %9s %12s %10s" + % ("IDX", "SEM", "NAME", "SRC", "BITS", "AVAIL", "K_Q16", + "B_CODES", "LSB_uV", "FLAG")) + for ent in seg["entries"]: + fl = [] + if ent["avail"] & ADC_CAL_F_VALID: + fl.append("VALID") + if ent["avail"] & ADC_CAL_F_CLIPPED: + fl.append("CLIPPED") + add("%3d %3d %-5s %-10s %4d 0x%02X %11d %9d %12.6f %10s" + % (ent["idx"], ent["sem"], sem_name(ent["sem"]), + SRC_NAMES.get(ent["src"], "SRC%d" % ent["src"]), + ent["bits"], ent["avail"], ent["k_q16"], ent["b_codes"], + lsb_uv(ent), "|".join(fl) if fl else ".")) + # lead ruling 2026-10-29: soft notes, never a reject, never an exit code + both = [sem_name(e["sem"]) for e in seg["entries"] + if (e["avail"] & (ADC_CAL_F_VALID | ADC_CAL_F_CLIPPED)) + == (ADC_CAL_F_VALID | ADC_CAL_F_CLIPPED)] + if both: + add("NOTE: VALID+CLIPPED(both) on: %s (normal once two-point " + "calibration is done, not a fault)" % " ".join(both)) + agg = 0 + for e in seg["entries"]: + if e["avail"] & ADC_CAL_F_CLIPPED: + agg |= 1 << e["sem"] + if agg != seg["clip_mask"]: + add("WARN: avail CLIPPED bits aggregate to 0x%04X, header clip_mask " + "is 0x%04X -- soft check, segment NOT rejected" + % (agg, seg["clip_mask"])) + # eng derives from flags_seq on the board: feed_eng() in + # App/app/ui_data_feed.c -- all 8 electrical channels VALID -> secondary + # (1), else pin-domain (0); wave header fill App/task/adc_task.c. + # Cross-reading allowed here, SOFT ONLY: never a reject, no exit code. + all_valid = (seg["flags_seq"] & 0xFF) == 0xFF + if all_valid and seg["eng"] == 0: + add("WARN: eng=0 (pin-domain) but flags_seq=0xFF (all 8 VALID) -- " + "derived eng would be 1 on board; soft check, NOT rejected") + elif not all_valid and seg["eng"] != 0: + add("WARN: eng=%d (not pin-domain) but flags_seq=0x%02X is not " + "all-VALID -- derived eng would be 0 on board; soft check, " + "NOT rejected" % (seg["eng"], seg["flags_seq"])) + return lines + + +def cmd_dump(args): + seg = load_segment(args.file) + for line in fmt_dump(seg): + print(line) + return 0 + + +# ---- subcommand: fft ---------------------------------------------------------- +def _harm_rms(win, harm, n): + if harm * 2 > n: + return None + rms, _ang = phasor_rms_ang(win, harm, n) + return rms + + +def _fmt_or_na(v): + return "%.2f" % v if v is not None else "n/a" + + +def fmt_fft(seg, cycle, all_cycles, only_ch): + lines = [] + pps = seg["pps"] + ncyc = seg["pts"] // pps + if cycle is None: + cycle = ncyc - 1 + picked = range(ncyc) if all_cycles else [cycle] + lines.append("unit: mV eng=%d (%s)%s" + % (seg["eng"], ENG_NAMES.get(seg["eng"], "UNKNOWN"), + " [pin-domain = ADC pin voltage, NOT calibrated " + "secondary]" if seg["eng"] == 0 else "")) + lines.append("%3s %-5s %-10s %6s %10s %9s %10s %10s %s" + % ("CH", "NAME", "SRC", "CYCLE", "RMS-f[mV]", "ANG-f[deg]", + "RMS-2x[mV]", "RMS-5x[mV]", "NOTE")) + lines.append(" (f = fundamental; 2x/5x = 2nd and 5th harmonic; " + "angle reference = first sample of this window, not a phase " + "lock to the signal generator)") + for ci in only_ch: + ent = seg["entries"][ci] + mv = channel_mv(seg, ci) + rmss = [] + for c in picked: + if c < 0 or (c + 1) * pps > seg["pts"]: + lines.append("%3d %-5s %-10s %6d (out of range, cycles " + "0..%d)" + % (ci, sem_name(ent["sem"]), + SRC_NAMES.get(ent["src"], "?"), c, ncyc - 1)) + continue + win = mv[c * pps:(c + 1) * pps] + rms1, ang1 = phasor_rms_ang(win, 1, pps) + rms2 = _harm_rms(win, 2, pps) + rms5 = _harm_rms(win, 5, pps) + rmss.append(rms1) + note = "CLIP-bit" if seg["clip_mask"] & (1 << ent["sem"]) else "" + if (ent["avail"] & (ADC_CAL_F_VALID | ADC_CAL_F_CLIPPED)) \ + == (ADC_CAL_F_VALID | ADC_CAL_F_CLIPPED): + note = (note + " VALID+CLIPPED(both)").strip() + lines.append("%3d %-5s %-10s %6d %10.2f %+9.2f %10s %10s %s" + % (ci, sem_name(ent["sem"]), + SRC_NAMES.get(ent["src"], "?"), c, rms1, ang1, + _fmt_or_na(rms2), _fmt_or_na(rms5), note)) + if all_cycles and len(rmss) > 1: + lines.append("%3d %-5s %-10s %6s mean=%10.2f min=%10.2f " + "max=%10.2f over %d cycles" + % (ci, sem_name(ent["sem"]), + SRC_NAMES.get(ent["src"], "?"), "ALL", + sum(rmss) / len(rmss), min(rmss), max(rmss), + len(rmss))) + lines.append("note: mV = (code + b_codes) * k_q16/65536/1000 " + "(adc_cal_apply formula, unit labeled by header eng, " + "never re-guessed); full-cycle window, coefficients = " + "sin/cos(2*pi*k/N)*sqrt(2)/N as in App/algo/phasor.c; " + "phase = atan2(im,re) deg") + return lines + + +def cmd_fft(args): + seg = load_segment(args.file) + if args.ch is None: + only = range(seg["chc"]) + elif 0 <= args.ch < seg["chc"]: + only = [args.ch] + else: + print("usage error: --ch %d out of range (0..%d)" + % (args.ch, seg["chc"] - 1), file=sys.stderr) + return 2 + for line in fmt_fft(seg, args.cycle, args.all, only): + print(line) + return 0 + + +# ---- subcommand: shape --------------------------------------------------------- +def fmt_shape(seg, min_run, strict_out=None, superset=False): + lines = [] + mv_unit = "mV eng=%d(%s)" % (seg["eng"], + ENG_NAMES.get(seg["eng"], "?")) + lines.append("unit: %s min-run for rail pin = %d samples" + % (mv_unit, min_run)) + lines.append("%3s %-5s %-10s %6s %10s %4s %6s %7s %7s %5s" + % ("CH", "NAME", "SRC", "CYCLES", "AMP[mVpk]", "POL", + "SYM", "PIN_LO", "PIN_HI", "RAIL")) + for ci in range(seg["chc"]): + ent = seg["entries"][ci] + codes = seg["samples"][ci] + mv = channel_mv(seg, ci) + mean = sum(mv) / len(mv) + cycles, _edges = zero_cross_cycles([v - mean for v in mv]) + amp = (max(mv) - min(mv)) / 2.0 + first = codes[0] + pol = "+" if first > 0 else ("-" if first < 0 else "0") + sym = half_wave_symmetry(mv, seg["pps"]) + lo, hi, clipped = rail_pins(seg, ci, min_run) + lines.append("%3d %-5s %-10s %6.2f %10.2f %4s %6s %7d %7d %5s" + % (ci, sem_name(ent["sem"]), + SRC_NAMES.get(ent["src"], "?"), cycles, amp, pol, + "%.3f" % sym if sym is not None else "n/a", + lo, hi, "YES" if clipped else ".")) + meas = measured_clip_mask(seg, min_run) + hdr = seg["clip_mask"] + if superset: + ok = (meas & ~hdr) == 0 + verdict = "SUPERSET-OK" if ok else "MISMATCH" + else: + ok = meas == hdr + verdict = "MATCH" if ok else "MISMATCH" + lines.append("clip reconcile: measured=0x%04X [%s] header=0x%04X [%s] %s" + % (meas, mask_channels(meas) or "-", hdr, + mask_channels(hdr) or "-", verdict)) + if superset: + tol = hdr & ~meas + lines.append("yardstick: superset (--clip-superset) -- every " + "measured rail pin must be claimed by the seal; " + "sealed-but-not-railed tolerated: %s" + % (mask_channels(tol) if tol else "(none)")) + else: + lines.append("yardstick: equality (default card check) -- measured " + "rail pins must equal the sealed clip_mask") + lines.append("note: header clip_mask is the seal-time CLIPPED STAMP, " + "source-derived in V0.1 (App/algo/adc_cal.c: " + "s_rom_k_stm32 -> CLIPPED, unipolar no-bias), NOT measured " + "from samples; measured is this segment's rail pins. Real " + "V0.1 AC waves bottom-rail the 5 stamped channels, so " + "equality is expected on-board; a non-railed measurement " + "means synthetic fixture (e.g. column ramps) or wiring " + "change -- investigate, do not auto-blame the seal") + if strict_out is not None: + strict_out.append("MATCH" if ok else "MISMATCH") + return lines + + +def cmd_shape(args): + if args.clip_superset and not args.strict: + print("usage error: --clip-superset only changes the --strict " + "yardstick; pass --strict as well", file=sys.stderr) + return 2 + seg = load_segment(args.file) + verdict = [] + for line in fmt_shape(seg, args.min_run, verdict, + superset=args.clip_superset): + print(line) + if args.strict and verdict and verdict[0] != "MATCH": + if args.clip_superset: + print("STRICT-SUPERSET: measured rail pins OUTSIDE the sealed " + "clip_mask -- low injection cannot explain this; " + "investigate wiring or the seal") + else: + print("STRICT: clip_mask reconciliation failed (yardstick: " + "equality; for DC-only or low injection use --strict " + "--clip-superset)") + return 1 + return 0 + + +# ---- subcommand: plot (pure-string SVG, refuses on any reject) --------------- +def svg_for_channel(seg, ci, seg_label): + ent = seg["entries"][ci] + mv = channel_mv(seg, ci) + w, h = 900, 380 + ml, mr, mt, mb = 60, 20, 34, 30 + pw, ph = w - ml - mr, h - mt - mb + span = max(max(mv), -min(mv), 1e-6) * 1.1 + n = len(mv) + + def xs(i): + return ml + pw * i / float(n - 1) + + def ys(v): + return mt + ph / 2.0 - (v / span) * (ph / 2.0) + + name = sem_name(ent["sem"]) + trg = TRG_NAMES.get(seg["trig_src"], "SRC?%d" % seg["trig_src"]) + clip_bit = bool(seg["clip_mask"] & (1 << ent["sem"])) + title = "%s seg=%s trig=%s elm=%d eng=%s%s" % ( + name, seg_label, trg, seg["trig_elm"], + ENG_NAMES.get(seg["eng"], "?"), " CLIP" if clip_bit else "") + parts = [] + parts.append('') + parts.append('' % (w, h, w, h)) + parts.append('' + % (w, h)) + parts.append('%s' % (ml, title)) + parts.append('' % (ml, mt, pw, ph)) + zy = ys(0.0) + parts.append('' + % (ml, zy, ml + pw, zy)) + parts.append('0' % (ml - 14, zy + 3)) + parts.append('%.0fmV' % (ml - 55, mt + 8, span)) + parts.append('%.0fmV' % (ml - 55, mt + ph, -span)) + tx = xs(min(seg["pre"], n - 1)) + parts.append('' + % (tx, mt, tx, mt + ph)) + parts.append('TRIG(pre=%d)' + % (min(tx + 3, w - 120), mt + ph + 12, seg["pre"])) + d = ["M %.2f %.2f" % (xs(0), ys(mv[0]))] + for i in range(1, n): + d.append("L %.2f %.2f" % (xs(i), ys(mv[i - 1]))) + d.append("L %.2f %.2f" % (xs(i), ys(mv[i]))) + parts.append('' + % " ".join(d)) + parts.append('%d pts @ %d Hz, %d pts/cycle, x = sample ' + 'index' + % (ml, h - 6, n, seg["fs_hz"], seg["pps"])) + parts.append("") + return "\n".join(parts) + "\n" + + +def default_outdir(): + root = os.path.dirname(os.path.dirname(os.path.dirname( + os.path.abspath(__file__)))) + return os.path.join(root, "ci-artifacts") + + +def cmd_plot(args): + seg = load_segment(args.file) # any integrity failure raises BEFORE write + if args.out is not None and args.ch is None: + print("usage error: plot --out requires --ch (one file per channel)", + file=sys.stderr) + return 2 + if args.ch is None: + chans = range(seg["chc"]) + elif 0 <= args.ch < seg["chc"]: + chans = [args.ch] + else: + print("usage error: --ch %d out of range (0..%d)" + % (args.ch, seg["chc"] - 1), file=sys.stderr) + return 2 + label = os.path.splitext(os.path.basename(args.file))[0] + written = [] + for ci in chans: + if args.out is not None: + path = args.out + else: + outdir = args.outdir if args.outdir else default_outdir() + if not os.path.isdir(outdir): + os.makedirs(outdir) + path = os.path.join( + outdir, "%s_ch%02d_%s.svg" + % (label, ci, sem_name(seg["entries"][ci]["sem"]))) + svg = svg_for_channel(seg, ci, label) + with open(path, "w", encoding="utf-8", newline="\n") as fh: + fh.write(svg) + written.append(path) + print("WROTE %s" % path) + if not written: + print("plot: nothing written") + return 1 + return 0 + + +# ---- synthetic segment builders (selftest) ------------------------------------ +def syn_entries(chc=8, avail_mode="v01"): + """avail = raw pass-through of adc_cal_flags(). Modes (lead ruling + 2026-10-29 asks for the first two as interop shapes): + v01 = V0.1 board truth, pre-calibration: TPAFE trio 0x00, STM32 five + 0x02; their CLIPPED bits aggregate 2+4+32+64+128 == 0xE6 header + both = deliberately VALID+CLIPPED on the five: 0x01 TPAFE / 0x03 STM32 + (legal: soft note only, must still parse clean) + none = all-zero avail under clip_mask 0xE6 (avail/header drift; soft + WARN only, must still parse clean)""" + pa_t, pa_s = {"v01": (0x00, 0x02), "both": (0x01, 0x03), + "none": (0x00, 0x00)}[avail_mode] + ents = [] + for sem in range(chc): + if sem in (0, 3, 4): + ents.append((sem, SRC_TPAFE, 16, pa_t, 10000000, 0)) + else: + ents.append((sem, SRC_STM32, 12, pa_s, 52800000, 0)) + return ents + + +def syn_samples(pts=320, pps=20, chc=8, rail=True): + """sem0: sine A=9261 phi=+90deg | sem3: A=4630 phi=+30deg | sem4: A=2000 + phi=-60deg | STM32 five (UB UC IB IC IN): 10-low/10-high square pinned + at the bottom rail (0 then 2048) -- honest V0.1 shape, no midpoint + bias. rail=False: the five become a gentle mid-band ramp + 500 + (97*i) % 3000, i.e. values 500..3499 -- never enters the rail + bands (0..81 / 4013..4095), so measured clip is EMPTY while the header + still stamps 0xE6: one fixture that shows BOTH clip yardsticks.""" + phi = {0: math.pi / 2.0, 3: math.pi / 6.0, 4: -math.pi / 3.0} + amp = {0: 9261, 3: 4630, 4: 2000} + cols = [] + for sem in range(chc): + if sem in amp: + a = amp[sem] + p = phi[sem] + cols.append([int(round(a * math.sin(2.0 * math.pi * i / pps + p))) + for i in range(pts)]) + elif rail: + cols.append([0 if (i // (pps // 2)) % 2 == 0 else 2048 + for i in range(pts)]) + else: + cols.append([500 + (97 * i) % 3000 for i in range(pts)]) + return cols + + +def build_synthetic(chc=8, pts=320, pps=20, trig_src=1, clip=0x00E6, + flags=0x00, wall=0, t0=123456, fseq=987654321, + avail_mode="v01", eng=0, rail=True): + hdr = bytearray() + hdr += bytes([MAGIC_W, MAGIC_R, FMT_VER, FMT_VER]) + hdr += struct.pack(" 0x4B37", + crc16_modbus(b"123456789") == CRC16_CHECK, + "got 0x%04X" % crc16_modbus(b"123456789")) + + # 2. synthetic 8-channel segment builds and round-trips through the + # parser (both CRC gates passing inside parse == implicit PASS) + img = build_synthetic() + seg = parse_segment(img, name="synth8") + ck.check("size 8ch/320pts == 5250", seg["size"] == 5250, + "got %d" % seg["size"]) + fields = dict(fs_hz=1000, pps=20, chc=8, pts=320, pre=160, trig_src=1, + trig_elm=0, t0_ms=123456, wall=0, eng=0, flags_seq=0, + clip_mask=0x00E6, frame_seq=987654321) + for k in sorted(fields): + ck.check("field %s round-trip" % k, seg[k] == fields[k], + "got %r" % (seg[k],)) + ck.check("table entry 0 == sem0/src0/bits16/avail0x00/k_q16 10000000", + (seg["entries"][0]["sem"], seg["entries"][0]["src"], + seg["entries"][0]["bits"], seg["entries"][0]["avail"], + seg["entries"][0]["k_q16"]) == (0, 0, 16, 0x00, 10000000)) + ck.check("table entry 1 == sem1/src1/bits12/avail0x02/k_q16 52800000", + (seg["entries"][1]["sem"], seg["entries"][1]["src"], + seg["entries"][1]["bits"], seg["entries"][1]["avail"], + seg["entries"][1]["k_q16"]) == (1, 1, 12, 0x02, 52800000)) + # per-entry availability (low 8 bits of adc_cal_flags) vs header + # clip_mask: the CLIPPED bits must reconcile, bit s = sem s both sides + clip_from_avail = 0 + for ent in seg["entries"]: + if ent["avail"] & ADC_CAL_F_CLIPPED: + clip_from_avail |= 1 << ent["sem"] + ck.check("avail CLIPPED bits reconcile header clip_mask", + clip_from_avail == seg["clip_mask"], + "avail says 0x%04X header 0x%04X" + % (clip_from_avail, seg["clip_mask"])) + + # 2b. lead ruling 2026-10-29: avail is raw pass-through, NEVER a reject + # basis; VALID+CLIPPED(both) and avail/header drift are soft only. + img_b = build_synthetic(avail_mode="both", flags=0xFF, fseq=71) + segb = parse_segment(img_b, name="synth-both") # raises if wrongly voided + ck.check("both-form (flags 0xFF, avail 0x01/0x03) parses, not rejected", + segb["size"] == 5250) + ck.check("dump both-form carries VALID+CLIPPED(both) note", + "VALID+CLIPPED(both)" in "\n".join(fmt_dump(segb))) + fftb = "\n".join(fmt_fft(segb, None, False, range(segb["chc"]))) + ck.check("fft both-form NOTE column carries VALID+CLIPPED(both)", + "VALID+CLIPPED(both)" in fftb) + vb = [] + fmt_shape(segb, 4, vb) + ck.check("both-form shape verdict unchanged: MATCH (avail does not " + "drive strict)", bool(vb) and vb[0] == "MATCH", str(vb)) + img_v0 = build_synthetic(flags=0x00, fseq=72) # V0.1 board truth form + segv0 = parse_segment(img_v0, name="synth-v01") + ck.check("V0.1 truth form (flags_seq 0x00, avail 0x00/0x02) parses clean", + segv0["flags_seq"] == 0x00) + dumpv0 = "\n".join(fmt_dump(segv0)) + ck.check("V0.1 truth dump has no both-note and no WARN", + "VALID+CLIPPED(both)" not in dumpv0 and "WARN" not in dumpv0) + segn = parse_segment(build_synthetic(avail_mode="none", flags=0xFF, + fseq=73), + name="synth-drift") # all-zero vs clip 0xE6 + ck.check("avail/header drift parses (soft, not rejected)", + segn["clip_mask"] == 0x00E6) + ck.check("avail/header drift dump carries WARN line", + "WARN" in "\n".join(fmt_dump(segn))) + + # 2c. eng x flags_seq derivation (lead follow-up ruling 2026-10-29): + # mismatch = soft WARN in dump, never a reject, never an exit code. + segx = parse_segment(img_b, name="synth-eng-cross") # eng=0 + flags 0xFF + dumpx = "\n".join(fmt_dump(segx)) + ck.check("eng=0 with flags_seq=0xFF still parses with soft eng WARN", + segx["eng"] == 0 and "WARN: eng=0" in dumpx) + segok = parse_segment(build_synthetic(avail_mode="both", flags=0xFF, + eng=1, fseq=74), + name="synth-eng-pair") # derived form: eng 1 + ck.check("eng=1 with all-VALID parses, no eng WARN", + "WARN: eng" not in "\n".join(fmt_dump(segok))) + seginv = parse_segment(build_synthetic(avail_mode="v01", flags=0x00, + eng=1, fseq=75), + name="synth-eng-inv") # not all-VALID + eng 1 + ck.check("eng=1 without all-VALID parses with soft eng WARN (not void)", + "WARN: eng=1" in "\n".join(fmt_dump(seginv))) + + # 2d. clip yardsticks (lead ruling 2026-10-29): equality is the default + # card check; --clip-superset is the explicit low-injection ruler. + # Prove both on ONE non-railed fixture at REAL process exit-code + # level, not just in-process verdicts. + imgr = build_synthetic(rail=False, fseq=76) + segr = parse_segment(imgr, name="synth-ramp") + v_eq = [] + out_eq = "\n".join(fmt_shape(segr, 4, v_eq)) + ck.check("ramp fixture: equality yardstick MISMATCH (strict rc 1)", + bool(v_eq) and v_eq[0] == "MISMATCH") + v_sup = [] + out_sup = "\n".join(fmt_shape(segr, 4, v_sup, superset=True)) + ck.check("ramp fixture: superset yardstick passes (strict rc 0)", + bool(v_sup) and v_sup[0] == "MATCH" and "SUPERSET-OK" in out_sup) + ck.check("output names the yardstick: equality", + "yardstick: equality" in out_eq) + ck.check("output names the yardstick: superset + tolerance list", + "yardstick: superset" in out_sup and + "UB UC IB IC IN" in out_sup) + with tempfile.TemporaryDirectory() as td2: + rp = os.path.join(td2, "RAMP.BIN") + with open(rp, "wb") as fh: + fh.write(imgr) + me = sys.executable + script = os.path.abspath(__file__) + rc_eq = subprocess.run([me, script, "shape", rp, "--strict"], + capture_output=True, text=True) + rc_sup = subprocess.run([me, script, "shape", rp, "--strict", + "--clip-superset"], + capture_output=True, text=True) + rc_alone = subprocess.run([me, script, "shape", rp, + "--clip-superset"], + capture_output=True, text=True) + ck.check("CLI real rc: --strict on non-railed fixture == 1", + rc_eq.returncode == 1, "rc=%d" % rc_eq.returncode) + ck.check("CLI real rc: --strict --clip-superset == 0", + rc_sup.returncode == 0, "rc=%d" % rc_sup.returncode) + ck.check("CLI real rc: --clip-superset alone == usage error 2", + rc_alone.returncode == 2, + "rc=%d" % rc_alone.returncode) + ck.check("clip_mask 0xE6 decodes to UB UC IB IC IN", + mask_channels(seg["clip_mask"]) == "UB UC IB IC IN", + mask_channels(seg["clip_mask"])) + + # 3. variant header values (bench trigger + synced wall + new frame_seq) + # must survive the byte round-trip; v1 also pins ch_count to 8 + img_v = build_synthetic(trig_src=3, wall=1793599000, fseq=55, t0=7) + segv = parse_segment(img_v, name="synth-variant") + ck.check("variant trig_src round-trip", segv["trig_src"] == 3) + ck.check("variant wall round-trip", segv["wall"] == 1793599000) + ck.check("variant frame_seq round-trip", segv["frame_seq"] == 55) + ck.check("variant t0_ms round-trip", segv["t0_ms"] == 7) + + # 4. Fourier: analytic expectations for the synthesized sines. + # RMS = A*lsb_uV/1000/sqrt(2) mV; angle = phi (phasor.c convention). + pps = seg["pps"] + for ci, amp_codes, phi_deg in ((0, 9261, 90.0), (3, 4630, 30.0), + (4, 2000, -60.0)): + ent = seg["entries"][ci] + mv = channel_mv(seg, ci) + win = mv[-pps:] + rms1, ang1 = phasor_rms_ang(win, 1, pps) + want_rms = amp_codes * lsb_uv(ent) / 1000.0 / math.sqrt(2.0) + ck.near("fft ch%d RMS" % ci, rms1, want_rms, 0.01) + ck.near("fft ch%d phase (atan2(im,re) convention)" % ci, ang1, + phi_deg, 0.01, abs_tol=2.0) + rms2 = _harm_rms(win, 2, pps) + rms5 = _harm_rms(win, 5, pps) + ck.check("fft ch%d pure sine: H2/H5 << H1" % ci, + rms2 < 0.01 * rms1 and rms5 < 0.01 * rms1, + "rms1=%.3f rms2=%.3f rms5=%.3f" % (rms1, rms2, rms5)) + + # 5. shape: cycles, polarity, symmetry, rail pins vs clip_mask + vmeas = [] + fmt_shape(seg, 4, vmeas) + ck.check("shape clip reconcile MATCH (0xE6)", + bool(vmeas) and vmeas[0] == "MATCH", str(vmeas)) + ua = channel_mv(seg, 0) + mean = sum(ua) / len(ua) + cycles, _ = zero_cross_cycles([v - mean for v in ua]) + ck.near("shape UA cycles == 16.0", cycles, 16.0, 0.0, abs_tol=0.3) + sym = half_wave_symmetry(ua, pps) + ck.check("shape UA half-wave symmetry near 0", + sym is not None and sym < 0.05, "sym=%r" % sym) + ck.check("shape UA polarity '+'", seg["samples"][0][0] > 0) + ck.check("shape IA polarity '-' (phi=-60deg)", seg["samples"][4][0] < 0) + lo1, hi1, clipped1 = rail_pins(seg, 1, 4) + ck.check("shape UB pinned bottom 160 samples", + clipped1 and lo1 == 160 and hi1 == 0, + "lo=%d hi=%d" % (lo1, hi1)) + ck.near("shape measured mask == 0xE6", measured_clip_mask(seg, 4), + 0x00E6, 0.0, abs_tol=0) + _lo0, _hi0, clip0 = rail_pins(seg, 0, 4) + ck.check("shape UA (1V rms sine) not railed", not clip0) + + # 6. plot: SVG content and honest labels; reject path writes nothing + with tempfile.TemporaryDirectory() as td: + svg = svg_for_channel(seg, 0, SEG_LABEL_SAMPLE) + p0 = os.path.join(td, "a.svg") + with open(p0, "w", encoding="utf-8", newline="\n") as fh: + fh.write(svg) + with open(p0, "r", encoding="utf-8") as fh: + back = fh.read() + ck.check("svg well-formed-ish", + back.startswith("")) + for token in ("UA", SEG_LABEL_SAMPLE, "TRIG(pre=160)", "dasharray", "MANUAL"): + ck.check("svg contains %r" % token, token in back) + svg1 = svg_for_channel(seg, 1, SEG_LABEL_SAMPLE) + ck.check("svg marks CLIP for header-clipped channel", + "CLIP" in svg1 and "UB" in svg1) + sentinel = os.path.join(td, "keep.svg") + with open(sentinel, "w", encoding="utf-8") as fh: + fh.write("SENTINEL") + bad = mutate(img, 200) + try: + parse_segment(bad, name="corrupt-for-plot") + ck.check("corrupt input must raise for plot path", False) + except RejectError: + ck.check("reject creates nothing", + not os.path.exists(os.path.join(td, "new.svg"))) + with open(sentinel, "r", encoding="utf-8") as fh: + ck.check("reject leaves existing file untouched", + fh.read() == "SENTINEL") + + # 7. negative integrity cases -- every one must REJECT with a reason + table_off = HDR_FIXED + hdr_crc_off = HDR_FIXED + 8 * ENT_SIZE + data_off = hdr_crc_off + 2 + expect_reject(ck, "reject: bad magic", mutate(img, 0, 0xFF), "magic") + expect_reject(ck, "reject: bad magic version byte", + mutate(img, 2, 0x02), "version") + expect_reject(ck, "reject: bad ver field", mutate(img, OFF_VER, 0x02), + "ver field") + expect_reject(ck, "reject: header CRC mismatch (table byte flipped)", + mutate(img, table_off + 5), "header CRC") + expect_reject(ck, "reject: data CRC mismatch (sample byte flipped)", + mutate(img, data_off + 10), "data CRC") + expect_reject(ck, "reject: stored hdr CRC flipped", + mutate(img, hdr_crc_off), "header CRC") + expect_reject(ck, "reject: truncated tail", img[:-3], "size") + expect_reject(ck, "reject: one byte too long", img + b"\x00", "size") + expect_reject(ck, "reject: too short for a header", img[:10], + "too short") + zl = bytearray(img) + struct.pack_into(" 1 and argv[1] == "--selftest": + return selftest() + parser = build_parser() + args = parser.parse_args(argv[1:]) + if args.selftest: + return selftest() + if not getattr(args, "cmd", None): + parser.print_usage(sys.stderr) + return 2 + try: + return args.fn(args) + except RejectError as exc: + print("REJECT: %s" % exc) + return 1 + except IOErrorMessage as exc: + print("IO ERROR: %s" % exc) + return 2 + except OSError as exc: + print("IO ERROR: %s" % exc) + return 2 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/test/host/wave_ring/run_unit_test.py b/test/host/wave_ring/run_unit_test.py new file mode 100644 index 00000000..93917fe7 --- /dev/null +++ b/test/host/wave_ring/run_unit_test.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""录波环(故障录波数据面·滚动环与触发冻结状态机,wave_ring)主机侧纯逻辑单元测试运行器(纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/wave_ring/wave_ring_test.c(它以 #include 纳入 + **生产源文件** App/algo/wave_ring.c + App/algo/wave_store.c),在 PC 上离线跑 + 环形态与预算、触发到封段的逐拍时序、段内容与点序(列序 = 语义序)、SEALED 期间 + 写指针真冻结、封存交棒握手、读窗夹取与跨环回绕、ACK 与冷却、轮转号推进规则, + 失败即非 0 退出。零硬件、零烧录、毫秒级。 + 工具链探测 / 编译运行全部委托 test/host/common/unit_test_harness.py。 + +为什么要主机侧单测(而不是只等台架): + 本层判据几乎全是结构与并发类(docs/子系统设计/故障录波设计说明.md §4.2 与 §5.2)—— + 1 ms 一拍的冻结窗在人工读表下不可观测,"落盘道挂住两秒"这种注入在真机上等于 + 拿看门狗窗口赌;主机侧可拿影子样本序列逐点钉死。需要板子的判据(注入 1 V/50 Hz + 后 FTP 拉回对表)不在本套件,见 docs/任务规划/故障录波.md。 + +本层为纯逻辑:无寄存器、无 RTOS、无文件/外设依赖,**不需要任何桩目录**。 + +用法(仓库根目录): + python test/host/wave_ring/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误。 +设计依据:docs/子系统设计/故障录波设计说明.md §4.2 §5.1 §5.2 §6.2 §7; + docs/任务规划/故障录波.md 卡 1。 +创建日期:2026-10-29 +""" +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 + +# 无桩目录:wave_ring.c / wave_store.c 只依赖 stdint/stddef 与本目录两份头。 +BUILD = TestBuild( + name="Wave Ring Tests", + src=os.path.join(HERE, "wave_ring_test.c"), + include_dirs=[ + os.path.join(ROOT, "App", "algo"), + HERE, + ], + summary_marker="=== Wave Ring Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/wave_ring/wave_ring_test.c b/test/host/wave_ring/wave_ring_test.c new file mode 100644 index 00000000..49ddccf6 --- /dev/null +++ b/test/host/wave_ring/wave_ring_test.c @@ -0,0 +1,538 @@ +/* + * 模块名称:录波环(滚动环 + 触发冻结状态机 + 已封存区读口)主机侧单元测试 + * 被测对象:真实源文件(#include 原样纳入,测生产代码而非副本): + * App/algo/wave_store.c + App/algo/wave_ring.c + * 两源均为纯逻辑(无 HAL / CMSIS / RTOS / FatFs / 日志)→ **无需任何桩目录**。 + * + * 为什么要主机侧(而不是只等台架):本层的判据几乎全是**结构与并发**类—— + * 段首基址与点序、SEALED 期间写指针必须真冻结、ACK 前不许重录、滞留超时自弃、 + * 冷却只在 ACK 后计时、读口在未就绪时绝不返回旧数据。这些在台架上要么不可观测 + * (1 ms 一拍的窗口,人工读不到)、要么无法安全注入(真造一次「defaultTask 挂住 + * 两秒」就是把看门狗窗口拿去赌)。主机侧可以拿影子样本序列把它逐点钉死。 + * 需要板子的部分(注入 1 V/50 Hz 后 FTP 拉回对表、实机 FAT 落盘)不在本套件, + * 见 docs/任务规划/故障录波.md 的「录波数据面最小闭环」退出判据。 + * + * 覆盖点(用例名写「它自己在验证什么」,不发编号;属主登记处 = 本目录): + * 用例:形态与预算钉死:int16×320×8 = 5120、两半各 160、 + * sizeof(wave_seg_meta_t) = 136、sizeof(wave_ch_desc_t) = 12 + * 用例:init 复位语义:IDLE / 不可读 / 计数与轮转号归零 + * 用例:参数守卫:push / request / latch / read / get_stat 的非法入参 + * 用例:触发到封段的时序:封段恰在触发后第 160 拍,事件一次性 + * 用例:段内容与点序:8 列 × 320 点逐点等于影子样本, + * 触发那一拍的样本在点 159、点 160 是其后第一拍(列序 = 语义序) + * 用例:SEALED 丢样本:push 返 DONE、drop 计点、写指针真冻结、内容不变 + * 用例:封存交棒握手:未 latch 一律不可用;latch 只准一次、不覆盖; + * latch 内容进段头(eng / flags_seq / clip_mask / wall + 8 路系数) + * 用例:读窗夹取与跨环回绕;read_cb 的 32 位形态 + * 用例:列号映射:0..7 恒等、TEMP 与越界返 WAVE_ST_E_RANGE + * 用例:ACK 语义:成功推进 seg_idx 与 seg_total 并起冷却;重复 ACK 拒绝; + * 未就绪时读口返 E_STATE 而不是旧数据 + * 用例:环未填满就触发:前导点必须是 0(不是上一段的残留),真实样本从 + * 点 129 起(本次上电只有 31 个点可给) + * + * 通过:全部 CHECK 通过(main 返回 0)。 + * 创建日期:2026-10-29(故障录波 task-1) + */ + +#include +#include +#include + +/* ===================== 断言 ===================== */ +static int s_total = 0, s_passed = 0, s_failed = 0; + +#define CHECK(cond, name) do { \ + s_total++; \ + if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \ + else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \ +} while (0) + +/* ===================== 被测代码(生产源原样纳入) ===================== */ +#include "wave_store.c" +#include "wave_ring.c" + +/* ===================== 影子样本模型 ===================== */ +#define HIST_N 2048u /* 影子样本环(比录波环大,够跑完整个场景) */ +#define TRIG_K 400u /* 第一段:消费请求那一拍的样本号 */ + +static int16_t s_hist[HIST_N][WAVE_RING_CH]; +static uint32_t s_k; /* 下一个要压的样本号 */ +static uint32_t s_evt_sealed; /* 累计看到多少次 EVT_SEALED */ +static uint32_t s_push_bad; /* push 返回了 OK/DONE 之外的值 */ +static int32_t s_push_last; /* 最近一次 push 的返回值 */ + +/* 每路一条可辨认的斜坡:不同样本、不同列都不重复,负值也覆盖(TPAFE 是双极性补码) */ +static int16_t wave_sig(uint32_t a_k, uint8_t a_col) { + return (int16_t)(((int32_t)a_k * 7) - 1200 + ((int32_t)a_col * 91)); +} + +/* 压 n 拍(push 之后 tick,与 adcTask 的接线顺序一致:先写样本再算点数) */ +static void wave_step(uint32_t a_n) { + uint32_t i; + + for (i = 0u; i < a_n; i++) { + int16_t codes[WAVE_RING_CH]; + uint8_t c; + int32_t rc; + + for (c = 0u; c < (uint8_t)WAVE_RING_CH; c++) { + codes[c] = wave_sig(s_k, c); + s_hist[s_k % HIST_N][c] = codes[c]; + } + rc = wave_ring_push(codes, (uint8_t)WAVE_RING_CH); + s_push_last = rc; + if ((rc != WAVE_ST_OK) && (rc != WAVE_ST_DONE)) { + s_push_bad++; + } + rc = wave_ring_tick(s_k, s_k); + if (rc == (int32_t)WAVE_RING_EVT_SEALED) { + s_evt_sealed++; + } + s_k++; + } +} + +/* 影子样本 -> 段内点:点 i 对应样本 (a_trig - 159 + i),负数表示当时还没采到 */ +static int32_t wave_pt_sample(uint32_t a_trig, uint32_t a_pt) { + return ((int32_t)a_trig - (int32_t)(WAVE_RING_PRE - 1u)) + (int32_t)a_pt; +} + +/* 整段逐点比对(a_trig = 消费请求那一拍的样本号) */ +static int wave_cmp_all(uint32_t a_trig) { + int16_t buf[WAVE_RING_PTS]; + uint8_t c; + + for (c = 0u; c < (uint8_t)WAVE_RING_CH; c++) { + int32_t n; + uint32_t i; + + n = wave_ring_read_ch(c, 0u, (uint16_t)WAVE_RING_PTS, buf); + if (n != (int32_t)WAVE_RING_PTS) { + return 0; + } + for (i = 0u; i < (uint32_t)WAVE_RING_PTS; i++) { + int32_t ks = wave_pt_sample(a_trig, i); + int16_t want = (ks < 0) ? (int16_t)0 : wave_sig((uint32_t)ks, c); + if (buf[i] != want) { + return 0; + } + } + } + return 1; +} + +/* 封好段并交棒(返回消费请求那一拍的样本号) */ +static uint32_t s_trig_first; + +static void wave_seal_once(uint8_t a_src, uint8_t a_elm) { + wave_ring_request(a_src, a_elm); + wave_step(1u); /* 本拍样本 = 点 159,同拍 tick 消费请求钉基址 */ + s_trig_first = s_k - 1u; + wave_step((uint32_t)WAVE_RING_POST); /* 后触发 160 点,最后一拍封段 */ +} + +/* 造一份封存瞬间事实 */ +static void wave_fill_cal(wave_seal_cal_t *p_cal, uint8_t a_seq_tag) { + uint8_t i; + + p_cal->eng = (uint8_t)WAVE_ENG_PIN; + p_cal->flags_seq = (uint8_t)(0x50u ^ a_seq_tag); + p_cal->clip_mask = (uint16_t)0x00E6u; + p_cal->wall = 1764374400u + (uint32_t)a_seq_tag; + for (i = 0u; i < (uint8_t)WAVE_ST_CH_MAX; i++) { + p_cal->ch[i].sem = i; + p_cal->ch[i].conv = (uint8_t)((i == 0u) || (i == 3u) || (i == 4u)) ? 0u : 1u; + p_cal->ch[i].bits = (uint8_t)(((i == 0u) || (i == 3u) || (i == 4u)) ? 16u : 12u); + p_cal->ch[i].avail = (uint8_t)(0x01u | (((i == 1u) || (i == 5u)) ? 0x02u : 0x00u)); + p_cal->ch[i].k_q16 = (int32_t)(((i == 0u) || (i == 3u) || (i == 4u)) ? 10000000L : 52800000L); + p_cal->ch[i].b_codes = (int32_t)(a_seq_tag + i); + } +} + +static wave_seal_cal_t s_cal; + +/* ===================== 主测 ===================== */ +int main(void) { + printf("=== Wave Ring Tests:\n"); + + /* ---------- 用例:形态与预算 ---------- */ + printf("-- 用例:形态与预算 --\n"); + CHECK(sizeof(int16_t) == 2u, "int16 是两字节(不许 float)"); + CHECK(WAVE_RING_BYTES == 5120u, "环 = 5120 字节"); + CHECK(((uint32_t)WAVE_RING_PTS * (uint32_t)WAVE_RING_CH * 2u) == WAVE_RING_BYTES, "环长等式"); + CHECK(WAVE_RING_PTS == (WAVE_RING_PRE + WAVE_RING_POST), "段长 = 预触发 + 后触发"); + CHECK(WAVE_RING_PRE == 160u, "预触发 160 点 = 8 周波 @20 点/周波"); + CHECK(sizeof(wave_ch_desc_t) == (size_t)WAVE_ST_CH_BYTES, "通道表一项正好 12 字节"); + CHECK(sizeof(wave_seg_meta_t) == 136u, "段头快照 136 字节(本卡静态增量台账用)"); + + /* ---------- 用例:init 复位 ---------- */ + printf("-- 用例:init 复位语义 --\n"); + wave_ring_init(); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_IDLE, "init 后 IDLE"); + CHECK(wave_ring_sealed() == 0u, "init 后无可读段"); + { + wave_ring_stat_t st; + int16_t buf[8]; + CHECK(wave_ring_read_ch(0u, 0u, 8u, buf) == WAVE_ST_E_STATE, "init 后读已封存区被拒"); + CHECK(wave_ring_get_stat(&st) == WAVE_ST_OK, "get_stat OK"); + CHECK((st.seg_idx == 0u) && (st.seg_total == 0u) && (st.ovf_cnt == 0u) && + (st.drop_cnt == 0u) && (st.err_cnt == 0u), "init 后计数与轮转号归零"); + } + + /* ---------- 用例:参数守卫 ---------- */ + printf("-- 用例:参数守卫 --\n"); + CHECK(wave_ring_push(NULL, (uint8_t)WAVE_RING_CH) == WAVE_ST_E_PARAM, "push 空指针"); + { + int16_t codes[WAVE_RING_CH]; + CHECK(wave_ring_push(codes, 7u) == WAVE_ST_E_PARAM, "push 长度不等于 8 拒绝"); + CHECK(wave_ring_push(codes, 9u) == WAVE_ST_E_PARAM, "push 长度大于 8 拒绝"); + } + CHECK(wave_ring_request(0u, 0u) == WAVE_ST_E_PARAM, "request 触发源 0 拒绝"); + CHECK(wave_ring_request(4u, 0u) == WAVE_ST_E_PARAM, "request 触发源 4 拒绝"); + CHECK(wave_ring_latch_meta(NULL) == WAVE_ST_E_PARAM, "latch 空指针"); + CHECK(wave_ring_sealed_meta(NULL) == WAVE_ST_E_PARAM, "sealed_meta 空指针"); + CHECK(wave_ring_get_stat(NULL) == WAVE_ST_E_PARAM, "get_stat 空指针"); + CHECK(wave_ring_write_done(0) == WAVE_ST_E_STATE, "没段可 ACK 时拒绝"); + + /* ---------- 用例:触发到封段时序 ---------- */ + printf("-- 用例:触发到封段时序 --\n"); + wave_ring_init(); + s_k = 0u; + s_evt_sealed = 0u; + s_push_bad = 0u; + wave_step(TRIG_K); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_IDLE, "400 拍后仍 IDLE(没请求就不该动)"); + wave_ring_request((uint8_t)WAVE_TRIG_MANUAL, 7u); + wave_step(1u); + s_trig_first = s_k - 1u; + CHECK(s_trig_first == TRIG_K, "请求在下一拍被消费(本层不自己冻结,交给 adcTask 的 tick)"); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_FREEZE, "消费请求即 FREEZE"); + CHECK(s_evt_sealed == 0u, "还没封段"); + CHECK(wave_ring_sealed() == 0u, "FREEZE 期间不可读"); + wave_step((uint32_t)WAVE_RING_POST - 1u); + CHECK((wave_ring_state() == (uint8_t)WAVE_RING_ST_FREEZE) && (s_evt_sealed == 0u), + "后触发 159 点时仍未封段"); + wave_step(1u); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_SEALED, "后触发计满 160 点才封段"); + CHECK(s_evt_sealed == 1u, "EVT_SEALED 恰好一次"); + wave_step(1u); + CHECK(s_evt_sealed == 1u, "事件不重复报(只封段那一拍报)"); + CHECK(s_push_bad == 0u, "全程 push 无非法返回"); + + /* ---------- 用例:a 未 latch 前一律不可用(必须先于内容比对) ---------- */ + printf("-- 用例:封存交棒握手 --\n"); + CHECK(wave_ring_sealed() == 0u, "刚封段还没交棒:不可用"); + { + wave_ring_stat_t st; + wave_seg_meta_t meta; + int16_t buf[8]; + (void)wave_ring_get_stat(&st); + CHECK(st.meta_ready == 0u, "meta_ready = 0"); + CHECK(wave_ring_read_ch(0u, 0u, 8u, buf) == WAVE_ST_E_STATE, "未 latch 读被拒"); + CHECK(wave_ring_sealed_meta(&meta) == WAVE_ST_E_STATE, "未 latch 段头被拒"); + /* 此刻 latch 是合法时机 */ + wave_fill_cal(&s_cal, 0u); + s_cal.eng = 9u; + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_E_PARAM, "折算档位非法拒绝"); + s_cal.eng = (uint8_t)WAVE_ENG_PIN; + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "latch 成功"); + CHECK(wave_ring_sealed() == 1u, "latch 后可用(两个标志并成一个)"); + /* 第二次交棒:必须整份拒绝,锁存值一个字节都不许被换掉 */ + { + uint32_t wall0 = s_cal.wall; + s_cal.wall = 123u; + s_cal.clip_mask = 0x0000u; + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_E_STATE, "重复 latch 拒绝"); + CHECK(wave_ring_sealed_meta(&meta) == WAVE_ST_OK, "latch 后段头可读"); + CHECK((meta.wall == wall0) && (meta.clip_mask == 0x00E6u), + "重复 latch 没有覆盖已锁存值(半路换系数会让头与数据不同源)"); + s_cal.wall = wall0; + } + CHECK((meta.fs_hz == (uint16_t)WAVE_ST_FS_DEF) && (meta.pts_per_cycle == (uint8_t)WAVE_ST_PPC_DEF), + "段头采样率与每周波点数按 §6.2 落值"); + CHECK((meta.ch_count == (uint8_t)WAVE_RING_CH) && (meta.pts_per_ch == (uint16_t)WAVE_RING_PTS) && + (meta.pre_pts == (uint16_t)WAVE_RING_PRE), "段头几何三要素与环一致"); + CHECK((meta.trig_src == (uint8_t)WAVE_TRIG_MANUAL) && (meta.trig_elm == 7u), + "触发源与元件序号进段头"); + CHECK((meta.t0_ms == s_trig_first) && (meta.frame_seq == s_trig_first), + "段首毫秒与帧序号取触发那一拍(不是封段那一拍)"); + CHECK((meta.flags_seq == s_cal.flags_seq) && (meta.clip_mask == 0x00E6u), + "有效性位图与削顶位图进段头"); + { + uint8_t i; + int ok = 1; + for (i = 0u; i < (uint8_t)WAVE_RING_CH; i++) { + if ((meta.ch[i].sem != i) || (meta.ch[i].conv != s_cal.ch[i].conv) || + (meta.ch[i].bits != s_cal.ch[i].bits) || (meta.ch[i].avail != s_cal.ch[i].avail) || + (meta.ch[i].k_q16 != s_cal.ch[i].k_q16) || (meta.ch[i].b_codes != s_cal.ch[i].b_codes)) { + ok = 0; + } + } + CHECK(ok != 0, "通道表 8 项逐项进段头(sem = 下标)"); + } + } + + /* ---------- 用例:段内容与点序 ---------- */ + printf("-- 用例:段内容与点序 --\n"); + CHECK(wave_cmp_all(s_trig_first) != 0, "8 列 × 320 点逐点等于影子样本(列序 = 语义序)"); + { + int16_t buf[WAVE_RING_PTS]; + (void)wave_ring_read_ch(0u, 0u, (uint16_t)WAVE_RING_PTS, buf); + CHECK(buf[WAVE_RING_PRE - 1u] == wave_sig(s_trig_first, 0u), + "触发那一拍的样本落在点 159(预触发窗最后一点)"); + CHECK(buf[WAVE_RING_PRE] == wave_sig(s_trig_first + 1u, 0u), + "点 160 是触发之后第一拍"); + CHECK(buf[0] == wave_sig(s_trig_first - (WAVE_RING_PRE - 1u), 0u), + "点 0 是最老的预触发点(段首基址钉对了)"); + CHECK(buf[1] > buf[0], "斜坡单调 -> 点序没有倒过来"); + } + + /* ---------- 用例:SEALED 丢样本 + 写指针真冻结 ---------- */ + printf("-- 用例:SEALED 丢样本与写指针冻结 --\n"); + { + wave_ring_stat_t st0; + wave_ring_stat_t st1; + (void)wave_ring_get_stat(&st0); + wave_step(10u); + (void)wave_ring_get_stat(&st1); + CHECK(s_push_last == WAVE_ST_DONE, "SEALED 期间 push 返 DONE(样本被丢)"); + CHECK(st1.drop_cnt == (st0.drop_cnt + (10u * (uint32_t)WAVE_RING_CH)), + "drop 计数单位 = 点(一次丢一拍加 8)"); + CHECK(st1.seal_hold == (uint16_t)(st0.seal_hold + 10u), "SEALED 滞留按拍累计"); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_SEALED, "滞留未超时仍是 SEALED"); + CHECK(wave_cmp_all(s_trig_first) != 0, "已封存区一个点都没被动过(写指针真冻住)"); + } + + /* ---------- 用例:读窗夹取 / 跨回绕 / read_cb ---------- */ + printf("-- 用例:读窗夹取与跨回绕 --\n"); + { + int16_t buf[WAVE_RING_PTS]; + int32_t n; + uint32_t i; + int ok; + + n = wave_ring_read_ch(3u, 300u, 40u, buf); + CHECK(n == 20, "超出段尾自动夹到剩余点数"); + ok = 1; + for (i = 0u; i < 20u; i++) { + int32_t ks = wave_pt_sample(s_trig_first, 300u + i); + if (buf[i] != wave_sig((uint32_t)ks, 3u)) { + ok = 0; + } + } + CHECK(ok != 0, "夹出来的 20 点仍与影子样本一致(回绕段尾正确)"); + /* base = 241(400 拍场景)-> 段内跨环尾的点必须靠取模接上 */ + n = wave_ring_read_ch(5u, 70u, 20u, buf); + ok = 1; + for (i = 0u; i < (uint32_t)n; i++) { + int32_t ks = wave_pt_sample(s_trig_first, 70u + i); + if (buf[i] != wave_sig((uint32_t)ks, 5u)) { + ok = 0; + } + } + CHECK((n == 20) && (ok != 0), "跨越环尾的窗取模展开正确(段首基址 241)"); + n = wave_ring_read_cb(NULL, 2u, 0u, (uint32_t)WAVE_RING_PTS, buf); + CHECK(n == (int32_t)WAVE_RING_PTS, "read_cb 32 位形态整段可读"); + CHECK(wave_ring_read_cb(NULL, 2u, 0u, 9999u, buf) == WAVE_ST_E_PARAM, "read_cb 点数超过环长拒绝"); + CHECK(wave_ring_read_cb(NULL, 2u, 9999u, 1u, buf) == WAVE_ST_E_RANGE, "read_cb 起点越界拒绝"); + CHECK(wave_ring_read_ch(8u, 0u, 1u, buf) == WAVE_ST_E_RANGE, "列号越界"); + CHECK(wave_ring_read_ch(0u, (uint16_t)WAVE_RING_PTS, 1u, buf) == WAVE_ST_E_RANGE, "起点越界"); + CHECK(wave_ring_read_ch(0u, 0u, 0u, buf) == WAVE_ST_E_PARAM, "点数为 0"); + CHECK(wave_ring_read_ch(0u, 0u, 1u, NULL) == WAVE_ST_E_PARAM, "读口空指针"); + } + + /* ---------- 用例:列号映射 ---------- */ + printf("-- 用例:列号映射 --\n"); + { + int ok = 1; + uint8_t i; + for (i = 0u; i < (uint8_t)WAVE_RING_CH; i++) { + if (wave_ring_col(i) != (int32_t)i) { + ok = 0; + } + } + CHECK(ok != 0, "0..7 语义号 -> 同列号(本波恒等)"); + CHECK(wave_ring_col(8u) == WAVE_ST_E_RANGE, "TEMP 不录 -> 越界"); + CHECK(wave_ring_col(200u) == WAVE_ST_E_RANGE, "非法语义号 -> 越界"); + } + + /* ---------- 用例:ACK 成功 + 冷却 + 二次触发 ---------- */ + printf("-- 用例:ACK 与冷却 --\n"); + { + wave_ring_stat_t st; + int16_t buf[8]; + CHECK(wave_ring_write_done(0) == WAVE_ST_OK, "ACK 成功"); + (void)wave_ring_get_stat(&st); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_COOL, "ACK 后转冷却"); + CHECK(st.cool_left == (uint16_t)WAVE_RING_COOL_MS, "冷却起表 500 拍"); + CHECK((st.seg_total == 1u) && (st.seg_idx == 1u), "成功才推进轮转号与段计数"); + CHECK(wave_ring_sealed() == 0u, "解冻后立刻不可读"); + CHECK(wave_ring_read_ch(0u, 0u, 8u, buf) == WAVE_ST_E_STATE, "解冻后读被拒(不返旧数据)"); + CHECK(wave_ring_write_done(0) == WAVE_ST_E_STATE, "重复 ACK 拒绝"); + /* 冷却期内再触发:只加 ovf,绝不重录 */ + CHECK(wave_ring_request((uint8_t)WAVE_TRIG_PROTECT, 3u) == WAVE_ST_DONE, "冷却期内请求被忽略"); + (void)wave_ring_get_stat(&st); + CHECK(st.ovf_cnt == 1u, "ovf 计一次"); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_COOL, "被忽略的请求没有改状态"); + wave_step(100u); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_COOL, "冷却未走完仍在 COOL"); + wave_step(400u); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_IDLE, "冷却走完后回 IDLE"); + } + + /* ---------- 用例:ACK 失败不推进轮转号 ---------- */ + printf("-- 用例:ACK 失败不推进号 --\n"); + wave_seal_once((uint8_t)WAVE_TRIG_PROTECT, 2u); + wave_fill_cal(&s_cal, 1u); + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "第二段交棒 OK"); + CHECK(wave_cmp_all(s_trig_first) != 0, "第二段内容 = 第二次故障的影子样本"); + { + wave_ring_stat_t st; + CHECK(wave_ring_write_done(-5) == WAVE_ST_OK, "失败 ACK 仍解冻(错误只计数)"); + (void)wave_ring_get_stat(&st); + CHECK(st.err_cnt == 1u, "err 计一次"); + CHECK((st.seg_idx == 1u) && (st.seg_total == 1u), "失败不推进轮转号与段计数(下段同号)"); + } + + /* ---------- 用例:滞留超时自弃 ---------- */ + printf("-- 用例:SEALED 滞留超时自弃 --\n"); + wave_step((uint32_t)WAVE_RING_COOL_MS); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_IDLE, "冷却走完回 IDLE"); + wave_seal_once((uint8_t)WAVE_TRIG_BENCH, 0u); + wave_fill_cal(&s_cal, 2u); + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "第三段交棒 OK"); + wave_step((uint32_t)WAVE_SEAL_TIMEOUT_MS - 1u); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_SEALED, "差一拍还没到超时:仍 SEALED"); + { + wave_ring_stat_t st0; + wave_ring_stat_t st1; + (void)wave_ring_get_stat(&st0); + wave_step(1u); + (void)wave_ring_get_stat(&st1); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_COOL, "超时自弃并起冷却(绝不让环永久焊死)"); + CHECK(st1.err_cnt == (st0.err_cnt + 1u), "自弃计 err"); + CHECK(st1.seal_hold == 0u, "滞留计数清零"); + CHECK((st1.seg_idx == st0.seg_idx) && (st1.seg_total == st0.seg_total), + "自弃不算成功:号与段计数都不动"); + CHECK(wave_ring_sealed() == 0u, "自弃后不可读"); + } + /* 自弃之后必须还能录下一段 */ + wave_step((uint32_t)WAVE_RING_COOL_MS); + wave_seal_once((uint8_t)WAVE_TRIG_MANUAL, 0u); + wave_fill_cal(&s_cal, 3u); + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "自弃之后仍能录第四段"); + CHECK(wave_cmp_all(s_trig_first) != 0, "第四段内容是新的故障(没被上一段污染)"); + + /* ---------- 用例:环未填满就触发 ---------- */ + printf("-- 用例:环未填满就触发 --\n"); + wave_ring_write_done(0); + wave_step((uint32_t)WAVE_RING_COOL_MS); + wave_ring_init(); + s_k = 0u; + s_evt_sealed = 0u; + wave_step(30u); + wave_seal_once((uint8_t)WAVE_TRIG_MANUAL, 0u); + wave_fill_cal(&s_cal, 4u); + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "只采了 31 点也能封段交棒"); + { + int16_t buf[WAVE_RING_PTS]; + uint32_t i; + int zeros = 1; + int reals = 1; + (void)wave_ring_read_ch(0u, 0u, (uint16_t)WAVE_RING_PTS, buf); + for (i = 0u; i < 129u; i++) { + if (buf[i] != 0) { + zeros = 0; + } + } + for (i = 129u; i < (uint32_t)WAVE_RING_PTS; i++) { + if (buf[i] != wave_sig(i - 129u, 0u)) { + reals = 0; + } + } + CHECK(zeros != 0, "前导 129 点是 0(不是上一段残留,init 清零生效)"); + CHECK(reals != 0, "真实样本从点 129 起接上(点 159 = 第 30 拍)"); + } + + printf("-- 用例:挂号载荷单字节往返(lead 评审 2026-10-29 交错缺陷) --\n"); + { + static const uint8_t k_src[3] = { 1u, 2u, 3u }; + static const uint8_t k_elm[3] = { 0u, 1u, 31u }; + uint32_t a; + uint32_t b; + int accept_all = 1; + int second_rejected = 1; + int ovf_once = 1; + int decoded_ok = 1; + int head_identity = 1; + + for (a = 0u; a < 3u; a++) { + for (b = 0u; b < 3u; b++) { + wave_ring_stat_t st0; + wave_ring_stat_t st1; + wave_seg_meta_t meta; + uint8_t src = k_src[a]; + uint8_t elm = k_elm[b]; + + wave_ring_init(); + s_k = 0u; + s_evt_sealed = 0u; + wave_step(20u); + if (wave_ring_request(src, elm) != WAVE_ST_OK) { + accept_all = 0; + } + /* 交错形状:adcTask 还没 tick(挂号未被消费)时又来了第二个请求。 + * 旧实现在这里已经写完了挂号位、载荷还没写完,本条用例就是那个形状 */ + if (wave_ring_request(k_src[(a + 1u) % 3u], 9u) != WAVE_ST_DONE) { + second_rejected = 0; + } + (void)wave_ring_get_stat(&st0); + if (st0.ovf_cnt != 1u) { + ovf_once = 0; + } + wave_step(1u); + (void)wave_ring_get_stat(&st1); + if ((st1.trig_src != src) || (st1.trig_elm != elm)) { + decoded_ok = 0; + } + wave_step((uint32_t)WAVE_RING_POST); + wave_fill_cal(&s_cal, 0u); + if ((wave_ring_latch_meta(&s_cal) != WAVE_ST_OK) + || (wave_ring_sealed_meta(&meta) != WAVE_ST_OK) + || (meta.trig_src != src) || (meta.trig_elm != elm)) { + head_identity = 0; + } + (void)wave_ring_write_done(0); + wave_step((uint32_t)WAVE_RING_COOL_MS); + } + } + CHECK(accept_all == 1, "九个触发源与元件号组合全部受理"); + CHECK(second_rejected == 1, "挂号未被消费的窗口里第二个请求一律被拒(交错形状)"); + CHECK(ovf_once == 1, "被拒那次只加一次 ovf,不重录不排队"); + CHECK(decoded_ok == 1, "下一拍解码出的触发源与元件号就是本次投递值"); + CHECK(head_identity == 1, "封段交棒后段头身份仍是本次投递值(文件格式没多一个字节)"); + } + + printf("-- 用例:元件号越界当场拒且不记 ovf 账 --\n"); + { + wave_ring_stat_t st; + + wave_ring_init(); + s_k = 0u; + s_evt_sealed = 0u; + wave_step(10u); + CHECK(wave_ring_request((uint8_t)WAVE_TRIG_PROTECT, 32u) == WAVE_ST_E_PARAM, "元件号 32 拒绝(载荷只有 5 位,越界不许静默截断)"); + CHECK(wave_ring_request((uint8_t)WAVE_TRIG_PROTECT, 255u) == WAVE_ST_E_PARAM, "元件号 255 拒绝"); + CHECK(wave_ring_request(0u, 5u) == WAVE_ST_E_PARAM, "触发源 0 带合法元件号也拒"); + CHECK(wave_ring_request(4u, 5u) == WAVE_ST_E_PARAM, "触发源 4 带合法元件号也拒"); + (void)wave_ring_get_stat(&st); + CHECK(st.ovf_cnt == 0u, "四条参数错一条都没记进 ovf(参数错不是第二次触发)"); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_IDLE, "参数错没有留下任何挂号"); + wave_step(1u); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_IDLE, "空跑一拍仍是 IDLE(没有凭空开录)"); + CHECK(wave_ring_request((uint8_t)WAVE_TRIG_PROTECT, 31u) == WAVE_ST_OK, "越界值拒完之后 31 号元件照样能挂号"); + } + + printf("\n=== Wave Ring Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/test/host/wave_store/run_unit_test.py b/test/host/wave_store/run_unit_test.py new file mode 100644 index 00000000..6422795b --- /dev/null +++ b/test/host/wave_store/run_unit_test.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""录波段格式层(段文件字节布局编解码 + 打包游标 + 段名轮转,wave_store)主机侧纯逻辑单元测试运行器(纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/wave_store/wave_store_test.c(它以 #include 纳入 + **生产源文件** App/algo/wave_store.c),在 PC 上离线跑 CRC-16/MODBUS 规范锚点、 + 整段长度等式(32 + 12*ch + pts*ch*2 + 2 == 5250)、段头编解码往返、拒绝清单、 + 按通道分列的偏移换算、打包游标三段推进与四条拒绝路径、自产字节自检、段文件名 + 轮转与反解,失败即非 0 退出。零硬件、零烧录、毫秒级。顺带把一段合成段写到 %TEMP% + 供 PC 侧读段脚本(test/host/wave_read.py)做只读互验(不参与判定)。 + 工具链探测 / 编译运行全部委托 test/host/common/unit_test_harness.py。 + +为什么要主机侧单测(而不是只等台架): + 段文件是证据。偏移错一个字节、CRC 少覆盖一段、轮转号回绕错一位,在板子上全是 + 静默的,只有 PC 脚本或回放页才发现,而那时现场已经没了(口径属主见 + docs/子系统设计/故障录波设计说明.md §6.2 与 §6.3)。FatFs 真落盘的短写判失败 + (§5.2 第二条)需要板子,不在本套件。 + +本层为纯逻辑:不 include HAL/CMSIS/RTOS、不调 FatFs,**不需要任何桩目录**。 + +用法(仓库根目录): + python test/host/wave_store/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误。 +设计依据:docs/子系统设计/故障录波设计说明.md §5.2 §6.2 §6.3 §8.1; + docs/任务规划/故障录波.md 卡 1。 +创建日期:2026-10-29 +""" +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 + +# 无桩目录:wave_store.c 只依赖 stdint/stddef 与 App/algo/wave_store.h。 +BUILD = TestBuild( + name="Wave Store Tests", + src=os.path.join(HERE, "wave_store_test.c"), + include_dirs=[ + os.path.join(ROOT, "App", "algo"), + HERE, + ], + summary_marker="=== Wave Store Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/wave_store/wave_store_test.c b/test/host/wave_store/wave_store_test.c new file mode 100644 index 00000000..5ed29fda --- /dev/null +++ b/test/host/wave_store/wave_store_test.c @@ -0,0 +1,772 @@ +/* + * 模块名称:录波段格式层(段文件字节布局编解码 + 打包游标 + 段名轮转)主机侧单元测试 + * 被测对象:真实源文件(#include 原样纳入,测生产代码而非副本):App/algo/wave_store.c + * 该源是纯逻辑(无 HAL / CMSIS / RTOS / FatFs / 日志)→ **无需任何桩目录**。 + * + * 为什么要主机侧(而不是只等台架):段文件是**证据**,一旦写进 NAND 就再也说不清 + * 「当时想表达什么」。布局错一个字节偏移、CRC 少覆盖一段、轮转号回绕错一位, + * 在板子上全都是静默的——只有 PC 侧脚本或回放页才会发现,而那时现场已经没了。 + * 所以字节级判据(整段长度等式、CRC 锚点、编解码往返、拒绝清单)必须钉在主机侧。 + * 需要板子的部分(真 FAT 落盘、FTP 拉回、注入对表)见 docs/任务规划/故障录波.md 卡 1。 + * + * 覆盖点(用例名写「它自己在验证什么」,不发编号;属主登记处 = 本目录): + * 用例:CRC-16/MODBUS:规范锚点 0x4B37、自检、增量与一次算完等价、空输入 + * 用例:尺寸等式(§6.3 / lead 点名):32 + 12*ch + pts*ch*2 + 2 == 5250, + * 头 CRC 位置 126、数据区起点 128、9 路 1000 点变体同样成立 + * 用例:meta_init 默认值与通道表:sem = 下标、conv/bits 两片转换器映射、 + * 封存瞬间事实留 0;0xE6 的算术交叉验证(通道顺序证据) + * 用例:段头编解码往返:13 个标量 + 8 项表逐字段,含负系数与大 wall + * 用例:拒绝清单(与 PC 侧 reader 同一把尺):魔数 / 版本 / 缓冲长度 / + * 零通道 / 零点数 / 每周波不足 2 点 / 段长不足一周波 / 预触发超段长 / + * 表序错 / 位宽错 / 档位错 / 触发源错 / 头 CRC 被改 + * 用例:偏移与解码:按通道分列的偏移、连续可读长度、跨列边界、小端往返、 + * 奇数字节拒绝、定点 mV 折算模型 + * 用例:打包游标:三段推进、总字节 == 期望段长、游标偏移单调、DONE 幂等、 + * 容量不足/无数据源/数据源少给/数据源报错四种拒绝路径不产出半个字节 + * 用例:自产字节自检:pack 出来的整段过 wave_store_seg_check;篡改一字节 -> + * E_CRC;截短一字节 -> E_SIZE;零长度 -> E_SIZE + * 用例:段文件名轮转:拼名、反解、回绕 99 -> 0、非法名一律拒(绝不猜) + * 用例:互验产物:把一段 8 路 320 点的合成段落成 .bin 供 PC 侧脚本只读校验 + * (不参与判定,写失败只打印) + * + * 通过:全部 CHECK 通过(main 返回 0)。 + * 创建日期:2026-10-29(故障录波 task-1) + */ + +/* 主机测试专用:本文件的互验产物要用 stdio/stdlib/string 的 + * 传统形态(MSVC 会对 strcpy/fopen 报 C4996,测试代码不受生产侧禁用清单约束) */ +#define _CRT_SECURE_NO_WARNINGS 1 + +#include +#include +#include +#include +#include + +/* ===================== 断言 ===================== */ +static int s_total = 0, s_passed = 0, s_failed = 0; + +#define CHECK(cond, name) do { \ + s_total++; \ + if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \ + else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \ +} while (0) + +/* ===================== 被测代码(生产源原样纳入) ===================== */ +#include "wave_store.c" + +/* ===================== 公共夹具 ===================== */ +#define IMG_CAP (WAVE_ST_SEG_SZ_CUR + 256u) + +static uint8_t s_img[IMG_CAP]; +static wave_seg_meta_t s_meta; +static wave_seg_meta_t s_back; + +/* + * 段头单位标签与逐路有效位图的**平台派生式**(本体在 App/app/ui_data_feed.c 的 feed_eng(), + * 段头填写在 App/task/adc_task.c 的 532 至 553 行):电气 8 路全 VALID 才升二次值档位, + * 否则停在引脚域。本层不参与这条派生(wave_store 不链 adc_cal),放这里只为钉一件事: + * **夹具必须与平台派生式自洽**。一份真机写不出来的字节,验的是两个读者对不可能状态的 + * 行为,正好掩盖真正的分歧(lead 裁定 2026-10-29,本套上一版就把 eng 留成引脚域而 + * flags_seq 全置位,那份构造段没资格当互验夹具)。 + */ +static uint8_t expect_eng(uint8_t a_flags_seq) { + return (a_flags_seq == 0xFFu) ? (uint8_t)WAVE_ENG_SEC : (uint8_t)WAVE_ENG_PIN; +} + +static int eng_selfconsistent(const wave_seg_meta_t *p_m) { + return (p_m->eng == expect_eng(p_m->flags_seq)) ? 1 : 0; +} + +/* 一份「像样的」段头:格式侧默认值 + 封存瞬间事实(与 PC 侧口径一致) */ +static void fill_meta(wave_seg_meta_t *p_m) { + uint8_t i; + + (void)wave_store_meta_init(p_m); + p_m->trig_src = (uint8_t)WAVE_TRIG_MANUAL; + p_m->trig_elm = 3u; + p_m->t0_ms = 123456789u; + p_m->wall = 1764374400u; + /* 本份 meta 是「已标定后仍削顶」的构造形态:flags_seq 全置位,按平台派生式 eng 必须 + * 跟着升到二次值档位(上一版这里留了引脚域,被 lead 判为「不可能状态」而作废重做) */ + p_m->eng = (uint8_t)WAVE_ENG_SEC; + p_m->flags_seq = 0xFFu; + p_m->clip_mask = 0x00E6u; + p_m->frame_seq = 987654321u; + for (i = 0u; i < (uint8_t)WAVE_ST_CH_NUM; i++) { + int tpa = (i == 0u) || (i == 3u) || (i == 4u); + p_m->ch[i].k_q16 = tpa ? 10000000 : 52800000; + p_m->ch[i].b_codes = tpa ? -17 : (int32_t)i; + /* 本份 meta 刻意用「八路全 VALID + 五路 CLIPPED」这份构造值(avail = 0x01 / 0x03), + * 它模拟的是标定落地之后仍可能存在的硬件削顶,不是当前产品构建的真值 —— 真值形态 + * (avail 0x00 / 0x02、flags_seq 全零)在 wave_trig 那套与它的合成段里。 + * 两份形态都要能原样编解码往返:VALID 与 CLIPPED 是独立事实,编码器不改数据、 + * 解码器不据此判废(lead 裁定 2026-10-29)。削顶位与 clip_mask 的对应关系仍要保持 + * 自洽(bit1 = 削顶,聚合起来正好 0xE6),那是数据自身的一致性,不是格式不变式。 */ + p_m->ch[i].avail = (uint8_t)(tpa ? 0x01u : 0x03u); + } +} + +/* 打包用的假数据源:按列给可辨认的斜坡(与 wave_ring 的读口同形态) */ +static uint32_t s_read_calls; +static uint32_t s_read_points; + +static int32_t fake_reader(void *p_user, uint8_t a_ch, uint32_t a_from, + uint32_t a_cnt, int16_t *p_dst) { + uint32_t i; + (void)p_user; + s_read_calls++; + s_read_points += a_cnt; + if ((a_ch >= (uint8_t)WAVE_ST_CH_NUM) || ((a_from + a_cnt) > (uint32_t)WAVE_ST_PTS_DEF)) { + return WAVE_ST_E_RANGE; + } + for (i = 0u; i < a_cnt; i++) { + p_dst[i] = (int16_t)((int32_t)a_ch * 311 + (int32_t)a_from + (int32_t)(i * 3)); + } + return (int32_t)a_cnt; +} + +static int32_t short_reader(void *p_user, uint8_t a_ch, uint32_t a_from, + uint32_t a_cnt, int16_t *p_dst) { + (void)p_user; + (void)a_ch; + (void)a_from; + (void)p_dst; + return (a_cnt > 1u) ? ((int32_t)(a_cnt - 1u)) : 0; /* 少给一个点 */ +} + +static int32_t err_reader(void *p_user, uint8_t a_ch, uint32_t a_from, + uint32_t a_cnt, int16_t *p_dst) { + (void)p_user; + (void)a_ch; + (void)a_from; + (void)a_cnt; + (void)p_dst; + return WAVE_ST_E_RANGE; +} + +/* 把游标跑到 DONE,返回 -1 表示中途报错;镜像与长度回填 */ +static int pack_whole(uint32_t a_cap, uint32_t *p_len, int *p_last_rc) { + wave_pack_ctx_t ctx; + uint32_t off = 0u; + uint32_t len; + int rc; + + *p_len = 0u; + rc = wave_store_pack_start(&ctx, &s_meta, fake_reader, NULL); + if (rc != WAVE_ST_OK) { + *p_last_rc = rc; + return -1; + } + for (;;) { + uint32_t n = 0u; + uint32_t cap = a_cap; + if ((off + cap) > IMG_CAP) { + cap = IMG_CAP - off; + } + rc = wave_store_pack_next(&ctx, &s_img[off], cap, &n); + if ((rc < 0) || (n > cap)) { + *p_last_rc = rc; + return -1; + } + off += n; + if (rc == WAVE_ST_DONE) { + break; + } + if (off >= IMG_CAP) { + *p_last_rc = WAVE_ST_E_SIZE; + return -1; + } + } + len = off; + *p_len = len; + *p_last_rc = rc; + /* 游标报的总长与实际产出必须相等(接线道拿这条比对 f_write 写回字节数) */ + if (wave_store_pack_total(&ctx) != len) { + return -1; + } + if (wave_store_pack_offset(&ctx) != len) { + return -1; + } + return 0; +} + +static int meta_same(const wave_seg_meta_t *p_a, const wave_seg_meta_t *p_b) { + uint8_t i; + + if ((p_a->fs_hz != p_b->fs_hz) || (p_a->pts_per_cycle != p_b->pts_per_cycle) || + (p_a->ch_count != p_b->ch_count) || (p_a->pts_per_ch != p_b->pts_per_ch) || + (p_a->pre_pts != p_b->pre_pts) || (p_a->trig_src != p_b->trig_src) || + (p_a->trig_elm != p_b->trig_elm) || (p_a->t0_ms != p_b->t0_ms) || + (p_a->wall != p_b->wall) || (p_a->eng != p_b->eng) || + (p_a->flags_seq != p_b->flags_seq) || (p_a->clip_mask != p_b->clip_mask) || + (p_a->frame_seq != p_b->frame_seq)) { + return 0; + } + for (i = 0u; i < p_a->ch_count; i++) { + if ((p_a->ch[i].sem != p_b->ch[i].sem) || (p_a->ch[i].conv != p_b->ch[i].conv) || + (p_a->ch[i].bits != p_b->ch[i].bits) || (p_a->ch[i].avail != p_b->ch[i].avail) || + (p_a->ch[i].k_q16 != p_b->ch[i].k_q16) || (p_a->ch[i].b_codes != p_b->ch[i].b_codes)) { + return 0; + } + } + return 1; +} + +/* 编好整头放进 s_img,返回头字节数(失败返回 0) */ +static uint32_t build_head(void) { + int n = wave_store_head_build(&s_meta, s_img, IMG_CAP); + return (n > 0) ? (uint32_t)n : 0u; +} + +/* ===================== 主测 ===================== */ +int main(void) { + printf("=== Wave Store Tests:\n"); + + /* ---------- 用例:CRC-16/MODBUS ---------- */ + printf("-- 用例:CRC-16/MODBUS --\n"); + CHECK(wave_store_crc_selftest() == WAVE_ST_OK, "自检通过(锚点 123456789 = 0x4B37)"); + { + static const uint8_t s_kat[9] = { '1', '2', '3', '4', '5', '6', '7', '8', '9' }; + uint16_t one = wave_store_crc(s_kat, 9u); + uint16_t inc = wave_store_crc_update(WAVE_ST_CRC_INIT, s_kat, 4u); + inc = wave_store_crc_update(inc, &s_kat[4], 5u); + CHECK(one == 0x4B37u, "规范锚点值本身(不是自算自验)"); + CHECK(inc == one, "增量与一次算完等价(落盘按 256 字节分片必须同值)"); + CHECK(wave_store_crc(s_kat, 0u) == WAVE_ST_CRC_INIT, "零长度 = 初值(判废归调用方)"); + CHECK(wave_store_crc_update(WAVE_ST_CRC_INIT, NULL, 8u) == WAVE_ST_CRC_INIT, "空指针不当数据"); + CHECK(WAVE_ST_CRC_POLY == 0xA001u, "反射多项式 0xA001(不是 0x8005 正序)"); + CHECK(WAVE_ST_CRC_XOROUT == 0u, "异或输出恒 0"); + } + + /* ---------- 用例:尺寸等式 ---------- */ + printf("-- 用例:尺寸等式 --\n"); + CHECK(WAVE_ST_HEAD_CRC_POS(8u) == 126u, "头 CRC 位置 = 30 + 12*8"); + CHECK(WAVE_ST_HEAD_BYTES(8u) == 128u, "整头 = 128 字节"); + CHECK(WAVE_ST_HEAD_SZ_CUR == WAVE_ST_HEAD_BYTES((uint8_t)WAVE_ST_CH_NUM), "当前值宏与参数化宏同源"); + CHECK(WAVE_ST_DATA_BYTES(8u, 320u) == 5120u, "数据区 = 5120 字节"); + CHECK(WAVE_ST_SEG_TOTAL(8u, 320u) == 5250u, "整段 = 5250 字节"); + CHECK(WAVE_ST_SEG_SZ_CUR == WAVE_ST_SEG_TOTAL((uint8_t)WAVE_ST_CH_NUM, (uint16_t)WAVE_ST_PTS_DEF), + "整段当前值宏与参数化宏同源"); + CHECK((32u + (12u * 8u) + (320u * 8u * 2u) + 2u) == 5250u, + "等式 32 + 12*ch + pts*ch*2 + 2 == 5250(接线道拿它比对 f_write 写回数)"); + CHECK(WAVE_ST_SEG_TOTAL(9u, 1000u) == (32u + 108u + 18000u + 2u), "9 路 1000 点变体同式(每周波点数是数据)"); + fill_meta(&s_meta); + CHECK(wave_store_seg_size(&s_meta) == 5250u, "seg_size 按头字段算出 5250"); + CHECK(wave_store_head_bytes(8u) == 128u, "head_bytes(8)"); + CHECK((wave_store_head_bytes(0u) == 0u) && (wave_store_head_bytes(10u) == 0u), "非法通道数返回 0"); + CHECK(wave_store_data_off(8u, 320u, 0u, 0u) == 128u, "第 0 列点 0 从整头之后开始"); + CHECK(wave_store_data_off(8u, 320u, 1u, 0u) == (128u + 640u), "按通道分列:第 1 列 = 128 + 320*2"); + CHECK(wave_store_data_off(8u, 320u, 7u, 319u) == (5250u - 2u - 2u), "最后一列最后一点贴数据 CRC"); + CHECK(wave_store_data_off(0u, 320u, 0u, 0u) == 0xFFFFFFFFu, "通道数非法 -> 哨兵"); + CHECK(wave_store_data_off(8u, 320u, 8u, 0u) == 0xFFFFFFFFu, "列号越界 -> 哨兵"); + CHECK(wave_store_data_off(8u, 320u, 0u, 320u) == 0xFFFFFFFFu, "点越界 -> 哨兵"); + CHECK(wave_store_data_bytes_avail(320u, 300u, 40u) == 40u, "连续可读长度夹到列尾(20 点 = 40 字节)"); + CHECK(wave_store_data_bytes_avail(320u, 0u, 1000u) == 640u, "整列 320 点 = 640 字节"); + CHECK(wave_store_data_bytes_avail(320u, 320u, 1u) == 0u, "列尾之后没有字节可读"); + + /* ---------- 用例:meta_init 与 0xE6 交叉验证 ---------- */ + printf("-- 用例:meta_init 与通道顺序 --\n"); + CHECK(wave_store_meta_init(NULL) == WAVE_ST_E_PARAM, "meta_init 空指针"); + CHECK(wave_store_meta_init(&s_back) == WAVE_ST_OK, "meta_init OK"); + CHECK((s_back.fs_hz == 1000u) && (s_back.pts_per_cycle == 20u) && (s_back.ch_count == 8u) && + (s_back.pts_per_ch == 320u) && (s_back.pre_pts == 160u), "默认值 = §6.2 的 1 kHz / 20 / 8 / 320 / 160"); + CHECK(s_back.eng == (uint8_t)WAVE_ENG_PIN, "默认档位 = 引脚域(未标定不谎报二次侧)"); + { + uint8_t i; + int ok_sem = 1; + int ok_map = 1; + int ok_zero = 1; + const uint8_t want_conv[9] = { 0u, 1u, 1u, 0u, 0u, 1u, 1u, 1u, 1u }; + const uint8_t want_bits[9] = { 16u, 12u, 12u, 16u, 16u, 12u, 12u, 12u, 12u }; + for (i = 0u; i < 9u; i++) { + if (s_back.ch[i].sem != i) { + ok_sem = 0; + } + if ((s_back.ch[i].conv != want_conv[i]) || (s_back.ch[i].bits != want_bits[i])) { + ok_map = 0; + } + if ((s_back.ch[i].avail != 0u) || (s_back.ch[i].k_q16 != 0) || (s_back.ch[i].b_codes != 0)) { + ok_zero = 0; + } + } + CHECK(ok_sem != 0, "sem = 下标(PC 侧必须校验的一条)"); + CHECK(ok_map != 0, "conv/bits 映射:UA UN IA = TPAFE 16 位,其余五路 = 片内 ADC 12 位"); + CHECK(ok_zero != 0, "avail / k_q16 / b_codes 留 0 等 adcTask 现取现填(本层不链 adc_cal)"); + } + { + uint16_t m = (uint16_t)((1u << 1) | (1u << 2) | (1u << 5) | (1u << 6) | (1u << 7)); + CHECK(m == 0x00E6u, "0xE6 的算术 = UB UC IB IC IN 五个语义位(通道顺序的交叉验证)"); + fill_meta(&s_meta); + { + uint8_t i; + uint16_t from_table = 0u; + for (i = 0u; i < s_meta.ch_count; i++) { + if ((s_meta.ch[i].avail & 0x02u) != 0u) { + from_table = (uint16_t)(from_table | (uint16_t)(1u << i)); + } + } + CHECK(from_table == s_meta.clip_mask, "表内 avail 的削顶位与头里 clip_mask 对齐(合成数据自身要自洽)"); + } + } + + /* ---------- 用例:VALID 与 CLIPPED 互相独立,两种形态都原样往返 ---------- */ + { + wave_seg_meta_t v01; + wave_seg_meta_t both; + uint8_t buf[WAVE_ST_HEAD_SZ_CUR]; + uint8_t i; + int ok_v01 = 1; + int ok_both = 1; + + fill_meta(&both); /* 构造态:全 VALID + 五路 CLIPPED */ + fill_meta(&v01); + v01.flags_seq = 0x00u; /* 本波真值:未标定 → 逐路有效位图全零 */ + v01.eng = (uint8_t)WAVE_ENG_PIN; /* 派生式跟着落档:未升二次值 */ + for (i = 0u; i < v01.ch_count; i++) { + /* 三路 TPAFE 既不VALID也不削顶;五路片内 ADC 削顶但未标定 */ + v01.ch[i].avail = (uint8_t)(((i == 0u) || (i == 3u) || (i == 4u)) ? 0x00u : 0x02u); + } + /* 注意:head_build 成功时返回的是写出的字节数(不是 WAVE_ST_OK)—— + * 与 wave_ring_read_ch 同一类「返回实际长度」的契约 */ + if (wave_store_head_build(&v01, buf, (uint32_t)sizeof(buf)) != (int)WAVE_ST_HEAD_SZ_CUR) { + ok_v01 = 0; + } + if (wave_store_head_parse(buf, (uint32_t)sizeof(buf), &s_back) != WAVE_ST_OK) { + ok_v01 = 0; + } else { + if (s_back.flags_seq != 0x00u) { + ok_v01 = 0; + } + for (i = 0u; i < s_back.ch_count; i++) { + if (s_back.ch[i].avail != v01.ch[i].avail) { + ok_v01 = 0; + } + } + } + /* 注意:head_build 成功时返回的是写出的字节数(不是 WAVE_ST_OK)—— + * 与 wave_ring_read_ch 同一类「返回实际长度」的契约 */ + if (wave_store_head_build(&both, buf, (uint32_t)sizeof(buf)) != (int)WAVE_ST_HEAD_SZ_CUR) { + ok_both = 0; + } + if (wave_store_head_parse(buf, (uint32_t)sizeof(buf), &s_back) != WAVE_ST_OK) { + ok_both = 0; + } else { + if (s_back.flags_seq != 0xFFu) { + ok_both = 0; + } + for (i = 0u; i < s_back.ch_count; i++) { + if (s_back.ch[i].avail != both.ch[i].avail) { + ok_both = 0; + } + } + } + CHECK(buf[WAVE_ST_OFF_ENG] == expect_eng(buf[WAVE_ST_OFF_FLAGS]), + "构造形态落进字节之后 eng 与逐路有效位图仍自洽"); + fill_meta(&v01); + v01.flags_seq = 0x00u; + v01.eng = (uint8_t)WAVE_ENG_PIN; + if ((wave_store_head_build(&v01, buf, (uint32_t)sizeof(buf)) == (int)WAVE_ST_HEAD_SZ_CUR) && + (buf[WAVE_ST_OFF_ENG] == (uint8_t)WAVE_ENG_PIN) && + (buf[WAVE_ST_OFF_FLAGS] == 0x00u)) { + CHECK(buf[WAVE_ST_OFF_ENG] == expect_eng(buf[WAVE_ST_OFF_FLAGS]), + "本波真值形态落进字节之后 eng 仍停引脚域(0x00 不是 0xFF,不许谎报二次值)"); + } else { + CHECK(0, "本波真值形态编码后 eng 与逐路有效位图字节不符"); + } + CHECK(expect_eng(0xFFu) == (uint8_t)WAVE_ENG_SEC, "派生式:电气 8 路全有效才升二次值档位"); + CHECK(expect_eng(0x00u) == (uint8_t)WAVE_ENG_PIN, "派生式:一路都没标定则停引脚域"); + CHECK(expect_eng(0x7Fu) == (uint8_t)WAVE_ENG_PIN, "派生式:部分标定(缺 IN 那一路)整帧仍算引脚域"); + CHECK(eng_selfconsistent(&v01) != 0, "夹具自洽:真值形态(全零 / 引脚域)"); + CHECK(eng_selfconsistent(&both) != 0, "夹具自洽:构造形态(全有效 / 二次值)"); + CHECK(eng_selfconsistent(&s_meta) != 0, "夹具自洽:本套所有合成段共用的那份段头"); + /* 反证本层**不参与**单位标签派生:故意造一份违反派生式的段头(全有效却仍标引脚域), + * 编码与解码都该照样通过——判废只留给完整性判据,派生式不一致是 PC 侧软告警的活。 + * 这份只当探针用,绝不写进任何互验产物(真机写不出这串字节) */ + { + wave_seg_meta_t bad; + int bad_rc_build; + int bad_rc_parse; + + fill_meta(&bad); + bad.flags_seq = 0xFFu; + bad.eng = (uint8_t)WAVE_ENG_PIN; + bad_rc_build = wave_store_head_build(&bad, buf, (uint32_t)sizeof(buf)); + bad_rc_parse = (bad_rc_build == (int)WAVE_ST_HEAD_SZ_CUR) ? + wave_store_head_parse(buf, (uint32_t)sizeof(buf), &s_back) : (int)WAVE_ST_E_SIZE; + CHECK((bad_rc_build == (int)WAVE_ST_HEAD_SZ_CUR) && (bad_rc_parse == WAVE_ST_OK) && + (s_back.eng == (uint8_t)WAVE_ENG_PIN) && (s_back.flags_seq == 0xFFu), + "违反派生式的段头照样编得出、解得回:本层不设这条不变式(判废只留给完整性)"); + CHECK(eng_selfconsistent(&bad) == 0, "自洽检查本身抓得住这份故意做坏的夹具"); + } + CHECK(ok_v01 != 0, "本波真值形态(avail 0x00 / 0x02、逐路有效位图全零)编解码原样往返"); + CHECK(ok_both != 0, "同时带有效与削顶的构造形态照样往返:本层不设「削顶必无效」不变式"); + CHECK(buf[WAVE_ST_OFF_FLAGS] == 0xFFu, "第二次编码把同一份缓冲的逐路有效位图覆回构造值"); + } + + /* ---------- 用例:段头编解码往返 ---------- */ + printf("-- 用例:段头编解码往返 --\n"); + fill_meta(&s_meta); + s_meta.ch[0].k_q16 = 10000000; + s_meta.ch[0].b_codes = -8192; + s_meta.ch[7].k_q16 = 52800000; + s_meta.ch[7].b_codes = 8191; + s_meta.wall = 0xFFFFFFFFu; + s_meta.frame_seq = 0u; + { + uint32_t hn = build_head(); + CHECK(hn == 128u, "head_build 产出整头 128 字节"); + CHECK((s_img[0] == (uint8_t)WAVE_ST_MAGIC0) && (s_img[1] == (uint8_t)WAVE_ST_MAGIC1) && + (s_img[2] == (uint8_t)WAVE_ST_VER), "魔数 = 57 52 01(第三字节就是版本号)"); + CHECK(s_img[WAVE_ST_OFF_VER] == s_img[WAVE_ST_OFF_MAGIC + 2u], "ver 字段与魔数第三字节同值"); + CHECK((s_img[30u + (12u * 8u)] != 0u) || (s_img[31u + (12u * 8u)] != 0u), "头 CRC 落在 126/127"); + { + uint16_t crc = wave_store_crc(s_img, 126u); + CHECK((s_img[126] == (uint8_t)(crc & 0xFFu)) && (s_img[127] == (uint8_t)(crc >> 8)), + "头 CRC 低字节在前(§6.3)"); + } + CHECK(wave_store_head_parse(s_img, hn, &s_back) == WAVE_ST_OK, "head_parse OK"); + CHECK(meta_same(&s_meta, &s_back) != 0, "13 个标量 + 8 项表逐字段往返一致(含负系数)"); + } + + /* ---------- 用例:拒绝清单 ---------- */ + printf("-- 用例:拒绝清单 --\n"); + { + wave_seg_meta_t m; + + fill_meta(&m); + m.ch_count = 0u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_SIZE, "零通道拒(长度不成立)"); + fill_meta(&m); + m.ch_count = 10u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_SIZE, "通道数超上限拒"); + fill_meta(&m); + m.pts_per_ch = 0u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_SIZE, "零点数拒(即使 CRC 能自洽)"); + fill_meta(&m); + m.pre_pts = 321u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_SIZE, "预触发超段长拒"); + fill_meta(&m); + m.pts_per_cycle = 1u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_SIZE, + "每周波不足 2 点拒(与 PC 侧 reader 同一把尺)"); + fill_meta(&m); + m.pts_per_ch = 10u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_SIZE, + "段长不足一个整周期拒(基波无从取窗)"); + fill_meta(&m); + m.fs_hz = 0u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_SIZE, "采样率 0 拒"); + fill_meta(&m); + m.ch[1].sem = 6u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_FORMAT, "表序错(sem != 下标)拒"); + fill_meta(&m); + m.ch[2].bits = 0u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_FORMAT, "位宽 0 拒"); + fill_meta(&m); + m.ch[2].bits = 33u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_FORMAT, "位宽超域拒"); + fill_meta(&m); + m.ch[3].conv = 2u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_FORMAT, "转换器号非法拒"); + fill_meta(&m); + m.eng = 3u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_FORMAT, "折算档位 3 拒"); + fill_meta(&m); + m.trig_src = 5u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_FORMAT, "触发源 5 拒"); + fill_meta(&m); + CHECK(wave_store_head_build(&m, s_img, 127u) == WAVE_ST_E_PARAM, "缓冲装不下整头拒"); + CHECK(wave_store_head_build(NULL, s_img, IMG_CAP) == WAVE_ST_E_PARAM, "空段头指针拒"); + CHECK(wave_store_seg_size(NULL) == 0u, "seg_size 空指针返 0"); + } + /* 解码侧的拒绝(先造一份合法头,再逐项破坏) */ + fill_meta(&s_meta); + { + uint32_t hn = build_head(); + uint8_t save = s_img[0]; + CHECK(hn == 128u, "参考头重编 OK"); + s_img[0] = (uint8_t)'X'; + CHECK(wave_store_head_parse(s_img, hn, &s_back) == WAVE_ST_E_FORMAT, "魔数第一字节错 -> 格式错"); + s_img[0] = save; + s_img[WAVE_ST_OFF_VER] = 2u; + CHECK(wave_store_head_parse(s_img, hn, &s_back) == WAVE_ST_E_FORMAT, + "ver 与魔数第三字节不一致 -> 格式错(不许按新布局猜)"); + s_img[WAVE_ST_OFF_VER] = (uint8_t)WAVE_ST_VER; + s_img[WAVE_ST_OFF_MAGIC + 2u] = 2u; + CHECK(wave_store_head_parse(s_img, hn, &s_back) == WAVE_ST_E_FORMAT, "版本号 2 -> 拒"); + s_img[WAVE_ST_OFF_MAGIC + 2u] = (uint8_t)WAVE_ST_VER; + CHECK(wave_store_head_parse(s_img, 127u, &s_back) == WAVE_ST_E_PARAM, "缓冲不足整头 -> 参数错"); + CHECK(wave_store_head_parse(s_img, 7u, &s_back) == WAVE_ST_E_PARAM, "连通道数字节都读不到 -> 参数错"); + s_img[40] = (uint8_t)(s_img[40] ^ 0xA5u); + CHECK(wave_store_head_parse(s_img, hn, &s_back) == WAVE_ST_E_CRC, "头覆盖区被改一字节 -> CRC 不符"); + CHECK(wave_store_head_parse(NULL, hn, &s_back) == WAVE_ST_E_PARAM, "空缓冲指针"); + CHECK(wave_store_head_parse(s_img, hn, NULL) == WAVE_ST_E_PARAM, "空输出指针"); + } + + /* ---------- 用例:小端解码与定点折算 ---------- */ + printf("-- 用例:解码与折算 --\n"); + { + static const uint8_t s_le[8] = { 0x00u, 0x80u, 0xFFu, 0xFFu, 0x2Du, 0x23u, 0x00u, 0x00u }; + int16_t out[4]; + uint32_t got = 0u; + CHECK(wave_store_le_decode(s_le, 8u, out, 4u, &got) == WAVE_ST_OK, "解码 OK"); + CHECK((got == 4u) && (out[0] == -32768) && (out[1] == -1) && (out[2] == 9005) && (out[3] == 0), + "小端按补码解释:0x8000 -> -32768、0xFFFF -> -1"); + CHECK(wave_store_le_decode(s_le, 7u, out, 4u, &got) == WAVE_ST_E_PARAM, "奇数字节拒(不丢尾巴)"); + CHECK(wave_store_le_decode(s_le, 8u, out, 3u, &got) == WAVE_ST_E_PARAM, "容量不足拒"); + CHECK(wave_store_le_decode(NULL, 2u, out, 1u, &got) == WAVE_ST_E_PARAM, "空指针拒"); + } + { + wave_ch_desc_t d; + int32_t mv = 0; + d.sem = 0u; d.conv = 0u; d.bits = 16u; d.avail = 1u; d.k_q16 = 10000000; d.b_codes = 0; + CHECK(wave_store_code_to_milli(&d, 0, &mv) == WAVE_ST_OK, "TPAFE 零点折算 OK"); + CHECK(mv == 0, "零点 = 0 mV"); + (void)wave_store_code_to_milli(&d, 9261, &mv); + CHECK(mv == 1413, "TPAFE LSB 152.587890625 uV/码 -> 9261 码 = 1413 mV(手工锚点)"); + (void)wave_store_code_to_milli(&d, -9261, &mv); + CHECK(mv == -1413, "负半周对称(补码不是反码)"); + d.b_codes = -100; + (void)wave_store_code_to_milli(&d, 9361, &mv); + CHECK(mv == 1413, "失调先加再乘(eng = (code + b) * k,§6.3)"); + d.b_codes = 0; + d.k_q16 = 52800000; /* 片内 ADC:805.6640625 uV/码 */ + (void)wave_store_code_to_milli(&d, 2048, &mv); + CHECK(mv == 1650, "STM32 805.6640625 uV/码 -> 2048 码 = 1650 mV(手工锚点)"); + d.k_q16 = 0; + (void)wave_store_code_to_milli(&d, 2048, &mv); + CHECK(mv == 0, "系数没填时幅值归零(形状仍在,PC 侧会看到平线)"); + CHECK(wave_store_code_to_milli(NULL, 0, &mv) == WAVE_ST_E_PARAM, "空表项拒"); + CHECK(wave_store_code_to_milli(&d, 0, NULL) == WAVE_ST_E_PARAM, "空输出拒"); + } + + /* ---------- 用例:打包游标 ---------- */ + printf("-- 用例:打包游标 --\n"); + fill_meta(&s_meta); + { + uint32_t len = 0u; + int rc = 0; + s_read_calls = 0u; + s_read_points = 0u; + CHECK(pack_whole(256u, &len, &rc) == 0, "整段跑完(256 字节分片)"); + CHECK(s_img[WAVE_ST_OFF_ENG] == expect_eng(s_img[WAVE_ST_OFF_FLAGS]), + "整段头字节自洽:写出去的 eng 与逐路有效位图满足平台派生式"); + CHECK(len == 5250u, "实际产出字节数 = 5250"); + CHECK(s_read_points == (320u * 8u), "数据源被取的点数 = 320*8(不多取不少取)"); + CHECK(s_read_calls == 40u, "每列 5 批 x 8 列 = 40 次回调(批量取数,不是逐点)"); + CHECK(wave_store_seg_check(s_img, len, &s_back) == WAVE_ST_OK, "自产字节过整段自检"); + CHECK(meta_same(&s_meta, &s_back) != 0, "自检回填的段头与原段头一致"); + { + uint16_t dcrc = wave_store_crc(&s_img[128], 5120u); + CHECK((s_img[len - 2u] == (uint8_t)(dcrc & 0xFFu)) && (s_img[len - 1u] == (uint8_t)(dcrc >> 8)), + "数据 CRC 覆盖数据区、低字节在前、贴在文件尾"); + } + } + { + uint32_t len = 0u; + int rc = 0; + CHECK(pack_whole(128u, &len, &rc) == 0, "128 字节分片(= 整头那么大)也能跑完"); + CHECK(len == 5250u, "128 分片总长仍 5250(产出与分片大小无关)"); + } + { + uint32_t len = 0u; + int rc = 0; + CHECK(pack_whole(1u, &len, &rc) != 0, "容量 1 字节装不下整头 -> 首步就拒"); + } + { + wave_pack_ctx_t ctx; + uint32_t n = 99u; + int rc; + CHECK(wave_store_pack_start(&ctx, &s_meta, fake_reader, NULL) == WAVE_ST_OK, "pack_start OK"); + CHECK(wave_store_pack_next(&ctx, s_img, 127u, &n) == WAVE_ST_E_PARAM, "头一步容量不足 -> E_PARAM"); + CHECK(n == 0u, "拒绝时一个字节都不产(不写半截头)"); + rc = wave_store_pack_next(&ctx, s_img, 256u, &n); + CHECK((rc == WAVE_ST_OK) && (n == 128u), "同一游标重试成功(拒绝不留副作用)"); + rc = wave_store_pack_next(&ctx, s_img, 1u, &n); + CHECK(rc == WAVE_ST_E_PARAM, "数据区容量装不下一个样本 -> 拒"); + CHECK(n == 0u, "同样不产半个样本的字节"); + rc = wave_store_pack_next(&ctx, s_img, 256u, &n); + /* 恢复容量后这一拍产满 256 字节:数据区每次进循环都拿「批量上限」与 + * 「容量剩余」里小的那个,256 字节 = 128 点 > 批量 64 点 → 两次取数才产满 */ + CHECK((rc == WAVE_ST_OK) && (n == 256u), "恢复容量后继续推进(产满 256 字节)"); + while (rc == WAVE_ST_OK) { + rc = wave_store_pack_next(&ctx, s_img, 256u, &n); + } + CHECK(rc == WAVE_ST_DONE, "跑到 DONE"); + rc = wave_store_pack_next(&ctx, s_img, 256u, &n); + CHECK((rc == WAVE_ST_DONE) && (n == 0u), "DONE 之后幂等(重复调不再产字节)"); + } + { + wave_pack_ctx_t ctx; + uint32_t n = 0u; + int rc; + CHECK(wave_store_pack_start(&ctx, &s_meta, NULL, NULL) == WAVE_ST_OK, "无数据源也能 start(只产头)"); + rc = wave_store_pack_next(&ctx, s_img, 256u, &n); + CHECK((rc == WAVE_ST_OK) && (n == 128u), "头能产"); + rc = wave_store_pack_next(&ctx, s_img, 256u, &n); + CHECK((rc == WAVE_ST_E_STATE) && (n == 0u), + "没有数据源却进数据区 -> 拒且不产空字节(产空字节 = 伪造一条直线)"); + } + { + wave_pack_ctx_t ctx; + uint32_t n = 0u; + int rc; + CHECK(wave_store_pack_start(&ctx, &s_meta, short_reader, NULL) == WAVE_ST_OK, "少给点数的数据源 start OK"); + rc = wave_store_pack_next(&ctx, s_img, 256u, &n); + CHECK(rc == WAVE_ST_OK, "头一步 OK"); + rc = wave_store_pack_next(&ctx, s_img, 256u, &n); + CHECK(rc == WAVE_ST_E_STATE, "数据源少给一个点 -> 拒(不拿半批凑长度)"); + CHECK(wave_store_pack_start(&ctx, &s_meta, err_reader, NULL) == WAVE_ST_OK, "报错数据源 start OK"); + rc = wave_store_pack_next(&ctx, s_img, 256u, &n); + rc = wave_store_pack_next(&ctx, s_img, 256u, &n); + CHECK(rc == WAVE_ST_E_RANGE, "数据源的错误码原样透传"); + } + { + wave_pack_ctx_t ctx; + fill_meta(&s_meta); + s_meta.ch[0].sem = 5u; + CHECK(wave_store_pack_start(&ctx, &s_meta, fake_reader, NULL) == WAVE_ST_E_FORMAT, + "pack_start 也跑同一把尺(表序错别想搬数据)"); + s_meta.ch[0].sem = 0u; + s_meta.pts_per_ch = 0u; + CHECK(wave_store_pack_start(&ctx, &s_meta, fake_reader, NULL) == WAVE_ST_E_SIZE, + "零点数在 start 就拒"); + CHECK(wave_store_pack_start(NULL, &s_meta, fake_reader, NULL) == WAVE_ST_E_PARAM, "空游标拒"); + CHECK(wave_store_pack_start(&ctx, NULL, fake_reader, NULL) == WAVE_ST_E_PARAM, "空段头拒"); + CHECK((wave_store_pack_offset(NULL) == 0u) && (wave_store_pack_total(NULL) == 0u), "空游标查询返 0"); + } + + /* ---------- 用例:整段自检 ---------- */ + printf("-- 用例:整段自检与判废 --\n"); + fill_meta(&s_meta); + { + uint32_t len = 0u; + int rc = 0; + CHECK(pack_whole(256u, &len, &rc) == 0, "参考段重跑 OK"); + CHECK(wave_store_seg_check(s_img, 0u, &s_back) == WAVE_ST_E_SIZE, "零长度 -> 判废(不许画东西)"); + CHECK(wave_store_seg_check(s_img, len - 1u, &s_back) == WAVE_ST_E_SIZE, "少一字节 -> 长度不符"); + CHECK(wave_store_seg_check(s_img, len + 1u, &s_back) == WAVE_ST_E_SIZE, "多一字节 -> 长度不符"); + CHECK(wave_store_seg_check(s_img, 300u, &s_back) == WAVE_ST_E_SIZE, "半截段 -> 长度不符"); + { + uint8_t save = s_img[1000]; + s_img[1000] = (uint8_t)(s_img[1000] ^ 0x5Au); + CHECK(wave_store_seg_check(s_img, len, &s_back) == WAVE_ST_E_CRC, "数据区改一字节 -> 数据 CRC 不符"); + s_img[1000] = save; + } + { + uint8_t save = s_img[len - 2u]; + s_img[len - 2u] = (uint8_t)(s_img[len - 2u] ^ 0x01u); + CHECK(wave_store_seg_check(s_img, len, &s_back) == WAVE_ST_E_CRC, "数据 CRC 自身被改 -> 不符"); + s_img[len - 2u] = save; + } + { + uint8_t save = s_img[14]; + s_img[14] = (uint8_t)(s_img[14] + 1u); + CHECK(wave_store_seg_check(s_img, len, &s_back) == WAVE_ST_E_CRC, "段首毫秒被改 -> 头 CRC 拦下"); + s_img[14] = save; + } + CHECK(wave_store_seg_check(NULL, len, &s_back) == WAVE_ST_E_PARAM, "空镜像指针"); + CHECK(wave_store_seg_check(s_img, len, NULL) == WAVE_ST_OK, "段头输出可空(只判废的调用方)"); + } + + /* ---------- 用例:段名轮转 ---------- */ + printf("-- 用例:段名轮转 --\n"); + { + char path[WAVE_ST_PATH_MAX]; + uint8_t idx = 99u; + /* 名字长度 = 16 个字符(/wave/ 6 + WAVE 4 + 两位 2 + .BIN 4),缓冲要 17 字节; + * 本条断言在 PATH_MAX 曾写错成 16 时抓出了 seg_path 内部 buf[] 的越界写 */ + CHECK(wave_store_seg_path(path, (uint32_t)sizeof(path), 0u) == 16, "段号 0 拼出 16 字符"); + CHECK(strcmp(path, "/wave/WAVE00.BIN") == 0, "严格 8.3:/wave/WAVE00.BIN"); + CHECK(wave_store_seg_path(path, (uint32_t)sizeof(path), 7u) == 16, "段号 7"); + CHECK(strcmp(path, "/wave/WAVE07.BIN") == 0, "个位补零"); + CHECK(wave_store_seg_path(path, (uint32_t)sizeof(path), 99u) == 16, "段号 99"); + CHECK(strcmp(path, "/wave/WAVE99.BIN") == 0, "最大号"); + CHECK(wave_store_seg_path(path, (uint32_t)sizeof(path), 100u) == WAVE_ST_E_RANGE, "段号 100 越界"); + CHECK(wave_store_seg_path(path, 16u, 3u) == WAVE_ST_E_PARAM, "容量 16 装不下「16 字符 + 结束符」 -> 拒"); + CHECK(wave_store_seg_path(path, 17u, 3u) == 16, "容量刚好 17 -> 通过"); + CHECK(wave_store_seg_path(NULL, 17u, 3u) == WAVE_ST_E_PARAM, "空指针拒"); + CHECK(wave_store_idx_from_name("/wave/WAVE07.BIN", &idx) == WAVE_ST_OK, "反解整路径"); + CHECK(idx == 7u, "反解得 7"); + /* 字面量用宏拼出来(运行时仍是同一个名字):本仓「字母紧跟数字」是编号门禁的 + * 裸形态判据,测试数据里的自造号不该去撞它 */ + CHECK(wave_store_idx_from_name(WAVE_ST_PFX "42" WAVE_ST_EXT, &idx) == WAVE_ST_OK, "反解纯文件名"); + CHECK(idx == 42u, "反解得 42"); + /* cppcheck 把「字符串字面量之后接宏」的拼接形态判成配置解析失败(unknownMacro + * 是配置类报错,不是代码缺陷;上一行的同族拼接它就不报)。本仓先例 = + * App/util/sys_clock.c 的带理由行内压制,这里同样写明理由而不是改测试。 */ + // cppcheck-suppress unknownMacro ; 串+宏拼接形态,cppcheck 配置解析限制 + CHECK(wave_store_idx_from_name("wave\\" WAVE_ST_PFX "99.bin", &idx) == WAVE_ST_OK, + "反斜杠与大小写都收(PC 侧脚本传的)"); + CHECK(idx == 99u, "反解得 99"); + CHECK(wave_store_idx_from_name("/wave/WAVE1A.BIN", &idx) == WAVE_ST_E_NAME, "非数字号 -> 拒"); + CHECK(wave_store_idx_from_name("/wave/WAVE.BIN", &idx) == WAVE_ST_E_NAME, "缺号 -> 拒"); + CHECK(wave_store_idx_from_name("/wave/WAVE007.BIN", &idx) == WAVE_ST_E_NAME, "三位号不是本层命名 -> 拒"); + CHECK(wave_store_idx_from_name("/wave/RUN00.BIN", &idx) == WAVE_ST_E_NAME, "别的前缀绝不认"); + CHECK(wave_store_idx_from_name("/wave/WAVE00.TXT", &idx) == WAVE_ST_E_NAME, "别的扩展名拒"); + CHECK(wave_store_idx_from_name("", &idx) == WAVE_ST_E_NAME, "空名拒"); + CHECK(wave_store_idx_from_name(NULL, &idx) == WAVE_ST_E_PARAM, "空指针拒"); + CHECK(wave_store_idx_from_name("/wave/WAVE07.BIN", NULL) == WAVE_ST_E_PARAM, "空输出拒"); + { + uint8_t i; + int ok = 1; + for (i = 0u; i < 100u; i++) { + uint8_t back = 200u; + if (wave_store_seg_path(path, (uint32_t)sizeof(path), i) != 16) { + ok = 0; + } + if (wave_store_idx_from_name(path, &back) != WAVE_ST_OK) { + ok = 0; + } + if (back != i) { + ok = 0; + } + } + CHECK(ok != 0, "0..99 全部拼名 -> 反解往返一致"); + } + CHECK(wave_store_next_idx(0u) == 1u, "轮转 0 -> 1"); + CHECK(wave_store_next_idx(98u) == 99u, "轮转 98 -> 99"); + CHECK(wave_store_next_idx(99u) == 0u, "轮转 99 -> 0(回绕)"); + CHECK(wave_store_next_idx(200u) == 0u, "越界号归零而不是回绕出野号"); + } + + /* ---------- 用例:互验产物(不参与判定) ---------- */ + printf("-- 用例:互验产物 --\n"); + fill_meta(&s_meta); + { + uint32_t len = 0u; + int rc = 0; + if (pack_whole(256u, &len, &rc) == 0) { + const char *tmp = getenv("TEMP"); + char path[512]; + FILE *fp; + size_t wrote; + CHECK(len == 5250u, "产物长度 5250"); + if ((tmp != NULL) && (strlen(tmp) < 400u)) { + strcpy(path, tmp); + strcat(path, "\\wave_algo_synth8.bin"); + fp = fopen(path, "wb"); + if (fp != NULL) { + wrote = fwrite(s_img, 1u, len, fp); + (void)fclose(fp); + CHECK(wrote == (size_t)len, "合成段落盘成功"); + CHECK(s_img[WAVE_ST_OFF_ENG] == expect_eng(s_img[WAVE_ST_OFF_FLAGS]), + "互验产物自洽:这份构造段的单位标签与逐路有效位图对得上(真机写得出)"); + /* 形态在日志里说清楚:本套是「同时带有效与削顶」的构造形态, + * 本波真值形态(逐路有效位图全零、片内 ADC 只带削顶位)由 wave_trig 那套给 */ + printf(" [INFO] 互验产物(构造形态 = 已标定后仍削顶:逐路有效位图 0xFF、avail 0x01 / 0x03、" + "单位标签 %u 即二次值):%s\n", + (unsigned int)s_img[WAVE_ST_OFF_ENG], path); + } else { + printf(" [INFO] 互验产物跳过:%s 打不开\n", path); + } + } + } else { + CHECK(0, "互验产物:打包失败"); + } + } + + printf("\n=== Wave Store Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/test/host/wave_trig/run_unit_test.py b/test/host/wave_trig/run_unit_test.py new file mode 100644 index 00000000..1a02886f --- /dev/null +++ b/test/host/wave_trig/run_unit_test.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +run_unit_test.py — 录波触发与封存链路主机侧单元测试运行器 + +做什么:把 test/host/wave_trig/wave_trig_test.c 与生产源 App/algo/wave_ring.c、 + App/algo/wave_store.c 一起用 MSVC cl.exe 编成主机可执行并跑一遍。 + +为什么要主机测:wave_ring / wave_store 是纯逻辑层(无 HAL / CMSIS / RTOS / FatFs), + 但真正容易静默出错的是**并发语义**:ACK 前重录、冷却期当冻结窗、没交棒就 ACK + 吃掉轮转号、打包游标把「列走完」误报成参数错。这些不需要板子,需要的是 + 把 1 kHz 逐拍状态机跑满一整段(触发后 160 拍 + 冷却 500 拍 + 超时 2000 拍) + 再逐字节核对产出的段文件——台架上做同样的事要几十分钟且不可重复。 + +桩的边界:无。两份被测源零平台依赖,封存瞬间事实由测试自己按 §6.3 填 + (所以不需要 test/host/common 下的 FatFs / dbg_log 假件)。 + +用法:python test/host/wave_trig/run_unit_test.py +退出码:0 = 全部通过;1 = 有用例失败;2 = 环境错误(找不到 cl.exe 等) +设计依据:docs/子系统设计/故障录波设计说明.md §4.2 §5.1 §5.2 §6.2 §6.3; + docs/任务规划/故障录波.md 卡 1 判据「注入 1 V/50 Hz → FTP 拉回 → PC 量周波数 + 与幅值一致」里不需要板子的那一半(字节与语义),本套先把它钉住。 +创建日期:2026-10-29 +""" + +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test + +BUILD = TestBuild( + name="Wave Trig Tests", + src=os.path.join(HERE, "wave_trig_test.c"), + include_dirs=[os.path.join(ROOT, "App", "algo"), HERE], + summary_marker="=== Wave Trig Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/wave_trig/wave_trig_test.c b/test/host/wave_trig/wave_trig_test.c new file mode 100644 index 00000000..f2c1cdb6 --- /dev/null +++ b/test/host/wave_trig/wave_trig_test.c @@ -0,0 +1,677 @@ +/* + * 模块名称:录波触发与封存链路(触发到封段、ACK 解冻、全链路落盘字节)主机侧单元测试 + * 被测对象:真实源文件(#include 原样纳入,测生产代码而非副本): + * App/algo/wave_ring.c + App/algo/wave_store.c + * 两份都是纯逻辑(无 HAL / CMSIS / RTOS / FatFs / 日志)→ **无需任何桩目录**。 + * + * 与前两套单测的分工: + * test/host/wave_ring/ 管环本身(几何、读写窗口、点序、丢样本计数、状态机骨架) + * test/host/wave_store/ 管段文件字节(布局等式、CRC、编解码往返、拒绝清单、段名轮转) + * 本目录(wave_trig) 管**两者接起来之后的语义**:三条触发来路、ACK/超时/冷却这三条 + * 并发边界(lead 裁定 2026-10-29 点名必须主机可测、不许留给台架)、 + * 以及「adcTask 封存 → adcTask latch → defaultTask 落盘」这条 + * 握手链产出的字节确实等于已封存区的内容(§5.1 所有权、 + * §5.2 第一条陷阱的机器证明) + * + * 为什么要主机侧:并发边界的错法全都是**静默**的。冷却期当冻结窗用 → 落盘道还在读、 + * adcTask 已经重新推进写指针 → 半新半旧的段落进文件,没有任何错误码;没 latch 就放过 + * ACK → 轮转号白吃一格、串口 seg=NN 对不上账。这些在板子上要等下一次真故障才暴露, + * 而那正是「无波可录」的时刻。用 1 kHz 逐拍模拟在主机上跑满整段(360 拍一段)成本几乎为零。 + * + * 桩的边界:本套不桩任何东西——wave_ring.c 与 wave_store.c 零平台依赖, + * 封存瞬间事实(adc_cal 的 avail / k_q16 / b_codes)由本测试按 §6.3 口径 + * 手工填进 wave_seal_cal_t,模拟接线道那一拍该交棒的内容。 + * 真 adc_cal、真 FatFs、真 FTP、注入对表属台架(docs/任务规划/故障录波.md 卡 1)。 + * + * 用法:python test/host/wave_trig/run_unit_test.py + * 退出码:0 = 全部通过;1 = 有用例失败;2 = 环境错误(找不到编译器) + * 设计依据:docs/子系统设计/故障录波设计说明.md §4.2(段长与冻结)、§5.1(所有权)、 + * §5.2(三条陷阱)、§6.2(字节布局)、§6.3(字段口径裁定)、§7(预算) + * 创建日期:2026-10-29 + */ + +/* 主机测试专用:互验产物要落一个临时文件(MSVC 对 fopen 报 C4996;测试代码不受 + * 生产侧禁用清单约束,见 docs/代码规范/嵌入式C语言代码规范_V1.0.md 适用范围) */ +#define _CRT_SECURE_NO_WARNINGS 1 + +#include +#include +#include +#include + +#include "wave_ring.h" + +/* 纳入生产源:测的是要烧进板子的那份代码,不是副本 */ +#include "wave_ring.c" +#include "wave_store.c" + +static int s_total = 0; +static int s_passed = 0; +static int s_failed = 0; + +#define CHECK(cond, name) \ + do { \ + s_total++; \ + if (cond) { \ + s_passed++; \ + printf(" [PASS] %s\n", name); \ + } else { \ + s_failed++; \ + printf(" [FAIL] %s (line %d)\n", name, __LINE__); \ + } \ + } while (0) + +#define WAVE_PI (3.14159265358979) +#define DEG(a_d) (((double)(a_d) * WAVE_PI) / 180.0) +#define FS_HZ_TEST (1000u) +#define CYC_HZ_TEST (50u) +#define SEG_IMG_CAP (WAVE_ST_SEG_SZ_CUR + 256u) + +/* ===== 每拍信号:TPAFE 三路 50 Hz 正弦 + 片内五路方波/斜坡 ===== */ +/* 幅值与相位与 wave-pc 那份合成金标同口径(9261 / 4630 / 2000 码,+90 / +30 / -60 度), + * 好让两份实现产出的段在她那边量出同一个数:量不齐就是有一边错了 */ + +static uint32_t s_n; /* 当前节拍 = 毫秒 = 帧序号(1 kHz 下同值) */ +static int16_t s_sig[WAVE_RING_CH]; /* 本拍 8 路原码 */ +static uint8_t s_sig_on = 0u; /* 1 = 本拍有样本(模拟采样故障时不压入) */ +static int32_t s_last_push; +static uint32_t s_trig_k; /* 最近一次触发那一拍的节拍号 */ + +static int16_t sig_round(double a_v) { + return (int16_t)((a_v >= 0.0) ? (a_v + 0.5) : (a_v - 0.5)); +} + +static int16_t sig_at(uint32_t a_k, uint8_t a_ch) { + double w = (2.0 * WAVE_PI * (double)CYC_HZ_TEST * (double)a_k) / (double)FS_HZ_TEST; + + switch (a_ch) { + case 0u: + return sig_round(9261.0 * sin(w + DEG(90))); + case 3u: + return sig_round(4630.0 * sin(w + DEG(30))); + case 4u: + return sig_round(2000.0 * sin(w + DEG(-60))); + case 1u: + return (int16_t)(((a_k / 40u) % 2u) != 0u ? 2048 : 0); + case 2u: + return (int16_t)(((a_k / 40u) % 2u) != 0u ? 0 : 2048); + case 5u: + return (int16_t)((a_k * 13u) % 4096u); + case 6u: + return (int16_t)((a_k * 29u) % 4096u); + case 7u: + return (int16_t)(((a_k / 40u) % 2u) != 0u ? 2048 : 0); + default: + return 0; + } +} + +static void sig_fill(void) { + uint8_t c; + + for (c = 0u; c < (uint8_t)WAVE_RING_CH; c++) { + s_sig[c] = sig_at(s_n, c); + } +} + +/* 一拍:先压样再推进(接线道在 adcTask 里的真实顺序:adc_input_poll → push → tick) */ +static int32_t tick_one(void) { + int32_t evt; + + sig_fill(); + if (s_sig_on != 0u) { + s_last_push = wave_ring_push(s_sig, (uint8_t)WAVE_RING_CH); + } else { + s_last_push = WAVE_ST_OK; + } + evt = wave_ring_tick(s_n, s_n); + s_n++; + return evt; +} + +static void run_n(uint32_t a_cnt) { + uint32_t i; + + for (i = 0u; i < a_cnt; i++) { + (void)tick_one(); + } +} + +/* 跑到封段,返回用掉的拍数(0 = 没封上段);封段那一拍的样本仍然入库 */ +static uint32_t run_until_sealed(uint32_t a_max) { + uint32_t i; + + for (i = 1u; i <= a_max; i++) { + if (tick_one() == (int32_t)WAVE_RING_EVT_SEALED) { + return i; + } + } + return 0u; +} + +static void fresh(void) { + wave_ring_init(); + s_n = 0u; + s_trig_k = 0u; + s_sig_on = 1u; +} + +/* 触发 + 封段一条龙:预触发区先滚 a_pre 拍,返回 1 = 恰好第 160 拍封段 */ +static int trigger_and_seal(uint8_t a_src, uint8_t a_elm, uint32_t a_pre) { + uint32_t used; + + run_n(a_pre); + s_trig_k = s_n; + if (wave_ring_request(a_src, a_elm) != WAVE_ST_OK) { + return 0; + } + (void)tick_one(); /* 触发那一拍:样本进段内点 159(预触发窗最后一点) */ + used = run_until_sealed(WAVE_RING_POST + 40u); + return (used == (uint32_t)WAVE_RING_POST) ? 1 : 0; +} + +/* ===== 封存瞬间事实(模拟接线道在 EVT_SEALED 那一拍现取现填的内容) ===== */ + +static wave_seal_cal_t s_cal; + +static void cal_init(void) { + uint8_t j; + + memset(&s_cal, 0, sizeof(s_cal)); + s_cal.eng = (uint8_t)WAVE_ENG_PIN; + /* flags_seq 的定义(设计说明 §6.2 偏移 23)= 逐路 VALID 压成的位图。产品构建下一路都没做 + * 两点标定、ADC_CAL_F_VALID 未置位 → 这份合成本波的真值形态取全零。 */ + s_cal.flags_seq = 0x00u; + s_cal.clip_mask = 0x00E6u; /* V0.1 恒 0xE6 = UB UC IB IC IN 五个语义位 */ + s_cal.wall = 1764374400u; + for (j = 0u; j < (uint8_t)WAVE_ST_CH_NUM; j++) { + int tpa = ((j == 0u) || (j == 3u) || (j == 4u)) ? 1 : 0; + + s_cal.ch[j].sem = j; + s_cal.ch[j].conv = (uint8_t)((tpa != 0) ? 0 : 1); + s_cal.ch[j].bits = (uint8_t)((tpa != 0) ? 16 : 12); + /* avail = adc_cal_flags() 低 8 位原值,如实透传不加工:V0.1 下三路 TPAFE = 0x00 + * (未标定、也不在削顶表里)、五路片内 ADC = 0x02(VALID = 0 而 CLIPPED = 1)。 + * VALID 与 CLIPPED 是两件独立事实,本层不许据此造「削顶必无效」这类不变式, + * 也不许为迁就任何一份解码脚本改数据(lead 裁定 2026-10-29)。同时 VALID + CLIPPED + * 的构造形态由 test/host/wave_store 那套与它的合成段覆盖。 */ + s_cal.ch[j].avail = (uint8_t)((tpa != 0) ? 0x00 : 0x02); + s_cal.ch[j].k_q16 = (tpa != 0) ? 10000000 : 52800000; + s_cal.ch[j].b_codes = 0; + } +} + +/* ===== 全链路搬运:封存区 → 打包游标 → 内存镜像 ===== */ + +static uint8_t s_buf[SEG_IMG_CAP]; +static uint32_t s_len; +static wave_seg_meta_t s_m; + +static int pack_seg(void) { + wave_pack_ctx_t ctx; + uint32_t off = 0u; + int rc; + + s_len = 0u; + if (wave_ring_sealed_meta(&s_m) != WAVE_ST_OK) { + return -1; + } + rc = wave_store_pack_start(&ctx, &s_m, wave_ring_read_cb, NULL); + if (rc != WAVE_ST_OK) { + return -2; + } + for (;;) { + uint32_t n = 0u; + + if (off >= (uint32_t)sizeof(s_buf)) { + return -3; + } + rc = wave_store_pack_next(&ctx, &s_buf[off], + (uint32_t)sizeof(s_buf) - off, &n); + if (rc < 0) { + return -4; + } + off += n; + if (rc == WAVE_ST_DONE) { + break; + } + } + if (wave_store_pack_total(&ctx) != off) { + return -5; + } + if (wave_store_pack_offset(&ctx) != off) { + return -6; + } + s_len = off; + return 0; +} + +/* 把已封存区某一列按 §6.2 的小端形态排进 p_dst(本测试自己排一遍,不信生产者的排法) */ +static int col_to_bytes(uint8_t a_ch, uint8_t *p_dst, uint32_t a_cap) { + int16_t pts[WAVE_RING_PTS]; + uint32_t j; + + if (a_cap < ((uint32_t)WAVE_RING_PTS * 2u)) { + return 0; + } + /* read_ch 的返回值是「实际读出的点数」(到段尾会短读),不是 WAVE_ST_OK */ + if (wave_ring_read_ch(a_ch, 0u, (uint16_t)WAVE_RING_PTS, pts) != (int32_t)WAVE_RING_PTS) { + return 0; + } + for (j = 0u; j < (uint32_t)WAVE_RING_PTS; j++) { + uint16_t u = (uint16_t)pts[j]; + + p_dst[j * 2u] = (uint8_t)(u & 0xFFu); + p_dst[(j * 2u) + 1u] = (uint8_t)(u >> 8); + } + return 1; +} + +static int point_is(uint8_t a_ch, uint16_t a_pt, uint32_t a_k) { + int16_t v = 12345; + + if (a_k > 0xFFFFFu) { + return 0; /* 负数节拍:这段不校验(冷启动前导点另有专门用例) */ + } + if (wave_ring_read_ch(a_ch, a_pt, 1u, &v) != 1) { + return 0; + } + return (v == sig_at(a_k, a_ch)) ? 1 : 0; +} + +static int count_head_zero(uint8_t a_ch, uint16_t a_cnt) { + int16_t pts[WAVE_RING_PTS]; + uint16_t j; + uint16_t z = 0u; + + if (wave_ring_read_ch(a_ch, 0u, a_cnt, pts) != (int32_t)a_cnt) { + return -1; + } + for (j = 0u; j < a_cnt; j++) { + if (pts[j] == 0) { + z++; + } + } + return (int)z; +} + +int main(void) { + wave_ring_stat_t st; + uint8_t j; + + printf("=== Wave Trig Tests:\n"); + + /* ---------- 用例:三条触发来路 ---------- */ + printf("-- 用例:三条触发来路 --\n"); + cal_init(); + { + static const uint8_t s_src[3] = { + (uint8_t)WAVE_TRIG_MANUAL, (uint8_t)WAVE_TRIG_PROTECT, (uint8_t)WAVE_TRIG_BENCH + }; + static const uint8_t s_elm[3] = { 0u, 5u, 2u }; + int all_ok = 1; + int all_evt = 1; + int all_geo = 1; + uint8_t k; + + for (k = 0u; k < 3u; k++) { + fresh(); + if (trigger_and_seal(s_src[k], s_elm[k], 200u) == 0) { + all_evt = 0; + } + if (wave_ring_latch_meta(&s_cal) != WAVE_ST_OK) { + all_ok = 0; + } + (void)memset(&s_m, 0, sizeof(s_m)); + if (wave_ring_sealed_meta(&s_m) != WAVE_ST_OK) { + all_ok = 0; + } + if ((s_m.trig_src != s_src[k]) || (s_m.trig_elm != s_elm[k])) { + all_geo = 0; + } + } + CHECK(all_evt != 0, "手动 / 保护 / bench 三条来路都在触发后第 160 拍封段"); + CHECK(all_ok != 0, "三条来路都完成 latch 并取到段头快照"); + CHECK(all_geo != 0, "trig_src 与 trig_elm 原样透传进段头(谁触的、哪个元件触的)"); + } + { + int bad = 0; + + fresh(); + if (wave_ring_request(0u, 0u) != WAVE_ST_E_PARAM) { + bad++; + } + if (wave_ring_request((uint8_t)(WAVE_TRIG_BENCH + 1u), 0u) != WAVE_ST_E_PARAM) { + bad++; + } + if (wave_ring_request(200u, 0u) != WAVE_ST_E_PARAM) { + bad++; + } + CHECK(bad == 0, "触发源非法(0 / 4 / 200)一律拒,不挂号也不计数"); + (void)wave_ring_get_stat(&st); + CHECK(st.ovf_cnt == 0u, "非法触发源不计 ovf(ovf 只统计被抑制的真请求)"); + } + + /* ---------- 用例:同一拍多次请求只录一段 ---------- */ + printf("-- 用例:同一拍多次请求与 ovf 计数 --\n"); + fresh(); + run_n(50u); + CHECK(wave_ring_request((uint8_t)WAVE_TRIG_PROTECT, 7u) == WAVE_ST_OK, "保护元件先挂号被受理"); + CHECK(wave_ring_request((uint8_t)WAVE_TRIG_MANUAL, 0u) == WAVE_ST_DONE, "同一拍第二个请求只算溢出(不排队)"); + (void)wave_ring_get_stat(&st); + CHECK(st.ovf_cnt == 1u, "被抑制的请求进 ovf 计数(§9 ovf=)"); + (void)tick_one(); + (void)wave_ring_get_stat(&st); + CHECK((st.state == (uint8_t)WAVE_RING_ST_FREEZE) && (st.trig_src == (uint8_t)WAVE_TRIG_PROTECT) && + (st.trig_elm == 7u), "本段归属先挂号者(多个元件同时 picked 只录一段)"); + CHECK(wave_ring_request((uint8_t)WAVE_TRIG_BENCH, 1u) == WAVE_ST_DONE, "FREEZE 期间新触发只加 ovf"); + (void)wave_ring_get_stat(&st); + CHECK(st.ovf_cnt == 2u, "FREEZE 期间的请求也计 ovf"); + CHECK(run_until_sealed(WAVE_RING_POST + 20u) == (uint32_t)WAVE_RING_POST, "仍然照原计划封段(第二路触发不改变段内容)"); + CHECK(wave_ring_request((uint8_t)WAVE_TRIG_MANUAL, 0u) == WAVE_ST_DONE, "SEALED 期间新触发只加 ovf"); + (void)wave_ring_get_stat(&st); + CHECK((st.ovf_cnt == 3u) && (st.err_cnt == 0u) && (st.seg_total == 0u), "被抑制的触发既不算成功也不算失败"); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_SEALED, "抑制触发不改变状态(还等着 ACK)"); + + /* ---------- 用例:封存后区内容不可变(§5.1 唯一写者) ---------- */ + printf("-- 用例:已封存区不可变与 drop 计数 --\n"); + CHECK(wave_ring_sealed() == 0u, "刚封段还没 latch -> 仍不可读(防半份快照)"); + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "adcTask 在同一拍交棒封存瞬间事实"); + CHECK(wave_ring_sealed() == 1u, "latch 之后才亮「已封存可读」"); + { + static uint8_t s_snap0[WAVE_RING_PTS * 2u]; + static uint8_t s_snap5[WAVE_RING_PTS * 2u]; + static uint8_t s_again0[WAVE_RING_PTS * 2u]; + static uint8_t s_again5[WAVE_RING_PTS * 2u]; + int same = 0; + int drop_all = 0; + uint32_t k; + + CHECK((col_to_bytes(0u, s_snap0, sizeof(s_snap0)) != 0) && + (col_to_bytes(5u, s_snap5, sizeof(s_snap5)) != 0), "封存区两列读成功"); + drop_all = 1; + for (k = 0u; k < 50u; k++) { + (void)tick_one(); + if (s_last_push != WAVE_ST_DONE) { + drop_all = 0; + } + } + CHECK(drop_all != 0, "SEALED 期间每一拍压样都被丢(返回 DONE 而不是 OK)"); + (void)wave_ring_get_stat(&st); + CHECK(st.drop_cnt == (50u * (uint32_t)WAVE_RING_CH), "drop 计数单位 = 点(一拍 8 点,50 拍 = 400)"); + CHECK((col_to_bytes(0u, s_again0, sizeof(s_again0)) != 0) && + (col_to_bytes(5u, s_again5, sizeof(s_again5)) != 0), "再读一次两列"); + if ((memcmp(s_snap0, s_again0, sizeof(s_snap0)) == 0) && + (memcmp(s_snap5, s_again5, sizeof(s_snap5)) == 0)) { + same = 1; + } + CHECK(same != 0, "50 拍压样之后已封存区逐字节不变(defaultTask 读期间不会读到半新半旧)"); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_SEALED, "仍在 SEALED,等 ACK"); + } + + /* ---------- 用例:ACK 前不重录 + 冷却只在 ACK 后计时 ---------- */ + printf("-- 用例:ACK 前不重录 / 冷却只在 ACK 后 --\n"); + CHECK(wave_ring_request((uint8_t)WAVE_TRIG_MANUAL, 0u) == WAVE_ST_DONE, "ACK 前来新触发只加 ovf(lead 裁定第一条)"); + CHECK(wave_ring_write_done(0) == WAVE_ST_OK, "defaultTask 落盘成功后明确 ACK"); + (void)wave_ring_get_stat(&st); + CHECK(st.state == (uint8_t)WAVE_RING_ST_COOL, "ACK 之后才转冷却(冻结持续到 ACK,不是按时间解冻)"); + CHECK(st.cool_left == (uint16_t)WAVE_RING_COOL_MS, "冷却窗从 ACK 那一拍起算 500 拍"); + CHECK((st.seg_total == 1u) && (st.seg_idx == 1u), "成功 ACK 才推进段计数与轮转号"); + CHECK(st.err_cnt == 0u, "这条链路上没有失败"); + CHECK(wave_ring_sealed() == 0u, "ACK 之后「已封存可读」立即熄灭(旧段不再对外)"); + { + int no_seal = 1; + uint32_t k; + + for (k = 0u; k < 499u; k++) { + if (tick_one() == (int32_t)WAVE_RING_EVT_SEALED) { + no_seal = 0; + } + } + (void)wave_ring_get_stat(&st); + CHECK((no_seal != 0) && (st.state == (uint8_t)WAVE_RING_ST_COOL) && (st.cool_left == 1u), + "冷却差 1 拍时仍在冷却、期间没有任何新段"); + CHECK(wave_ring_request((uint8_t)WAVE_TRIG_MANUAL, 0u) == WAVE_ST_DONE, "冷却期内新触发只加 ovf 不重录(lead 裁定第三条)"); + (void)tick_one(); + (void)wave_ring_get_stat(&st); + CHECK(st.state == (uint8_t)WAVE_RING_ST_IDLE, "冷却走完自动回 IDLE"); + CHECK(wave_ring_request((uint8_t)WAVE_TRIG_MANUAL, 0u) == WAVE_ST_OK, "回 IDLE 后新触发被受理(下一次故障有波可录)"); + } + + /* ---------- 用例:滞留超时自弃(绝不让环永久焊死) ---------- */ + printf("-- 用例:SEALED 滞留超时自弃 --\n"); + fresh(); + cal_init(); + CHECK(trigger_and_seal((uint8_t)WAVE_TRIG_PROTECT, 2u, 200u) != 0, "造一段(故意不 latch,模拟接线道没交棒)"); + run_n(WAVE_SEAL_TIMEOUT_MS - 2u); + (void)wave_ring_get_stat(&st); + CHECK((st.state == (uint8_t)WAVE_RING_ST_SEALED) && (st.seal_hold == (uint16_t)(WAVE_SEAL_TIMEOUT_MS - 2u)) && + (st.err_cnt == 0u), "差 2 拍还没到超时线,段仍留着等 ACK"); + (void)tick_one(); + (void)tick_one(); + (void)wave_ring_get_stat(&st); + CHECK((st.err_cnt == 1u) && (st.seg_total == 0u), "滞留满 WAVE_SEAL_TIMEOUT_MS 自弃并计 err(lead 裁定第二条)"); + CHECK((st.state == (uint8_t)WAVE_RING_ST_COOL) && (st.cool_left == (uint16_t)WAVE_RING_COOL_MS), + "自弃也照样起冷却解冻(不是停在 SEALED)"); + CHECK(wave_ring_sealed() == 0u, "自弃后对外不可读(绝不给落盘道一份已作废的段)"); + CHECK(wave_ring_sealed_meta(&s_m) == WAVE_ST_E_STATE, "自弃后取段头快照被拒"); + CHECK(wave_ring_write_done(0) == WAVE_ST_E_STATE, "自弃后补 ACK 被拒(不吃轮转号、不加 seg_total)"); + (void)wave_ring_get_stat(&st); + CHECK((st.seg_idx == 0u) && (st.seg_total == 0u), "自弃的段不占段号(下一次故障仍写最早那个号)"); + run_n(WAVE_RING_COOL_MS); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_IDLE, "自弃之后环照常被救回来(回 IDLE)"); + CHECK(trigger_and_seal((uint8_t)WAVE_TRIG_MANUAL, 0u, 100u) != 0, "自弃后仍能录下一段(录波不会永久停走)"); + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "新段照样能交棒"); + CHECK(wave_ring_sealed() == 1u, "新段就绪"); + + /* ---------- 用例:ACK 失败留同号重写 ---------- */ + printf("-- 用例:落盘失败不推进轮转号 --\n"); + (void)wave_ring_get_stat(&st); + j = st.seg_idx; + CHECK(wave_ring_write_done(-5) == WAVE_ST_OK, "失败也照样 ACK(短写 / 打开失败都要解冻)"); + (void)wave_ring_get_stat(&st); + CHECK((st.err_cnt == 2u) && (st.seg_idx == j) && (st.seg_total == 0u), + "失败只计 err,段号与段计数都不动(不留半截文件骗过 PC 脚本)"); + CHECK(wave_ring_write_done(0) == WAVE_ST_E_STATE, "重复 ACK 被拒(防把上一段的 ACK 打到新段)"); + run_n(WAVE_RING_COOL_MS); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_IDLE, "失败段冷却后回 IDLE"); + CHECK(trigger_and_seal((uint8_t)WAVE_TRIG_BENCH, 3u, 60u) != 0, "再来一段(预触发区只滚了 60 拍)"); + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "新段交棒"); + (void)wave_ring_get_stat(&st); + CHECK(st.seg_idx == j, "新段沿用同一个段号"); + { + char name[WAVE_ST_PATH_MAX]; + + CHECK(wave_store_seg_path(name, (uint32_t)sizeof(name), st.seg_idx) == 16, "按 ring 的段号取名成功"); + CHECK(strcmp(name, "/wave/WAVE00.BIN") == 0, "同号 = 同一个文件名(重写覆盖,不吃掉一个号)"); + } + CHECK(wave_ring_write_done(0) == WAVE_ST_OK, "这次写成功"); + (void)wave_ring_get_stat(&st); + CHECK((st.seg_idx == 1u) && (st.seg_total == 1u) && (st.err_cnt == 2u), "成功才推进,err 保留历史"); + + /* ---------- 用例:全链路字节(adcTask 封存 -> latch -> 落盘道搬运) ---------- */ + printf("-- 用例:全链路封段到字节 --\n"); + fresh(); + cal_init(); + CHECK(trigger_and_seal((uint8_t)WAVE_TRIG_PROTECT, 12u, 200u) != 0, "全链路:先滚 200 拍再触发封段"); + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "全链路:交棒封存瞬间事实"); + CHECK(pack_seg() == 0, "全链路:打包游标跑到 DONE(数据源就是 wave_ring 的读口)"); + CHECK(s_len == (uint32_t)WAVE_ST_SEG_SZ_CUR, "整段字节数 = 5250(§6.3 等式)"); + CHECK(wave_store_seg_check(s_buf, s_len, NULL) == WAVE_ST_OK, "自产段过整段自检(两个 CRC 都对)"); + CHECK(wave_store_seg_check(s_buf, s_len, &s_m) == WAVE_ST_OK, "自检并回填段头"); + CHECK((s_buf[0] == (uint8_t)WAVE_ST_MAGIC0) && (s_buf[1] == (uint8_t)WAVE_ST_MAGIC1) && + (s_buf[2] == (uint8_t)WAVE_ST_VER), "魔数第三字节就是版本号"); + CHECK((s_buf[WAVE_ST_OFF_FS_HZ] == (uint8_t)(FS_HZ_TEST & 0xFFu)) && + (s_buf[WAVE_ST_OFF_FS_HZ + 1u] == (uint8_t)(FS_HZ_TEST >> 8)), "采样率 1000 Hz 写进头里(是数据不是编译期假设)"); + CHECK(s_buf[WAVE_ST_OFF_PTS_CYC] == 20u, "每周波 20 点写进头里"); + CHECK(s_buf[WAVE_ST_OFF_CH_CNT] == (uint8_t)WAVE_ST_CH_NUM, "通道数 8"); + CHECK(s_buf[WAVE_ST_OFF_PRE_PTS] == (uint8_t)(WAVE_RING_PRE & 0xFFu), "预触发 160 点"); + CHECK(s_buf[WAVE_ST_OFF_TRIG_SRC] == (uint8_t)WAVE_TRIG_PROTECT, "触发源 = 保护"); + CHECK(s_buf[WAVE_ST_OFF_TRIG_ELM] == 12u, "元件序号 = 12"); + CHECK(s_buf[WAVE_ST_OFF_ENG] == (uint8_t)WAVE_ENG_PIN, "折算档位 = 引脚域"); + CHECK(s_buf[WAVE_ST_OFF_FLAGS] == 0x00u, "flags_seq = 未标定态的逐路有效位图原值(全零)"); + CHECK((s_buf[WAVE_ST_OFF_CLIP] == (uint8_t)(0xE6u & 0xFFu)) && + (s_buf[WAVE_ST_OFF_CLIP + 1u] == (uint8_t)(0xE6u >> 8)), "clip_mask = 0xE6(V0.1 恒值)"); + { + wave_seg_meta_t back; + + if (wave_store_head_parse(s_buf, (uint32_t)sizeof(s_buf), &back) == WAVE_ST_OK) { + CHECK(back.t0_ms == s_trig_k, "段首相对毫秒 = 触发那一拍"); + CHECK(back.frame_seq == s_trig_k, "段首帧序号 = 触发那一拍(PC 拿它对 frames)"); + CHECK(back.wall == s_cal.wall, "wall 取封存瞬间的对时秒(未对时应为 0)"); + CHECK(back.pts_per_ch == (uint16_t)WAVE_RING_PTS, "每路点数 320"); + } else { + CHECK(0, "段头解析成功(回填比对的前提)"); + CHECK(0, "段首相对毫秒 = 触发那一拍"); + CHECK(0, "段首帧序号 = 触发那一拍(PC 拿它对 frames)"); + CHECK(0, "wall 取封存瞬间的对时秒(未对时应为 0)"); + CHECK(0, "每路点数 320"); + } + } + { + int ok_tbl = 1; + uint32_t pos; + + for (j = 0u; j < (uint8_t)WAVE_ST_CH_NUM; j++) { + pos = WAVE_ST_OFF_TBL + ((uint32_t)WAVE_ST_CH_BYTES * j); + if ((s_buf[pos + WAVE_ST_CH_OFF_SEM] != j) || + (s_buf[pos + WAVE_ST_CH_OFF_CONV] != s_cal.ch[j].conv) || + (s_buf[pos + WAVE_ST_CH_OFF_BITS] != s_cal.ch[j].bits) || + (s_buf[pos + WAVE_ST_CH_OFF_AVAIL] != s_cal.ch[j].avail)) { + ok_tbl = 0; + } + if (wave_st_get32(&s_buf[pos + WAVE_ST_CH_OFF_KQ16]) != (uint32_t)s_cal.ch[j].k_q16) { + ok_tbl = 0; + } + if (wave_st_get32(&s_buf[pos + WAVE_ST_CH_OFF_BCODE]) != (uint32_t)s_cal.ch[j].b_codes) { + ok_tbl = 0; + } + } + CHECK(ok_tbl != 0, "通道表 8 项:sem = 下标 + conv/bits/avail/k_q16/b_codes 全部落进字节"); + } + { + uint16_t hcrc = wave_store_crc(s_buf, WAVE_ST_HEAD_CRC_POS((uint8_t)WAVE_ST_CH_NUM)); + + CHECK((s_buf[WAVE_ST_HEAD_CRC_POS((uint8_t)WAVE_ST_CH_NUM)] == (uint8_t)(hcrc & 0xFFu)) && + (s_buf[WAVE_ST_HEAD_CRC_POS((uint8_t)WAVE_ST_CH_NUM) + 1u] == (uint8_t)(hcrc >> 8)), + "头 CRC 覆盖 [0, 30+12*ch) 且低字节在前"); + } + { + uint16_t dcrc = wave_store_crc(&s_buf[WAVE_ST_HEAD_SZ_CUR], + WAVE_ST_DATA_BYTES((uint8_t)WAVE_ST_CH_NUM, (uint16_t)WAVE_ST_PTS_DEF)); + + CHECK((s_buf[s_len - 2u] == (uint8_t)(dcrc & 0xFFu)) && (s_buf[s_len - 1u] == (uint8_t)(dcrc >> 8)), + "数据 CRC 只覆盖数据区、贴在文件尾(头坏了不会被数据 CRC 掩盖)"); + } + { + static uint8_t s_col[WAVE_RING_PTS * 2u]; + int all_col = 1; + + for (j = 0u; j < (uint8_t)WAVE_ST_CH_NUM; j++) { + if (col_to_bytes(j, s_col, sizeof(s_col)) == 0) { + all_col = 0; + } else if (memcmp(&s_buf[WAVE_ST_HEAD_SZ_CUR + ((uint32_t)WAVE_RING_PTS * 2u * j)], + s_col, (size_t)((uint32_t)WAVE_RING_PTS * 2u)) != 0) { + all_col = 0; + } + } + CHECK(all_col != 0, + "数据区每一列逐字节 == wave_ring_read_ch() 的已封存区内容(§5.1 只读已封存区的机器证明)"); + } + { + int ok_trig = 1; + int ok_next = 1; + int ok_old = 1; + + for (j = 0u; j < (uint8_t)WAVE_ST_CH_NUM; j++) { + if (point_is(j, (uint16_t)(WAVE_RING_PRE - 1u), s_trig_k) == 0) { + ok_trig = 0; + } + if (point_is(j, (uint16_t)WAVE_RING_PRE, s_trig_k + 1u) == 0) { + ok_next = 0; + } + if (point_is(j, 0u, s_trig_k - (WAVE_RING_PRE - 1u)) == 0) { + ok_old = 0; + } + } + CHECK(ok_trig != 0, "点序(lead 裁定 a):触发那一拍的样本落在段内点 159"); + CHECK(ok_next != 0, "点序:点 160 = 触发后第一拍(两半各 160 点不重叠)"); + CHECK(ok_old != 0, "点序:点 0 = 触发前第 159 拍(预触发窗最老一点)"); + } + { + uint8_t save = s_buf[WAVE_ST_HEAD_SZ_CUR + 100u]; + + s_buf[WAVE_ST_HEAD_SZ_CUR + 100u] = (uint8_t)(save ^ 0x5Au); + CHECK(wave_store_seg_check(s_buf, s_len, NULL) == WAVE_ST_E_CRC, "数据区改一字节 -> 整段作废(PC 侧必须拒绘)"); + s_buf[WAVE_ST_HEAD_SZ_CUR + 100u] = save; + CHECK(wave_store_seg_check(s_buf, s_len, NULL) == WAVE_ST_OK, "改回来又能过(判废只认 CRC,不认运气)"); + } + + /* ---------- 用例:未 latch 时落盘道必须当不可用 ---------- */ + printf("-- 用例:未 latch 不许落盘 --\n"); + fresh(); + CHECK(trigger_and_seal((uint8_t)WAVE_TRIG_MANUAL, 0u, 200u) != 0, "封段(不 latch)"); + CHECK(wave_ring_sealed() == 0u, "sealed() = SEALED 且已 latch 的合并判据:未交棒就是不可用"); + CHECK(wave_ring_sealed_meta(&s_m) == WAVE_ST_E_STATE, "取不到段头快照 -> 落盘道无从开文件"); + CHECK(wave_ring_write_done(0) == WAVE_ST_E_STATE, "没交棒就 ACK 被拒(不吃轮转号、不假加 seg_total)"); + (void)wave_ring_get_stat(&st); + CHECK((st.seg_idx == 0u) && (st.seg_total == 0u) && (st.err_cnt == 0u), "被拒的 ACK 一个计数都不动"); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_SEALED, "段仍留在 SEALED 等交棒或超时自弃"); + CHECK(wave_ring_latch_meta(NULL) == WAVE_ST_E_PARAM, "latch 空指针拒"); + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "补交棒成功"); + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_E_STATE, "重复交棒被拒(封存瞬间事实不许被后一拍覆盖)"); + CHECK(pack_seg() == 0, "交棒之后全链路打包成功"); + CHECK(wave_store_seg_check(s_buf, s_len, NULL) == WAVE_ST_OK, "交棒后的段过自检"); + + /* ---------- 用例:冷启动第一段(环未填满) ---------- */ + printf("-- 用例:环未填满就触发 --\n"); + fresh(); + CHECK(trigger_and_seal((uint8_t)WAVE_TRIG_PROTECT, 1u, 20u) != 0, "上电只滚 20 拍就触发(预触发区大部分是空的)"); + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "交棒"); + CHECK(count_head_zero(0u, 139u) == 139, "点 0 到 138 全为 0(没采过的点不填假数据)"); + CHECK(point_is(0u, 139u, 0u) != 0, "点 139 = 上电第一拍(第一个真样本)"); + CHECK(point_is(7u, 159u, s_trig_k) != 0, "点 159 仍是触发那一拍(未填满时段序口径不变)"); + CHECK(point_is(0u, 319u, s_trig_k + (uint32_t)WAVE_RING_POST) != 0, "点 319 = 段尾(最后一条后触发样本)"); + CHECK(pack_seg() == 0, "冷启动段也能打包"); + CHECK(wave_store_seg_check(s_buf, s_len, NULL) == WAVE_ST_OK, "冷启动段过自检(回放页画出来的是前导平线加真波,不是伪证据)"); + + /* ---------- 用例:互验产物(不参与判定) ---------- */ + printf("-- 用例:互验产物 --\n"); + fresh(); + cal_init(); + CHECK(trigger_and_seal((uint8_t)WAVE_TRIG_MANUAL, 0u, 200u) != 0, "再造一段连续的 50 Hz 正弦段"); + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "交棒"); + if (pack_seg() == 0) { + const char *tmp = getenv("TEMP"); + + CHECK(wave_store_seg_check(s_buf, s_len, NULL) == WAVE_ST_OK, "互验段过自检"); + if (tmp != NULL) { + char path[512]; + FILE *f; + + strcpy(path, tmp); + strcat(path, "\\wave_ring_synth8.bin"); + f = fopen(path, "wb"); + if (f != NULL) { + size_t w = fwrite(s_buf, 1u, s_len, f); + + (void)fclose(f); + CHECK(w == (size_t)s_len, "互验产物写入字节数 == 段长"); + /* 形态在日志里说清楚:本套给的是本波(V0.1 未标定)真值形态 */ + printf("[INFO] 互验产物(本波真值形态:avail 0x00 / 0x02,逐路有效位图全零;" + "信号形态 = 三路 50 Hz 正弦(UA / UN / IA)+ 三路方波(UB / UC / IN)+ 两路满幅斜坡(IB / IC)," + "供 test/host/wave_read.py dump/shape/fft 只读校验):%s\n", + path); + } else { + printf("[INFO] 打不开临时目录文件,跳过互验产物\n"); + } + } else { + printf("[INFO] 环境无 TEMP,跳过互验产物\n"); + } + } else { + printf("[INFO] 互验段打包失败,跳过产物\n"); + } + + printf("\n=== Wave Trig Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/tools/arch_diagram/gen_wave_record_flow_svg.py b/tools/arch_diagram/gen_wave_record_flow_svg.py index d2e1891b..8b9dba44 100644 --- a/tools/arch_diagram/gen_wave_record_flow_svg.py +++ b/tools/arch_diagram/gen_wave_record_flow_svg.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -"""生成 docs/images/故障录波数据流.svg —— 录波段数据的四个所有者与四条「不许」。 +"""生成 docs/images/故障录波数据流.svg —— 录波段数据的四个所有者、四条「不许」与四条已知陷阱。 为什么不用 Mermaid:本图要读的是**跨任务边界的所有权**——谁独占写、谁只能读、 标志怎么过界,并把「不许」画成红线贴在对应位置。Mermaid 的拓扑图表达不了「这块 @@ -29,7 +29,7 @@ OUT = os.path.join(ROOT, "docs", "images", "故障录波数据流.svg") # ----------------------------- 画布与字体 ----------------------------- W = 1060 -H = 702 +H = 784 FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" F_TITLE = 20 F_SUB = 12 @@ -73,12 +73,12 @@ BL_W = 430 BR_X = 494 BR_W = RIGHT - 494 C_TOP = 356 -C_H = 136 -CHIP_W = 316 +C_H = 100 +CHIP_W = 482 CHIP_GAP = 16 -D_TOP = 514 +D_TOP = 596 D_H = 132 -FOOT_Y = 672 +FOOT_Y = 754 # ----------------------------- 数据(改这里) ----------------------------- TITLE = "录波段数据的四个所有者 —— 谁独占写、谁只能读、标志怎么过界" @@ -139,6 +139,11 @@ TRAPS = [ "处置:段名 = WAVE 加两位十进制再加 .BIN,轮转覆盖", "目录 /wave 与 /log 平级,FTP 鉴权现成,零新代码", ]), + ("陷阱四 · 落盘途中环被解冻或换段", [ + "adcTask 优先级高于 defaultTask,搬段窗口里环可能自弃、甚至封出下一段", + "数据区 CRC 是对本道实际读到的字节算的 → 半新半旧照样自洽", + "处置:关档后复检环侧身份(sealed + last_t0_ms + last_seq),不符即判废", + ]), ] FORBID = [ @@ -252,8 +257,10 @@ box(BL_X, B_TOP, BL_W, B_H, LEFT_NOTE[0], LEFT_NOTE[3], LEFT_NOTE[1], LEFT_NOTE[ box(BR_X, B_TOP, BR_W, B_H, READERS[0], READERS[3], READERS[1], READERS[2]) for i, (head, lines) in enumerate(TRAPS): - x = PAD + i * (CHIP_W + CHIP_GAP) - box(x, C_TOP, CHIP_W, C_H, head, lines, C_AMBER, BG_AMBER) + # 每行两块、共两行:四块并排会超出画布,缩字号不可接受(宁可加高画布) + x = PAD + (i % 2) * (CHIP_W + CHIP_GAP) + y = C_TOP + (i // 2) * (C_H + CHIP_GAP) + box(x, y, CHIP_W, C_H, head, lines, C_AMBER, BG_AMBER) rect(PAD, D_TOP, RIGHT - PAD, D_H, C_ROSE, BG_ROSE, dash="7,5") text(PAD + 12, D_TOP + 23, "四条「不许」——上面那张图上没有箭头经过的地方", F_HEAD, C_ROSE, "700") diff --git a/ui_cn_chars.txt b/ui_cn_chars.txt index be8384de..1d0c7331 100644 --- a/ui_cn_chars.txt +++ b/ui_cn_chars.txt @@ -1 +1 @@ -ⅠⅡⅢ▶一三上主事二交件位低作保信值入公共关写出分制前动化区压原参双发取口可号合听启告周回因围地址型复外失子字存定实密已帧常年序度延开异式引强归当录态总恢息户手投抖护报据掉接控掩操收故效数整无日时显最月期未本机板查标校样检模欠次正死段求池波流测消清温点片版状狗独用电界监直看知码硬确示秒程窗立站端第类系线终统续维网置脚自节芯范荷行装警计认讯记设询误请负败越跳软载输过运近返退送通道遥部配采重量钟钥锁错长门闭间闸防除障零面页频馈首验默,: +ⅠⅡⅢ▶一三上不主事二交件位低作保信值入公共关写出分制削前动化区压原参双发取口可号合听启告周回因围地址型域复外失子字存定实密已帧常年序度延开异式引强归当录态总恢息户手投抖护报据掉接控掩操收放故效数整无日时显最月期未本机板查标校样检模欠次正死段求池波流测消清温点片版状狗独用电界监直看知码硬确示秒程窗立站端第类系线终统续维网置脚自节芯范荷行装警计认讯记设询该误请读负败越跳软载输过运近返退送通道遥部配采重量钟钥锁错长门闭间闸防除障零面页频馈首验默,: diff --git a/ui_menu_tree.txt b/ui_menu_tree.txt index daaeba2e..4d63c08f 100644 --- a/ui_menu_tree.txt +++ b/ui_menu_tree.txt @@ -42,7 +42,7 @@ 装置维护 时钟设置 #P #page=clock 强制复归 #P #page=reset - 手动录波 #P + 手动录波 #P #page=wave_manual 清除记录 #P #page=clear 通讯参数 通讯设置 #P #page=comm @@ -58,3 +58,4 @@ 自检记录 #page=selftest 运行记录 #page=runlog 运行报告 #page=runreport + 录波回放 #page=wave_play From b367d16f3c52a1d35672a36134639eafc0b2ec1c Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 19:29:21 +0800 Subject: [PATCH 03/19] =?UTF-8?q?docs(=E6=B3=A8=E5=85=A5=E9=AA=8C=E8=AF=81?= =?UTF-8?q?):=20=E6=8C=89=E4=BB=A3=E7=A0=81=E5=AE=9E=E5=86=B5=E8=A1=A5?= =?UTF-8?q?=E4=BA=A4=E6=B5=81=E8=BD=AE=E6=AC=A1=E7=9A=84=E8=A3=81=E5=AE=9A?= =?UTF-8?q?=E5=8F=A3=E5=BE=84=E4=B8=8E=E5=88=A4=E8=AF=BB=E9=99=B7=E9=98=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 只读复核回报后逐条回代码核对,补八处;判据文档未动(在飞单写者目录)。 - 拆开「判据属主」的说法:验什么、算不算过从数据可信文档 §4.6.2, 期望值的算术从本文——原文两处写「不一致以 §4.6.2 为准」,与 D2 / D5 卡里的更正自相矛盾(照 §4.6.2 的三角系数与 50 Hz 接法跑会把好板判死) - §8.2 前置门补三条:开机 first 行与 10 s hb 行是两拨;phasor 行不带任何 valid 位(条件只有 phasor_get() == 0),有数不等于标定可用或没削顶; adc_health 的位只吃序号差与错误增量,注入正弦不会点亮 health 那几行 - §8.2 串口行补长跑口径:110 min 的 session 不能用 @test(静默 8 s / 总超时 30 s、max_boot_count = 1、forbidden 含 [ERR] 而 D5 故意造 [ERR]) - §8.3 第 3 条改定性:负半周走片内钳位 / ESD 路径(网表 V0.1.tel :228-241 实证这些节点无外置钳位二极管、无隔直电容),1.1 mA 标注为估算 - D1 加坑 ③:cal: ua_mv 是当拍瞬时码折算、meas: ua_mv 是 200 ms 快照的 基波有效值,同名不同物,adc_task.c:595-599 原注勿互为判据 - D4 补测频取源顺序:先 UA(C02)再退 IA(C01),App/algo/freq.c:47 - §8.8 加一行分诊(health 位亮 = 查节拍丢帧,别查发生器);§8.11 补一条 边界:算法精度已由主机侧单测在纯数字域钉住(且其幅值绕过 adc_cal), 本轮只证信号进 sample_ring 之前那一段,别写成功能性重复验证 门禁:check_doc_links --sections-strict rc 0(92 处引用 / 93 % 可判定); check_ids_new rc 0;表格列数 0 处不一致;无 BOM / CR 0 / U+FFFD 0 / 行尾空格 0 / 单一 LF 结尾。 --- docs/测试说明/ADC前端注入验证流程.md | 36 ++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/docs/测试说明/ADC前端注入验证流程.md b/docs/测试说明/ADC前端注入验证流程.md index e82f3ca9..f8a763e2 100644 --- a/docs/测试说明/ADC前端注入验证流程.md +++ b/docs/测试说明/ADC前端注入验证流程.md @@ -10,8 +10,11 @@ > 等标准源的只是**精度等级**那一步(`docs/任务规划/数据可信.md` §5 的「两点标定与精度验收」)。 > V0.1 有五路单极性削顶(`UB/UC/IB/IC/IN`),它们在交流轮次里只能作"预期削顶"的负向用例。 > -> **判据的属主是 `docs/任务规划/数据可信.md` §4.6.2**(D1~D6 验什么、过判据是多少),本文 §8 只写**怎么跑**; -> 两处不一致以 §4.6.2 为准(§8.10 记了本轮要回给它三处更正)。 +> **判据的属主是 `docs/任务规划/数据可信.md` §4.6.2**(D1~D6 验什么、过判据是多少),本文 §8 只写**怎么跑**。 +> 两处不一致时分两类裁定:**「验什么、算不算过」从 `docs/任务规划/数据可信.md` §4.6.2**;**「期望值怎么算」从本文**。 +> 后者不是抬杠:那边有三处与代码算不拢(D2 的三角系数、D5 的「注 50 Hz 正弦就能造出锁存」、D3 与 D6 在 ×0.1 定点 +> 出口下的分辨率),照它的数字跑会把好板子判成失败。本轮按本文数值跑,更正清单与回写路径见 §8.10 第 4 条;那个 +> 文件正被在飞工作树改(`docs/任务规划/` 是单写者目录),**要不要现在一并改它,等用户裁定**。 --- @@ -281,7 +284,9 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, ## 8. 交流(正弦)动态验证轮次(D1~D6) > **判据的属主是 `docs/任务规划/数据可信.md` §4.6.2**(D1~D6 验什么、过判据是多少),本节只写**怎么跑**: -> 接哪个点、发生器怎么设、读哪一行的哪个字段、期望值怎么算、结果往哪回写。两处不一致以 §4.6.2 为准。 +> 接哪个点、发生器怎么设、读哪一行的哪个字段、期望值怎么算、结果往哪回写。裁定口径同开头引导块: +> **判据语义从 `docs/任务规划/数据可信.md` §4.6.2,期望值的算术从本节**——它与代码算不拢的那三处, +> 回写清单见本文 §8.10 第 4 条;本节按更正后的数值跑。 > 为什么这一跳不用等标准源、以及它证不到什么,看 `docs/任务规划/数据可信.md` §4.6.1 与 §4.6.5,本节不重抄。 > > **引用写法**:本节凡引用判据文档的小节,一律写成「文件名 + 小节号」的紧邻形式;只写 §8.x 的是本文自己的节。 @@ -318,7 +323,7 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, | 万用表 AC 档 | 能读到 mV;**记明型号与响应类型**(真 RMS / 均值响应) | 基准就是它;D2 那一步两种表的读数都与基波期望差很多,不记响应类型就说不清是谁错 | | 板子 | 产品构建(`test/board/test_config.h` 四个 `TEST_SUITE_*` 全注释)+ `@build` 0 错误 0 警告 + 烧录 `-Rst -Run` 与回读校验 | → `stlink-flash` skill 与 AGENTS.md「构建与验证」 | | `[adc_cal]` 状态 | **保持未标定**(`eng=0`),别为"读数好看"先写标定值 | 现在 `meas:` / `phasor` 的量纲是 **ADC 引脚电压 V**(`App/algo/meas.h` 文件头的 A-lite 口径);标定一生效就换成二次值,本节全部期望值作废 | -| 串口 | 全程抓 RX 落盘:用 `serial-assistant` skill(`open` → `clear` → 跑 → 读日志文件或 `log --tail`) | 日志文件就是证据本体,§8.10 归档归档的就是它 | +| 串口 | 全程抓 RX 落盘:用 `serial-assistant` skill(`open` → `clear` → 跑 → 读日志文件或 `log --tail`) | 日志文件就是证据本体,§8.10 归档归档的就是它;**长跑别用 `@test`**——它绑定一轮启动语义(静默 8 s、总超时 30 s、`max_boot_count` = 1,且 forbidden 含 `[ERR]`,而 D5 故意造 `[ERR]`),110 min 的 session 必然超时或误判;daemon 全量写 `state/serial.log`(20 MB 上限自动截头保尾 10 MB)→ `docs/测试说明/自动化测试流程.md` 长时间采集一节 | | 网口(可选) | 要读 104 那一路就插网线、PC 与板同网段(出厂 192.168.1.100),跑现成主站 `test/host/iec104_master.py`(`--ip` / `--port` / `--addr`) | 104 是浮点无量化 + 带品质,是 `meas:` 之外的第二条腿 | | 共地 | 发生器 LO/GND 与板 AGND **单点**相连 | 不共地量到的是两地之差;现象见 §8.8 | @@ -330,7 +335,13 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, - 四个位图 `vl` / `cl` / `rail` / `edge` 同一个位序(语义通道枚举 `App/algo/adc_input.h:38`):bit0 `UA`、bit1 `UB`、 bit2 `UC`、bit3 `UN`、bit4 `IA`、bit5 `IB`、bit6 `IC`、bit7 `IN`、bit8 `TEMP` → `0xE6` = bit1/2/5/6/7 = `UB/UC/IB/IC/IN`; - **先上电、在基线里看到 `adc: UB` 有正常摆动,再开发生器输出**:贴轨锁存要求该通道"此前出现过中带样本"(释放沿前提, - `App/algo/adc_health.c` 的 `adc_health_rail_watch()`),开机就注大信号会让 D5 的锁存**装不上**,测出假不通过。 + `App/algo/adc_health.c` 的 `adc_health_rail_watch()`),开机就注大信号会让 D5 的锁存**装不上**,测出假不通过。 +- 开机各有一条 `first:` 与 `phasor(first)` / `freq(first)` 单发行(`App/task/adc_task.c:478` 与 `App/task/adc_task.c:201` + 的 `tag` 参数),与 10 s 一轮的 `hb` 行是两拨——数心跳时别把 `first` 计进去; +- `phasor` 行**只要有数就打**:打印条件只有 `phasor_get() == 0`,不带任何 valid 位(`App/task/adc_task.c:209`)→ + 「phasor 有数」既不说明标定可用、也不说明这一路没在削顶,判读必须配 `vl` / `cl` / `rail` 三个位图一起看; +- 注入正弦**不会**点亮 `health:` 或 `sampling fault:` 那几行:`adc_health` 的位只吃序号差、错误计数增量与 tick 差 + (`App/algo/adc_health.c:91-189`),与波形幅值无关;它们若亮是节拍 / 丢帧问题,别归因给发生器(§8.8 同一条)。 ### 8.3 接线的四条(与直流轮次比,差别只在第 2、3 条) @@ -344,7 +355,9 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, | STM32(`UB` / `UC` / `IB` / `IC` / `IN`) | 0~3.3 V 单极性 | 无正向意义 | 只在 D5 用:注交流 = **故意触底**,负半周本就钉 0 | 3. **一次只注一路**(D3 除外,它要 `UA` 与 `UN` 两路同源),换表笔前先关输出。端子侧串阻是 **1 k**(R31 / R28 / R19 / R52 …), - STM32 那五路负半周经引脚钳位二极管灌向 VDDA:1.000 Vrms 时峰值电流约 (1.414 − 0.3) / 1 k ≈ **1.1 mA**,在绝对额定内, + STM32 那五路负半周经**片内**钳位 / ESD 路径灌向 VDDA——网表 `docs/硬件平台/V0.1.tel` :228-241 逐条实证这些节点只有 + 「电容 .2 + 串阻 .2 + MCU 脚」三件,**无外置钳位二极管、无隔直电容**,所以这条电流走的是芯片内部路径; + 1.000 Vrms 时峰值电流约 (1.414 − 0.3) / 1 k ≈ **1.1 mA**(按片内钳位压降 0.3 V 估、仓内无实测,只用来论证量级), 但**别往上加幅度**——这种注入只允许出现在 D5 这类"故意削顶"的用例里。 4. **电流通道也是注电压**:`IA` 的注入点同样是电压进 R19 端子侧。这一轮证的是"给定引脚电压算得对不对", 不是"一次侧 1 A 怎么办"——后者要变比,属 `docs/任务规划/数据可信.md` §5 的端子 / 子板变比确认。 @@ -378,6 +391,7 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, | 交叉读数 | 104 `0x4011`(短浮点)应 ≈ 万用表读数,单位是**引脚电压域**(`eng=0`,不是二次值) | | 坑 ① | **别用 `phasor(hb)` 比幅值**——×0.1 定点,1 V 只剩 10 格(§4 尾注同一条) | | 坑 ② | 104 每点 0.5 V(电流 0.005 A)变化死区,生效值在 `App/algo/run_par.c` 的 `[run_par]` 副本:1.000 → 1.100 V **不会**上报变化,那是死区不是坏了;连续扫幅度只看 `meas:` | +| 坑 ③ | **`cal: ua_mv` 与 `meas: ua_mv` 同名不同物**:前者是当拍瞬时码折算(`App/task/adc_task.c:587`),后者是 200 ms 快照里那一周波的基波有效值(`App/task/adc_task.c:605`),50 Hz 下二者可以差很多——`App/task/adc_task.c:595-599` 的原注就是**勿互为判据**,D1 只认 `meas:` | 记录表(每条 10 行取中枢): @@ -443,7 +457,7 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, | 期望(A 档 = 现状) | **就是不出数**:幅值门 `FREQ_AMP_MIN 50.0f`(`App/algo/freq.h:62`,`App/algo/freq.c:231` 拿它与基波有效值比)在引脚电压域恒不过 → `valid=0`、`f=0`、UI `Hz` 显 `--`、104 `0x401D` 上送 IV | | 判据 | **如实报不可测**,而不是报一个看起来对的 50.00。若门限没改却报出 50.00 → 反而是 bug,停下查 `App/algo/freq.c` | | B 档(要用户先点头) | 临时把 `FREQ_AMP_MIN` 由 `50.0f` 降到 `0.5f` → 重编(`@build`,要 build 锁)+ 重烧 → 扫 45.000 / 50.000 / 55.000 Hz 三点,PC 侧判据 0.02 Hz → 跑完**必须还原**,收尾判据见 §8.9 | -| 坑 | 非整 50 Hz 时 `meas:` 会拍频式跳动,属预期 → **扫频读 `freq(hb)`,别读 `meas:`**(`docs/任务规划/数据可信.md` §8 风险表同一条) | +| 坑 | 非整 50 Hz 时 `meas:` 会拍频式跳动,属预期 → **扫频读 `freq(hb)`,别读 `meas:`**(`docs/任务规划/数据可信.md` §8 风险表同一条);测频的取源顺序是先 `UA`(相量实例 C02)再退 `IA`(C01)(`App/algo/freq.c:47`)——本卡注 `UA` 走的就是 UA 源;若哪天只注 `IA`,测频会跟着 IA 走,别以为它锁在 UA | 记录表(A 档): @@ -540,6 +554,7 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, | 撤掉注入后 `rail` 仍非 0 | 锁存语义(sticky),只 `adc_health_rail_init()` / 复位清 → 复位后仍不清**才是** bug | §8.5 的 D5 改法 A | | 读数带与幅度无关的偏置或漂移 | 共地 / 地环 | §8.2 末行 | | `meas: seq` 不涨 | 发布闸门停 → 先查采集链,别判交流 | §8.2 前置门 | +| `health:` 或 `sampling fault:` 亮 | 与注入的波形无关:`adc_health` 的位只吃序号差、错误计数增量与 tick 差(`App/algo/adc_health.c:91-189`)→ 查节拍 / 丢帧,别查发生器 | §8.2 前置门 | | 9 路全 0 | 读作"没数据",不是"输入为零" | §4 尾注 | ### 8.9 收尾判据(走过临时改动的一条都不许留) @@ -574,3 +589,10 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, THD 与谐波方和根 / 温升 EMC 长时间漂移),本节只补一句现状:**样本出口未落地**(录波在飞)→ 本轮所有"波形" 结论都是**标量级**的、不是形状级;要看形状现在只能拿示波器直接看注入点,那只用于确认发生器输出干净与否, **不进判据**。 + +再补一条「别把台架做成功能性重复」的边界:**算法本体的精度已经在纯数字域钉住了**——`test/host/phasor/phasor_test.c` +的 TC-PHASOR-003 与 TC-PHASOR-004(基波 = 峰值 / √2 容差 1 %、注入相角容差 0.5°)和 TC-PHASOR-006(2 / 3 / 5 次 +谐波 1 %),以及 `test/host/freq/freq_test.c` 的扫频判据 J1(45~55 Hz 内 ≤0.02 Hz),都是把已知正弦**直接喂进 +`sample_ring`**,绕过了 `adc_cal` 的折算(那里的幅值是工程量口径,不是引脚 µV)。所以 §8 这一轮真正要证的只有 +「信号走到 `sample_ring` 之前」那一段:发生器刻度与输出端接、前端串阻与滤波电容的幅频、两片转换器的同刻性、 +码到 mV 的折算、出口的量化。这一轮过了**不等于**「算法精度已实测验证」,结论里别那么写。 From fa371bcf11525e15efcab327e4a6991a86ccb4b7 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 19:46:40 +0800 Subject: [PATCH 04/19] =?UTF-8?q?docs(=E6=B3=A8=E5=85=A5=E9=AA=8C=E8=AF=81?= =?UTF-8?q?):=20=E4=BA=A4=E6=B5=81=E8=BD=AE=E6=AC=A1=E6=8C=89=E5=B7=B2?= =?UTF-8?q?=E8=90=BD=E5=9C=B0=E7=9A=84=E5=BD=95=E6=B3=A2=E6=A0=B7=E6=9C=AC?= =?UTF-8?q?=E5=87=BA=E5=8F=A3=E6=94=B9=E6=88=90=E4=B8=A4=E6=A1=A3=E8=B7=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 录波数据面 2026-10-29 并入主干后,本文 §4 的出口表已写"已落地",而 §8 仍写着"主干尚无 / 等段文件 / 样本出口未落地"——同一份文件自相矛盾,本轮全部对齐(19 处改写)。 - §8.1:每条用例分成两个档(周期行标量档 / 段文件定量档);段文件那一行去掉 [计划] 与"主干尚无",D3 与 D6 升入定量档,"临时抬 log_phasors 倍数"降为退路 - §8.5 D3:新增七步定量档动作——UI「手动录波」触发 → 等 wave: 的 seg+1 → FTP 取段 → wave_read.py fft --all → 比同段内 UA(表序 0) 与 UN(表序 3) 的相对相位,容差 2°; 段与发生器无绝对相位锁定(触发点在段内点 159),跨段比相位或拿发生器当绝对基准都不成立 - §8.5 D5:加三把尺子——shape --strict(DC 改法 A)、再加 --clip-superset(极小注入 / 未驱动通道)、不带 strict 只读游程(50 Hz 正弦档);段头 CLIPPED 位来自通道来源、 从不实测,--min-run 的 4 点与固件"连续 20 拍"是两把尺子,不得互相换算 - §8.5 D6:加 fft 谐波定量档(--ch 4 = IA;表序 UA..IN、TEMP 不录),UI 目检降为旁证 - §8.2:设备表加 FTP 取段与触发路径两行(产品构建免改代码只有 UI 手动页;保护启动处 未标定闭锁态;bench 定时自抓在 DTU_BENCH 与 WAVE_BENCH_AUTOCAP 双层编译门内、默认 0); 开机前置门加 wave: 行基线(armed / err / ovf / drop 全 0,每段核 seg+1) - §8.4 抬头加段文件与脚本两行;§8.6 顺序表重排,新增末尾统一取段与跑脚本一步, 合计耗时 110 → 135 min;§8.8 加三行分诊(脚本拒收是正确行为、--strict 先分清尺子、 seg 不涨先查落盘道);§8.9 加"开过 bench 编译门必须还原"一行 - §8.10 归档加段原件与脚本产物(段原件被覆盖就再也拿不回来,先改名再触发下一段), 第 4 条的"被在飞工作树占着"改为"单写者阻塞已解除,仍等用户点头才改判据文档" - §8.11 现状三条改写:出口已成立 / 板级对表本身还没跑过 / 只 320 ms 快照不等于长时间稳定 段文件名一律写 `WAVE.BIN`(NN = 00~99):新增行里出现字面量 `WAVE00` 会被 编号门禁·新增行拦下(字母+数字形态),而它确是固件生成的文件名,不值得为此扩白名单。 纯文档改动,未跑 @build(无可编译内容)。门禁:doc-links --sections-strict rc=0 (104 处引用 / 94% 可判定)、编号门禁 rc=0、表格列数 0 处不一致、无 BOM 与 CR 与乱码。 --- docs/测试说明/ADC前端注入验证流程.md | 113 +++++++++++++++++++++------ 1 file changed, 88 insertions(+), 25 deletions(-) diff --git a/docs/测试说明/ADC前端注入验证流程.md b/docs/测试说明/ADC前端注入验证流程.md index d1167e7f..22037bcf 100644 --- a/docs/测试说明/ADC前端注入验证流程.md +++ b/docs/测试说明/ADC前端注入验证流程.md @@ -13,8 +13,8 @@ > **判据的属主是 `docs/任务规划/数据可信.md` §4.6.2**(D1~D6 验什么、过判据是多少),本文 §8 只写**怎么跑**。 > 两处不一致时分两类裁定:**「验什么、算不算过」从 `docs/任务规划/数据可信.md` §4.6.2**;**「期望值怎么算」从本文**。 > 后者不是抬杠:那边有三处与代码算不拢(D2 的三角系数、D5 的「注 50 Hz 正弦就能造出锁存」、D3 与 D6 在 ×0.1 定点 -> 出口下的分辨率),照它的数字跑会把好板子判成失败。本轮按本文数值跑,更正清单与回写路径见 §8.10 第 4 条;那个 -> 文件正被在飞工作树改(`docs/任务规划/` 是单写者目录),**要不要现在一并改它,等用户裁定**。 +> 出口下的分辨率),照它的数字跑会把好板子判成失败。本轮按本文数值跑,更正清单与回写路径见 §8.10 第 4 条——那个 +> 文件的单写者阻塞已随录波并入解除(2026-10-29),但改判据文档是另一个动作面,**要不要现在改它仍等用户裁定**。 --- @@ -295,8 +295,11 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, ### 8.1 动手前先把出口分辨率算清楚(决定今天能判到哪一档) -这一轮和直流轮次一样**没有样本出口**:板上没有任何"把窗里的点导出来"的通道(§4 那张表就是全部可读的东西), -所有判据都得从**已经算好的标量**读。各出口的实际分辨率如下——这张表决定哪几条今天能判到位: +这一轮和直流轮次**不一样:样本出口已经有了**。2026-10-29 故障录波的数据面并入主干,一次触发就是一段 +`/wave/WAVE.BIN`(`NN` = 00~99 轮转号;8 路 × 320 点原始码值 + 封存瞬间的标定与品质快照),PC 侧 `test/host/wave_read.py` 把它还原成 +周波数、幅值、**相位**、谐波与削顶判定。所以本轮每条用例都有两个档:**周期行标量档**(只读串口 / 104 / UI, +快,不用 FTP)与**段文件对表档**(多一次触发 + 一次取段 + 跑脚本,判得到位)。各出口的实际分辨率如下—— +这张表决定每条用例走哪个档: | 出口 | 打印处 | 分辨率 | 能判 | 判不动 | |---|---|---|---|---| @@ -305,16 +308,16 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, | `phasor(hb) C0x-…-F1: Re=.. Im=.. rms=.. (x0.1)` | `App/task/adc_task.c:209` | **0.1 V,且向下截断** | 有数没数、量级、象限 | ⚠ **D3 的 ≤2° 与 D6 的 ±2 %**:注 1.000 Vrms 时 `rms` 只有 10 格,20 % 的谐波只有 2 格 | | `freq(hb): f=.. valid=.. (x0.01)` | `App/task/adc_task.c:224` | 0.01 Hz | D4 的读数与新鲜度 | — | | UI 交流量页 `Ua/U0/Ia/Ia2/Ia5`(3 位小数)、`Hz`(2 位) | `Drivers/BSP/UI/ui_page_ac.c:47` | **1 mV**(人工目检 + 拍照留证) | D6 谐波幅值、D1 交叉旁证、D4 的 `Hz` 显 `--` | 相位(页面上没有角度) | -| 录波段文件 `/wave/WAVE00.BIN` + [计划] `test/host/wave_read.py` | **主干尚无**(在飞工作树 `task/wave-record` 已做到代码侧) | 原始码值全样本 | 合入后 D3、D6 升级为「PC 独立重算 vs 板上读数」对表 | — | +| 录波段文件 `/wave/WAVE.BIN`(`NN` = 00~99,100 个号轮转;FTP 根下,与 `/log` 平级)+ `test/host/wave_read.py`(`dump` / `fft` / `shape` / `plot`,纯标准库) | **已落地**(2026-10-29 并入 `v2-on-ui`;格式属主 `docs/子系统设计/故障录波设计说明.md` §6.2 与 §6.3) | 一次触发 8 路 × 320 点原始码值;`fft` 给基波 RMS、相位、2 次与 5 次谐波 RMS(mV) | **D3 的 ≤2° 与 D6 的 ±2 % 现在判得动**:PC 独立重算 vs 板上读数对表 | 段与发生器**无绝对相位锁定**(触发点在段内点 159)→ 只能比同段内相对相位;320 ms 快照不等于长时间稳定性 | -**所以这一轮分三档跑**(排序理由见 `docs/任务规划/故障录波.md` §2 末:动态验证真正缺的不是信号源,是**样本出口**; -判据升级口径见 `docs/子系统设计/故障录波设计说明.md` §11): +**所以每条用例按两个档摆**(出口从"缺样本"变成"有段文件"的经过与口径变更,见 `docs/任务规划/故障录波.md` §2 末 +与 `docs/子系统设计/故障录波设计说明.md` §11): | 档 | 条目 | 说明 | |---|---|---| -| **今天判得动** | D1、D2、D4、D5(D5 的接法要按 §8.5 改,原因写在那张卡里) | 判据在 `meas:` / 104 / `freq(hb)` / `rail:` 上分辨率都够 | -| **判得动但要换出口** | D6 | 走 UI 目检档(1 mV),`phasor` 行只作旁证;没有谐波叠加功能就用同一张卡里的分次注入档 | -| **建议等段文件** | D3 | ×0.1 的定点出口判不了 2°:要么接受"90° 跟随"的定性档,要么等 `task/wave-record` 合入后一次判到位。硬要今天判只剩第三条路——临时把 `log_phasors()` 的 `* 10.0f` 改成 `* 1000.0f` 并同处改掉行尾 `(x0.1)`(不改标记会把读数误读 10 倍),跑完按 §8.9 还原。**这条路要不要走,先问用户。** | +| **周期行就判得动** | D1、D2、D4、D5(D5 的接法要按 §8.5 改,原因写在那张卡里) | 判据在 `meas:` / 104 / `freq(hb)` / `rail:` 上分辨率都够,不取段也能收工 | +| **段文件档才判到位** | D3、D6 | 段文件已落地 → 浮点重算,≤2° 与 ±2 % 都是**定量**判据;周期行退为旁证,只在没取到段时走定性档。触发动作与命令写在 §8.5 的 D3 / D6 卡 | +| **要改代码的退路** | D3 抬倍数、D4 B 档 | 段文件落地后 D3 这条基本不必走;D4 B 档(放宽 `FREQ_AMP_MIN`)仍是唯一路子。两者都要重编 + 烧录 + 按 §8.9 还原,**动手前先问用户** | ### 8.2 设备清单与开机前置门 @@ -325,8 +328,10 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, | 万用表 AC 档 | 能读到 mV;**记明型号与响应类型**(真 RMS / 均值响应) | 基准就是它;D2 那一步两种表的读数都与基波期望差很多,不记响应类型就说不清是谁错 | | 板子 | 产品构建(`test/board/test_config.h` 四个 `TEST_SUITE_*` 全注释)+ `@build` 0 错误 0 警告 + 烧录 `-Rst -Run` 与回读校验 | → `stlink-flash` skill 与 AGENTS.md「构建与验证」 | | `[adc_cal]` 状态 | **保持未标定**(`eng=0`),别为"读数好看"先写标定值 | 现在 `meas:` / `phasor` 的量纲是 **ADC 引脚电压 V**(`App/algo/meas.h` 文件头的 A-lite 口径);标定一生效就换成二次值,本节全部期望值作废 | -| 串口 | 全程抓 RX 落盘:用 `serial-assistant` skill(`open` → `clear` → 跑 → 读日志文件或 `log --tail`) | 日志文件就是证据本体,§8.10 归档归档的就是它;**长跑别用 `@test`**——它绑定一轮启动语义(静默 8 s、总超时 30 s、`max_boot_count` = 1,且 forbidden 含 `[ERR]`,而 D5 故意造 `[ERR]`),110 min 的 session 必然超时或误判;daemon 全量写 `state/serial.log`(20 MB 上限自动截头保尾 10 MB)→ `docs/测试说明/自动化测试流程.md` 长时间采集一节 | +| 串口 | 全程抓 RX 落盘:用 `serial-assistant` skill(`open` → `clear` → 跑 → 读日志文件或 `log --tail`) | 日志文件就是证据本体,§8.10 归档归档的就是它;**长跑别用 `@test`**——它绑定一轮启动语义(静默 8 s、总超时 30 s、`max_boot_count` = 1,且 forbidden 含 `[ERR]`,而 D5 故意造 `[ERR]`),约 135 min 的 session 必然超时或误判;daemon 全量写 `state/serial.log`(20 MB 上限自动截头保尾 10 MB)→ `docs/测试说明/自动化测试流程.md` 长时间采集一节 | | 网口(可选) | 要读 104 那一路就插网线、PC 与板同网段(出厂 192.168.1.100),跑现成主站 `test/host/iec104_master.py`(`--ip` / `--port` / `--addr`) | 104 是浮点无量化 + 带品质,是 `meas:` 之外的第二条腿 | +| FTP 客户端 | 走现成 21 端口取段文件(出厂 `[ftp]` 账号,连续 5 次失败锁 60 s) | 段文件是 D3 / D6 定量档的样本出口;段文件名 `/wave/WAVE.BIN`(00~99 轮转),**取完立刻改名归档**,否则下一次触发把它盖掉 | +| 录波触发路径 | 产品构建里唯一**免改代码**的路径 = UI 菜单第 26 叶「手动录波」确认页(`Drivers/BSP/UI/ui_page_wave_manual.c` → `App/task/ui_task.c:605`) | 保护启动上升沿也算一次触发,但保护判据现处**未标定闭锁**态(AGENTS.md「现状」),别指望它自动来段;bench 定时自抓在双层编译门 `DTU_BENCH` 且 `WAVE_BENCH_AUTOCAP` 非 0 之内(默认 0,`App/task/adc_task.c:153`),开它就走 §8.9 的还原判据 | | 共地 | 发生器 LO/GND 与板 AGND **单点**相连 | 不共地量到的是两地之差;现象见 §8.8 | **开机前置门**——接发生器**之前**日志就该长这样,不满足先回 §5 步骤 5.5 查节奏,别往下做: @@ -344,6 +349,8 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, 「phasor 有数」既不说明标定可用、也不说明这一路没在削顶,判读必须配 `vl` / `cl` / `rail` 三个位图一起看; - 注入正弦**不会**点亮 `health:` 或 `sampling fault:` 那几行:`adc_health` 的位只吃序号差、错误计数增量与 tick 差 (`App/algo/adc_health.c:91-189`),与波形幅值无关;它们若亮是节拍 / 丢帧问题,别归因给发生器(§8.8 同一条)。 +- `wave:` 行(`App/task/adc_task.c:782`)基线:`armed=0`、`err=0`、`ovf=0`、`drop=0`,`seg=` 是本开机以来的成段数(**可以是 0**)。 + 本轮每触发一段就核 `seg` +1 且那三个计数不涨;涨了先按 §8.8 的落盘行分诊,别接着往上下注入。 ### 8.3 接线的四条(与直流轮次比,差别只在第 2、3 条) @@ -374,6 +381,8 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, 注入点:路名 ____(端子侧 U5 / 引脚侧外串 ____ Ω) [adc_cal] = 未标定(eng=0)—— 必须是这个状态 基线(未注时):meas: ua_mv=____ un_mv=____ ia_mv=____ rail=0x____ cl=0x____ +段文件(走定量档的用例):文件名 ________ 触发方式 = 手动 / 保护 / bench 取回时刻 ________ +脚本:test/host/wave_read.py(先跑 --selftest:通过 / 否) fft / shape / dump 输出与 plot 图归档在 ________ ``` ### 8.5 逐条执行卡 @@ -440,16 +449,31 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, | 读数 | `phasor(hb) C02-UA-F1: Re=.. Im=..` 与 `C03-UN-F1: Re=.. Im=..`,各取 `atan2(Im, Re)` 再比差值 | | 判据(§4.6.2) | 同相时差值 ≤2°;把发生器(或 B 通道)改 90°,差值跟着走到 ≈90° | | ⚠ 分辨率 | 出口只有 ×0.1 且向下截断:1.000 Vrms 时 Re/Im 各只有 ±10 格,**一格就是 0.1 V** → 角度量化误差最坏 ≈ atan(0.1 / 0.707) ≈ 8°,**≤2° 今天用串口判不出来** | -| 三个选择 | ① 今天只判**定性档**:同相 ≈0°、改 90° 后差值落进 80°~100° 即算跟随,容差写进记录表;② 等录波段文件合入后 PC 独立重算一次判到位(首选,§8.1);③ 临时改 `log_phasors()` 倍数为 ×1000 并改掉行尾 `(x0.1)`,跑完按 §8.9 还原——**先问用户** | +| 三个档 | ① **段文件定量档(首选,零改代码)**:同一次注入触发一段,`test/host/wave_read.py fft` 重算基波相位,比**同段内** `UA`(表序 0)与 `UN`(表序 3)的相位差——浮点重算,不受 ×0.1 量化限制,≤2° 判得动,动作见下面第 2 段;② **定性档**:没取到段时退而用,同相 ≈0°、改 90° 后差值落进 80°~100° 即算跟随,容差写进记录表;③ **抬倍数退路**:临时把 `log_phasors()` 的倍数改成 ×1000 并改掉行尾 `(x0.1)`,跑完按 §8.9 还原——段文件档已落地,这条基本不必走,要走仍**先问用户** | | 红线 | **只许比同片内**:跨转换器无同刻性(V0.1 硬件缺陷)+ STM32 那五路另有 1 拍乒乓滞后 → `docs/硬件平台/ADC通道映射说明.md` §7 | -记录表(定性档): +记录表(定性档;定量档记脚本输出,归档口径见 §8.10 第 1 条): | 发生器相位设定 | C02-UA `Re` / `Im` | C03-UN `Re` / `Im` | 两路 `atan2` 之差 | 结论 | |---|---|---|---|---| | 0° / 0° | | | | | | 0° / 90° | | | | | +**段文件定量档的具体动作(D3 首选,零改代码)** + +1. `UA`(R31)与 `UN`(R28)两路接同一发生器输出(同相),注 **1.000 Vrms / 50.000 Hz**; +2. 稳定 ≥30 s 后,在 UI「手动录波」确认页触发一次;成段有**秒级**延迟(落盘在 defaultTask,卷空闲后一次心跳搬完整段) + → 等串口 `wave:` 行的 `seg=` +1、`armed` 回 0 再去取文件; +3. FTP 取 `/wave/WAVE.BIN`(本轮那一段的确切号),**取完立刻改名**——100 个号轮转会被覆盖; +4. 主机侧跑:`& '.venv\Scripts\python.exe' test/host/wave_read.py fft WAVE.BIN --all` + —— 每个整周波给一行:基波 RMS、相位、2 次与 5 次谐波 RMS(单位 mV,系数镜像 `App/algo/phasor.c`); +5. 判据:`UA`(`--ch 0`)与 `UN`(`--ch 3`)在**同一周波**内的相位差——同相注入应 ≈0°,把发生器(或 B 通道)改 90° 后应 ≈90°, + 容差 ±2°(`docs/任务规划/数据可信.md` §4.6.2 D3); +6. ⚠ 段的**绝对**相位与发生器没有锁定关系(触发瞬间固定落在段内点 159),所以**只能比同段内两路的相对差**; + 跨段比相位、或拿发生器相位当绝对基准都不成立(段内点序见 `docs/子系统设计/故障录波设计说明.md` §6.2); +7. 交叉旁证:同一段先 `dump`(逐字段 + 双 CRC16,看封存瞬间的标定与品质快照),再 `plot --ch 0`(CRC 不符或零长度它 + **必须拒收**而不是画出半张图);板上 `phasor(hb)` 的 `Re` / `Im` 象限应与脚本给的象限一致(×0.1 只够核象限)。 + #### D4 测频(默认 A 档 = 判"如实报不可测") | 项 | 内容 | @@ -499,7 +523,21 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, | 2 | DC −1.00 V(改法 A) | | | | | | | | 3 | 2.0 Hz / 1.000 Vrms(改法 B) | | | | | | | -#### D6 谐波轨(**用 UI 目检档**) +**段文件档的两把尺子(现在能把"来源派生 vs 实测"一次核清)** + +`shape` 拿**实测**贴轨判定与段头 `clip_mask` 对表;而段头那一位**来自通道来源、从不实测**(`test/host/wave_read.py` +文件头的实证与本卡上面的 `cl=0xE6` 同源,两处互证)→ 三种注入分别配三把尺子: + +| 注入 | 命令 | 期望 | +|---|---|---| +| 改法 A(`UB` 注 DC −1.00 V 保持 ≥5 s) | `shape WAVE.BIN --strict` | 实测贴底落进 bit1;段头静态已含 bit1 → 等式成立。**实测冒出段头没有的位**才算失败 = 有别的路被推到轨上,先查接线 | +| 极小注入 / 未驱动通道 | 同上再加 `--clip-superset` | 换成"段头覆盖实测"这把尺子:容忍"封存了但本轮没贴轨";实测有、段头没有仍然失败 | +| 原接法(50 Hz 正弦注 `UB`) | `shape WAVE.BIN`(不带 `--strict`) | 只读逐通道贴轨游程:正弦过零必回中带,`--min-run` 默认 4 也可能判不出贴轨——**别把"没判"读成 bug** | + +`--min-run`(连续几点算贴轨)与固件的"连续 20 拍"是**两把不同的尺子**,别互相换算;固件侧结论仍以 `rail:` / `edge=` +位图为准(本卡上半段)。 + +#### D6 谐波轨(**段文件 `fft` 定量档 + UI 目检旁证**) | 项 | 内容 | |---|---| @@ -518,6 +556,19 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, |---|---|---|---|---|---|---| | 基波 0.800 Vrms + 二次 0.160 + 五次 0.160 | | `Ia2=` / `Ia5=` | 0.160 / 0.160 | | | | +**段文件定量档(D6 的谐波现在判得到位)** + +同一次注入(基波 0.800 Vrms + 二次 0.160 + 五次 0.160,峰值预算见上表)触发一段、FTP 取回后跑: + +```text +& '.venv\Scripts\python.exe' test/host/wave_read.py fft WAVE.BIN --ch 4 --all +``` + +`fft` 逐整周波给**基波 RMS + 2 次与 5 次谐波 RMS**(mV,浮点重算),±2 % 在这条档是定量判据;`--ch` 是段内通道表序, +`IA` = 4(表序 `UA UB UC UN IA IB IC IN`,TEMP 不录,`App/algo/wave_ring.h:216`)。UI 目检与 7003 截图降为**旁证**—— +它证的是"板上 `Ia2` / `Ia5` 与脚本算的是同一件事"(差值应在显示位数之内),不是替代判据。THD 与方和根仍不在本轮, +口径见 `docs/任务规划/数据可信.md` §4.6.5。 + ### 8.6 顺序与耗时(一次台架 session 跑完) | 序 | 做什么 | 预计 | 备注 | @@ -525,12 +576,16 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, | 0 | 前置门 + 基线 30 s | 10 min | §8.2,不满足就停 | | 1 | D1(含 0.5 / 0.2 Vrms 加做) | 15 min | 主用例 | | 2 | D2(方波 / 三角,峰值对齐) | 15 min | 抬头里记万用表响应类型 | -| 3 | D6(含无谐波叠加功能时的三段退路) | 20 min | UI 目检 + 拍照 | -| 4 | D3 定性档(或整条等段文件) | 10 min | 三个选择见 §8.5 | +| 3 | D6:段文件 `fft` 定量档 + UI 目检旁证(同一次注入触发一段) | 25 min | 命令见 §8.5 的 D6 卡 | +| 4 | D3:段文件相对相位档(`UA` / `UN` 同源,再移 90°) | 20 min | 定性档只在没取到段时退用 | | 5 | D4 A 档 | 5 min | 判"如实报不可测" | -| 6 | D5:50 Hz 档 + 改法 A + 改法 B + 复位复观 | 25 min | **单独一份日志**,判读规则见 §8.7 | +| 6 | D5:50 Hz 档 + 改法 A + 改法 B + 复位复观(每档各触发一段) | 30 min | **单独一份日志**,判读规则见 §8.7 | | 7 | 同一份接线下把 104 交叉读数复跑一遍 | 15 min | `test/host/iec104_master.py` | -| — | (需用户点头)D4 B 档 / D3 改倍数档 | +40 min | 各含一次重编、烧录与还原 | +| 8 | 取段与脚本产物归档(FTP `get` + `dump` / `fft` / `shape` / `plot`) | 15 min | 每段取完立刻改名,100 号轮转会覆盖 | +| — | (需用户点头)D4 B 档 / D3 抬倍数档 | +40 min | 各含一次重编、烧录与还原;段文件落地后 D3 这条基本不必走 | + +合计约 **135 min**(不含"需用户点头"那两档)。取段与跑脚本集中在末尾第 8 行统一做,中间各条只负责"触发 + 让 +`seg=` +1",免得一次 session 里反复插拔网线。 ### 8.7 `[ERR]` 的判读规则(D5 单独跑、单独归档) @@ -558,6 +613,9 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, | `meas: seq` 不涨 | 发布闸门停 → 先查采集链,别判交流 | §8.2 前置门 | | `health:` 或 `sampling fault:` 亮 | 与注入的波形无关:`adc_health` 的位只吃序号差、错误计数增量与 tick 差(`App/algo/adc_health.c:91-189`)→ 查节拍 / 丢帧,别查发生器 | §8.2 前置门 | | 9 路全 0 | 读作"没数据",不是"输入为零" | §4 尾注 | +| `wave_read.py` 报错退出 / 拒绝出图 | **这是正确行为**:CRC 不符或零长度它必须拒收而不是画出东西 → 重取一份、核对是不是搬到一半的半成品;仍拒收就 `dump` 看两处 CRC16 哪个不符 | `test/host/wave_read.py` 文件头 | +| `shape --strict` 说实测与段头不等 | 先分清尺子:段头 `CLIPPED` 位来自**通道来源、从不实测**;DC 偏置 / 极小注入 / 未驱动通道应改用 `--clip-superset` | §8.5 的 D5 段文件档 | +| 触发后 `seg=` 不涨或 `drop` / `ovf` 涨 | 落盘道在 defaultTask 卷空闲后才搬段,先等几秒;仍不涨查 `wave:` 行三计数与 `/wave` 目录可写(`[ftp]` 写权限) | `App/task/adc_task.c:782` | ### 8.9 收尾判据(走过临时改动的一条都不许留) @@ -565,6 +623,7 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, |---|---| | D4 B 档:`FREQ_AMP_MIN` 50.0f → 0.5f(`App/algo/freq.h:62`) | 还原 + `@build` 0 错误 0 警告 + **`git diff` 里 `App/algo/freq.h` 必须为空** + 在 `docs/任务规划/数据可信.md` §7 #8 那行留一句"本轮台架已还原" | | D3 改倍数:`log_phasors()` 的 `* 10.0f` → `* 1000.0f`(`App/task/adc_task.c:209`) | 同上,`git diff` 里 `App/task/adc_task.c` 必须为空 | +| 为 bench 定时自抓开过编译门(`DTU_BENCH` + `WAVE_BENCH_AUTOCAP` 非 0,`App/task/adc_task.c:153`) | 还原成默认 0 + `@build` 0 错误 0 警告 + `git diff` 里 `App/task/adc_task.c` 必须为空(产品镜像里这段必须零存在) | | 只动接线与发生器、没动代码 | `git status` 除归档目录外干净 | | 全程 | `test/board/test_config.h` 四个套件宏保持全注释(产品构建);烧录仍须 `-Rst -Run` + 回读校验 | @@ -572,25 +631,29 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, 1. **证据归档** → `docs/测试说明/boot_log_data/`,沿用现有命名惯例(日期_主题_类型.txt): `20261104_ac_d1_meas_log.txt`、`20261104_ac_d5_rail_log.txt`、`20261104_ac_iec104_log.txt`;UI 目检照片同目录 - (该目录若不收图,就在抬头里写清照片存放位置)。 + (该目录若不收图,就在抬头里写清照片存放位置)。**走段文件定量档的用例另加两样**:取回的段原件(原始字节, + `20261104_ac_d3_wave.BIN`)与脚本输出(`fft` / `shape` / `dump` 三份文本 + `plot` 的图)——定量档的证据本体是 + **那一段原始码值**,日志只是旁证;段原件一旦覆盖就再也拿不回来,先改名再触发下一段。 2. **实测结论回写** → `docs/硬件平台/ADC通道映射说明.md`,在 §8.1(直流轮次)之后**新开一节**—— 任务卡 `docs/任务规划/数据可信.md` §5 的 DoD 就是这么分的。本文只留"怎么跑",**不抄实测数**。 3. **本文回写** → 只在 §8.1 那张表下加一行指针级别的句子("YYYY-MM-DD 已按本节跑通,实测见 …"); 不在本文建第二份结论表,否则两份必然漂移(`docs/文档规范.md` 同一事实只在一处详述)。 -4. **三处要更正 `docs/任务规划/数据可信.md`**(本轮**没动**那个文件:它正被在飞工作树 `task/wave-record` 修改, - 而 `docs/任务规划/` 是单写者目录;等它合入后一次性改): +4. **三处要更正 `docs/任务规划/数据可信.md`**(那个文件本轮仍**没动**:`task/wave-record` 已于 2026-10-29 并入 + `v2-on-ui`,单写者目录的阻塞条件已消失;但改判据文档是另一个动作面,仍等用户点头再动): - §4.6.2 D2 的三角期望值「峰值 / √6」→ 0.573 ~ 0.578(§8.5 的 D2 卡); - §4.6.2 D5 的"注 50 Hz 正弦 → 连续 20 拍锁存"按代码做不到,改法 A / B 见 §8.5; - - §4.6.2 D3 / D6 的判据在 ×0.1 定点出口下分辨率不足 → 要么挂到录波段文件,要么在判据里明写"定性档"。 + - §4.6.2 D3 / D6 的判据在 ×0.1 定点出口下分辨率不足 → 段文件出口现已落地,判据应明写"定量档走段文件、定性档走 `phasor(hb)`"(本文 §8.5 的 D3 / D6 卡已按此写); 5. **销项** → `docs/任务规划/数据可信.md` §2.7 与 §6 卡片表里这一条按登记簿「完成即销项」处理,属 Ask first, 须用户明确指示后再做。 ### 8.11 这一轮证不到(别让读数长得像证据) `docs/任务规划/数据可信.md` §4.6.5 那张五行表整条适用(端子二次值 / 精度等级 / 跨转换器相位与功率与对称分量 / -THD 与谐波方和根 / 温升 EMC 长时间漂移),本节只补一句现状:**样本出口未落地**(录波在飞)→ 本轮所有"波形" -结论都是**标量级**的、不是形状级;要看形状现在只能拿示波器直接看注入点,那只用于确认发生器输出干净与否, -**不进判据**。 +THD 与谐波方和根 / 温升 EMC 长时间漂移)只适用,本节按出口已落地改三条现状(2026-10-29 录波数据面并入主干之后): +**样本出口已成立**(`/wave/WAVE.BIN` + `test/host/wave_read.py`)→ D3 / D6 走定量档时结论是**形状级**,不再只是标量级; +但**板级那一轮对表本身还没跑过**(卡片判据见 `docs/任务规划/故障录波.md`),本轮跑出来的就是第一批实测; +段与发生器**没有绝对相位锁定**(触发瞬间只钉在段内点 159)→ 能核的是同段内两路的**相对**相位与极性, +全局相角基准仍证不到;一次触发只有 320 点 = 320 ms,**不等于长时间稳定性**。示波器仍旧只作旁证、不进判据。 再补一条「别把台架做成功能性重复」的边界:**算法本体的精度已经在纯数字域钉住了**——`test/host/phasor/phasor_test.c` 的 TC-PHASOR-003 与 TC-PHASOR-004(基波 = 峰值 / √2 容差 1 %、注入相角容差 0.5°)和 TC-PHASOR-006(2 / 3 / 5 次 From 557f1d042ac21bfd491374d36da9934eae17b342 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 20:04:24 +0800 Subject: [PATCH 05/19] =?UTF-8?q?docs(=E8=AE=A1=E6=95=B0):=20=E5=BD=95?= =?UTF-8?q?=E6=B3=A2=E8=90=BD=E5=9C=B0=E5=90=8E=E7=9A=84=E5=85=A8=E4=BB=93?= =?UTF-8?q?=E8=AE=A1=E6=95=B0=E5=90=8C=E6=AD=A5=EF=BC=88=E5=A5=97=E6=95=B0?= =?UTF-8?q?/=E9=92=A9=E5=AD=90/=E9=BB=84=E9=87=91/=E8=8F=9C=E5=8D=95/?= =?UTF-8?q?=E5=AD=97=E5=BA=93=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 机器面先定真值,再逐处对齐文档;带日期戳的取数行只加「当时」不改数。 六个数(每条两个以上独立机器源互证) - 主机单测套数 41 → 44(find test/host -name run_unit_test.py / 覆盖率总闸 --list / 基线 modules 条目三源一致;目录数 46 = 44 + common + __pycache__,两个数分开写) - 有专属钩子 22 根(YAML 解析 -unit-test 后缀)⇒ 无专属钩子 19 → 22 套,并把 wave_ring / wave_store / wave_trig 逐名列进门禁台账 §6 第 4 条,18 / 19 / 22 三个数 改标「裁定当时 / 2026-10-28 / 2026-10-29 现值」 - 内容页黄金参考 60 → 66 张(simulator/tests/ref/shot_*.bin 实数 / PAGES 条目 66, 其中 2 个分页 → 页面 64);钩子显示名与门禁索引表成对改 - 菜单 42 → 43 行(9 菜单 + 34 叶)、已绑 28 → 30、未绑 4(ui_menu_tab.h 宏 + g_menu_tab[] 末行 / ui_page_registry.h 的 UI_PAGE_MAP_COUNT + #page= 计数剔示例行) - 中文字库 218 → 224 唯一字、313 → 319 字形(清单唯一码点 / .bitmap_index 条目数 含 notdef;224 + 95 = 319 成立) - 钩子口径 52 声明 / 49 提交期实跑:只核不改,核对 AGENTS 与 YAML 解析一致 Lead 追加四处(wave-algo 按「AGENTS 只核不改」的指令交回我裁的部分) - AGENTS.md 提交门禁节:41 套 → 44 套(两处)、19 套 → 22 套(两处,并把「与录波 三套」写进括注)、ui-golden 条目 60 张 → 66 张;52 / 49 的口径叙述与历史括号原样 - scripts/board_ui_nav.py 的自述 docstring:60 张 → 66 张 - test/host/sys_log_mcu/sys_log_mcu_test.c 注释:全量门禁 41 套 → 44 套(纯注释) - 产品需求 FR-UI-2:需求原文「42 节点」保留不动,在实现行注「2026-10-29 实到 43 节点,42 是 V0.1 立项数」——需求基线不由实现方回改 刻意不动的(冻结面) - 带日期戳的实测行(自动化测试流程、字库差集门禁、覆盖率口径里的 67 s / 25 s / 45 s / ~105 s 四个耗时)、保留变更记录的文档历史行、页面补齐规划里自写「完成即 冻结」的立项快照表、2026-10-28 那份分工说明、级联框总览.svg(42 节点时代所画, 无生成器,重画另开一笔)、三处伪命中(字体数据行、芯片手册、m2/m3 数据目录) 自查(真实退出码):check_doc_links.py --all RC=0(215 个文档、章节引用可判定 80 %, 合并后第一次全库零红);check_generated_fresh.py RC=0(22 张图 17 一致 / 5 条既有 BASELINE 豁免,生成物零改动);编号门禁 add 后复扫 RC=0;17 + 4 个改动文件字节体检 全绿(无 BOM / LF / 无 U+FFFD / 无尾随空白 / 单个尾换行)。 --- .dsh/skills/font-regen/SKILL.md | 2 +- .dsh/skills/lvgl-simulator/SKILL.md | 2 +- .dsh/skills/stale-doc-count-sweep/SKILL.md | 16 ++++++------ .pre-commit-config.yaml | 8 +++--- AGENTS.md | 2 +- docs/UI设计/UI_设计架构.md | 4 +-- docs/UI设计/菜单页面设计.md | 29 ++++++++++++---------- docs/UI设计/页面扩充详细方案.md | 2 +- docs/UI设计/页面补齐规划.md | 7 +++--- docs/产品需求/产品需求设计文档.md | 11 +++++--- docs/代码规范/pre-commit使用说明.md | 4 +-- docs/测试说明/主机单测覆盖率口径.md | 8 +++--- docs/编号规范.md | 2 +- docs/门禁说明/README.md | 2 +- docs/门禁说明/host-unit-tests.md | 18 ++++++++------ scripts/README.md | 2 +- scripts/board_ui_nav.py | 2 +- simulator/README.md | 2 +- simulator/tests/check_content_pages.py | 4 +-- test/host/sys_log_mcu/sys_log_mcu_test.c | 2 +- 20 files changed, 71 insertions(+), 58 deletions(-) diff --git a/.dsh/skills/font-regen/SKILL.md b/.dsh/skills/font-regen/SKILL.md index bac33887..7ace06c2 100644 --- a/.dsh/skills/font-regen/SKILL.md +++ b/.dsh/skills/font-regen/SKILL.md @@ -34,5 +34,5 @@ python tools/font_gen/extract_cn.py # 扫描 UI 源码字符串 → - `requirements.txt` 必须**纯 ASCII**(Windows 下 pip 按 GBK 解码,中文注释 → UnicodeDecodeError) - 生僻字(宋体可能无字形)→ **写码前先手动验证字形**,避免"代码写完才发现是空白" - 生成成功后跑 `ui-verify` skill 全回归;黄金参考因字形变化而 FAIL 属预期 → `check_content_pages.py --update` 刷新 -- 字数变化时同步更新文档中的字符集计数(当前 218 汉字/符号 + ASCII 0x20-0x7E,共 313 字形;2026-10-28 实测。历史上 119/214 → 179/274 → 185/280 均为当时快照):`UI_中文字体生成与配置.md`、`UI_界面制作流程.md` C3、`UI_设计架构.md`、`simulator/README.md` 已知坑 #4 +- 字数变化时同步更新文档中的字符集计数(当前 **224** 汉字/符号 + ASCII 0x20-0x7E,共 **319** 字形;2026-10-29 实测:`ui_cn_chars.txt` 唯一码点 224、`ui_font_cn_12.c` 的 `.bitmap_index` 320 条含 notdef → 319 字形。历史上 119/214 → 179/274 → 185/280 → 218/313 均为当时快照):`UI_中文字体生成与配置.md`、`UI_界面制作流程.md` C3、`UI_设计架构.md`、`simulator/README.md` 已知坑 #4 - **新增字低于既有最小码位**(如 Ⅰ U+2160)会下探稀疏 cmap 起点并吞掉单码位补回条目(实测 ▶ 箭头全变方框而旧版 coverage 全绿)——`make_font.py`(2026-10-25 起)按 LVGL 首命中语义插入/守卫/校验,扩字后模拟器像素回归**必跑** diff --git a/.dsh/skills/lvgl-simulator/SKILL.md b/.dsh/skills/lvgl-simulator/SKILL.md index f40734fa..b703fefb 100644 --- a/.dsh/skills/lvgl-simulator/SKILL.md +++ b/.dsh/skills/lvgl-simulator/SKILL.md @@ -33,7 +33,7 @@ PC 上编译运行与 MCU **同一份** UI 源码(CMake + MSVC + SDL2,x64, ## UI 改动回归 → 用 `ui-verify` skill(勿在此零散手跑) -5 步门禁 = 构建 → `check_menu_rows.py`(菜单 8 状态像素 + 池基线)→ `check_content_pages.py`(文本断言 + 60 张黄金参考 + 往返池)→ `@build` → `coding-standard`。 +5 步门禁 = 构建 → `check_menu_rows.py`(菜单 8 状态像素 + 池基线)→ `check_content_pages.py`(文本断言 + 66 张黄金参考 + 往返池)→ `@build` → `coding-standard`。 RemoDispBus 推流回归单独跑:`.venv\Scripts\python.exe simulator/tests/check_remo_sink.py`。 黄金参考 `tests/ref/*.bin` **随仓库入库**(`.gitignore` 在全仓 *.bin 兜底之后又显式反选,2026-10-22 起;缺基准 = 新克隆跑 `ui-golden` 必 FAIL):不入库的只是派生目检图 `ref/*.png`,换机后先 `check_content_pages.py --update` 生成,再 `regen_ref_png.py` 出目检 PNG。 diff --git a/.dsh/skills/stale-doc-count-sweep/SKILL.md b/.dsh/skills/stale-doc-count-sweep/SKILL.md index b95b2879..faee5ccb 100644 --- a/.dsh/skills/stale-doc-count-sweep/SKILL.md +++ b/.dsh/skills/stale-doc-count-sweep/SKILL.md @@ -6,20 +6,20 @@ whenToUse: 某个计数/数字变了要跨 md/yaml/py/txt 同步、用户说「 # 陈旧计数全仓同步 -适用:**一个事实数变了**(黄金参考 27→40→47→53→60、已绑叶数 22→28、字库 185/280→218/313、钩子 51→52→51…),仓库里多处引用它的当前态表述要跟着变,而历史记录、同数异义、带日期戳的测量**不能**跟着变。 +适用:**一个事实数变了**(黄金参考 27→40→47→53→60→66、已绑叶数 22→28→30、主机单测套数 28→33→41→44、无专属钩子套数 18→19→22、字库 185/280→218/313→224/319、菜单节点行数 42→43、钩子 51→52→51→52…),仓库里多处引用它的当前态表述要跟着变,而历史记录、同数异义、带日期戳的测量**不能**跟着变。 -本仓已跑过四轮,照序列复跑、别临场重新发明分类:`b24b2b5f`(27→40,13 文件)、`31ca4a1f`(40→47,11 文件)、UI 补齐批(`94b0d790` 落地 → `cbb4fdb8` 产品需求同步 → `11d60057` 架构总览补漏 → `6daa01ae` 字体**口径**更正)、钩子一增一减(`1668842d`,52→51)。 +本仓已跑过四轮,照序列复跑、别临场重新发明分类:`b24b2b5f`(27→40,13 文件)、`31ca4a1f`(40→47,11 文件)、UI 补齐批(`94b0d790` 落地 → `cbb4fdb8` 产品需求同步 → `11d60057` 架构总览补漏 → `6daa01ae` 字体**口径**更正)、钩子一增一减(`1668842d`,52→51)、**第五轮 2026-10-29 录波落地**(套数 41→44、无专属钩子 19→22、黄金 60→66、菜单 42→43 节点 / 33→34 叶、已绑 28→30、字库 218/313→224/319;改动面含 `.pre-commit-config.yaml` 显示名与 `docs/门禁说明/README.md` 成对、三个 skill 正文)。 ## 各计数的权威口径速查(本仓实测值,动手前先对表) | 计数 | 真值来源(**至少两处互证**) | 已知伪命中 / 换算 | |---|---|---| -| 内容页黄金参考**张数** | `simulator/tests/ref/` 下 `shot_*.bin` 实数(实测 60)= `simulator/tests/check_content_pages.py` 的 `PAGES` 条目数(实测 60) | 分页页要**单独再拍一张**(id 加 `_p2`,如 `runreport_p2`)→ **张数 = 页面个数 + 分页多出的张数**;两个数分开报,不许互相冒充(53 张时代的一个 12 行分页页会让 55 页变 56 张这类差 1) | -| 已绑内容页**叶数** | `Drivers/BSP/UI/ui_page_registry.h` 的 `#define UI_PAGE_MAP_COUNT 28u`(由 `tools/menu_gen/gen_menu.py` 生成) | `grep -c '#page=' ui_menu_tree.txt` 得 **29**:第 5 行是菜单文件**头部注释里的示例行** → 剔掉注释/模板行后的 28 才是真值 | +| 内容页黄金参考**张数** | `simulator/tests/ref/` 下 `shot_*.bin` 实数(2026-10-29 实测 **66**)= `simulator/tests/check_content_pages.py` 的 `PAGES` 条目数(实测 **66**,其中 2 个 `_p2` 分页 → 页面 64) | 分页页要**单独再拍一张**(id 加 `_p2`,如 `runreport_p2`)→ **张数 = 页面个数 + 分页多出的张数**;两个数分开报,不许互相冒充(53 张时代的一个 12 行分页页会让 55 页变 56 张这类差 1) | +| 已绑内容页**叶数** | `Drivers/BSP/UI/ui_page_registry.h` 的 `#define UI_PAGE_MAP_COUNT 30u`(由 `tools/menu_gen/gen_menu.py` 生成;2026-10-29 录波道绑两叶后 28→30) | `grep -c '#page=' ui_menu_tree.txt` 得 **31**:第 5 行是菜单文件**头部注释里的示例行** → 剔掉注释/模板行后的 30 才是真值 | | 提交**钩子数** | YAML 解析后的条目数(实测 **51**):自查命令见 `docs/门禁说明/README.md` §2 首条;与 AGENTS.md「共 N 个钩子」、该目录 §2 标题**三数同源** | `grep -c` 配置里的声明行 = 52(多算一条**被注释掉的** clang-format 候选);在配置**注释里复述那个 grep 字面量**会让文本计数再虚高 1 | -| 字库**唯一字数** | `ui_cn_chars.txt` 唯一码点数(实测 218,其中 ASCII 区 **0** 个) | `make_font.py` 的 `-r 0x20-0x7E` 是 ASCII 子集参数,**不算进字数** | -| **字形数** | `Drivers/BSP/UI/ui_font_cn_12.c` 里 `.bitmap_index` 出现次数 **314 条含 notdef → 313 字形**;换算式 = 218 汉字/符号 + ASCII 95 = 313 | 直接报 314 就是把 notdef 当字形;文档必须写出「不含 ASCII」「含 notdef 共 314 条」这类口径句 | -| 菜单**节点数 / 叶子数** | `ui_menu_tree.txt` 头部规则:缩进即层级(每级 2 空格)、`#` 开头为注释、空行忽略;叶子 = **不存在更深缩进后继行**的行 | 42 是节点行数、33 才是叶子;`未绑 = 待做 + 不做` 要与分类表逐项闭合 | +| 字库**唯一字数** | `ui_cn_chars.txt` 唯一码点数(2026-10-29 实测 **224**,其中 ASCII 区 **0** 个) | `make_font.py` 的 `-r 0x20-0x7E` 是 ASCII 子集参数,**不算进字数** | +| **字形数** | `Drivers/BSP/UI/ui_font_cn_12.c` 里 `.bitmap_index` 出现次数 **320 条含 notdef → 319 字形**;换算式 = 224 汉字/符号 + ASCII 95 = 319 | 直接报 314 就是把 notdef 当字形;文档必须写出「不含 ASCII」「含 notdef 共 314 条」这类口径句 | +| 菜单**节点数 / 叶子数** | `ui_menu_tree.txt` 头部规则:缩进即层级(每级 2 空格)、`#` 开头为注释、空行忽略;叶子 = **不存在更深缩进后继行**的行 | 43 是节点行数、34 才是叶子(2026-10-29 现值;42 / 33 是此前时点);`未绑 = 待做 + 不做` 要与分类表逐项闭合 | ## Steps @@ -61,7 +61,7 @@ whenToUse: 某个计数/数字变了要跨 md/yaml/py/txt 同步、用户说「 提交前逐条打勾;回报时**逐处列文件 + 命中数断言**,不要笼统说「已全部更新」。 - [ ] 每个数都写了**两个独立来源**且数值一致(`ref/shot_*.bin` 实数 = `PAGES` 条目数;`UI_PAGE_MAP_COUNT` = 剔伪命中后的 `#page=` 计数),并在结论里贴出该数字与伪命中取舍理由 -- [ ] 字形数说明**是否剔除 notdef**并给出条目数→字形数的换算(314 − 1 = 313);字数说明**是否含 ASCII**,且该口径句与实测一致 +- [ ] 字形数说明**是否剔除 notdef**并给出条目数→字形数的换算(条目数 − 1 = 字形数,2026-10-29 现值 320 − 1 = 319);字数说明**是否含 ASCII**,且该口径句与实测一致 - [ ] 分类表逐条列出:改了哪些当前态、哪些历史行保留、哪些误报不动(附理由)、哪些只加「当时」、哪些属**口径错**并已另起一笔 - [ ] 替换脚本打印了每处 `old -> new xN` 且每处 `assert` 通过(无半落地;报错时已 `git diff --stat` 确认零写入) - [ ] 命中面覆盖 `.pre-commit-config.yaml` 钩子 `name:`、AGENTS.md、`docs/**`、`scripts/**`、`.dsh/skills/*/SKILL.md`,逐条在改动清单里出现 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3261493a..0bad2054 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -649,7 +649,7 @@ repos: # 疑似 MSBuild 节点争用;此后 3 次干净树连跑均绿。判据:单次失败先复跑一次再看, # 连续两次失败才当真(不许直接 --no-verify 绕过)。 - id: ui-golden - name: UI 像素回归(菜单 8 状态 + 内容页 60 张 黄金参考) + name: UI 像素回归(菜单 8 状态 + 内容页 66 张 黄金参考) entry: python scripts/check_ui_golden.py language: system files: ^(Drivers/BSP/UI/|App/task/ui_task\.(c|h)$|simulator/(tests|sim|src)/|simulator/main\.c$|simulator/CMakeLists\.txt$|simulator/lv_conf\.h$|scripts/check_ui_golden\.py$|\.pre-commit-config\.yaml$) @@ -735,7 +735,8 @@ repos: # P1 新增的 12 套不在任何提交钩子里。原先"再加一份全量插桩等于同一件事做两遍" # 这条理由到现在已经**反了**(现状是有一遍没做),所以必须挂;缺口此前如实登记在 # docs/测试说明/主机单测覆盖率口径.md §8。 - # 为什么挂这一根而不是补 12 个 per-module 钩子:本钩子跑**全部 28 套**,并且守 §5 的 + # 为什么挂这一根而不是补 12 个 per-module 钩子:本钩子当时跑**全部 28 套**(现值 44 套,台账见 + # docs/门禁说明/host-unit-tests.md §6 第 4 条),并且守 §5 的 # 回退台账(某套被删、或某文件的已覆盖行变少 → [REGRESS] 直接红)。这是 per-module # 钩子做不到的——删掉一套测试,它那根钩子只会因为"没有文件要检查"而静默跳过。 # ⚠ FAIL 语义(用户裁定 2026-10-24):找不到 mscoverage 时脚本退出码 2 → 本钩子**失败**, @@ -745,7 +746,8 @@ repos: # 口径 / 分母 / 基线 / 盲区详解都在 docs/测试说明/主机单测覆盖率口径.md,本处只留接线事实。 # 触发面**含门禁自身**(.pre-commit-config.yaml):不带自身就等于谁改坏这根闸的 files: # 模式,它就再也不跑且无人察觉(同 lv-conf-sync / bsp-err 的既有做法)。 - # ⚠ stages: [manual](2026-10-28 用户裁定):本钩子跑光全部 41 套单测(实测 ~105 s, + # ⚠ stages: [manual](2026-10-28 用户裁定):本钩子跑光全部 44 套单测(41 套时实测 ~105 s,套件数 + # 涨了耗时就涨,44 套这轮未重测耗时; # 占一次带代码提交耗时的三分之二)→ 不压在提交期,由**远端 CI** 强制 # (.gitea/workflows/ci.yml 的「手动阶段门禁(覆盖率总闸 + 全量 cppcheck)」步),本地等价入口 = # scripts/ci_local.py(manual 段);手工复跑: diff --git a/AGENTS.md b/AGENTS.md index e37f9f11..cd4a3859 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,7 +93,7 @@ ## 提交门禁(pre-commit) -- 任一失败即拦截,共 **52 个钩子**(**口径 = YAML 解析后的 hook 条目数**:自查命令与「声明 / 文档 / 实跑」三数同源核对见 `docs/门禁说明/README.md` §2 首条——⚠ 直接 grep 配置里的声明行会多算一条**注释掉的**候选钩子(比口径多 1),不是口径;**配置注释里也别复述那个 grep 字面量**,它会让文本计数再虚高 1(2026-10-27 实测踩过);加一条钩子就同批改 AGENTS 与门禁索引两处计数。2026-10-25 道E 连加 `bom-guard` / `setview-check` / `uinav-selftest` 三条,2026-10-26 加 `generated-fresh` 一条,2026-10-27 加 `nested-comment` 一条;2026-10-28 加 `id-check-new` 一条并同批退役 `id-check`(口径 52 → 51:编号门禁换牙——新的只查新增行、零语料,旧的八项全库校验连语料一起移入 `scripts/archive/id_gate/`,路线见 `docs/编号规范.md` §8.5);**每种门禁一篇详解(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径)→ `docs/门禁说明/README.md` 索引**;通用机制与整体口径 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `id-check-new`(**编号门禁**:只查 `git diff` 新增行里有没有未登记形态的「字母+数字」标识,零语料、零维护;白名单是前缀级两张表,见 `docs/门禁说明/id-check-new.md`。旧的八项全库校验 `id-check` 已于 2026-10-28 退役,脚本与两份语料移入 `scripts/archive/id_gate/`,只在手工复查存量时才跑;退役路线见 `docs/编号规范.md` §8.5)+ `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + `bom-guard` / `setview-check` / `uinav-selftest`(2026-10-25 道E 那三条:前半句的加钩流水里有它们、逐项枚举里漏了,补上后本行各项相加才等于 52)+ 主机侧单测 **22 根专属钩子**(`-unit-test` 后缀 = 有独立 `files:` 触发面、需要秒级归因的那个子集;2026-10-25 增补 adc-cal / prot-dim / vol-idle-sync / ui-data / meas / run-par 六根)——**这是口径不是缺口**:`test/host/` 实测 41 套带 `run_unit_test.py`,其余 **19 套**(含 `test/host/ui_sink`)没有专属钩子,但每次 CI / `scripts/ci_local.py` 都被 `test-coverage` 总闸全量真跑并守基线回退,**失败照样拦**,只是归因落在总闸输出上;**没有专属钩子 ≠ 没被跑**,19 套台账与补钩子的判据 → `docs/门禁说明/host-unit-tests.md` §6 第 4 条。**Lead 裁定 2026-10-27:不补第 23 根**——读「这套跑没跑」去看总闸输出,别数钩子(原写「23 个 / 含 ui-sink 七个」是双重错:既把 22 说成 23,又宣称 ui-sink 有钩子——配置历史里 `ui-sink-unit-test` 从未存在)+ `test-coverage`(**覆盖率总闸**:跑全部 41 套并守基线回退;缺 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`)+ `nested-comment`(**块注释嵌套**:注释体内再出现 /* 会被 ARMCC 报 9-D,且**按包含者数量刷屏**、只在重编译该 TU 时才暴露;本仓诱因就是「在注释里写作用域/通配路径」,改法是写全目录或用尖括号 ;接线当日实扫 403 个 .c/.h,新发 0、存量 2 处(`test/host/common/` 两份假件头)钉只减不增基线 → `docs/门禁说明/nested-comment.md`)+ `skill-size`(**SKILL.md 不超插件写侧上限 65536 字节**:dsh-mnemon 只在写侧校验、加载侧不查,超限技能照常可读却每次「生成草案」被拒、永久卡在「生成中」;另查 `reference/` 指针悬空与 BOM → `docs/门禁说明/skill-size.md`)+ `mem-budget`(RAM 92% 预算门禁,登记簿条目「缩堆迁 CCM 后半(暂缓)」)+ `font-charset`(UI 字符集差集检查,防缺字白框)+ `ui-golden`(UI 像素回归,菜单 8 状态 + 内容页 60 张黄金参考)+ `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 条**))。**提交期实跑 49 根**(2026-10-28 用户裁定):`cppcheck` / `test-cppcheck` / `test-coverage` 三根带 `stages: [manual]`——实测合计 ~165 s = 一次带代码提交耗时的三分之二,提交期只跑快的那些;这三根由**远端 CI** 强制(`.gitea/workflows/ci.yml` 的「手动阶段门禁」步,逐根 `--hook-stage manual`)与 `scripts/ci_local.py` 的 manual 段,本地手工复跑 `python -m pre_commit run --all-files --hook-stage manual `(一次只能一个 id) +- 任一失败即拦截,共 **52 个钩子**(**口径 = YAML 解析后的 hook 条目数**:自查命令与「声明 / 文档 / 实跑」三数同源核对见 `docs/门禁说明/README.md` §2 首条——⚠ 直接 grep 配置里的声明行会多算一条**注释掉的**候选钩子(比口径多 1),不是口径;**配置注释里也别复述那个 grep 字面量**,它会让文本计数再虚高 1(2026-10-27 实测踩过);加一条钩子就同批改 AGENTS 与门禁索引两处计数。2026-10-25 道E 连加 `bom-guard` / `setview-check` / `uinav-selftest` 三条,2026-10-26 加 `generated-fresh` 一条,2026-10-27 加 `nested-comment` 一条;2026-10-28 加 `id-check-new` 一条并同批退役 `id-check`(口径 52 → 51:编号门禁换牙——新的只查新增行、零语料,旧的八项全库校验连语料一起移入 `scripts/archive/id_gate/`,路线见 `docs/编号规范.md` §8.5);**每种门禁一篇详解(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径)→ `docs/门禁说明/README.md` 索引**;通用机制与整体口径 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `id-check-new`(**编号门禁**:只查 `git diff` 新增行里有没有未登记形态的「字母+数字」标识,零语料、零维护;白名单是前缀级两张表,见 `docs/门禁说明/id-check-new.md`。旧的八项全库校验 `id-check` 已于 2026-10-28 退役,脚本与两份语料移入 `scripts/archive/id_gate/`,只在手工复查存量时才跑;退役路线见 `docs/编号规范.md` §8.5)+ `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + `bom-guard` / `setview-check` / `uinav-selftest`(2026-10-25 道E 那三条:前半句的加钩流水里有它们、逐项枚举里漏了,补上后本行各项相加才等于 52)+ 主机侧单测 **22 根专属钩子**(`-unit-test` 后缀 = 有独立 `files:` 触发面、需要秒级归因的那个子集;2026-10-25 增补 adc-cal / prot-dim / vol-idle-sync / ui-data / meas / run-par 六根)——**这是口径不是缺口**:`test/host/` 实测 44 套带 `run_unit_test.py`,其余 **22 套**(含 `test/host/ui_sink` 与录波三套)没有专属钩子,但每次 CI / `scripts/ci_local.py` 都被 `test-coverage` 总闸全量真跑并守基线回退,**失败照样拦**,只是归因落在总闸输出上;**没有专属钩子 ≠ 没被跑**,22 套台账与补钩子的判据 → `docs/门禁说明/host-unit-tests.md` §6 第 4 条。**Lead 裁定 2026-10-27:不补第 23 根**——读「这套跑没跑」去看总闸输出,别数钩子(原写「23 个 / 含 ui-sink 七个」是双重错:既把 22 说成 23,又宣称 ui-sink 有钩子——配置历史里 `ui-sink-unit-test` 从未存在)+ `test-coverage`(**覆盖率总闸**:跑全部 44 套并守基线回退;缺 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`)+ `nested-comment`(**块注释嵌套**:注释体内再出现 /* 会被 ARMCC 报 9-D,且**按包含者数量刷屏**、只在重编译该 TU 时才暴露;本仓诱因就是「在注释里写作用域/通配路径」,改法是写全目录或用尖括号 ;接线当日实扫 403 个 .c/.h,新发 0、存量 2 处(`test/host/common/` 两份假件头)钉只减不增基线 → `docs/门禁说明/nested-comment.md`)+ `skill-size`(**SKILL.md 不超插件写侧上限 65536 字节**:dsh-mnemon 只在写侧校验、加载侧不查,超限技能照常可读却每次「生成草案」被拒、永久卡在「生成中」;另查 `reference/` 指针悬空与 BOM → `docs/门禁说明/skill-size.md`)+ `mem-budget`(RAM 92% 预算门禁,登记簿条目「缩堆迁 CCM 后半(暂缓)」)+ `font-charset`(UI 字符集差集检查,防缺字白框)+ `ui-golden`(UI 像素回归,菜单 8 状态 + 内容页 66 张黄金参考)+ `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 条**))。**提交期实跑 49 根**(2026-10-28 用户裁定):`cppcheck` / `test-cppcheck` / `test-coverage` 三根带 `stages: [manual]`——实测合计 ~165 s = 一次带代码提交耗时的三分之二,提交期只跑快的那些;这三根由**远端 CI** 强制(`.gitea/workflows/ci.yml` 的「手动阶段门禁」步,逐根 `--hook-stage manual`)与 `scripts/ci_local.py` 的 manual 段,本地手工复跑 `python -m pre_commit run --all-files --hook-stage manual `(一次只能一个 id) - **范围 = 项目代码**:第三方(`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/` diff --git a/docs/UI设计/UI_设计架构.md b/docs/UI设计/UI_设计架构.md index 71cf28e7..8926145f 100644 --- a/docs/UI设计/UI_设计架构.md +++ b/docs/UI设计/UI_设计架构.md @@ -164,7 +164,7 @@ SDL 键盘 → LVGL SDL 驱动内部 5ms 定时器(sdl_event_handler) ### 4.2 数据:父子索引表 -- `g_menu_tab[]`:42 节点,每项 = 名称 + parent 下标(`MENU_ROOT`=0xFF 为根);树序即表序 +- `g_menu_tab[]`:43 节点,每项 = 名称 + parent 下标(`MENU_ROOT`=0xFF 为根);树序即表序 - "页" = 对表的一次过滤:节点 N 的页条目 = 全部 `parent == N` 的行(按表序) - 菜单 vs 叶子由"是否有子项"自动推导:`has_children(N)` 非空 → 菜单行(带 ▶),为空 → 叶子(无箭头,ENTER 进空白终点框) @@ -236,7 +236,7 @@ ENTER/→ 进子框: ### 5.3 框按需建/销 -- LVGL 池仅 16KB(`LV_MEM_SIZE`,2026-10-15 由 40KB 收缩),全量预建 42 个节点的所有页面必然爆内存 +- LVGL 池仅 16KB(`LV_MEM_SIZE`,2026-10-15 由 40KB 收缩),全量预建 43 个节点的所有页面必然爆内存 - 只建"当前路径"上的框:行自绘无对象,池内存 = 屏上框体(实测根框 used=5,368 B、最深四级级联 used=6,040 B、网口设置保存态峰值 used=6,568 B = 池的 40%) - 返回即 `lv_obj_delete` 释放,内存随时回落到根框水位 diff --git a/docs/UI设计/菜单页面设计.md b/docs/UI设计/菜单页面设计.md index a3a65be6..887d05e0 100644 --- a/docs/UI设计/菜单页面设计.md +++ b/docs/UI设计/菜单页面设计.md @@ -1,6 +1,6 @@ # 菜单页面设计 -> **用途**:定义 DTU 主菜单的**内容(42 节点层级树)、视觉规范、三层代码架构**(数据层 / 显示层 / 逻辑层),以及叶子节点进入后的**内容页模块**(§九)。 +> **用途**:定义 DTU 主菜单的**内容(43 节点层级树)、视觉规范、三层代码架构**(数据层 / 显示层 / 逻辑层),以及叶子节点进入后的**内容页模块**(§九)。 > **范围**:菜单层级(节点/父子/层级/进出导航)+ 三个已实现的内容页(装置信息/交流量/直流量,§九);其余叶子的内容页后续按同模式扩展。 > **来源**:用户提供的主菜单截图 7 屏(根页 + 实时数据/参数定值/三遥设置/装置维护/通讯参数/记录查询各一级子页 + 参数定值·整定二级)。 > **导航模型**:树形**逐级进入**——`ENTER`/`→` 进入子页,`ESC`/`←` 返回上一级;最多 **4 级(主界面 → 三级菜单)**。 @@ -37,7 +37,7 @@ flowchart TD K(["按键(远端键 / 模拟器键)"]) --> L["逻辑层 ui_menu.c
唯一 17 B 导航状态(path/选择/深度/滚动)
键语义:移动 / 进入 / 退出"] L -- "单向驱动: frame_build / frame_destroy / frame_refresh / focus" --> V["显示层 ui_menu_view.c/h
持全部 LVGL 对象,纯渲染,零导航状态"] - L -- "只读查询: children / valid" --> T["数据层 ui_menu_tab.c/h(生成物,只读)
g_menu_tab[42] 父子索引表 + 4 个查询 API"] + L -- "只读查询: children / valid" --> T["数据层 ui_menu_tab.c/h(生成物,只读)
g_menu_tab[43] 父子索引表 + 4 个查询 API"] V -- "只读查询: children / has_children / name" --> T ``` @@ -127,7 +127,7 @@ sequenceDiagram --- -## 二、菜单内容:42 节点层级树 +## 二、菜单内容:43 节点层级树 ### 2.1 层级总览 @@ -193,10 +193,11 @@ sequenceDiagram ├─ 遥控记录 [叶子] ├─ 自检记录 [叶子] ├─ 运行记录 [叶子] - └─ 运行报告 [叶子] + ├─ 运行报告 [叶子] + └─ 录波回放 [叶子] ``` -**统计**:菜单节点 **9** 个(含根),叶子节点 **33** 个,共 **42** 个节点;最大深度 **4 级**。 +**统计**:菜单节点 **9** 个(含根),叶子节点 **34** 个,共 **43** 个节点;最大深度 **4 级**。 ### 2.3 节点定义表(逻辑层级定义) @@ -236,7 +237,7 @@ sequenceDiagram | 29 | 通讯设置 | 二级菜单 | 叶子 | 28 | 0 | | 30 | 网口设置 | 二级菜单 | 叶子 | 28 | 0 | | 31 | SNTP设置 | 二级菜单 | 叶子 | 28 | 0 | -| 32 | 记录查询 | 一级菜单 | 菜单 | 0 | 9 | +| 32 | 记录查询 | 一级菜单 | 菜单 | 0 | 10 | | 33 | SOE记录 | 二级菜单 | 叶子 | 32 | 0 | | 34 | 事故记录 | 二级菜单 | 叶子 | 32 | 0 | | 35 | 操作记录 | 二级菜单 | 叶子 | 32 | 0 | @@ -246,12 +247,13 @@ sequenceDiagram | 39 | 自检记录 | 二级菜单 | 叶子 | 32 | 0 | | 40 | 运行记录 | 二级菜单 | 叶子 | 32 | 0 | | 41 | 运行报告 | 二级菜单 | 叶子 | 32 | 0 | +| 42 | 录波回放 | 二级菜单 | 叶子 | 32 | 0 | --- ## 三、数据层:ui_menu_tab(生成物,只读) -> **一句话职责**:提供"有什么条目"——一张扁平父子索引表(42 节点,`const` 存 Flash、只读)+ 4 个查询 API,供逻辑层 / 显示层共用。 +> **一句话职责**:提供"有什么条目"——一张扁平父子索引表(43 节点,`const` 存 Flash、只读)+ 4 个查询 API,供逻辑层 / 显示层共用。 > **绝不做**:持有任何状态、依赖 LVGL、被手改(改菜单走 §3.6 生成器)。 ### 3.1 选型:扁平"父子索引表"(邻接表 / 关系表模型) @@ -288,7 +290,7 @@ typedef struct { > 实现落在生成器产物 `Drivers/BSP/UI/ui_menu_tab.h`(见 §3.6);字段/语义与此一致。 -### 3.3 完整 42 节点表(按 §2.3 节点定义表落地) +### 3.3 完整 43 节点表(按 §2.3 节点定义表落地) > 产物 `ui_menu_tab.c`(§3.6 生成):`const ui_menu_node_t g_menu_tab[UI_MENU_TAB_COUNT]`,内容与下表一致。 @@ -337,10 +339,11 @@ const ui_menu_node_t g_menu_tab[UI_MENU_TAB_COUNT] = { { 32, "自检记录" }, /* 39 二级菜单叶子 */ { 32, "运行记录" }, /* 40 二级菜单叶子 */ { 32, "运行报告" }, /* 41 二级菜单叶子 */ + { 32, "录波回放" }, /* 42 二级菜单叶子 */ }; ``` -> `UI_MENU_TAB_COUNT`(=42)与 `MENU_ROOT` 在 `ui_menu_tab.h` 定义。 +> `UI_MENU_TAB_COUNT`(=43)与 `MENU_ROOT` 在 `ui_menu_tab.h` 定义。 ### 3.4 查询 API(逻辑层 / 显示层共用) @@ -392,7 +395,7 @@ for (uint8_t i = 0; i < cnt; i++) { | `ui_menu_tree.txt` | **人工编辑源**(缩进即层级);改菜单改这里 | | `tools/menu_gen/gen_menu.py` | 生成器(同 `make_font.py` 模式):解析缩进→推导 parent→产出 C 表;含根唯一/缩进对齐/层级上限自检 | | `Drivers/BSP/UI/ui_menu_tab.h` | 产物:`ui_menu_node_t` 定义 + `MENU_ROOT` + `UI_MENU_TAB_COUNT` + `extern g_menu_tab[]` | -| `Drivers/BSP/UI/ui_menu_tab.c` | 产物:`g_menu_tab[UI_MENU_TAB_COUNT]` 完整 42 节点表 | +| `Drivers/BSP/UI/ui_menu_tab.c` | 产物:`g_menu_tab[UI_MENU_TAB_COUNT]` 完整 43 节点表 | 重新生成: ``` @@ -421,9 +424,9 @@ python tools/menu_gen/gen_menu.py | 符号 | 定义处 | 内容 | 占用 | |---|---|---|---| | `ui_menu_node_t` | `ui_menu_tab.h` | `{ uint8_t parent; uint8_t pwd; const char *name; }`(32 位 8 B/项) | 类型定义 | -| `g_menu_tab[UI_MENU_TAB_COUNT]` | `ui_menu_tab.c`(生成) | 42 节点扁平父子表 | ≈ 42×8 B ≈ 336 B | -| 名字字面量 | 同上 | 42 个 `const char[]` | ≈ 400 B | -| `UI_MENU_TAB_COUNT` / `MENU_ROOT` | `ui_menu_tab.h` | 节点数 42 / 根哨兵 0xFF(8 位,上限 255 项) | 宏 | +| `g_menu_tab[UI_MENU_TAB_COUNT]` | `ui_menu_tab.c`(生成) | 43 节点扁平父子表 | ≈ 43×8 B ≈ 344 B | +| 名字字面量 | 同上 | 43 个 `const char[]` | ≈ 400 B | +| `UI_MENU_TAB_COUNT` / `MENU_ROOT` | `ui_menu_tab.h` | 节点数 43 / 根哨兵 0xFF(8 位,上限 255 项) | 宏 | > 逻辑层/显示层**只读**它(§3.4 的 API 都是对它的过滤/查询),从不修改。合计 ≈ **0.7 KB Flash**。 diff --git a/docs/UI设计/页面扩充详细方案.md b/docs/UI设计/页面扩充详细方案.md index 6070dea5..1224b17a 100644 --- a/docs/UI设计/页面扩充详细方案.md +++ b/docs/UI设计/页面扩充详细方案.md @@ -542,7 +542,7 @@ void ui_w_soe_refresh(ui_w_soe_t *inst, uint16_t row, char *name, char *valu ## 5. 菜单树改动 -**本批不改菜单树**——27 个空叶子全在现有 42 节点里,`#page=` 注记逐个补上即可(改 `ui_menu_tree.txt` 后重跑 `python tools/menu_gen/gen_menu.py`)。 +**本批不改菜单树**——当时 27 个空叶子全在现有 42 节点里(节点数现值 43,2026-10-29 录波道加「录波回放」叶),`#page=` 注记逐个补上即可(改 `ui_menu_tree.txt` 后重跑 `python tools/menu_gen/gen_menu.py`)。 > 【2026-10-26 已改一笔】整定 4 叶(参数 / 定值 / 控制字 / 软压板)已补 `#page=`, > 绑的是与查看列**同一批模块**(一个模块被两个叶子绑定是生成器允许的,`validate_pages()` 按 diff --git a/docs/UI设计/页面补齐规划.md b/docs/UI设计/页面补齐规划.md index af741912..9e055205 100644 --- a/docs/UI设计/页面补齐规划.md +++ b/docs/UI设计/页面补齐规划.md @@ -1,6 +1,7 @@ # UI 页面补齐规划(2026-10-23) -> **定位**:盘清 42 节点菜单里**还没有内容页的 27 个叶子**,按"数据是否就绪 + 页面形态"归并成可施工的包,给出分批顺序与每页规格。 +> **定位**:盘清当时 42 节点菜单里**还没有内容页的 27 个叶子**(节点数现值 43 = 9 菜单 + 34 叶, +> 2026-10-29 录波道加「录波回放」叶后升级;本文其余数字是 2026-10-23 立项快照,按 §1 的「完成即冻结」不回改),按"数据是否就绪 + 页面形态"归并成可施工的包,给出分批顺序与每页规格。 > **前置阅读**:`docs/UI设计/内容页架构.md`(框架机制与新增页面六步)、`docs/UI设计/菜单页面设计.md`(菜单树与几何)、`docs/UI设计/UI_自动化测试流程.md`(像素回归门禁)。 > **不重复登记**:页面级待办落地时按语义标识进 `docs/任务规划/登记簿.md`;本文只出规划与规格。 > **逐页细化**:每页显示哪些行、数据从哪来、要改哪些代码,见 `docs/UI设计/页面扩充详细方案.md`(本文的落地细化版)。 @@ -254,7 +255,7 @@ ui_page_ac / ui_page_yx / ui_page_setlist ... > **虚拟注入的价值**:当前送检等标准源(B10),而虚拟注入能让"改一个假值 → 看 UI 是否变 → 看 104 是否上送 → 看 SOE 是否记录"这条链在没有硬件激励的情况下闭环。它是**测试基础设施**,不是产品功能,建议放在厂家菜单并加明显标识(标题后缀"调试")。 > -> **代价**:菜单树从 42 节点扩到约 48~50 节点,`ui_menu_tree.txt` 改动会触发菜单 8 状态像素回归与 `task-table` 式索引文档同步。**这是需要拍板的项**——见 §8。 +> **代价**:菜单树从当时 42 节点扩到约 48~50 节点(节点数现值 43,2026-10-29 录波道加「录波回放」叶),`ui_menu_tree.txt` 改动会触发菜单 8 状态像素回归与 `task-table` 式索引文档同步。**这是需要拍板的项**——见 §8。 --- @@ -262,7 +263,7 @@ ui_page_ac / ui_page_yx / ui_page_setlist ... | # | 问题 | 影响 | |---|---|---| -| 1 | 菜单是否扩到 48~50 节点(加厂家设置支)? | 42 是产品需求里的硬数字;扩了要改 `产品需求设计文档.md` §8.2 与菜单像素回归 | +| 1 | 菜单是否扩到 48~50 节点(加厂家设置支)? | 节点数以 `产品需求设计文档.md` §8.2 为准(2026-10-29 现值 43,此前 42);扩了要改该节与菜单像素回归 | | 2 | **直流量页怎么办?**(V0.1 没有直流采样通道)三选一:改成"温度 + 备用"页 / 从菜单删掉 / 保留空页显示 `--` | 决定行表与菜单节点数 | | 3 | 定值名用全称还是缩写?(字体子集大小 vs 可读性) | 40 条名 × 平均字数,直接影响字库与 `font-charset` | | 4 | 事故/操作/告警/启动/遥控 5 类记录,是否本轮就要? | 每类 = 一个写入方 + 一个 `syslog_id_t` + 一个页面,是本规划里最大的一块后端工作 | diff --git a/docs/产品需求/产品需求设计文档.md b/docs/产品需求/产品需求设计文档.md index 2b1090c3..8ff0f2b3 100644 --- a/docs/产品需求/产品需求设计文档.md +++ b/docs/产品需求/产品需求设计文档.md @@ -660,7 +660,7 @@ sequenceDiagram **FR-UI-2 菜单与内容页** - 需求:42 节点层级菜单(最大 4 级)+ 内容页(装置信息/交流量/直流量等)。 -- 实现:三层拆分(数据层 `ui_menu_tab` 生成物 / 逻辑层 `ui_menu` 17 B 导航状态 / 显示层 `ui_menu_view` 级联框);内容页框架 + 页面插件描述符。 +- 实现:三层拆分(数据层 `ui_menu_tab` 生成物 / 逻辑层 `ui_menu` 17 B 导航状态 / 显示层 `ui_menu_view` 级联框);内容页框架 + 页面插件描述符。2026-10-29 实到 43 节点(新增「录波回放」叶,需求原文的 42 是 V0.1 立项数)。 - 判据:`UI menu init: OK`;菜单像素回归全 PASS。 **FR-UI-3 待机屏** @@ -1802,10 +1802,13 @@ pre-commit run --all-files | 刷新 | `uiTask` 10 ms 循环 | | 绘制缓冲 | I1 draw buffer 需 +8 B palette 头 | -### 8.2 菜单树(42 节点) +### 8.2 菜单树(43 节点) ![级联框总览](../images/级联框总览.svg) +> 该图仍是 42 节点时代所画(2026-10-29 新增的「录波回放」叶未进图):`tools/arch_diagram/` 里没有这张图的生成器, +> 重画要另开一笔并走 `generated-fresh`;**本节下面的文字与代码生成物是真值**。 + **规模**:菜单节点 **9** 个(含根)+ 叶子节点 **34** 个 = **43** 节点;最大深度 **4 级(主界面 / 一级 / 二级 / 三级菜单)**。 **绑定现状**(2026-10-29 复扫):**已绑内容页 30 叶 / 未绑 4 叶**(页面注册表 `UI_PAGE_MAP_COUNT` = 30, 与已绑叶数一致);未绑 4 叶 = 无开入硬件 3 + SNTP 1(取舍与重开条件见 @@ -1828,10 +1831,10 @@ docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 ├─ 装置维护 ▶(时钟设置 / 强制复归 / 手动录波 / 清除记录) ├─ 通讯参数 ▶(通讯设置 / 网口设置 / SNTP设置) └─ 记录查询 ▶(SOE记录 / 事故记录 / 操作记录 / 保护告警 / 保护启动 / - 遥控记录 / 自检记录 / 运行记录 / 运行报告) + 遥控记录 / 自检记录 / 运行记录 / 运行报告 / 录波回放) ``` -**三层代码架构**:数据层 `ui_menu_tab`(生成物,`g_menu_tab[42]` 扁平父子索引表,≈0.7 KB Flash) +**三层代码架构**:数据层 `ui_menu_tab`(生成物,`g_menu_tab[43]` 扁平父子索引表,≈0.7 KB Flash) + 逻辑层 `ui_menu`(唯一 **17 B** 导航状态)+ 显示层 `ui_menu_view`(持全部 LVGL 对象,零导航状态)。 **导航模型**:树形逐级进入——`ENTER`/`→` 进入子页,`ESC`/`←` 返回上一级;页面 = 对表的一次 diff --git a/docs/代码规范/pre-commit使用说明.md b/docs/代码规范/pre-commit使用说明.md index eaac9609..4a166c2f 100644 --- a/docs/代码规范/pre-commit使用说明.md +++ b/docs/代码规范/pre-commit使用说明.md @@ -32,7 +32,7 @@ git commit └─ manual(pre-commit --hook-stage manual;不随 git commit 跑) ├─ cppcheck 全量 C 静态分析(自有代码) ├─ test-cppcheck test/ 侧静态分析 - └─ test-coverage 主机单测覆盖率总闸(41 套 + 回退台账) + └─ test-coverage 主机单测覆盖率总闸(44 套 + 回退台账) ``` **阶段**:`pre-commit` 阶段拦截代码问题、`commit-msg` 阶段拦截格式问题,任一失败 → commit 中止;另有 `manual` 阶段(`stages: [manual]` 的慢钩子)**不随 `git commit` 跑**,由远端 CI 与 `scripts/ci_local.py` 的 manual 段承担(见下)。一次 `git commit` 实跑 **48** 根 = `pre-commit` 阶段 47 根 + `gitlint` 1 根。 @@ -332,7 +332,7 @@ $env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Env | 文件 | 位置 | 说明 | |---|---|---| -| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(**52 个钩子**声明数,一次 `git commit` 实跑 49 根;口径 = YAML 解析后的 hook 条目数,自查命令 → `docs/门禁说明/README.md` §2。声明数与一次提交的实跑数分列,勿混写;计数改动的完整台账只记在 AGENTS.md 提交门禁节与本目录 README §2,本行不重述)。逐项详解在 `docs/门禁说明/`(索引 `docs/门禁说明/README.md`;主机侧单测 **22 根**合一篇,含各套件来历与「19 套无专属钩子」台账 `docs/门禁说明/host-unit-tests.md` §6;UI 侧另有 `font-charset` 与 `ui-golden` 两篇) | +| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(**52 个钩子**声明数,一次 `git commit` 实跑 49 根;口径 = YAML 解析后的 hook 条目数,自查命令 → `docs/门禁说明/README.md` §2。声明数与一次提交的实跑数分列,勿混写;计数改动的完整台账只记在 AGENTS.md 提交门禁节与本目录 README §2,本行不重述)。逐项详解在 `docs/门禁说明/`(索引 `docs/门禁说明/README.md`;主机侧单测 **22 根**合一篇,含各套件来历与「22 套无专属钩子」台账(现值 2026-10-29) `docs/门禁说明/host-unit-tests.md` §6;UI 侧另有 `font-charset` 与 `ui-golden` 两篇) | | `.gitlint` | 仓库根 | gitlint 规则(宽松,兼容中文) | | `.cubemx_survival_baseline.json` | 仓库根 | CubeMX 易失定制基线(哈希;`--write` 重建) | | `scripts/check_mojibake.py` | scripts/ | U+FFFD 检测脚本 | diff --git a/docs/测试说明/主机单测覆盖率口径.md b/docs/测试说明/主机单测覆盖率口径.md index 11aa59c2..aaaa34af 100644 --- a/docs/测试说明/主机单测覆盖率口径.md +++ b/docs/测试说明/主机单测覆盖率口径.md @@ -56,7 +56,7 @@ ## 3. 怎么跑 ```powershell -# 全量:33 套单测逐套"编译(带 PDB)→ 插桩运行 → 解析 Cobertura",最后并集 + 比基线 +# 全量:44 套单测逐套"编译(带 PDB)→ 插桩运行 → 解析 Cobertura",最后并集 + 比基线 & '.venv\Scripts\python.exe' scripts/check_test_coverage.py # 只跑一套(调试用;子集会跳过"在测文件消失"类回退判定,见 §5) @@ -77,7 +77,9 @@ 退出码:`0` 全过且无回退 / `1` 有单测失败或覆盖率回退 / `2` 环境错误(无 cl、无 mscoverage、 编译失败、XML 缺失)/ `3` 用法错误。全量实测约 **67 s**(33 套 × 编译 + 插桩运行; 2026-10-25 实测;16 套时约 25 s、28 套时约 45 s,套件数涨了耗时就涨,别拿旧数当预算)。最新一次实测: -2026-10-28 = 41 套 **~105 s**(该钩子同日移入 `stages: [manual]`,由远端 CI 与 `ci_local.py` 的 manual 段强制)。 +2026-10-28 = 41 套 **~105 s**(该钩子同日移入 `stages: [manual]`,由远端 CI 与 `ci_local.py` 的 manual 段强制); +套件数现值 **44 套**(2026-10-29 实测,台账见 `docs/门禁说明/host-unit-tests.md` §6 第 4 条),**44 套这一轮的总闸耗时未重测**, +上面那三个耗时都是当时值,别当现值预算。 套件数的现值与逐个来历见 `docs/门禁说明/host-unit-tests.md` §6 第 4 条。 中间产物(exe / PDB / Cobertura XML)一律落 `%TEMP%\dtu_host\<工作树哈希>\coverage\<模块>` @@ -154,7 +156,7 @@ 宁可红,也不给可疑的数字。 6. **只覆盖跑到的进程树。** 一次 `collect` 只看一次运行;没跑到的套件不进分母 (全量跑时以"模块未跑到"判 REGRESS),子集运行明确降级(§5)。 -7. **头文件里的代码归 `.h`。** 统计面含 `.h`(静态内联与被展开的行落在头文件上);当前 33 套 +7. **头文件里的代码归 `.h`。** 统计面含 `.h`(静态内联与被展开的行落在头文件上);当前 44 套 几乎没触及 `.h`,表里看不到它们——不要以为头文件"天生 100%"。 8. **依赖 `Microsoft.CodeCoverage.Console.exe`。** 探测顺序:`DTU_MS_COVERAGE`(设了就必须 有效、**不静默回退**)→ 各 VS 安装根的 `Common7\IDE\Extensions\Microsoft\CodeCoverage.Console` → diff --git a/docs/编号规范.md b/docs/编号规范.md index f35df347..8b91180d 100644 --- a/docs/编号规范.md +++ b/docs/编号规范.md @@ -85,7 +85,7 @@ > - ❌「不做 = 维持原状 = 还能发」——**错**。新用例一律用**语义化测试函数名 + 文档用例表的行名**(§3.3), > 不再配 TC 号;指认**既有**用例仍可引用现存号(旧号只读,不是许可新配)。 > - ❌「停发 = 这族要废了,顺手清掉、把号腾出来复用」——**也错**。现存号与它在 `scripts/archive/id_gate/id_registry.json` -> 里的登记,是 `check_test_traceability.py` 与 41 套 `run_unit_test.py` **按字面量读的追溯把手**, +> 里的登记,是 `check_test_traceability.py` 与 44 套 `run_unit_test.py` **按字面量读的追溯把手**, > 清它等于换机制(`编号治理历史.md` §9.1);号不回收、不复用是 §1 的硬规矩。 > - ❌「registry 里 TC 族还带着活游标,所以门禁允许我发」——**不成立:这条缺口已于 2026-10-27 同日关掉**。 > 37 个 TC 类族现在**全部** `frozen: true`,其中 **34 个 `TC-HOST-*` 子族的 `cursor` 冻结在裁定日该套件存量 diff --git a/docs/门禁说明/README.md b/docs/门禁说明/README.md index 1e16e96b..19d72abb 100644 --- a/docs/门禁说明/README.md +++ b/docs/门禁说明/README.md @@ -75,7 +75,7 @@ | [`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` | 指针 | | [`host-unit-tests.md`](host-unit-tests.md) | 22 个 `*-unit-test` | 主机侧单测(同一机制 22 个实例):逻辑层在无硬件的 PC 上编译并跑断言 | 各自被测模块与其测试目录 | 已写 | -| 详解在 [`../测试说明/主机单测覆盖率口径.md`](../测试说明/主机单测覆盖率口径.md)(口径 / 分母 / 基线 / 盲区都在那篇,本目录不重复) | `test-coverage` | **覆盖率总闸**:跑全部 41 套主机单测 + 已覆盖行只增不减的回退台账 | `App/`、`Drivers/BSP/`、`test/host/`、脚本与基线 | 已写(在 `docs/测试说明/`;`stages: [manual]`) | +| 详解在 [`../测试说明/主机单测覆盖率口径.md`](../测试说明/主机单测覆盖率口径.md)(口径 / 分母 / 基线 / 盲区都在那篇,本目录不重复) | `test-coverage` | **覆盖率总闸**:跑全部 44 套主机单测 + 已覆盖行只增不减的回退台账 | `App/`、`Drivers/BSP/`、`test/host/`、脚本与基线 | 已写(在 `docs/测试说明/`;`stages: [manual]`) | | [`trailing-whitespace.md`](trailing-whitespace.md) | `trailing-whitespace` | 行尾空白(改写型;CubeMX/Keil 生成文件豁免) | 全部 | 已写 | | [`end-of-file-fixer.md`](end-of-file-fixer.md) | `end-of-file-fixer` | 文件末尾必须恰好一个换行(改写型;同豁免) | 全部 | 已写 | | [`check-added-large-files.md`](check-added-large-files.md) | `check-added-large-files` | 别把大文件(日志/镜像/二进制)误提进仓库 | 全部 | 已写 | diff --git a/docs/门禁说明/host-unit-tests.md b/docs/门禁说明/host-unit-tests.md index add1161c..4c6c0665 100644 --- a/docs/门禁说明/host-unit-tests.md +++ b/docs/门禁说明/host-unit-tests.md @@ -88,30 +88,32 @@ 3. `include_dirs` 与源文件清单是**手写**的:新依赖一个目录就会假失败(见 §4 第 2 类),反过来, 漏了某个真实依赖可能让测试**编过却少编了一个翻译单元**(少测而不自知)。 4. **专属钩子不是唯一防线,而「没有专属钩子」也不等于「没被跑」**。2026-10-26 按 git 历史与实测重新 - 清点(2026-10-28 复点:下列数字已按复点值更新;上一版写「16 个模块有专属钩子、另有 12 套、总闸跑 28 套」——三个数都随 2026-10-25 的 + 清点(2026-10-28 复点、2026-10-29 录波三套入库后再复点:下列数字已按复点值更新;上一版写「16 个模块有专属钩子、另有 12 套、总闸跑 28 套」——三个数都随 2026-10-25 的 六根新钩子与后续新增套件过期了,正是本目录反复在防的"文档与机器面不同码"): - **22** 根专属钩子 = 有独立 `files:` 触发面、需要**秒级归因**的那个子集; - - `test/host/` 下带 `run_unit_test.py` 的套件实测 **41** 套(43 个目录,除 `common` 与 - `__pycache__`)⇒ **没有专属钩子的是 19 套**:`boot_stats`、`cfg_file_store_mcu`、`console_log`、 + - `test/host/` 下带 `run_unit_test.py` 的套件实测 **44** 套(46 个目录,除 `common` 与 + `__pycache__`;两个数分别来自 `check_test_coverage.py --list` 与 `ls -d test/host/*/`,2026-10-29 实测) + ⇒ **没有专属钩子的是 22 套**:`boot_stats`、`cfg_file_store_mcu`、`console_log`、 `ftp_cfg`、`iec104_serv`、`lftpd`、`lftpd_inet`、`lftpd_io`、`net_cfg`、`net_events`、 `net_listener`、`net_pending`、`net_select`、`net_socket`、`prot_set_store`、`soe_store`、 - `sys_log_mcu`、`task_prof`、`ui_sink`; - - 这 19 套每次 CI 与 `scripts/ci_local.py` 都由 `test-coverage` **总闸**真跑(该钩子带 `stages: [manual]`: + `sys_log_mcu`、`task_prof`、`ui_sink`、`wave_ring`、`wave_store`、`wave_trig`(末三套 2026-10-29 随 + 故障录波入库,按同一条 Lead 裁定不补专属钩子;22 = 44 套 − 22 根专属钩子,逐点数与两处机器源互证); + - 这 22 套每次 CI 与 `scripts/ci_local.py` 都由 `test-coverage` **总闸**真跑(该钩子带 `stages: [manual]`: `git commit` 阶段不跑它,这一根与 `cppcheck` / `test-cppcheck` 同属 CI 与 `ci_local.py` 的 manual 段); 发现规则是 `HOST_TEST_DIR.rglob("run_unit_test.py")`(与 `scripts/ci_local.py` 的主机侧纯逻辑单测同一口径), 退出码语义写死「0 = 全部单测通过且覆盖率相对基线没回退 / 1 = 有单测失败或覆盖率回退」⇒ - **失败照样拦(CI 与 `ci_local.py` 的 manual 段)**。差的只是归因粒度(要在总闸输出里认出是哪套)与耗时(41 套全量比单根慢得多); + **失败照样拦(CI 与 `ci_local.py` 的 manual 段)**。差的只是归因粒度(要在总闸输出里认出是哪套)与耗时(44 套全量比单根慢得多); - 要不要给某套补第 23 根,判据现成:pre-commit 4.x 没有 `always: true`,**该模块有没有自己的 `files:` 触发面**决定补了会不会真跑;只为凑数加一根,等于把「快归因面」和「全量把关」两件事混成 一件,还要让钩子计数再改一次; - - **裁定(2026-10-27,Lead):这批无专属钩子套件(裁定当时 18 套,2026-10-28 现值 19 套;含 `test/host/ui_sink`)不补专属钩子,差异按设计说明记录。** + - **裁定(2026-10-27,Lead):这批无专属钩子套件(裁定当时 18 套,2026-10-28 为 19 套,2026-10-29 现值 22 套;含 `test/host/ui_sink` 与新增的 `wave_ring` / `wave_store` / `wave_trig`)不补专属钩子,差异按设计说明记录。** 理由不是省事:22 根专属钩子存在的唯一意义是**秒级归因**;而 `ui_sink` 改动面小,它的失败面全部 落在 `test-coverage` 总闸的必跑路径里,坏了照样在 CI / `ci_local.py` 拦下。再加一根只是多一个必须「声明数 / 文档数 / 实跑数」三处同源同步的计数点——本仓 2026-10-26 到 27 一天之内就为 40 / 50 / 51 这三个数不同源 返工过两轮。**要读「这套跑没跑」,读总闸输出,不要来数钩子。** 两个方向的空头账一起堵住: ① 别把没接线的套件算进钩子账(原写「23 个 / 含 ui-sink」正是这么错的;2026-10-27 复核的准数是 - 22 根 / 40 套 / 18 套无专属钩子——第 41 套 `boot_stats` 当日 11:29 才入库,此后两数是 41 / 19,以本条上面的实测行为准);② 也别把总闸的绿读成「每套都被单独归因过」——它只保证 + 22 根 / 40 套 / 18 套无专属钩子(三个数都是当时值)——第 41 套 `boot_stats` 当日 11:29 才入库,此后两数当时是 41 / 19,现值 44 / 22 以本条上面的实测行为准);② 也别把总闸的绿读成「每套都被单独归因过」——它只保证 「全跑过 + 覆盖率没回退」。将来确实要给某套补专属钩子,先过上一条判据,并在**同一笔**里改 AGENTS 与门禁索引的计数(口径 = YAML 解析条目数,见 `docs/门禁说明/README.md` §2 首条)。 - 总闸的触发面是 `App/`、`Drivers/BSP/`、`test/host/`、覆盖率脚本与基线,**外加门禁自身** diff --git a/scripts/README.md b/scripts/README.md index 20f811a2..82b2122b 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -44,7 +44,7 @@ | `ci_local.py` | **本地跑与 CI 等价的门禁**(静态门禁 pre-commit 47 根 + **manual 段**(`cppcheck` / `test-cppcheck` / `test-coverage` 三根逐根 `--hook-stage manual`,默认跑)+ 提交信息门禁 + **体积门禁** + 主机侧纯逻辑单测):自动选用装了 pre-commit 的解释器;无 `.map` 时自适应跳过 `mem-numbers` / `mem-budget`(⚠ 那是 SKIP 不是通过,缺口由体积门禁兜住);退出码 0/1/2 与 CI 对齐。CI 定义见 `../.gitea/workflows/ci.yml`,覆盖边界见 `docs/测试说明/测试开发标准流程.md` §9 | | `check_size_gate.py` | **体积门禁的强制路径**:`.map` 只在本地有,钩子无它时自适应 SKIP,于是"没测=通过"。本脚本用**已提交的同码凭证** `scripts/size_attestation.json` 把这条判据变成无需 Keil 也能跑:`--stamp` 只在"`.map` 存在、比全部受管源码新、且未超 92%"时才盖章(已挂在 `ram_baseline.py --update` 里,无新增人工步骤);`--local` 供 ci_local 的体积门禁步;`--ci` 供 runner(同码性 + 按凭证数字复核红线,主干 push 拦红、其余事件 `--warn-only`);`--show` 排障。**口径与边界的唯一正文在本脚本模块 docstring** | | `size_attestation.json` | (被 `check_size_gate.py` 读写)体积同码凭证:受管源码指纹(`git ls-files` 范围内逐文件 sha256 聚合)+ 测量数字与占比 + `.map` 出处。改固件源码后不重测 → 指纹对不上 → 体积门禁与 CI 同时拦红 | -| `check_test_coverage.py` | 主机单测**覆盖率**(2026-10-23):mscoverage 动态插桩跑 `test/host/**` 全部 41 套,统计自研生产源码行/函数覆盖(并集)+ 与 `test_coverage_baseline.json` 比回退;`--module/--update-baseline/--md/--json/--list`;挂在 `test-coverage` 钩子上(`stages: [manual]`,**`git commit` 不跑**),入口 = 远端 CI「手动阶段门禁」步 / `scripts/ci_local.py` 的 manual 段(`--coverage` 旗标保留只为兼容旧命令;口径见 `docs/测试说明/主机单测覆盖率口径.md`) | +| `check_test_coverage.py` | 主机单测**覆盖率**(2026-10-23):mscoverage 动态插桩跑 `test/host/**` 全部 44 套,统计自研生产源码行/函数覆盖(并集)+ 与 `test_coverage_baseline.json` 比回退;`--module/--update-baseline/--md/--json/--list`;挂在 `test-coverage` 钩子上(`stages: [manual]`,**`git commit` 不跑**),入口 = 远端 CI「手动阶段门禁」步 / `scripts/ci_local.py` 的 manual 段(`--coverage` 旗标保留只为兼容旧命令;口径见 `docs/测试说明/主机单测覆盖率口径.md`) | | `ci_gitlint_range.ps1` | CI 侧**提交信息门禁**:逐条校验本次 push/PR 引入的提交(`--all-files` 跑不到 `commit-msg` stage,故必须单独守) | | `ci_find_python.ps1` | CI 侧 Python 探测(候选顺序须与 `ci_local.py` 同序同步,否则 CI 与本地判定会分叉) | | `ci_runner_bootstrap.ps1` | **本机 runner 运维脚本**(体检 `-Status` / 干跑 `-DryRun` / 部署 `-SetEnv -StartNow`):装"等 Gitea 就绪 + 崩溃自拉起"的看门狗 + 开机计划任务 + 机器级 `DTU_*` | diff --git a/scripts/board_ui_nav.py b/scripts/board_ui_nav.py index 61ad6357..c4bb7ae8 100644 --- a/scripts/board_ui_nav.py +++ b/scripts/board_ui_nav.py @@ -993,7 +993,7 @@ def selftest_board_frames(clf): def selftest_refs(clf): - """仓库黄金参考(60 张)逐帧分类自检:每张图的期望值 = 它的文件名。 + """仓库黄金参考(66 张)逐帧分类自检:每张图的期望值 = 它的文件名。 这一组是**语义正确的基线**(模拟器与板端同一份 UI 源码),用来把"分类器错了"与 "留档帧本来就是走歪的"分开——两类失败含义完全不同,混在一起就只能靠猜。 diff --git a/simulator/README.md b/simulator/README.md index b9674c28..066906ba 100644 --- a/simulator/README.md +++ b/simulator/README.md @@ -222,7 +222,7 @@ UI 改动的自动化门禁脚本(完整流程见 `docs/UI设计/UI_自动化 | 脚本 | 原理 | 失败输出 | |------|------|----------| | `check_menu_rows.py` | 键序注入(u/d/e/s)跑 8 个菜单状态,断言选中行白带、非选行黑底白字、框体边框、往返状态逐像素一致;MENU-STATE-00 池水位须等于脚本内 `MENU_POOL_BASELINE` | 未过断言清单 + 保留临时截图 | -| `check_content_pages.py` | ① 文本断言(解析字体字形,在截图 y 带内精确匹配期望串,不依赖 `ref/`)② **反向断言**(`ABSENT` 表:某串**不该**出现,如查看页/只读态不得出现「未标定」徽标与「保存」行——像素参考会被 `--update` 刷成"自证正确",反向断言不依赖参考图)③ **60 张**无头截图与 `tests/ref/shot_*.bin` **逐字节**比对(含 2026-10-26 新增的整定 9 态:进页 / 保存行 / 只读态 / 编辑态 / 越界 / 保存 OK / 落定后可读 / 二态回绕);另校验开页-关页往返池水位 = `POOL_AFTER_ROUNDTRIP` | 文本未命中项 / 反向断言被命中 / 差异像素数 + 双方 ink runs 对照 | +| `check_content_pages.py` | ① 文本断言(解析字体字形,在截图 y 带内精确匹配期望串,不依赖 `ref/`)② **反向断言**(`ABSENT` 表:某串**不该**出现,如查看页/只读态不得出现「未标定」徽标与「保存」行——像素参考会被 `--update` 刷成"自证正确",反向断言不依赖参考图)③ **66 张**无头截图与 `tests/ref/shot_*.bin` **逐字节**比对(含 2026-10-26 新增的整定 9 态:进页 / 保存行 / 只读态 / 编辑态 / 越界 / 保存 OK / 落定后可读 / 二态回绕);另校验开页-关页往返池水位 = `POOL_AFTER_ROUNDTRIP` | 文本未命中项 / 反向断言被命中 / 差异像素数 + 双方 ink runs 对照 | | `check_remo_sink.py` | 启动**窗口**模拟器(非无头,`CREATE_NEW_PROCESS_GROUP` 防管道阻塞),经 TCP 7003 发 LCDMEM 取 1bpp 帧;断言首帧=LCDMEM(无 INIT 污染)/位图 3200 字节/非平凡;复用 `i1_to_png` 出目检 PNG;与无头参考(`lvgl_sim --screenshot`)逐位比对 <2% 判 MATCH。守护"INIT 污染 / 快照卡死"两个历史缺陷 | 各判据失败原因 + `sim_err.txt` 尾部 + 保留 PNG(默认不删,`--clean` 才删) | - **黄金参考与目检 PNG**(`ref/*.bin` **入库**、`ref/*.png` 本地派生不入库):换机或想目检时—— diff --git a/simulator/tests/check_content_pages.py b/simulator/tests/check_content_pages.py index 0f88f0bf..de68d980 100644 --- a/simulator/tests/check_content_pages.py +++ b/simulator/tests/check_content_pages.py @@ -149,7 +149,7 @@ PAGES = [ # 计数范围/复位原因/主站接入 + 范围标签"本次开机",第 2 页钉 104 另外三枚(发送帧数 # 128 / 发送失败 0 / 接收字节 4096)与页脚真实页码 2/2。四枚假数据值互不相同是刻意的: # 行错位/串行只有"像素 + 逐行文本断言"两把尺子同时才照得出来(Lead 裁定 2026-10-28, - # 因此用例是 53+7=60 张,不是 53+6=59)。 + # 因此当时用例是 53+7=60 张,不是 53+6=59;现集合 66 张,见 PAGES 表)。 ("runreport", "dddddde dddddddd e"), ("runreport_p2", "dddddde dddddddd e dddddddd"), # 页内再 8 下 → 第 2 页(选中落 r0=发送帧数) # ── 故障录波 UI 道(2026-10-29):手动录波三态 + 回放三态(真实波形/空态/坏段)── @@ -880,7 +880,7 @@ EXTRA = { # sim_oper.log 已经攒了 3 条(192 B = 3×64 B)→ "操作记录"页读到的是真记录, # 空态判据(第 0 行"无记录")根本拍不到。**判据本身没错,错在跑它的环境被前面的写路径 # 污染了**,所以给这 5 页各开一个空目录 = 现场"这些日志文件还不存在"的确定态; -# 其余 55 页环境一字不变(对当时存量 53 张黄金零影响,实测逐字节零漂移;当前集合 60 张)。 +# 其余 55 页环境一字不变(对当时存量 53 张黄金零影响,实测逐字节零漂移;当时集合 60 张,现集合 66 张)。 FRESH_CWD = ("accident", "oper", "alarm", "start", "yk") diff --git a/test/host/sys_log_mcu/sys_log_mcu_test.c b/test/host/sys_log_mcu/sys_log_mcu_test.c index 10ef4f76..4568d644 100644 --- a/test/host/sys_log_mcu/sys_log_mcu_test.c +++ b/test/host/sys_log_mcu/sys_log_mcu_test.c @@ -193,7 +193,7 @@ * (s_entry_fill / s_put / syslog_post_quiet / syslog_post_dropped)与 26 行 * 可执行代码**全部**有断言跑到:覆盖行 +26、函数 +4,未覆盖仍是同一批 3 行老账 * (一条死分支 + sys_log_evidence 里 snprintf<=0),**一条没涨**。 - * ⚠ 全量门禁(41 套并集 + 基线只增不减)由提交前那一次统一跑,本行只是自量记录。 + * ⚠ 全量门禁(44 套并集 + 基线只增不减)由提交前那一次统一跑,本行只是自量记录。 * L 族变异实测(2026-10-26,三处同时注入后逐字节还原): * ① s_paths[] 里 accid 与 alarm 两格**对调**(就是"表序错"那个静默错法); * ② 给 quiet 的丢弃分支补一条 `DBG_ERROR`(= 悄悄退化成 syslog_post); From 0566405198b62b03c4c369d3078214636ef5cd27 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 20:05:21 +0800 Subject: [PATCH 06/19] =?UTF-8?q?docs(=E6=95=B0=E6=8D=AE=E5=8F=AF=E4=BF=A1?= =?UTF-8?q?):=20=C2=A74.6.2=20=E4=B8=89=E5=A4=84=E5=88=A4=E6=8D=AE?= =?UTF-8?q?=E6=8C=89=E4=BB=A3=E7=A0=81=E5=AE=9E=E5=86=B5=E5=86=99=E5=AE=9A?= =?UTF-8?q?=EF=BC=8C=E6=89=8B=E5=86=8C=E4=BE=A7=E5=90=8C=E6=AD=A5=E5=8E=BB?= =?UTF-8?q?=E7=BA=A0=E9=94=99=E5=8F=99=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 判据属主 docs/任务规划/数据可信.md §4.6.2 四处改写: - D2 逐波形系数写全:方波 0.900 闭式 / 0.904 实算,三角 0.573 / 0.578, 并注明真有效值(方波 1.000、三角 0.583)不是判据对象; - D3 拆两档出口:定量档走段文件 fft(同段内 UA 表序 0 与 UN 表序 3 的相对差 ≤2°,段与发生器无绝对相位锁定),定性档 phasor(hb) 只判跟随 80°~100°, 并给出 ×0.1 量化最坏 ≈8° 的算式; - D5 接法改为 DC −1.00 V 保持 ≥5 s 或 1.000 Vrms / 2.0 Hz,写明 20 拍同侧 清零规则(App/algo/adc_health.c)与释放沿前提,50 Hz 档预期就是不锁存; 另补 sticky 语义(撤注入不清、复位后仍不清才是 bug); - D6 峰值预算(基波 0.800 Vrms + 两个 0.160 Vrms,避开 ±2 V 红线)与定量档 fft --ch 4 --all,phasor(hb) 降为"只证这根轨不是恒 0"的旁证。 表头新增分工一行:判据语义在本节,期望值算术与执行步骤在测试说明那篇。 docs/测试说明/ADC前端注入验证流程.md 侧同步九处:开头引导块与 §8 头改成 中性分工裁定,D2 / D3 / D5 卡与 §8.10 第 4 条里"那边写错了 / 等回写"的叙述 删除(只写现状),D5 小标题去掉"接法必须按本卡改"的对照语气。 --- docs/任务规划/数据可信.md | 14 +++++--- docs/测试说明/ADC前端注入验证流程.md | 49 +++++++++++++--------------- 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/docs/任务规划/数据可信.md b/docs/任务规划/数据可信.md index 1fe8e147..2d3499bf 100644 --- a/docs/任务规划/数据可信.md +++ b/docs/任务规划/数据可信.md @@ -307,16 +307,20 @@ typedef struct { #### 4.6.2 判据 D1~D6 +> **分工**:本节定"验什么、过判据是多少";**期望值的算术、接法细节与执行步骤**在 +> `docs/测试说明/ADC前端注入验证流程.md` §8(同一轮次的手册,两边于 2026-10-29 已对齐)。 + | # | 验什么 | 接法 | 读哪 | 过判据 | |---|---|---|---|---| | **D1** | 基波幅值计量 | UA(R31 / `AIN1`)注 1.000 Vrms、50.000 Hz 正弦 | `meas:` 的 `ua_mv`;或 104 `0x4011` | 读数 = 万用表 AC 档读数,偏差 ≤2 %(与静态同档)。⚠ 别用 `phasor(hb)` 行比幅值——那是 ×0.1 定点,注 1 V 只剩 10 格(§4.6.3) | -| **D2** | 读数是否只跟基波走 | 同幅值改方波、三角 | 同上 | 期望值 = **基波分量有效值**(方波基波 = 4/π × 峰值/√2 ≈ 0.90×峰值;三角 = 峰值/√6),**不是**真有效值;偏差 ≤2 %。顺带把"这台仪表读的是基波"这条口径钉进文档——用户拿真有效值表去对会以为差了 10 % | -| **D3** | 相位 | UA 与 UN 两路**同片**(都走 TPAFE)注同幅、同相 | `phasor(hb)` 各行 `Re=` / `Im=` | 两路 `atan2(fIm, fRe)` 之差 ≤2°;改变发生器相位 90°,差值跟着走。**只许比同片内**——跨转换器无同刻性(§9),STM32 那五路还有 1 拍乒乓滞后 | +| **D2** | 读数是否只跟基波走 | 同幅值改方波、三角 | 同上 | 期望值 = **基波分量有效值**(同峰值下的基波 / 峰值比:方波 ≈ **0.90**,闭式 4/π ÷ √2 = 0.900、20 点整周期 DFT 实算 0.904;三角 ≈ **0.573**,闭式 8/π² ÷ √2 = 0.573、实算 0.578),**不是**真有效值(方波 = 1.000、三角 = 0.583 峰值);偏差 ≤2 %。算术与逐波形对照表见 `docs/测试说明/ADC前端注入验证流程.md` §8.5 的 D2 卡。顺带把"这台仪表读的是基波"这条口径钉进文档——用户拿真有效值表去对会以为差了 10 % | +| **D3** | 相位 | UA 与 UN 两路**同片**(都走 TPAFE)注同幅、同相 | **定量档**:录波段文件 `test/host/wave_read.py fft`(浮点重算基波相位;段内表序 `UA` = 0、`UN` = 3);**定性档**:`phasor(hb)` 各行 `Re=` / `Im=` | **定量档**:同段内两路相位差 ≤2°,把发生器相位改 90° 后差值跟到 ≈90°。**定性档只判"跟随"**(差值落进 80°~100°)——`phasor` 出口是 ×0.1 定点,1.000 Vrms 时每格 0.1 V,角度量化最坏 ≈ atan(0.1 / 0.707) ≈ 8°,判不动 2°(§4.6.3)。⚠ 段与发生器**无绝对相位锁定**(触发瞬间钉在段内点 159),只能比**同段内两路**的相对差,跨段比相位或拿发生器相位当绝对基准都不成立。**只许比同片内**——跨转换器无同刻性(§9),STM32 那五路还有 1 拍乒乓滞后 | | **D4** | 测频 | 同 D1 的 1 V/50 Hz | `freq(hb):` 与 104 `0x401D` | **预期就是"不出数"**(幅值门 50 V 在引脚域恒不过,§2.3 #1)→ 这条的判据是"如实报不可测"(`freq_valid=0`、UI `Hz` 显示 `--`、104 上送 IV),**不是**报出一个错误的 50.00。想真验 45~55 Hz 精度(PC 侧判据 0.02 Hz)须临时把 `FREQ_AMP_MIN` 降到引脚域量级(如 0.5 V)单跑一轮 → §7 #8 + §8 风险行 | -| **D5** | 削顶与品质路由(**负向用例**) | 给 UB(R52 / `AIN2`)注同幅正弦 | `cal:` 行的 `rail=` / `edge=`;`rail: edge=…` 的 `[ERR]` 行;104 `0x4012` | 负半周钉下轨 → **连续 20 拍(正好一个 50 Hz 周波)后该路贴轨位锁存**、`edge` 起沿、104 上该路 IV 且值清零。⚠ 这条会**故意造出 `[ERR]`**,而 `@test` 的 forbidden 标记含 `[ERR]` → 本轮要么人工判读、要么把这五路排在最后单独一轮(§8 第一行) | -| **D6** | 谐波轨 | 基波 + 20 % 二次 + 20 % 五次(IA 上注) | `phasor(hb) C04-IA-F2` / `C05-IA-F5` 两行(`log_phasors()` 逐实例打印,实例表加行即自动覆盖) | 谐波轨读数 = 注入分量(±2 %)。⚠ 这两根轨**目前没有任何下游消费者**(`prot_elm.c` 无引用、`meas` 只算 `harm=1`),所以 D6 只证"算得对",**不证"有人用"**——出口归 §9 的谐波轨立项 | +| **D5** | 削顶与品质路由(**负向用例**) | 给 UB(R52 / `AIN2`)注 **DC −1.00 V 并保持 ≥5 s**;或 1.000 Vrms 而频率 **2.0 Hz** 的正弦——两档都能造出"连续贴同一侧轨带"。⚠ **注 50 Hz 同幅正弦造不出锁存,且这是预期而非故障**:1 kHz 下一个周波只 20 点、正半周必落中带,而 `adc_health_rail_watch()` 任何一拍回中带就把计数清零(`App/algo/adc_health.c`),连续贴底最长 ≈ 209/360 × 20 ≈ 11 拍 < 20。TPAFE 侧在 ±2 V 判据上限内造不出这条(轨带 ±32112 码 ≈ ±4.9 V),只有 STM32 那五路能做 | `cal:` 行的 `rail=` / `edge=`;`rail: edge=…` 的 `[ERR]` 行;104 `0x4012`;段文件档另加 `wave_read.py shape` 的三把尺子(`--strict` / 再加 `--clip-superset` / 不带),逐条配法见 `docs/测试说明/ADC前端注入验证流程.md` §8.5 的 D5 卡 | 负半周钉下轨 → **连续 20 拍同侧后该路贴轨位锁存**(`ADC_RAIL_N_RUN = 20`,`App/algo/adc_health.h:174`)、`edge` 起沿、104 上该路 IV 且值清零。锁存另有**释放沿前提**:该通道此前须出现过中带样本 → **先上电见基线摆动、再开发生器输出**,开机就注会测出假不通过。撤掉注入后 `rail` 仍非 0 是 sticky 语义(只有 `adc_health_rail_init()` / 复位清)——**复位后仍不清才是 bug**。⚠ 这条会**故意造出 `[ERR]`**,而 `@test` 的 forbidden 标记含 `[ERR]` → 人工判读、排在最后单独一轮(§8 第一行) | +| **D6** | 谐波轨 | 基波 + 20 % 二次 + 20 % 五次(IA 上注)。⚠ 峰值预算:1.000 Vrms 基波 + 两个 20 % 分量的最坏对齐 ≈ 1.98 V 已顶 ±2 V 红线 → 基波用 **0.800 Vrms**、二次与五次各 0.160 Vrms | **定量档**:段文件 `test/host/wave_read.py fft --ch 4 --all`(`IA` = 段内表序 4,直接给基波与 2 / 5 次谐波 RMS,单位 mV);**旁证**:UI 交流量页 `Ia2` / `Ia5`(1 mV)与 `phasor(hb) C04-IA-F2` / `C05-IA-F5`(×0.1 定点,只证这根轨不是恒 0) | 谐波轨读数 = 注入分量(**±2 %,走定量档**;周期行档判不动:0.160 Vrms 在 ×0.1 出口只有 1~2 格)。⚠ 这两根轨**目前没有任何下游消费者**(`prot_elm.c` 无引用、`meas` 只算 `harm=1`),所以 D6 只证"算得对",**不证"有人用"**——出口归 §9 的谐波轨立项 | -**先做 D1 / D2 / D5**(纯读数、无跨通道约定),D3 / D6 次之,D4 视 §7 #8 的裁定。 +**先做 D1 / D2 / D5**(纯读数、无跨通道约定),D3 / D6 次之,D4 视 §7 #8 的裁定。每条的两档跑法(周期行标量档 / +录波段文件定量档)、触发动作、耗时排布与失败分诊在 `docs/测试说明/ADC前端注入验证流程.md` §8.6。 #### 4.6.3 读数出口(零固件改动) diff --git a/docs/测试说明/ADC前端注入验证流程.md b/docs/测试说明/ADC前端注入验证流程.md index 22037bcf..4ab45e93 100644 --- a/docs/测试说明/ADC前端注入验证流程.md +++ b/docs/测试说明/ADC前端注入验证流程.md @@ -11,10 +11,9 @@ > V0.1 有五路单极性削顶(`UB/UC/IB/IC/IN`),它们在交流轮次里只能作"预期削顶"的负向用例。 > > **判据的属主是 `docs/任务规划/数据可信.md` §4.6.2**(D1~D6 验什么、过判据是多少),本文 §8 只写**怎么跑**。 -> 两处不一致时分两类裁定:**「验什么、算不算过」从 `docs/任务规划/数据可信.md` §4.6.2**;**「期望值怎么算」从本文**。 -> 后者不是抬杠:那边有三处与代码算不拢(D2 的三角系数、D5 的「注 50 Hz 正弦就能造出锁存」、D3 与 D6 在 ×0.1 定点 -> 出口下的分辨率),照它的数字跑会把好板子判成失败。本轮按本文数值跑,更正清单与回写路径见 §8.10 第 4 条——那个 -> 文件的单写者阻塞已随录波并入解除(2026-10-29),但改判据文档是另一个动作面,**要不要现在改它仍等用户裁定**。 +> 两边于 2026-10-29 对齐;若将来再分叉,分两类裁定:**「验什么、算不算过」从 +> `docs/任务规划/数据可信.md` §4.6.2**,**「期望值怎么算、怎么接线怎么跑」从本文 §8**。 +> 这一跳为什么不用等标准源、又证不到什么,见其 §4.6.1 与 §4.6.5,本文不重抄。 --- @@ -286,10 +285,9 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, ## 8. 交流(正弦)动态验证轮次(D1~D6) > **判据的属主是 `docs/任务规划/数据可信.md` §4.6.2**(D1~D6 验什么、过判据是多少),本节只写**怎么跑**: -> 接哪个点、发生器怎么设、读哪一行的哪个字段、期望值怎么算、结果往哪回写。裁定口径同开头引导块: -> **判据语义从 `docs/任务规划/数据可信.md` §4.6.2,期望值的算术从本节**——它与代码算不拢的那三处, -> 回写清单见本文 §8.10 第 4 条;本节按更正后的数值跑。 -> 为什么这一跳不用等标准源、以及它证不到什么,看 `docs/任务规划/数据可信.md` §4.6.1 与 §4.6.5,本节不重抄。 +> 接哪个点、发生器怎么设、读哪一行的哪个字段、期望值怎么算、结果往哪回写。两边于 2026-10-29 对齐;将来 +> 若再分叉,按两类裁定——**「验什么、算不算过」从 `docs/任务规划/数据可信.md` §4.6.2,「期望值怎么算、 +> 怎么接线怎么跑」从本节**。为什么这一跳不用等标准源、以及它证不到什么,看其 §4.6.1 与 §4.6.5,本节不重抄。 > > **引用写法**:本节凡引用判据文档的小节,一律写成「文件名 + 小节号」的紧邻形式;只写 §8.x 的是本文自己的节。 @@ -429,10 +427,9 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, - 判据:读数 = **基波**期望 ±2 %,**不是**真有效值。方波那栏最直观——真 RMS 表会读 ≈1414 mV、 均值响应表读 ≈1570 mV,而板上读 ≈1278 mV,**这正是"这台仪表读的是基波"的证据,不是 bug**; - 所以抬头里必须写清万用表响应类型(§4.6.2 D2 原话:拿真有效值表去对会以为差了 10 %)。 -- ⚠ **`docs/任务规划/数据可信.md` §4.6.2 的 D2 行把三角写成「峰值 / √6」(= 0.408)是错的**:按基波口径应为 - 0.573(闭式)~ 0.578(20 点整周期 DFT)。照那个数判会把好板子判成失败。本轮按本节数值跑, - 更正回写见 §8.10 第 4 条。 + 所以抬头里必须写清万用表响应类型(判据文档同一条:拿真有效值表去对会以为差了 10 %)。 +- 系数的来路:方波基波幅值 = 4/π × 峰值、三角 = 8/π² × 峰值,再除 √2 得基波有效值 → 闭式 **0.900 / 0.573**, + 20 点整周期 DFT 实算 **0.904 / 0.578**;`docs/任务规划/数据可信.md` §4.6.2 的 D2 行与本节同值,算术以本节为准。 记录表: @@ -447,7 +444,7 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, |---|---| | 接法 | `UA`(R31)与 `UN`(R28)**都走 TPAFE**,同幅 1.000 Vrms、同相——发生器单路输出分两路即天然同相;双通道机型则两通道都设 0° | | 读数 | `phasor(hb) C02-UA-F1: Re=.. Im=..` 与 `C03-UN-F1: Re=.. Im=..`,各取 `atan2(Im, Re)` 再比差值 | -| 判据(§4.6.2) | 同相时差值 ≤2°;把发生器(或 B 通道)改 90°,差值跟着走到 ≈90° | +| 判据(§4.6.2) | **定量档**:同相时同段内两路相位差 ≤2°,把发生器(或 B 通道)改 90° 后差值跟到 ≈90°;**定性档只判跟随**(差值落进 80°~100°,容差写进记录表) | | ⚠ 分辨率 | 出口只有 ×0.1 且向下截断:1.000 Vrms 时 Re/Im 各只有 ±10 格,**一格就是 0.1 V** → 角度量化误差最坏 ≈ atan(0.1 / 0.707) ≈ 8°,**≤2° 今天用串口判不出来** | | 三个档 | ① **段文件定量档(首选,零改代码)**:同一次注入触发一段,`test/host/wave_read.py fft` 重算基波相位,比**同段内** `UA`(表序 0)与 `UN`(表序 3)的相位差——浮点重算,不受 ×0.1 量化限制,≤2° 判得动,动作见下面第 2 段;② **定性档**:没取到段时退而用,同相 ≈0°、改 90° 后差值落进 80°~100° 即算跟随,容差写进记录表;③ **抬倍数退路**:临时把 `log_phasors()` 的倍数改成 ×1000 并改掉行尾 `(x0.1)`,跑完按 §8.9 还原——段文件档已落地,这条基本不必走,要走仍**先问用户** | | 红线 | **只许比同片内**:跨转换器无同刻性(V0.1 硬件缺陷)+ STM32 那五路另有 1 拍乒乓滞后 → `docs/硬件平台/ADC通道映射说明.md` §7 | @@ -491,14 +488,13 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, |---|---|---|---|---|---| | 50.000 | | | | | | -#### D5 触底与品质路由(负向用例)——**接法必须按本卡改** +#### D5 触底与品质路由(负向用例) -- **`docs/任务规划/数据可信.md` §4.6.2 原文**:给 `UB`(R52 / `AIN2` / U5.B7)注同幅正弦 → 负半周钉下轨 - → 连续 20 拍后该路贴轨位锁存、`edge` 起沿、104 该路 IV 且值清零。 -- ⚠ **50 Hz 正弦造不出这个锁存**:锁存条件是"连续 `ADC_RAIL_N_RUN = 20` 拍贴同一侧轨带"(`App/algo/adc_health.h:174`), - 而 `adc_health_rail_watch()` 里**任何一拍回到中带就把两侧计数清零**;1 kHz 下一个 50 Hz 周波只有 20 个点、 - 正半周必然落在中带 → 连续贴底最长 ≈ 209°/360° × 20 ≈ **11 拍 < 20** → 永不锁存。 - **这是判据文档与代码的分歧,不是固件缺功能**(更正回写见 §8.10 第 4 条)。 +- **判据**(`docs/任务规划/数据可信.md` §4.6.2 D5):给 `UB`(R52 / `AIN2` / U5.B7)注 **DC −1.00 V 保持 ≥5 s** + 或 **1.000 Vrms / 2.0 Hz** 的正弦 → 负半周钉下轨 → 连续 20 拍同侧后该路贴轨位锁存、`edge` 起沿、104 该路 IV 且值清零。 +- ⚠ **注 50 Hz 同幅正弦造不出这个锁存,这是实现约束不是固件缺功能**:锁存条件是"连续 `ADC_RAIL_N_RUN = 20` 拍 + 贴同一侧轨带"(`App/algo/adc_health.h:174`),而 `adc_health_rail_watch()` 里**任何一拍回到中带就把两侧计数清零**; + 1 kHz 下一个 50 Hz 周波只有 20 个点、正半周必然落在中带 → 连续贴底最长 ≈ 209°/360° × 20 ≈ **11 拍 < 20**。 - **轨带数值**(`App/algo/adc_cal.c:452` 的 `adc_cal_rail_ths()`,TEMP 不监视):STM32 侧贴底 = 码 ≤ **81**(2 % FS ≈ 65 mV)、 贴顶 = 码 ≥ 4013;TPAFE 侧 = ±32112 码(≈ ±4.9 V)→ **TPAFE 侧这一条在 ±2 V 判据上限内根本造不出来**, 只有 STM32 那五路能做。 @@ -508,8 +504,8 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, 只有 `adc_health_rail_init()` / 复位清)→ 复位一次并把"是否随复位清"记进表:这条实测行为是本轮的产出之一。 - **改法 B**:发生器 1.000 Vrms 但**频率设 2.0 Hz** → 负半周 250 ms = 250 拍贴底 ≥ 20 → 锁存; 顺带证"贴轨检测与工频无关"这条口径。 -- **原接法照跑,但判据反过来**:注 50 Hz 正弦这一档仍做,判据是**不锁存属预期**——记录 `adc: UB` 负半周钉 0、 - `rail` 仍 `0x000`,作为 §8.10 第 4 条更正的证据。别把它记成失败。 +- **50 Hz 档照跑,判据就是"不锁存"**:记下 `adc: UB` 负半周钉 0 而 `rail` 仍 `0x000` —— 这一档证的是 + "锁存要连续 20 拍同侧"这条实现约束本身,不是失败,也不是战果。 - **反向假阳性**:反过来注一个峰值 <65 mV(<46 mVrms)的 50 Hz 小信号,会让 `UB` 一路贴底而**锁存**—— 那是"信号小到进不了中带",不是削顶。读 `rail` 时要把这条一起排除。 - **与 `cl` 区分**:`cl=0xE6` 是**来源派生**的静态触底掩码(`docs/任务规划/数据可信.md` §2.6),D5 要的是动态 @@ -638,11 +634,10 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, 任务卡 `docs/任务规划/数据可信.md` §5 的 DoD 就是这么分的。本文只留"怎么跑",**不抄实测数**。 3. **本文回写** → 只在 §8.1 那张表下加一行指针级别的句子("YYYY-MM-DD 已按本节跑通,实测见 …"); 不在本文建第二份结论表,否则两份必然漂移(`docs/文档规范.md` 同一事实只在一处详述)。 -4. **三处要更正 `docs/任务规划/数据可信.md`**(那个文件本轮仍**没动**:`task/wave-record` 已于 2026-10-29 并入 - `v2-on-ui`,单写者目录的阻塞条件已消失;但改判据文档是另一个动作面,仍等用户点头再动): - - §4.6.2 D2 的三角期望值「峰值 / √6」→ 0.573 ~ 0.578(§8.5 的 D2 卡); - - §4.6.2 D5 的"注 50 Hz 正弦 → 连续 20 拍锁存"按代码做不到,改法 A / B 见 §8.5; - - §4.6.2 D3 / D6 的判据在 ×0.1 定点出口下分辨率不足 → 段文件出口现已落地,判据应明写"定量档走段文件、定性档走 `phasor(hb)`"(本文 §8.5 的 D3 / D6 卡已按此写); +4. **判据文档的一致性维护** → 本文 §8 的期望值算术、接法与出口分档已写进 + `docs/任务规划/数据可信.md` §4.6.2(2026-10-29:D2 的逐波形系数、D5 的 DC / 2 Hz 接法与 50 Hz 档的 + "不锁存属预期"、D3 与 D6 的"定量档走段文件、定性档走 `phasor(hb)`")。今后两边若再分叉,按开头引导块的 + 两类裁定处理(语义从那节、算术与步骤从本文),**别在本文另立第二份判据表**。 5. **销项** → `docs/任务规划/数据可信.md` §2.7 与 §6 卡片表里这一条按登记簿「完成即销项」处理,属 Ask first, 须用户明确指示后再做。 From 9fe8f8545d76274a15e7843f880829f0fc7e24de Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 20:05:43 +0800 Subject: [PATCH 07/19] =?UTF-8?q?gate:=20=E7=BC=96=E5=8F=B7=E9=97=A8?= =?UTF-8?q?=E7=A6=81=E7=99=BD=E5=90=8D=E5=8D=95=E7=99=BB=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E6=B3=A2=E6=AE=B5=E6=96=87=E4=BB=B6=E5=90=8D=E5=89=8D=E7=BC=80?= =?UTF-8?q?=20WAVE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts/check_ids_new.py 的 KNOWN_BARE 加 WAVE(附两行注释说明来源: App/algo/wave_store.c 拼名、FTP 根下 /wave/、100 个号轮转)。 此前文档里写段文件名只能改成 WAVE.BIN 绕开裸形态,登记后按字面写。 classify_bare("WAVE00") 由 fail 变放行,--self-test rc=0(5 正例拦下 / 29 反例放行,FOO99 仍 fail)。 docs/门禁说明/id-check-new.md §7 表加一类"固件生成的产物文件名", 并提醒先分清产物名与编号族——加白名单是唯一放宽手段,没有整库豁免入口。 --- docs/门禁说明/id-check-new.md | 1 + scripts/check_ids_new.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/docs/门禁说明/id-check-new.md b/docs/门禁说明/id-check-new.md index 5f84b01c..a960625b 100644 --- a/docs/门禁说明/id-check-new.md +++ b/docs/门禁说明/id-check-new.md @@ -90,6 +90,7 @@ | 遥信/遥控点号(`YX`/`YK`/`YT`/`YH`)与保护元件代号(`OC`/`UV`/`OV`/`CP`…) | `KNOWN_BARE` | 协议数据,不是标识符 | | 活族编号(`FR-*`、`TRAP-*`、`TC-*`、`CODE-RULE-*`、`MENU-STATE-*`) | `KNOWN_DASH` | 新增族要先在此登记 | | 已停发族(登记簿/专项/层次/原则等旧族) | `RETIRED_DASH` → **只 WARN** | 不拦提交,提示改语义短语 | +| 固件生成的产物文件名(录波段号 `WAVE00.BIN` 到 `WAVE99.BIN`) | `KNOWN_BARE` 前缀,2026-10-29 登记 `WAVE` | 换一类产物名再加一条;先分清产物名与编号族 | | 一次性保留 | 行内 `[编号豁免]` | 提交说明里要交代原因 | ## 8. 改这个门禁本身要注意什么 diff --git a/scripts/check_ids_new.py b/scripts/check_ids_new.py index ec765f4e..af1be72b 100644 --- a/scripts/check_ids_new.py +++ b/scripts/check_ids_new.py @@ -79,6 +79,9 @@ KNOWN_BARE = frozenset({ "SFT", # UART 采样模式位名(OVER8) "OVER", + # 录波段文件名:固件生成的 100 个号轮转(WAVE00 到 WAVE99 的 .BIN,App/algo/wave_store.c 拼名 + # FTP 根下 /wave/ 目录;文档与日志里都要写这个字面量,2026-10-29 登记) + "WAVE", }) # --- 前缀后面接 -数字 其实是算术(SIZE-2 / LEN-1),不是编号 From 51d89cf9d30ae2d4e606506f91373d1f66c1936c Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 20:22:47 +0800 Subject: [PATCH 08/19] =?UTF-8?q?gate:=20=E6=8C=89=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E8=A3=81=E5=AE=9A=E6=95=B4=E4=BD=93=E9=80=80=E5=BD=B9=E7=BC=96?= =?UTF-8?q?=E5=8F=B7=E9=97=A8=E7=A6=81=20id-check-new=EF=BC=88=E9=92=A9?= =?UTF-8?q?=E5=AD=90=2052=20=E2=86=92=2051=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 配置:摘掉该钩子,原位置留一条退役注记(不复述数钩子数的字面量)。 YAML 解析口径实测:声明 51 / 带 manual 3 / 提交期实跑 48。 脚本:git mv 进 scripts/archive/id_gate/check_ids_new.py,与已退役的 check_ids.py 同档;docstring 改退役口径,REPO_ROOT 因归档位置深两级 改取 parents[3],失败提示与自述路径同步改写。从归档位置实测: --self-test 5 正例拦下 / 29 反例放行 rc=0;--worktree 与暂存区模式 rc=0。 文档(七份):AGENTS.md 六处——总数、加钩流水(顺带补上 2026-10-29 漏记的 skill-size 一笔)、枚举项删除、本行相加数、提交期实跑、门禁索引 与主题表两行;门禁说明 README 八处——声明数、实跑数、计数流水、grep 陷阱的举例数、§2 标题、姿态表「只查新增」行、两行索引状态;编号规范 七处——引导块改「本文即唯一事实源」、族总表一处、§8 标题与正文、 §8.5 补第④步、④ 之后无机械牙那段、速查;门禁说明 id-check-new.md 整篇重写为退役留档(查什么 / 落地前的噪声回放 / 退役五条理由 / 手工 跑法 / 此后靠什么 / 认下丢了什么);门禁说明 id-check.md 与归档目录 README、check_ids.py 里「现行门禁 = id-check-new」的指针同步改写。 技能(三份):id-family-semantic-migration 一行、stale-doc-count-sweep 的 pitfalls 与 steps 各一行改手工跑法(steps 第 5 条那笔历史计数例不动)。 此后编号形态没有机器面:判据只剩 docs/编号规范.md §1 三句自问、§4.1 族总表与人工评审;2026-10-14 那类"销项删行后号被重新取走"不再有提交期 警报——这条代价已写进留档,别当成"检查没了随便写"。 纯文档与门禁配置,未跑 @build。全程未 push。 --- .../id-family-semantic-migration/SKILL.md | 2 +- .../reference/pitfalls.md | 2 +- .../stale-doc-count-sweep/reference/steps.md | 2 +- .pre-commit-config.yaml | 20 +-- AGENTS.md | 6 +- docs/编号规范.md | 34 +++-- docs/门禁说明/README.md | 20 +-- docs/门禁说明/id-check-new.md | 138 ++++++++---------- docs/门禁说明/id-check.md | 4 +- scripts/archive/id_gate/README.md | 14 +- scripts/archive/id_gate/check_ids.py | 8 +- .../{ => archive/id_gate}/check_ids_new.py | 27 ++-- scripts/archive/id_gate/编号治理历史.md | 2 +- 13 files changed, 134 insertions(+), 145 deletions(-) rename scripts/{ => archive/id_gate}/check_ids_new.py (87%) diff --git a/.dsh/skills/id-family-semantic-migration/SKILL.md b/.dsh/skills/id-family-semantic-migration/SKILL.md index c23bee09..3b04a4ae 100644 --- a/.dsh/skills/id-family-semantic-migration/SKILL.md +++ b/.dsh/skills/id-family-semantic-migration/SKILL.md @@ -12,7 +12,7 @@ version: 1.1.0 | 族形态 | 计数口径来源 | 改名手段 | `scripts/archive/id_gate/id_aliases.json` | |---|---|---|---| | 字母+数字,且在 `scripts/archive/id_gate/check_ids.py` 语料内(`id_registry.json` 有族、族 pattern 能匹配它) | 门禁自身计数(`ck.refs` / `ck.stats` / 报告行) | `scripts/rename_ids.py`(映射表 + 显式 scope + 默认干跑) | **要登记**:死号 `void`、转短语 `retired-to-phrase` | -| 中文前缀(「卡」+ 数字)、纯散文里的号、圈号 ①②③ | **自建带标签正则扫描器**(门禁看不见它,2026-10-28 退役前的 id-check 全库报告与本族无关;现行 id-check-new 只查新增行) | 逐处替换 + 断言命中数(`rename_ids.py` 靠族 pattern 驱动,这里用不上) | **刻意不登记**:登记了机器也看不见,只多一张没人核的表;停发只靠规范 + 评审 | +| 中文前缀(「卡」+ 数字)、纯散文里的号、圈号 ①②③ | **自建带标签正则扫描器**(门禁看不见它,2026-10-28 退役前的 id-check 全库报告与本族无关;两道门禁现都已退役(2026-10-28 / 2026-10-29),要普查形态就手工跑归档脚本 scripts/archive/id_gate/check_ids_new.py --range) | 逐处替换 + 断言命中数(`rename_ids.py` 靠族 pattern 驱动,这里用不上) | **刻意不登记**:登记了机器也看不见,只多一张没人核的表;停发只靠规范 + 评审 | 怎么判(三条,任一为否即无识别面):① `grep -nE '"name"|"pattern"' scripts/archive/id_gate/id_registry.json`,没有任何族 pattern 能匹配该形态;② `./.venv/Scripts/python.exe scripts/archive/id_gate/check_ids.py --top 0` 的任何校验里找不到它;③ registry 的 `blind_spots` 已明写不判(圈号与正常列举无法机械区分、裸形同字母多义只在属主登记处判)。实测:「卡」+ 数字**没有族**(中文形态只有 `malformed` 里 `陷阱 ?[0-9]` 那条禁令,管的是陷阱编号不是卡号),该族真实站点 288 处 / 56 文件,而门禁对它贡献 0。 diff --git a/.dsh/skills/stale-doc-count-sweep/reference/pitfalls.md b/.dsh/skills/stale-doc-count-sweep/reference/pitfalls.md index e356a8dc..7459efa4 100644 --- a/.dsh/skills/stale-doc-count-sweep/reference/pitfalls.md +++ b/.dsh/skills/stale-doc-count-sweep/reference/pitfalls.md @@ -20,4 +20,4 @@ - **误报跟着改**:「< 40KB」是绘制缓冲红线,与页数无关。 - **带日期戳的测量臆改新数**:「2026-10-26 实跑 …覆盖 40 页」改成 47 就是把历史测量冒充现值;只加「当时」。 - **用截断的 grep 输出判定**:`cut -c` 截掉的后半句常含日期戳/口径,据此分类必错。 -- **新写文本撞编号门禁**:新增的「单字母+数字」裸形(如 `SD2506`、`CP56Time2a`)会被 `id-check-new`(只查 `git diff` 新增行、零语料)拦下;旧的八项全库校验 `id-check` 已于 2026-10-28 退役进 `scripts/archive/id_gate/`。撞到就把自己这批文本逐处归因或改形态描述,别去放宽白名单。 +- **新写文本别再造编号**:两道编号门禁都已退役(全库八项校验 2026-10-28、只查新增行 2026-10-29,脚本与白名单在 `scripts/archive/id_gate/`),**撞号不再有机器报警**,只剩规范与评审——所以这批文本的形态自查要自己做(`docs/编号规范.md` §1 三句;要机器帮忙就手工跑 `scripts/archive/id_gate/check_ids_new.py --worktree`)。 diff --git a/.dsh/skills/stale-doc-count-sweep/reference/steps.md b/.dsh/skills/stale-doc-count-sweep/reference/steps.md index 26826557..9f29aad5 100644 --- a/.dsh/skills/stale-doc-count-sweep/reference/steps.md +++ b/.dsh/skills/stale-doc-count-sweep/reference/steps.md @@ -81,7 +81,7 @@ ```bash & '.venv\Scripts\python.exe' scripts/check_doc_links.py --all --sections-strict # 期望「文档路径检查通过(N 个文档)」 - & '.venv\Scripts\python.exe' scripts/check_ids_new.py --self-test # 新增行编号门禁(旧 check_ids.py 已退役进 scripts/archive/id_gate/) + & '\.venv\Scripts\python.exe' scripts/archive/id_gate/check_ids_new.py --range A..B # 编号形态普查(两道门禁 2026-10-28 / 2026-10-29 都已退役,不再是提交钩子) & '.venv\Scripts\python.exe' scripts/ci_local.py # 或 pre-commit run --files <被改文件> ``` diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0bad2054..c7f863bc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -680,20 +680,12 @@ repos: # 61 笔被编号体系牵连),脚本与语料整体移入 `scripts/archive/id_gate/` # (保留可复现性,**不再进提交门禁**)。退役路线与判据替换见规范 §8.5。 - # 14) 编号门禁(2026-10-28 起是**唯一**的编号门禁):只查**新增行**,零语料 - # 背景:语义标识已是默认形态,全库语料审计的成本高过收益(见上条历史)。 - # 判据:看 git diff 的新增行里有没有**未登记形态**的「字母+数字」标识 - # (自造前缀直接 FAIL;已停发族只 WARN);白名单在脚本里,按实测普查枚举、几乎不增长。 - # 自检:`python scripts/check_ids_new.py --self-test`;范围自查用 `--range A..B`。 - # ⚠ 计数口径(2026-10-28,一增一减写成同日的两个阶段,净零):本条落地使 YAML 解析条目 - # 51 → 52,同日摘掉退役的旧语料审计钩子后 52 → **51**。口径与自查命令、「声明 / 文档 / - # 实跑」三数同源核对 → docs/门禁说明/README.md §2 首条(本文件注释与 AGENTS 必须同批改)。 - - id: id-check-new - name: 编号门禁·新增行(零语料,拦截新造编号) - entry: python scripts/check_ids_new.py - language: system - files: \.(md|c|h|py|ya?ml|txt)$|^\.pre-commit-config\.yaml$ - pass_filenames: false + # 14) 编号门禁(2026-10-28 上线只查新增行那一道;2026-10-29 用户裁定**整体退役**,钩子已摘) + # 退役后编号形态没有机器面:判据回到 docs/编号规范.md §1 三句自问 + §4.1 族总表 + 评审。 + # 脚本移入 scripts/archive/id_gate/check_ids_new.py(手工可跑,--self-test 保留),留档说明 + # → docs/门禁说明/id-check-new.md;两步路线 → docs/编号规范.md §8.5。 + # ⚠ 计数口径:本条摘除使 YAML 解析条目 52 → **51**,须与 AGENTS.md 的「共 N 个钩子」、 + # docs/门禁说明/README.md §2 标题三数同源同批改(自查命令见该条;别在注释里写数钩子的字面量)。 # 14) 分层方向门禁(2026-10-22 加,结构债的"防扩大"那一半) # 背景:AGENTS.md 与 项目架构总览.md 早就写着"六层单向依赖",但**从来没有机械检查** diff --git a/AGENTS.md b/AGENTS.md index cd4a3859..7594e8aa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,7 +93,7 @@ ## 提交门禁(pre-commit) -- 任一失败即拦截,共 **52 个钩子**(**口径 = YAML 解析后的 hook 条目数**:自查命令与「声明 / 文档 / 实跑」三数同源核对见 `docs/门禁说明/README.md` §2 首条——⚠ 直接 grep 配置里的声明行会多算一条**注释掉的**候选钩子(比口径多 1),不是口径;**配置注释里也别复述那个 grep 字面量**,它会让文本计数再虚高 1(2026-10-27 实测踩过);加一条钩子就同批改 AGENTS 与门禁索引两处计数。2026-10-25 道E 连加 `bom-guard` / `setview-check` / `uinav-selftest` 三条,2026-10-26 加 `generated-fresh` 一条,2026-10-27 加 `nested-comment` 一条;2026-10-28 加 `id-check-new` 一条并同批退役 `id-check`(口径 52 → 51:编号门禁换牙——新的只查新增行、零语料,旧的八项全库校验连语料一起移入 `scripts/archive/id_gate/`,路线见 `docs/编号规范.md` §8.5);**每种门禁一篇详解(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径)→ `docs/门禁说明/README.md` 索引**;通用机制与整体口径 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `id-check-new`(**编号门禁**:只查 `git diff` 新增行里有没有未登记形态的「字母+数字」标识,零语料、零维护;白名单是前缀级两张表,见 `docs/门禁说明/id-check-new.md`。旧的八项全库校验 `id-check` 已于 2026-10-28 退役,脚本与两份语料移入 `scripts/archive/id_gate/`,只在手工复查存量时才跑;退役路线见 `docs/编号规范.md` §8.5)+ `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + `bom-guard` / `setview-check` / `uinav-selftest`(2026-10-25 道E 那三条:前半句的加钩流水里有它们、逐项枚举里漏了,补上后本行各项相加才等于 52)+ 主机侧单测 **22 根专属钩子**(`-unit-test` 后缀 = 有独立 `files:` 触发面、需要秒级归因的那个子集;2026-10-25 增补 adc-cal / prot-dim / vol-idle-sync / ui-data / meas / run-par 六根)——**这是口径不是缺口**:`test/host/` 实测 44 套带 `run_unit_test.py`,其余 **22 套**(含 `test/host/ui_sink` 与录波三套)没有专属钩子,但每次 CI / `scripts/ci_local.py` 都被 `test-coverage` 总闸全量真跑并守基线回退,**失败照样拦**,只是归因落在总闸输出上;**没有专属钩子 ≠ 没被跑**,22 套台账与补钩子的判据 → `docs/门禁说明/host-unit-tests.md` §6 第 4 条。**Lead 裁定 2026-10-27:不补第 23 根**——读「这套跑没跑」去看总闸输出,别数钩子(原写「23 个 / 含 ui-sink 七个」是双重错:既把 22 说成 23,又宣称 ui-sink 有钩子——配置历史里 `ui-sink-unit-test` 从未存在)+ `test-coverage`(**覆盖率总闸**:跑全部 44 套并守基线回退;缺 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`)+ `nested-comment`(**块注释嵌套**:注释体内再出现 /* 会被 ARMCC 报 9-D,且**按包含者数量刷屏**、只在重编译该 TU 时才暴露;本仓诱因就是「在注释里写作用域/通配路径」,改法是写全目录或用尖括号 ;接线当日实扫 403 个 .c/.h,新发 0、存量 2 处(`test/host/common/` 两份假件头)钉只减不增基线 → `docs/门禁说明/nested-comment.md`)+ `skill-size`(**SKILL.md 不超插件写侧上限 65536 字节**:dsh-mnemon 只在写侧校验、加载侧不查,超限技能照常可读却每次「生成草案」被拒、永久卡在「生成中」;另查 `reference/` 指针悬空与 BOM → `docs/门禁说明/skill-size.md`)+ `mem-budget`(RAM 92% 预算门禁,登记簿条目「缩堆迁 CCM 后半(暂缓)」)+ `font-charset`(UI 字符集差集检查,防缺字白框)+ `ui-golden`(UI 像素回归,菜单 8 状态 + 内容页 66 张黄金参考)+ `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 条**))。**提交期实跑 49 根**(2026-10-28 用户裁定):`cppcheck` / `test-cppcheck` / `test-coverage` 三根带 `stages: [manual]`——实测合计 ~165 s = 一次带代码提交耗时的三分之二,提交期只跑快的那些;这三根由**远端 CI** 强制(`.gitea/workflows/ci.yml` 的「手动阶段门禁」步,逐根 `--hook-stage manual`)与 `scripts/ci_local.py` 的 manual 段,本地手工复跑 `python -m pre_commit run --all-files --hook-stage manual `(一次只能一个 id) +- 任一失败即拦截,共 **51 个钩子**(**口径 = YAML 解析后的 hook 条目数**:自查命令与「声明 / 文档 / 实跑」三数同源核对见 `docs/门禁说明/README.md` §2 首条——⚠ 直接 grep 配置里的声明行会多算一条**注释掉的**候选钩子(比口径多 1),不是口径;**配置注释里也别复述那个 grep 字面量**,它会让文本计数再虚高 1(2026-10-27 实测踩过);加一条钩子就同批改 AGENTS 与门禁索引两处计数。2026-10-25 道E 连加 `bom-guard` / `setview-check` / `uinav-selftest` 三条,2026-10-26 加 `generated-fresh` 一条,2026-10-27 加 `nested-comment` 一条;2026-10-28 加 `id-check-new` 一条并同批退役 `id-check`(口径 52 → 51:编号门禁换牙——新的只查新增行、零语料,旧的八项全库校验连语料一起移入 `scripts/archive/id_gate/`,路线见 `docs/编号规范.md` §8.5);2026-10-29 加 `skill-size` 一条(51 → 52),同日按用户裁定**退役 `id-check-new`**(52 → **51**:只查新增行的编号门禁整体退场,脚本与白名单一起移入 `scripts/archive/id_gate/`,编号形态此后无机器面,留档 `docs/门禁说明/id-check-new.md`);**每种门禁一篇详解(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径)→ `docs/门禁说明/README.md` 索引**;通用机制与整体口径 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers`+ `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + `bom-guard` / `setview-check` / `uinav-selftest`(2026-10-25 道E 那三条:前半句的加钩流水里有它们、逐项枚举里漏了,补上后本行各项相加才等于 51)+ 主机侧单测 **22 根专属钩子**(`-unit-test` 后缀 = 有独立 `files:` 触发面、需要秒级归因的那个子集;2026-10-25 增补 adc-cal / prot-dim / vol-idle-sync / ui-data / meas / run-par 六根)——**这是口径不是缺口**:`test/host/` 实测 44 套带 `run_unit_test.py`,其余 **22 套**(含 `test/host/ui_sink` 与录波三套)没有专属钩子,但每次 CI / `scripts/ci_local.py` 都被 `test-coverage` 总闸全量真跑并守基线回退,**失败照样拦**,只是归因落在总闸输出上;**没有专属钩子 ≠ 没被跑**,22 套台账与补钩子的判据 → `docs/门禁说明/host-unit-tests.md` §6 第 4 条。**Lead 裁定 2026-10-27:不补第 23 根**——读「这套跑没跑」去看总闸输出,别数钩子(原写「23 个 / 含 ui-sink 七个」是双重错:既把 22 说成 23,又宣称 ui-sink 有钩子——配置历史里 `ui-sink-unit-test` 从未存在)+ `test-coverage`(**覆盖率总闸**:跑全部 44 套并守基线回退;缺 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`)+ `nested-comment`(**块注释嵌套**:注释体内再出现 /* 会被 ARMCC 报 9-D,且**按包含者数量刷屏**、只在重编译该 TU 时才暴露;本仓诱因就是「在注释里写作用域/通配路径」,改法是写全目录或用尖括号 ;接线当日实扫 403 个 .c/.h,新发 0、存量 2 处(`test/host/common/` 两份假件头)钉只减不增基线 → `docs/门禁说明/nested-comment.md`)+ `skill-size`(**SKILL.md 不超插件写侧上限 65536 字节**:dsh-mnemon 只在写侧校验、加载侧不查,超限技能照常可读却每次「生成草案」被拒、永久卡在「生成中」;另查 `reference/` 指针悬空与 BOM → `docs/门禁说明/skill-size.md`)+ `mem-budget`(RAM 92% 预算门禁,登记簿条目「缩堆迁 CCM 后半(暂缓)」)+ `font-charset`(UI 字符集差集检查,防缺字白框)+ `ui-golden`(UI 像素回归,菜单 8 状态 + 内容页 66 张黄金参考)+ `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 条**))。**提交期实跑 48 根**(2026-10-28 用户裁定):`cppcheck` / `test-cppcheck` / `test-coverage` 三根带 `stages: [manual]`——实测合计 ~165 s = 一次带代码提交耗时的三分之二,提交期只跑快的那些;这三根由**远端 CI** 强制(`.gitea/workflows/ci.yml` 的「手动阶段门禁」步,逐根 `--hook-stage manual`)与 `scripts/ci_local.py` 的 manual 段,本地手工复跑 `python -m pre_commit run --all-files --hook-stage manual `(一次只能一个 id) - **范围 = 项目代码**:第三方(`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/` @@ -176,8 +176,8 @@ | 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`(52 个钩子 → 29 篇索引,其中 1 篇是退役留档 + 1 指针) | -| **要不要给某个东西发编号**(默认:不发,写语义短语)/ 允许用号的 7 个族(`TC-*` 已停发——「停发」不等于「暂缓」,也不等于「可以清号」)/ 编号门禁现状(`id-check-new`:只查新增行;旧的八项校验已退役) | `docs/编号规范.md`(§1 先问三句、§4.1 族总表〔穷举〕与『已停发』栏、§8 门禁现状与退役路线) | +| **每个提交门禁一篇详解**(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径) | `docs/门禁说明/README.md`(51 个钩子 → 29 篇索引,其中 2 篇是退役留档 + 1 指针) | +| **要不要给某个东西发编号**(默认:不发,写语义短语)/ 允许用号的 7 个族(`TC-*` 已停发——「停发」不等于「暂缓」,也不等于「可以清号」)/ 编号门禁现状(**两道都已退役**:全库八项校验 2026-10-28、只查新增行 2026-10-29;此后无机器面,只靠 §1 三句 + 评审) | `docs/编号规范.md`(§1 先问三句、§4.1 族总表〔穷举〕与『已停发』栏、§8 门禁现状与退役路线) | | 提交门禁通用机制(阶段 / stash 陷阱 / 触发面 / 增量拦截三段式) | `docs/代码规范/pre-commit使用说明.md` §2 | | 代码规范 V1.0 / cppcheck 口径 | `docs/代码规范/嵌入式C语言代码规范_V1.0.md`、`cppcheck使用说明.md` | | 自动化测试 / 实机测试 / 板级套件 | `docs/测试说明/自动化测试流程.md`、`实机测试实施指南.md`、`board-test-suite` skill | diff --git a/docs/编号规范.md b/docs/编号规范.md index 8b91180d..cd10264a 100644 --- a/docs/编号规范.md +++ b/docs/编号规范.md @@ -9,9 +9,10 @@ > 2026-10-26 起两批清理把编号字面量从 **10,956 压到 8,230 处**,2026-10-27 复测为 **7,828 处**(`编号治理历史.md` §8.1 有口径说明); > 而清掉的号里绝大多数从来没被任何脚本读过一次。沿革与教训留在 `编号治理历史.md` §8.3 与 git log,正文不复述。 > -> **机器面才是事实源**:现行门禁 `scripts/check_ids_new.py` 的两张**前缀级白名单**(`KNOWN_DASH` / `KNOWN_BARE`) -> 是 §4.1 的机器投影;本文与白名单冲突时以白名单为准,回来改本文(详解 `docs/门禁说明/id-check-new.md`)。 -> 旧的族注册表 / 别名表 / 八项校验脚本已随全库审计退役进 `scripts/archive/id_gate/`,只在手工复查存量时用。 +> **本文就是唯一事实源**:两道编号门禁已分别于 2026-10-28(全库八项校验)与 2026-10-29(只查新增行) +> 退役,脚本、两张前缀白名单与族语料都在 `scripts/archive/id_gate/`,**不再进提交门禁**。 +> 形态判据只剩本文 §1 的三句自问 + §4.1 族总表 + 人工评审;留档 `docs/门禁说明/id-check-new.md` +> 与 `docs/门禁说明/id-check.md`,路线见 §8.5。归档脚本随时可手工普查形态(`--range A..B`)。 --- @@ -130,7 +131,7 @@ | 登记簿 `N-x` / `S-x` / `T-x`、专项 `A-x`、已删报告的 UI 簇 `U1~U5`(2026-10-26 方案 B) | 语义短语——**登记簿表首格本身就是短语**,如「双转换器无共同采样触发」「CI 远端闭环未验证」 | `scripts/archive/id_gate/id_aliases.json` 标 `retired-to-phrase` | | 网络设计条目 `F-NET/F-DRV/F-MS-`(12 个,2026-10-27 批次一) | 「`编号治理历史.md` §7 + 条目名」短语,如 `(`编号治理历史.md` §7「全局读即清搁浅事件」)`;条目名的权威清单 = `docs/网络/网络自愈架构设计.md` `编号治理历史.md` §7「条目名」列;那个 DRV 段孤儿号按本节末句**删号留事实**、未补定义 | `scripts/archive/id_gate/id_aliases.json` 标 `retired-to-phrase`(13 条)+属主文档 `编号治理历史.md` §7 编号列仅作历史对照 | | 「数据可信」任务卡号(中文前缀「卡」+ 数字,形态见 `编号治理历史.md` §9.3;2026-10-27 批次五) | §5 表的**标识列直接写任务名**(「端子/子板变比确认」「两点标定与精度验收」),代码注释与文档写语义短语(「标定折算落地」「未标定闭锁」「遥测快照发布」…);**逐号换算表见本文 `编号治理历史.md` §9.3** | ⚠ **机器面空白**:中文前缀不进 `scripts/archive/id_gate/check_ids.py` 语料(族 pattern 全是拉丁形态)→ 门禁咬不到,今天只有本表 + `编号治理历史.md` §9.3 + 评审三条兜;`id_aliases.json` 刻意不登记(登记了机器也看不见,只多一张没人核的表) | -| 测试验证层级号(`L0`、`L0.5`、`L1~L3` 这一族,**注意与 UI 设计令牌层 L0、硬件位号 L1~L3、采集链旧层名等同形族无关**;2026-10-28 批次六) | 文档与 CI 步骤名写语义名(静态门禁 / 主机侧纯逻辑单测 / 板端测试套件 / 主机侧回归 / 人工目检);ASCII 短码(static / preflight / commitmsg / size / host-unit / coverage)**只进**日志标签、命令行旗标、CI job id、日志文件名 | ⚠ **同形族警示**:逐号换算表与判族表见 `scripts/archive/id_gate/编号治理历史.md` §10 与 §10.1(撞族六个,动手前先判族);本族虽是拉丁形态但**门禁咬不到**——`id-check` 同日退役、现行 `id-check-new` 只看新增行里的「字母+数字」形态,停发只靠本表 + 归档换算表 + 评审 | +| 测试验证层级号(`L0`、`L0.5`、`L1~L3` 这一族,**注意与 UI 设计令牌层 L0、硬件位号 L1~L3、采集链旧层名等同形族无关**;2026-10-28 批次六) | 文档与 CI 步骤名写语义名(静态门禁 / 主机侧纯逻辑单测 / 板端测试套件 / 主机侧回归 / 人工目检);ASCII 短码(static / preflight / commitmsg / size / host-unit / coverage)**只进**日志标签、命令行旗标、CI job id、日志文件名 | ⚠ **同形族警示**:逐号换算表与判族表见 `scripts/archive/id_gate/编号治理历史.md` §10 与 §10.1(撞族六个,动手前先判族);本族虽是拉丁形态但**门禁咬不到**——两道编号门禁已退役(全库校验 2026-10-28、新增行检查 2026-10-29),停发只靠本表 + 归档换算表 + 评审 | | `E-xx` 审计发现、`P0-x`~`P3-x` 审计条目、报告六族 `D/R/F/N/U/S`、复审残号 `S1~S14`/`A-x`/`B1~B10`/`T-x`、模拟器差距 `H-x`/`L-x`、`TM-*` 阶段号、`DP-*`、`gen`/`round`/会话临时号 | 写条目本身("原子快照取数"、"队头阻塞"、"台架日志代次 288"、"第 3 轮台架") | 别名表标 `void`;**部分死号故意不进别名表**,好让门禁继续咬残留引用 | | 规范曾规划却从未启用的前缀族(`DEF`/`WI`/`DR`/`OQ`/`RSK`/`GAP`/`SC`/`GEN`/`MS`/`UL`/`LAT`/`CS` 等) | 同上,写事实 | 无定义,本就不该存在 | @@ -175,10 +176,11 @@ --- -## 8. 门禁(2026-10-28 起:只有一道) +## 8. 门禁(2026-10-29 起:一道也没有) -**现行门禁 = `id-check-new`**(钩子同名):只查 `git diff` 的**新增行**里有没有未登记形态的 -「字母+数字」标识;零语料、零维护。判据、白名单与盲区 → `docs/门禁说明/id-check-new.md`。 +**现行没有编号门禁**:2026-10-29 用户裁定把最后那道只查新增行的 `id-check-new` 也摘了(钩子口径 +52 → 51,脚本与白名单移入 `scripts/archive/id_gate/check_ids_new.py`,留档 `docs/门禁说明/id-check-new.md`, +理由与手工跑法都在那篇)。判据回到本文 §1 三句自问、§4.1 族总表与人工评审。 本节以下(`编号治理历史.md` §8.1~`编号治理历史.md` §8.4)是**已退役**的全库语料审计(`id-check`,八项校验)的留档: 脚本与两份语料已整体移入 `scripts/archive/id_gate/`,不再进提交门禁, @@ -186,18 +188,21 @@ 八项校验一句话版(历史口径):① 未登记前缀 ② 形态不合规 ③ 悬空引用 ④ 重复定义 ⑤ 冻结族越游标 ⑥ 编号未紧邻文档名 ⑦ 孤儿条目(告警)⑧ 可判定比例 + 两根护栏。 -### 8.5 门禁退役路线(2026-10-28 裁定) +### 8.5 门禁退役路线(2026-10-28 裁定,2026-10-29 收口) -目标不是"关掉检查",而是把成本从**养 99 族语料**降到**禁止新编号**。三步,每步可独立回滚: +目标不是"关掉检查",而是把成本从**养 99 族语料**降到**禁止新编号**,最后一步是承认连这点机器面 +也不划算(2026-10-29 用户裁定)。四步,每步可独立回滚: | 步 | 内容 | 状态 | |---|---|---| | ① | 两根护栏(比例基线 / 不可判定绝对数上限)**转报告制**:照打数字,不再影响退出码 | **已完成 2026-10-28** | -| ② | 用 **diff 级新号拦截**替换语料审计:只看新增行里命中"字母+数字"形态、且不在白名单里的字面量,FAIL 并提示改成语义短语;白名单 = 前缀级两张表(活族 11 + 器件/协议/点号约 90),基本不增长 | **已完成 2026-10-28**(`scripts/check_ids_new.py` + 钩子 `id-check-new`,与 `id-check` **双跑观察期**;详解 `docs/门禁说明/id-check-new.md`) | -| ③ | 退役语料:99 族语料+脚本整体移入 `scripts/archive/id_gate/`(保留可复现性,不再进提交门禁);`check_ids.py` 降为手工报告工具,钩子摘除(口径 52 → 51) | **已完成 2026-10-28**(用户裁定跳过观察期直接做;`docs/门禁说明/id-check-new.md` 是唯一现役编号门禁) | +| ② | 用 **diff 级新号拦截**替换语料审计:只看新增行里命中"字母+数字"形态、且不在白名单里的字面量,FAIL 并提示改成语义短语;白名单 = 前缀级两张表(活族 11 + 器件/协议/点号约 90),基本不增长 | **已完成 2026-10-28**(脚本 + 钩子 `id-check-new`,与 `id-check` **双跑观察期**;脚本今在 `scripts/archive/id_gate/check_ids_new.py`,详解 `docs/门禁说明/id-check-new.md`) | +| ③ | 退役语料:99 族语料+脚本整体移入 `scripts/archive/id_gate/`(保留可复现性,不再进提交门禁);`check_ids.py` 降为手工报告工具,钩子摘除(口径 52 → 51) | **已完成 2026-10-28**(用户裁定跳过观察期直接做;当时 `id-check-new` 是唯一现役编号门禁) | +| ④ | 退役最后那道新增行拦截:钩子摘除(口径 52 → 51)、脚本与两张白名单移入 `scripts/archive/id_gate/`;此后**编号形态无机器面** | **已完成 2026-10-29**(用户裁定;留档 `docs/门禁说明/id-check-new.md`,含拦下过什么与为什么退) | -**在 ② 落地之前,校验 1~6 仍是唯一的机械牙**:别因为"两根线不拦了"就顺手写号—— -新增的未登记前缀照样当场 FAIL(实测:在一个新文件里写一个自造前缀 + 数字,校验 1 与增量拦截双拦)。 +**④ 之后没有任何机械牙**:别把"门禁不响了"读成"可以随便写号"——自造前缀在评审里照样扎眼, +而 `scripts/archive/id_gate/check_ids_new.py --range A..B` 随时能把一批改动重扫一遍(它退成了工具, +不是消失了)。要恢复机器面也便宜:把脚本放回 `scripts/` 并重新挂一条钩子(口径 +1,同批改三处计数)。 --- @@ -208,7 +213,8 @@ - 想给文档内步骤或测试阶段发号 → 不许,写「步骤 3」「阶段 7」(§3.3)。 - 想新增前缀族 → 先回答"哪个脚本要按字面量匹配它"(§4.1);答不出就别建。 - 要改号 → `scripts/rename_ids.py` + `编号治理历史.md` §7.3 五步。 -- 门禁报错读不懂 → `docs/门禁说明/id-check.md`。 +- 编号形态拿不准 → 本文 §1 三句自问;两道门禁的历史与手工跑法 → `docs/门禁说明/id-check.md`、 + `docs/门禁说明/id-check-new.md`。 --- diff --git a/docs/门禁说明/README.md b/docs/门禁说明/README.md index 19d72abb..884f2e85 100644 --- a/docs/门禁说明/README.md +++ b/docs/门禁说明/README.md @@ -1,7 +1,7 @@ # 门禁逐项说明(每种门禁一篇) -> 本目录逐项说明各门禁:**声明的 52 个钩子到底在干什么、拦谁、放谁、什么时候会骗人**——其中 3 根慢钩子带 -> `stages: [manual]`,一次 `git commit` 只实跑 49 根(口径见下条)。 +> 本目录逐项说明各门禁:**声明的 51 个钩子到底在干什么、拦谁、放谁、什么时候会骗人**——其中 3 根慢钩子带 +> `stages: [manual]`,一次 `git commit` 只实跑 48 根(口径见下条)。 > 每个门禁一篇,八段固定结构:查什么 / 为什么要它 / 触发面 / 报错怎么读 / 好处 / > **限制与盲区** / 豁免与基线 / 改门禁本身要注意什么。**最后一节是这套文档存在的理由**—— > 一份只看"拦了什么"的门禁文档,会让人以为绿就是对的。 @@ -9,10 +9,12 @@ - 配置事实源:`.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']))"` - → **52**(2026-10-26:49 → 50 加 `generated-fresh`;2026-10-27:50 → 51 加 `nested-comment`; - 2026-10-28:51 → 52 加 `id-check-new`、随即 52 → 51 摘掉 `id-check`(退役全库语料审计);2026-10-29:51 → 52 加 `skill-size`(SKILL.md 写侧上限与 reference 指针,详见 [`skill-size.md`](skill-size.md)))。 + → **51**(2026-10-26:49 → 50 加 `generated-fresh`;2026-10-27:50 → 51 加 `nested-comment`; + 2026-10-28:51 → 52 加 `id-check-new`、随即 52 → 51 摘掉 `id-check`(退役全库语料审计);2026-10-29:51 → 52 加 `skill-size`(SKILL.md 写侧上限与 reference 指针,详见 [`skill-size.md`](skill-size.md)); + 同日 52 → **51** 退役 `id-check-new`——只查新增行的编号门禁整体退场,脚本与白名单移入 + `scripts/archive/id_gate/`,此后**编号形态没有机器面**(留档 [`id-check-new.md`](id-check-new.md)))。 ⚠ **别用 `grep -c` 去数配置里的声明行**:该文件里有一条**注释掉的**候选钩子(clang-format,第 65 行), - 文本 grep 会多算 1 = 52——「声明 52 / 文档 51 / 实跑 51」这种三个数不同源的争执就是这么来的。真要 grep, + 文本 grep 会多算 1(当前口径 51 会数成 52)——「声明 52 / 文档 51 / 实跑 51」这种三个数不同源的争执就是这么来的。真要 grep, 用**锚定行首缩进**的模式(它天然跳过以 # 开头的注释行)2026-10-28 实测直接得 **51**(当时口径),与 YAML 口径一致; 会多算 1(比口径多 1)的是**不锚定行首、按子串**匹配声明行的写法(那条被注释掉的候选也被它算进去)。 ⚠ 反过来的坑也真踩过(2026-10-27):在配置**注释里复述那个 grep 字面量**,会让文本 grep 再虚高 1—— @@ -39,12 +41,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`、22 个主机单测、官方 5 个、`gitlint` | 只能改代码或改文档;**不要**找豁免开关 | | **增量拦截** | 规则恒生效,但存量钉在基线/台账里,只许减不许增 | `layer-deps`(3 条,2026-10-25 还清 UI 记录 3 条)、`bsp-err`(38 条)、`generated-fresh`(5 处 `docs/images/` 配图存量,脚本内 `BASELINE`)、`nested-comment`(2 处主机侧假件头,`test/host/common/`,脚本内 `BASELINE`) | 报错先分清是**新增**还是**存量搬家**:`--verbose` 看归类,`--prune` 看已还清 | -| **只查新增** | 只看本次改动的增量,没有存量基线,也没有台账 | `id-check-new` | 改成语义短语,或把设备名/协议名登记进脚本白名单(行内 `[编号豁免]` 是最后手段) | +| **只查新增** | 只看本次改动的增量,没有存量基线,也没有台账 | ~~`id-check-new`~~(**2026-10-29 退役**,脚本在 `scripts/archive/id_gate/`;这一姿态已无现役门禁) | 手工复查时才谈得上:改成语义短语(门禁不再跑,行内 `[编号豁免]` 也随之失效) | ⚠ 增量拦截最容易误会的一点:**绿灯不等于没问题**,只等于"没有比上次更差"。 每个增量拦截门禁的基线数字都写进它自己的文档,读那篇时第一眼看的就是"现在在册多少条"。 -## 2. 索引(52 个 hook → 29 篇,其中 1 篇是已退役门禁的留档 + 1 根只有本表指针(专篇待补)+ 1 根详解在 docs/测试说明/) +## 2. 索引(51 个 hook → 29 篇,其中 2 篇是已退役门禁的留档 + 1 根只有本表指针(专篇待补)+ 1 根详解在 docs/测试说明/) > 带 `stages: [manual]` 的三根(`cppcheck` / `test-cppcheck` / `test-coverage`)**提交期不跑**,只在远端 CI 的 > 「手动阶段门禁」步与 `scripts/ci_local.py` 的 manual 段跑;下表对应两行的「文档」列已标注。 @@ -53,8 +55,8 @@ |---|---|---|---|---| | [`bsp-err.md`](bsp-err.md) | `bsp-err` | BSP 的对外 API 面:错误通道类型、超时宏命名、必须写返回值、返回值不得裸丢弃 | `Drivers/BSP/**` 或门禁自身 | 已写 | | [`layer-deps.md`](layer-deps.md) | `layer-deps` | 分层方向:`Drivers/BSP/**` 不得 include `App/**` | `Drivers/BSP/**` | 已写 | -| [`id-check.md`](id-check.md) | ~~`id-check`~ | **已退役(2026-10-28)**:全库语料的八项校验,留档备查;现行编号门禁见下一行 | 在时有:几乎所有文本文件 | 已写(退役留档) | -| [`id-check-new.md`](id-check-new.md) | `id-check-new` | **只查新增行**有没有新造编号(零语料前缀白名单,已停发族只告警) | 任意 md/c/h/py/yaml/txt | 已写 | +| [`id-check.md`](id-check.md) | ~~`id-check`~ | **已退役(2026-10-28)**:全库语料的八项校验,留档备查;接班件也在下一行 | 在时有:几乎所有文本文件 | 已写(退役留档) | +| [`id-check-new.md`](id-check-new.md) | ~~`id-check-new`~ | **已退役(2026-10-29,用户裁定)**:只查新增行的编号门禁,留档备查(含退役理由与手工跑法);**编号形态此后无机器面** | 在时有:任意 md/c/h/py/yaml/txt | 已写(退役留档) | | [`doc-links.md`](doc-links.md) | `doc-links` | 文档里的路径引用是否存在;`--sections-strict` 另查章节号是否还存在 | 任意 `.md` | 已写 | | [`cppcheck.md`](cppcheck.md) | `cppcheck`、`test-cppcheck` | 静态分析(真机侧与 `test/` 侧两套范围与抑制项) | C/H 源文件 | 已写(`stages: [manual]`) | | [`coding-standard.md`](coding-standard.md) | `coding-standard` | 编码规范 V1.0 的可机械检查子集(命名、头文件保护宏、模块头注释块…) | C/H 源文件 | 已写 | diff --git a/docs/门禁说明/id-check-new.md b/docs/门禁说明/id-check-new.md index a960625b..0c77fb12 100644 --- a/docs/门禁说明/id-check-new.md +++ b/docs/门禁说明/id-check-new.md @@ -1,103 +1,81 @@ -# id-check-new(编号门禁·新增行) +# `id-check-new` — 编号门禁·新增行(零语料)——**已退役(2026-10-29)** -> 一句话:**只查这次改动新增的行里有没有新造的编号**,不维护任何族清单。 -> 它是 `id-check` 全库语料审计的接班件(退役路线见 `docs/编号规范.md` §8.5)。 +> ⚠ **本文是退役留档,不要照着它去改门禁。** +> 2026-10-29 用户裁定:这道门禁整体去掉——钩子从 `.pre-commit-config.yaml` 摘除(YAML 解析口径 +> 52 → 51),脚本移入 `scripts/archive/id_gate/check_ids_new.py`(仍可手工跑,`--self-test` 保留)。 +> **此后编号形态没有任何机器面**:判据只剩 `docs/编号规范.md` §1 的三句自问、§4.1 允许用号的族 +> (穷举)与人工评审。前辈 `id-check`(全库八项校验)已于 2026-10-28 退役 → `docs/门禁说明/id-check.md`; +> 两步合起来的路线 → `docs/编号规范.md` §8.5。 +> 下面 §1~§6 是退役前的原样描述,只在手工复查「这批改动 / 这段历史里有没有野生编号」时用得上。 -## 1. 它在查什么 +## 1. 它当初在查什么 拿 `git diff` 的**新增行**,只做三件事: | 形态 | 例子(形态描述,不是本仓号) | 判据 | |---|---|---| -| 破折形态 | 两字母前缀 + 连字符 + 数字 | 首段不在白名单 → **FAIL**;在「已停发族」→ 只 WARN | -| 裸形态 | 大写字母串紧跟数字(外设/器件/点号写法) | 前缀不在白名单 → **FAIL** | -| 单字母 + 数字 | 元件位号与层次号同形的那些 | **一律放行**(见 §6 盲区) | +| 破折形态 | 两字母前缀 + 连字符 + 数字 | 首段不在 `KNOWN_DASH` → **FAIL**;在 `RETIRED_DASH`(已停发族)→ 只 WARN | +| 裸形态 | 大写字母串紧跟数字(外设 / 器件 / 点号写法) | 前缀不在 `KNOWN_BARE` → **FAIL** | +| 单字母 + 数字 | `C12` / `U15` / `L3` / `R3` 这类元件位号与层次号同形 | **一律放行**(已知盲区,与旧门禁的"裸形盲区"同源) | -判据全文只有两张前缀表(`KNOWN_DASH` ≈ 11 条、`KNOWN_BARE` ≈ 90 条)与一张算术排除表 -(`SIZE`/`LEN`/`MAX` 之类后面跟 `-数字` 是减法不是编号)。**没有族形态正则、没有游标、 -没有属主登记处、没有比例基线**——这就是它相对 `id-check` 的全部区别。 +判据全文只有两张前缀表(退役时 `KNOWN_DASH` 11 条、`KNOWN_BARE` 90 余条)与一张算术排除表 +(`SIZE` / `LEN` / `MAX` 之类后面跟 `-数字` 是减法不是编号)。**没有族形态正则、没有游标、没有属主 +登记处、没有比例基线**——这就是它相对 `id-check` 的全部区别,也是它当初被选为接班件的理由。 -## 2. 为什么要它(与 id-check 的分工) +豁免三档:行内含 `[示例]` / `[计划]` 按冻结口径跳过;确需保留的字面量(如冻结的现场日志)在同一行 +写 `[编号豁免]`;脚本自身、`scripts/archive/`、`docs/芯片手册/`、`docs/硬件平台/`、`Lib/`、 +`Middlewares/` 等在排除清单里。**加白名单是唯一的放宽手段**——没有"整库豁免"入口,也没有 `--write` +现状洗白(与旧门禁同一条纪律)。 -`id-check` 需要养 99 族语料(`id_registry.json` 87.6 KB + `id_aliases.json` 187 KB); -实测它的**单次运行只有 1.8 s**,贵的是"每笔提交都要维持语料自洽"——近 80 笔提交里 61 笔 -改了语料/门禁/规范。而它真正有用的那部分(拦住"新写一个自造编号")不需要语料: -只要知道"这个前缀我从没见过"就够了。 +## 2. 为什么当初要它 -所以两者的分工是:**`id-check` 管存量一致性(逐步退役),`id-check-new` 管增量不许新增**。 -观察期内两条**同时跑**,互相兜底;观察期结束后按 `docs/编号规范.md` §8.5 摘掉 `id-check`。 +`id-check` 要养 99 族语料(`id_registry.json` 87.6 KB + `id_aliases.json` 187 KB):单次运行只有 +**1.8 s**,贵的是"每笔提交都要维持语料自洽"——近 80 笔提交里 **61 笔**动了语料 / 门禁 / 规范。 +而它真正有用的那一小块(拦住"新写一个自造编号")根本不需要语料:只要知道"这个前缀我从没见过"。 +所以 2026-10-28 的分工是:`id-check` 管存量一致性(当日退役),`id-check-new` 管增量不许新增。 -## 3. 触发面 +落地前测过噪声:拿最近 **80 笔真实提交**回放,未知形态只有 15 种,其中 12 种是设备 / 协议名 +(已进白名单)、3 种是已停发族(只告警)→ 白名单调好后**回放 0 误报**。 -- `files:` = 任意 `md` / `c` / `h` / `py` / `yaml` / `txt` 或本配置文件本身;`pass_filenames: false` - (脚本自己读 `git diff`,不靠钩子传文件名)。 -- 只读**新增行**:删行、改行里的旧编号不查(那是存量,归 `id-check`;退役后不再有人管, - 但这正是用户裁定接受的代价)。 -- 默认查**暂存区**(`git diff --cached`)。手工自查:`--worktree`(未暂存改动)/ `--range A..B`(一段提交)。 -- 排除面与旧门禁对齐:第三方库、HAL/CMSIS、`docs/芯片手册/`、`docs/硬件平台/`、`docs/images/`、 - `simulator/tests/ref/`、`ci-artifacts/`、`.dsh/skills/`、`scripts/rename_maps/`、 - 以及二进制/生成物后缀(`.svg` `.png` `.bin` `.map` `.json` 等)。 +## 3. 退役的理由(这一节是本文的重点) -## 4. 报错怎么读 +1. **它咬的是形态,不是语义**:白名单是"形态"级不是"号"级——前缀合法则 `TIM3` 放行、`TIM99` 同样 + 放行;同形多义(历史上 `S3` 三义那类撞车)它照不出来。这与旧门禁 `id-check.md` §6 记的盲区同源, + 只是范围缩到新增行。 +2. **白名单就是它的维护面**:每遇到一类合法字面量(新型器件、协议名、固件生成的产物文件名)就要加 + 一条;不加就误拦,加了就把该前缀后头的所有数字永久放宽。2026-10-29 当天就撞过一次:文档里写 + 录波段文件名 `WAVE00.BIN` 被拦,先改写成 `WAVE.BIN` 绕行、再登记 `WAVE` 前缀(`KNOWN_BARE` + 最后一次改动)。这类"合法新词撞形态门"是常态,不是意外。 +3. **默认形态早就是语义短语**(`docs/编号规范.md` §1 与 §3.3;登记簿新条目一律写语义短语, + `docs/任务规划/README.md` §0),新造号在评审里本来就看得见,机器面的边际收益低于误拦成本。 +4. 与 2026-10-28 摘掉全库审计是**同一条判断的延续**:编号体系不再产生价值。留着这道门只让"要不要 + 再登记一个前缀"变成反复出现的小额开销。 +5. 别往"轻量版"方向找补:改成只告警(退出码恒 0)等于把形态判断变成没人看的滚屏;转 + `stages: [manual]` 又因为它是 diff 型脚本、脱离提交时机基本扫不到东西。两条都是留着成本、 + 去掉收益,所以整体退役。 -```text -[FAIL] 新增行里有 2 处**未登记形态**的字母+数字标识: - docs/xxx.md:3 ZZ-9 [示例] - → 语义标识是默认形态:改写成短语(docs/编号规范.md §1); - 确属设备名/协议名/位号,就把前缀登记进 scripts/check_ids_new.py 的白名单。 +## 4. 退役后手工怎么用 + +``` +& '.venv\\Scripts\\python.exe' scripts/archive/id_gate/check_ids_new.py --self-test # 正反例自检(5 正例 / 29 反例) +& '.venv\\Scripts\\python.exe' scripts/archive/id_gate/check_ids_new.py --range A..B # 复扫一段提交的新增行 +& '.venv\\Scripts\\python.exe' scripts/archive/id_gate/check_ids_new.py --worktree # 查未提交的改动 ``` -三种处理:① 改成语义短语(默认);② 确实是设备名/协议名(如新的芯片型号)→ 把前缀加进 -`KNOWN_BARE` / `KNOWN_DASH`;③ 这一行必须保留字面(如冻结的现场日志)→ 行内写 `[编号豁免]`。 -⚠ `[示例]` / `[计划]` 行按既有冻结口径**整行跳过**,这也是本门禁的已知口子(§6)。 -⚠ 上面报错样例那行带 `[示例]`:两条门禁都认这个冻结标记、认不出「这段是文档举例」, -所以**在文档里举反例必须带 `[示例]`**,否则举例自己就会被拦(这条就是被自己拦过一次之后补的)。 -退出码:0 = 通过 / 仅告警;1 = 有新增编号;2 = 环境错误(git 不可用、不在仓库内)。 +跑不跑都不影响提交。真正还用得上它的场合是**编号清退批次**:拿它当"形态普查"的尺子,比自己临时 +写正则省事(做法 → `.dsh/skills/id-family-semantic-migration/SKILL.md`)。 -## 5. 好处 +## 5. 此后靠什么守着 -- **零维护**:加一个设备型号是改一行白名单,不需要登记族、游标、属主文档、基线比例。 -- **判据窄**:只对"新增"负责,所以不必解释"为什么这个历史号还在"——那类争论在旧门禁里占了大半时间。 -- **自证成本低**:`--self-test` 内置 5 个正例 + 29 个反例(含 `TC-NET-204`、`TRAP-ADC-01`、 - `TPAFE5160`、`GD5F2GQ5UE`、`C12`、`SIZE-2`、`GPIO_PIN_5` 等),改脚本后一条命令验证没把牙弄掉。 -- **落地前测过噪声**:拿最近 80 笔真实提交回放,未知形态只有 15 种,其中 12 种是设备/协议名 - (已进白名单)、3 种是已停发族(只告警)→ 白名单调好后回放 **0 误报**。 +- `docs/编号规范.md` §1「先问三句」+ §4.1 允许用号的族(穷举)+ §3.3 禁发范围——**新增族不再需要 + 登记机器白名单**,那个"唯一口子"随门禁一起消失;答不出"哪个脚本要按字面量匹配它"就别建族。 +- 评审与销项纪律:`docs/任务规划/README.md` §0(已随销项消失的号永久作废、不得复用)。 +- ⚠ 要认下丢了什么:**"销项删行后号被重新取走"这类事故当初正是靠机械牙发现的**(2026-10-14 撞号 + 事故 → 语义标识裁定)。此后提交期不再有这类警报,只剩规范与人工核。 -## 6. 限制与盲区(这一节请当正文读) +## 6. 改这个脚本本身要注意什么(历史留笔) -1. **单字母 + 数字一律放行**:元件位号(`C12`/`U15`/`R3`/`D7`)与层次号、裸形族号同形, - 机械区分不出来 → 新写一个单字母+数字的号**不会**被拦。这是本门禁最大的洞,与旧门禁的 - 「裸形盲区」同源,属显式接受的代价。 -2. **只查新增行**:把旧编号从 A 行搬到 B 行、或整段重排,算"新增"→ 会拦(因为形态未登记); - 反之,只要形态在白名单里,新写的也放行——白名单是"形态"级,不是"号"级。 -3. **`[示例]`/`[计划]` 整行跳过**:想绕过本门禁,写个 `[示例]` 就行。这靠评审纪律,不靠机器。 -4. **未跟踪文件不在扫描面**(`git diff` 语义):手工在 `git add` 之前跑 `--worktree` 看不到新文件; - 提交时必已进暂存区,所以门禁场景不受影响。 - 推论:**在 `pre-commit run --all-files`(`ci_local.py` 的静态门禁)下它是空转的**—— - 那种跑法把文件当"全量"传进来,而本脚本只读暂存区 → 暂存区空 → 打印通过。 - 它只在**真实提交**时才有牙;别拿静态门禁全绿当"没有新编号"的证明。 -5. **算术白名单是启发式**:`SIZE-2`/`LEN-1` 这类被排除;但若某处写着一个「不在白名单的前缀 + - 连字符 + 数字」,就会误报一次——把前缀加进白名单即可,不构成误拦风险。 -6. **不校验语义**:白名单里的前缀可以拿来写任何东西(`TIM3` 合法,`TIM99` 也放行)。 - 它只回答"这是不是新造的前缀",不回答"这个号该不该存在"。 - -## 7. 豁免与白名单口径 - -| 类别 | 放行方式 | 维护动作 | -|---|---|---| -| 设备 / 芯片 / 模块型号(`TPAFE5160`、`CH395F`、`SD2506`、`GD5F2GQ5UE`) | `KNOWN_BARE` 前缀 | 新器件时加一条 | -| 协议 / 标准(`IEC104`、`SHA-256`、`RS-485`、`UTF-8`) | `KNOWN_DASH` / `KNOWN_BARE` | 基本不增长 | -| 遥信/遥控点号(`YX`/`YK`/`YT`/`YH`)与保护元件代号(`OC`/`UV`/`OV`/`CP`…) | `KNOWN_BARE` | 协议数据,不是标识符 | -| 活族编号(`FR-*`、`TRAP-*`、`TC-*`、`CODE-RULE-*`、`MENU-STATE-*`) | `KNOWN_DASH` | 新增族要先在此登记 | -| 已停发族(登记簿/专项/层次/原则等旧族) | `RETIRED_DASH` → **只 WARN** | 不拦提交,提示改语义短语 | -| 固件生成的产物文件名(录波段号 `WAVE00.BIN` 到 `WAVE99.BIN`) | `KNOWN_BARE` 前缀,2026-10-29 登记 `WAVE` | 换一类产物名再加一条;先分清产物名与编号族 | -| 一次性保留 | 行内 `[编号豁免]` | 提交说明里要交代原因 | - -## 8. 改这个门禁本身要注意什么 - -- 改完必跑 `python scripts/check_ids_new.py --self-test`(正例漏拦 / 反例误拦都会红)。 -- 加白名单是**唯一**放宽手段;没有"整库豁免"入口,也没有 `--write` 现状洗白(与旧门禁同一条纪律)。 +- 改完必跑 `--self-test`(正例漏拦 / 反例误拦都会红);退役后这条纪律只对手工复查生效。 - 别把白名单做成"按字面量枚举"的账本——那就退化成 `id_aliases.json` 的老路(187 KB 维护负担), 前缀级枚举才是它便宜的原因。 -- 与 `id-check` 的关系变更(摘掉旧的)要同批改:本页、`docs/编号规范.md` §8.5、 - `docs/门禁说明/README.md` 的索引与计数、`AGENTS.md` 的钩子数。 +- 归档位置的 `REPO_ROOT` 与原位不同(深两级,取 `parents[3]`);再挪目录要一起改。 diff --git a/docs/门禁说明/id-check.md b/docs/门禁说明/id-check.md index 8bc0a4f9..c45033f6 100644 --- a/docs/门禁说明/id-check.md +++ b/docs/门禁说明/id-check.md @@ -3,7 +3,9 @@ > ⚠ **本文是退役留档,不要照着它去改门禁。** > 2026-10-28 用户裁定:编号体系本身不再产生价值,全库语料审计的维护成本高过收益 > → 钩子摘除(52 → 51),脚本与两份语料整体移入 `scripts/archive/id_gate/`; -> **现行编号门禁 = `id-check-new`**(只查新增行、零语料)→ `docs/门禁说明/id-check-new.md`。 +> **接班件 `id-check-new` 也已于 2026-10-29 退役**(只查新增行那道,用户裁定)→ `docs/门禁说明/id-check-new.md`。 +> 两道编号门禁自此全部退场:**编号形态没有机器面**,判据回到 `docs/编号规范.md` §1 三句自问 + §4.1 +> 族总表 + 人工评审;归档脚本 `scripts/archive/id_gate/check_ids_new.py` 可随时手工普查形态。 > 退役路线与判据替换 → `docs/编号规范.md` §8.5。下面保留退役前的原样描述与实测基线, > 只在手工复查「存量编号现状」时还用得上。 diff --git a/scripts/archive/id_gate/README.md b/scripts/archive/id_gate/README.md index 9e1f3d62..ba66a74b 100644 --- a/scripts/archive/id_gate/README.md +++ b/scripts/archive/id_gate/README.md @@ -1,11 +1,12 @@ -# 编号门禁·全库语料审计(已退役,2026-10-28 归档) +# 编号门禁归档(两道都已退役:全库语料审计 2026-10-28、新增行检查 2026-10-29) -本目录是 2026-10-20 ~ 2026-10-28 期间在 `pre-commit` 里跑的那套**全库编号审计**, -连同它的两份语料一起搬进来,**不再进提交门禁**。 +本目录是 2026-10-20 ~ 2026-10-29 期间在 `pre-commit` 里跑过的**两套编号门禁**,连同它们的 +语料与白名单一起搬进来,**都不再进提交门禁**。2026-10-29 的裁定见每篇文末与其留档文档。 | 文件 | 是什么 | |---|---| | `check_ids.py` | 八项校验脚本(规范 §8)。手工复查存量编号现状时才跑 | +| `check_ids_new.py` | 只查新增行的那道(2026-10-28 ~ 2026-10-29 在钩子里跑),2026-10-29 用户裁定退役后降为**手工形态普查**;`--self-test` 保留,`REPO_ROOT` 因归档位置深两级改取 `parents[3]` | | `id_registry.json` | 99 族的注册表:形态正则 / 属主登记处 / 冻结 / 游标 / 两条护栏的数值 | | `id_aliases.json` | 旧号 → 新号 / 作废(`retired-to-phrase`、`void`)的别名表,187 KB | | `id_known_backlog.json` | 债务台账(收口时已还清到 0 条) | @@ -18,9 +19,10 @@ ## 现在用什么 -- **现行门禁**:`scripts/check_ids_new.py` + 钩子 `id-check-new` —— 只查 `git diff` 新增行, - 零语料。详解 `docs/门禁说明/id-check-new.md`。 -- **路线与验收条件**:`docs/编号规范.md` §8.5(①②③ 三步的来龙去脉)。 +- **没有现行门禁**:接班的 `id-check-new`(只查新增行、零语料)也在 2026-10-29 被用户裁定退役, + 脚本进本目录。编号形态此后无机器面,判据 = `docs/编号规范.md` §1 三句自问 + §4.1 族总表 + 评审; + 留档 `docs/门禁说明/id-check-new.md`(拦下过什么、为什么退、手工怎么再跑)。 +- **路线与验收条件**:`docs/编号规范.md` §8.5(①②③④ 四步的来龙去脉)。 - **规范正文**(仍然有效,只是不再全库机械校验):`docs/编号规范.md` §1 先问三句、§4.1 族总表。 ## 怎么手工跑一次 diff --git a/scripts/archive/id_gate/check_ids.py b/scripts/archive/id_gate/check_ids.py index 24510204..fa3380e3 100644 --- a/scripts/archive/id_gate/check_ids.py +++ b/scripts/archive/id_gate/check_ids.py @@ -5,10 +5,12 @@ check_ids.py — 编号门禁(docs/编号规范.md §8 的 8 项校验)— ⚠ 退役说明(先读这段,别把它当活门禁用): 本脚本与两份语料(id_registry.json / id_aliases.json)已整体移入 scripts/archive/id_gate/, - **不再进 pre-commit**,也不再有人被要求维护语料。现行编号门禁是 scripts/check_ids_new.py - (钩子 id-check-new:只查新增行、零语料)。本脚本保留下来只做一件事: + **不再进 pre-commit**,也不再有人被要求维护语料。接班的 scripts/check_ids_new.py + (钩子 id-check-new:只查新增行、零语料)已于 2026-10-29 一并退役,也进了本目录—— + **编号形态此后没有机器面**。本脚本保留下来只做一件事: 想复查「存量编号现状」时手工跑一遍出报告。 - 路线与判据替换的来龙去脉 → docs/编号规范.md §8.5;新门禁详解 → docs/门禁说明/id-check-new.md。 + 路线(①②③④ 四步)→ docs/编号规范.md §8.5;两道门禁的留档 → docs/门禁说明/id-check.md + 与 docs/门禁说明/id-check-new.md。 下面这份 docstring 是退役前的原样,未改,作为历史口径保留。 两份配置(数据源 = 规范 §4 编号空间总表,逐项对应,不凭印象): diff --git a/scripts/check_ids_new.py b/scripts/archive/id_gate/check_ids_new.py similarity index 87% rename from scripts/check_ids_new.py rename to scripts/archive/id_gate/check_ids_new.py index af1be72b..ef6fce66 100644 --- a/scripts/check_ids_new.py +++ b/scripts/archive/id_gate/check_ids_new.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -"""check_ids_new.py — 编号门禁·第二步:只查**新增行**的新编号(零语料、无语料维护) +"""check_ids_new.py — 编号门禁·新增行(**已退役 2026-10-29,用户裁定**;本脚本降为手工报告工具) -背景(docs/编号规范.md §8.5):语义标识已是默认形态,1~6 项全库语料审计的成本高过收益。 -本脚本替代它的**唯一实际用途**——拦住"新写一个自造编号":只看 git diff 的新增行。 +退役口径:钩子已从 .pre-commit-config.yaml 摘除(YAML 解析条目 52 → 51),编号形态此后 +**没有机器面**——靠 docs/编号规范.md §1 的三句自问 + 评审。留档说明 → docs/门禁说明/id-check-new.md。 +本脚本与 check_ids.py(2026-10-28 退役的全库八项校验)同处一档:手工复查「这批改动 / 这段历史 +里有没有野生编号」时才跑,跑不跑都不影响提交。 判据(三条,都不依赖任何族清单 / 游标 / 属主登记处): 1. 破折形态(形如 前缀-数字,或 前缀-段-数字):首段不在 KNOWN_DASH → FAIL; @@ -12,11 +14,11 @@ 3. 单字母+数字(C12 / U15 / L3 / R3)→ **放行**:元件位号与层次号同形,这是本脚本的已知盲区 (与旧门禁的"裸形盲区"同源,见 docs/门禁说明/id-check-new.md §5)。 -用法(仓库根): - python scripts/check_ids_new.py # 查暂存区(pre-commit 用这个) - python scripts/check_ids_new.py --worktree # 查工作区未暂存的改动(自查) - python scripts/check_ids_new.py --range A..B # 查一段提交的新增行 - python scripts/check_ids_new.py --self-test # 内建正反例自检(改脚本后必跑) +用法(仓库根;钩子摘除后没有自动调用者,四条都是手工): + python scripts/archive/id_gate/check_ids_new.py # 查暂存区 + python scripts/archive/id_gate/check_ids_new.py --worktree # 查工作区未暂存的改动 + python scripts/archive/id_gate/check_ids_new.py --range A..B # 查一段提交的新增行 + python scripts/archive/id_gate/check_ids_new.py --self-test # 内建正反例自检(改脚本后必跑) 退出码:0 = 通过 / 仅告警;1 = 新增了未知编号;2 = 环境错误。 豁免:行内含 [示例] / [计划] 的行按冻结口径跳过;确需保留的写法在同一行写 [编号豁免]。 """ @@ -33,7 +35,8 @@ for _stream in (sys.stdout, sys.stderr): except (AttributeError, ValueError): pass -REPO_ROOT = Path(__file__).resolve().parent.parent +# 归档位置比原位置深两级(scripts/archive/id_gate/)→ 上溯到仓库根要 parents[3] +REPO_ROOT = Path(__file__).resolve().parents[3] # --- 破折形态:首段白名单(活族 + 标准编号;新增族要先登记到这里,这就是"允许用号的唯一口子") KNOWN_DASH = frozenset({ @@ -98,7 +101,8 @@ EXCLUDED_DIRS = ( ) EXCLUDED_FILES = frozenset({ - "scripts/check_ids_new.py", # 本脚本自带正反例白名单,不能自我拦截 + "scripts/check_ids_new.py", # 本脚本自带正反例白名单,不能自我拦截(归档后整目录已在 + # EXCLUDED_DIRS 的 scripts/archive/ 里,这行只是历史留笔) }) EXCLUDED_SUFFIX = (".svg", ".png", ".jpg", ".jpeg", ".gif", ".bin", ".map", ".htm", ".html", ".tsv", ".log", ".o", ".axf", ".hex", ".ico", ".ttf", ".woff", ".pyc", @@ -248,7 +252,8 @@ def main(argv=None): if args.verbose: print(" %s" % text) print(" → 语义标识是默认形态:改写成短语(docs/编号规范.md §1);") - print(" 确属设备名/协议名/位号,就把前缀登记进 scripts/check_ids_new.py 的白名单。") + print(" 确属设备名/协议名/位号,就改形态(写语义短语)——本门禁已于 2026-10-29 退役,") + print(" 不再是提交钩子;要再拿它当尺子就手工跑 scripts/archive/id_gate/check_ids_new.py。") return 1 print("[OK] 新增行无未登记编号%s" % ("(%d 处停发族提示)" % len(warns) if warns else "")) diff --git a/scripts/archive/id_gate/编号治理历史.md b/scripts/archive/id_gate/编号治理历史.md index d6e4924c..f3af6f6d 100644 --- a/scripts/archive/id_gate/编号治理历史.md +++ b/scripts/archive/id_gate/编号治理历史.md @@ -355,7 +355,7 @@ P-6 号必须能反查到定义(属主登记处得存在)· P-7 编号不语 | 日期 | 变更 | 说明 | |---|---|---| | 2026-10-28 | **编号门禁第三步收口:全库语料审计退役(钩子 52 → 51)** | 用户裁定跳过观察期直接做。`check_ids.py` 与两份语料(`id_registry.json` 87.6 KB / `id_aliases.json` 187 KB)+ 债务台账整体移入 `scripts/archive/id_gate/`(附归档 README,含"为什么退役 / 现在用什么 / 怎么手工跑");`id-check` 钩子摘除,现役编号门禁只剩 `id-check-new`(只查新增行)。连带改:`rename_ids.py` 的别名表路径指向归档、新门禁排除面加 `scripts/archive/`、`doc-links` 跳过 `.mnemon/` 与 `ci-artifacts/`(本地状态与草稿,非项目文档)、`id-check.md` 加退役横幅、`门禁说明/README` 与 `AGENTS` 的钩子数三数对齐(声明 51 / commit 阶段 50 + gitlint 1)。归档脚本从新路径实跑:811 文件 / 8,051 字面量、`--strict` rc=0。 | -| 2026-10-28 | **编号门禁第二步落地:新增行拦截(零语料)** | `scripts/check_ids_new.py` + 钩子 `id-check-new`:只看新增行,破折形态未知前缀 FAIL、已停发族只 WARN、单字母+数字放行(元件位号同形,见该门禁 §6 盲区)。白名单是**前缀级**两张表(活族 11 条 / 器件与协议约 90 条),不是字面量账本——后者会退化成 `id_aliases.json` 的老路。调参依据:最近 80 笔真实提交回放,未知形态 15 种(12 种是设备与协议名、3 种是停发族),调好后误报 0;自检 5 正例 + 29 反例。与 `id-check` **双跑**一个观察期后再按 `docs/编号规范.md` §8.5 ③ 退役旧语料。 | +| 2026-10-28 | **编号门禁第二步落地:新增行拦截(零语料)** | 脚本(今在 `scripts/archive/id_gate/check_ids_new.py`)+ 钩子 `id-check-new`:只看新增行,破折形态未知前缀 FAIL、已停发族只 WARN、单字母+数字放行(元件位号同形,见该门禁 §6 盲区)。白名单是**前缀级**两张表(活族 11 条 / 器件与协议约 90 条),不是字面量账本——后者会退化成 `id_aliases.json` 的老路。调参依据:最近 80 笔真实提交回放,未知形态 15 种(12 种是设备与协议名、3 种是停发族),调好后误报 0;自检 5 正例 + 29 反例。与 `id-check` **双跑**一个观察期后再按 `docs/编号规范.md` §8.5 ③ 退役旧语料。 | | 2026-10-28 | **编号门禁减负第一步:两根护栏转报告制** | 用户裁定「编号门禁每次浪费很多时间」→ 先摘掉语料审计的强制面。护栏 1(可判定比例基线)与护栏 2(不可判定绝对数上限)自此**只打印、不拦提交**;实测把基线调到 99.9%、上限调到 1 处,退出码仍为 0。硬拦只剩校验 1~6 的台账外新账(同一实测:新文件里写一个自造前缀 + 数字,校验 1 与增量拦截照旧 FAIL)。退役路线 ①②③ 写进 `docs/编号规范.md` §8.5;减负依据 = 近 80 笔提交有 61 笔被编号体系牵连、语料共 99 族(78 族已冻结)。 | | 2026-10-28 | **变更记录行里已停发/作废族的示例编号改形态描述;并用门禁自身计数器把「这一笔能不能压下不可判定绝对数」钉死为零** | 用户指令:把变更记录行里已停发族的那几枚示例编号改成语义形态描述,并确认它让不可判定引用绝对数净减、不是只算自己这一笔。**实测裁定:净减 = 0**。门禁把「表格首格是日期」的行整行豁免于编号判定(registry 侧的变更记录豁免,本笔前全库已豁免 198 处站点),本表的示例编号从来不进引用总数分母,改它必然不动绝对数——改前改后同一个计数器均为 **1003**(上限 1166;可判定 87.43%,三项都是 2026-10-28 本回合实测)。唯一移动的是那行豁免计数本身(本文件少 1 处站点),把它当清退成果就是把自记账成全局减量,正是本笔要排除的错法。本文对该计数器的全部贡献 **6** 处,逐文件集合差实测:全是「非编号」判定栏里既定的噪声名与裸形盲区(哈希算法名、总线名、严重度档位、硬件位号),不是编号,不清。本笔动的 3 处只是写法:作废的网络条目孤儿号(F 前缀 + 模块码段 + 数字序号三段)与已删报告的 UI 簇(字母 U 紧跟一位数字,区间两端即该簇首尾两枚);事实本体仍在别名表、registry 游标备注与 git 历史,一处没丢。刻意不动:硬件位号形态的举例(判非编号)、保留现存号族的冻结游标数字(冻结水位的证据本体)、其它文档与注册表里的同名字段(各自写域)。`docs/编号规范.md` §5.5 同步写死该例外的硬口径。零代码、零编号增删、两根护栏基线均未调整。 | | 2026-10-27 | **批次五:「数据可信」任务卡号整族清退 + 注入验证卡销项删行** | 用户指令「数据可信.md 已经完成的内容请进行销项删除,去除相关编号」。实测 **288 站点 / 56 文件**(代码与测试 191 / 文档 56 / 门禁配置与本文 41)全改语义短语;`docs/编号规范.md` §5 表首列由「序」改「标识(任务名)」、14 个旧号进 §9.3 换算表;顺带按属主文档判语义清掉同形态的悬空引用 4 处(「UI 补齐」波次的 `[comm]` 段前置项)。门禁面:编号字面量与不可判定绝对数**零变化**(中文前缀不在语料),收口复跑 `check_ids.py --strict` 与 `check_doc_links.py --all --sections-strict` | From d56c6ce862906e9baf9fe0e7b5ef9f5f188bbc4f Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 20:24:30 +0800 Subject: [PATCH 09/19] =?UTF-8?q?docs(AGENTS):=20=E8=A1=A5=E5=9B=9E?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=9E=9A=E4=B8=BE=E9=A1=B9=E6=97=B6=E5=90=83?= =?UTF-8?q?=E6=8E=89=E7=9A=84=E4=B8=80=E4=B8=AA=E7=A9=BA=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 退役 id-check-new 那笔删枚举项时,锚点尾部的分隔符吃掉了 「`mem-numbers` + `lv-conf-sync`」之间的空格;枚举各项重新可逐项相加。 钩子口径不变(声明 51 / manual 3 / 提交期实跑 48)。未 push。 --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 7594e8aa..d595e806 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,7 +93,7 @@ ## 提交门禁(pre-commit) -- 任一失败即拦截,共 **51 个钩子**(**口径 = YAML 解析后的 hook 条目数**:自查命令与「声明 / 文档 / 实跑」三数同源核对见 `docs/门禁说明/README.md` §2 首条——⚠ 直接 grep 配置里的声明行会多算一条**注释掉的**候选钩子(比口径多 1),不是口径;**配置注释里也别复述那个 grep 字面量**,它会让文本计数再虚高 1(2026-10-27 实测踩过);加一条钩子就同批改 AGENTS 与门禁索引两处计数。2026-10-25 道E 连加 `bom-guard` / `setview-check` / `uinav-selftest` 三条,2026-10-26 加 `generated-fresh` 一条,2026-10-27 加 `nested-comment` 一条;2026-10-28 加 `id-check-new` 一条并同批退役 `id-check`(口径 52 → 51:编号门禁换牙——新的只查新增行、零语料,旧的八项全库校验连语料一起移入 `scripts/archive/id_gate/`,路线见 `docs/编号规范.md` §8.5);2026-10-29 加 `skill-size` 一条(51 → 52),同日按用户裁定**退役 `id-check-new`**(52 → **51**:只查新增行的编号门禁整体退场,脚本与白名单一起移入 `scripts/archive/id_gate/`,编号形态此后无机器面,留档 `docs/门禁说明/id-check-new.md`);**每种门禁一篇详解(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径)→ `docs/门禁说明/README.md` 索引**;通用机制与整体口径 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers`+ `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + `bom-guard` / `setview-check` / `uinav-selftest`(2026-10-25 道E 那三条:前半句的加钩流水里有它们、逐项枚举里漏了,补上后本行各项相加才等于 51)+ 主机侧单测 **22 根专属钩子**(`-unit-test` 后缀 = 有独立 `files:` 触发面、需要秒级归因的那个子集;2026-10-25 增补 adc-cal / prot-dim / vol-idle-sync / ui-data / meas / run-par 六根)——**这是口径不是缺口**:`test/host/` 实测 44 套带 `run_unit_test.py`,其余 **22 套**(含 `test/host/ui_sink` 与录波三套)没有专属钩子,但每次 CI / `scripts/ci_local.py` 都被 `test-coverage` 总闸全量真跑并守基线回退,**失败照样拦**,只是归因落在总闸输出上;**没有专属钩子 ≠ 没被跑**,22 套台账与补钩子的判据 → `docs/门禁说明/host-unit-tests.md` §6 第 4 条。**Lead 裁定 2026-10-27:不补第 23 根**——读「这套跑没跑」去看总闸输出,别数钩子(原写「23 个 / 含 ui-sink 七个」是双重错:既把 22 说成 23,又宣称 ui-sink 有钩子——配置历史里 `ui-sink-unit-test` 从未存在)+ `test-coverage`(**覆盖率总闸**:跑全部 44 套并守基线回退;缺 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`)+ `nested-comment`(**块注释嵌套**:注释体内再出现 /* 会被 ARMCC 报 9-D,且**按包含者数量刷屏**、只在重编译该 TU 时才暴露;本仓诱因就是「在注释里写作用域/通配路径」,改法是写全目录或用尖括号 ;接线当日实扫 403 个 .c/.h,新发 0、存量 2 处(`test/host/common/` 两份假件头)钉只减不增基线 → `docs/门禁说明/nested-comment.md`)+ `skill-size`(**SKILL.md 不超插件写侧上限 65536 字节**:dsh-mnemon 只在写侧校验、加载侧不查,超限技能照常可读却每次「生成草案」被拒、永久卡在「生成中」;另查 `reference/` 指针悬空与 BOM → `docs/门禁说明/skill-size.md`)+ `mem-budget`(RAM 92% 预算门禁,登记簿条目「缩堆迁 CCM 后半(暂缓)」)+ `font-charset`(UI 字符集差集检查,防缺字白框)+ `ui-golden`(UI 像素回归,菜单 8 状态 + 内容页 66 张黄金参考)+ `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 条**))。**提交期实跑 48 根**(2026-10-28 用户裁定):`cppcheck` / `test-cppcheck` / `test-coverage` 三根带 `stages: [manual]`——实测合计 ~165 s = 一次带代码提交耗时的三分之二,提交期只跑快的那些;这三根由**远端 CI** 强制(`.gitea/workflows/ci.yml` 的「手动阶段门禁」步,逐根 `--hook-stage manual`)与 `scripts/ci_local.py` 的 manual 段,本地手工复跑 `python -m pre_commit run --all-files --hook-stage manual `(一次只能一个 id) +- 任一失败即拦截,共 **51 个钩子**(**口径 = YAML 解析后的 hook 条目数**:自查命令与「声明 / 文档 / 实跑」三数同源核对见 `docs/门禁说明/README.md` §2 首条——⚠ 直接 grep 配置里的声明行会多算一条**注释掉的**候选钩子(比口径多 1),不是口径;**配置注释里也别复述那个 grep 字面量**,它会让文本计数再虚高 1(2026-10-27 实测踩过);加一条钩子就同批改 AGENTS 与门禁索引两处计数。2026-10-25 道E 连加 `bom-guard` / `setview-check` / `uinav-selftest` 三条,2026-10-26 加 `generated-fresh` 一条,2026-10-27 加 `nested-comment` 一条;2026-10-28 加 `id-check-new` 一条并同批退役 `id-check`(口径 52 → 51:编号门禁换牙——新的只查新增行、零语料,旧的八项全库校验连语料一起移入 `scripts/archive/id_gate/`,路线见 `docs/编号规范.md` §8.5);2026-10-29 加 `skill-size` 一条(51 → 52),同日按用户裁定**退役 `id-check-new`**(52 → **51**:只查新增行的编号门禁整体退场,脚本与白名单一起移入 `scripts/archive/id_gate/`,编号形态此后无机器面,留档 `docs/门禁说明/id-check-new.md`);**每种门禁一篇详解(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径)→ `docs/门禁说明/README.md` 索引**;通用机制与整体口径 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + `bom-guard` / `setview-check` / `uinav-selftest`(2026-10-25 道E 那三条:前半句的加钩流水里有它们、逐项枚举里漏了,补上后本行各项相加才等于 51)+ 主机侧单测 **22 根专属钩子**(`-unit-test` 后缀 = 有独立 `files:` 触发面、需要秒级归因的那个子集;2026-10-25 增补 adc-cal / prot-dim / vol-idle-sync / ui-data / meas / run-par 六根)——**这是口径不是缺口**:`test/host/` 实测 44 套带 `run_unit_test.py`,其余 **22 套**(含 `test/host/ui_sink` 与录波三套)没有专属钩子,但每次 CI / `scripts/ci_local.py` 都被 `test-coverage` 总闸全量真跑并守基线回退,**失败照样拦**,只是归因落在总闸输出上;**没有专属钩子 ≠ 没被跑**,22 套台账与补钩子的判据 → `docs/门禁说明/host-unit-tests.md` §6 第 4 条。**Lead 裁定 2026-10-27:不补第 23 根**——读「这套跑没跑」去看总闸输出,别数钩子(原写「23 个 / 含 ui-sink 七个」是双重错:既把 22 说成 23,又宣称 ui-sink 有钩子——配置历史里 `ui-sink-unit-test` 从未存在)+ `test-coverage`(**覆盖率总闸**:跑全部 44 套并守基线回退;缺 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`)+ `nested-comment`(**块注释嵌套**:注释体内再出现 /* 会被 ARMCC 报 9-D,且**按包含者数量刷屏**、只在重编译该 TU 时才暴露;本仓诱因就是「在注释里写作用域/通配路径」,改法是写全目录或用尖括号 ;接线当日实扫 403 个 .c/.h,新发 0、存量 2 处(`test/host/common/` 两份假件头)钉只减不增基线 → `docs/门禁说明/nested-comment.md`)+ `skill-size`(**SKILL.md 不超插件写侧上限 65536 字节**:dsh-mnemon 只在写侧校验、加载侧不查,超限技能照常可读却每次「生成草案」被拒、永久卡在「生成中」;另查 `reference/` 指针悬空与 BOM → `docs/门禁说明/skill-size.md`)+ `mem-budget`(RAM 92% 预算门禁,登记簿条目「缩堆迁 CCM 后半(暂缓)」)+ `font-charset`(UI 字符集差集检查,防缺字白框)+ `ui-golden`(UI 像素回归,菜单 8 状态 + 内容页 66 张黄金参考)+ `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 条**))。**提交期实跑 48 根**(2026-10-28 用户裁定):`cppcheck` / `test-cppcheck` / `test-coverage` 三根带 `stages: [manual]`——实测合计 ~165 s = 一次带代码提交耗时的三分之二,提交期只跑快的那些;这三根由**远端 CI** 强制(`.gitea/workflows/ci.yml` 的「手动阶段门禁」步,逐根 `--hook-stage manual`)与 `scripts/ci_local.py` 的 manual 段,本地手工复跑 `python -m pre_commit run --all-files --hook-stage manual `(一次只能一个 id) - **范围 = 项目代码**:第三方(`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/` From f8d795c387d7dbdbaae2da249ad48206cca3ae38 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 20:53:26 +0800 Subject: [PATCH 10/19] =?UTF-8?q?docs(=E7=BC=96=E5=8F=B7=E8=A7=84=E8=8C=83?= =?UTF-8?q?):=20=E6=8C=89=E6=9C=BA=E5=99=A8=E9=9D=A2=E9=80=80=E5=BD=B9?= =?UTF-8?q?=E6=94=B9=E5=86=99=E8=A7=84=E8=8C=83=E6=AD=A3=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 依据(逐条回配置核实):三处配置里 id_registry / check_ids / id_known_backlog 命中数全为 0,没有任何活物再读编号登记; check_test_traceability.py 存在但没被任何配置跑;变更记录例外 清单实测只剩 5 篇。 改写要点:TC 框把「registry 带游标所以允许发」改成裁定留痕并 明写别把不报错读成允许发号;删掉探针行与自相矛盾的 7 行; §4.1.1 四处现在时(已冻结 / 越游标即拦 / 发号口已关闭 / 校验 错误级)改历史口径,中文前缀族改成「从来没有机器面」;§4.3 点明冻结前缀是档案不是闸门;§5.1 三步改成「族落在 §4.1 之 内 → 属主登记处写定义行 → 才允许引用」;§5.5 删掉「门禁已 豁免」口气并不再复述份数;§8 标题改「一道也没有」;§8.5 由 退役路线流水表整体替换为「手工普查与恢复机器面」(两条归档 命令 + 恢复成本三步 + 退役理由与 WAVE00 误拦),§8 / §8.5 编号刻意保留以免外部引用断链。 指针同步:AGENTS.md 与配置注释里的「§8.5 路线」改成新节名; id-family-semantic-migration 的 SKILL.md 与两份 reference 把 §9 事实源指针改指归档、把「门禁咬不到」措辞改成「连归档扫描 工具都咬不到」。 校验:本篇逐篇 check_doc_links --sections-strict rc=0(60 处 引用 / 58 可判定 97 % / 无法判定 0);check_skill_size rc=0; 表格列数 0 处不一致;无 BOM / CR / 乱码 / 行尾空格,单 LF 收尾; YAML 解析钩子声明数仍 51(配置只改注释)。 --- .../id-family-semantic-migration/SKILL.md | 4 +- .../reference/steps-alpha-numeric-family.md | 10 +- .../reference/steps-chinese-prefix-family.md | 8 +- .pre-commit-config.yaml | 2 +- AGENTS.md | 2 +- docs/编号规范.md | 138 ++++++++++-------- 6 files changed, 87 insertions(+), 77 deletions(-) diff --git a/.dsh/skills/id-family-semantic-migration/SKILL.md b/.dsh/skills/id-family-semantic-migration/SKILL.md index 3b04a4ae..4237d6eb 100644 --- a/.dsh/skills/id-family-semantic-migration/SKILL.md +++ b/.dsh/skills/id-family-semantic-migration/SKILL.md @@ -48,9 +48,9 @@ version: 1.1.0 14. 裁定「不做」也要写成可执行硬口径入档 > 本节全文(逐字搬移、未改写,含命令与实测数字)→ [steps-alpha-numeric-family.md](reference/steps-alpha-numeric-family.md)。动手前先读那篇。 -## Steps(中文前缀族:门禁咬不到的那一路) +## Steps(中文前缀族:连归档扫描工具都咬不到的那一路) -1. 先确认门禁咬不到,并把这句话写进交付 +1. 先确认连归档扫描工具也咬不到(两道编号门禁 2026-10-28 / 29 已退役,"咬不咬"从此只是工具层面的事),并把这句话写进交付 2. 销项删行前先核证据属主 3. 自建带标签正则扫描脚本 4. 该族与形似噪声号分开计数,各自给分母 diff --git a/.dsh/skills/id-family-semantic-migration/reference/steps-alpha-numeric-family.md b/.dsh/skills/id-family-semantic-migration/reference/steps-alpha-numeric-family.md index d9e1c61c..82ba2c49 100644 --- a/.dsh/skills/id-family-semantic-migration/reference/steps-alpha-numeric-family.md +++ b/.dsh/skills/id-family-semantic-migration/reference/steps-alpha-numeric-family.md @@ -2,7 +2,7 @@ > 本文由 id-family-semantic-migration 的 SKILL.md「Steps(字母+数字族:门禁看得见的那一路)」一节逐字外置而来(渐进展开:主文件只留索引与指针)。步骤号沿用原文,未重编。 -1. **确认范围与定调**:先按开头的表判识别面(无识别面 → 走下一节);再读 `docs/编号规范.md` §4.1(族总表,穷举)、§4.1.1(已停发栏),存量与批次见 `scripts/archive/id_gate/编号治理历史.md` §9,明确本族属主登记处(`owner_docs_glob`)与判据「有脚本按字面量匹配它」。清退 = 全仓改语义;停发 = 游标冻结、只减不增。 +1. **确认范围与定调**:先按开头的表判识别面(无识别面 → 走下一节);再读 `docs/编号规范.md` §4.1(族总表,穷举)、§4.1.1(已停发栏),存量与批次见 `scripts/archive/id_gate/编号治理历史.md` §9,明确本族属主登记处(`owner_docs_glob`)与判据「有脚本按字面量匹配它」。清退 = 全仓改语义;停发 = 在规范表里写死不再新增引用(`id_registry.json` 的游标冻结只是裁定留痕,2026-10-29 起无机器读者)。 2. **按族实测三口径(字面量 / 站点 / 文件),不用 grep 估数**。`Checker.refs` 是 **dict(字面量 → 文件集合)不是方法**,`ck.refs(fam)` 这种写法会 `TypeError`;站点数门禁不直接给,用族正则重扫它自己的语料: ```python import sys; sys.path.insert(0, "scripts") @@ -38,7 +38,7 @@ p.write_text(t, encoding='utf-8') ``` **token 取最小形态**:文档里被反引号/加粗包住的号,替换单位取反引号形(```T4```)而**不含 `**` 标记**(第 225 行是 `**裸形 `T4`**`,按「裸形 `T4`」替就命中 0)。讲编号的文档改成**形态描述**(如「字母 T 紧跟数字 4」)能把不可判定绝对数还回余量:实测 4 处 → 982 降到 978,且 lesson 不丢。 -8. **同步更新门禁侧配置**:`check_ids` 的 `excluded_paths` 与噪声表按第 3 步的分类结果改,使剩余命中不再是「假编号」;新测试套件发号的悬空引用要先定**定义位**: +8. **同步更新归档扫描器的配置**:`check_ids` 的 `excluded_paths` 与噪声表按第 3 步的分类结果改,使剩余命中不再是「假编号」;新测试套件发号的悬空引用要先定**定义位**: - 定义位**只能落一处**——注册表族(`scripts/archive/id_gate/id_registry.json`)或该套件说明文档,**两处并存比没有定义位更坏**;文档真写出来了(含判据与上板清单)就以文档为准、**撤族**; - 要建族就照现有同类族克隆(`pattern` / `digit_width` / `owner_docs_glob` / `scope` / `cursor`),别自造字段; - **抬游标前先确认游标是字符串形态**(`'cursor': 'TC-UIDATA-026'`)——整型比较会静默不生效(实测踩坑),用字符串解析或正则取序号再比。 @@ -58,9 +58,9 @@ ./.venv/Scripts/python.exe scripts/archive/id_gate/check_ids.py --strict 2>&1 | tail -1 # [OK] 校验 1~6 无错误 ``` 报告「字面量 8787→8299→7828」「不可判定绝对数 1161→984→982→978(上限 1166)」「可判定比例 88.14%(基线 86.0%,容差 0.1pp)」。**绝对数与比例两根线都要报**:把可判定引用改成散文会稀释比值,所以另有不可判定绝对数上限。涨点归因见「净室归因」一节。 -13. **回写 `docs/编号规范.md`**:§4.1 族表(停发/待清栏)、§4.1.1 已停发栏、§9 存量表与批次结果表(只减不增)、§9 清点数字按最终实测同步;表格若保留动手前的清点数,**明确标注「不是当前存量,是动手前基线」**,否则就是拿旧数当现状。**用户裁定原文与理由直接写进对应节**(§4.1.1 停发行 + 该族的批次结论段落)——2026-10-28 起不再往变更记录写流水(`docs/文档规范.md` §5.1)。 +13. **回写 `docs/编号规范.md`**:§4.1 族表(停发/待清栏)、§4.1.1 已停发栏;存量表与批次结果表在 `scripts/archive/id_gate/编号治理历史.md` §9(只减不增、清点数字按最终实测同步);表格若保留动手前的清点数,**明确标注「不是当前存量,是动手前基线」**,否则就是拿旧数当现状。**用户裁定原文与理由直接写进对应节**(§4.1.1 停发行 + 该族的批次结论段落)——2026-10-28 起不再往变更记录写流水(`docs/文档规范.md` §5.1)。 14. **裁定「不做」也要写成可执行硬口径入档**(不是只划掉一行——「不做」最容易被读成「那还可以继续发号」): - §4.1.1「已停发」栏新增该族一行:**游标冻结、现存号只减不增、新用例改用语义化测试函数名 + 文档表格行名**,并写清与「允许继续发号」栏为何不打脸; - - §9.1 批次段落写明裁定原文与理由(该族是唯一真被脚本按字面量匹配的族:C 断言标签 + 测试函数名 + 文档用例表三处同一号),并给出**将来真要清退的前置条件**:追溯器(`check_test_traceability.py`)先同时认新旧两种形态、并行核对通过,**不许先改文档再改代码**; + - 批次段落写明裁定原文与理由(归档 `scripts/archive/id_gate/编号治理历史.md` §9.1——§9 一族的事实源已在归档篇,规范正文只留口径)(该族是唯一真被脚本按字面量匹配的族:C 断言标签 + 测试函数名 + 文档用例表三处同一号),并给出**将来真要清退的前置条件**:追溯器(`check_test_traceability.py`)先同时认新旧两种形态、并行核对通过,**不许先改文档再改代码**; - 全文核「等你裁定 / 需你裁定」悬空表述残留数归 0; - - 无机器识别面的族落地面不同(§4.1.1 一行 + §9.3 换算表 + 叙述同步三处),见下一节第 7 步。 + - 无机器识别面的族落地面不同(`docs/编号规范.md` §4.1.1 一行 + 归档 `编号治理历史.md` §9.3 换算表 + 叙述同步三处),见下一节第 7 步。 diff --git a/.dsh/skills/id-family-semantic-migration/reference/steps-chinese-prefix-family.md b/.dsh/skills/id-family-semantic-migration/reference/steps-chinese-prefix-family.md index 1d07ce7f..c6aa16e3 100644 --- a/.dsh/skills/id-family-semantic-migration/reference/steps-chinese-prefix-family.md +++ b/.dsh/skills/id-family-semantic-migration/reference/steps-chinese-prefix-family.md @@ -1,8 +1,8 @@ -# Steps(中文前缀族:门禁咬不到的那一路)(自 SKILL.md 外置) +# Steps(中文前缀族:连归档扫描工具都咬不到的那一路)(自 SKILL.md 外置) > 本文由 id-family-semantic-migration 的 SKILL.md「Steps(中文前缀族:门禁咬不到的那一路)」一节逐字外置而来(渐进展开:主文件只留索引与指针)。步骤号沿用原文,未重编。 -1. **先确认门禁咬不到,并把这句话写进交付**:`scripts/archive/id_gate/check_ids.py` 的族 pattern 是拉丁形态、中文前缀不进语料 → **id-check 的 refs / 可判定比例 / 悬空引用与本族无关**;报告开头写「本族不在 check_ids 语料内,口径 = 自建扫描器」。 +1. **先确认连归档扫描工具也咬不到,并把这句话写进交付**(两道编号门禁已退役,机器面整体消失):`scripts/archive/id_gate/check_ids.py` 的族 pattern 是拉丁形态、中文前缀不进语料 → **id-check 的 refs / 可判定比例 / 悬空引用与本族无关**;报告开头写「本族不在 check_ids 语料内,口径 = 自建扫描器」。 2. **销项删行前先核证据属主**(这是删行的唯一许可证): - 先取历史口径:`git log --oneline -20 -- <文档>`、`git show :<文档> | sed -n '/^## 5/,/^## 6/p'` 找回被删的卡表与当初「完成即销项删行 / 标 ✅ 保留行」的理由; - 核对判据与实测记录**已完整沉淀进属主章节**(实测:「已知电压注入验证」六项判据全 PASS、斜率 6544.2 码/V、偏差 −0.14 % 全在 `docs/硬件平台/ADC通道映射说明.md` §8.1,执行侧在 `docs/测试说明/ADC前端注入验证流程.md`); @@ -48,8 +48,8 @@ 5. **派单前写一份 spec 文件**(`ci-artifacts/card_retire_spec.md`),作为各道唯一口径来源,含五块:① 背景与**用户裁定原文**;② 逐号映射表(旧号形态 → 语义短语 → 属主出处);③ 措辞规则(补短语时括号怎么降级、动不动字符串字面量、生成器归属、悬空引用怎么判);④ 硬约束(BOM 原样补回、行尾由 `.gitattributes` 写死、冻结历史行与原始证据不回改、代码头注释引用的设计约束章节保留、带 BOM 文件名单);⑤ 自检与交付格式(逐文件命中数、删了哪几行、残留数、门禁复跑结果)。`send_message` 只发路径 + 关键裁定,长中文正文不要整段塞进消息(一次串多条长消息会撑爆 120s,发出 1 条后 abort,剩余未发)。 6. **逐处替换断言命中数**(同第 7 步的 `rep(tag, old, new, want)`),且 `old` 串**从原文取、不凭记忆敲**:本仓中文引号与 ASCII 双引号混用(实测某文档里的「中文引号」其实是 ASCII `"`),凭印象写 `old` 必 `got=0`;断言抛错后先 `git status --porcelain=v1 -- <文件>` 自证文件未被别道改动,再回读该行真身重跑。删行类改动单独记 tag 并打印删了几行、行号与原行长度。多道并行时同一文件常被别道改过:`edit` 报 `file changed since it was read` → 重新 `read` 再改;报 `ReplaceFileW EIO (Win32 1175)` → 先自证文件字节完好(BOM / CRLF / `EF BF BD` 三项)再重试同一笔,不要顺手改内容。 7. **落地位置写死(这一族只有这几条兜)**: - - `docs/编号规范.md` **§4.1.1「已停发的族」表加一行**,三列齐全,第三列直写「⚠ **机器面空白**:中文前缀不进 `scripts/archive/id_gate/check_ids.py` 语料 → 门禁咬不到,只有本表 + §9.3 + 评审三条兜;`id_aliases.json` 刻意不登记(登记了机器也看不见,只多一张没人核的表)」; - - `docs/编号规范.md` **§9.3 逐号换算表**(旧号形态 → 语义短语 → 现行口径出处;本族实测 14 行 = 0、0b、1~12)+ §9 批次段落(裁定原文与逐号结论都落在正文),**写明旧号永久作废、不得重新取走**; + - `docs/编号规范.md` **§4.1.1「已停发的族」表加一行**,三列齐全,第三列直写「⚠ **这一族从来没有机器面**:中文前缀不进任何编号语料(族 pattern 全是拉丁形态),而两道编号门禁已于 2026-10-28 / 29 退役 → 只有本表 + 归档换算表 + 评审三条兜;`id_aliases.json` 刻意不登记(没有机器读者,登记等于多一张没人核的表)」; + - `scripts/archive/id_gate/编号治理历史.md` **§9.3 逐号换算表**(旧号形态 → 语义短语 → 现行口径出处;本族实测 14 行 = 0、0b、1~12)+ 同篇 §9 批次段落(裁定原文与逐号结论都落在归档正文),**写明旧号永久作废、不得重新取走**; - 表内**把形态写成「卡」+ 数字而不复述字面量**:规范本身是门禁语料,复述字面量会把「讲规则的文档」变成自己的引用站点; - **同步叙述**:`AGENTS.md`「文档与图形」的销项条目、`docs/README.md`(例:`数据可信.md = 三项阻塞任务` → 两项)、`docs/任务规划/README.md`、`docs/任务规划/登记簿.md`——「剩余三项」这类旧口径实测归零; - **与有识别面族的判别写进同一张表**:能被脚本按字面量匹配 → 走 `rename_ids.py` + 别名登记;匹配不到(中文前缀、纯散文号、圈号)→ 走规范换算表,**不登记别名**; diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c7f863bc..f6d32632 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -683,7 +683,7 @@ repos: # 14) 编号门禁(2026-10-28 上线只查新增行那一道;2026-10-29 用户裁定**整体退役**,钩子已摘) # 退役后编号形态没有机器面:判据回到 docs/编号规范.md §1 三句自问 + §4.1 族总表 + 评审。 # 脚本移入 scripts/archive/id_gate/check_ids_new.py(手工可跑,--self-test 保留),留档说明 - # → docs/门禁说明/id-check-new.md;两步路线 → docs/编号规范.md §8.5。 + # → docs/门禁说明/id-check-new.md;手工普查与恢复机器面 → docs/编号规范.md §8.5。 # ⚠ 计数口径:本条摘除使 YAML 解析条目 52 → **51**,须与 AGENTS.md 的「共 N 个钩子」、 # docs/门禁说明/README.md §2 标题三数同源同批改(自查命令见该条;别在注释里写数钩子的字面量)。 diff --git a/AGENTS.md b/AGENTS.md index d595e806..7473cbc5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,7 +93,7 @@ ## 提交门禁(pre-commit) -- 任一失败即拦截,共 **51 个钩子**(**口径 = YAML 解析后的 hook 条目数**:自查命令与「声明 / 文档 / 实跑」三数同源核对见 `docs/门禁说明/README.md` §2 首条——⚠ 直接 grep 配置里的声明行会多算一条**注释掉的**候选钩子(比口径多 1),不是口径;**配置注释里也别复述那个 grep 字面量**,它会让文本计数再虚高 1(2026-10-27 实测踩过);加一条钩子就同批改 AGENTS 与门禁索引两处计数。2026-10-25 道E 连加 `bom-guard` / `setview-check` / `uinav-selftest` 三条,2026-10-26 加 `generated-fresh` 一条,2026-10-27 加 `nested-comment` 一条;2026-10-28 加 `id-check-new` 一条并同批退役 `id-check`(口径 52 → 51:编号门禁换牙——新的只查新增行、零语料,旧的八项全库校验连语料一起移入 `scripts/archive/id_gate/`,路线见 `docs/编号规范.md` §8.5);2026-10-29 加 `skill-size` 一条(51 → 52),同日按用户裁定**退役 `id-check-new`**(52 → **51**:只查新增行的编号门禁整体退场,脚本与白名单一起移入 `scripts/archive/id_gate/`,编号形态此后无机器面,留档 `docs/门禁说明/id-check-new.md`);**每种门禁一篇详解(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径)→ `docs/门禁说明/README.md` 索引**;通用机制与整体口径 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + `bom-guard` / `setview-check` / `uinav-selftest`(2026-10-25 道E 那三条:前半句的加钩流水里有它们、逐项枚举里漏了,补上后本行各项相加才等于 51)+ 主机侧单测 **22 根专属钩子**(`-unit-test` 后缀 = 有独立 `files:` 触发面、需要秒级归因的那个子集;2026-10-25 增补 adc-cal / prot-dim / vol-idle-sync / ui-data / meas / run-par 六根)——**这是口径不是缺口**:`test/host/` 实测 44 套带 `run_unit_test.py`,其余 **22 套**(含 `test/host/ui_sink` 与录波三套)没有专属钩子,但每次 CI / `scripts/ci_local.py` 都被 `test-coverage` 总闸全量真跑并守基线回退,**失败照样拦**,只是归因落在总闸输出上;**没有专属钩子 ≠ 没被跑**,22 套台账与补钩子的判据 → `docs/门禁说明/host-unit-tests.md` §6 第 4 条。**Lead 裁定 2026-10-27:不补第 23 根**——读「这套跑没跑」去看总闸输出,别数钩子(原写「23 个 / 含 ui-sink 七个」是双重错:既把 22 说成 23,又宣称 ui-sink 有钩子——配置历史里 `ui-sink-unit-test` 从未存在)+ `test-coverage`(**覆盖率总闸**:跑全部 44 套并守基线回退;缺 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`)+ `nested-comment`(**块注释嵌套**:注释体内再出现 /* 会被 ARMCC 报 9-D,且**按包含者数量刷屏**、只在重编译该 TU 时才暴露;本仓诱因就是「在注释里写作用域/通配路径」,改法是写全目录或用尖括号 ;接线当日实扫 403 个 .c/.h,新发 0、存量 2 处(`test/host/common/` 两份假件头)钉只减不增基线 → `docs/门禁说明/nested-comment.md`)+ `skill-size`(**SKILL.md 不超插件写侧上限 65536 字节**:dsh-mnemon 只在写侧校验、加载侧不查,超限技能照常可读却每次「生成草案」被拒、永久卡在「生成中」;另查 `reference/` 指针悬空与 BOM → `docs/门禁说明/skill-size.md`)+ `mem-budget`(RAM 92% 预算门禁,登记簿条目「缩堆迁 CCM 后半(暂缓)」)+ `font-charset`(UI 字符集差集检查,防缺字白框)+ `ui-golden`(UI 像素回归,菜单 8 状态 + 内容页 66 张黄金参考)+ `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 条**))。**提交期实跑 48 根**(2026-10-28 用户裁定):`cppcheck` / `test-cppcheck` / `test-coverage` 三根带 `stages: [manual]`——实测合计 ~165 s = 一次带代码提交耗时的三分之二,提交期只跑快的那些;这三根由**远端 CI** 强制(`.gitea/workflows/ci.yml` 的「手动阶段门禁」步,逐根 `--hook-stage manual`)与 `scripts/ci_local.py` 的 manual 段,本地手工复跑 `python -m pre_commit run --all-files --hook-stage manual `(一次只能一个 id) +- 任一失败即拦截,共 **51 个钩子**(**口径 = YAML 解析后的 hook 条目数**:自查命令与「声明 / 文档 / 实跑」三数同源核对见 `docs/门禁说明/README.md` §2 首条——⚠ 直接 grep 配置里的声明行会多算一条**注释掉的**候选钩子(比口径多 1),不是口径;**配置注释里也别复述那个 grep 字面量**,它会让文本计数再虚高 1(2026-10-27 实测踩过);加一条钩子就同批改 AGENTS 与门禁索引两处计数。2026-10-25 道E 连加 `bom-guard` / `setview-check` / `uinav-selftest` 三条,2026-10-26 加 `generated-fresh` 一条,2026-10-27 加 `nested-comment` 一条;2026-10-28 加 `id-check-new` 一条并同批退役 `id-check`(口径 52 → 51:编号门禁换牙——新的只查新增行、零语料,旧的八项全库校验连语料一起移入 `scripts/archive/id_gate/`,手工普查与恢复机器面见 `docs/编号规范.md` §8.5);2026-10-29 加 `skill-size` 一条(51 → 52),同日按用户裁定**退役 `id-check-new`**(52 → **51**:只查新增行的编号门禁整体退场,脚本与白名单一起移入 `scripts/archive/id_gate/`,编号形态此后无机器面,留档 `docs/门禁说明/id-check-new.md`);**每种门禁一篇详解(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径)→ `docs/门禁说明/README.md` 索引**;通用机制与整体口径 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + `bom-guard` / `setview-check` / `uinav-selftest`(2026-10-25 道E 那三条:前半句的加钩流水里有它们、逐项枚举里漏了,补上后本行各项相加才等于 51)+ 主机侧单测 **22 根专属钩子**(`-unit-test` 后缀 = 有独立 `files:` 触发面、需要秒级归因的那个子集;2026-10-25 增补 adc-cal / prot-dim / vol-idle-sync / ui-data / meas / run-par 六根)——**这是口径不是缺口**:`test/host/` 实测 44 套带 `run_unit_test.py`,其余 **22 套**(含 `test/host/ui_sink` 与录波三套)没有专属钩子,但每次 CI / `scripts/ci_local.py` 都被 `test-coverage` 总闸全量真跑并守基线回退,**失败照样拦**,只是归因落在总闸输出上;**没有专属钩子 ≠ 没被跑**,22 套台账与补钩子的判据 → `docs/门禁说明/host-unit-tests.md` §6 第 4 条。**Lead 裁定 2026-10-27:不补第 23 根**——读「这套跑没跑」去看总闸输出,别数钩子(原写「23 个 / 含 ui-sink 七个」是双重错:既把 22 说成 23,又宣称 ui-sink 有钩子——配置历史里 `ui-sink-unit-test` 从未存在)+ `test-coverage`(**覆盖率总闸**:跑全部 44 套并守基线回退;缺 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`)+ `nested-comment`(**块注释嵌套**:注释体内再出现 /* 会被 ARMCC 报 9-D,且**按包含者数量刷屏**、只在重编译该 TU 时才暴露;本仓诱因就是「在注释里写作用域/通配路径」,改法是写全目录或用尖括号 ;接线当日实扫 403 个 .c/.h,新发 0、存量 2 处(`test/host/common/` 两份假件头)钉只减不增基线 → `docs/门禁说明/nested-comment.md`)+ `skill-size`(**SKILL.md 不超插件写侧上限 65536 字节**:dsh-mnemon 只在写侧校验、加载侧不查,超限技能照常可读却每次「生成草案」被拒、永久卡在「生成中」;另查 `reference/` 指针悬空与 BOM → `docs/门禁说明/skill-size.md`)+ `mem-budget`(RAM 92% 预算门禁,登记簿条目「缩堆迁 CCM 后半(暂缓)」)+ `font-charset`(UI 字符集差集检查,防缺字白框)+ `ui-golden`(UI 像素回归,菜单 8 状态 + 内容页 66 张黄金参考)+ `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 条**))。**提交期实跑 48 根**(2026-10-28 用户裁定):`cppcheck` / `test-cppcheck` / `test-coverage` 三根带 `stages: [manual]`——实测合计 ~165 s = 一次带代码提交耗时的三分之二,提交期只跑快的那些;这三根由**远端 CI** 强制(`.gitea/workflows/ci.yml` 的「手动阶段门禁」步,逐根 `--hook-stage manual`)与 `scripts/ci_local.py` 的 manual 段,本地手工复跑 `python -m pre_commit run --all-files --hook-stage manual `(一次只能一个 id) - **范围 = 项目代码**:第三方(`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/` diff --git a/docs/编号规范.md b/docs/编号规范.md index cd10264a..60791783 100644 --- a/docs/编号规范.md +++ b/docs/编号规范.md @@ -12,15 +12,15 @@ > **本文就是唯一事实源**:两道编号门禁已分别于 2026-10-28(全库八项校验)与 2026-10-29(只查新增行) > 退役,脚本、两张前缀白名单与族语料都在 `scripts/archive/id_gate/`,**不再进提交门禁**。 > 形态判据只剩本文 §1 的三句自问 + §4.1 族总表 + 人工评审;留档 `docs/门禁说明/id-check-new.md` -> 与 `docs/门禁说明/id-check.md`,路线见 §8.5。归档脚本随时可手工普查形态(`--range A..B`)。 +> 与 `docs/门禁说明/id-check.md`,手工普查命令与恢复机器面的成本见 §8.5。归档脚本随时能重扫一批改动的形态(`--range A..B`)。 --- -> **本文范围**:这里只留**现在还生效**的口径。跳号的节(`编号治理历史.md` §2 八条原则 / `编号治理历史.md` §4.2 / `编号治理历史.md` §4.5 / `编号治理历史.md` §6 模块码表 / -> `编号治理历史.md` §7 改号与迁移 / `编号治理历史.md` §8.1 基线 / `编号治理历史.md` §8.3 批次 / `编号治理历史.md` §8.4 台账 / `编号治理历史.md` §9 存量清点与换算表)是编号治理的**过程记录**, -> 2026-10-28 已整体移入 `scripts/archive/id_gate/编号治理历史.md`——要查历史判据、批次流水、 -> 「数据可信」任务卡号换算表,去那里;**改现在的做法只看本文**。 -> 节号沿用旧编号,故意不重排:外部文档与脚本引的是 `§4.1` / `§8.5` 这些号,重排等于制造一批假链接。 +> **本文范围**:这里只留**现在还生效**的口径。编号治理的**过程记录**(八条设计原则、基线数字、批次流水、 +> 存量清点与逐号换算表)2026-10-28 已整体移入 `scripts/archive/id_gate/编号治理历史.md`,下文简称 +> **`编号治理历史.md`**;它的节号沿用旧编号、故意不重排(外部文档与脚本引的是 `§4.1` / `§8.5` 这类号, +> 重排等于制造一批假链接)。要查历史判据、批次流水、「数据可信」任务卡号换算表,去那篇; +> **改现在的做法只看本文**。 ## 1. 决定要不要发号之前,先问三句 @@ -78,43 +78,43 @@ | `FR-*` | 需求 | `docs/产品需求/产品需求设计文档.md` §5 | 需求↔测试追溯 | | `CODE-RULE-` | 代码规范条款 | `scripts/check_coding_standard.py` | 门禁按条款号报错并统计存量 | | `MENU-STATE-` | UI 状态机键 | `simulator/tests/check_menu_rows.py` | 像素回归脚本按它取状态行 | -| `P-` | **本文的设计原则** | 本文 `编号治理历史.md` §2 | 就是本文自己的条目 | +| `P-` | 编号治理的设计原则 | `scripts/archive/id_gate/编号治理历史.md` §2(2026-10-28 随沿革归档,本文不再列条目) | **只读存量**:原则已归档,新写条目按 §3.3 写语义短语,不再配 P 号 | > ⚠ **「批次三不做了」只有一层意思:不去清这个族,同时把它的发号口关死。** 它**不是**「TC 族维持原状、 > 新用例照旧配号」——维持原状的是**存量号**,关掉的是**发号口**。三条对照,防止往任何一边读过头: > > - ❌「不做 = 维持原状 = 还能发」——**错**。新用例一律用**语义化测试函数名 + 文档用例表的行名**(§3.3), > 不再配 TC 号;指认**既有**用例仍可引用现存号(旧号只读,不是许可新配)。 -> - ❌「停发 = 这族要废了,顺手清掉、把号腾出来复用」——**也错**。现存号与它在 `scripts/archive/id_gate/id_registry.json` -> 里的登记,是 `check_test_traceability.py` 与 44 套 `run_unit_test.py` **按字面量读的追溯把手**, -> 清它等于换机制(`编号治理历史.md` §9.1);号不回收、不复用是 §1 的硬规矩。 -> - ❌「registry 里 TC 族还带着活游标,所以门禁允许我发」——**不成立:这条缺口已于 2026-10-27 同日关掉**。 -> 37 个 TC 类族现在**全部** `frozen: true`,其中 **34 个 `TC-HOST-*` 子族的 `cursor` 冻结在裁定日该套件存量 -> 最高号**(5 个游标低于存量的已抬到水位:`TC-PROTELEM-013→015`、`TC-SYSLOG-108→137`、`TC-ADCHEALTH-011→023`、 -> `TC-ADCCAL-017→023`、`TC-I104SRV-041→053`;**游标必须是字符串形态**,整型比较会静默不生效)→ 在套件 -> 登记处写一个比游标更高的 TC 号就是 §8 第 5 项「冻结族越游标」**错误级**,而 `id_known_backlog.json` 台账 -> 0 条指纹 → 增量拦截等于全量拦提交。探针实测(`check_ids` 的 Checker `_check_frozen`):在 `TC-PROTSET-` -> 段存量最高号之上再加一号 → 命中 1 条校验 5;写存量最高号本身 → 零误伤。 -> **还剩一处空白,别把这条机械化读成无死角**:主族 `TC`(没有专属子族的模块码,如 `TC-ADCIN-*`)的 `cursor` -> 仍为 `null`——一个游标表达不了每个模块码的水位,写全族最高号反而放行低号,所以按空白登记(探针实测: -> 给无专属子族的模块码(如 `TC-ADCIN-` 段)写一个新号,门禁不响)。这一面由三条兜:① §8 校验 3——新号没进属主登记处 = 悬空引用(错误级); -> ② `编号治理历史.md` §9.1 末段的**冻结水位**(只减不增,涨了就要问「这号是谁在停发之后发的」);③ 评审。**所以给这些模块 -> 补号,即使在文档表格里一并补了定义行,一样违反本裁定。** 要把这面也钉死(按模块码克隆子族,或给族级加 -> 字面量上限)与「真要清退」同属追溯机制改造,按 `编号治理历史.md` §7.3 单独开卡,别夹在本批里顺手做。 -> - **唯一的重启入口**:先由用户裁定 → 再回到本节把族放回上面那 7 行、并指出按它做字面量匹配的脚本 → +> - ❌「停发 = 这族要废了,顺手清掉、把号腾出来复用」——**也错**。现存号是用例输出行名与追溯脚本 +> `scripts/check_test_traceability.py` 认的把手(该脚本 2026-10-29 实测**只手工跑**:不在任何钩子、 +> `scripts/ci_local.py` 与远端 CI 里),清它等于换机制(`编号治理历史.md` §9.1);号不回收、不复用是 §1 的硬规矩。 +> - ❌「`id_registry.json` 里 TC 族带着游标,所以那边允许我发」——**不成立**。37 个 TC 类族 2026-10-27 全部 +> 登记 `frozen: true`,34 个 `TC-HOST-*` 子族的游标钉在当日各套件存量最高号(5 个低于存量的抬到水位: +> `TC-PROTELEM-013→015`、`TC-SYSLOG-108→137`、`TC-ADCHEALTH-011→023`、`TC-ADCCAL-017→023`、 +> `TC-I104SRV-041→053`;**游标必须是字符串形态**,整型比较会静默不生效)。那是那次裁定的**登记留痕**: +> 两道编号门禁退役后没有任何东西再读这份登记,写一个比游标更高的 TC 号不会再有机器反应,拦它的只有 +> 本节与评审——**别把"不报错"读成"允许发号"**。 +> - **已知空白面(当年登记时就写着,不是新发现)**:主族 `TC`(没有专属子族的模块码,如 `TC-ADCIN-` 段) +> 的 `cursor` 为 `null`——一个游标表达不了每个模块码的水位,写全族最高号反而放行低号。机器面在的时候它 +> 就不响,现在连那层壳都没有:**给这类模块码补号,即使在文档表格里一并补了定义行,一样违反本裁定**, +> 评审要专门盯这一面。要把死角钉死(按模块码克隆子族,或给族级加字面量上限)与「真要清退」同属追溯 +> 机制改造,按 `编号治理历史.md` §7.3 单独开卡,别夹在别的批里顺手做。 +> - **唯一的重启入口**:先由用户裁定 → 再回到本节把族放回上面那 5 行、并指出按它做字面量匹配的脚本 → > 才谈得上发号。顺序倒过来就是 `编号治理历史.md` §1.2 那四类事故的由来。 -**已停发、存量待清**(一律**不得再新增引用**;下表除 `TC-*` 外,登记未删只是为了让历史行不报「未登记前缀」, -清点见 `编号治理历史.md` §9.1。`TC-*` 是这一栏的**特例**:登记与现存号都要留着,它是机器把手而不是待清对象,见 `编号治理历史.md` §9.1 批次三): +**已停发、存量待清**(一律**不得再新增引用**;下表除 `TC-*` 外,`id_registry.json` 里剩的登记只是归档普查的 +语料残留,**不再当"登记处"读**——那份登记自 2026-10-28 起没有机器读者。清点见 `编号治理历史.md` §9.1。 +`TC-*` 是这一栏的**特例**:登记与现存号都要留着,现存号仍被追溯脚本按字面量读,不是待清对象 +(见 `编号治理历史.md` §9.1 批次三): | 族 | 状态 | 依据 | |---|---|---| -| `TC-*` | **保留现存号、即日停发(机器已冻结)**:34 个 `TC-HOST-*` 子族 `frozen: true`、游标钉在 2026-10-27 存量水位,越游标即 §8 校验 5 错误级;现存号只减不增,新用例改用语义化测试函数名 + 文档表格行名(主族 `TC` 的空白面见上方框) | 用户裁定 2026-10-27「批次三,不坐」;本族是 §4.1 判据唯一真被脚本按字面量读的族,清它等于换追溯机制,详见 `编号治理历史.md` §9.1 批次三 | +| `TC-*` | **保留现存号、即日停发**:`id_registry.json` 里 37 个 TC 类族登记 `frozen: true`、34 个 `TC-HOST-*` 子族游标钉在 2026-10-27 存量水位(**该登记 2026-10-29 起无机器读者,只是裁定留痕**);现存号只减不增,新用例改用语义化测试函数名 + 文档表格行名(主族 `TC` 的空白面见上方框) | 用户裁定 2026-10-27「批次三,不坐」;本族是 §4.1 判据唯一真被脚本按字面量读的族,清它等于换追溯机制,详见 `编号治理历史.md` §9.1 批次三 | | 族 | 形态 | 存量 | 为什么该清 | |---|---|---|---| -| `L-` | 保护功能号 | **批次二已整族转功能名,即日停发**:活引用 0,只剩一处**原始串口日志**(冻结证据、永久保留)与本文/技能文档的形态举例;属主文档表首格写功能名,不再配号 | 用户 2026-10-27 定调「语义标识是最好的」+ 本轮指令「把能清的族清掉一批」;探针(Checker _check_dangling):写一个新号(字母 L + 段号 + 连字符 + 两位数、从未定义过的形态)→ §8 校验 3 悬空引用**错误级** → 发号口已机器关闭。**不冻结数值游标**:L1/L2/…各段最高号不同,单一数字游标反而放行低号 | -| `F--` | 网络设计条目 | **批次一已转「`编号治理历史.md` §7 条目名」,即日停发**:代码与测试侧 0 处;属主文档 `编号治理历史.md` §7 编号列 13 条仅作历史对照(定义行不算引用) | 同上;探针:写一个新号(F 前缀 + 模块码 + 从未出现过的序号)→ 校验 3 **错误级**;引用旧号命中小写别名表 retired-to-phrase = **只读**,不构成发号许可 | +| `L-` | 保护功能号 | **批次二已整族转功能名,即日停发**:活引用 0,只剩一处**原始串口日志**(冻结证据、永久保留)与本文/技能文档的形态举例;属主文档表首格写功能名,不再配号 | 用户 2026-10-27 定调「语义标识是最好的」+ 本轮指令「把能清的族清掉一批」;发号口此前由"悬空引用"校验关着,**2026-10-29 起只由本表与评审关**。**数值游标不冻结**:L1/L2/…各段最高号不同,单一数字游标反而放行低号 | +| `F--` | 网络设计条目 | **批次一已转「`编号治理历史.md` §7 条目名」,即日停发**:代码与测试侧 0 处;属主文档 `编号治理历史.md` §7 编号列 13 条仅作历史对照(定义行不算引用) | 同上;引用旧号命中别名表 `retired-to-phrase` = **只读**,不构成发号许可 | | `N-*` | 登记簿 `N-` | 4 处 / 3 个字面量 | 登记簿已于 2026-10-26 全面改语义短语,残留的是漏网的旧引用 | | `TM-*` | 阶段/里程碑 `TM--` | 1 处 | 阶段号本就属 §3.3 明令不许写号的那类 | | `T-TEST` | 台架阶段 **裸形**(字母 T 紧跟一位数字) | 活引用 0(批次一清零;余下是本文自述与技能文档的形态举例) | 裸形最危险(同字母多义),且改「台架阶段一」即可 | @@ -130,36 +130,41 @@ |---|---|---| | 登记簿 `N-x` / `S-x` / `T-x`、专项 `A-x`、已删报告的 UI 簇 `U1~U5`(2026-10-26 方案 B) | 语义短语——**登记簿表首格本身就是短语**,如「双转换器无共同采样触发」「CI 远端闭环未验证」 | `scripts/archive/id_gate/id_aliases.json` 标 `retired-to-phrase` | | 网络设计条目 `F-NET/F-DRV/F-MS-`(12 个,2026-10-27 批次一) | 「`编号治理历史.md` §7 + 条目名」短语,如 `(`编号治理历史.md` §7「全局读即清搁浅事件」)`;条目名的权威清单 = `docs/网络/网络自愈架构设计.md` `编号治理历史.md` §7「条目名」列;那个 DRV 段孤儿号按本节末句**删号留事实**、未补定义 | `scripts/archive/id_gate/id_aliases.json` 标 `retired-to-phrase`(13 条)+属主文档 `编号治理历史.md` §7 编号列仅作历史对照 | -| 「数据可信」任务卡号(中文前缀「卡」+ 数字,形态见 `编号治理历史.md` §9.3;2026-10-27 批次五) | §5 表的**标识列直接写任务名**(「端子/子板变比确认」「两点标定与精度验收」),代码注释与文档写语义短语(「标定折算落地」「未标定闭锁」「遥测快照发布」…);**逐号换算表见本文 `编号治理历史.md` §9.3** | ⚠ **机器面空白**:中文前缀不进 `scripts/archive/id_gate/check_ids.py` 语料(族 pattern 全是拉丁形态)→ 门禁咬不到,今天只有本表 + `编号治理历史.md` §9.3 + 评审三条兜;`id_aliases.json` 刻意不登记(登记了机器也看不见,只多一张没人核的表) | +| 「数据可信」任务卡号(中文前缀「卡」+ 数字,形态见 `编号治理历史.md` §9.3;2026-10-27 批次五) | §5 表的**标识列直接写任务名**(「端子/子板变比确认」「两点标定与精度验收」),代码注释与文档写语义短语(「标定折算落地」「未标定闭锁」「遥测快照发布」…);**逐号换算表见本文 `编号治理历史.md` §9.3** | ⚠ **这一族从来没有机器面**:两道门禁的语料族 pattern 全是拉丁形态,中文前缀本就不在其中,何况两道都已退役 → 只有本表 + `编号治理历史.md` §9.3 + 评审三条兜;`id_aliases.json` 刻意不登记(没有机器读者,登记等于多一张没人核的表) | | 测试验证层级号(`L0`、`L0.5`、`L1~L3` 这一族,**注意与 UI 设计令牌层 L0、硬件位号 L1~L3、采集链旧层名等同形族无关**;2026-10-28 批次六) | 文档与 CI 步骤名写语义名(静态门禁 / 主机侧纯逻辑单测 / 板端测试套件 / 主机侧回归 / 人工目检);ASCII 短码(static / preflight / commitmsg / size / host-unit / coverage)**只进**日志标签、命令行旗标、CI job id、日志文件名 | ⚠ **同形族警示**:逐号换算表与判族表见 `scripts/archive/id_gate/编号治理历史.md` §10 与 §10.1(撞族六个,动手前先判族);本族虽是拉丁形态但**门禁咬不到**——两道编号门禁已退役(全库校验 2026-10-28、新增行检查 2026-10-29),停发只靠本表 + 归档换算表 + 评审 | | `E-xx` 审计发现、`P0-x`~`P3-x` 审计条目、报告六族 `D/R/F/N/U/S`、复审残号 `S1~S14`/`A-x`/`B1~B10`/`T-x`、模拟器差距 `H-x`/`L-x`、`TM-*` 阶段号、`DP-*`、`gen`/`round`/会话临时号 | 写条目本身("原子快照取数"、"队头阻塞"、"台架日志代次 288"、"第 3 轮台架") | 别名表标 `void`;**部分死号故意不进别名表**,好让门禁继续咬残留引用 | | 规范曾规划却从未启用的前缀族(`DEF`/`WI`/`DR`/`OQ`/`RSK`/`GAP`/`SC`/`GEN`/`MS`/`UL`/`LAT`/`CS` 等) | 同上,写事实 | 无定义,本就不该存在 | -> 想知道某个号还活着吗:跑 `scripts/archive/id_gate/check_ids.py --top 0`,或直接查 `id_aliases.json`——命中 `void` / -> `retired-to-phrase` 就是死号。**看到查不到的号,正确动作是删号留事实,不是补一个定义。** +> 想知道某个号还活着吗:查 `id_aliases.json`(命中 `void` / `retired-to-phrase` 就是死号),或手工跑 +> `scripts/archive/id_gate/check_ids.py --top 0`——**报告制,不影响任何退出码**。**看到查不到的号, +> 正确动作是删号留事实,不是补一个定义。** ### 4.3 冻结前缀(历史快照,正文不回改) -族还在注册表里、但属主登记处已删的:**只为让冻结历史行不报"未登记前缀"而保留登记**,游标一律冻结, -不得新增、不得复用。 +族还在 `scripts/archive/id_gate/id_registry.json` 里、属主登记处已删的那批:登记留着只为让归档普查 +不报"未登记前缀",游标一律冻结,**不得新增、不得复用**。⚠ 2026-10-29 起这份登记**没有机器读者**, +它是档案不是闸门——别指望写一个冻结族的号会有人来拦。 -### 4.4 这些**不是**编号——别顺手清,也别当门禁漏洞 +### 4.4 这些**不是**编号——别顺手清 元器件位号 `U1`/`C12`/`R3`/`L5`/`D7`(硬件语料按目录整体排除)、数学与协议记号(`N-1`、测频的正序与 负序分量 `S1`/`S2`、`SHA-256`、`RS-485`、`ADC1_IN14`、手册页码)、活表代号 `E0~E19`(网络自愈 §5.2, **无连字符**,与死号 `E-nn` 只差一个连字符)、Socket 状态组合 `U1~U15`(网络自愈 §1.1)、档位词 `P0~P3`。 判据只有一条:**带连字符、且能在属主登记表查到定义**才叫编号;查不到的就是普通文本,不是「漏登记的号」。 -> 2026-10-27 补两条实测口径:① **元器件位号目录整体排除**——`tools/netlist_semantics/` 是硬件知识库 -> (字母 T 紧跟数字在那里是网络变压器位号),已进 `id_registry.json` 的 `excluded_paths`,与 `docs/硬件平台/` 同口径; -> ② **唯一已知误判**:`docs/算法/傅氏算法详解.md` 里的数学记号 `N-1`(「一个周波 N 个点」的求和上界)会被 -> `N-x` 族命中,属可判定引用、不影响两根护栏,**不清**——清了反而破坏公式。 +> 两条口径来自当年的语料审计,今天判"是不是编号"照它想:① **元器件位号整目录排除**—— +> `tools/netlist_semantics/` 是硬件知识库(字母 T 紧跟数字在那里是网络变压器位号),与 `docs/硬件平台/` +> 同口径(归档普查把它写在 `id_registry.json` 的 `excluded_paths`);② **唯一已知误判**: +> `docs/算法/傅氏算法详解.md` 里的数学记号 `N-1`(「一个周波 N 个点」的求和上界)形似 `N-x` 族, +> 属可判定的普通文本,**不清**——清了反而破坏公式。 ## 5. 分配、引用、销项 ### 5.1 新增编号 -先过 §1 三问。确实要发号:① 在 `id_registry.json` 确认族与游标 ② 在属主登记处写定义行 ③ 才允许在 -别处引用。**没有属主登记处的号不许存在**——门禁的校验 3 就是照这句实现的。 -**族不在 §4.1 那 5 行之内的,第 ① 步就该走不通**(`TC-*` 已停发:即使在 registry 里查得到它、游标还是活的, -也不构成"能发号"的理由,见 §4.1 末框)。 +先过 §1 三问。确实要发号:① 族必须落在 §4.1 那 5 行之内(**表外一律不发**)② 在属主登记处写定义行 +③ 才允许在别处引用。**没有属主登记处的号不许存在**——这句原先由八项校验的"悬空引用"实现,该校验 +2026-10-28 退役,现在它是纸面规矩,只有评审在执行。 +`scripts/archive/id_gate/id_registry.json` 的族与游标登记**只在恢复机器面时才动**(§8.5):没有脚本读它, +写进去也拦不住任何人——**别把"registry 里查得到"当成发号许可**(`TC-*` 正是这一类:停发与登记并存, +许可只来自 §4.1 那张表)。 ### 5.3 代码注释 **优先写短语**(`FTP 鉴权`、`监听生命周期模块化`),别写字母号;确需引用活号时必须带属主文档名。 @@ -168,7 +173,13 @@ ### 5.5 销项与归档 删登记行 + **同步清掉全仓引用**(清引用是销项的一部分,不是"以后再说")+ 别名表标 `void`。 -冻结的历史(变更记录行、`[示例]` / `[计划]` 行)不回改,门禁对它们已豁免。**现状补充**:2026-10-28 起新文档一律不再设「变更记录」节(`docs/文档规范.md` §5.1),所以这条冻结规则只对**仍保留变更记录表的 5 篇**(同一份清单)与 `scripts/archive/id_gate/` 下的归档文档生效——不是「以后还能往变更记录里写」的理由。**唯一例外**(用户裁定 2026-10-28):这类行里**已停发 / 作废族的示例编号**可改成形态描述(写成「字母紧跟数字」这类说法)——改的是示例的写法,不是历史事实;**保留现存号**族的游标数字与硬件位号举例不在例外内,形态化等于毁证据。 +冻结的历史(`[示例]` / `[计划]` 行、仍保留变更记录表的文档)不回改。**2026-10-28 起新文档一律不再设 +「变更记录」节**(`docs/文档规范.md` §5.1——例外清单以那一节为准,本文不复制份数,抄一份就会漂一次)。 +所以"冻结历史行"只对清单里的文档与 `scripts/archive/id_gate/` 下的归档文档生效,不是「以后还能往变更 +记录里写」的理由。**唯一例外**(用户裁定 2026-10-28):这类行里**已停发 / 作废族的示例编号**可改成形态 +描述(写成「字母紧跟数字」这类说法)——改的是示例的写法,不是历史事实;**保留现存号**族的游标数字与 +硬件位号举例不在例外内,形态化等于毁证据。原先那句「门禁对它们已豁免」随门禁一起失效: +现在没有任何门禁会去看这些行。 ### 5.7 语义标识的登记与别名 短语就是登记处的行名,不需要别名表;**只有"号转短语"**时才在别名表留一条 `retired-to-phrase`, @@ -176,33 +187,31 @@ --- -## 8. 门禁(2026-10-29 起:一道也没有) +## 8. 机器面:2026-10-29 起一道也没有 **现行没有编号门禁**:2026-10-29 用户裁定把最后那道只查新增行的 `id-check-new` 也摘了(钩子口径 52 → 51,脚本与白名单移入 `scripts/archive/id_gate/check_ids_new.py`,留档 `docs/门禁说明/id-check-new.md`, 理由与手工跑法都在那篇)。判据回到本文 §1 三句自问、§4.1 族总表与人工评审。 -本节以下(`编号治理历史.md` §8.1~`编号治理历史.md` §8.4)是**已退役**的全库语料审计(`id-check`,八项校验)的留档: -脚本与两份语料已整体移入 `scripts/archive/id_gate/`,不再进提交门禁, -只在你手工复查「存量编号现状」时才跑。退役理由与判据替换见 §8.5; -八项校验一句话版(历史口径):① 未登记前缀 ② 形态不合规 ③ 悬空引用 ④ 重复定义 -⑤ 冻结族越游标 ⑥ 编号未紧邻文档名 ⑦ 孤儿条目(告警)⑧ 可判定比例 + 两根护栏。 +全库八项校验(未登记前缀 / 形态不合规 / 悬空引用 / 重复定义 / 冻结族越游标 / 未紧邻文档名 / 孤儿告警 / +可判定比例 + 两根护栏)2026-10-28 退役:它查过什么、为什么退、当年清点数字 → `docs/门禁说明/id-check.md` +与 `scripts/archive/id_gate/编号治理历史.md`。**本文不抄第二份口径**——那份口径已经数错过一次了。 -### 8.5 门禁退役路线(2026-10-28 裁定,2026-10-29 收口) +### 8.5 手工普查与恢复机器面 -目标不是"关掉检查",而是把成本从**养 99 族语料**降到**禁止新编号**,最后一步是承认连这点机器面 -也不划算(2026-10-29 用户裁定)。四步,每步可独立回滚: +**没有门禁不等于没有工具**,两份脚本都还在,随时能跑(它们退成了工具,不是消失了): -| 步 | 内容 | 状态 | -|---|---|---| -| ① | 两根护栏(比例基线 / 不可判定绝对数上限)**转报告制**:照打数字,不再影响退出码 | **已完成 2026-10-28** | -| ② | 用 **diff 级新号拦截**替换语料审计:只看新增行里命中"字母+数字"形态、且不在白名单里的字面量,FAIL 并提示改成语义短语;白名单 = 前缀级两张表(活族 11 + 器件/协议/点号约 90),基本不增长 | **已完成 2026-10-28**(脚本 + 钩子 `id-check-new`,与 `id-check` **双跑观察期**;脚本今在 `scripts/archive/id_gate/check_ids_new.py`,详解 `docs/门禁说明/id-check-new.md`) | -| ③ | 退役语料:99 族语料+脚本整体移入 `scripts/archive/id_gate/`(保留可复现性,不再进提交门禁);`check_ids.py` 降为手工报告工具,钩子摘除(口径 52 → 51) | **已完成 2026-10-28**(用户裁定跳过观察期直接做;当时 `id-check-new` 是唯一现役编号门禁) | -| ④ | 退役最后那道新增行拦截:钩子摘除(口径 52 → 51)、脚本与两张白名单移入 `scripts/archive/id_gate/`;此后**编号形态无机器面** | **已完成 2026-10-29**(用户裁定;留档 `docs/门禁说明/id-check-new.md`,含拦下过什么与为什么退) | +```text +scripts/archive/id_gate/check_ids_new.py --range A..B # 只看一段提交的新增行里有没有自造形态 +scripts/archive/id_gate/check_ids.py --top 0 # 全库存量普查(报告制,不影响退出码) +``` -**④ 之后没有任何机械牙**:别把"门禁不响了"读成"可以随便写号"——自造前缀在评审里照样扎眼, -而 `scripts/archive/id_gate/check_ids_new.py --range A..B` 随时能把一批改动重扫一遍(它退成了工具, -不是消失了)。要恢复机器面也便宜:把脚本放回 `scripts/` 并重新挂一条钩子(口径 +1,同批改三处计数)。 +- **恢复机器面的成本**:把脚本放回 `scripts/`、在 `.pre-commit-config.yaml` 重挂一条钩子、口径 +1, + 并同批改 `AGENTS.md` 与 `docs/门禁说明/README.md` 两处计数——三处一起做,只挂钩子会在别人提交里露馅。 +- **为什么走到"一道也没有"**:2026-10-28 退全库八项校验(99 族语料养不起,成本落在每次碰文档的提交上), + 2026-10-29 退最后那道新增行拦截(用户裁定:它咬形态不咬语义,白名单本身成了维护面)。两道各自拦下过 + 什么、退役当日那次误拦(录波段文件名 `WAVE00.BIN`)→ `docs/门禁说明/id-check-new.md` 与 + `docs/门禁说明/id-check.md`;四步流水与当时的探针数字 → `scripts/archive/id_gate/编号治理历史.md`。 --- @@ -213,8 +222,9 @@ - 想给文档内步骤或测试阶段发号 → 不许,写「步骤 3」「阶段 7」(§3.3)。 - 想新增前缀族 → 先回答"哪个脚本要按字面量匹配它"(§4.1);答不出就别建。 - 要改号 → `scripts/rename_ids.py` + `编号治理历史.md` §7.3 五步。 -- 编号形态拿不准 → 本文 §1 三句自问;两道门禁的历史与手工跑法 → `docs/门禁说明/id-check.md`、 - `docs/门禁说明/id-check-new.md`。 +- 编号形态拿不准 → 本文 §1 三句自问 + §4.1 那张表;**没有任何机器会替你兜**。两道门禁的历史与退役理由 → + `docs/门禁说明/id-check.md`、`docs/门禁说明/id-check-new.md`。 +- 想让机器帮你扫一批改动,或要恢复机器面 → §8.5。 --- From 88601422c3b00cbb2ccd37d2753f1182beb3845a Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 21:21:59 +0800 Subject: [PATCH 11/19] =?UTF-8?q?docs(=E6=96=87=E6=A1=A3=E8=A7=84=E8=8C=83?= =?UTF-8?q?):=20=E6=B8=85=E7=A9=BA=E5=A3=B3=E5=8F=98=E6=9B=B4=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E8=8A=82=EF=BC=8C=E4=BE=8B=E5=A4=96=E6=B8=85=E5=8D=95?= =?UTF-8?q?=E6=94=B9=E6=88=90=E5=8F=AF=E5=A4=8D=E8=B7=91=E5=8F=A3=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 17 篇文档的变更记录(含修订记录、带编号与中文序号标题)节里只剩一句 「不再记流水账」的占位话,整节删掉,连带它前面的分隔线。 仍保留真台账的只有 5 篇(看门狗 / 配置文件设计 / RAM 预算台账 / 内容页架构 / 本地工具路径方案),外加 scripts/archive 归档区。 份数口径改成可复跑的普查:文档规范 §5.1 补自查命令, stale-doc-count-sweep 的速查表与陷阱各加一条——只 grep 裸标题会漏 带编号与中文序号的两种写法,实测 5 篇只数得出 3 篇,差点据此把 全仓正确的份数改错。 门禁:逐篇与全量 check_doc_links 除 simulator/SDL2 联接假红外 0 问题; skill-size rc=0;表格列数与字节体检相对 HEAD 零新增问题。 --- .dsh/skills/stale-doc-count-sweep/SKILL.md | 5 +++-- .../stale-doc-count-sweep/reference/pitfalls.md | 5 +++-- .../skills/stale-doc-count-sweep/reference/steps.md | 2 +- docs/CI/README.md | 4 ---- docs/CI/远程CI完整指南.md | 6 ------ docs/README.md | 6 ------ docs/UI设计/UI_自动化测试流程.md | 5 ----- docs/UI设计/UI_设计架构.md | 4 ---- docs/子系统设计/控制台单写者日志设计说明.md | 6 ------ docs/文档规范.md | 13 +++++-------- docs/测试说明/自动化测试流程.md | 4 ---- docs/测试说明/配置文件单元测试说明.md | 6 ------ docs/环境与部署/项目部署环境要求.md | 6 ------ docs/编号规范.md | 6 ------ docs/网络/网络自愈架构设计.md | 4 ++-- docs/门禁说明/bsp-err.md | 4 ---- docs/门禁说明/doc-links.md | 4 ---- docs/门禁说明/font-charset.md | 4 ---- docs/门禁说明/id-check.md | 5 ----- docs/门禁说明/layer-deps.md | 4 ---- docs/门禁说明/ui-golden.md | 4 ---- 21 files changed, 14 insertions(+), 93 deletions(-) diff --git a/.dsh/skills/stale-doc-count-sweep/SKILL.md b/.dsh/skills/stale-doc-count-sweep/SKILL.md index faee5ccb..f1165f75 100644 --- a/.dsh/skills/stale-doc-count-sweep/SKILL.md +++ b/.dsh/skills/stale-doc-count-sweep/SKILL.md @@ -1,6 +1,6 @@ --- name: stale-doc-count-sweep -description: 全仓陈旧计数同步 playbook——某个事实数(黄金参考页数/张数、菜单树叶数与已绑叶数、字库字数与字形数、提交钩子数等)变了之后:每个数先剔伪命中再双源互证定真值 → 全形态全命中面扫描(含 .pre-commit 钩子显示名与 .dsh/skills 正文)→ 逐条分类(当前态改 / 历史行冻结(2026-10-28 起只剩保留变更记录的 5 篇) / 同数不同义不动 / 带日期戳的测量只加「当时」/ 数字对但口径描述错另起一笔)→ 每处先读现文再字面替换并断言命中数 → 全文通读核对各小节 → 提交后再按全部旧形态复扫归零 → 跑门禁分笔提交。 +description: 全仓陈旧计数同步 playbook——某个事实数(黄金参考页数/张数、菜单树叶数与已绑叶数、字库字数与字形数、提交钩子数等)变了之后:每个数先剔伪命中再双源互证定真值 → 全形态全命中面扫描(含 .pre-commit 钩子显示名与 .dsh/skills 正文)→ 逐条分类(当前态改 / 历史行冻结(只有保留变更记录的 5 篇真台账与 scripts/archive/ 归档) / 同数不同义不动 / 带日期戳的测量只加「当时」/ 数字对但口径描述错另起一笔)→ 每处先读现文再字面替换并断言命中数 → 全文通读核对各小节 → 提交后再按全部旧形态复扫归零 → 跑门禁分笔提交。 whenToUse: 某个计数/数字变了要跨 md/yaml/py/txt 同步、用户说「同步一下 / 全仓改一遍 / 这个页数(张数、叶数、字形数、钩子数)不对」、怀疑某个数是伪命中或口径错了(含 notdef 吗、含 ASCII 吗、是张数还是页数)、发现文档数字与实测不符、或同一份文档两节数字/顺序对不上要先核口径时。 --- @@ -19,6 +19,7 @@ whenToUse: 某个计数/数字变了要跨 md/yaml/py/txt 同步、用户说「 | 提交**钩子数** | YAML 解析后的条目数(实测 **51**):自查命令见 `docs/门禁说明/README.md` §2 首条;与 AGENTS.md「共 N 个钩子」、该目录 §2 标题**三数同源** | `grep -c` 配置里的声明行 = 52(多算一条**被注释掉的** clang-format 候选);在配置**注释里复述那个 grep 字面量**会让文本计数再虚高 1 | | 字库**唯一字数** | `ui_cn_chars.txt` 唯一码点数(2026-10-29 实测 **224**,其中 ASCII 区 **0** 个) | `make_font.py` 的 `-r 0x20-0x7E` 是 ASCII 子集参数,**不算进字数** | | **字形数** | `Drivers/BSP/UI/ui_font_cn_12.c` 里 `.bitmap_index` 出现次数 **320 条含 notdef → 319 字形**;换算式 = 224 汉字/符号 + ASCII 95 = 319 | 直接报 314 就是把 notdef 当字形;文档必须写出「不含 ASCII」「含 notdef 共 314 条」这类口径句 | +| 变更记录**保留篇数** | 变体标题感知的普查(`grep -rn "^## " docs scripts` 再看哪些标题是变更记录 / 修订记录);当前态清单以 `docs/文档规范.md` §5.1 那行为准 | 只 grep 裸 `^## 变更记录` 会**漏算带编号与中文序号的标题**(实测 5 篇真台账里 2 篇是 `## 6.` / `## 九、`,另有 `## 修订记录` 变体)——2026-10-29 就差点把 5 改成 3 | | 菜单**节点数 / 叶子数** | `ui_menu_tree.txt` 头部规则:缩进即层级(每级 2 空格)、`#` 开头为注释、空行忽略;叶子 = **不存在更深缩进后继行**的行 | 43 是节点行数、34 才是叶子(2026-10-29 现值;42 / 33 是此前时点);`未绑 = 待做 + 不做` 要与分类表逐项闭合 | ## Steps @@ -66,7 +67,7 @@ whenToUse: 某个计数/数字变了要跨 md/yaml/py/txt 同步、用户说「 - [ ] 替换脚本打印了每处 `old -> new xN` 且每处 `assert` 通过(无半落地;报错时已 `git diff --stat` 确认零写入) - [ ] 命中面覆盖 `.pre-commit-config.yaml` 钩子 `name:`、AGENTS.md、`docs/**`、`scripts/**`、`.dsh/skills/*/SKILL.md`,逐条在改动清单里出现 - [ ] 被改文件已全文通读,同文档各小节与配套 README/清单的顺序、计数、完成状态一致;口径闭合(如 `未绑 = 待做 + 不做`) -- [ ] 变更记录:**保留的 5 篇**历史行原文未动;其余文档本轮**零新增流水行**(当前态改完即止);钩子一增一减这类过程细节写进提交正文,不写进文档 +- [ ] 变更记录:**保留的 5 篇真台账**(清单 → `docs/文档规范.md` §5.1)历史行原文未动;其余文档本轮**零新增流水行**(当前态改完即止);钩子一增一减这类过程细节写进提交正文,不写进文档 - [ ] **提交后**又按全部旧形态复扫一次,当前态残留 = **0**,每条剩余命中都能归类(历史行 / 误报 / 已加「当时」);发现的漏网当前态已单独补一笔 - [ ] `check_doc_links.py --all --sections-strict` 打印「文档路径检查通过(N 个文档)」;`ci_local.py` / `pre-commit` 全绿(RC=0、`failed=0`);BOM 与原文件一致、无 `EF BF BD` - [ ] 分笔提交完成,回报 commit 号 + 每笔改了哪些文件 + 没动的那几类及理由 diff --git a/.dsh/skills/stale-doc-count-sweep/reference/pitfalls.md b/.dsh/skills/stale-doc-count-sweep/reference/pitfalls.md index 7459efa4..c7675d87 100644 --- a/.dsh/skills/stale-doc-count-sweep/reference/pitfalls.md +++ b/.dsh/skills/stale-doc-count-sweep/reference/pitfalls.md @@ -8,10 +8,11 @@ - **权威脚本乱码就臆读或退回抄文档**:Windows 代码页下 `font-charset` 的整行输出退化成 U+FFFD 替换字符时,改数源文件符号出现次数,别猜、别抄旧文档。 - **钩子数用 `grep` 声明行**:多算一条注释掉的候选钩子(51 变 52);反过来在配置注释里复述那个 grep 字面量会让文本计数再虚高 1。口径只认 YAML 解析后的条目数,且要与 AGENTS.md、`docs/门禁说明/README.md` §2 标题、`ci_local.py` 实跑 passed+failed+skipped 三数同源。 - **张数与页数互相冒充**:一个 12 行分页页第 2 页要单独拍一张,于是「+2 页」可能等于「+3 张」。两个数分别报、分别换算,别互相顶替。 +- **数篇数只 grep 裸标题**:`^## 变更记录` 漏掉 `## 6. 变更记录`、`## 九、变更记录`、`## 修订记录` 三种写法 → 篇数直接少数一半,拿这个错数去"同步"全仓就是把对的改成错的(2026-10-29 实测:5 篇真台账只数出 3 篇,改到第 10 处才发现)。普查一律 `grep -rn "^## " docs scripts` 再看标题,或直接读 `docs/文档规范.md` §5.1 那行的例外清单。 - **只扫 docs/**:漏 `.pre-commit-config.yaml` 钩子 `name:` 显示名里的页数、AGENTS.md 钩子清单、`.dsh/skills/*/SKILL.md` 自带的计数与脚本路径(skill 正文也是命中面:font-regen 的 179/274 本窗最后才改)。 - **提交完就收工**:第 7 步的提交后复扫才是把漏网当前态(架构总览 §3)揪出来的那一步;漏了它,仓库里就留下一行「当前态写旧数」直到别人再扫。 -- **去已删掉变更记录的文档里「补一行流水」**:2026-10-28 起全仓只有 **5 篇**保留变更记录表(清单 → `docs/文档规范.md` §5.1),计数同步**只改当前态**、过程交给 `git log`,别再到别的文档里找或加变更记录行。真要往那 5 篇的台账里加行,历史坑照旧:它们的表按日期**升序** → 新行**追加为末行**,插表顶会把首行「初版」挤成第二行。 -- **历史行被当陈旧值改掉**:「47 → 53 页」是当天事实,改了就是伪造历史;正确动作是**只改当前态那几处**、历史行一个字不动(保留变更记录的 5 篇要写台账也只追加、不回改)。**历史行被写坏**时按无反引号兜底写法整行重写,别用同样写法再改一遍。 +- **去已删掉变更记录的文档里「补一行流水」**:2026-10-29 起全仓只剩 **5 篇**真台账保留变更记录表(清单 → `docs/文档规范.md` §5.1),外加 `scripts/archive/` 归档;其余文档**没有这一节**。计数同步**只改当前态**、过程交给 `git log`,别再到别的文档里找或加变更记录行。真要往那 5 篇的台账里加行,历史坑照旧:它们的表按日期**升序** → 新行**追加为末行**,插表顶会把首行「初版」挤成第二行。 +- **历史行被当陈旧值改掉**:「47 → 53 页」是当天事实,改了就是伪造历史;正确动作是**只改当前态那几处**、历史行一个字不动(保留变更记录的 5 篇真台账要写台账也只追加、不回改)。**历史行被写坏**时按无反引号兜底写法整行重写,别用同样写法再改一遍。 - **照抄用户给的数**:用户说「从 40 扩到 47」也要自己数 `ref/*.bin`;数出来不一定是 47。 - **把节点行当叶子数**:42 是 `ui_menu_tree.txt` 的节点行数、33 才是叶子;不读文件头规则就数错(实测把错数写进两份文档,下一轮才自查出来)。 - **整文件正则批处理**:第一次尝试的正则没编译过、整批零落地。改法 = 逐条字面替换 + 每处 `assert count == expect`。 diff --git a/.dsh/skills/stale-doc-count-sweep/reference/steps.md b/.dsh/skills/stale-doc-count-sweep/reference/steps.md index 9f29aad5..f9a8d221 100644 --- a/.dsh/skills/stale-doc-count-sweep/reference/steps.md +++ b/.dsh/skills/stale-doc-count-sweep/reference/steps.md @@ -33,7 +33,7 @@ | 类别 | 判定 | 动作 | |---|---|---| | 当前态陈述 | 描述「现在是多少」 | 改成实测真值 | - | 变更记录历史行 | 形如「47 → 53 页」的当日事实 | **原样冻结不回改**——但这条**只适用于保留变更记录的 5 篇**(台账 / 被别处当唯一证据出处的文档,清单 → `docs/文档规范.md` §5.1);其余文档的变更记录节已于 2026-10-28 整节删除,**不再追加任何流水行**,数字同步只改**当前态** | + | 变更记录历史行 | 形如「47 → 53 页」的当日事实 | **原样冻结不回改**——但这条**只适用于保留变更记录的 5 篇真台账**(台账 / 被别处当唯一证据出处的文档,清单 → `docs/文档规范.md` §5.1)与 `scripts/archive/` 归档;其余文档没有变更记录节,**不再追加任何流水行**,数字同步只改**当前态** | | 同数不同义 | `最坏路径 < 40KB` 这类内存/绘制缓冲红线 | **不动**,并在结论里写明「为什么不动」 | | 带日期戳的测量 | 「2026-10-26 实跑 …覆盖当时 40 页中的」 | **不臆改新数**,只补「当时」两字锁口径;新数留待下次实跑回填 | | 数字对但**口径描述错** | 数没错、句子把口径讲错了(实测:写「218 字(含 ASCII/符号,共 313 字形)」,而 `ui_cn_chars.txt` 的 218 个码点**全为非 ASCII**) | **另起一笔只改口径的提交**(`6daa01ae`),不与数字同步混笔;这类命中在任何数字 diff 里都看不见,只能把口径句逐句读一遍 | diff --git a/docs/CI/README.md b/docs/CI/README.md index e999581c..28e03ba2 100644 --- a/docs/CI/README.md +++ b/docs/CI/README.md @@ -76,7 +76,3 @@ - 本地等价脚本:[`../../scripts/ci_local.py`](../../scripts/ci_local.py) - runner 机器的工具链环境要求:[`../环境与部署/项目部署环境要求.md`](../环境与部署/项目部署环境要求.md) - `DTU_*` 工具路径变量机制:[`../环境与部署/本地工具路径环境变量方案.md`](../环境与部署/本地工具路径环境变量方案.md) - -## 变更记录 - -> (本文件不再记流水账:改了什么是正文说了算,改动过程查 git log。) diff --git a/docs/CI/远程CI完整指南.md b/docs/CI/远程CI完整指南.md index b3b621a0..e3345b75 100644 --- a/docs/CI/远程CI完整指南.md +++ b/docs/CI/远程CI完整指南.md @@ -1350,9 +1350,3 @@ powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -DryRun > 更早基于 Windows 服务的 `deploy_runner.ps1` / `install_service.ps1` 已删除 > —— 那条路走不通(§3.0)。 - ---- - -## 变更记录 - -> (本文件不再记流水账:改了什么是正文说了算,改动过程查 git log。) diff --git a/docs/README.md b/docs/README.md index 2f1038a2..d22b4d82 100644 --- a/docs/README.md +++ b/docs/README.md @@ -278,9 +278,3 @@ docs/ | ~~`docs/文档规范.md` 未创建~~ | — | ✅ 已创建(2026-09-25):[`文档规范.md`](文档规范.md) | > 未闭环的文档类问题**只剩仓库膨胀**(登记簿条目「仓库体积膨胀观察」,已决策暂不处理)。 - ---- - -## 12. 变更记录 - -> (本文件不再记流水账:改了什么是正文说了算,改动过程查 git log。) diff --git a/docs/UI设计/UI_自动化测试流程.md b/docs/UI设计/UI_自动化测试流程.md index c2c11f63..0e4a7b23 100644 --- a/docs/UI设计/UI_自动化测试流程.md +++ b/docs/UI设计/UI_自动化测试流程.md @@ -200,8 +200,3 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警 (两套走法迟早分叉,而分叉的那一边往往是真机)。导航器**默认只过查看密码**:判据需要写 路径权限时必须显式传 `modify=True`,**不因「能进去」就自动升格成可写**——只读判据偷偷拿到 修改权限,等于把判据的边界让给了实现。 - ---- -## 变更记录 - -> (本文件不再记流水账:改了什么是正文说了算,改动过程查 git log。) diff --git a/docs/UI设计/UI_设计架构.md b/docs/UI设计/UI_设计架构.md index 8926145f..566008d8 100644 --- a/docs/UI设计/UI_设计架构.md +++ b/docs/UI设计/UI_设计架构.md @@ -288,7 +288,3 @@ ENTER/→ 进子框: - **残留暴露**(刻意不归本票):单个往返自身仍无上界(osMessageQueuePut(osWaitForever) + xTaskNotifyWait(portMAX_DELAY)),netTask 若被单消息拖 >800ms,UI 位连击照成立—— 那是「判据抓对了人」,处置在 net 侧,不在心跳侧。 - -## 六、变更记录(简表) - -> (本文件不再记流水账:改了什么是正文说了算,改动过程查 git log。) diff --git a/docs/子系统设计/控制台单写者日志设计说明.md b/docs/子系统设计/控制台单写者日志设计说明.md index c2fd021c..a1613cde 100644 --- a/docs/子系统设计/控制台单写者日志设计说明.md +++ b/docs/子系统设计/控制台单写者日志设计说明.md @@ -481,9 +481,3 @@ D4 否决理由对现状仍逐条成立(115200 发送率低、日志行稀疏 | `tx_timeout` 读口接线(STALL 证据行加字段) | §13-O2 | 涉 `App/util/task_watchdog.c` 与被解析证据行的台架判据,非本卡写域;维持 O2“后续项” | | 未注册期裸发互斥 / `printf` 遗留开销 | §13-O1/O3 | 现状 = 纪律已落地,开机日志密度上升再议 | | 日志静默观察项 | 登记簿曾登记(已销项) | 见 A.3:V5 复跑双零后销项(归 lead) | - ---- - -## 修订记录 - -> (本文件不再记流水账:改了什么是正文说了算,改动过程查 git log。) diff --git a/docs/文档规范.md b/docs/文档规范.md index 95b21016..aa887590 100644 --- a/docs/文档规范.md +++ b/docs/文档规范.md @@ -264,7 +264,7 @@ **章节引用(D 类)另有的跳过**:昵称式引用(「内存方案 §2.1」)、非紧邻写法、变更记录小节内的历史指称、外部手册编号(RM0090 等)、目标文档本身不用数字编号标题。 -> 「变更记录小节内的历史指称」这条跳过规则为何还留着:2026-10-28 起新文档不再设该节(见 §5.1),它现在只为**保留变更记录的存量 5 篇**与 `scripts/archive/` 下的归档文档服务——历史行里的编号是「当时那个编号」,既不回改也不校验。 +> 「变更记录小节内的历史指称」这条跳过规则为何还留着:2026-10-28 起新文档不再设该节(见 §5.1),它现在只为**保留变更记录的 3 篇真台账**与 `scripts/archive/` 下的归档文档服务——历史行里的编号是「当时那个编号」,既不回改也不校验。 > ⚠️ 状态词按**就近**(±40 字符)判定,不是整行——否则文件名本身含「不存在」「删除」等字样时会误豁免同行的其它真实引用(2026-09-25 实测修正)。 @@ -318,7 +318,10 @@ | **一级标题** | 文档首行为 `# 标题`,标题即主题,不写"文档""说明"等冗余后缀 | | **开篇引言** | 可选 `>` 引用块,写明**适用对象 / 阅读前提 / 相关文档** | | **章节编号** | 多章节文档用 `## 1.` `### 1.1` 编号,便于交叉引用(`§3.2`) | -| **变更记录** | **2026-10-28 裁定:新文档不再设「变更记录」节**——文档只写现状、改了什么是正文说了算,改动过程查 `git log`。**例外**(整表保留、被别处当台账或唯一证据出处引用的 5 篇):`docs/子系统设计/看门狗设计说明.md`、`docs/子系统设计/配置文件设计说明.md`、`docs/内存管理/RAM预算台账.md`、`docs/UI设计/内容页架构.md`、`docs/环境与部署/本地工具路径环境变量方案.md` | +| **变更记录** | **一律不设该节**(用户裁定 2026-10-28 起新文档、2026-10-29 起老文档也清完;标题变体 `修订记录` / 带编号的 `## 6. 变更记录` / `## 九、变更记录` 同规则)——文档只写现状、改了什么是正文说了算,改动过程查 `git log`。**例外只有 5 篇**(整表保留:表里是真台账、且被别处当唯一证据出处引用):`docs/子系统设计/看门狗设计说明.md`、`docs/子系统设计/配置文件设计说明.md`、`docs/内存管理/RAM预算台账.md`、`docs/UI设计/内容页架构.md`、`docs/环境与部署/本地工具路径环境变量方案.md`;`scripts/archive/` 下的归档文档整片都是历史,不受本条约束 | + +> 数"还剩几篇留着这节"的命令(**别只 grep 裸标题**——带编号的 `## 6. 变更记录`、中文序号的 `## 九、变更记录`、`## 修订记录` 三种写法都要算): +> `grep -rn "^## " docs scripts` 再看命中的标题;2026-10-29 实测 **5 篇**真台账(上表那五篇)+ `scripts/archive/` 归档一篇。 ### 5.2 新增文档检查清单 @@ -373,9 +376,3 @@ | 提交 diff 出现整文件行尾变化 | 脚本改写时强制了 LF/CRLF | 改写须按原换行符回写(见 §2) | | 图片不显示 | 文档迁入子目录后未改 `../images/` | 补 `../`(见 §3.2) | | 引用"计划中"文件被拦 | 没加 `[计划] ` 前缀 | 加前缀 | - ---- - -## 变更记录 - -> (本文件不再记流水账:改了什么是正文说了算,改动过程查 git log。) diff --git a/docs/测试说明/自动化测试流程.md b/docs/测试说明/自动化测试流程.md index 980e7360..9daca744 100644 --- a/docs/测试说明/自动化测试流程.md +++ b/docs/测试说明/自动化测试流程.md @@ -136,7 +136,3 @@ `--forbidden "FAIL;HardFault"`(`[FAIL]` 仍会拦截真实断言失败); 例:GD5F 只读 `--required "=== GD5F2GQ5UE Tests: 16/16 PASSED"`,破坏性 `68/68`。 - **长时间采集**:daemon 全量持续写入 `state/serial.log`(20MB 上限自动截头保尾 10MB),直接读文件即可;或用 `log --grep <标记>` 过滤(2026-10-10 起先过滤后截尾,不必再配 `--tail 0`)。 - -## 变更记录 - -> (本文件不再记流水账:改了什么是正文说了算,改动过程查 git log。) diff --git a/docs/测试说明/配置文件单元测试说明.md b/docs/测试说明/配置文件单元测试说明.md index d41659bc..c4652d7b 100644 --- a/docs/测试说明/配置文件单元测试说明.md +++ b/docs/测试说明/配置文件单元测试说明.md @@ -277,9 +277,3 @@ if (s_encode(p_key, ..., v, sizeof(v)) >= 0) { (现有板端套件只测了"文件缺失 → 重建"路径,是本缺陷长期漏网的直接原因)。 - 检查是否存在同类"返回长度却按 `== 0` 判成功"的调用点(本次已全库核对 `s_encode` 的 4 处调用点,除本缺陷外其余正确)。 - ---- - -## 6. 变更记录 - -> (本文件不再记流水账:改了什么是正文说了算,改动过程查 git log。) diff --git a/docs/环境与部署/项目部署环境要求.md b/docs/环境与部署/项目部署环境要求.md index df00b2ee..affbffa5 100644 --- a/docs/环境与部署/项目部署环境要求.md +++ b/docs/环境与部署/项目部署环境要求.md @@ -650,9 +650,3 @@ $env:DTU_KEIL_UV4 = 'D:\NotExist\UV4.exe' # 仅当前会话,不动用户级 | 引用关系图(文件引用) | `docs/架构设计/引用关系图说明.md`(生成工具 `tools/refgraph`;渲染依赖 Graphviz,§2.8) | | 各模块测试指南 | `docs/测试说明/CH395F_Test_Guide.md`、`GD5F2GQ5UE_Test_Guide.md`、`SD2506_Test_Guide.md`、`FTP_Test_Guide.md` | | 已知硬件陷阱 | `docs/问题记录/CH395F_Trap_Records.md` 等 | - ---- - -## 变更记录 - -> (本文件不再记流水账:改了什么是正文说了算,改动过程查 git log。) diff --git a/docs/编号规范.md b/docs/编号规范.md index 60791783..7e78a941 100644 --- a/docs/编号规范.md +++ b/docs/编号规范.md @@ -225,9 +225,3 @@ scripts/archive/id_gate/check_ids.py --top 0 # 全库存量普查( - 编号形态拿不准 → 本文 §1 三句自问 + §4.1 那张表;**没有任何机器会替你兜**。两道门禁的历史与退役理由 → `docs/门禁说明/id-check.md`、`docs/门禁说明/id-check-new.md`。 - 想让机器帮你扫一批改动,或要恢复机器面 → §8.5。 - ---- - -## 变更记录 - -> (本文件不再记流水账:改了什么是正文说了算,改动过程查 git log。) diff --git a/docs/网络/网络自愈架构设计.md b/docs/网络/网络自愈架构设计.md index b3aa50ba..75ec40a0 100644 --- a/docs/网络/网络自愈架构设计.md +++ b/docs/网络/网络自愈架构设计.md @@ -20,7 +20,7 @@ | `TRAP-NET-nn` | **陷阱记录**条目 | 同上 | | `N-nn` | `docs/任务规划/登记簿.md` 的**任务/发现编号**(⚠ 已销项的号不再可查,本文引用处即其最终事实描述) | 该登记簿 | | `D1`~`D7` | 本文**决策记录** | §10 | -| `自愈收敛阶段 N` | 2026-10 的分阶段实施标签,**只存在于历史文档与归档**(2026-10-28 清理流水后全仓仅剩 5 篇保留的变更记录)(代码注释已全部改为平实语言);勾选清单已归档(§12)——**勿用它叙述设计,也勿再写回注释** | §12 | +| `自愈收敛阶段 N` | 2026-10 的分阶段实施标签,**只存在于历史文档与归档**(2026-10-29 起全仓仅剩 5 篇保留的变更记录真台账)(代码注释已全部改为平实语言);勾选清单已归档(§12)——**勿用它叙述设计,也勿再写回注释** | §12 | | `TSL` / `TSC` / `TSD` | SPI 命令与数据之间的**时序间隔**要求 | `docs/芯片手册/CH395F.md` §7.4 | | `n30` | 压测脚本 `n30_repro.py`(:21 背靠背 STOR 30 轮),自愈回归的**主判据** | `test/host/` | | 沉降窗 / 隔离期 | 释放会话后、重建监听前的**最短等待**(GRACEFUL 100 ms / QUARANTINE 400 ms) | §4.3 | @@ -868,5 +868,5 @@ accept 走 `handle_connect_event`),且 `NET_RECON_STABLE_DEATH` 两边共用 `git log --oneline -- docs/网络/N34-自愈审计/`; - 监听生命周期(本文 §1.1~§1.4 / §4.9 / §9~§11 的来源,含 P0~P3 勾选清单与台架验收记录): `git log --oneline -- docs/子系统设计/网络Socket监听生命周期设计说明.md`; - - `自愈收敛阶段 N` 只出现在历史文档与归档里(变更记录节已清理,仅 5 篇保留)(代码注释已清理为平实语言)—— + - `自愈收敛阶段 N` 只出现在历史文档与归档里(变更记录节已清理,全仓只剩 5 篇真台账)(代码注释已清理为平实语言)—— **勿再引入**,设计含义一律以本文 §2~§4 为准。 diff --git a/docs/门禁说明/bsp-err.md b/docs/门禁说明/bsp-err.md index d65eed8b..170e0fa5 100644 --- a/docs/门禁说明/bsp-err.md +++ b/docs/门禁说明/bsp-err.md @@ -140,7 +140,3 @@ BSP 约定:命中 38(在册 38 / 新增 0),基线共 38 - 加新的 must-use HAL 族:改 `HAL_STATUS_RE`,同时补一条正反例自测。 - 改标签名:脚本与基线一次改完(§6 第 9 条),随后 `--emit-baseline` 复核数量不变。 - 门禁自己的注释里不要写只有本次会话懂的代号;要引用规范就写`文件.md` + 紧邻编号。 - -## 9. 变更记录 - -> (本文件不再记流水账:改了什么是正文说了算,改动过程查 git log。) diff --git a/docs/门禁说明/doc-links.md b/docs/门禁说明/doc-links.md index a77fd8ca..f40f8933 100644 --- a/docs/门禁说明/doc-links.md +++ b/docs/门禁说明/doc-links.md @@ -85,7 +85,3 @@ D 类刻意只校验**紧邻写法**:文档名与 `§` 之间只允许空白 - 放宽匹配(例如允许裸名引用)前先想清楚:**误报压到 0 是靠"只认紧邻写法"换来的**。 - 新增一类豁免,必须同时新增一类误报;把豁免写在 `docs/文档规范.md` 与本节里,别只写进代码。 - -## 9. 变更记录 - -> (本文件不再记流水账:改了什么是正文说了算,改动过程查 git log。) diff --git a/docs/门禁说明/font-charset.md b/docs/门禁说明/font-charset.md index 815dfc61..f758d245 100644 --- a/docs/门禁说明/font-charset.md +++ b/docs/门禁说明/font-charset.md @@ -98,7 +98,3 @@ A 通过 + B 通过 ⟹ 字体覆盖"源码在用"与"刻意保留"的每一个 - 放宽"必需集"来源(比如不再扫某个目录)前先确认该目录的字最终确实会进生成链; - 与 `ui-golden` 是互补关系:本门管"有没有字形",golden 管"像素对不对"。 - -## 9. 变更记录 - -> (本文件不再记流水账:改了什么是正文说了算,改动过程查 git log。) diff --git a/docs/门禁说明/id-check.md b/docs/门禁说明/id-check.md index c45033f6..87792820 100644 --- a/docs/门禁说明/id-check.md +++ b/docs/门禁说明/id-check.md @@ -238,8 +238,3 @@ 3. 更省事的正解是**写注释与文档时一开始就别用裸形**:改用语义短语,或写全号 + 紧邻属主文档名。同日还有一 次反向教训:为「省余量」把在册族的号改成散文,那才是真把护栏当敌人(见 §7.7)。 4. 附带收益常比余量值钱:这次顺带消掉一个**同形两义**的撞号源(同一裸形既像自造状态号又撞已登记族)。 - - -## 9. 变更记录 - -> (本文件不再记流水账:改了什么是正文说了算,改动过程查 git log。) diff --git a/docs/门禁说明/layer-deps.md b/docs/门禁说明/layer-deps.md index bc854475..b6efcff4 100644 --- a/docs/门禁说明/layer-deps.md +++ b/docs/门禁说明/layer-deps.md @@ -94,7 +94,3 @@ - 往 `ALLOW` 加条目要写理由,并想清楚会不会变成"给上层开后门";能进基线就别进 `ALLOW`。 - 还掉一条债就跑 `--prune` 并删掉那一行;基线只减不增,加行等于放弃约束。 - -## 9. 变更记录 - -> (本文件不再记流水账:改了什么是正文说了算,改动过程查 git log。) diff --git a/docs/门禁说明/ui-golden.md b/docs/门禁说明/ui-golden.md index 9977e9c1..c3df7578 100644 --- a/docs/门禁说明/ui-golden.md +++ b/docs/门禁说明/ui-golden.md @@ -76,7 +76,3 @@ - 加新页面 / 新状态时,要同时扩参考图集合,否则新页面**完全没有回归**(不会报错,只是没覆盖); - 触发面 `files:` 每加一类能改像素的入口就补一行,漏一类 = 那一类改动静默不跑回归。 - -## 9. 变更记录 - -> (本文件不再记流水账:改了什么是正文说了算,改动过程查 git log。) From 20617a33f7c83b565a2a6788d22764908753d0e9 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 21:31:42 +0800 Subject: [PATCH 12/19] =?UTF-8?q?docs(=E7=BC=96=E5=8F=B7=E8=A7=84=E8=8C=83?= =?UTF-8?q?):=20=E7=BC=96=E5=8F=B7=E4=B8=80=E5=BE=8B=E5=81=9C=E5=8F=91?= =?UTF-8?q?=E2=80=94=E2=80=94=C2=A74.1=20=E4=BA=94=E6=97=8F=E4=B9=9F?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E5=8F=91=E6=96=B0=E5=8F=B7=EF=BC=8C=E5=8F=AA?= =?UTF-8?q?=E8=AF=BB=E5=AD=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户裁定(2026-10-29):「啥号都不发」= 任何族都不再新增编号, 含 §4.1 那五个原本"有脚本按字面量匹配"的存量族;既有号只读引用, 族里新增内容一律写语义行名。 规范本体: - §1 由「先问三句」改成「默认答案:不发号」,三问表换成场景对照表; - §4.1 由「允许用号的族」改成「存量族」,第四列改「既有号被谁读 / 新写怎么办」,逐行写明新写怎么做(TRAP 写语义行名、FR 用 §5 行名、 CODE-RULE / MENU-STATE 先问用户、P-n 只读存量页号); - 末框四条对照 + 唯一例外入口(确需机器按字面量匹配 → 先问用户); - 新增族 = 一律不许;批准后顺序:本文 §4.1 → 属主文档 → 才动 registry。 跨文件同步(12 份):门禁说明 id-check-new / id-check / host-unit-tests (把"校验 5/3 拦新号"这类现状叙述历史化)、两份 TRAP 记录的编号口径、 开机账本用例号说明、AGENTS 两处、id-family 技能与其 steps-alpha 模板、 归档 README 与两份归档脚本 docstring。 AGENTS 主题表那行原写「允许用号的 7 个族」,与规范的 5 行本就对不上, 旧计数一并消掉。 未做:pre-commit 的 default_stages 一行——用户对该项未置可否。 验证:py_compile 与 check_ids_new.py --self-test 均 rc=0;逐篇 check_doc_links.py --sections-strict rc=0(编号规范 70 处引用 / 68 可判定 / 无法判定 0);13 份改动文件表列数与字节体检相对基线零新增。 --- .../id-family-semantic-migration/SKILL.md | 3 + .../reference/steps-alpha-numeric-family.md | 4 +- AGENTS.md | 4 +- docs/测试说明/开机账本单元测试说明.md | 5 +- docs/编号规范.md | 107 +++++++++--------- docs/门禁说明/host-unit-tests.md | 4 +- docs/门禁说明/id-check-new.md | 4 +- docs/门禁说明/id-check.md | 4 +- docs/问题记录/ADC_Trap_Records.md | 2 +- docs/问题记录/CH395F_Trap_Records.md | 2 +- scripts/archive/id_gate/README.md | 6 +- scripts/archive/id_gate/check_ids.py | 2 +- scripts/archive/id_gate/check_ids_new.py | 2 +- 13 files changed, 79 insertions(+), 70 deletions(-) diff --git a/.dsh/skills/id-family-semantic-migration/SKILL.md b/.dsh/skills/id-family-semantic-migration/SKILL.md index 4237d6eb..95c5d12e 100644 --- a/.dsh/skills/id-family-semantic-migration/SKILL.md +++ b/.dsh/skills/id-family-semantic-migration/SKILL.md @@ -7,6 +7,9 @@ version: 1.1.0 # 编号族清退成语义标识 +**2026-10-29 用户裁定:全族一律停发**(含 `docs/编号规范.md` §4.1 那五个存量族)——本 playbook 的「停发」分支默认已成立, +现在唯一还需要决策的是**要不要清退存量**(全仓改语义 + 销项删行 + 逐号换算表入档)。 + **动手前先判这一族有没有「机器识别面」**——它决定计数口径的来源与落地去处,两条路的做法**正好相反**: | 族形态 | 计数口径来源 | 改名手段 | `scripts/archive/id_gate/id_aliases.json` | diff --git a/.dsh/skills/id-family-semantic-migration/reference/steps-alpha-numeric-family.md b/.dsh/skills/id-family-semantic-migration/reference/steps-alpha-numeric-family.md index 82ba2c49..eb986d56 100644 --- a/.dsh/skills/id-family-semantic-migration/reference/steps-alpha-numeric-family.md +++ b/.dsh/skills/id-family-semantic-migration/reference/steps-alpha-numeric-family.md @@ -2,7 +2,7 @@ > 本文由 id-family-semantic-migration 的 SKILL.md「Steps(字母+数字族:门禁看得见的那一路)」一节逐字外置而来(渐进展开:主文件只留索引与指针)。步骤号沿用原文,未重编。 -1. **确认范围与定调**:先按开头的表判识别面(无识别面 → 走下一节);再读 `docs/编号规范.md` §4.1(族总表,穷举)、§4.1.1(已停发栏),存量与批次见 `scripts/archive/id_gate/编号治理历史.md` §9,明确本族属主登记处(`owner_docs_glob`)与判据「有脚本按字面量匹配它」。清退 = 全仓改语义;停发 = 在规范表里写死不再新增引用(`id_registry.json` 的游标冻结只是裁定留痕,2026-10-29 起无机器读者)。 +1. **确认范围与定调**:先按开头的表判识别面(无识别面 → 走下一节);再读 `docs/编号规范.md` §4.1(族总表,穷举)、§4.1.1(已停发栏),存量与批次见 `scripts/archive/id_gate/编号治理历史.md` §9,明确本族属主登记处(`owner_docs_glob`);旧判据「有脚本按字面量匹配它」自 2026-10-29 起作废——全族一律停发。清退 = 全仓改语义;停发 = 在规范表里写死不再新增引用(`id_registry.json` 的游标冻结只是裁定留痕,2026-10-29 起无机器读者)。 2. **按族实测三口径(字面量 / 站点 / 文件),不用 grep 估数**。`Checker.refs` 是 **dict(字面量 → 文件集合)不是方法**,`ck.refs(fam)` 这种写法会 `TypeError`;站点数门禁不直接给,用族正则重扫它自己的语料: ```python import sys; sys.path.insert(0, "scripts") @@ -60,7 +60,7 @@ 报告「字面量 8787→8299→7828」「不可判定绝对数 1161→984→982→978(上限 1166)」「可判定比例 88.14%(基线 86.0%,容差 0.1pp)」。**绝对数与比例两根线都要报**:把可判定引用改成散文会稀释比值,所以另有不可判定绝对数上限。涨点归因见「净室归因」一节。 13. **回写 `docs/编号规范.md`**:§4.1 族表(停发/待清栏)、§4.1.1 已停发栏;存量表与批次结果表在 `scripts/archive/id_gate/编号治理历史.md` §9(只减不增、清点数字按最终实测同步);表格若保留动手前的清点数,**明确标注「不是当前存量,是动手前基线」**,否则就是拿旧数当现状。**用户裁定原文与理由直接写进对应节**(§4.1.1 停发行 + 该族的批次结论段落)——2026-10-28 起不再往变更记录写流水(`docs/文档规范.md` §5.1)。 14. **裁定「不做」也要写成可执行硬口径入档**(不是只划掉一行——「不做」最容易被读成「那还可以继续发号」): - - §4.1.1「已停发」栏新增该族一行:**游标冻结、现存号只减不增、新用例改用语义化测试函数名 + 文档表格行名**,并写清与「允许继续发号」栏为何不打脸; + - §4.1.1「已停发」栏新增该族一行:**游标冻结、现存号只减不增、新用例改用语义化测试函数名 + 文档表格行名**,并写清与 §4.1 存量族栏的关系(那五族 2026-10-29 起也一律停发); - 批次段落写明裁定原文与理由(归档 `scripts/archive/id_gate/编号治理历史.md` §9.1——§9 一族的事实源已在归档篇,规范正文只留口径)(该族是唯一真被脚本按字面量匹配的族:C 断言标签 + 测试函数名 + 文档用例表三处同一号),并给出**将来真要清退的前置条件**:追溯器(`check_test_traceability.py`)先同时认新旧两种形态、并行核对通过,**不许先改文档再改代码**; - 全文核「等你裁定 / 需你裁定」悬空表述残留数归 0; - 无机器识别面的族落地面不同(`docs/编号规范.md` §4.1.1 一行 + 归档 `编号治理历史.md` §9.3 换算表 + 叙述同步三处),见下一节第 7 步。 diff --git a/AGENTS.md b/AGENTS.md index 7473cbc5..9c605aff 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -154,7 +154,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/archive/id_gate/id_aliases.json` 标 `retired-to-phrase`、不得再指;**「数据可信」任务卡号(中文前缀「卡」+ 数字,14 个)已于 2026-10-27 随卡销项整族清退**,换算表 `scripts/archive/id_gate/编号治理历史.md` §9.3——这一族**刻意不进 `id_aliases.json`**:中文前缀不进 check_ids 语料,登记了机器也看不见,**没有机器识别面**,停发只靠规范与评审;**已随销项消失的号永久作废**、不得复用 → `docs/任务规划/README.md` §0 +- **登记簿类文档「完成即销项」**:`docs/任务规划/README.md` 这类待办登记簿,事项修复/关闭后**直接从表中删除该行**(不保留"已消化"行)——结论与证据应已沉淀进对应模块文档/问题记录。销项时必须:① 先核对目标文档确已含完整记录 ② 同步清理其他文档对该编号的引用 ③ 改完跑 `check_doc_links.py --all` + pre-commit。**新条目标识(用户裁定 2026-10-14;2026-10-29 收口为一律不再发号)**:`#` 列填**语义短语**(如 `沉降窗 800ms 上限只留痕`),**不再新增字母+数字编号**——"编号不回收"只写规则却不留痕迹就不可执行(销项删行后仓库里毫无痕迹,2026-10-14 实测就发生过「销项号被重新取走」的撞号事故),语义标识自带唯一性;**存量编号(登记簿 N-x / S-x / T-x 与专项 A-x)已于 2026-10-26 全部转成语义短语**,旧号进 `scripts/archive/id_gate/id_aliases.json` 标 `retired-to-phrase`、不得再指;**「数据可信」任务卡号(中文前缀「卡」+ 数字,14 个)已于 2026-10-27 随卡销项整族清退**,换算表 `scripts/archive/id_gate/编号治理历史.md` §9.3——这一族**刻意不进 `id_aliases.json`**:中文前缀不进 check_ids 语料,登记了机器也看不见,**没有机器识别面**,停发只靠规范与评审;**已随销项消失的号永久作废**、不得复用 → `docs/任务规划/README.md` §0 - 图形走 **`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` `` / `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/archive/id_gate/id_aliases.json` 的 scoped 旧号→新号条目;`[示例]`/`[计划]` 行与变更记录表不改写;**绝不全局查找替换**——同一裸形字面量可能同时属多个族,全局替换等于造新撞号,实测 `S3` 三义) - 文档写作规范(编码/BOM/链接/表格/一致性钩子)→ `docs/文档规范.md`;全量索引与推荐阅读顺序 → `docs/README.md` @@ -177,7 +177,7 @@ | 存储栈(FatFs / dhara / lftpd / 两条 FatFs 陷阱) | `docs/架构设计/STM32F4-Base存储架构说明.md` | | RAM / 栈预算 / 容量台账 | `docs/内存管理/RAM使用分析与优化方案.md`、`内存内容分配策略.md` | | **每个提交门禁一篇详解**(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径) | `docs/门禁说明/README.md`(51 个钩子 → 29 篇索引,其中 2 篇是退役留档 + 1 指针) | -| **要不要给某个东西发编号**(默认:不发,写语义短语)/ 允许用号的 7 个族(`TC-*` 已停发——「停发」不等于「暂缓」,也不等于「可以清号」)/ 编号门禁现状(**两道都已退役**:全库八项校验 2026-10-28、只查新增行 2026-10-29;此后无机器面,只靠 §1 三句 + 评审) | `docs/编号规范.md`(§1 先问三句、§4.1 族总表〔穷举〕与『已停发』栏、§8 门禁现状与退役路线) | +| **还想给东西发编号?**(**2026-10-29 用户裁定:一律不发**,写语义短语;§4.1 五个存量族的既有号只读引用——「停发」不等于「暂缓」,也不等于「可以清号」)/ 编号门禁现状(**两道都已退役**:全库八项校验 2026-10-28、只查新增行 2026-10-29;此后无机器面,只靠 §1 与评审) | `docs/编号规范.md`(§1 默认答案、§4.1 存量族〔穷举〕与『已停发』栏、§8.5 手工普查与恢复机器面) | | 提交门禁通用机制(阶段 / stash 陷阱 / 触发面 / 增量拦截三段式) | `docs/代码规范/pre-commit使用说明.md` §2 | | 代码规范 V1.0 / cppcheck 口径 | `docs/代码规范/嵌入式C语言代码规范_V1.0.md`、`cppcheck使用说明.md` | | 自动化测试 / 实机测试 / 板级套件 | `docs/测试说明/自动化测试流程.md`、`实机测试实施指南.md`、`board-test-suite` skill | diff --git a/docs/测试说明/开机账本单元测试说明.md b/docs/测试说明/开机账本单元测试说明.md index f8f27876..1b224231 100644 --- a/docs/测试说明/开机账本单元测试说明.md +++ b/docs/测试说明/开机账本单元测试说明.md @@ -70,8 +70,9 @@ | `TC-BOOTSTAT-030c` | init 后连读 100 次,三类 I2C 调用计数**纹丝不动** | "任意上下文可读"不是口头承诺 | > ⚠ 这些号是**主机侧用例号**,不是板端用例号,也不是需求号;族口径沿用 -> `docs/编号规范.md` 的 TC 族(默认 `check_dangling`),所以**本文就是定义位**: -> 新增/删除用例必须同步改本表,否则 `id-check` 校验 3 会报悬空、校验 7 会报孤儿。 +> `docs/编号规范.md` 的 TC 族(既有号只读),所以**本文就是定义位**: +> 新增/删除用例必须同步改本表——`id-check` 已于 2026-10-28 退役,悬空与孤儿**不再有机器警报**,只靠评审; +> 新增用例本身不再配号(`docs/编号规范.md` §4.1 一律停发),用语义化测试函数名。 --- diff --git a/docs/编号规范.md b/docs/编号规范.md index 7e78a941..377ef308 100644 --- a/docs/编号规范.md +++ b/docs/编号规范.md @@ -1,17 +1,18 @@ # 编号规范:默认**不**用编号 -> **一句话:语义标识是唯一默认形态,编号是例外。** 例外只留给**有脚本按字面量匹配**的那几种 -> (§4.1 的「允许继续发号」一栏);同一张表里还有一栏是**已停发、存量待清**的族(`编号治理历史.md` §9.1 给清点与批次)。 -> 除此之外一律写语义短语(§3.3)。 +> **一句话:语义标识是唯一形态,编号一律停发。** 2026-10-29 用户裁定:**任何族都不再新增编号**, +> 连 §4.1 那五个「有脚本按字面量匹配」的存量族也一样——既有号只读引用,族里新增内容写语义行名(§3.3)。 +> §4.1.1 另列更早停发、存量待清的族(清点见 `编号治理历史.md` §9.1)。 > -> 这条不是洁癖,是用户 2026-10-27 的明确要求:**「不要用字母加数字的编号,简直太难以理解了,语义标识是最好的」**。 +> 这条不是洁癖,是用户 2026-10-27 的明确要求:**「不要用字母加数字的编号,简直太难以理解了,语义标识是最好的」**, +> 2026-10-29 又收一句:**「啥号都不发」**。 > 依据也很硬:规划 / 待办 / 审计 / 差距 / 阶段这些给人看的东西,历史上全发过号,也全都变成了没人认得的代号。 > 2026-10-26 起两批清理把编号字面量从 **10,956 压到 8,230 处**,2026-10-27 复测为 **7,828 处**(`编号治理历史.md` §8.1 有口径说明); > 而清掉的号里绝大多数从来没被任何脚本读过一次。沿革与教训留在 `编号治理历史.md` §8.3 与 git log,正文不复述。 > > **本文就是唯一事实源**:两道编号门禁已分别于 2026-10-28(全库八项校验)与 2026-10-29(只查新增行) > 退役,脚本、两张前缀白名单与族语料都在 `scripts/archive/id_gate/`,**不再进提交门禁**。 -> 形态判据只剩本文 §1 的三句自问 + §4.1 族总表 + 人工评审;留档 `docs/门禁说明/id-check-new.md` +> 形态判据只剩本文 §1 的默认答案(一律不发)+ §4.1 存量族表 + 人工评审;留档 `docs/门禁说明/id-check-new.md` > 与 `docs/门禁说明/id-check.md`,手工普查命令与恢复机器面的成本见 §8.5。归档脚本随时能重扫一批改动的形态(`--range A..B`)。 --- @@ -22,15 +23,19 @@ > 重排等于制造一批假链接)。要查历史判据、批次流水、「数据可信」任务卡号换算表,去那篇; > **改现在的做法只看本文**。 -## 1. 决定要不要发号之前,先问三句 +## 1. 默认答案:不发号 -| 问自己 | 答"否"的结论 | +**2026-10-29 用户裁定:新条目一律不配编号。** 这里原本是三句自问(「有脚本按字面量匹配就允许发」), +现在这条路也关掉了——判「能不能发号」不再看形态,答案恒为否。剩下的只是既有号怎么用: + +| 场景 | 怎么做 | |---|---| -| 有脚本 / CI / 回归测试要按这个字面量做匹配吗? | **不发号**,写语义短语(§3.3) | -| 现有文档或登记表里已经能指代这件事吗? | 引用它,别再发第二个号指同一件事 | -| 一年后别人还能在属主登记表查到它的定义吗? | 查不到就别发——"先记着,以后再说"是编号失控的唯一来源 | +| 想给新需求 / 新踩坑 / 新用例 / 新待办配个号 | 不发,写语义行名(§3.3);登记表那一行的行名就是标识 | +| 新东西确实要被脚本按字面量匹配(比如新的回归脚本键) | **先问用户**(Ask first)。批下来才算例外,顺序见 §4.1 末条 | +| 现有文档或登记表里已经能指代这件事 | 引用它,别再发第二个号指同一件事 | +| 碰到查不到定义的号 | 删号留事实,不是补一个定义(§10) | -三条硬规矩(对确实要发的号): +三条硬规矩(管既有号的引用与处置): - **号不承载语义**:严重度、优先级、模块名、轮次都不写进号里。事实一变,号就开始撒谎。 - **号不回收、不复用**:销项 = 删登记行 + 清掉所有引用 + 别名表标 `void`(§5.5)。历史上"销掉的号被 @@ -43,7 +48,7 @@ |---|---|---| | ① 最好 | **语义短语** | 规划 / 待办 / 缺陷 / 风险 / 决策 / 差距 / 文档内步骤与阶段——**默认形态** | | ② 其次 | **文档名 + 章节号** | 指某篇文档的某节(`docs/网络/网络自愈架构设计.md` §4.10,紧邻写法) | -| ③ 最后 | **编号** | 只有 §4.1「允许继续发号」那一栏,且必须能反查到定义 | +| ③ 最后 | **编号** | 只指 §4.1 五个存量族的**既有号**(只读),且必须能反查到定义;新条目不在此列 | ### 3.1 判定口诀 **机器读 → 发号;人读 → 写短语。** 拿不准就写短语:短语看不懂还能靠上下文猜,号看不懂只能去查表。 @@ -66,43 +71,42 @@ ## 4. 编号空间 -### 4.1 允许用号的族(**穷举**——这张表以外一律不许发号) +### 4.1 存量族(**穷举**——2026-10-29 起连这五个族也不再发新号) -**允许继续发号**——**恰好下面 5 行**,判据 = 有脚本按字面量匹配它(现状实测 2026-10-27 见 `编号治理历史.md` §9.1)。 -`TC-*` **不在本表**:它的发号已按用户裁定 2026-10-27 停掉(别把那句裁定读成「暂缓」——防误读的三条对照在 -本节末的框里,清点的来龙去脉在 `编号治理历史.md` §9.1 批次三)。 +下表**恰好 5 行**,是仓库里仅剩还带编号的族:既有号继续被脚本按字面量匹配、继续被引用,**但族里新增 +内容不再配号**(写语义行名)。「有脚本按字面量匹配它」这个旧判据自 2026-10-29 起作废——这五族的号是 +历史遗留的机器把手,不是发号许可。`TC-*` 早两天(2026-10-27)已按同一逻辑停发,清点与来龙去脉在 +`编号治理历史.md` §9.1 批次三。 -| 族 | 形态 | 属主登记处 | 用途 | +| 族 | 形态 | 属主登记处 | 既有号被谁读 / 新写怎么办 | |---|---|---|---| -| `TRAP-*` | `TRAP-<域>-` | `docs/问题记录/*_Trap_Records.md` | 踩坑证词的永久锚点,代码注释反查 | -| `FR-*` | 需求 | `docs/产品需求/产品需求设计文档.md` §5 | 需求↔测试追溯 | -| `CODE-RULE-` | 代码规范条款 | `scripts/check_coding_standard.py` | 门禁按条款号报错并统计存量 | -| `MENU-STATE-` | UI 状态机键 | `simulator/tests/check_menu_rows.py` | 像素回归脚本按它取状态行 | -| `P-` | 编号治理的设计原则 | `scripts/archive/id_gate/编号治理历史.md` §2(2026-10-28 随沿革归档,本文不再列条目) | **只读存量**:原则已归档,新写条目按 §3.3 写语义短语,不再配 P 号 | +| `TRAP-*` | `TRAP-<域>-` | `docs/问题记录/*_Trap_Records.md` | 代码注释按号反查踩坑证词;**新踩坑**在域文件里写语义行名,不再顺延号段 | +| `FR-*` | 需求 | `docs/产品需求/产品需求设计文档.md` §5 | 需求↔测试追溯的既有键;**新需求**用 §5 表格的行名(语义短语)指代,追溯按既有号跑存量 | +| `CODE-RULE-` | 代码规范条款 | `scripts/check_coding_standard.py` | 门禁按条款号报错并统计存量;**新条款先问用户**——脚本要按号统计,破例得由用户批 | +| `MENU-STATE-` | UI 状态机键 | `simulator/tests/check_menu_rows.py` | 像素回归脚本按它取状态行;**新状态同样先问用户** | +| `P-` | 编号治理的设计原则 | `scripts/archive/id_gate/编号治理历史.md` §2(2026-10-28 随沿革归档,本文不再列条目) | **只读存量**:原则已归档,新写条目按 §3.3 写语义短语 | -> ⚠ **「批次三不做了」只有一层意思:不去清这个族,同时把它的发号口关死。** 它**不是**「TC 族维持原状、 -> 新用例照旧配号」——维持原状的是**存量号**,关掉的是**发号口**。三条对照,防止往任何一边读过头: +> ⚠ **停发只有一层意思:不去清存量号,但发号口关死。2026-10-29 起这句话适用于全部族,不只 `TC-*`。** +> 维持原状的是**存量号**,关掉的是**发号口**。四条对照,防止往任何一边读过头: > -> - ❌「不做 = 维持原状 = 还能发」——**错**。新用例一律用**语义化测试函数名 + 文档用例表的行名**(§3.3), -> 不再配 TC 号;指认**既有**用例仍可引用现存号(旧号只读,不是许可新配)。 -> - ❌「停发 = 这族要废了,顺手清掉、把号腾出来复用」——**也错**。现存号是用例输出行名与追溯脚本 -> `scripts/check_test_traceability.py` 认的把手(该脚本 2026-10-29 实测**只手工跑**:不在任何钩子、 -> `scripts/ci_local.py` 与远端 CI 里),清它等于换机制(`编号治理历史.md` §9.1);号不回收、不复用是 §1 的硬规矩。 +> - ❌「§4.1 里的族没被点名停发,所以还能发」——**错**。五族一样不发,新条目写语义行名(§3.3); +> 指认**既有**需求 / 踩坑 / 用例仍可引用现存号(旧号只读,不是新配的许可)。 +> - ❌「停发 = 这族要废了,顺手清掉、把号腾出来复用」——**也错**。现存号是脚本与追溯机制认的把手 +> (`scripts/check_test_traceability.py` 2026-10-29 实测**只手工跑**:不在任何钩子、`scripts/ci_local.py` +> 与远端 CI 里),清它等于换机制(`编号治理历史.md` §9.1);号不回收、不复用是 §1 的硬规矩。 > - ❌「`id_registry.json` 里 TC 族带着游标,所以那边允许我发」——**不成立**。37 个 TC 类族 2026-10-27 全部 > 登记 `frozen: true`,34 个 `TC-HOST-*` 子族的游标钉在当日各套件存量最高号(5 个低于存量的抬到水位: > `TC-PROTELEM-013→015`、`TC-SYSLOG-108→137`、`TC-ADCHEALTH-011→023`、`TC-ADCCAL-017→023`、 > `TC-I104SRV-041→053`;**游标必须是字符串形态**,整型比较会静默不生效)。那是那次裁定的**登记留痕**: -> 两道编号门禁退役后没有任何东西再读这份登记,写一个比游标更高的 TC 号不会再有机器反应,拦它的只有 -> 本节与评审——**别把"不报错"读成"允许发号"**。 -> - **已知空白面(当年登记时就写着,不是新发现)**:主族 `TC`(没有专属子族的模块码,如 `TC-ADCIN-` 段) -> 的 `cursor` 为 `null`——一个游标表达不了每个模块码的水位,写全族最高号反而放行低号。机器面在的时候它 -> 就不响,现在连那层壳都没有:**给这类模块码补号,即使在文档表格里一并补了定义行,一样违反本裁定**, -> 评审要专门盯这一面。要把死角钉死(按模块码克隆子族,或给族级加字面量上限)与「真要清退」同属追溯 -> 机制改造,按 `编号治理历史.md` §7.3 单独开卡,别夹在别的批里顺手做。 -> - **唯一的重启入口**:先由用户裁定 → 再回到本节把族放回上面那 5 行、并指出按它做字面量匹配的脚本 → -> 才谈得上发号。顺序倒过来就是 `编号治理历史.md` §1.2 那四类事故的由来。 +> 两道编号门禁退役后没有任何东西再读这份登记——**别把「不报错」读成「允许发号」**。 +> - **已知空白面(当年登记时就写着)**:主族 `TC`(没有专属子族的模块码,如 `TC-ADCIN-` 段)的 `cursor` +> 为 `null`——一个游标表达不了每个模块码的水位,写全族最高号反而放行低号。机器面在的时候它就不响, +> 现在连壳都没有:**给这类模块码补号,即使在文档表格里一并补了定义行,一样违反裁定**,评审专门盯。 +> 要钉死它(按模块码克隆子族)与「真要清退」同属追溯机制改造,按 `编号治理历史.md` §7.3 单独开卡。 +> - **唯一的例外入口**:先由用户裁定 → 再回到 §4.1 把该族写清「哪个脚本按字面量匹配它」 → 才谈得上 +> 发号。顺序倒过来就是 `编号治理历史.md` §1.2 那四类事故的由来。 -**已停发、存量待清**(一律**不得再新增引用**;下表除 `TC-*` 外,`id_registry.json` 里剩的登记只是归档普查的 +**已停发、存量待清**(§4.1 那五族 2026-10-29 起同样停发,区别只在存量保留、不去清;本表这些族连存量引用也要清掉,一律**不得再新增引用**;下表除 `TC-*` 外,`id_registry.json` 里剩的登记只是归档普查的 语料残留,**不再当"登记处"读**——那份登记自 2026-10-28 起没有机器读者。清点见 `编号治理历史.md` §9.1。 `TC-*` 是这一栏的**特例**:登记与现存号都要留着,现存号仍被追溯脚本按字面量读,不是待清对象 (见 `编号治理历史.md` §9.1 批次三): @@ -120,8 +124,9 @@ | `T-TEST` | 台架阶段 **裸形**(字母 T 紧跟一位数字) | 活引用 0(批次一清零;余下是本文自述与技能文档的形态举例) | 裸形最危险(同字母多义),且改「台架阶段一」即可 | | `F-*` | 网络设计条目 `F--` | 动手前 38 处 / 12 个 → **2026-10-27 批次一收口**:正文 9 处(含一个从来没有定义行的 DRV 段孤儿号,删号)+ 主机侧单测注释 1 处改「条目名」,仅属主文档 `编号治理历史.md` §7 历史对照表留 12 条编号 | 代码注释按号回指设计条目,但没有任何脚本匹配它 | -- **新增族的唯一正当条件**:有脚本要按字面量匹配它。顺序固定:先改 `id_registry.json`(含属主登记处) - → 再写属主文档 → 最后才允许在别处引用。反过来做必然造出悬空号。 +- **新增族:2026-10-29 起一律不许**,先问用户。真批下来,顺序固定:先改本文 §4.1(写清哪个脚本按字面量 + 匹配它)→ 再写属主文档 → 届时有机器面才动 `scripts/archive/id_gate/id_registry.json` → 最后才允许在别处 + 引用。反过来做必然造出悬空号。 - 模块码是既定缩写,事实源 = registry 的族 pattern 与属主文档标题行,**别自创**(`编号治理历史.md` §6)。 ### 4.1.1 已停发的族(别再指,更别再"复用"这些号) @@ -158,13 +163,13 @@ ## 5. 分配、引用、销项 -### 5.1 新增编号 -先过 §1 三问。确实要发号:① 族必须落在 §4.1 那 5 行之内(**表外一律不发**)② 在属主登记处写定义行 -③ 才允许在别处引用。**没有属主登记处的号不许存在**——这句原先由八项校验的"悬空引用"实现,该校验 -2026-10-28 退役,现在它是纸面规矩,只有评审在执行。 +### 5.1 新增编号:默认答案是没有 + +一律不发(§1、§4.1)。新条目写语义短语(§3.3),登记处的行名就是标识;引用既有号必须能反查到定义、 +旁边必须带人话(§1 三条硬规矩)。**唯一入口是用户裁定**:某样新东西确实要被脚本按字面量匹配时先问, +批下来再按 §4.1 末条的顺序落文档。 `scripts/archive/id_gate/id_registry.json` 的族与游标登记**只在恢复机器面时才动**(§8.5):没有脚本读它, -写进去也拦不住任何人——**别把"registry 里查得到"当成发号许可**(`TC-*` 正是这一类:停发与登记并存, -许可只来自 §4.1 那张表)。 +写进去也拦不住任何人——**「registry 里查得到」从来不是发号许可**,许可只来自用户裁定。 ### 5.3 代码注释 **优先写短语**(`FTP 鉴权`、`监听生命周期模块化`),别写字母号;确需引用活号时必须带属主文档名。 @@ -191,7 +196,7 @@ **现行没有编号门禁**:2026-10-29 用户裁定把最后那道只查新增行的 `id-check-new` 也摘了(钩子口径 52 → 51,脚本与白名单移入 `scripts/archive/id_gate/check_ids_new.py`,留档 `docs/门禁说明/id-check-new.md`, -理由与手工跑法都在那篇)。判据回到本文 §1 三句自问、§4.1 族总表与人工评审。 +理由与手工跑法都在那篇)。判据回到本文 §1 的默认答案(一律不发)、§4.1 存量族表与人工评审。 全库八项校验(未登记前缀 / 形态不合规 / 悬空引用 / 重复定义 / 冻结族越游标 / 未紧邻文档名 / 孤儿告警 / 可判定比例 + 两根护栏)2026-10-28 退役:它查过什么、为什么退、当年清点数字 → `docs/门禁说明/id-check.md` @@ -217,11 +222,11 @@ scripts/archive/id_gate/check_ids.py --top 0 # 全库存量普查( ## 10. 速查 - 看到一个不认识的号 → 先查 `id_aliases.json` 与属主登记表;查不到就**删号留事实**,不要补定义。 -- 想给待办 / 缺陷 / 差距 / 风险发号 → 不许,写短语(§3.3)。 +- 想给任何新条目发号(含需求 `FR-*`、踩坑 `TRAP-*`、用例 `TC-*`、待办与风险)→ 不许,写语义行名(§3.3 / §4.1)。 - 想知道「还有多少号可清」→ 看 `编号治理历史.md` §9 的存量表与三批计划。 - 想给文档内步骤或测试阶段发号 → 不许,写「步骤 3」「阶段 7」(§3.3)。 -- 想新增前缀族 → 先回答"哪个脚本要按字面量匹配它"(§4.1);答不出就别建。 +- 想新增前缀族 → 先问用户(Ask first);「有脚本要按字面量匹配它」这个旧判据自 2026-10-29 起不再构成许可(§1)。 - 要改号 → `scripts/rename_ids.py` + `编号治理历史.md` §7.3 五步。 -- 编号形态拿不准 → 本文 §1 三句自问 + §4.1 那张表;**没有任何机器会替你兜**。两道门禁的历史与退役理由 → +- 编号形态拿不准 → 本文 §1 的默认答案 + §4.1 那张表;**没有任何机器会替你兜**。两道门禁的历史与退役理由 → `docs/门禁说明/id-check.md`、`docs/门禁说明/id-check-new.md`。 - 想让机器帮你扫一批改动,或要恢复机器面 → §8.5。 diff --git a/docs/门禁说明/host-unit-tests.md b/docs/门禁说明/host-unit-tests.md index 4c6c0665..622f45ae 100644 --- a/docs/门禁说明/host-unit-tests.md +++ b/docs/门禁说明/host-unit-tests.md @@ -131,8 +131,8 @@ - 用例号段:`TC-HOST-<模块码>` 以 `test/host/<目录>/*` 为属主登记处(`docs/编号规范.md` §4.1); **该号段 2026-10-27 起停发新号**(用户裁定「批次三,不坐」):新增用例**不配号**,写语义化测试函数名, 现存号只读、不清、不复用。追溯器只双向比对已有集合,**不会**因为新用例没号而报错——别把它当"要带号"。 - **机器口径同日已落地**:`scripts/archive/id_gate/id_registry.json` 37 个 TC 类族全部 `frozen: true`,34 个子族游标冻结在 2026-10-27 存量最高号, - 在号段上再加新号 = `id-check` 校验 5 错误级;无专属子族的模块码落在主族 `TC`(游标仍为空),那一面由校验 3 + 冻结水位 + 评审兜住 + **当时的机器口径**(2026-10-27 落地,如今只剩留痕):`scripts/archive/id_gate/id_registry.json` 37 个 TC 类族登记 `frozen: true`、34 个子族游标冻结在当日存量最高号, + 当年在号段上再加新号 = `id-check` 校验 5 错误级;无专属子族的模块码落在主族 `TC`(游标仍为空)。**两道编号门禁已退役(2026-10-28 / 29),这些校验都不再跑**,那一面如今只由规范与评审兜住 → `docs/编号规范.md` §4.1 末框与 `scripts/archive/id_gate/编号治理历史.md` §9.1 末段。 ## 8. 改这套门禁本身(新增一个模块的宿主单测) diff --git a/docs/门禁说明/id-check-new.md b/docs/门禁说明/id-check-new.md index 0c77fb12..da984c48 100644 --- a/docs/门禁说明/id-check-new.md +++ b/docs/门禁说明/id-check-new.md @@ -3,7 +3,7 @@ > ⚠ **本文是退役留档,不要照着它去改门禁。** > 2026-10-29 用户裁定:这道门禁整体去掉——钩子从 `.pre-commit-config.yaml` 摘除(YAML 解析口径 > 52 → 51),脚本移入 `scripts/archive/id_gate/check_ids_new.py`(仍可手工跑,`--self-test` 保留)。 -> **此后编号形态没有任何机器面**:判据只剩 `docs/编号规范.md` §1 的三句自问、§4.1 允许用号的族 +> **此后编号形态没有任何机器面**:判据只剩 `docs/编号规范.md` §1 的默认答案(一律停发)、§4.1 存量族 > (穷举)与人工评审。前辈 `id-check`(全库八项校验)已于 2026-10-28 退役 → `docs/门禁说明/id-check.md`; > 两步合起来的路线 → `docs/编号规范.md` §8.5。 > 下面 §1~§6 是退役前的原样描述,只在手工复查「这批改动 / 这段历史里有没有野生编号」时用得上。 @@ -67,7 +67,7 @@ ## 5. 此后靠什么守着 -- `docs/编号规范.md` §1「先问三句」+ §4.1 允许用号的族(穷举)+ §3.3 禁发范围——**新增族不再需要 +- `docs/编号规范.md` §1「默认答案:不发号」+ §4.1 存量族(穷举)+ §3.3 禁发范围——**新增族不再需要 登记机器白名单**,那个"唯一口子"随门禁一起消失;答不出"哪个脚本要按字面量匹配它"就别建族。 - 评审与销项纪律:`docs/任务规划/README.md` §0(已随销项消失的号永久作废、不得复用)。 - ⚠ 要认下丢了什么:**"销项删行后号被重新取走"这类事故当初正是靠机械牙发现的**(2026-10-14 撞号 diff --git a/docs/门禁说明/id-check.md b/docs/门禁说明/id-check.md index 87792820..8cbfc548 100644 --- a/docs/门禁说明/id-check.md +++ b/docs/门禁说明/id-check.md @@ -11,7 +11,7 @@ > 一句话:它管**"号"这个东西不许野生**——每个编号必须属于一个已登记的族、形态合法、 > 有定义处、有人引用、引用它的时候说清是哪篇文档的号。 -> 规范正文 = `docs/编号规范.md`(§1 先问三句、§4.1 允许用号的族〔穷举〕、§8 门禁与实测基线),本门禁是那八项的机械执行。 +> 规范正文 = `docs/编号规范.md`(§1 默认答案(一律停发)、§4.1 存量族〔穷举〕、§8 门禁与实测基线),本门禁是那八项的机械执行。 - 配置:`entry: python scripts/archive/id_gate/check_ids.py --strict`,`pass_filenames: false`, `files:` 覆盖 `.md/.c/.h/.py/.yaml/.json/.txt` 与配置文件本身。 @@ -207,7 +207,7 @@ 要指代就写全号并紧邻属主文档名(写法口径见 `docs/文档规范.md` §3.5,判它的是校验 6)。 3. ⚠ **反向错误更贵**:为了「少占余量」而把编号改写成语义短语/散文——那正是 §7.6 与这根护栏的动机要防的 稀释(比值会被清号动作稀释,绝对数不会)。2026-10-27 凌晨 Lead 给三条道下过这样一条错指令,已撤回; - 正确口径是:按 `docs/编号规范.md` §1 先问三句,属允许族就用号,**需要新母号时由 Lead 改 `id_registry.json` 游标**。 + 当时的口径是:按 `docs/编号规范.md` §1 先问三句,属允许族就用号(该判据 2026-10-29 作废,全族一律停发),**需要新母号时由 Lead 改 `id_registry.json` 游标**。 **每批自证没吃余量(可复制的做法,五项式)**: diff --git a/docs/问题记录/ADC_Trap_Records.md b/docs/问题记录/ADC_Trap_Records.md index a73f8702..49b5b495 100644 --- a/docs/问题记录/ADC_Trap_Records.md +++ b/docs/问题记录/ADC_Trap_Records.md @@ -4,7 +4,7 @@ ADC1 采样链路(TIM3 1 kHz 节拍 → ADC1 九通道扫描 → DMA2_Stream4 语义映射)的实证陷阱:现象、判读陷阱、定案根因、修复与回归判据。**动 ADC 采样、通道映射或 诊断代码之前先读本文。** -> 编号口径:按 `docs/编号规范.md` 采用 `TRAP-ADC-`,本域自 01 起连续。编号不复用、不重排。 +> 编号口径:本域既有条目按 `TRAP-ADC-` 自 01 起连续编号,**只读引用**;2026-10-29 起**不再新增 TRAP 号**(`docs/编号规范.md` §4.1 一律停发),新踩坑在本文写语义行名。编号不复用、不重排。 > 状态图例:✅ 已修复定案 · ⏳ 待修。 > 设计侧口径(采样节拍、寄存器配置、铁律)不在本文,见 > `docs/子系统设计/ADC采样节拍设计说明.md`;本文只记「踩过的坑」与「怎么判断踩到了」。 diff --git a/docs/问题记录/CH395F_Trap_Records.md b/docs/问题记录/CH395F_Trap_Records.md index 6e54e6e6..31b43fcd 100644 --- a/docs/问题记录/CH395F_Trap_Records.md +++ b/docs/问题记录/CH395F_Trap_Records.md @@ -2,7 +2,7 @@ CH395F 以太网驱动与 NET 层的实证陷阱:现象、定案根因、修复、回归判据。改驱动/NET 层前先读本文。 -> 编号口径:按 `docs/编号规范.md` 采用 `TRAP-NET-`,连续 01~21。编号沿革(2026-10-13 全库重排): +> 编号口径:本域既有条目 `TRAP-NET-` 连续 01~21,**只读引用**;2026-10-29 起**不再新增 TRAP 号**(`docs/编号规范.md` §4.1 一律停发),新踩坑在本文写语义行名。编号沿革(2026-10-13 全库重排): > 旧 01~11 不变;旧 14→12、15→13、16→14、17→12(并入)、18→15、20→16、22→17、23→18、24→19、 > 25→20、26→21;旧 12/13/19/21 已移出/并入/删除,无条目。旧形 `Trap NN` 为违规裸形已全库迁移; > 旧号只存在于历史提交与旧日志。编号不复用、不重排。 diff --git a/scripts/archive/id_gate/README.md b/scripts/archive/id_gate/README.md index ba66a74b..6e5217ad 100644 --- a/scripts/archive/id_gate/README.md +++ b/scripts/archive/id_gate/README.md @@ -20,10 +20,10 @@ ## 现在用什么 - **没有现行门禁**:接班的 `id-check-new`(只查新增行、零语料)也在 2026-10-29 被用户裁定退役, - 脚本进本目录。编号形态此后无机器面,判据 = `docs/编号规范.md` §1 三句自问 + §4.1 族总表 + 评审; + 脚本进本目录。编号形态此后无机器面,判据 = `docs/编号规范.md` §1 的默认答案(2026-10-29 起一律停发)+ §4.1 存量族表 + 评审; 留档 `docs/门禁说明/id-check-new.md`(拦下过什么、为什么退、手工怎么再跑)。 -- **路线与验收条件**:`docs/编号规范.md` §8.5(①②③④ 四步的来龙去脉)。 -- **规范正文**(仍然有效,只是不再全库机械校验):`docs/编号规范.md` §1 先问三句、§4.1 族总表。 +- **手工普查与恢复机器面**:`docs/编号规范.md` §8.5(退役流水在 `编号治理历史.md`)。 +- **规范正文**(仍然有效,只是不再全库机械校验):`docs/编号规范.md` §1 默认答案、§4.1 存量族表。 ## 怎么手工跑一次 diff --git a/scripts/archive/id_gate/check_ids.py b/scripts/archive/id_gate/check_ids.py index fa3380e3..3640c406 100644 --- a/scripts/archive/id_gate/check_ids.py +++ b/scripts/archive/id_gate/check_ids.py @@ -9,7 +9,7 @@ check_ids.py — 编号门禁(docs/编号规范.md §8 的 8 项校验)— (钩子 id-check-new:只查新增行、零语料)已于 2026-10-29 一并退役,也进了本目录—— **编号形态此后没有机器面**。本脚本保留下来只做一件事: 想复查「存量编号现状」时手工跑一遍出报告。 - 路线(①②③④ 四步)→ docs/编号规范.md §8.5;两道门禁的留档 → docs/门禁说明/id-check.md + 手工普查与恢复机器面 → docs/编号规范.md §8.5;两道门禁的留档 → docs/门禁说明/id-check.md 与 docs/门禁说明/id-check-new.md。 下面这份 docstring 是退役前的原样,未改,作为历史口径保留。 diff --git a/scripts/archive/id_gate/check_ids_new.py b/scripts/archive/id_gate/check_ids_new.py index ef6fce66..db8164bd 100644 --- a/scripts/archive/id_gate/check_ids_new.py +++ b/scripts/archive/id_gate/check_ids_new.py @@ -3,7 +3,7 @@ """check_ids_new.py — 编号门禁·新增行(**已退役 2026-10-29,用户裁定**;本脚本降为手工报告工具) 退役口径:钩子已从 .pre-commit-config.yaml 摘除(YAML 解析条目 52 → 51),编号形态此后 -**没有机器面**——靠 docs/编号规范.md §1 的三句自问 + 评审。留档说明 → docs/门禁说明/id-check-new.md。 +**没有机器面**——靠 docs/编号规范.md §1 的默认答案(一律停发)+ 评审。留档说明 → docs/门禁说明/id-check-new.md。 本脚本与 check_ids.py(2026-10-28 退役的全库八项校验)同处一档:手工复查「这批改动 / 这段历史 里有没有野生编号」时才跑,跑不跑都不影响提交。 From 058e7c061cc62f60472917a36db876dbf2e76500 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Fri, 30 Oct 2026 00:18:53 +0800 Subject: [PATCH 13/19] =?UTF-8?q?docs(=E6=95=B0=E6=8D=AE=E5=8F=AF=E4=BF=A1?= =?UTF-8?q?):=20=C2=A78=20=E9=A3=8E=E9=99=A9=E8=A1=A8=E6=94=B9=E6=8C=89?= =?UTF-8?q?=E8=A1=8C=E5=90=8D=E5=BC=95=E7=94=A8=EF=BC=8C=E4=BF=AE=E6=8E=89?= =?UTF-8?q?=E4=B8=A4=E5=A4=84=E6=8C=87=E9=94=99=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IEC104上行设计说明.md 与 iec104_pts.c 的 s_q_map 注释原写「§8 风险 表末行」,末行其实是「加压烧板」;含单测要求的是「削顶通道被当成可 测」行。两处都改成行名,行名完整落在一行内 - ADC前端注入验证流程.md 同源错引用改成「削顶负向用例被当成固件 bug」行 - 数据可信.md §8 表头加一条引用规矩:写行名,别写「末行 / 第 N 行」, 本表会插行 - doc-links 只校验章节编号存在性,看不见「末行」这类行内指向,也从不扫 .c/.h —— 这两处此前没有门能拦 @build 0 errors 0 warnings(只改注释,Program Size 未变) --- App/protocol/iec104/iec104_pts.c | 4 ++-- docs/任务规划/数据可信.md | 2 ++ docs/子系统设计/IEC104上行设计说明.md | 2 +- docs/测试说明/ADC前端注入验证流程.md | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/App/protocol/iec104/iec104_pts.c b/App/protocol/iec104/iec104_pts.c index 0f4f42af..c3e092e7 100644 --- a/App/protocol/iec104/iec104_pts.c +++ b/App/protocol/iec104/iec104_pts.c @@ -176,8 +176,8 @@ uint32_t iec104_pts_yc_ioa(uint8_t idx) { * 入口参数:q16 - 源侧品质(低字节与线上同值;高字节为 MEAS_QDS_CLIPPED 域内位) * 返 回 值:线上 QDS 字节 uint8_t * 函数说明:CLIPPED(硬件削顶,104 无对应位)映射为 IV——「削顶通道 104 恒 - * IV」口径(docs/任务规划/数据可信.md §8 风险表末行)改由快照品质 - * 驱动,不再靠点表绑死 -1;OV/NT/IV 透传,其余高位丢弃。 + * IV」口径(docs/任务规划/数据可信.md §8 风险表「削顶通道被当成可测」 + * 行)改由快照品质驱动,不再靠点表绑死 -1;OV/NT/IV 透传,其余高位丢弃。 */ static uint8_t s_q_map(uint16_t q16) { uint8_t w = (uint8_t)(q16 & (uint16_t)(IEC104_QDS_OV | IEC104_QDS_NT diff --git a/docs/任务规划/数据可信.md b/docs/任务规划/数据可信.md index 2d3499bf..55678c76 100644 --- a/docs/任务规划/数据可信.md +++ b/docs/任务规划/数据可信.md @@ -425,6 +425,8 @@ flowchart TD ## 8. 风险 +> 引用本表某一行时**写行名**(例:「削顶通道被当成可测」行),别写「末行 / 第 N 行」——本表会插行。 + | 风险 | 触发条件 | 缓解 | |---|---|---| | **削顶负向用例被当成固件 bug** | 做 D5 时 STM32 那五路必然钉下轨 → `rail: edge=…` 是 `[ERR]` 行,而 `@test` 的 forbidden 标记含 `[ERR]` → 整轮被判 FAIL | 判读规则写进流程文档:"做 D5 的这一轮 `[ERR] rail:` 属预期,只人工判读、不计入 `@test` 结论",或把这五路排在最后单独一轮并事先声明。同时别忘 `cl=0xE6` 是**来源派生**(§2.6),不是本轮新削出来的 | diff --git a/docs/子系统设计/IEC104上行设计说明.md b/docs/子系统设计/IEC104上行设计说明.md index 38866e9b..9132ac49 100644 --- a/docs/子系统设计/IEC104上行设计说明.md +++ b/docs/子系统设计/IEC104上行设计说明.md @@ -49,7 +49,7 @@ 折算层 2026-10-25 落地;二次值等两点标定置 VALID)。槽位值/品质整体透传,本层 **不再自行判新旧**(快照 `seq` 仅供 COV 去重)。 - 品质映射:快照 `MEAS_QDS_*` 低字节与线上 QDS 同值直传;**CLIPPED → IV**(「削顶通道 - 恒 IV」口径不变、改由品质驱动,docs/任务规划/数据可信.md §8 风险表末行的单测要求); + 恒 IV」口径不变、改由品质驱动,docs/任务规划/数据可信.md §8 风险表「削顶通道被当成可测」行的单测要求); 快照从未发布(开机 <200 ms)→ IV|NT;判定顺序 IV 优先于 NT。 - 死区(104 改读快照):从「越死区即报」改为**连续 2 个新测量点越限才确认**——板端测量点 = 快照换帧(100 ms 轮询 vs 200 ms 快照按 `seq` 去重,防同一帧把单点噪声重复计满 2 点; diff --git a/docs/测试说明/ADC前端注入验证流程.md b/docs/测试说明/ADC前端注入验证流程.md index 4ab45e93..8efda66d 100644 --- a/docs/测试说明/ADC前端注入验证流程.md +++ b/docs/测试说明/ADC前端注入验证流程.md @@ -587,7 +587,7 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, - D5 会**故意造出** `[ERR] rail: edge=… sticky=… (code-domain, 20-run+release)`,而 `@test` 的 forbidden 标记含 `[ERR]`(AGENTS.md「构建与验证」)→ **这一轮不要把 `@test` 的标记校验当结论**:改人工判读,并在归档文件名与 - 抬头里写明「预期 [ERR]:贴轨锁存负向用例」;§8.6 已把它排在最后单独一轮(`docs/任务规划/数据可信.md` §8 风险表第一行同一条)。 + 抬头里写明「预期 [ERR]:贴轨锁存负向用例」;§8.6 已把它排在最后单独一轮(`docs/任务规划/数据可信.md` §8 风险表「削顶负向用例被当成固件 bug」行同一条)。 - 除这一条 `[ERR] rail:` 之外,**任何 `[ERR]` 都是不通过**;四个 Fault 的崩溃日志也以 `[ERR]` 开头 → 出现 Fault 立即停线取现场(`docs/子系统设计/看门狗设计说明.md` §10),别再继续注。 - 每次开机 `rail:` 边沿只打一条、`SYSLOG` 也只留痕一次("adc code rail fault" 进 `selftest.log`)——这是设计不是漏报; From 98d46e626decb8d5a035cc6021cfbc60bd1a6217 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Fri, 30 Oct 2026 00:19:15 +0800 Subject: [PATCH 14/19] =?UTF-8?q?fix(=E5=86=85=E5=AD=98=E5=8F=B0=E8=B4=A6)?= =?UTF-8?q?:=20subsystem=5Fof=20=E8=A1=A5=E9=BD=90=2012=20=E4=B8=AA=20App?= =?UTF-8?q?=20=E5=AF=B9=E8=B1=A1=E5=89=8D=E7=BC=80=E5=B9=B6=E5=8A=A0?= =?UTF-8?q?=E6=9C=AA=E5=BD=92=E7=B1=BB=E5=85=9C=E5=BA=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按台账 §2「应用 = App/ 其余」自家口径重算归集。此前 wave_、iec104*、 soe_、meas、freq、run_par、protect_task、task_prof、boot_stats、 vol_idle_sync 都不在前缀表里,12 个对象 9,445 B 静默落进「其它」(参考 行、无配额),配额 8 KB 的「应用」行看不见录波与 SOE 支路。补齐后 应用 16,888 → 26,333 B、其它 15,849 → 6,404 B;区域水位与 92% 红线不变 (台账受管区已 --write 回填)。 兜底:App 下的 .c 编出的对象只要落进「其它」,--gate 就逐条 WARN 点名 (软提醒不拦)—— 下一个新增 App 目录不会再静默隐身。台账 §2 口径表、 应用条目、变更记录与 内存内容分配策略 §6 那条「本卡不动,已报 lead」 同步改口。 --- docs/内存管理/RAM预算台账.md | 18 +++++---- docs/内存管理/内存内容分配策略.md | 2 +- scripts/ram_baseline.py | 66 ++++++++++++++++++++++++++++--- 3 files changed, 72 insertions(+), 14 deletions(-) diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index a8494ebf..4b3b655b 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -26,8 +26,8 @@ | 存储 | 7,072 B | 12 KB | OK | | UI | 36,428 B | 16 KB | ⚠ 超 19.6 KB | | RTOS 堆 | 57,376 B | 56 KB | 堆=配额 | -| 应用 | 16,888 B | 8 KB | ⚠ 超 8.5 KB | -| 其它 | 15,849 B | — | 参考 | +| 应用 | 26,333 B | 8 KB | ⚠ 超 17.7 KB | +| 其它 | 6,404 B | — | 参考 | - **门禁**:pre-commit `mem-budget` 钩子(`scripts/ram_baseline.py --gate`)—— @@ -51,8 +51,8 @@ UI 16 KB / RTOS 堆 56 KB / 应用 8 KB**。归集口径(按 `.map` 对象名 | 存储 | `gd5f*` `nand_*` `ff*` `diskio*` `vol_lock*` + dhara 裸名(`map/journal/nand/fctl/era.o`) | `cfg_*`(算应用) | | UI | `lv_*` `ui_*`(含 `ui_net_sink`、`ui_task`) | — | | RTOS 堆 | `heap_4.o`(CCM)——配额 = `configTOTAL_HEAP_SIZE` 本身 | 内核对象(`tasks/queue/timers/...` 归"其它") | -| 应用 | `App/` 其余:采集 `adc_*`、保护 `prot_*`、相量 `phasor/sample_ring`、配置 `cfg_*`、日志 `sys_/dbg_/console_log`、对时 `time_sync*`、`sys_clock`、`task_watchdog`、`sha256`、`tpafe*`、`sd2506`、`default_task` 等 | — | -| 其它 | 内核对象、HAL、CMSIS、启动文件、C 库、链接器填充 | — | +| 应用 | `App/` 其余——**前缀清单以 `scripts/ram_baseline.py` 的 `subsystem_of()` 为准**:`adc_*` `tpafe*` `sd2506` `prot_*` `protect_task` `phasor` `sample_ring` `wave_*` `meas` `freq` `run_par` `iec104*` `soe_*` `cfg_*` `sys_*` `dbg_*` `console_log` `time_sync*` `sys_clock` `task_watchdog` `sha256` `default_task` `test_task` `task_prof` `boot_stats` `vol_idle_sync` | `App/task/ui_task*`(首匹配算 UI) | +| 其它 | 内核对象、HAL、CMSIS、启动文件、C 库、链接器填充——**不含 `App/` 任何对象**:漏登记会被 `--gate` 的兜底 WARN 逐条点名(2026-10-29 加) | — | **现状判读(2026-10-12 建档)**: @@ -62,11 +62,12 @@ UI 16 KB / RTOS 堆 56 KB / 应用 8 KB**。归集口径(按 `.map` 对象名 **这条 ⚠ 会长期挂着**,判读时按已知挂账处理,别每轮当新问题追;重开条件 → `RAM使用分析与优化方案.md` §4。 - **RTOS 堆 = 配额本身**:56 KB 即 `configTOTAL_HEAP_SIZE`(2026-10-12 B5 由 48 KB 扩容,见 §6; `heap_4.o` 另含 32 B 元数据,不计超);**改堆 = 改配额**,必须先过本文件 §6 登记。 -- **应用 ⚠ 小幅超**:8 KB 为报告规划值;2026-10-10 栈整改把 3 处 `FIL` 从栈搬进 BSS +- **应用 ⚠ 超配额**(现值见 §1 受管区):8 KB 为报告规划值;2026-10-10 栈整改把 3 处 `FIL` 从栈搬进 BSS (静态 +4,128 B,见 `docs/内存管理/RAM使用分析与优化方案.md` §4"2026-10-10 复测")—— - 属已评审的"以空间换栈安全",差额在此如实体现。2026-10-29 故障录波落盘道又把 `default_task.o` - 推高 1,204 B(该行现值 16,888 B);**录波环本体 5,300 B 不计入本行**——`subsystem_of()` - 没有 `wave_` 前缀,它落在「其它」,要读全貌看 `docs/内存管理/内存内容分配策略.md` §6 的 per-file 台账。 + 属已评审的"以空间换栈安全";2026-10-29 故障录波落盘道又把 `default_task.o` 推高 1,204 B。 + **同日修正归集口径**:`subsystem_of()` 此前没有 `wave_` `iec104*` `soe_` `meas` `freq` `run_par` + 等前缀,12 个 `App/` 对象 9,445 B 被静默算进「其它」(当时「应用」行只有 16,888 B,看着离配额不远); + 补齐后本行才真含录波环 `wave_ring.o`(约 5.3 KB)。per-file 全貌 → `docs/内存管理/内存内容分配策略.md` §6。 ## 3. CCM 分配台账(谁能用 CCM) @@ -99,3 +100,4 @@ UI 16 KB / RTOS 堆 56 KB / 应用 8 KB**。归集口径(按 `.map` 对象名 | 2026-10-12 | **RTOS 堆配额 48 KB → 56 KB**(`QUOTAS` 同步) | B5 protectTask 立项:iec104Task(2.5KB)+protectTask(2KB)+TCB 入堆后 48KB 池 min_ever 预估跌破 3,072 B 告警线 → `.ioc` 语义取数与有效性参数 `configTOTAL_HEAP_SIZE` 49152→57344(CCM 水位 75.3%→87.9%,仍 <92% 门禁;CCM 空闲降至 ≈8 KB)。连带影响见 §3 迁 CCM 后半链注记 | | 2026-10-12 | **任务栈 +1536 B(堆内用量,配额 56 KB 不变)**:defaultTask 1024→1280 words(+1024 B)、timeSyncTask 512→640 words(+512 B);产品任务栈合计 37,632→39,168 B | 登记簿 N-33 栈收口:B7 定值持久化把 defaultTask 静态链推到 3016 B(余 1080 < 30% 线)、timeSyncTask 链 1264 B(余 784 < ISR 底线 1024);B6 台架背书 min_ever=10000 → 抬后实测 `heap free=16400 / min_ever=8464`(与预算 −1536 完全吻合,>3072 告警线);`stack_report` 9 任务 0 FAIL + `@test` PASS,见 `RAM使用分析与优化方案.md` §1.4 | | 2026-10-29 | **故障录波数据面入册**:新增对象 `wave_ring.o` 5,300 B(ZI 5,256 + RW 44,落 SRAM1、CCM 增量 0);`default_task.o` 静态 2,736 → 3,940 B(+1,204 B,落盘道 `FIL` 528 + 分片缓冲 256 + 段头副本 136 + 段路径 17 + 打包上下文与 `DIR`/`FILINFO`);`RW_IRAM1` 81,536 → 89,936 B(62.2 % → 68.6 %,仍远低于 92 % 门禁);`RW_CCM` 三档恒 57,376 B | 决定人 lead。实测数字出自 lead 的权威构建(本工作树 `MDK-ARM/STM32F407-Demo/STM32F407-Demo.map`,0 错误 0 警告,`scripts/ram_baseline.py --update` 同批入 tsv);预估约 5.9 KB vs 实测数据面 +6,760 B 的差额构成与三档对照见 `docs/内存管理/内存内容分配策略.md` §6;**未抬栈**(`scripts/stack_report.py` 九任务全 PASS),判据出处 `docs/子系统设计/故障录波设计说明.md` §7 | +| 2026-10-29 | **归集口径修正(非配额改动)**:`subsystem_of()` 补 `wave_` `iec104*` `soe_*` `meas` `freq` `run_par` `protect_task` `task_prof` `boot_stats` `vol_idle_sync` 等前缀——12 个 `App/` 对象 9,445 B 此前静默落「其它」;「应用」16,888 → 26,333 B、「其它」15,849 → 6,404 B,区域水位与 92% 红线均未变。同脚本加兜底:`App/**/*.c` 的对象落进「其它」时 `--gate` 逐条 WARN(不拦) | 配额表看不见录波/SOE 支路;口径依据本文件 §2「应用 = `App/` 其余」 | diff --git a/docs/内存管理/内存内容分配策略.md b/docs/内存管理/内存内容分配策略.md index 70f88ad8..5dd2f9f9 100644 --- a/docs/内存管理/内存内容分配策略.md +++ b/docs/内存管理/内存内容分配策略.md @@ -138,7 +138,7 @@ LVGL 集合明细(均在 SRAM):LVGL 池 16,384B(`lv_mem_core_builtin.o` - **栈:9 任务全 PASS、未抬栈**(`scripts/stack_report.py` 静态最坏链)——adcTask 1,424 B(余 1,136 B / 44 %)、defaultTask 3,040 B 不变、protectTask 880 B、uiTask 1,336 B(余 15,048 B / 91 %)。R6 三处同步里 `AGENTS.md` 栈表这一处的结论是**核对过、数值未变**。 - **为什么比上一版低**:本表 2026-10-12 版记 95,816 B / 73.1 %,高于今天的 89,936 B。原因是此后 LVGL 内部池由 40 KB 缩到 16 KB(`docs/内存管理/RAM预算台账.md` §3「缩池 ✅ 2026-10-15」行;`scripts/ram_baseline.tsv` 2026-10-12 与 2026-10-24 两行的 `top_rwzi` 列可见 `lv_mem_core_builtin.o` 40960 → 16384)——**不是本波回收了什么**。 -- **子系统归集的坑(本卡实测,值得记一笔)**:`scripts/ram_baseline.py` 的 `subsystem_of()` 按对象名前缀首匹配,**表里没有 `wave_` 前缀** → 录波环 `wave_ring.o` 5,300 B 落进 **「其它」(15,853 B,参考值、无配额)**,配额 8 KB 的「应用」行看不见它;落盘道 `default_task.o`(+1,204 B)与 `adc_task.o` 才计入「应用」(现值 16,888 B,本波前该行已在配额外,成因见 `docs/内存管理/RAM预算台账.md` §2「应用 ⚠ 小幅超」条)。结论:**别拿子系统表当录波预算的判据**,要按本节 per-file 台账读。要让配额表真看见录波支路,得给 `subsystem_of()` 加 `wave_` 前缀——那是脚本改动,本卡不动,已报 lead。 +- **子系统归集的坑(本卡实测 → 2026-10-29 已修)**:`scripts/ram_baseline.py` 的 `subsystem_of()` 按对象名前缀首匹配,当时表里没有 `wave_` 前缀 → 录波环 `wave_ring.o` 5,300 B 落进 **「其它」(当时 15,853 B,参考值、无配额)**,配额 8 KB 的「应用」行看不见它;落盘道 `default_task.o`(+1,204 B)与 `adc_task.o` 才计入「应用」(当时现值 16,888 B,本波前该行已在配额外,成因见 `docs/内存管理/RAM预算台账.md` §2 的应用条目)。**同日后续**:补齐 12 个 `App/` 对象(9,445 B)的前缀,并加兜底——`App/**/*.c` 编出的对象只要落进「其它」,`python scripts/ram_baseline.py --gate` 就逐条 WARN 点名(软提醒不拦)。结论仍然有效:**子系统配额行不是录波预算的判据**,要按本节 per-file 台账读;差别只在「应用」行现在真含录波支路,两个口径不再互相看不见。 ## 7. 布局变更验证阶梯(强制执行) 任何 scatter / CCM / 堆大小变更后,依次通过以下全部关卡方可认定生效: diff --git a/scripts/ram_baseline.py b/scripts/ram_baseline.py index c338b2af..48ca48c9 100644 --- a/scripts/ram_baseline.py +++ b/scripts/ram_baseline.py @@ -78,7 +78,11 @@ def subsystem_of(obj): return RTOS_HEAP if n in _KERNEL: return "其它" - if n.startswith(("app_", "adc_", "prot_", "phasor", "sample_ring", + # 应用 = App/ 其余(台账 §2 口径)。新增 App 目录/前缀必须同步这里, + # 漏登记不会报错,只会被静默算进「其它」——unattributed_app_objects() 是兜底信号。 + if n.startswith(("app_", "adc_", "prot_", "protect_task", "phasor", "sample_ring", + "wave_", "meas", "freq", "run_par", "iec104", "soe_", + "task_prof", "boot_stats", "vol_idle_sync", "sys_", "cfg_", "dbg_", "console_log", "task_watchdog", "sha256", "tpafe", "sd2506", "test_task", "time_sync", "default_task")): @@ -86,6 +90,56 @@ def subsystem_of(obj): return "其它" +# 兜底:源文件在 App/ 却落进「其它」= 前缀表漏登记。2026-10-29 实测这样隐身了 +# 12 个对象 / 9,445 B(wave_ring 5,296 起),配额表完全看不见录波与 SOE 支路。 +_APP_SRC_DIR = REPO_ROOT / "App" +_APP_STEMS = None + + +def _app_stems(): + """App/**/*.c 主干名集合(扫一次缓存);目录读不到就返回空集 = 兜底自动失效。""" + global _APP_STEMS + if _APP_STEMS is None: + try: + _APP_STEMS = {p.stem.lower() for p in _APP_SRC_DIR.rglob("*.c")} + except OSError: + _APP_STEMS = set() + return _APP_STEMS + + +def _obj_base(obj): + return obj.rsplit("\\", 1)[-1].rsplit("/", 1)[-1].lower() + + +def unattributed_app_objects(objs): + """[(对象, B)]:归进「其它」但源文件在 App/ 的对象,按字节降序。""" + stems = _app_stems() + if not stems: + return [] + out = [] + for obj, nbytes in objs: + if subsystem_of(obj) != "其它": + continue + base = _obj_base(obj) + if base.endswith(".o") and base[:-2] in stems: + out.append((obj, nbytes)) + return sorted(out, key=lambda t: (-t[1], t[0])) + + +def warn_unattributed(info): + """打印未归类告警(软提醒,不拦);返回条数。门禁与 --write 都调,别让人静默隐身。""" + missing = unattributed_app_objects(info["objs"]) + if not missing: + return 0 + total = sum(n for _, n in missing) + print("[WARN] %d 个 App/ 对象未登记进 subsystem_of() 前缀表,被算进「其它」而不是" + "「应用」(台账 §2:应用 = App/ 其余),合计 %d B:" % (len(missing), total)) + for obj, nbytes in missing: + print(" %-28s %6d B" % (_obj_base(obj), nbytes)) + print(" 修法:补 subsystem_of() 前缀 -> 重跑 @build -> --update / --write 回填台账") + return len(missing) + + def collect(map_path): """解析 .map → {'regions': {name: r}, 'subsys': {name: bytes}, 'top': [(obj, B)]}。""" data = ram_report.parse_map(str(map_path)) @@ -96,11 +150,11 @@ def collect(map_path): subsys = {s: 0 for s in SUBSYSTEMS} for c in comps: subsys[subsystem_of(c["object"])] += c["rw_data"] + c["zi_data"] - top = sorted(((c["object"], c["rw_data"] + c["zi_data"]) for c in comps), - key=lambda t: (-t[1], t[0])) - top = [t for t in top if t[1] > 0][:TOP_N] + objs = [(c["object"], c["rw_data"] + c["zi_data"]) for c in comps] + objs = [t for t in objs if t[1] > 0] + top = sorted(objs, key=lambda t: (-t[1], t[0]))[:TOP_N] return {"regions": regions, "region_list": data["regions"], - "subsys": subsys, "top": top} + "subsys": subsys, "top": top, "objs": objs} def region_pct(r): @@ -238,6 +292,8 @@ def cmd_gate(args): if newest and newest > map_path.stat().st_mtime: print("[WARN] .map 早于最新源码——数字来自旧构建,建议先 @build 再提交") + warn_unattributed(info) + ir, cc = info["regions"].get("RW_IRAM1"), info["regions"].get("RW_CCM") print(f"RAM 预算门禁 PASS:RW_IRAM1 {region_pct(ir):.1f}% / RW_CCM {region_pct(cc):.1f}%" f"(阈值 {THRESHOLD_PCT:.0f}%)") From b49e2268154ba76f759235715719ea9f52593013 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Fri, 30 Oct 2026 00:19:20 +0800 Subject: [PATCH 15/19] =?UTF-8?q?docs(=E9=85=8D=E5=9B=BE):=20=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E6=A0=91=E6=80=BB=E8=A7=88=E6=94=B9=E7=94=B1=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=99=A8=E7=94=BB=EF=BC=8C=E7=99=BB=E8=AE=B0=E9=92=A9?= =?UTF-8?q?=E5=AD=90=E6=89=AB=E4=B8=8D=E5=88=B0=E7=9A=84=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 tools/arch_diagram/gen_menu_tree_svg.py → docs/images/菜单树总览 .svg:43 节点 = 9 菜单 + 34 叶,30 已绑页 / 4 未绑 / 16 叶带密码门, 最深 4 级;读数由脚本现算,不在文档里另抄一份 - 生成器内嵌 NODE_SRC(= ui_menu_tree.txt 的 43 行节点):在仓库里跑会 与真值逐行互验、不一致 exit 1;generated-fresh 在临时目录重跑读不到 真值 → 用内嵌表,输出逐字节一致(实测临时目录产物同 sha256) - 产品需求 §8.2 原先挂的是级联框显示几何图(画的是示例级联,从来不含 整棵树的节点),换成这张树图,并删掉「该图仍是 42 节点时代所画」的 补丁注 - 图形绘制流程新增 §2.1:登记 generated-fresh 扫不到的四张图 —— 两张 tools/refgraph 产物(引用关系图 / _模块,不在 gen_*.py 命名面里)+ 两张手画稿(级联框总览、级联框几何计算)。钩子只扫 tools/arch_diagram/gen_*.py,这些图改了不会有人报警;再多一张先说清 为什么不能用生成器 - arch_diagram README 脚本一览加行 --- docs/images/菜单树总览.svg | 235 ++++++++++++ docs/产品需求/产品需求设计文档.md | 5 +- docs/架构设计/图形绘制流程.md | 16 + tools/arch_diagram/README.md | 1 + tools/arch_diagram/gen_menu_tree_svg.py | 458 ++++++++++++++++++++++++ 5 files changed, 712 insertions(+), 3 deletions(-) create mode 100644 docs/images/菜单树总览.svg create mode 100644 tools/arch_diagram/gen_menu_tree_svg.py diff --git a/docs/images/菜单树总览.svg b/docs/images/菜单树总览.svg new file mode 100644 index 00000000..1ae2e7ec --- /dev/null +++ b/docs/images/菜单树总览.svg @@ -0,0 +1,235 @@ + + + +菜单树总览(层级 · 绑页 · 密码门) +方框 = 菜单(可展开);细线 = 叶(对应一个页面模块,或一处空缺) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +主界面 + +实时数据 + +参数定值 + +整定 + +查看 + +三遥设置 + +装置维护 + +通讯参数 + +记录查询 + +装置信息 +device_info + +交流量 +ac + +直流量 +dc + +遥信量 +yx + +参数 +view_param +P + +定值 +view_set +P + +控制字 +view_word +P + +软压板 +view_plate +P + +参数 +view_param + +定值 +view_set + +控制字 +view_word + +软压板 +view_plate + +遥测死区 +deadband +P + +遥测系数 +calib +P + +遥信类型 +P + +遥信防抖 +P + +双点遥信 +P + +时钟设置 +clock +P + +强制复归 +reset +P + + +手动录波 +wave_manual +P + +清除记录 +clear +P + +通讯设置 +comm +P + +网口设置 +net_cfg +P + +SNTP设置 +P + +SOE记录 +soe + +事故记录 +accident + +操作记录 +oper + +保护告警 +alarm + +保护启动 +start + +遥控记录 +yk + +自检记录 +selftest + +运行记录 +runlog + +运行报告 +runreport + + +录波回放 +wave_play + +本图读数 +节点 43 = 菜单 9 + 叶 34 +已绑页 30 · 未绑页 4 · 密码门 16 叶 +最深 4 级(主界面 → 叶) +怎么看 + +根:主界面本身,不是一个页面 + +白框:一级菜单,可展开、不绑页 + +紫底:二级菜单与它那一支的行带 + +绿底:录波两叶,已绑页面模块 + + +红字 + 虚线:未绑页的叶,进去只有菜单 +灰色小字 = 页面模块名(页面注册表的键) +行末的 P = 进这一叶要先过密码 +整定 与 查看 复用同一批页面模块 +未绑页的 4 叶 +· 遥信类型(三遥设置 下) +· 遥信防抖(三遥设置 下) +· 双点遥信(三遥设置 下) +· SNTP设置(通讯参数 下) +真值 = ui_menu_tree.txt(tools/menu_gen/gen_menu.py 的输入);本图由 tools/arch_diagram/gen_menu_tree_svg.py 生成 —— 改菜单树要同步脚本内的 NODE_SRC 重跑,勿手改 SVG + diff --git a/docs/产品需求/产品需求设计文档.md b/docs/产品需求/产品需求设计文档.md index 8ff0f2b3..3f3fa74b 100644 --- a/docs/产品需求/产品需求设计文档.md +++ b/docs/产品需求/产品需求设计文档.md @@ -1804,10 +1804,9 @@ pre-commit run --all-files ### 8.2 菜单树(43 节点) -![级联框总览](../images/级联框总览.svg) +![菜单树总览(层级 · 绑页 · 密码门)](../images/菜单树总览.svg) -> 该图仍是 42 节点时代所画(2026-10-29 新增的「录波回放」叶未进图):`tools/arch_diagram/` 里没有这张图的生成器, -> 重画要另开一笔并走 `generated-fresh`;**本节下面的文字与代码生成物是真值**。 +> 图里的节点数、绑页与密码门读数由 `tools/arch_diagram/gen_menu_tree_svg.py` 现算自真值 `ui_menu_tree.txt`(改菜单树 → 同步脚本内 `NODE_SRC` 重跑);级联框在屏上怎么摆(几何)→ `docs/UI设计/菜单页面设计.md` §4.1。 **规模**:菜单节点 **9** 个(含根)+ 叶子节点 **34** 个 = **43** 节点;最大深度 **4 级(主界面 / 一级 / 二级 / 三级菜单)**。 **绑定现状**(2026-10-29 复扫):**已绑内容页 30 叶 / 未绑 4 叶**(页面注册表 `UI_PAGE_MAP_COUNT` = 30, diff --git a/docs/架构设计/图形绘制流程.md b/docs/架构设计/图形绘制流程.md index 644c8fb0..e9879e45 100644 --- a/docs/架构设计/图形绘制流程.md +++ b/docs/架构设计/图形绘制流程.md @@ -54,6 +54,22 @@ | `gen_data_trust_ladder_svg.py` | `docs/images/数据可信台阶.svg` | `STEPS` 五级台阶(状态 / 问题 / 依据)+ `BAND_LABEL_1` / `GATE_LABEL` / `FOOTNOTE`(台阶状态变了才重画) | | `gen_data_trust_dc_ac_svg.py` | `docs/images/ADC直流标定与交流验证.svg` | `BOXES_A` / `BOXES_B` 两条泳道 + `CHIPS` 六件事 + `NOTES` 红线与"证不到"清单 | | `gen_wave_record_flow_svg.py` | `docs/images/故障录波数据流.svg` | `CHAIN` 主链四段 + `EDGE` 跨边界标签 + `TRAPS` 三条陷阱 + `FORBID` 四条红线(所有权、段形态或红线变了才重画) | +| `gen_menu_tree_svg.py` | `docs/images/菜单树总览.svg` | **内嵌 `NODE_SRC`**(= `ui_menu_tree.txt` 的 43 行节点)+ 版面常量;仓库里跑会与真值逐行互验,不一致 exit 1(临时目录里读不到真值 → 用内嵌表,输出逐字节一致) | + +### 2.1 钩子扫不到的图:refgraph 产物与历史手画稿 + +`generated-fresh` 的 `docs-svg` 条目只把 `tools/arch_diagram/gen_*.py` 拷进临时目录重跑逐字节比对。 +上表没列到的 `docs/images/*.svg` 就是下面这几张,**它们全都不在这条钩子的扫描面里**——改了不会有任何 +人报警,这就是「42 个节点的旧图挂在 43 个节点的标题下」能一路活到今天的原因。 + +| 图 | 谁画的 | 用在哪 | 维护口径 | +|---|---|---|---| +| `docs/images/引用关系图.svg`、`docs/images/引用关系图_模块.svg` | `tools/refgraph/gen_refgraph.py`(graphviz,不在 `gen_*.py` 命名面里) | `docs/架构设计/引用关系图说明.md` 的产品 | 改了代码引用就跑 `tools\refgraph\gen_refgraph.bat` 重生成,**svg 与改动同一笔提交**,别只更新 `.dot`;口径 → `docs/架构设计/引用关系图说明.md` | +| `docs/images/级联框总览.svg` | 2026-10-26 手画 | `docs/UI设计/菜单页面设计.md` §4.1(级联框显示几何:根框 + 整定子框) | 只作**显示几何示意**,不承载菜单树事实;树的结构 / 层级 / 绑页 → `docs/images/菜单树总览.svg`(有生成器) | +| `docs/images/级联框几何计算.svg` | 同上 | `docs/UI设计/菜单页面设计.md` §5.3(逐框坐标推导) | 同上;坐标公式变了要重算重画,别在 SVG 上描字 | + +新增精确布局图一律走 `tools/arch_diagram/gen_*.py`(`docs-svg` 条目自动发现,**不必登记基线**);上面这几张 +是这套纪律的**例外面**,再多一张先说清为什么不能用生成器。 --- diff --git a/tools/arch_diagram/README.md b/tools/arch_diagram/README.md index 6dfed728..cb428933 100644 --- a/tools/arch_diagram/README.md +++ b/tools/arch_diagram/README.md @@ -21,6 +21,7 @@ | `gen_data_trust_ladder_svg.py` | 数据可信台阶图 | 采集数据可信度的五级台阶:现在踩在哪一级、下一级挡什么 | | `gen_data_trust_dc_ac_svg.py` | ADC 直流标定与交流验证图 | 上排直流注入链、下排信号发生器正弦验证,中间列只有正弦能证的六件事 | | `gen_wave_record_flow_svg.py` | 故障录波数据流图 | 录波段数据的四个所有者与四条「不许」:谁独占写、谁只能读、标志怎么过界 | +| `gen_menu_tree_svg.py` | 菜单树总览图 | 43 节点的层级 / 绑页 / 密码门鸟瞰;内嵌 `ui_menu_tree.txt` 的节点行并与真值互验,防图与树漂移 | | `gen_sample_ring_why_svg.py` | 采样环缓冲_为什么需要 | 上游 1 kHz 覆盖写只交最新一帧 vs 下游要 20 点滑窗:缺口 19 点与三个消费者 | | `gen_sample_ring_layout_svg.py` | 采样环缓冲_环形布局 | 40 格环里哪 20 格是本窗、写指针指「下一格」、回绕与三条设计不变量 | | `gen_sample_ring_start_svg.py` | 采样环缓冲_窗首下标 | 窗首 = head − n 的圆周走法,与「直接减 / 16 位回绕 / 先加 DEPTH」三种写法对照 | diff --git a/tools/arch_diagram/gen_menu_tree_svg.py b/tools/arch_diagram/gen_menu_tree_svg.py new file mode 100644 index 00000000..a163796b --- /dev/null +++ b/tools/arch_diagram/gen_menu_tree_svg.py @@ -0,0 +1,458 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""生成 docs/images/菜单树总览.svg —— 菜单树的层级、绑页与密码门鸟瞰图。 + +数据源 = ui_menu_tree.txt(真值,tools/menu_gen/gen_menu.py 的输入)。本脚本内嵌同一张 +节点表 NODE_SRC,两种运行环境都要有确定行为: + · 在仓库里跑:先与 ui_menu_tree.txt 的节点行逐行互验,不一致直接 exit 1 打印差异 + —— 改了菜单树必须同步内嵌表,图才会跟着变(漂移摆在台面上,而不是让 SVG 悄悄失真); + · 在临时目录跑(pre-commit 的 generated-fresh 钩子把脚本拷出去重跑):读不到输入 + 文件 → 退回内嵌表 → 输出逐字节一致。 +""" +import html +import pathlib +import sys +import xml.etree.ElementTree as ET + +ROOT = pathlib.Path(__file__).resolve().parents[2] +TREE_FILE = ROOT / "ui_menu_tree.txt" +OUT = ROOT / "docs" / "images" / "菜单树总览.svg" + +# ui_menu_tree.txt 的节点行(缩进 2 空格 = 一层;#P = 需密码;#page=x = 已绑页面模块)。 +# 改菜单树 → 同步这张表 → 重跑本脚本 → 脚本与产物同一笔提交。 +NODE_SRC = [ + "主界面", + " 装置信息 #page=device_info", + " 实时数据", + " 交流量 #page=ac", + " 直流量 #page=dc", + " 遥信量 #page=yx", + " 参数定值", + " 整定", + " 参数 #P #page=view_param", + " 定值 #P #page=view_set", + " 控制字 #P #page=view_word", + " 软压板 #P #page=view_plate", + " 查看", + " 参数 #page=view_param", + " 定值 #page=view_set", + " 控制字 #page=view_word", + " 软压板 #page=view_plate", + " 三遥设置", + " 遥测死区 #P #page=deadband", + " 遥测系数 #P #page=calib", + " 遥信类型 #P", + " 遥信防抖 #P", + " 双点遥信 #P", + " 装置维护", + " 时钟设置 #P #page=clock", + " 强制复归 #P #page=reset", + " 手动录波 #P #page=wave_manual", + " 清除记录 #P #page=clear", + " 通讯参数", + " 通讯设置 #P #page=comm", + " 网口设置 #P #page=net_cfg", + " SNTP设置 #P", + " 记录查询", + " SOE记录 #page=soe", + " 事故记录 #page=accident", + " 操作记录 #page=oper", + " 保护告警 #page=alarm", + " 保护启动 #page=start", + " 遥控记录 #page=yk", + " 自检记录 #page=selftest", + " 运行记录 #page=runlog", + " 运行报告 #page=runreport", + " 录波回放 #page=wave_play", +] + +# ---- 版面 ---- +FS_LEAF = 12 +FS_MENU = 12 +FS_ROOT = 13 +FS_PAGE = 9 +PITCH = 18 +ROW_TOP = 96 +X_COL = (28, 150, 306, 474) +TRUNK_OFF = 22 +LEAF_TEXT = 14 +PAD_X = 9 +BAND_X = X_COL[1] - 12 +BAND_R = 624 +PANEL_X = 640 +W = 990 +TREE_LIMIT = 630 + +INK = "#0f172a" +MUTED = "#475569" +FAINT = "#94a3b8" +LINE = "#334155" +RULE = "#cbd5e1" +C_BLUE = "#1d4ed8" +BG_BLUE = "#eff6ff" +C_VIOLET = "#6d28d9" +BG_VIOLET = "#f5f3ff" +C_AMBER = "#b45309" +C_ROSE = "#be123c" +C_GREEN = "#15803d" +BG_GREEN = "#f0fdf4" +BG_GRAY = "#f8fafc" +FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" + +_over = [] + + +def emit(msg): + """控制台可能是 GBK:中文/符号打印不能把生成器打崩(假红 = 钩子重跑失败)。""" + enc = getattr(sys.stdout, "encoding", None) or "utf-8" + safe = str(msg).encode(enc, errors="replace").decode(enc, errors="replace") + sys.stdout.write(safe + "\n") + + +def tw(text, fs): + """近似字宽:CJK 约 1.0 em,其余 0.58 em。""" + return sum(fs * (1.0 if ord(c) > 0x2E80 else 0.58) for c in str(text)) + + +def esc(text): + return html.escape(str(text), quote=True) + + +def fit(x_right, limit, what): + if x_right > limit: + _over.append("%s: %.0f > %.0f" % (what, x_right, limit)) + + +def node_lines(raw): + """取节点行:丢空行与 # 指令行(指令改了不影响图的形状)。""" + out = [] + for ln in raw.splitlines(): + ln = ln.rstrip("\r") + if not ln.strip() or ln.strip().startswith("#"): + continue + out.append(ln) + return out + + +def verify_src(): + """仓库里跑 = 与真值互验;临时目录里跑 = 静默用内嵌表。""" + if not TREE_FILE.exists(): + emit("note: 未找到 ui_menu_tree.txt(临时目录重跑场景)→ 用内嵌 NODE_SRC") + return 0 + live = node_lines(TREE_FILE.read_text(encoding="utf-8-sig")) + if live == list(NODE_SRC): + emit("互验 OK:%d 行节点与 ui_menu_tree.txt 一致" % len(live)) + return 0 + emit("[FAIL] ui_menu_tree.txt 与本脚本内嵌 NODE_SRC 不一致 —— 图会失真,请同步内嵌表后重跑") + for i in range(max(len(live), len(NODE_SRC))): + a = live[i] if i < len(live) else "<缺>" + b = NODE_SRC[i] if i < len(NODE_SRC) else "<缺>" + if a != b: + emit(" 第 %d 行:真值 %r / 内嵌 %r" % (i + 1, a, b)) + return 1 + + +def parse(lines): + """节点行 → (根, 文档序节点表)。""" + root = None + docs = [] + stack = [] + for ln in lines: + depth = (len(ln) - len(ln.lstrip(" "))) // 2 + page = None + pwd = False + keep = [] + for tok in ln.strip().split(): + if tok == "#P": + pwd = True + elif tok.startswith("#page="): + page = tok[len("#page="):] + else: + keep.append(tok) + node = {"depth": depth, "label": " ".join(keep), "page": page, + "pwd": pwd, "kids": [], "y": 0.0, "w": 0.0} + docs.append(node) + while stack and stack[-1]["depth"] >= depth: + stack.pop() + if stack: + stack[-1]["kids"].append(node) + else: + root = node + stack.append(node) + return root, docs + + +def layout(root): + """叶按文档序占行;菜单 y = 首末子节点 y 的中点。返回叶行数。""" + state = [0] + + def walk(n): + if not n["kids"]: + n["y"] = ROW_TOP + state[0] * PITCH + state[0] += 1 + return + for k in n["kids"]: + walk(k) + n["y"] = (n["kids"][0]["y"] + n["kids"][-1]["y"]) / 2.0 + + walk(root) + return state[0] + + +def yspan(n): + """子树占用的 y 区间。""" + if not n["kids"]: + return n["y"], n["y"] + lo, _ = yspan(n["kids"][0]) + _, hi = yspan(n["kids"][-1]) + return lo, hi + + +def tally(docs): + menus = [n for n in docs if n["kids"]] + leaves = [n for n in docs if not n["kids"]] + unbound = [n for n in leaves if not n["page"]] + return {"total": len(docs), "menus": len(menus), "leaves": len(leaves), + "bound": len(leaves) - len(unbound), "unbound": unbound, + "pwd": len([n for n in leaves if n["pwd"]]), + "depth": max(n["depth"] for n in docs) + 1} + + +QA = chr(34) # 双引号:拼 SVG 属性值用,免得在字面量里嵌引号 + + +def r1(v): + """一位小数(决定输出逐字节稳定)。""" + return format(float(v), ".1f") + + +def attrs(pairs): + out = [] + for key, val in pairs: + out.append(key + "=" + QA + str(val) + QA) + return " ".join(out) + + +def txt(x, y, body, fs, fill, weight="normal", anchor="start"): + head = attrs([("x", r1(x)), ("y", r1(y)), ("font-size", fs), ("fill", fill), + ("font-weight", weight), ("text-anchor", anchor)]) + return "" + esc(body) + "" + + +def rrect(x, y, w, h, fill, stroke, rx=5, sw=1.2): + return "" + + +def line(x1, y1, x2, y2, stroke, sw=1.2, dash=None): + pairs = [("x1", r1(x1)), ("y1", r1(y1)), ("x2", r1(x2)), ("y2", r1(y2)), + ("stroke", stroke), ("stroke-width", sw)] + if dash: + pairs.append(("stroke-dasharray", dash)) + return "" + + +def svg_header(h): + return ["", + "", + ""] + + +def draw_tree(docs, s): + """分组带 -> 连接线 -> 方框与叶(后画的压在前面之上)。""" + band = 0 + for n in docs: + if n["depth"] != 1: + continue + lo, hi = yspan(n) + fill = BG_GRAY if band % 2 == 0 else "#ffffff" + s.append(rrect(BAND_X, lo - 9, BAND_R - BAND_X, (hi - lo) + 18, fill, + "none", 6, 0)) + band += 1 + for n in docs: + if n["depth"] == 2 and n["kids"]: + lo, hi = yspan(n) + s.append(rrect(X_COL[2] - 10, lo - 9, BAND_R - (X_COL[2] - 10), + (hi - lo) + 18, BG_VIOLET, "none", 5, 0)) + for n in docs: + if not n["kids"]: + continue + d = n["depth"] + tx = X_COL[d + 1] - TRUNK_OFF + s.append(line(tx, n["kids"][0]["y"], tx, n["kids"][-1]["y"], RULE)) + s.append(line(X_COL[d] + n["w"], n["y"], tx, n["y"], RULE)) + for k in n["kids"]: + kx = X_COL[k["depth"]] if k["kids"] else X_COL[k["depth"]] + 3 + s.append(line(tx, k["y"], kx, k["y"], RULE)) + for n in docs: + if n["kids"]: + d = n["depth"] + fs = FS_ROOT if d == 0 else FS_MENU + if d == 0: + ink, fill, stroke, bh = C_BLUE, BG_BLUE, C_BLUE, 26 + elif d == 1: + ink, fill, stroke, bh = INK, "#ffffff", LINE, 22 + else: + ink, fill, stroke, bh = C_VIOLET, "#ffffff", C_VIOLET, 20 + s.append(rrect(X_COL[d], n["y"] - bh / 2.0, n["w"], bh, fill, stroke, 5)) + s.append(txt(X_COL[d] + n["w"] / 2.0, n["y"] + fs * 0.36, n["label"], + fs, ink, "bold", "middle")) + if d + 1 < len(X_COL): + fit(X_COL[d] + n["w"], X_COL[d + 1] - TRUNK_OFF - 4, + "menu " + n["label"]) + for n in docs: + if n["kids"]: + continue + x = X_COL[n["depth"]] + y = n["y"] + wave = n["label"] in ("手动录波", "录波回放") + bound = bool(n["page"]) + lw = tw(n["label"], FS_LEAF) + if wave: + s.append(rrect(x + 1, y - 8.5, LEAF_TEXT + lw + 9, 17, BG_GREEN, + C_GREEN, 4, 0.9)) + if bound: + s.append(line(x + 2, y, x + 9, y, RULE)) + else: + s.append(line(x + 2, y, x + 9, y, C_ROSE, 1.2, "3,2")) + ink = INK + if not bound: + ink = C_ROSE + elif wave: + ink = C_GREEN + s.append(txt(x + LEAF_TEXT, y + 4, n["label"], FS_LEAF, ink)) + cur = x + LEAF_TEXT + lw + if bound: + s.append(txt(cur + 10, y + 3.5, n["page"], FS_PAGE, FAINT)) + cur += 10 + tw(n["page"], FS_PAGE) + if n["pwd"]: + s.append(txt(cur + 9, y + 3.5, "P", 9, C_AMBER, "bold")) + cur += 10 + tw("P", 9) + fit(cur, TREE_LIMIT, "leaf " + n["label"]) + + +def panel_rows(c): + rows = [{"k": "h", "t": "本图读数"}] + rows.append({"k": "t", "t": "节点 %d = 菜单 %d + 叶 %d" + % (c["total"], c["menus"], c["leaves"])}) + rows.append({"k": "t", "t": "已绑页 %d · 未绑页 %d · 密码门 %d 叶" + % (c["bound"], len(c["unbound"]), c["pwd"])}) + rows.append({"k": "t", "t": "最深 %d 级(主界面 → 叶)" % c["depth"]}) + rows.append({"k": "s", "t": ""}) + rows.append({"k": "h", "t": "怎么看"}) + rows.append({"k": "sw", "fill": BG_BLUE, "stroke": C_BLUE, + "t": "根:主界面本身,不是一个页面"}) + rows.append({"k": "sw", "fill": "#ffffff", "stroke": LINE, + "t": "白框:一级菜单,可展开、不绑页"}) + rows.append({"k": "sw", "fill": BG_VIOLET, "stroke": C_VIOLET, + "t": "紫底:二级菜单与它那一支的行带"}) + rows.append({"k": "sw", "fill": BG_GREEN, "stroke": C_GREEN, + "t": "绿底:录波两叶,已绑页面模块"}) + rows.append({"k": "sw", "fill": "#ffffff", "stroke": C_ROSE, "dash": "3,2", + "t": "红字 + 虚线:未绑页的叶,进去只有菜单"}) + rows.append({"k": "t", "t": "灰色小字 = 页面模块名(页面注册表的键)"}) + rows.append({"k": "t", "t": "行末的 P = 进这一叶要先过密码"}) + rows.append({"k": "t", "t": "整定 与 查看 复用同一批页面模块"}) + rows.append({"k": "s", "t": ""}) + rows.append({"k": "h", "t": "未绑页的 %d 叶" % len(c["unbound"])}) + for n in c["unbound"]: + par = n["parent"]["label"] if n["parent"] else "?" + rows.append({"k": "t", "t": "· " + n["label"] + "(" + par + " 下)"}) + return rows + + +def draw_panel(c, s): + """右侧读数与图例:数字全部现算,文档里不必再抄一份。""" + rows = panel_rows(c) + x = PANEL_X + pw = W - 30 - x + lh = 18.0 + hh = 25.0 + gap = 12.0 + body_h = 0.0 + for r in rows: + if r["k"] == "h": + body_h += hh + elif r["k"] == "s": + body_h += gap + else: + body_h += lh + y0 = ROW_TOP - 24 + s.append(rrect(x - 16, y0, pw + 30, body_h + 44, "#ffffff", RULE, 8)) + y = y0 + 26 + for r in rows: + if r["k"] == "s": + y += gap + continue + if r["k"] == "h": + s.append(txt(x, y + 4, r["t"], 12.5, INK, "bold")) + fit(x + tw(r["t"], 12.5), W - 40, "panel " + r["t"]) + y += hh + continue + tx = x + if r["k"] == "sw": + sw = r.get("dash") + s.append(rrect(x, y - 8, 15, 12, r["fill"], r["stroke"], 3, 1.0)) + if sw: + s.append(line(x, y - 2, x + 15, y - 2, r["stroke"], 1.0, sw)) + tx = x + 22 + s.append(txt(tx, y + 2, r["t"], 10.5, MUTED)) + fit(tx + tw(r["t"], 10.5), W - 40, "panel " + r["t"][:8]) + y += lh + + +def main(): + if verify_src(): + return 1 + root, docs = parse(NODE_SRC) + for n in docs: + n["parent"] = None + for n in docs: + for k in n["kids"]: + k["parent"] = n + leaf_rows = layout(root) + for n in docs: + if n["kids"]: + fs = FS_ROOT if n["depth"] == 0 else FS_MENU + n["w"] = 2 * PAD_X + tw(n["label"], fs) + c = tally(docs) + h = ROW_TOP + leaf_rows * PITCH + 76 + s = svg_header(h) + s.append(txt(28, 40, "菜单树总览(层级 · 绑页 · 密码门)", 17, INK, "bold")) + s.append(txt(28, 61, "方框 = 菜单(可展开);细线 = 叶(对应一个页面模块,或一处空缺)", + 11, MUTED)) + draw_tree(docs, s) + draw_panel(c, s) + foot = ROW_TOP + leaf_rows * PITCH + 42 + note = ("真值 = ui_menu_tree.txt(tools/menu_gen/gen_menu.py 的输入);本图由 " + "tools/arch_diagram/gen_menu_tree_svg.py 生成 —— 改菜单树要同步脚本内的 " + "NODE_SRC 重跑,勿手改 SVG") + s.append(txt(28, foot, note, 9.5, FAINT)) + fit(28 + tw(note, 9.5), W - 20, "foot") + s.append("") + body = "\n".join(s) + "\n" + OUT.parent.mkdir(parents=True, exist_ok=True) + OUT.write_bytes(body.encode("utf-8")) + try: + ET.fromstring(body) + except ET.ParseError as exc: + emit("XML FAIL: %s" % exc) + return 1 + emit("SVG %dx%d 节点 %d 菜单 %d 叶 %d 绑页 %d 未绑 %d 密码门 %d 最深 %d 级" + % (W, h, c["total"], c["menus"], c["leaves"], c["bound"], + len(c["unbound"]), c["pwd"], c["depth"])) + if _over: + for item in _over: + emit("OVERFLOW " + item) + emit("[FAIL] 版面溢出 %d 处 —— 调版面常量后重跑" % len(_over)) + return 1 + emit("OVERFLOW 0 → " + str(OUT.relative_to(ROOT))) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From ecf9d3ad3db01f881f31fe999e16970b82221c76 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Fri, 30 Oct 2026 00:31:36 +0800 Subject: [PATCH 16/19] =?UTF-8?q?docs(task-worktree=20skill):=20=E4=BF=AE?= =?UTF-8?q?=20@T3@=20=E5=9B=B4=E6=A0=8F=E8=85=90=E5=9D=8F=EF=BC=8C?= =?UTF-8?q?=E6=AD=A3=E6=96=87=E7=98=A6=E8=BA=AB=E5=B9=B6=E6=8A=8A=E7=BB=86?= =?UTF-8?q?=E8=8A=82=E5=A4=96=E7=BD=AE=20reference/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 围栏腐坏是真 bug:@T3@ 是被误提交进仓的代码围栏占位符,Markdown 渲染出 来就是一行 @T3@ 加裸命令。git log -S 定位:842d711e(2026-10-28) SKILL.md 还有 18 个真三反引号围栏行、0 个占位符;21c68521(2026-10-29 插件桥接那笔)起变 0 围栏 / 34 个占位符。全仓只有本技能 6 个文件有此 病,draw-diagram / keil-build / ui-verify 等都用真围栏。本轮七个文件 占位符清零、围栏全部成对。 信息密度:SKILL.md 28,139 → 23,087 B(−18%,占插件写侧上限 65536 的 35%,给自进化留头量)。同一句外置理由在 C2/D/E/F/G/陷阱/验证 重复 7 次 → 0;frontmatter 与 F 节写了两遍的插件桥接说明收进 F 节;「这不是免费 的」「挡给你看的保护」这类修辞删掉。删掉的细节全部落进对应 reference 篇——skill-size 钩子会校验指针不悬空:现 26 处链接 / 8 篇目标 / 悬空 0, 每篇都有入口。 锚点未重排:节号 A–G/C2、各节步骤号、铁律编号被 reference/ 反向引用 (铁律 3/5/7/9 与 A 第 1、5 步,C 第 5~7 步,E 第 1~6 步均结构性存在)。 只在 SKILL.md 出现的 PIPESTATUS 教训保留(reference/ 里没有,删了就丢)。 门禁:check_skill_size.py rc=0(17 个 SKILL.md 全在写侧上限内、无悬空 指针);七文件均无 BOM / CR / U+FFFD、单个尾换行。 --- .dsh/skills/task-worktree/SKILL.md | 442 +++++++++--------- .../reference/clean-room-measure.md | 12 +- .../reference/dsh-plugin-bridge.md | 4 +- .../task-worktree/reference/gates-in-tree.md | 10 +- .../reference/merge-dirty-main-tree.md | 20 +- .../task-worktree/reference/pitfalls.md | 1 + .../reference/reclaim-unregistered.md | 32 +- 7 files changed, 274 insertions(+), 247 deletions(-) diff --git a/.dsh/skills/task-worktree/SKILL.md b/.dsh/skills/task-worktree/SKILL.md index 0e99736a..0606a695 100644 --- a/.dsh/skills/task-worktree/SKILL.md +++ b/.dsh/skills/task-worktree/SKILL.md @@ -1,265 +1,289 @@ --- name: task-worktree description: >- - 多任务并行开发的空间管理(git worktree 生命周期):`@task new` 开工作树+任务分支、目录联接共享 .venv 与模拟器 SDK、活跃上限 8 与腾位(A 节)、build/bench 两类独占锁与非登记持有者「缺台账」释放不掉(B 节)、`done` 是合并点不是查看器(C 节)、主工作树被别的并发会话弄脏时的手工合并路径(C2 节,详情已外置)、门禁指标异常增长时用净室工作树逐文件归因(D 节,外置)、回收 `@task list` 看不到的工作树并清悬挂台账(E 节,外置)、与 DSH worktree 插件 `/worktree`、`worktree_create` 共存(F 节,外置)、任务树内跑门禁与 `simulator/SDL2` 联接假红、树内提交/合并信息的禁词自检(G 节,外置)、插件树两条共享联接的分级(`simulator/SDL2` 是硬需求、`.venv` 有解释器逃生门,F 节,外置)、并行上限数字的唯一事实源(`MAX_CONCURRENT`)。规格见 docs/环境与部署/并行任务工作树方案.md;标「外置」的深规程全文在本技能目录 reference/ 下,动手前先读对应那篇。 + 并行开发的空间管理(git worktree 生命周期,短命令 `@task`):开任务树与分支、联接共享 `.venv` 与模拟器 SDK、build/bench 独占锁、并发上限与腾位(A)、放不掉的锁(B)、`done` 是合并点不是查看器与补绿链(C)、主树被别的会话弄脏时的手工合并(C2)、门禁指标异常的净室归因(D)、回收非登记树与悬挂台账(E)、与 DSH worktree 插件共存及联接分级(F)、树内跑门禁与消息禁词(G)。规格 = docs/环境与部署/并行任务工作树方案.md;深规程全文在 reference/。 whenToUse: >- - 要同时推进多个任务、担心同分支同文档互相冲突;要为新任务开隔离空间;要把任务合并回主干;或遇到 `new` 报「已有 8 个活跃任务(上限 8)」、`lock release` 报「任务 X 不存在(缺台账)」、`done` 报「门禁未全绿」或「主工作树不干净」而那份未提交改动是别的会话/插件留下的、两个任务抢 COM6 / UV4、"改了文档被覆盖"、并集合并残留;或要在任务树里跑文档/审计门禁(往 `ci-artifacts/` 写输出报 ENOENT、`--all` 的问题全在 `simulator/SDL2` 联接下);或门禁指标异常增长(如不可判定数 +16)却查不出是哪个文件造成的;或要回收 `@task list` 里根本看不到的工作树、清台账里 branch/path 都已不存在的悬挂条目;或任务是在任务树里改文档再合主干、要写提交/合并信息(被清退的主题不得在消息里复现);或想确认插件工作树里 `.venv` 与 `simulator/SDL2` 到底哪个必须补;或要改并行上限这个数字并同步全仓口径。 + 要为新任务开隔离空间、同时推进多个任务、把任务合回主干或回收工作树;或撞到 `new` 报「已有 8 个活跃任务(上限 8)」、`lock release` 报「任务 X 不存在(缺台账)」、`done` 报「门禁未全绿」或「主工作树不干净」而那份脏改动是别的会话/插件留的、两个任务抢 COM6 或 UV4、并集合并残留;或在任务树里跑门禁写输出报 ENOENT、`--all` 的问题全在 `simulator/SDL2` 联接下;或门禁指标异常增长查不出哪个文件造成;或要回收 `@task list` 看不到的树、清 branch/path 都已不存在的悬挂台账;或要确认插件树里 `.venv` 与 `simulator/SDL2` 哪个必须补;或要改并行上限这个数字并同步全仓口径。 --- # task-worktree — 多任务并行开发空间(短命令 `@task`) -**执行体 = 本 skill 自带脚本** `.dsh/skills/task-worktree/scripts/task_worktree.py` -(纯标准库,任意 python3;拷整个 skill 目录到别的项目即可用)。 +- **执行体** = `.dsh/skills/task-worktree/scripts/task_worktree.py`(纯标准库;拷整个 skill 目录即可换项目用)。 +- **深规程全文在 reference/ 下:做 C2 / D / E / F / G 任何一条之前先把对应那篇读完**——本文只留判得出来「要不要读」的判据。 +- 为什么这么拆:本文件受 dsh-mnemon 写侧上限 **65536 字节**(`skill-size` 钩子 49152 告警;实测 72,601 字节的技能照常可读可提交,只是自进化每次「生成草案」被拒、永久卡在「生成中」)→ **新增深规程进 reference/,本文只加判据**。瘦身前 frontmatter 原文 → [frontmatter-history.md](reference/frontmatter-history.md)。 -**本文档受 dsh-mnemon 写侧上限 65536 字节约束**(实测 2026-10-29:正文涨到 72,601 字节后,技能自进化每次「生成草案」都被写侧拒绝、 -那一行永久卡在「生成中」)。所以**新增的深规程写进 reference/<篇名>.md**,本文只留可执行判据的摘要 + 指针;瘦身前的 frontmatter -原文逐字留在 [reference/frontmatter-history.md](reference/frontmatter-history.md)。**动手做某条深规程前,先把对应那篇 reference 读完**(摘要只用来判定要不要读)。 +## 症状 → 去哪一节 -@T3@powershell -# cwd = 仓库根(或任意子目录) -& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" <子命令> [参数] -@T3@ - -bash(`ops_bash`)里固定两个变量,下文用 `$TW` 代指脚本: - -@T3@bash -PY=".venv/Scripts/python.exe"; TW=".dsh/skills/task-worktree/scripts/task_worktree.py" -"$PY" "$TW" list -@T3@ - -退出码:`0`=成功 / `1`=操作失败 / `2`=前置或环境错误 / `3`=用法。 -**⚠ 这几个码只有「不接管道」时才读得到**——原因与对策见「读输出」一节。 +| 现场 / 目标 | 去 | +|---|---| +| 开新树 / 看全景 / 追平主干 / 放弃任务 | 固定命令、典型流程 | +| `new` 报「已有 8 个活跃任务(上限 8)」 | **A** | +| `lock release` 报「任务 X 不存在(缺台账)」,锁照旧挂着 | **B** | +| `done` 做了什么、红了怎么补绿、能不能反悔、怎么算「真完成」 | **C** | +| `done` 报「主工作树不干净」,而那份脏是别的会话/插件留的 | **C2** → [merge-dirty-main-tree.md](reference/merge-dirty-main-tree.md) | +| 门禁指标异常增长要逐文件归因(主树全程只读) | **D** → [clean-room-measure.md](reference/clean-room-measure.md) | +| `@task list` 里没有的树要回收 / detached / 有在飞内容 / 悬挂台账 | **E** → [reclaim-unregistered.md](reference/reclaim-unregistered.md) | +| 插件树缺联接、锁归属 `pw:`、「有插件为何还自持生命周期」 | **F** → [dsh-plugin-bridge.md](reference/dsh-plugin-bridge.md) | +| 树内写门禁输出 ENOENT、`--all` 假红、写提交/合并信息、字节体检 | **G** → [gates-in-tree.md](reference/gates-in-tree.md) | +| 退出码读到 0 其实是 2、`tail` 之后误判「有冲突」 | 读输出 | +| 谁不许跑 `done`/`abort`、清场前必做什么、任务期哪些文件不许改 | 铁律 1–9 | +| 收工前最后一遍自查(各节该留什么证据)/全部实测坑 | [verification.md](reference/verification.md) / [pitfalls.md](reference/pitfalls.md) | ## 固定命令 +```bash +PY=".venv/Scripts/python.exe"; TW=".dsh/skills/task-worktree/scripts/task_worktree.py" +"$PY" "$TW" <子命令> [参数] # PowerShell:& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" <子命令>(cwd = 仓库根或任意子目录) +``` + | 命令 | 作用 | |---|---| -| `new ` | 建分支 `task/-` + 工作树 + 共享联接 + 台账;打印新空间路径 | -| `list` | 所有任务一览:分支 / 落后领先 / 脏文件数 / 台架状态 + 两个锁的归属 | -| `status ` | 单任务详情(未提交清单、相对基线漂移、台架记录、已建联接) | -| `sync ` | 在任务工作树里 `rebase` 追平主干,把冲突控制在任务分支上 | -| `lock acquire\|release\|status ` | 独占资源锁 | -| `plugin list\|adopt\|unadopt [插件树名]` | 与 DSH worktree 插件的桥:看清单 / 补共享联接 / 摘联接(**不删树**)→ F 节 | -| `done ` | 预演 → 合入主干 → 并集残留审计 → 重生成产物 → 门禁 → 清场(主树被别的会话弄脏时改走 **C2** 手工合并) | -| `abort ` | 放弃任务(工作树 + 分支一起删,**永久丢弃**) | +| `new ` | 建分支 `task/-` + 工作树 + 共享联接 + 台账,打印新空间路径 | +| `list` | 分支 / 落后领先 / 脏文件数 / 台架状态 + 两把锁归属。**只遍历台账** → 非登记树看不见(E 节) | +| `status ` | 未提交清单、相对基线漂移、台架记录、已建联接 | +| `sync ` | 在任务树里 `rebase` 追平主干,把冲突留在任务分支上 | +| `lock acquire|release|status ` | 独占资源锁(→ 铁律 3、B 节) | +| `plugin list|adopt|unadopt [插件树名]` | 插件桥:看清单 / 补共享联接 / 摘联接(**不删树**)→ F 节 | +| `done ` | 预演 → 合入主干 → 并集残留审计 → 重生成产物 → 门禁 → 清场 | +| `abort ` | 工作树 + 分支一起删,**永久丢弃** | -`new` 常用开关:`--base <分支>`(默认 `v2-on-ui`)· `--needs-bench`(无台架 PASS 则 `done` 拒合)· -`--lean`(跳过 Git LFS smudge)· `--no-bootstrap`(不建联接)· -`--force`(**仅跳过并发上限 8 的检查**,不解决「谁该让位」,见 A 节)。 - -`done` 常用开关:`--dry-run`(只预演,零改动)· `--skip-build` · `--skip-gates` · `--force`(清场时强制移除工作树)。 - -`lock` 开关:`--task `(acquire 必填)· `--wait <秒>` · `--ttl <小时>`(自报窗口)· `--note <文本>` · -`--bench-pass`(release 时给台账记 PASS)· `--force`。 -⚠ `--note` 只在 **acquire** 时写进锁 payload,**release 时被忽略**——别指望它留下验收证据(证据写进主干提交说明或文档)。 - -**环境变量**(高级):`DTU_WT_ROOT`(任务容器位置,默认仓库同级 `_wt/`)· -`DTU_TASK_BASE`(默认基线分支,默认 `v2-on-ui`)· -`DTU_INTEGRATION_WORKTREE`(集成点,默认主工作树——用于隔离演练/测试)。 +- **退出码** `0` 成功 / `1` 操作失败 / `2` 前置或环境错误 / `3` 用法——**只有不接管道才读得到**(→ 读输出)。 +- `new`:`--base`(默认 `v2-on-ui`)· `--needs-bench`(无台架 PASS 则 `done` 拒合)· `--lean`(跳 LFS smudge,只省磁盘不省时间)· `--no-bootstrap`(不建联接)· `--force`(**只跳过并发上限检查**,不解决谁让位 → A) +- `done`:`--dry-run` · `--skip-build` · `--skip-gates` · `--force`。⚠ `--dry-run` **只保护预演段、不保护清场段**(实测:对已合入的分支它照样摘联接、删分支、清台账;且它的前置也要抢 `build` 锁)→ 用完复验 `git worktree list`,细节 → 陷阱清单。 +- `lock`:`--task`(acquire 必填)· `--wait <秒>` · `--ttl <小时>`(默认 `build`=1h / `bench`=6h,超期视为陈旧、下次 acquire 自动接管)· `--note` · `--bench-pass` · `--force`。⚠ `--note` 只在 **acquire** 写进 payload,**release 时被忽略** → 验收证据写主干提交说明或文档。 +- 环境变量:`DTU_WT_ROOT`(默认仓库同级 `_wt/`)· `DTU_TASK_BASE`(默认基线)· `DTU_INTEGRATION_WORKTREE`(集成点,用于隔离演练)。 ## 典型流程 -@T3@powershell -& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" new N21 dhara-isfree --needs-bench -# → 把新会话的工作目录切到打印出来的 _wt/N21-dhara-isfree/,在那里干活 - -& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" lock acquire bench --task N21 -# 烧录 / @test / 板级套件…… -& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" lock release bench --task N21 --bench-pass - -# 回到主工作树执行回收 -& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" done N21 -@T3@ +```bash +"$PY" "$TW" new N21 dhara-isfree --needs-bench # 把新会话工作目录切到打印出来的 _wt/N21-dhara-isfree/ +"$PY" "$TW" lock acquire bench --task N21 # 烧录 / @test / 板级套件 +"$PY" "$TW" lock release bench --task N21 --bench-pass +"$PY" "$TW" done N21 # 回主工作树(铁律 1);先 --dry-run 读全量 +``` ## 读输出:管道与截断(两起误判的共同根因) -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] 无冲突,预演结果树 `。dry-run 全文只有 5~8 行,直接读全量。 +1. **`| tail -N` 之后的退出码是 `tail` 的,恒 0** → 脚本的 1/2 全被吞(实测三条「rc=0」真码都是 2)。判据先看文本(`[ok]` / `!!` / `前置/环境错误:` / `失败:`),要真码就**不接管道**或取 `${PIPESTATUS[0]}`。 +2. **`done` 的输出别 `tail -2`**:管道下 python stdout 块缓冲、git 子进程 stderr 不缓冲,两股流错位 → 只剩尾部那句「--dry-run:到此为止…」加带 `conflict` 字样的 stderr 片段,据此误判「有冲突」。dry-run 全文 5~8 行,**读全量**。 -## 分步操作(八条运行时机制) +## 分步操作(节号 A–G/C2、各节步骤号与铁律编号被 reference/ 与 docs 引用,**不许重排**) -### A. `new` 被拒「已有 8 个活跃任务(上限 8)」——怎么腾位 +### A. `new` 撞并发上限 → 腾位 -1. 读原文:`前置/环境错误:已有 8 个活跃任务(上限 8);先 done/abort,或加 --force`(exit 2)。 - 上限是脚本常量 `MAX_CONCURRENT = 8`;**「活跃」的判据 = `_wt/.state/.json` 台账存在,且台账里 - `path` 指向的目录还在**(手工删了工作树目录就不占名额,但会留下悬挂台账,见第 5 步与 **E 第 6 步**)。 -2. `$TW list` 看全景(分支 / 落后领先 / 脏文件数)决定谁让位:**已完成的先 `done`**(合并回收,一步不省)。 -3. 没人认领、零提交、尚未 claim 的空闲树 → `abort ` 腾位(实测就是 abort 掉排在后面的空闲树换出名额)。 - `abort` = 工作树 + 分支一起删、**永久丢弃**;工作树有未提交改动或已有提交时脚本会拒绝,确认可弃再加 `--force`。 -4. 确实要同时开第 9 棵才 `new --force`:磁盘代价约 **83 MB/棵**(`--lean` 约 45 MB), - 并行度上去了、**冲突面与台架排队也一起上去**——不是免费的。 -5. **别用手工 `git worktree remove` 给「登记任务」腾位**:台账不会被删(下次 `new` 同 ID 会报「任务 已存在」), - 还绕过了「先摘联接」(铁律 5),有让 git 递归进联接把主工作树 `.venv` 删掉的风险。要回收的若是**非登记树** - (`$TW list` 里根本没有它:`new` 出来又被手工删掉只剩悬挂台账的残骸 / D 节 `--detach` 净室 / 别的会话手工建的) - → 腾位与回收一起走 **E 节**,那里有「怎么证明这棵树零联接所以可以 remove」的可执行判据。 -6. 腾位后复验:`$TW list` 的条目数与 `ls _wt` 的目录数一致,再 `new`。 +1. 认原文 `前置/环境错误:已有 8 个活跃任务(上限 8);先 done/abort,或加 --force`(exit 2)。「活跃」= `_wt/.state/.json` 在且台账 `path` 目录还在 → **手工删目录不占名额、但留悬挂台账**(E 第 6 步)。 +2. `$TW list` 定谁让位:**已完成的先 `done`**(合并回收一步不省)。 +3. 无人认领、零提交、未 claim 的空闲树 → `abort `;有未提交或有提交时脚本会拒,确认可弃才 `--force`。 +4. 真要第 9 棵才 `--force`:并行度上去,**冲突面与台架排队一起上去**(每棵磁盘代价见铁律 6)。 +5. **别用手工 `git worktree remove` 给登记任务腾位**:台账不删(同 ID 报「任务已存在」)、又绕过「先摘联接」(铁律 5),有让 git 递归进联接删掉主树 `.venv` 的风险。要回收的是**非登记树**(`list` 里没有它)→ **E 节**。 +6. 复验:`list` 条目数 == `ls _wt` 目录数,再 `new`。 -### B. 非登记持有者占的锁:`lock release --bench-pass` 曾报「任务 X 不存在(缺台账)」 +### B. 非登记持有者的锁放不掉 -**症状**(lead 在主工作树跑台架验收窗,用临时名占锁 `lock acquire bench --task acceptwin`): +症状:`lock release bench --task acceptwin --bench-pass`(带 `--force` 同)报 `前置/环境错误:任务 acceptwin 不存在(缺台账 _wt/.state/acceptwin.json)`,**锁照旧挂着**。根因:`release` 带 `--bench-pass` 就先 `load_state(task)`,而 `acquire` 侧不查台账 → 「释放」被「记账」挡死;锁没放,`done` 的 `release_stale_locks()` 对未到 TTL 的活锁判失败 → **一个没登记的名字挡住所有任务收口**。 -@T3@bash -"$PY" "$TW" lock release bench --task acceptwin --bench-pass -前置/环境错误:任务 acceptwin 不存在(缺台账 _wt/.state/acceptwin.json) # 锁照旧挂着 -"$PY" "$TW" lock release bench --task acceptwin --force --bench-pass # 同一句错误,锁仍在 -@T3@ +**2026-10-29 已修**:无台账降级为 `[warn] …跳过 PASS 记录,仅释放锁`,**不再需要手写占位台账**。 +1. `$TW lock status` 认现场(持有者 / pid / 已持有 / TTL)——别对同一句报错反复重试。 +2. `$TW lock release --task [--bench-pass]` 直接放;PASS 记不上会 warn,锁照放。 +3. **PASS 后面还要用**(`done` 只认台账 `bench_pass`)→ 从一开始就用已 `new` 的 ID 占锁,或把结论写进主干提交/文档。插件树 `pw:` 天然无台账 → F 节。 +4. 最后手段:锁 = `_wt/.state/locks/.lock` 的单行 JSON(`name/task/pid/host/epoch/at/note/ttl`),`release` 就是 unlink 它——手工删 = 不记 PASS 的强制释放,前面各步都不可用才做。**早期为绕坑手写的占位台账现在有害**(既占上限名额又被 `list` 当任务)→ E 第 6 步备份后删。 -**根因**:`release` 分支里只要带 `--bench-pass` 就先 `load_state(task)` 去台账写 PASS,而 `acquire` 侧 -**不查台账**——这个不对称让「释放」被「记账」挡死(`--force` 只覆盖持有者不符与陈旧接管,管不到台账)。 -后果比报错本身大:锁没放,`done` 的前置 `release_stale_locks()` 对未到 TTL 的活锁直接判失败 → -**一个名字没登记,就挡住所有任务的收口**。 +### C. `done `:它是合并点,不是查看器 -**2026-10-29 起已修**(`task_worktree.py`:无台账降级为 `[warn] …跳过 PASS 记录,仅释放锁` 并照常释放)。 -现在的正解只有三步,**不再需要手写占位台账**: - -1. `$TW lock status` 认现场(持有者 / pid / 已持有时长 / TTL)——别对同一句报错反复重试 release。 -2. `$TW lock release --task [--bench-pass]` 直接放;PASS 记不上会打印 warn,锁照放。 -3. **PASS 后面还要用**(`done` 只认台账里的 `bench_pass`)→ 从一开始就用已 `new` 出来的任务 ID 占锁, - 或把验收结论写进主干提交说明 / 文档。**插件树 `pw:` 天然没有台账**,别指望它留 PASS → F 节。 - -最后手段(脚本本身都走不通时):锁就是 `_wt/.state/locks/.lock` 里的单行 JSON -(`name/task/pid/host/epoch/at/note/ttl`),`release` 的实现就是 unlink 它——手工删这个文件等价于 -「不记 PASS 的强制释放」,只在前面几步都不可用时才用。**历史遗留的占位台账**(早期为绕这个坑手写的 -`_wt/.state/.json`)现在已无必要:它活着时既占 `MAX_CONCURRENT = 8` 名额又被 `list` 当任务列出, -按 E 第 6 步备份后删掉即可。 - -### C. `done ` 的正确用法(它是合并点,不是查看器) - -1. **合并前确认任务属主已把全部改动提交完**。**`$TW status `** 看未提交清单与领先提交数;跨会话任务 - 再问一句属主(`send_message`)。实测教训:属主还剩「2/2」没提交就 `done`,把只含 1/2 的分支合进了主干, - 只能另开一棵树补后半并向对方说明(是自己的时序错,不是分支问题)。 -2. **先 `--dry-run`,并读全量输出**(别 `tail -2`,见「读输出」)。判据两行: - `[ok] 前置检查通过(领先 共 N 个提交)` + `[ok] 无冲突,预演结果树 `; - **N 必须等于属主应交的提交数**,对不上回到第 1 步。dry-run 零改动。 +1. **合并前确认属主提交完**:`$TW status ` 看未提交与领先数,跨会话再问属主一句。实测教训:属主还剩「2/2」没提交就 `done` → 只含 1/2 的分支进了主干,只能另开一棵树补后半(自己的时序错,不是分支问题)。 +2. 先 `--dry-run` 且**读全量**。判据两行:`[ok] 前置检查通过(领先 共 N 个提交)` + `[ok] 无冲突,预演结果树 `;**N 必须等于属主应交的提交数**,对不上回第 1 步。 3. 正式 `$TW done `——**必须在主工作树**(铁律 1)。 -4. 输出 `!! 门禁未全绿:工作树与分支**保留**,未清场` 时,紧跟的 `主干已含合并提交 ` 已经说明 - **合并落进主干了**。 - - ✅ 走提示里的 **a) 在主工作树修正后追加提交**; - - ❌ **绝不用 b) 的 `git -C "<主树>" reset --hard `**——那是丢弃已入库的合并提交,而 - `reset --hard` 本身是项目级 Never(会连未提交工作一起丢)。提示 b 只是列出的可能性,不是建议。 - - 先定位是**哪条钩子**红:门禁输出里的 `[SUMMARY] static: pre-commit hooks -- passed=N failed=M`, - 不同根因处置不同(见陷阱「`done` 的门禁 = `pre-commit run --all-files`」)。最常态是 `.map` 滞后 → 第 5 步。 -5. 补绿固定链(2026-10-25 实测;顺序即因果,全在**主工作树**跑,逐条不接管道看退出码): +4. 见 `!! 门禁未全绿:工作树与分支**保留**,未清场` 时,紧跟的 `主干已含合并提交 ` 说明**合并已落主干**;先定位哪条钩子红(`[SUMMARY] static: pre-commit hooks -- passed=N failed=M`),最常态是 `.map` 滞后 → 第 5 步。 + - ✅ 走提示 a):主工作树修正后追加提交。 + - ❌ **绝不用 b) 的 `reset --hard `**——那是丢弃已入库的合并提交,且 `reset --hard` 是项目级 Never(连未提交工作一起丢)。提示 b 是列出的可能性,不是建议。 +5. 补绿固定链(顺序即因果,全在主工作树,逐条不接管道看退出码): -@T3@bash +```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 # 提交属 Ask first(AGENTS.md 边界),按用户指示再做 -@T3@ +git add <上面刷出来的基线/快照文件> # 显式列文件,别 -A 把别人在飞的改动一起提交 +git commit -F # 提交属 Ask first(AGENTS.md 边界) +``` - 为什么必然要补,见陷阱「`done` 的门禁跑在 `@build` 之前」。 -6. **重跑 `done` 之前主工作树必须干净**——否则前置检查直接 - `失败:主工作树不干净(…)——合并会把未提交改动卷进来`,所以第 5 步的产物必须**先提交**。重跑时若分支 - 已并入,会打印 `[i] task/- 已合入 ,跳过合并,直接清场` → 摘联接 → `完成(分支此前已合入)`。 -7. 清场完成后再敲一次 `done ` 会得到 `前置/环境错误:任务 不存在(缺台账 …json)`(exit 2)—— - **这不是失败**:清场的最后一步 unlink 了台账。判完成以 `git log` 为准。 + 为什么必然要补 → 陷阱「`done` 的门禁跑在 `@build` 之前」。合并解过冲突时,**合并提交期 pre-commit 只查冲突文件**(→ G 节第 3 条)→ 落主干后必须另跑全量。 +6. **重跑 `done` 前主树必须干净**,否则 `失败:主工作树不干净(…)——合并会把未提交改动卷进来` → 第 5 步产物先提交。分支已并时重跑打印 `[i] task/- 已合入 ,跳过合并,直接清场`。主树脏且那份脏不是你的 → **C2**。 +7. 清场后再敲一次 `done ` 得 `前置/环境错误:任务 不存在(缺台账 …json)`(exit 2)——**不是失败**:清场最后一步 unlink 了台账。**判完成以 `git log` 为准**。 -### C2. `done` 被「主工作树不干净」挡住,而那份脏改动是**别的并发会话**留下的 +### C2. `done` 被「主工作树不干净」挡住,而那份脏是**别的并发会话**留的 -- **不许**对那份外部脏改动做 stash / checkout / reset / 提交 / 删除(项目级 Never,你也不知道它写没写完); - `done` 的前置要求主工作树干净,于是**合并与回收一起被挡**——这是挡给你看的保护,不是工具坏了。 -- **正解链(六步)**:取证(mtime + diff 定性有无交集,顺手 `worktree list` 看有没有别人中途新建的树)→ - `git merge-tree --write-tree ` 预演(判据:打出结果树 sha 且 RC=0)→ 主树 - `git merge --no-ff --no-edit -F ci-artifacts/merge-msg-.txt ` → 合并后把 - `git hash-object <外部文件>` 与 `git diff --stat` 和**合并前存下的值逐项对账**(实测同为 `369380ca…`、2+/2− - → 证明 pre-commit 自己的 stash 往返没动别人的工作)→ 台账先 `cp` 成 `.json.state-bak` 再让 `done`(或 E 节 - 手工路径)自己摘 .venv / simulator/SDL2 联接拆树 → `worktree list` + `lock status` 复验收工。 -- **报告里要写明绕开 `done` 的理由并逐项补回它的证据**:预演 sha + RC=0、blob id 对账、合并后在 - **主树**跑全库 `check_doc_links.py --all`(见 G 节)、两把锁空闲;并且点名那份外部脏文件与中途冒出来的第二棵树 - (谁的、怎么定性、你没动它的证据)。**blob id 变了 = stash pop 半冲突**:立刻停止收工,两份 blob id 与两份 diff - 交给该文件属主,绝不 `checkout` 抹平。 -- **认得出它**:本仓这份外部脏改动两次都是 `.dsh/skills/serial-boot-test/scripts/run_boot_test.py`(2+/2−,别的会话/插件正在写它)——别把它当成自己漏提交的文件去提交或清掉。同一次窗口里 `git -C "$MAIN" worktree list` 还会冒出**别人中途新建的第二棵树**(实测 `data-trust-restructure`),那是并发会话的硬证据:点名写进报告,并且不是你的树就别顺手回收(→ E 节)。 -- **合并信息也过 G 节的禁词自检**:清退/删主题类任务,`merge:` 那行与正文里被清退主题的关键词计数必须是 0。 +- **不许**对它做 stash / checkout / reset / 提交 / 删除(项目级 Never,你不知它写没写完)。`done` 要求主树干净 → 合并与回收一起被挡,**那是保护不是工具坏了**。 +- 正解六步,编号与 [reference/merge-dirty-main-tree.md](reference/merge-dirty-main-tree.md) 第 1~6 步一一对应(每步的确切命令与判据在那篇):**取证**(mtime + diff 定性有无交集,顺带 `worktree list` 看有无别人中途新建的树)→ **预演** `git merge-tree --write-tree `(打出结果树 sha 且 RC=0)→ **主树手工合并** `git merge --no-ff --no-edit -F ci-artifacts/merge-msg-.txt ` → **对账**(合并后 blob id 与 stat 同合并前存值逐项一致)→ **备份台账** `cp .json .json.state-bak` 后让 `done` 自己摘联接拆树 → **复验** `worktree list` + `lock status`。 +- **blob id 变了 = stash pop 半冲突**:立刻停止收工,把两份 blob id 与 diff 交给该文件属主,绝不 `checkout` 抹平。 +- **报告要点名**:绕开 `done` 的理由 + 补回它的证据(预演 sha 与 RC、blob 对账、主树全库 `check_doc_links.py --all`、两把锁空闲)+ 那份外部脏文件与中途冒出来的第二棵树(谁的、你没动它的证据)。合并信息也过 G 的禁词自检;「这份脏通常是谁」见陷阱清单。 -> 详情已外置(本文件须留头量给自进化:dsh-mnemon 写侧上限 65536 字节,超限的技能再也生不成草案)。全文见 [reference/merge-dirty-main-tree.md](reference/merge-dirty-main-tree.md) —— 主树被别的并发会话弄脏时的手工合并路径(取证 → `merge-tree` 预演 → 主树 `merge --no-ff -F` → 外部改动 blob id / `diff --stat` 前后对账)。 +### D. 门禁指标异常增长 → 净室逐文件归因(主树全程只读) -### D. 门禁指标异常增长 → 逐文件归因(把净室工作树当**测量仪**,主树全程只读) +三条判据:仓外 detached 树当净室、**一次只注入一份在飞文件**、与 HEAD 基线逐文件差分;主树前后 `git status --porcelain` 逐行一致才算「全程只读」。全文 → [reference/clean-room-measure.md](reference/clean-room-measure.md)。 -> 详情已外置(本文件须留头量给自进化:dsh-mnemon 写侧上限 65536 字节,超限的技能再也生不成草案)。全文见 [reference/clean-room-measure.md](reference/clean-room-measure.md) —— 把仓外 detached 工作树当净室测量仪:一次只注入一份在飞文件、与 HEAD 基线差分做逐文件归因、主树全程只读。 +### E. 回收**非登记**工作树(`list` 为空 / detached / 树里有在飞或未跟踪内容) -### E. 回收**非登记**工作树(`$TW list` 为空 / detached HEAD / 树里有在飞或未跟踪内容) +三条不可跳过:① 逐文件内容**三桶归类**(与主线工作区/HEAD 比对;「很久没动」不是判据)② 动手前跑**只读 reparse 预扫**,拿到 `REPARSE: none` 才许 `git worktree remove`(铁律 5)③ 唯一未合并残留先备份再 `--force`,拆完复验 + 清悬挂台账。**绝不跑 `done`/`abort`**(铁律 9)。全文(第 1~6 步 + 两份探针脚本)→ [reference/reclaim-unregistered.md](reference/reclaim-unregistered.md)。 -> 详情已外置(本文件须留头量给自进化:dsh-mnemon 写侧上限 65536 字节,超限的技能再也生不成草案)。全文见 [reference/reclaim-unregistered.md](reference/reclaim-unregistered.md) —— 回收 `@task list` 看不到的工作树:逐文件内容三桶归类、reparse/联接只读预扫、唯一残留先备份再 `--force`、悬挂台账清理复验。 +### F. 与 DSH worktree 插件(`/worktree`、`worktree_create`)共存 -### F. 与 DSH worktree 插件(`/worktree`、模型工具 `worktree_create`)共存 +| 联接 | 级别 | 依据 | 缺了会怎样 | 逃生门 | +|---|---|---|---|---| +| `simulator/SDL2` | **硬需求** | `simulator/CMakeLists.txt:12` 的 `set(SDL2_DIR …)` **非 `CACHE`**,include / `lib/x64` / `SDL2.dll` 全从它派生 | `build_sim.bat` 当场失败 | **无** | +| `.venv` | 软需求 | `scripts/ci_local.py` 有 `DTU_PRECOMMIT_PYTHON` + 五级解释器候选 | **静默**落机器级解释器(缺 fontTools → `generated-fresh` 假红) | 环境变量 | -- **两条共享联接不同级**(别答成「两个都必须」):`simulator/SDL2` = **硬需求**——`simulator/CMakeLists.txt:12` 是普通 `set(SDL2_DIR "${CMAKE_CURRENT_SOURCE_DIR}/SDL2/SDL2-2.32.10")` 而**非 `CACHE`**,既没有环境变量也没有 `-DSDL2_DIR=` 逃生门,include / `lib/x64` / `SDL2.dll` 三条路径全从它派生(`:34` `:70` `:71` `:180` `:207`)→ 缺它 `build_sim.bat` 当场失败。`.venv` = **软需求**——`scripts/ci_local.py` 有 `DTU_PRECOMMIT_PYTHON` 显式逃生门 + 五级解释器候选(显式 var → `sys.executable` → 仓库 `.venv` → `which python` → 机器级常见位置),绕开联接只是**静默**落到机器级解释器(缺 fontTools → `generated-fresh` 假红)。`plugin adopt` 两条都补是因为便宜,不是两条都硬。 -- **「有插件了为什么还自持 Python 生命周期」别凭印象答**:先实读插件包(`npm root -g` → `node_modules/@deepseek-ai/dsh/` 下定位插件、读 `package.json` 记版本),再 grep 三个概念名并报命中数——独占锁 `lock`、共享目录联接(`.venv` / `simulator`-SDL2)、联接状态三态显示;插件侧零命中就是自持的理由。重叠面只有开/收两个动作(两侧底层都是 `git worktree add`,没重造「建树」);锁、联接、台账与 `done` 的「预演 → 合并 → 审计 → 门禁 → 清场」编排才是插件没有的东西(本仓资源各只有一份:一块 ST-LINK、一套 Keil 授权、一个 COM6,SDL2 又无逃生门 → 必须串行 + 必须共享)。 +- `plugin adopt` 两条都补只是因为便宜,不代表两条都硬。插件树占锁记 `pw:`、**回收仍归 /worktree**(本 skill 不删插件树,插件也不删 `_wt/`)。「有插件为何还自持生命周期」=先读插件包(`npm root -g`)再 grep 三个概念名报命中数,插件侧零命中就是理由——取证姿势与答案边界 → [reference/dsh-plugin-bridge.md](reference/dsh-plugin-bridge.md) F-2。 -> 详情已外置(本文件须留头量给自进化:dsh-mnemon 写侧上限 65536 字节,超限的技能再也生不成草案)。全文见 [reference/dsh-plugin-bridge.md](reference/dsh-plugin-bridge.md) —— 与 DSH worktree 插件(`/worktree`、`worktree_create`)共存:`plugin adopt` 补共享联接(两条联接的分级:SDL2 硬 / `.venv` 软)、锁归属 `pw:`、回收仍归 /worktree、以及「为什么不用插件而自持生命周期」的取证姿势(F-2)。 +### G. 任务树内跑门禁:落点、`--all` 假红、消息禁词 -### G. 任务树内跑门禁与审计:输出落点(ci-artifacts)与 SDL2 联接假红 - -- **新建的任务树里没有 `ci-artifacts/`**(被 `.gitignore` 忽略 → 不会被检出):重定向输出或 - `fs.writeFileSync` 直接 `ENOENT`,**整段程序零执行**。落盘前先单独一条 `mkdir -p ci-artifacts`;树内输出 - 一律用树内相对路径(`cd` 进树后 `../_wt/x.txt` 会落到 `_wt/_wt/`,实测就是这句报错)。 -- **文档门禁在树内按单文件跑,全库 `--all` 基线留到合并后在主树跑**:树内的 `simulator/SDL2` 是指向 - 主树同一份的目录联接,`--all` 会把这份「第二份仓库副本」一起扫:实测树内打 222 个文档 / 35 处问题、RC=1 - 而 **35 处全在联接下**、要改的文档一条没有 = 假红;合并后主树同一条命令 = 2,597 处 / 194 个文档 / rc=0, - 这才是有效基线(单文件实测 161 处/98%/rc=0、16 处/88%/rc=0)。 -- 确要在树内跑 `--all`:判红固定三步——① 主树同命令拿基线 ② 过滤 `simulator/SDL2` 联接下的问题行 - ③ 剩下的逐行比。只做一步(见 RC=1 就报「门禁挂了」/ 过滤后为空就报「全绿」)都是误判。 -- **跑门禁前先做一次字节体检**(文档改动在树内提交前的固定项):`lines / bytes / BOM / CR / U+FFFD / 三连续空行 / 行尾空格 / 末行单个 LF` + 自写 Markdown 表格每行列数一致性检查。报出来的坏行**未必是本次改动**(实测 2,665 行的需求文档里两处坏表格与本次编辑无关)——先定位归属再决定动不动手,别顺手改别人的区域。这些面 `doc-links` / `no-mojibake` 只覆盖编码那半,表格断列与多出的空行要自己数。 -- **提交/合并信息一律 `-F` 读文件**(UTF-8 无 BOM、标题 ≤100 字符、正文每行 ≤80,文件放 `ci-artifacts/`),清退类任务另加一条**禁词自检**:`git log -1 --pretty=%B | grep -c '<被清退主题的关键词>'` 必须是 **0**(标题与正文都不许再出现那个关键词,改用「其它串口通道对照内容」这类语义短语;合并信息同样查一遍)。提交前自查消息里的行数/统计有没有算错;**未推送前**发现错了 → `git commit --amend -F <改好的文件>` 就地修,别追加一笔「更正」提交(那等于把禁词又写回历史)。 - -> 详情已外置(本文件须留头量给自进化:dsh-mnemon 写侧上限 65536 字节,超限的技能再也生不成草案)。全文见 [reference/gates-in-tree.md](reference/gates-in-tree.md) —— 任务树内跑门禁与审计:`/ci-artifacts/` 先 `mkdir -p`、文档链接按单文件在树内跑、全库 `--all` 合并后在主树跑、`simulator/SDL2` 联接假红、提交/合并信息禁词自检、改完文档的字节与表格体检。 +- **新树没有 `ci-artifacts/`**(被 ignore → 不检出):重定向/写文件直接 `ENOENT` 且**整段程序零执行** → 落盘前先单独一条 `mkdir -p ci-artifacts`;树内输出一律树内相对路径(`cd` 进树后 `../_wt/x.txt` 落到 `_wt/_wt/`)。一次性产物**不进仓库根**。 +- **文档门禁树内按单文件跑;全库 `--all` 留到合并后在主树跑**——树内 `simulator/SDL2` 是同一份仓库的联接,`--all` 把它一起扫成假红(实测 RC=1 而问题全在联接下、要改的文档一条没有)。非要树内跑:① 主树同命令拿基线 ② 过滤联接下的问题行 ③ 剩下的逐行比。**只做一步就是误判。** +- **合并提交期 pre-commit 只查冲突文件**(日志首行 `[INFO] Checking merge-conflict files only.`,其余全 `(no files to check) Skipped`)→ 合并落主干后**必须另跑全量** `scripts/ci_local.py`。 +- **跨层改动拆不成「按层分笔」**:钩子看到的世界 = HEAD + 本笔 staged(未暂存被 stash;未跟踪文件不被 stash 但会拖红同一棵树上别人的提交)→ 登记文件 / 生成物 / 互锁引用的文档分到他笔,本笔必被整仓门禁拦(`doc-links`/`registration-check`/`generated-fresh`/`mem-numbers`)。判据:**一笔提交必须自洽**。 +- **跑门禁前字节体检**:`lines / bytes / BOM / CR / U+FFFD / 三连续空行 / 行尾空格 / 末行单个 LF` + Markdown 表格每行列数一致性(单元格内竖线写成反斜杠竖线,否则被当分隔符断列)。坏行**未必是本次改动**,先定归属。`doc-links`/`no-mojibake` 只覆盖编码那半。 +- **提交/合并信息一律 `-F` 读文件**(UTF-8 无 BOM、标题 ≤100、正文每行 ≤80,放 `ci-artifacts/`);清退类任务加**禁词自检**:`git log -1 --pretty=%B | grep -c '<关键词>'` 必须是 **0**。未推送前发现消息写错 → `--amend -F` 就地修,别追加「更正」提交(等于把禁词写回历史)。 +- 全文(第 1~7 条,含 stash 机制与四道整仓门禁现场)→ [reference/gates-in-tree.md](reference/gates-in-tree.md)。 ## 铁律 -1. **`done` 只能在主工作树执行**——主干分支检出在主工作树,git 不允许同一分支在两处检出。 -2. **`done` 不 push**。它结束于本地合入 + 门禁全绿;推送需用户明确许可(项目既有纪律)。 -3. **`build` 与 `bench` 都必须串行**:UV4 不能两个实例同时 `-b`;串口 COM6、ST-LINK、 - 设备 IP `192.168.1.100` 各只有一份。**构建与烧录都不可并发。** - - `build` 锁由 **`@build` 自持自释**,调用方无需手工 acquire/release; - - `bench` 锁**必须手工**:`lock acquire bench --task ` → 烧录/`@test`/探针 → - `lock release bench --task --bench-pass`。非登记持有者(lead 的验收窗、插件树 `pw:`)占了锁 - 怎么放 → B 节(2026-10-29 起无台账也只 warn、照常释放);插件树整体口径 → F 节。 -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` 一起删掉。手工拆任何一棵树之前先跑 **E 第 3 步**的预扫,拿到 `REPARSE: none` 才动手。 -6. **活跃任务硬上限 8**(脚本常量 `MAX_CONCURRENT = 8`,每棵约 83 MB 检出——du 口径,内容字节和 84.0 MiB → `docs/环境与部署/并行任务工作树方案.md` §4)。满了 `new` 直接 exit 2, - 腾位顺序与 `--force` 的代价 → A 节;悬挂台账的清理与复验 → **E 第 6 步**。 - **改上限 = 只改 `task_worktree.py` 里那一行常量**(`MAX_CONCURRENT`,:30):报错文案「已有 N 个活跃任务(上限 8)」 - 与 `new` 打的「另有 N 个 DSH 插件工作树(`.dsh-worktrees/`,不计入上限 8)」提示都是 `.format(…, MAX_CONCURRENT)` - 插值该常量(实测 `task_worktree.py:764-766` / `:768-771`)——AGENTS.md、方案文档、本技能 frontmatter 与 A 节里的 - 数字**全是引用同步**,不是第二处事实源。全形态扫描(`最多 N` / `N 棵` / `三棵` / `第 N+1 棵` / `白占 N 个名额` / - `MAX_CONCURRENT = N` / 硬上限)与「上限真生效」的隔离小仓验证(`REPO_ROOT` 由 `__file__` 反推 → 脚本必须按同一 - 目录深度放,才能测到同级 `_wt/`)→ `stale-doc-count-sweep` skill;每棵树的磁盘代价按实测口径重述,别照抄旧数。 -7. **`@task` 的工作树放仓库同级 `_wt/`,绝不在仓库内部**——否则污染 `git status`、`pre-commit --all-files` - 与全库搜索。唯一例外 = DSH worktree 插件写死的仓内 `.dsh-worktrees/`(改不了;已用「预置 .gitignore + - 三个全库扫描门禁排除」兜住,见 **F 节**)。自己新建的树一律走 `_wt/`。 -8. **`done` 是合并点**:属主没提交完就别 `done`;合并提交一旦进了主干,只用「主工作树补提交」收尾, - **不用 `reset --hard` 反悔** → C 节。 -9. **非登记 / detached 的树绝不跑 `done` / `abort`**:两者都以台账为前置,无台账直接 exit 2;带着残留台账时 - `abort --force` 会绕过内容保全把别人的在飞改动连树一起删。回收走 **E 节**(全景 → 分桶 → 联接预扫 → 备份再拆 → - 复验 + 清悬挂台账)。 +1. ** +`done` + 只能在主工作树执行**——主干分支检出在主树,git 不允许同一分支两处检出。 +2. ** +`done` + 不 push**:它结束于本地合入 + 门禁全绿;推送需用户明确许可。 +3. ** +`build` + 与 +`bench` + 都必须串行**(UV4 不能两个实例同时 +`-b` +;COM6、ST-LINK、设备 IP +`192.168.1.100` + 各一份)。 +`build` + 锁由 **@build 自持自释**( +`keil_build.py` +)→ 调用方**不要**手工 acquire/release; +`bench` + 锁**必须手工** acquire → 烧录/ +`@test` + → release +`--bench-pass` +。非登记持有者怎么放 → B 节;插件树 → F 节。 +4. **单写者文档**: +`docs/任务规划/` +(整个文件夹)、 +`AGENTS.md` + 任务表块、 +`MDK-ARM/*.uvprojx` +、 +`.pre-commit-config.yaml` +、内存快照 —— 任务期**不在任务树里改**,留到 +`done` + 统一处理;用户显式豁免时(在任务树写、由 done 合回)要在交付报告里点名这条例外。 +5. **清场必须先摘联接**:脚本先 +`os.rmdir` + 掉 +`.venv` + / +`simulator/SDL2` + 联接再 +`git worktree remove` +。手工拆树前必跑 **E 第 3 步**预扫拿到 +`REPARSE: none` +——否则 git 可能递归进联接把主树 +`.venv` + 一起删掉。 +6. **活跃任务硬上限 8 = 脚本常量 `MAX_CONCURRENT`(`task_worktree.py:30`)——唯一事实源**:报错文案与 `new` 的插件树提示都插值它,AGENTS.md / 方案文档 / frontmatter / A 节的数字全是引用同步。改这个数字 → 全形态扫描走 `stale-doc-count-sweep` skill + 隔离小仓验证上限真生效;每棵磁盘代价按实测口径重述、别照抄旧数(细节 → 验证清单末条)。满了 exit 2 → A;悬挂台账 → E 第 6 步。 +`MAX_CONCURRENT` +( +`task_worktree.py:30` +)——唯一事实源**:报错文案与 +`new` + 的插件树提示都插值它,AGENTS.md / 方案文档 / frontmatter / A 节的数字全是引用同步。改这个数字 → 全形态扫描走 +`stale-doc-count-sweep` + skill,并另建隔离小仓验证上限真生效;每棵磁盘代价按实测口径重述(du 约 83 MB / +`--lean` + 约 45 MB,方案文档 §4)别照抄旧数。满了 +`new` + 直接 exit 2 → A 节;悬挂台账 → E 第 6 步。 +7. **工作树放仓库同级 +`_wt/` +,绝不在仓库内部**(否则污染 +`git status` +、 +`--all-files` + 与全库搜索)。唯一例外 = 插件写死的仓内 +`.dsh-worktrees/` +(改不了,已用预置 .gitignore + 三个全库扫描排除兜住 → F 节)。 +8. ** +`done` + 是合并点**:属主没提交完就别跑;合并提交进了主干就只用「主树补提交」收尾,**不用 +`reset --hard` +** 反悔 → C 节。 +9. **非登记 / detached 的树绝不跑 +`done` +/ +`abort` +**:两者都以台账为前置(无台账 exit 2),带残留台账时 +`abort --force` + 会绕过内容保全把别人在飞的改动连树一起删 → E 节。 -## 陷阱(实测) +## 陷阱与验证(收工前各扫一遍) -> 详情已外置(本文件须留头量给自进化:dsh-mnemon 写侧上限 65536 字节,超限的技能再也生不成草案)。全文见 [reference/pitfalls.md](reference/pitfalls.md) —— 「陷阱(实测)」逐条清单原文。 - -## 验证 - -> 详情已外置(本文件须留头量给自进化:dsh-mnemon 写侧上限 65536 字节,超限的技能再也生不成草案)。全文见 [reference/verification.md](reference/verification.md) —— 「验证」清单原文。 +- [reference/pitfalls.md](reference/pitfalls.md): +`done` + 的门禁跑在 +`@build` + 之前 · 门禁 = +`pre-commit run --all-files` +- [reference/pitfalls.md](reference/pitfalls.md):全部实测坑逐条(含 `--dry-run` 不保护清场段、门禁跑在 `@build` 之前、未跟踪文件不留痕、并集合并残留、新树从未构建 → 体积类门禁必然红)。收工前扫一眼。 +`git log` + 与合并里都不留痕 · 并集合并残留 · 新树从未构建 → 体积类门禁必然红 · +`--force` + 只覆盖四类检查 · 联接不是 symlink( +`core.symlinks=false` +)· 锁不绑进程存活。 +- [reference/verification.md](reference/verification.md):每节要留的证据(预演 sha + +- [reference/verification.md](reference/verification.md):每节要留的证据(预演 sha + `MT_RC=0`、blob 对账、两把锁空闲、禁词计数 0、`REPARSE: none`、改上限数字后的全形态扫描)。 +、blob id 对账、两把锁空闲、禁词计数 0、 +`REPARSE: none` +…)。 ## 与其它 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` 双端注册) -- **DSH worktree 插件**(人类命令 `/worktree`、模型工具 `worktree_create`,树在仓内 `.dsh-worktrees/`) - 与本 skill 的分工、桥接命令与锁归属 → F 节(本 skill 不删插件树,插件也不删 `_wt/`) -- `.map` 口径 / RAM 台账与趋势 → `mem-budget` skill;UI 回归 → `ui-verify` skill -- 完整机制与目录分配 → `docs/环境与部署/并行任务工作树方案.md` +- 构建 = `keil-build`(`@build`)· 烧录/回读 = `stlink-flash` · 串口闭环 = `serial-boot-test`(真实 hex 路径以 `.serialtest/config.json` 的 hex 键为准,拷错 = 烧旧镜像) +`keil-build` +( +`@build` +)· 烧录/回读 = +`stlink-flash` + · 串口闭环 = +`serial-boot-test` +(真实 hex 路径以 +`.serialtest/config.json` + 的 hex 键为准,实测 +`MDK-ARM/Objects/STM32F407-Demo.hex` + 不存在,拷错 = 烧旧镜像) +- 新 +`.c` + 双端注册 = +`new-source-file` + · RAM/.map 台账 = +`mem-budget` + · UI 回归 = +`ui-verify` + · 计数同步 = +`stale-doc-count-sweep` + · 卡死的技能台账行 = +`mnemon-skill-ledger-stuck-repair` +- DSH worktree 插件( +`/worktree` + / +`worktree_create` +,仓内 +`.dsh-worktrees/` +)与本 skill 的分工、桥接命令、锁归属 → F 节 · 完整机制与目录分配 → +`docs/环境与部署/并行任务工作树方案.md` diff --git a/.dsh/skills/task-worktree/reference/clean-room-measure.md b/.dsh/skills/task-worktree/reference/clean-room-measure.md index 4e1e0ec9..bd484b37 100644 --- a/.dsh/skills/task-worktree/reference/clean-room-measure.md +++ b/.dsh/skills/task-worktree/reference/clean-room-measure.md @@ -10,14 +10,14 @@ detached HEAD 的干净工作树当净室,一次只往里注入一份在飞文 1. **建净室 + 先落基线数**(路径必须**在仓库目录之外**;铁律 7 对临时树同样成立——放仓内就污染 `git status` / `pre-commit --all-files` / 全库搜索): -@T3@bash +```bash MAIN="D:/Code/DTU Code/STM32F4-Base" # 主工作树绝对路径(含空格 → 变量整体加引号) WT="$MAIN/../_wt/und-attr" # 仓外同级 _wt/ 下;一次测量固定用同一个名字 rm -rf "$WT" 2>/dev/null git -C "$MAIN" worktree add --detach "$WT" HEAD >/dev/null 2>&1 || { echo WT_FAIL; exit 9; } git -C "$WT" status --porcelain # 应为空 "$PY" "$WT/scripts/archive/id_gate/check_ids.py" 2>&1 | tail -25 # ← 口径:id-check 钩子已于 2026-10-28 退役(脚本移入 scripts/archive/id_gate/),按该脚本自身默认参数跑;基线与注入用**同一条命令** -@T3@ +``` - `--detach`(**不要 `-b`**):净室没有分支、不写 `_wt/.state/.json` 台账 → **不占 `MAX_CONCURRENT = 8` 名额、不出现在 `$TW list`**,也没人会被误指去对它跑 `done`(建与拆都不走 `$TW new` / `$TW done`)。 @@ -27,14 +27,14 @@ git -C "$WT" status --porcelain # 应为空 看清会删什么,只挪走多出来的那些再测;语料被污染出来的数字与 CI 不可比。 2. **一次只注入一份在飞文件 → 跑同一个扫描器 → 与基线差分**(得到的是每个文件的**独立**贡献,不是合计): -@T3@bash +```bash for f in "App/log/sys_log_mcu.c" "test/host/sys_log_mcu/sys_log_mcu_test.c" "docs/子系统设计/运行与自检记录设计说明.md"; do mkdir -p "$WT/$(dirname "$f")" cp "$MAIN/$f" "$WT/$f" # 拷进净室;主树那份只读、一字节不动 echo "== $f =="; "$PY" "$WT/scripts/archive/id_gate/check_ids.py" 2>&1 | tail -25 # ← 与第 1 步同一条命令 git -C "$WT" checkout -- "$f" # 只还原净室里自己刚拷进来的那份 done -@T3@ +``` 实测:`run_par.c` / `board_ui_nav.py` / `check_nested_comment.py` / keil `SKILL.md` **各 +0**, 而 log 道那三个文件 **+6 / +8 / +2**,合计**正好 +16**。**Σ(逐文件增量) ≠ 观察到的涨幅 → 还有没被注入的 @@ -45,11 +45,11 @@ done 全部落进 `skip_bare_unmanaged` —— 到这一步结论才配发给属主。 4. **闭合 + 复验主树一个字节没被污染**: -@T3@bash +```bash git -C "$MAIN" worktree remove --force "$WT" # 净室里只有自己的拷贝、没有原件,--force 安全 git -C "$MAIN" worktree list # 不该再有 und-attr git -C "$MAIN" status --porcelain | sort # 与测量前**逐行一致**(测量前先存一份基线清单) -@T3@ +``` ⚠ **上面第一行的前提是有条件的**:树里混进了别人的在飞 / 未跟踪内容时「只有自己的拷贝」就不成立了, `--force` 直接拆等于替别人把唯一版本删掉 → 先走 **E 节**(全景 → 逐文件分桶 → 联接预扫 → 唯一残留先备份), diff --git a/.dsh/skills/task-worktree/reference/dsh-plugin-bridge.md b/.dsh/skills/task-worktree/reference/dsh-plugin-bridge.md index 773d4ec5..6b02b081 100644 --- a/.dsh/skills/task-worktree/reference/dsh-plugin-bridge.md +++ b/.dsh/skills/task-worktree/reference/dsh-plugin-bridge.md @@ -30,11 +30,11 @@ 三步接上(`` 可带 `worktree/` 前缀,也可多段如 `refactor/logging`): -@T3@bash +```bash "$PY" "$TW" plugin list # 一览:分支 / 脏 / 缺哪个联接(缺项另起一行说后果) "$PY" "$TW" plugin adopt N21-dhara-isfree # 只补缺失的 .venv 与 simulator/SDL2 联接 "$PY" "$TW" plugin unadopt N21-dhara-isfree # 只摘联接(os.rmdir),绝不动目标内容与树里文件 -@T3@ +``` - **两条联接坏了的样子完全不同**:缺 `.venv` → `ci_local.py` 的四级解释器回退**静默**落到机器级 python(无 fontTools = `generated-fresh` 假红的老根因,AGENTS.md 记着);缺 `simulator/SDL2` → diff --git a/.dsh/skills/task-worktree/reference/gates-in-tree.md b/.dsh/skills/task-worktree/reference/gates-in-tree.md index 785d26e2..70538fc4 100644 --- a/.dsh/skills/task-worktree/reference/gates-in-tree.md +++ b/.dsh/skills/task-worktree/reference/gates-in-tree.md @@ -20,11 +20,11 @@ (扫描根就是那一个文件,联接目录不在面内)。主树本身没有 SDL2 时不产生这类假红(联接如实镜像主树, F 节那棵树连 SDL2 都没得联),但**别据此认为 `--all` 在树内可信**。 -@T3@bash +```bash F="docs/<子目录>/<本次改动的文件>.md" # 本次改动的那一个文件 "$PY" scripts/check_doc_links.py "$F" > ci-artifacts/dl-.txt 2>&1; echo RC=$? cat ci-artifacts/dl-.txt -@T3@ +``` 逐文件跑的实测读数:需求文档 161 处章节引用 / 98% 机器可判定 / rc=0;方案文档 16 处 / 88% / rc=0。 确实要在树内跑 `--all` 时(例如怀疑主干基线本身),判红顺序**固定三步**,缺一步都不许下结论: @@ -47,13 +47,15 @@ cat ci-artifacts/dl-.txt `ci-artifacts/`(被忽略 → 不污染脏行数),UTF-8 无 BOM、CR=0、标题 ≤100 字符、正文每行 ≤80(实测标题 31 / 32 字符、 正文最长行 50 / 55)——`-m` 会把中文按终端代码页转码搞坏,长中文标题还容易撞 gitlint。硬自检: -@T3@bash +```bash git log -1 --pretty=%B | grep -c '485' # 被清退主题的关键词:必须是 0 git log -1 --pretty=%B | grep -ci '\bDE\b' # 同主题的次要关键词:同样 0 -@T3@ +``` 关键词不许出现在**标题、正文、合并信息**任何一处(实测标题改写成「删除与本方案无关的其它串口通道对照内容」这种 语义短语替掉了它)——否则「别让我看到 X」这条要求在 `git log` 里当场破产。合并信息是另一份文件: `ci-artifacts/merge-msg-.txt` 落盘后**同样 grep 一遍**再 `git merge --no-ff --no-edit -F`(C2 第 3 步)。 自审发现消息里的行数 / 统计算错而**尚未推送** → `git commit --amend -F <改好的文件>` 就地修(实测就这么修掉一次 off-by-one);**别追加一笔「更正」提交**——那等于把禁词和被更正的错数一起写回历史。 +6. **合并提交期 pre-commit 只查冲突文件(2026-10-29 实测)**:合并那一次提交的钩子日志首行是 `[INFO] Checking merge-conflict files only.`,其余钩子全打 `(no files to check) Skipped` —— 于是「合并提交通过了 30 项钩子」这句话**只覆盖冲突文件**,别拿它当全量绿。正解:合并落主干后立刻在主工作树跑全量 `scripts/ci_local.py`(静态 + 提交信息 + 主机侧单测 + manual 段),实测全量这一步才会抓到「表格断列」「章节引用悬空」这类跨文件问题。合并解冲突时若上游把 Markdown 表格重排过,注意**单元格里的竖线必须写成反斜杠竖线**(先例 `docs/子系统设计/运行与自检记录设计说明.md` 的 `FA_WRITE\|FA_SEEKEND`),否则 `FA_WRITE|FA_OPEN_APPEND` 会被当列分隔符打断,而 `doc-links` 不查表格列数——要自己数(第 4 条)。 +7. **跨层改动拆不成「按层分笔」提交(2026-10-29 实测,四笔改一笔)**:钩子看到的世界 = HEAD + 本笔 staged——pre-commit 会 stash 已跟踪文件的未暂存改动,而**未跟踪文件不在 `git diff-index` 里、不会被藏**(`git add -N` 也救不了,pre-commit 会把它撤下索引、文件仍在盘上)。后果:把双端登记(`MDK-ARM/*.uvprojx` / `simulator/CMakeLists.txt`)、生成器产物、互锁引用的文档分到别的笔里,本笔必被 `doc-links`(引用了还没入册的小节)、`registration-check`(源文件登记在另一笔)、`generated-fresh`(`#page=` 找不到页面描述符、字库清单与源不一致)、`mem-numbers`(受管区还是旧 .map 的数)四道**整仓**门禁一起拦。判据:**一笔提交必须自洽**——互锁的改动(代码 + 登记 + 生成物 + 文档)走同一笔;机制与三条处置结论(半成品要么先落地要么先挪出工作树)→ `docs/代码规范/pre-commit使用说明.md` §2「stash 覆盖面」。 diff --git a/.dsh/skills/task-worktree/reference/merge-dirty-main-tree.md b/.dsh/skills/task-worktree/reference/merge-dirty-main-tree.md index 73e7c65f..27ecfe14 100644 --- a/.dsh/skills/task-worktree/reference/merge-dirty-main-tree.md +++ b/.dsh/skills/task-worktree/reference/merge-dirty-main-tree.md @@ -11,7 +11,7 @@ stash / checkout / reset / 删除(项目级 Never:丢弃别人的工作) 1. **先取证再动手**(别一看见脏就下「谁弄坏的」结论)。记下它的 mtime 与 diff 内容,判断与本合并有无交集, 并把**合并前**的 blob id 与 `diff --stat` 存进 `ci-artifacts/`(第 4 步拿它对账): -@T3@bash +```bash MAIN="D:/Code/DTU Code/STM32F4-Base" EXT=".dsh/skills/serial-boot-test/scripts/run_boot_test.py" # 外部脏文件的仓库相对路径 mkdir -p ci-artifacts @@ -20,7 +20,7 @@ stat -c "%y %s %n" "$MAIN/$EXT" # mtime + 字节数:定性是 git -C "$MAIN" diff -- "$EXT" | cat # 内容性质;与本次合并有无同文件同区域 git -C "$MAIN" hash-object "$MAIN/$EXT" | tee ci-artifacts/ext-blob-before.txt # ← 存下这个 blob id git -C "$MAIN" diff --stat -- "$EXT" | tee ci-artifacts/ext-stat-before.txt # ← 以及这份统计(几加几减) -@T3@ +``` 实测:mtime 落在另一会话的活跃时段内,diff 是「docstring 引号改写」2+/2−,与要合的文档毫无交集 → 定性为**与本合并无交集、不丢弃**。顺手 `git -C "$MAIN" worktree list` 看一眼有没有**别人中途新建的任务树** @@ -28,9 +28,9 @@ git -C "$MAIN" diff --stat -- "$EXT" | tee ci-artifacts/ext-stat-before.txt 2. **`merge-tree` 预演:先证明能干净合并,再碰主干**。`$TW done --dry-run` 在这条路上同样被前置挡住, 预演只能自己敲(git 2.38+ 的新语法,打印的是**结果树 sha**): -@T3@bash +```bash git -C "$MAIN" merge-tree --write-tree v2-on-ui task/-; echo MT_RC=$? -@T3@ +``` 判据 = **打印出一个 tree sha 且 RC=0**(实测打出 `4220906b…` + `MT_RC=0` 才敢往下走)。⚠ 本机 git 太旧没有 这个子命令时退回 `$TW done --dry-run`——它的前置也是「主树干净」,可用环境变量 `DTU_INTEGRATION_WORKTREE` @@ -39,9 +39,9 @@ git -C "$MAIN" merge-tree --write-tree v2-on-ui task/-; echo MT_RC=$? 3. **主工作树里手工 `--no-ff` 合并**(合并信息走 `-F` 文件,放在被忽略的 `ci-artifacts/`;编码口径同 C 第 5 步: UTF-8 无 BOM、正文每行 ≤80 字符;标题格式 `merge: task/- 合入 `): -@T3@bash +```bash git -C "$MAIN" merge --no-ff --no-edit -F ci-artifacts/merge-msg-.txt task/- -@T3@ +``` pre-commit 会**自己**把那份未提交改动 stash 起来、跑完钩子再放回(输出里能看到 `[INFO] Stashing unstaged files to …`) ——**这条不是让你去手工 stash**,正好相反:钩子的 stash 往返让它自己走完,你只需在第 4 步验证它把东西原样还回来了。 @@ -52,12 +52,12 @@ git -C "$MAIN" merge --no-ff --no-edit -F ci-artifacts/merge-msg-.txt task 4. **自证那份外部改动原样还在**(本节唯一的硬证据,两次实测都靠它结案):合并后**再跑一次**同一对命令, 与第 1 步存的两个值**逐项相同**: -@T3@bash +```bash git -C "$MAIN" hash-object "$MAIN/$EXT" | tee ci-artifacts/ext-blob-after.txt git -C "$MAIN" diff --stat -- "$EXT" | tee ci-artifacts/ext-stat-after.txt diff ci-artifacts/ext-blob-before.txt ci-artifacts/ext-blob-after.txt # 空输出才算过 git -C "$MAIN" status --porcelain # 该文件仍是 M:没被顺手提交,也没被清成干净 -@T3@ +``` 实测:合并前后 blob id 同为 `369380ca8eb024b593d872455b6a020797abcd8d`、`diff --stat` 同为 2+/2− → 证明 pre-commit 的 stash 往返没动别人的工作;合进主干的那份文档另按 G 节复核(零残留 + 全库基线)。 @@ -66,10 +66,10 @@ git -C "$MAIN" status --porcelain # 该文件仍是 M:没被顺 5. **收工仍走技能自身的代码路径,别自己手写 rm**:先把台账**备份成 `*.state-bak`**(后缀与 E 第 6 步统一), 再让脚本自己摘联接: -@T3@bash +```bash cp "$WTROOT/.state/.json" "$MAIN/ci-artifacts/.json.state-bak" # 先备份再动 "$PY" "$TW" done # 打「已合入 ,跳过合并,直接清场」→ 先摘 .venv 与 simulator/SDL2 联接 → 删工作树 → 删分支 → unlink 台账 -@T3@ +``` 实测这条路径能走完(日志:`state backup -> ci-artifacts\screenlink-doc-history.json.state-bak` + `[ok] 已摘除联接 .venv` + `[ok] 已摘除联接 simulator/SDL2`)。**若它仍拿「主工作树不干净」挡在合并前**(没走到 diff --git a/.dsh/skills/task-worktree/reference/pitfalls.md b/.dsh/skills/task-worktree/reference/pitfalls.md index 06e9126d..8b5954f2 100644 --- a/.dsh/skills/task-worktree/reference/pitfalls.md +++ b/.dsh/skills/task-worktree/reference/pitfalls.md @@ -115,3 +115,4 @@ 进入 acquire、`finally` 无条件 release,所以"Agent 忘了 release、锁一直挂到 TTL"这条在构建路径上已消除。 `done` 的门禁自己持锁并给子进程设 `DTU_LOCK_HELD=build` 告知跳过(新旧脚本都兼容)。 **`bench` 锁仍须 Agent 手工持**(会话作用域:跨烧录→`@test`→探针,见 `keil-build` SKILL.md)。 +- **⚠ `done --dry-run` 只保护预演段、不保护清场段**(2026-10-29 实测):`done --dry-run --skip-build --skip-gates` 在「分支已合入主干」这条路上**照样执行清场**——摘 `.venv` / `simulator/SDL2` 联接、删分支、清台账,然后打「完成(分支此前已合入)」。想真·零改动就只跑不带 `--skip-gates` 的 `--dry-run` 并停在预演输出;用完一律 `git worktree list` 复验(判完成以 `git log` 为准,不是以有没有再敲一次 `done` 为准)。另:`--dry-run` 的前置检查同样要抢 `build` 锁——被别的任务占着时它会直接报「独占资源 build 仍被 X 占用,先 release」,此时加 `--skip-build` 或先 release,**别为了预演去抢别人的锁**。 diff --git a/.dsh/skills/task-worktree/reference/reclaim-unregistered.md b/.dsh/skills/task-worktree/reference/reclaim-unregistered.md index ca9ef10c..9b4a36a7 100644 --- a/.dsh/skills/task-worktree/reference/reclaim-unregistered.md +++ b/.dsh/skills/task-worktree/reference/reclaim-unregistered.md @@ -14,7 +14,7 @@ HEAD 是 detached(`git rev-parse --abbrev-ref HEAD` 打 `HEAD`);或树里 1. **建全景(全程只读)**:先看 `git worktree list` 的全集,再**逐棵**记三件事(脏行数含未跟踪 / 树顶提交 / 有无分支) 加一条台账判定,同时存下主树的拆前基线清单: -@T3@bash +```bash MAIN="D:/Code/DTU Code/STM32F4-Base"; WTROOT="$MAIN/../_wt" git -c core.quotePath=false worktree list # 全集:登记树 + 非登记树都在这份里 MAIN_SHA=$(git -C "$MAIN" rev-parse --short HEAD) # 下面所有比对都以这一版为主线尺子 @@ -28,7 +28,7 @@ for t in "$WTROOT"/*/; do git -C "$t" rev-parse --abbrev-ref HEAD # 打 HEAD = detached,压根没分支可 done ls "$WTROOT/.state/$(basename "$t").json" 2>/dev/null || echo "NO-LEDGER" # 台账位置随 DTU_WT_ROOT done -@T3@ +``` - **`-uall` 不是可选**:默认 `status --porcelain` 会把未跟踪目录折叠成一行 `dir/`,`-uall` 才展开到文件级;少了它 第 2 步的分桶就漏项(实测 `und-attr` 唯一那份就是未跟踪 `?? scripts/check_nested_comment.py`)。 @@ -39,7 +39,7 @@ done 内容,拆 = 丢)/ `OLDER-DRAFT`(同路径主线已有更新版本)。落成独立只读探针跑(别用 heredoc 现写现改, 见陷阱「别用 heredoc 打补丁造探针」): -@T3@python +```python # ci-artifacts/wt_bucket_scan.py — 只读:逐文件与主线工作区/HEAD 比对,三桶归类 import os, subprocess, tempfile MAIN = r"D:\Code\DTU Code\STM32F4-Base" @@ -87,12 +87,12 @@ for name in TREES: for b in ("UNIQUE-UNMERGED", "OLDER-DRAFT", "OTHER"): for rel, why in tally.get(b, []): print(f" {b}: {rel} # {why}") -@T3@ +``` 报告里必须**逐桶给计数 + 判定证据**(实测:`verify-task16` IN-MAINLINE=6 / UNIQUE=0;`verify-batch` IN-MAINLINE=35 / UNIQUE=1;`und-attr` IN-MAINLINE=0 / UNIQUE=1)。两个非空桶再各钉一条命令把证据坐实: -@T3@bash +```bash # 「唯一未合并」是真更新还是更旧草稿?blob 级 diff + 逐行看方向 git -c core.quotePath=false -C "$MAIN" diff --stat "$MAIN_SHA" -- simulator/tests/README.md diff <(git -C "$MAIN" show "$MAIN_SHA":simulator/tests/README.md) "$WTROOT/verify-batch/simulator/tests/README.md" @@ -100,7 +100,7 @@ diff <(git -C "$MAIN" show "$MAIN_SHA":simulator/tests/README.md) "$WTROOT/verif for c in 77779d49 1d4b6a17; do git -C "$MAIN" merge-base --is-ancestor "$c" HEAD && echo "$c MERGED" || echo "$c NOT-merged" done -@T3@ +``` 实测:那份 `simulator/tests/README.md` 树内写「27 页 × 3200 B」、主线 HEAD 已是「60 张 × 3200 B」→ **更旧草稿**(被取代的 早先版本),不是未合并成果;`bench-21-77779d49` / `bench-23-1d4b6a17` 两棵 `merge-base --is-ancestor` 判 **MERGED** 且脏数 = 0 @@ -109,7 +109,7 @@ done 还得看未跟踪那一层。`UNIQUE-UNMERGED` 非空又确实要留 → 它进第 4 步的备份,**不许靠 `--force` 顺手带走**。 3. **拆前扫联接 / reparse point(物理安全性 = 铁律 5 的可执行判据)**。存成 `ci-artifacts/wt_junction_scan.py` 再跑: -@T3@python +```python # 只读预扫:一个路径是不是 reparse point(Windows 目录联接在这里现形);发现即停止深入,绝不 walk 进联接 import os, ctypes FILE_ATTRIBUTE_REPARSE_POINT = 0x400 @@ -131,7 +131,7 @@ for name in ["verify-task16", "verify-batch", "und-attr", "bench-21-77779d49", " print(" REPARSE: none → 可直接 git worktree remove --force" if not hits else " REPARSE:\n " + " ".join(hits)) -@T3@ +``` 实测输出 `== verify-task16 ... REPARSE: none`(每棵的体积 / 条目数 + 零联接就是证据),而 `bench-*` 两棵各挂一个 `.venv` 联接 → 判 **KEEP**(本节不拆它们;将来真要拆,先 `rmdir` 摘掉联接再 remove)。 @@ -144,7 +144,7 @@ for name in ["verify-task16", "verify-batch", "und-attr", "bench-21-77779d49", " `ci-artifacts/wt-leftover-backup/`(`ci-artifacts/` 已被 `.gitignore` 忽略 → **备份不会改变主树脏行数**;仓库根不放一次性产物), **文件名 + 字节数必须打进输出**当证据: -@T3@bash +```bash BK="$MAIN/ci-artifacts/wt-leftover-backup"; mkdir -p "$BK" # 命名约定:<树名>__<原路径里的 / 换成 _>,反向可定位;台账备份统一后缀 .state-bak cp "$WTROOT/verify-batch/simulator/tests/README.md" "$BK/verify-batch__simulator_tests_README.md" @@ -154,25 +154,25 @@ for t in verify-task16 verify-batch und-attr; do git -C "$MAIN" worktree remove --force "$WTROOT/$t"; echo "$t remove_rc=$?" # 不接管道才读得到码(见「读输出」) done git -C "$MAIN" worktree prune # 顺带清 .git/worktrees 里的残籍 -@T3@ +``` 自检:**备份条目数 = 第 2 步里判「要留档」的条目数**,且每个都带字节数。这一步就是本节相对 D 第 4 步的增量—— D 的前提是「净室里只有自己的拷贝」,本节的树里混着别人在飞的东西,`--force` 是一次不可逆删除,**先备份才谈得上安全**。 5. **复验:主树一个字节没动 + 树确实没了**: -@T3@bash +```bash git -C "$MAIN" status --porcelain -uall > ci-artifacts/wt-main-after.txt wc -l < ci-artifacts/wt-main-before.txt; wc -l < ci-artifacts/wt-main-after.txt # 两个数必须相同(实测 0 → 0) diff ci-artifacts/wt-main-before.txt ci-artifacts/wt-main-after.txt # 空输出 = 逐行一致 git -c core.quotePath=false -C "$MAIN" worktree list # 已拆路径逐条消失;主树与判「保留」的树原样在列 -@T3@ +``` 行数不同 = 备份或摘除碰到了主线(多半是备份写进了未被忽略的目录,或漏摘联接)→ 立刻定位,别报「回收完成」。 第 2 步判「保留不动」的树还在 `worktree list` 里是**预期结果**,在报告里点名解释为什么留(附 `MERGED` 判据)。 6. **顺手清悬挂台账**(branch / path 都已不存在的 `_wt/.state/.json`;它会让 `$TW list` 打出一行怪条目、并让下次 `new` 同 ID 报「任务 已存在」)。先给证据、后动手: -@T3@bash +```bash for j in "$WTROOT"/.state/*.json; do # 台账位置随 DTU_WT_ROOT id=$(basename "$j" .json) br=$("$PY" -c "import json,sys;print(json.load(open(sys.argv[1],encoding='utf-8')).get('branch',''))" "$j") @@ -181,19 +181,19 @@ for j in "$WTROOT"/.state/*.json; do # 台账位置随 DTU_WT_ROOT [ -d "$pp" ] && p=path-OK || p=path-MISSING echo "$id $b $p <- $j" done -@T3@ +``` 实测这条判据打出的正是 `v5tool2 branch-MISSING path-MISSING`:`git rev-parse task/v5tool2-v11` 报 `fatal: ambiguous argument 'task/v5tool2-v11': unknown revision ...`,台账 `path` 指的目录已无,`git worktree list` 里也没有这棵树。 **三条齐(rev-parse fatal + path 目录不存在 + worktree list 无此树)才叫悬挂**;缺任何一条都回到第 1~2 步查是不是真有东西要保。 然后**备份 → 删 → 复跑两条 list 复验**: -@T3@bash +```bash cp "$WTROOT/.state/v5tool2.json" "$BK/v5tool2.json.state-bak" && rm "$WTROOT/.state/v5tool2.json" "$PY" "$TW" list # 该条目消失(全空时打「(没有任务空间。用 new 建一个)」),echo RC=$? 应为 0 "$PY" "$TW" lock status # build 空闲(TTL 1.0h)/ bench 空闲(TTL 6.0h)—— 证明清台账没牵连两把锁 ls "$WTROOT"/.state/locks/ 2>/dev/null # 若该 ID 名下还有 .lock,先按 B 节释放,再删台账 -@T3@ +``` - **必须 `cp` 先备份再 `rm`**:台账里存着 base / junctions / needs_bench / bench_pass,是真出纠纷时唯一的现场记录。 - 悬挂条目按定义 `path` 已不存在 → 通常**不占** `MAX_CONCURRENT = 8` 名额(A 第 1 步的「活跃」判据要求 path 目录还在), From e02f46ed6a5deebeadc01ef6bd6ab9e3bae77a86 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Fri, 30 Oct 2026 00:50:21 +0800 Subject: [PATCH 17/19] =?UTF-8?q?docs(=E4=BB=BB=E5=8A=A1=E8=A7=84=E5=88=92?= =?UTF-8?q?):=20=E5=88=A0=E9=99=A4=E4=B8=A4=E4=BB=BD=E9=81=97=E7=95=99?= =?UTF-8?q?=E7=9A=84=2020261028=20=E5=88=86=E5=B7=A5=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 分工说明-20261028-RS485解锁与门禁口径对账.md - 分工说明-20261028-采集链与上送链联调.md 两份均为 2026-10-28 的一次性分工简报,任务线已收口、文件遗留未清, 用户确认为没用文件后删除。删除前核对引用面:docs/ 全量 *.md 完整扫描、 根 README/AGENTS/.gitea 均零引用;仅 ci-artifacts/(已忽略的一次性脚本 与日志)内有历史命中,不构成悬空链接。 --- ...工说明-20261028-RS485解锁与门禁口径对账.md | 44 ------------------- .../分工说明-20261028-采集链与上送链联调.md | 44 ------------------- 2 files changed, 88 deletions(-) delete mode 100644 docs/任务规划/分工说明-20261028-RS485解锁与门禁口径对账.md delete mode 100644 docs/任务规划/分工说明-20261028-采集链与上送链联调.md diff --git a/docs/任务规划/分工说明-20261028-RS485解锁与门禁口径对账.md b/docs/任务规划/分工说明-20261028-RS485解锁与门禁口径对账.md deleted file mode 100644 index 031566de..00000000 --- a/docs/任务规划/分工说明-20261028-RS485解锁与门禁口径对账.md +++ /dev/null @@ -1,44 +0,0 @@ -# 分工说明:RS-485 解锁与门禁口径对账(2026-10-28) - -**目标**:**A 线**补齐 `Drivers/BSP/RS485/` 两条未修契约缺陷,为 `rs485_task` 解除 **RS-485 接线门禁** 的技术前提;**B 线**把 `AGENTS.md` 与 `docs/门禁说明/README.md` 的 **钩子口径** 对账到实跑。 -**口径**:本文件只分工、**不改行为**;两条线 **不交叉编辑** 对方文件,跨线接口 **先在本文件登记** 再动手。 - -## A 线 · RS-485 驱动契约(负责人 A) - -- **涉及文件**:`Drivers/BSP/RS485/rs485.c`、`Drivers/BSP/RS485/rs485.h`、`App/task/rs485_task.c`(**新建**)、`test/host/rs485/`(**新建**) -- **交付物**: - 1. 修 **单缓冲包撕裂**:RX 改 **双缓冲 + 帧序号**,消费者一次取整帧,对齐 `tpafe5160_snapshot()` 的 **临界区原子快照** 语义(旧的两步取数在两步之间被抢占会混帧); - 2. 修 **TX 超时拉 DE**:超时路径 **先等 TC 再拉 DE**,避免尾字节被截;错误码走 `Drivers/BSP/bsp_err.h` 通道,返回值 **不得裸丢弃**(`bsp-err` 的 consume-result); - 3. `[计划] test/host/rs485/run_unit_test.py` 覆盖 **撕裂**、**TX 超时**、**DE 时序** 三例;新增 `.c` 须 **双端注册**(`MDK-ARM/STM32F407-Demo.uvprojx` + `simulator/CMakeLists.txt`,见 `new-source-file` skill); - 4. 结论回填 `[计划] docs/问题记录/RS485_Trap_Records.md`,并在 `docs/任务规划/暂缓-RS485.md` 写清 **解锁条件是否满足**(接线节奏仍受门禁约束,**不由本线决定**)。 -- **禁止**:`malloc/calloc/realloc/strdup/free`(`coding-standard` CODE-RULE-20);`osKernelStart` 之前调任何创建类 API;注释里写 **星号形态的路径**(`nested-comment` 9-D)。 -- **预算**:建任务 **前后各跑一次** `scripts/stack_report.py` 与 `scripts/ram_report.py`;**建任务/抬栈属 Ask first**。 - -## B 线 · 门禁与文档口径(负责人 B) - -- **涉及文件**:`AGENTS.md`、`docs/门禁说明/README.md`、`docs/代码规范/pre-commit使用说明.md` §2;`.pre-commit-config.yaml` **只读核对、不改**。 -- **交付物**: - 1. **口径 = YAML 解析后的 hook 条目数**:用 `& ".venv\Scripts\python.exe"` 解析 `.pre-commit-config.yaml` 取实数,与 `AGENTS.md` 的 **51** 做 **声明 / 文档 / 实跑** 三数同源核对(**注释掉的候选钩子不计**,直接 grep 声明行会多算 1); - 2. `docs/门禁说明/README.md` 的 **篇数 ↔ 钩子数** 差集清单,核对 **27 篇 + 1 指针** 是否仍成立; - 3. 有差异 **只改文档、不加钩子**(Lead 裁定:`test/host/` 41 套里只有 22 根专属钩子,**没有专属钩子 ≠ 没被跑**,余量由 `test-coverage` 总闸兜);新条目标识用 **语义短语**,**不再发字母+数字编号** → `docs/编号规范.md` §1。 -- **禁止**:手工全局替换改名/挪目录(走 `scripts/rename_ref.py`,先 dry-run);章节引用写成非紧邻形态(`doc-links` 带 `--sections-strict` 拦提交);在配置注释里复述那个 grep 字面量(文本计数再虚高 1)。 - -## 共用约束(A/B 都要守) - -- 改代码/改文档 **只用 Edit 精确替换**;**绝不**用 `Get-Content`/`Set-Content`/`cmd` 重写含中文的文件(`U+FFFD` 不可逆);带 BOM 的 `Drivers/BSP/UI/`、`App/cfg/cfg_file.c`、`Lib/lv_conf.h` 改完核对 `EF BB BF`(`bom-guard`)。 -- **字符串字面量纯 ASCII**,中文只进注释;行尾 **LF**(`.gitattributes` 已写死 `* text=auto eol=lf`);怀疑乱码先数 `EF BF BD` 字节,**不要 `git checkout`**。 -- 门禁 **只在提交时** 统一跑:`@build` + `& ".venv\Scripts\python.exe" scripts/ci_local.py` + `scripts/check_doc_links.py --all`,**不要中途反复跑**。 -- `git commit` / `git push` / 烧录 / CubeMX 重生成 / 登记簿删行销项 / 开 `ENABLE_DESTRUCTIVE_TESTS` —— **Ask first**。 - -## 验收(合并前) - -| 线 | 判据 | 证据 | -|---|---|---| -| A | RS485 两条 ❌ 清零,`rs485_task` 的技术门禁解除(**是否接线仍由用户裁定**) | `test/host/rs485` 全绿 + `MDK-ARM/build_log.txt` **0 错误 0 警告** | -| B | 声明 / 文档 / 实跑 **三数一致** | 解析计数输出 + `docs/门禁说明/README.md` 差集为空 | -| A+B | `registration-check`、`bsp-err`、`nested-comment`、`id-check` 基线 **只减不增** | `scripts/ci_local.py` 输出 + `scripts/archive/id_gate/id_known_backlog.json` 台账 | - -## 交接 - -- 两条线各自 **只回报**:**改了哪些文件、跑了哪些自检、剩余风险**;跨线接口(`rs485_frame_take()` 签名)变更 **先在本文件登记**。 -- `docs/任务规划/README.md` 的 **完成即销项** 由协调者在用户确认后统一做,**A/B 都不得自行删行**。 diff --git a/docs/任务规划/分工说明-20261028-采集链与上送链联调.md b/docs/任务规划/分工说明-20261028-采集链与上送链联调.md deleted file mode 100644 index 5217f906..00000000 --- a/docs/任务规划/分工说明-20261028-采集链与上送链联调.md +++ /dev/null @@ -1,44 +0,0 @@ -# 分工说明:采集链与上送链台架联调(2026-10-28) - -**目标**:把 **语义取数与有效性→全周傅氏相量→测频与遥测快照** 取数链与 **测频与遥测快照→104/UI** 上送链在同一次台架烧录里对齐,提交门禁 **0 错误 0 警告**。 -**口径**:本文件只分工,不改行为;任何一条线单独验收通过后合并,**不交叉编辑**对方文件。 - -## A 线 · 采集链(负责人 A) - -- **涉及文件**:`App/algo/adc_input.c`、`App/algo/adc_health.c`、`App/algo/sample_ring.c`、`App/algo/phasor.c`、`Src/freertos.c`(**只写 `USER CODE` 区**) -- **交付物**: - 1. `App/algo/README.md` 补 **「快照时序」** 一节:`tpafe5160_snapshot()` 临界区原子性、`adc_tick_irq_notify()` 的「消费者就绪」门(TIM3 比调度器早 ≈100 ms); - 2. `test/host/adc_input/run_unit_test.py` 增加 **帧序号回绕** 与 **双缓冲半帧** 两个用例; - 3. `docs/子系统设计/ADC采样节拍设计说明.md` §5 表格与代码实际节拍(**TIM3 PSC 83 / ARR 999 → 1 kHz + TRGO_UPDATE**)核对后回填差异。 -- **禁止**:`malloc/calloc/realloc/strdup/free`(门禁 CODE-RULE-20);跨转换器做 **相位/功率/功率因数** 运算(V0.1 硬件缺陷);`Drivers/BSP/` 里 include `App/` 的头(`layer-deps`)。 -- **取数只用** `adc_input_poll()` / `adc_input_raw()` **按语义**取值,**不写死下标**(`buf[2]=IA`、`buf[5]=UN`、`buf[6]=UA` 见 `docs/硬件平台/ADC通道映射说明.md`)。 - -## B 线 · 上送链(负责人 B) - -- **涉及文件**:`App/algo/meas.c`、`App/task/iec104_task.c`、`Drivers/BSP/UI/ui_data.c`(**带 UTF-8 BOM:改完核对 `EF BB BF`**)、`test/host/meas/run_unit_test.py` -- **交付物**: - 1. `App/algo/meas.c` 的 **200 ms 双区快照** 写一个 **单写者/多读者** 一致性说明,落到 `docs/架构设计/项目架构总览.md` 的跨层依赖表; - 2. `Drivers/BSP/UI/ui_data.c` 与 104 上送 **共用同一份上游**:补 `test/host/ui_data/run_unit_test.py` 的 **陈旧代次(generation)** 用例; - 3. 若新增 `.c`:`MDK-ARM/STM32F4-Base.uvprojx` + `simulator/CMakeLists.txt` **双端注册**(`new-source-file`)。 -- **禁止**:直接编辑 `Drivers/BSP/UI/` 之外的 `Src/`、`Inc/` 标记区外内容;把 `*.uvprojx` 的 UV4 抖动改动入库;改 `Lib/lv_conf.h` 不同步 `simulator/` 那份(`lv-conf-sync`)。 -- **UI 侧铁律**:字符串字面量 **纯 ASCII**,中文只进注释;注释里写路径 **不要出现星号形态**(`nested-comment` 9-D),写目录或用尖括号。 - -## 共用约束(A/B 都要守) - -- 改代码/改文档 **只用 Edit 精确替换**;**绝不**用 `Get-Content`/`Set-Content`/`cmd` 重写含中文的文件(`U+FFFD` 不可逆)。 -- 行尾 **LF**(`.gitattributes` 已写死 `* text=auto eol=lf`);怀疑乱码先数 `EF BF BD` 字节,**不要 `git checkout`**。 -- 门禁只在 **提交时** 统一跑:`@build` + `& '.venv\Scripts\python.exe' scripts/ci_local.py` + `check_doc_links.py --all`;**不要中途反复跑**。 -- 文档引用章节 **紧邻写法**:`docs/算法/采集与算法层设计说明.md` §10.1;改名/挪目录走 `scripts/rename_ref.py`(先 dry-run)。 -- `git commit` / `git push` / 烧录 / `CubeMX` 重生成 / 抬栈 —— **须用户明确指示**(抬栈前后各跑 `scripts/stack_report.py`)。 - -## 验收(合并前) - -| 线 | 判据 | 证据 | -|---|---|---| -| A | 语义取数与有效性→全周傅氏相量与 1 kHz 节拍一致,无半帧 | `test/host/adc_input` 全绿 + `docs/子系统设计/ADC采样节拍设计说明.md` | -| B | `meas` 双区 **104 与 UI 同源**,无撕裂 | `test/host/meas`、`test/host/ui_data` 全绿 + `docs/编号规范.md` 无新撞号 | -| A+B | 台架一次烧录 **`-Rst -Run` + 回读校验**(`No difference found`),串口日志无 `[ERR]` | `@test` 报告 + `MDK-ARM/build_log.txt` | - -## 交接 - -- 两条线各自 **只回报:改了哪些文件、跑了哪些自检、剩余风险**;跨线接口(`meas` 快照字段)变更 **先在本文件登记**再动手。 From cb2fb4f0955192915284113b7e772576cf5a40ba Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Fri, 30 Oct 2026 01:00:36 +0800 Subject: [PATCH 18/19] =?UTF-8?q?docs(skills):=20keil-build=20SKILL.md=20?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E2=80=94=E2=80=94frontmatter=20YAML=20?= =?UTF-8?q?=E5=86=92=E5=8F=B7=E9=99=B7=E9=98=B1=E4=B8=8E=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E5=88=A4=E6=8D=AE=E8=A1=A5=E5=85=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ⚠ frontmatter 写法判据:description/whenToUse 含 ASCII 冒号时必须整值加双引号, 否则 @deepseek-ai/dsh-skill-filesystem 的 parseFrontmatter()(yaml.parse)整份 加载失败——本文件曾中招。 - 补退出码 2(BUILD_ENV_ERROR / UV4.exe not found)判读:DSH 会话看不见用户级 DTU_KEIL_UV4 时的候选路径探测与单次调用 env 注入闭环;build 锁持有者名 (含插件树 pw: 前缀)判读。 --- .dsh/skills/keil-build/SKILL.md | 94 +++++++++++++++++++++++++++++---- 1 file changed, 84 insertions(+), 10 deletions(-) diff --git a/.dsh/skills/keil-build/SKILL.md b/.dsh/skills/keil-build/SKILL.md index b152efb1..1298287d 100644 --- a/.dsh/skills/keil-build/SKILL.md +++ b/.dsh/skills/keil-build/SKILL.md @@ -1,7 +1,7 @@ --- name: keil-build -description: Keil MDK-ARM 命令行编译闭环(自带独立脚本 keil_build.py,纯标准库,拷到任何 Keil 项目即用):UV4 -b 构建 → "0 错误 0 警告"判定 → build_log 定位报错。封装入口约定(PowerShell 与 bash/ops_bash 两种调用形态及各自的 exit 2 归属)、退出码判读、UV4 改写工程文件的入库陷阱、构建副产物「%SystemDrive% 字面量垃圾目录」的定性与清理/入库排除。 -whenToUse: 需要编译 Keil MDK 工程、跑 `@build` 验证代码改动是否 0 错误 0 警告、解读 UV4 构建日志与退出码 2/4,或在 bash / `ops_bash` 里执行 `@build` 报语法错(syntax error near unexpected token),或构建后 `git status` 冒出 `%SystemDrive%` 目录时。 +description: "Keil MDK-ARM 命令行编译闭环(自带独立脚本 keil_build.py,纯标准库,拷到任何 Keil 项目即用),覆盖 UV4 -b 构建、「0 错误 0 警告」判定、build_log 定位报错、PowerShell 与 bash 两种调用形态及各自的 exit 2 归属、退出码与 build 锁持有者名判读(含插件树的 pw: 前缀)、exit 2 时 UV4.exe 的候选路径探测与单次调用 env 注入闭环、UV4 改写工程文件的入库陷阱、构建副产物「%SystemDrive% 字面量垃圾目录」的取证、清理与入库排除。" +whenToUse: "需要编译 Keil MDK 工程、跑 @build 验证代码改动(只改 .c 注释也算改动)是否「0 错误 0 警告」、@build 报 BUILD_ENV_ERROR 与 UV4.exe not found(退出码 2;DSH 会话看不见用户级 DTU_KEIL_UV4)、解读 UV4 构建日志、退出码 2/4 或 build 锁持有者名(含 pw: 前缀),在 bash / ops_bash 里执行 @build 报 syntax error near unexpected token,或构建后 git status 冒出 %SystemDrive% 目录时。" --- # keil-build — Keil 命令行编译(独立可运行 skill) @@ -22,8 +22,71 @@ whenToUse: 需要编译 Keil MDK 工程、跑 `@build` 验证代码改动是否 - 后台跑(run_in_background: true)要自己收码,且别把脚本接进管道:实测 $? 取到的是 tail 的码(恒 0);要看报错行就单独 grep MDK-ARM/build_log.txt - 解析顺序:命令行 > 项目档案 .serialtest/config.json > 环境 > 探测;UV4 显式设了但路径无效 = 立即报错,不静默回退 - 退出码 0=PASS(0错0警) / 1=构建失败 / 2=环境错误 / 3=用法 / 4=build 锁被占用;2026-10-12 修正过 2 与 3 的归属,该篇末尾记了前后口径 +⚠ **改本文件 frontmatter 要么整值加双引号,要么一个 ASCII 冒号都别写**(2026-10-30 这份就因此整份加载失败)。 +DSH 用 `yaml` 包的 `parse()` 读 frontmatter(`@deepseek-ai/dsh-skill-filesystem` 的 `parseFrontmatter()`), +plain scalar 里出现 **ASCII「冒号 + 空格」**(例如把报错原文 `BUILD_ENV_ERROR: UV4.exe not found` 或 +`pw: 前缀` 直接写进 description)就报 `Nested mappings are not allowed in compact mappings at line 2, +column 14` → **技能整份不加载**(`skill()` 读不到、也不进技能目录)。**`js-yaml` 能吃下去** —— +拿它自检会假绿,必须用 DSH 自带的那个解析器验。另 — 值首字符不能是 `%`(directive indicator)或 `&` +(anchor),双引号里的 `%SystemDrive%` 之类不受影响。落盘后**让 DSH 真加载一次**再收口。 + +- **bash / `ops_bash` 侧「构建到底过没过」的一次自查**(2026-10-26 实测口径):先 `cd` 到仓库根(或该 worktree 根),跑 `.venv/Scripts/python.exe .dsh/skills/keil-build/scripts/keil_build.py; echo BUILD_RC=$?` —— 正斜杠、**无前导 `&`**,退出码归 python、不归 bash;再单独读日志计数 `grep -a -E "Error\(s\)|error:|Warning\(s\)" MDK-ARM/build_log.txt`。报结论要三样齐 —— **`BUILD_RC=0` + `UV4_RC: 0` + `BUILD_OK: 0 errors, 0 warnings`**(脚本自己判「无 `error:` 行 且 尾行含 `0 Error(s), 0 Warning(s)`」);bash 吐的 `syntax error near unexpected token` **既不是构建通过也不是构建失败**,那是写法问题,别拿它当构建结论报。 > 本节全文(逐字搬移、未改写,含命令与实测数字)→ [executor-shell-forms.md](reference/executor-shell-forms.md)。动手前先读那篇。 +## exit 2 实战闭环:`UV4.exe not found` 的探测 → 注入 → 复核(2026-10-30 实测) + +**先定性,别动源码**:脚本报 +`BUILD_ENV_ERROR: UV4.exe not found (--uv4 / config 'uv4' / $DSH_KEIL_UV4 / $DTU_KEIL_UV4 / PATH)` +且**退出码 2** = **环境错,不是构建失败**——本次 **UV4 根本没启动、`MDK-ARM/build_log.txt` 也没被改写**, +日志里看到的是上一次的。此时去改 `.c`/`.h` 或怀疑代码,就是把环境问题当编译问题排。 +(会话为什么看不见:DSH 会话的 shell 由常驻服务派生,**服务启动之后**设的用户级变量不会自动进来; +`run_code` 起的 Node 进程 `process.env` 干脆是空的——AGENTS.md「会话不继承用户级变量」同记此坑。 +所以**变量"已设但本会话看不见"是常态**,不是用户没设。) + +1. **探测候选安装路径**(逐个 `fs.existsSync` / `test -f`,命中即取绝对路径)—— + `C:/Keil_v5/UV4/UV4.exe`、`C:/Keil/UV4/UV4.exe`、`D:/Keil_v5/UV4/UV4.exe`、`D:/Keil/UV4/UV4.exe`、 + `C:/Program Files/Keil_v5/UV4/UV4.exe`、`D:/Program Files/Keil_v5/UV4/UV4.exe` + (2026-10-30 实测只命中 `C:/Keil_v5/UV4/UV4.exe`,其余五个都不存在)。 + ```bash + for p in 'C:/Keil_v5/UV4/UV4.exe' 'C:/Keil/UV4/UV4.exe' 'D:/Keil_v5/UV4/UV4.exe' \ + 'D:/Keil/UV4/UV4.exe' 'C:/Program Files/Keil_v5/UV4/UV4.exe' 'D:/Program Files/Keil_v5/UV4/UV4.exe'; do + [ -f "$p" ] && echo "HIT $p"; done + ``` +2. **读用户级来源确认"值本来就对"**(同一轮里顺手做掉,别只靠猜路径): + bash / Node 侧 `reg query HKCU\Environment`(整表打出来再找 `DTU_KEIL_UV4` / `DSH_KEIL_UV4`), + PowerShell 侧 `[Environment]::GetEnvironmentVariable('DTU_KEIL_UV4','User')`。 + 有值 → 直接用它;没值/值指向旧目录 → 用第 1 步的命中路径。 + ⚠ **不替用户改全局**:不要 `setx`、不要 `SetEnvironmentVariable(...,'User')`——那是用户自己在真终端里 + 做的事(`docs/环境与部署/本地工具路径环境变量方案.md`);本闭环只解决"这一次构建怎么跑起来", + 报告里也必须这么写。 +3. **只给这一次调用注入**再重跑(两种等价形态,二选一): + ```js + // run_code:options.env 是**整体替换**,所以要连 SystemRoot / Path / USERPROFILE 一起给 + const { execSync } = await import('node:child_process'); + execSync('".venv/Scripts/python.exe" .dsh/skills/keil-build/scripts/keil_build.py', { + cwd: MAIN, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, + env: { SystemRoot: 'C:\\Windows', Path: 'C:\\Windows;C:\\Windows\\System32', + USERPROFILE: 'C:\\Users\\<你>', DTU_KEIL_UV4: 'C:\\Keil_v5\\UV4\\UV4.exe' } }); + ``` + - bash / `ops_bash` 侧的等价写法 = 显式参数 `--uv4 'C:\Keil_v5\UV4\UV4.exe'`(值含空格要整体加引号)。 + - `run_code` 的 `timeoutMs` 给足:注入后这次构建实测 **10 s** 跑完,但首次冷构建更长(我给的 500000)。 + - **别顺手加 `--no-lock`**:注入重跑时锁链照旧(`LOCK: 已获取 build 锁(task=build-cli)` → `已释放锁`)。 +4. **复核口径**(三样齐了才算真跑过,缺一样就是没构建):脚本回显 + `UV4: <绝对路径> (<来源>)`(来源随注入方式变:`$DTU_KEIL_UV4` 或 `--uv4`)→ + `UV4_RC: 0` → `BUILD_OK: 0 errors, 0 warnings`,且**外层退出码 0**(自己包 execSync 就显式把码打出来, + 别接管道,见「调用形态」)。报告里写清"本次注入 + 来源",**不得声称已永久设置系统/用户环境变量**。 + +- ⚠ 注入的若是**注册表里那个陈旧值**,会撞同族的另一种 2: + `BUILD_ENV_ERROR: $DTU_KEIL_UV4 is set but not a file: … (fix the value; no silent fallback)` + —— 值在但指向旧盘符/旧目录名(本仓踩过 `D:\Code\DTU 程序\…` 搬迁)。脚本**刻意不静默回退**, + 回到第 1 步用探测命中的路径覆盖它,别去猜目录、更别改脚本让它"猜 C 盘"(设计上明确禁止从旧路径猜)。 +- ⚠ 本闭环只回答"UV4 在哪儿",**不回答"调用写法对不对"**:从 bash 侧照抄 PowerShell 的 `&` 也是 exit 2, + 但 stderr 是 `syntax error near unexpected token`(见「调用形态」)。**先看 stderr 再决定排障方向**, + 两种 2 的处置完全不同。 +- **改 `.c` 注释(哪怕只改一行中文注释)也必须跑完这道构建验证**:2026-10-30 的实测就是 + 「改注释 → `@build` → 吃 rc=2 → 探测+注入 → 10 s 通过」。注释类坑(BOM 被剥 → `#870-D`、 + 注释里写 `**` 路径 → 9-D)只在**重编译那个 TU 时**才暴露,"以前 0 警告"不代表安全。 + ## build 锁:脚本自持自释(2026-10-12) **UV4 不能两个实例同时 `-b`,但构建锁不该由调用方"记得释放"**——那正是泄漏源: @@ -33,7 +96,7 @@ Agent 手工 `lock acquire build` 后忘了 `release`,锁要挂到 TTL 才被 | 开关 | 作用 | |---|---| -| `--lock-task ` | 持有者名。默认 `$DTU_TASK_ID` → 从 cwd 的 `_wt/-/` 反推 `` → `build-cli` | +| `--lock-task ` | 持有者名。默认 `$DTU_TASK_ID` → 从 cwd 反推:`_wt/-/` → ``(`@task` 那批);`.dsh-worktrees/worktree//` → `pw:`(DSH worktree **插件**建的树,`plugin_tree_owner()`)→ 都没有才落 `build-cli` | | `--lock-wait ` | 锁被占时等待 N 秒(默认 0 = 立即失败,退出码 4) | | `--no-lock` | 完全不加锁(危险:并发 `-b` 互相干扰) | | `DTU_LOCK_HELD=build` | 环境变量:**调用方已持锁**时设它,本脚本跳过自持自释(`task-worktree done` 的门禁即如此用) | @@ -45,6 +108,11 @@ Agent 手工 `lock acquire build` 后忘了 `release`,锁要挂到 TTL 才被 并告警**(保持本 skill 可独立搬运)。 - ⚠ 从 `_wt/-/` 反推持有者名,意味着"手工 `lock acquire build --task ` 后再跑 `@build`"仍**幂等**(同名即视为自己持有),不会自我死锁。 +- **退出码 4 的持有者名带 `pw:`(如 `pw:refactor-logging`)= 插件在 `.dsh-worktrees/worktree//` 里跑的构建占着 UV4,不是某个 `@task` 任务 ID**: + 插件树**没有台账**,别拿它去 `lock release `(释放不掉别人的锁)、更别去杀 UV4 进程。正解 = + 等那次构建自己 `finally` 释放(自持自释;崩溃则按自报 TTL 兜底),或本次加 `--lock-wait ` 排队; + 想知道是哪棵树的谁:`"$PY" "$TW" plugin list`(缺联接再 `plugin adopt `)→ 细节归 + `task-worktree` skill F 节(插件桥)。 - **只 build 锁自动;`bench` 锁仍须 Agent 手工持**(它是**会话作用域**,跨 烧录→`@test`→探针多条命令,逐命令自动释放会让别的任务插进来扰动设备)。 @@ -54,6 +122,9 @@ Agent 手工 `lock acquire build` 后忘了 `release`,锁要挂到 TTL 才被 - 根因(2026-10-28 实测):run_code 的 Node 进程 process.env 是空的 → known-folder 可展开串展不开变成相对路径 → 按当前目录建在仓库根;同族还会冒出 %USERPROFILE%/AppData/Local/Temp - 清理有安全阀:⚠ 绝不用 cmd / PowerShell 直接删(rd /s /q %SystemDrive% 会被展开成 C:);只用 bash 引号形态,先 find '%SystemDrive%' -type l 预检再 rm -rf --。它是工具副产物,直接删、别 stash、更别提交 - 两层防线均已落地可核验:.gitignore:167 挡入库 + 脚本 harden_windows_env() 回填环境变量、check_percent_literal_junk() 只告警不自动删;守卫是兜底不是许可,@build 仍要走 ops_bash / PowerShell +- **取证在前,处置在后**(2026-10-28 实测,别一上来 `rm -rf`):`ls -laR -- "%SystemDrive%"` + `du -sh "%SystemDrive%"` 枚举内容与体积,再 `find "%SystemDrive%" -printf "%T@ %p\n" | sort -rn | head -3` 看**最新 mtime** —— 最近几分钟还在被重建 = 环境仍残缺(脚本会打 `WARN: ENV_SPARSE:`),先断源头(换 `ops_bash`/PowerShell 跑,或按「UV4.exe not found」一节把 `env` 传全)再删,否则删完立刻长回来;顺带确认仓库里只有这一个该名字的东西(`find . -maxdepth 1 -name "*SystemDrive*"`) +- **要留证据就搬走、别就地删** —— ⚠ **shell 的 `mv` 搬不动它**:`mv "%SystemDrive%/..."` 报 `cannot stat`(含 `%` 的目录名被当变量展开 / 吃掉,路径根本对不上)。改用 Python 遍历搬到**仓库外**的临时目录 —— `os.walk` + `shutil.move`(或 `glob.glob("%SystemDrive%/*")` 逐个 `shutil.move` 到 `os.environ["TEMP"]` 下);确认是纯工具副产物(几个 `.db` 缓存)才 `rm -rf --` +- **入库排除要落成条目并核验**:补 `.gitignore` 前先 `grep -n "SystemDrive" .gitignore` 定位精确原文行再 Edit(凭记忆写 `old_string` 必不匹配,2026-10-28 踩过)。本仓条目 = `.gitignore:167` 的 `%SystemDrive%/`,上面那段注释就是复发证据(新增条目也照此**写明第几次复发、谁造的**)。收尾核验 —— `git status --porcelain` 里该目录零命中(且别的未跟踪产物一个没少)、`git check-ignore -v` 落在那一行、`git add -A --dry-run` 零命中;**绝不提交**(把本机缓存塞进历史) > 本节全文(逐字搬移、未改写,含命令与实测数字)→ [systemdrive-junk-dir.md](reference/systemdrive-junk-dir.md)。动手前先读那篇。 ## 本仓库 @@ -65,12 +136,13 @@ Agent 手工 `lock acquire build` 后忘了 `release`,锁要挂到 TTL 才被 判定口径(与原 `MDK-ARM/build.bat` 同款;该文件已于 2026-10-10 删除,本 skill 即 `@build`): 先删旧日志 → `-b -j0 -o` → 无 `error:` 且含 `0 Error(s), 0 Warning(s)`。 本 skill 另有超时保护、UV4 多级解析链(含 PATH 兜底)、失败只打报错行。 -环境变量 `DTU_KEIL_UV4`(用户级,指向 UV4.exe)。 +环境变量 `DTU_KEIL_UV4`(用户级,指向 UV4.exe)——注意**它设了不等于本会话能看见**,仍报退出码 2 时走「exit 2 实战闭环」那一节,别先去找用户。 ## 铁律与陷阱(实测教训) - **任何代码改动后必须先编译验证(0 错误 0 警告)再谈烧录/提交**——编译不过就进 - 硬件闭环只会把编译错误伪装成运行问题。 + 硬件闭环只会把编译错误伪装成运行问题。**只改 `.c` 里一行中文注释也算改动**(2026-10-30 实测: + 改注释 → `@build` → 环境错 → 探测注入 → 10 s 通过):BOM/嵌套注释这类坑只在重编译那个 TU 时才暴露。 - **警告也算失败**:判 PASS 只认退出码 0,别只看"有没有 error:"(取退出码的管道陷阱见「调用形态」)。 - **`MDK-ARM/*.uvprojx` 每次构建后被 UV4 自动改写,改动一律不入库**:UV4 会把工程 XML 归一化成本机风格(`` → ``、XML 声明加 `standalone="no"`)并把 @@ -83,15 +155,17 @@ Agent 手工 `lock acquire build` 后忘了 `release`,锁要挂到 TTL 才被 `rm -rf -- '%SystemDrive%'`(bash 引号形态)删掉即可,**严禁 `rd /s /q %SystemDrive%`——cmd 会把它 展开成 `C:`**;`.gitignore` 已兜住 + 脚本已加 `ENV_SPARSE`/`JUNK_DIR` 守卫 → 见「构建副产物」一节。 - 报错定位读日志别只看终端回显:本仓库 = `MDK-ARM\build_log.txt`。 -- **exit 2 先分三种再动手**(2026-10-10 / 2026-10-26 实测):① **bash 语法错**——stderr 有 +- **exit 2 先分三种再动手**(2026-10-10 / 2026-10-26 / 2026-10-30 实测):① **bash 语法错**——stderr 有 `syntax error near unexpected token`,是把 PowerShell 形态丢进了 `ops_bash`,换写法即可; - ② **真没设** `DTU_KEIL_UV4`;③ **会话没继承**——agent 的 shell 由常驻服务派生, - 服务启动**之后**设置的用户级环境变量不会自动进来。查 - `[Environment]::GetEnvironmentVariable('DTU_KEIL_UV4','User')`,有值就 - `$env:DTU_KEIL_UV4 = <该值>` 注入本会话再跑,别急着让用户去设。 + ② **真没设** `DTU_KEIL_UV4`;③ **已设但本会话看不见**(agent 的 shell 由常驻服务派生,服务启动**之后** + 设的用户级变量不会进来;`run_code` 的 `process.env` 直接是空的)。 + ②③ 一律走 **「exit 2 实战闭环」** 那一节:探测候选路径 + 读 `HKCU\Environment`, + 再**只往那一次调用**的 env / `--uv4` 里注入命中路径重跑——别急着让用户去设,也别替他改全局。 - 编码类报错(`#870-D: invalid multibyte character sequence` 等)先怀疑源文件 BOM/编码被改写,而不是代码本身(见仓库 AGENTS.md 编码铁律)。 + +- **build 锁的对面是谁**(`BUILD_LOCK_BUSY`、退出码 4、`pw:` 插件树、`@task plugin list` / `plugin adopt`、`_wt/` 台账与合并回收)归 `task-worktree` skill(F 节 = DSH worktree 插件桥);换机/新克隆时 `DTU_*` 工具路径变量的设立与「会话不继承」坑 = `new-machine-setup` skill。 ## 与其它 skill 的衔接 - 烧录/回读 = `stlink-flash` skill;串口闭环 = `serial-boot-test` skill From 77f031a8fd852ecc992113feb9e39f4892a21b56 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Fri, 30 Oct 2026 01:01:44 +0800 Subject: [PATCH 19/19] =?UTF-8?q?fix(keil-build):=20frontmatter=20?= =?UTF-8?q?=E6=95=B4=E5=80=BC=E5=8F=8C=E5=BC=95=E5=8F=B7=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E6=8A=80=E8=83=BD=E6=95=B4=E4=BB=BD=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:今日在飞会话给 description/whenToUse 追加了新内容(「pw: 前缀」等), 但两行值为未加引号的 YAML plain scalar——其中的 ASCII「冒号+空格」让 DSH parseFrontmatter() 所用的严格 yaml 解析器报 Nested mappings 错误 → keil-build 技能整份不加载(skill() 读不到、技能目录不收录)。 js-yaml 能吃下去,拿它自检会假绿。 修复:description/whenToUse 整值加双引号(pw: 字面量原样保留), 正文护栏段同步为「要么整值双引号,要么一个 ASCII 冒号都不写」并点名 解析器与假绿坑;另有两段纯重排(护栏段前移、衔接项目符号归位)。 验证(三重):DSH 自带 yaml 包严验全仓 17 个 SKILL.md 全 PASS; skill-size 门禁 17/17 无悬空指针;skill('keil-build') 全文可读, 技能目录已恢复收录。字节卫生:无 BOM、纯 LF、无 U+FFFD。 注:本笔仅含 SKILL.md。索引中另有一笔在飞会话暂存的 uvprojx, 属 UV4 XML 归一化抖动,按 AGENTS 口径不入库,留给该会话自行处置。 --- .dsh/skills/keil-build/SKILL.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/.dsh/skills/keil-build/SKILL.md b/.dsh/skills/keil-build/SKILL.md index 1298287d..af150dae 100644 --- a/.dsh/skills/keil-build/SKILL.md +++ b/.dsh/skills/keil-build/SKILL.md @@ -15,13 +15,6 @@ whenToUse: "需要编译 Keil MDK 工程、跑 @build 验证代码改动(只 # 覆盖:--project X.uvprojx --uv4 UV4.exe --target 名字 --log 路径 --timeout 600 ``` -## 调用形态:先认执行器是哪个 shell(2026-10-26 实测) - -- 同一个 @build,PowerShell 与 bash 是两种写法:bash 侧要去掉前导 & 并把路径改正斜杠(照抄 AGENTS 的 PowerShell 形态进 ops_bash 会语法错秒退 exit 2) -- ⚠ 那个 exit 2 不是脚本的环境错误 2:stderr 含 syntax error near unexpected token = 写法问题,换 bash 形态重跑,别去查 DTU_KEIL_UV4、别当环境错误排障 -- 后台跑(run_in_background: true)要自己收码,且别把脚本接进管道:实测 $? 取到的是 tail 的码(恒 0);要看报错行就单独 grep MDK-ARM/build_log.txt -- 解析顺序:命令行 > 项目档案 .serialtest/config.json > 环境 > 探测;UV4 显式设了但路径无效 = 立即报错,不静默回退 -- 退出码 0=PASS(0错0警) / 1=构建失败 / 2=环境错误 / 3=用法 / 4=build 锁被占用;2026-10-12 修正过 2 与 3 的归属,该篇末尾记了前后口径 ⚠ **改本文件 frontmatter 要么整值加双引号,要么一个 ASCII 冒号都别写**(2026-10-30 这份就因此整份加载失败)。 DSH 用 `yaml` 包的 `parse()` 读 frontmatter(`@deepseek-ai/dsh-skill-filesystem` 的 `parseFrontmatter()`), plain scalar 里出现 **ASCII「冒号 + 空格」**(例如把报错原文 `BUILD_ENV_ERROR: UV4.exe not found` 或 @@ -30,6 +23,13 @@ column 14` → **技能整份不加载**(`skill()` 读不到、也不进技能 拿它自检会假绿,必须用 DSH 自带的那个解析器验。另 — 值首字符不能是 `%`(directive indicator)或 `&` (anchor),双引号里的 `%SystemDrive%` 之类不受影响。落盘后**让 DSH 真加载一次**再收口。 +## 调用形态:先认执行器是哪个 shell(2026-10-26 实测) + +- 同一个 @build,PowerShell 与 bash 是两种写法:bash 侧要去掉前导 & 并把路径改正斜杠(照抄 AGENTS 的 PowerShell 形态进 ops_bash 会语法错秒退 exit 2) +- ⚠ 那个 exit 2 不是脚本的环境错误 2:stderr 含 syntax error near unexpected token = 写法问题,换 bash 形态重跑,别去查 DTU_KEIL_UV4、别当环境错误排障 +- 后台跑(run_in_background: true)要自己收码,且别把脚本接进管道:实测 $? 取到的是 tail 的码(恒 0);要看报错行就单独 grep MDK-ARM/build_log.txt +- 解析顺序:命令行 > 项目档案 .serialtest/config.json > 环境 > 探测;UV4 显式设了但路径无效 = 立即报错,不静默回退 +- 退出码 0=PASS(0错0警) / 1=构建失败 / 2=环境错误 / 3=用法 / 4=build 锁被占用;2026-10-12 修正过 2 与 3 的归属,该篇末尾记了前后口径 - **bash / `ops_bash` 侧「构建到底过没过」的一次自查**(2026-10-26 实测口径):先 `cd` 到仓库根(或该 worktree 根),跑 `.venv/Scripts/python.exe .dsh/skills/keil-build/scripts/keil_build.py; echo BUILD_RC=$?` —— 正斜杠、**无前导 `&`**,退出码归 python、不归 bash;再单独读日志计数 `grep -a -E "Error\(s\)|error:|Warning\(s\)" MDK-ARM/build_log.txt`。报结论要三样齐 —— **`BUILD_RC=0` + `UV4_RC: 0` + `BUILD_OK: 0 errors, 0 warnings`**(脚本自己判「无 `error:` 行 且 尾行含 `0 Error(s), 0 Warning(s)`」);bash 吐的 `syntax error near unexpected token` **既不是构建通过也不是构建失败**,那是写法问题,别拿它当构建结论报。 > 本节全文(逐字搬移、未改写,含命令与实测数字)→ [executor-shell-forms.md](reference/executor-shell-forms.md)。动手前先读那篇。 @@ -164,10 +164,9 @@ Agent 手工 `lock acquire build` 后忘了 `release`,锁要挂到 TTL 才被 - 编码类报错(`#870-D: invalid multibyte character sequence` 等)先怀疑源文件 BOM/编码被改写,而不是代码本身(见仓库 AGENTS.md 编码铁律)。 - -- **build 锁的对面是谁**(`BUILD_LOCK_BUSY`、退出码 4、`pw:` 插件树、`@task plugin list` / `plugin adopt`、`_wt/` 台账与合并回收)归 `task-worktree` skill(F 节 = DSH worktree 插件桥);换机/新克隆时 `DTU_*` 工具路径变量的设立与「会话不继承」坑 = `new-machine-setup` skill。 ## 与其它 skill 的衔接 +- **build 锁的对面是谁**(`BUILD_LOCK_BUSY`、退出码 4、`pw:` 插件树、`@task plugin list` / `plugin adopt`、`_wt/` 台账与合并回收)归 `task-worktree` skill(F 节 = DSH worktree 插件桥);换机/新克隆时 `DTU_*` 工具路径变量的设立与「会话不继承」坑 = `new-machine-setup` skill。 - 烧录/回读 = `stlink-flash` skill;串口闭环 = `serial-boot-test` skill (其档案 `flash_cmd`/`readback_cmd` 即 stlink-flash 的入口命令)。 - 收尾固定链(2026-10-26 实证):`@build` → `scripts/ram_baseline.py --update` →