e4d70f9fcb
证据源 = 主树 ci-artifacts/archify/architecture-dtu-overview-20261030-095132/ evidence.md §11。该目录不入库,故把每条的判据与实测源抄进正文,别让证据只活在 一台机器上。处置口径:能改的改成与代码同源,核实后确认文档没问题的明确写「不改」。 判停点与就绪屏障(条 1、条 3) - AGENTS 原写 `protect_task.c:98` 直接停走判据:`:98` 是 `s_prev_cal_lock` 变量, 判停点是 `App/task/protect_task.c:268 if (adc_cal_prot_blocked() != 0u)`, 判定本体在标定层 `adc_cal_prot_blocked()`、protectTask 只是消费者 → 改引用, 同步 `ui_task.c:240` 页面注释与 `docs/任务规划/数据可信.md` 的同一处行号。 - `SYS_EVT_FTP_READY` 有生产者(`Src/freertos.c:490`,注释自陈预留)无消费者; `SYS_EVT_TIMESYNC_READY` 全仓无生产者也无消费者(只有定义与测试桩)。 AGENTS、`sys_events.h` 位注释、屏障配图(生成器 + 产物)三处从「四个就绪位 并列」改成「bit0/bit1 在用 + bit2/bit3 预留」,并指向 `docs/架构设计/项目启动顺序管理.md` §3.2 的逐位对照表。ftpTask 实际等的是 `NET|STORAGE`(`Src/freertos.c:477-479`,WaitAll|NoClear,100 ms 分片喂狗)。 /log 不是「三本」(条 5) - `App/log/sys_log_mcu.c` 的 `s_paths[]` = run / selftest / accid / alarm / start / oper / yk 七本 64B 定长文本 + `soe.log`(64B 定长二进制)+ `wdev.log` (变长取证行,`App/fs/vol_lock` 侧投,不走定长格式)。 → AGENTS 两处、总览 §2.1 与 §4 的「三本」改成「七本 + SOE(+ wdev 取证行)」。 主机单测 44 套 → 47 套(条 2,两把机器尺互证) - `scripts/check_test_coverage.py --list` 打印 47 行;`ls -d test/host/*/` = 48 个 一级目录,唯一没有 `run_unit_test.py` 的是 `common/` ⇒ 47 套。 - 差额三名 = 屏板链路套件:`sb_proto`、`sb_link`(2026-10-29 入库)、 `sb_led_map`(2026-10-30 入库)。专属钩子仍是 22 根 ⇒ 无专属钩子的从 22 套变 25 套。 - 同步面:AGENTS、`docs/门禁说明/README.md`、`host-unit-tests.md` §6 台账(名单补 三名)、`docs/代码规范/pre-commit使用说明.md`、`主机单测覆盖率口径.md`(含耗时句)、 `scripts/README.md`、总览目录树、`.pre-commit-config.yaml` 三处注释、 `test/host/sys_log_mcu/sys_log_mcu_test.c` 一处注释。 - 带日期戳的历史测量值按纪律冻结不动:28 套、41 套 ~105 s、40 套/18 套那三处。 本仓 LVGL 版本(条 6) - vendored 版本 = `Lib/lvgl/lv_version.h:9-11` 的 9.3.0;总览两处「LVGL v9.2」与 `gen_arch_svg.py` 图内文字改 v9.3 并重跑产物(脚本与产物同一笔,`generated-fresh` 逐字节核)。 - **刻意不动**:`docs/内存管理/RAM使用分析与优化方案.md` 两处「LVGL v9.2 文档」+ GitHub `release/v9.2` 分支 URL ——那是对外游某分支文档的引用,不是本仓版本声明。 触发口径与栈值(条 4、7、9、10) - ADC 映射 §1「ADC1 为软件触发」是旧口径:`Src/adc.c:53` 实为 `ADC_EXTERNALTRIGCONV_T3_TRGO` → 改成 TIM3 TRGO,并补一句「TIM3 与 TPAFE 的 CONVST 之间没有硬件耦合,仍不构成共同采样触发」,缺陷结论本身不变。 - 同文档 adcTask 栈 512 → 640 words(`Src/freertos.c:159 .stack_size = 640 * 4`)。 - `docs/UI设计/UI_设计架构.md` 的 uiTask 栈「16384 words」是单位错: 实为 4096 words = 16384 B(`Src/freertos.c:218`),顺手补上行号。 - `App/app/app_main.c:197` 注释「DE = PD0」陈旧:`:198` 实传 `ST_DIR4_*` (V0.1 实板 PA15,V0.0 才是 PD0)→ 注释改指 V0.1 网表报告的引脚迁移表。 - 两处已退役编号 `N-18`(ADC 映射、V0.1 网表报告)换成登记簿条目名 「双转换器无共同采样触发」。 核实后确认「文档没问题」的三条(条 8、12、13) - 黄金参考「菜单 8 状态 + 内容页 66 张」:`simulator/tests/ref/` 实测 66 个 `shot_*.bin`,与 `ui-golden` 钩子显示名一致 → 不改。 - `App/task/rs485_task.c` 存在但 `Src/freertos.c` 不创建它(grep 无 rs485): AGENTS 与总览都已写明「尚未接线 / 暂缓」,图侧画成「实现待接线」→ 不改。 - 104 端口 `:2404` 是工厂默认、可经 `/config.ini` `[comm]` 改 → 总览索引表补 「默认」二字;「uiTask 10 ms 节拍」核实 = `ui_tokens.h:65 UI_TASK_TICK_MS 10` ✓。 并行勘查会话的同族发现(它扫「文档声明数 vs 真值」,逐处独立复验后采纳 3 处) - `App/protocol/iec104/iec104_link.h` 头注释写「端口:TCP 2404(§1.5 B0 定义)」, 同文件下方已写「服务端口真源已迁 [comm] 段」= 自相矛盾 → 补「工厂默认」并指回 [comm] 与「下次重 bind 即生效」。 - `scripts/gen_task_table.py` 的 iec104Task 描述「:2404 监听」→「工厂默认 :2404 监听」,`--write` 重生成 README 任务表(AGENTS 表无描述列,实测未被改写)。 - `App/log/README.md` 五行写入点行号全漂:事故 181→188、告警 184→191、启动 176→183(`App/task/protect_task.c`);操作 `iec104_pts.c` 574/602→582/610、 `default_task.c` 242→266、`sys_log_mcu.c` 646→682;遥控 573/576/582/586/596/ 598/609 → 581/584/590/594/604/606/617(宿主 `sim_syslog.c:245` 原值正确、未动)。 - **撤销一条**:我早先记的「`docs/算法/继电保护功能复现规划.md:225` 声称记录本已 有消费者」与原文不符——:225 实为规划表里「动作/SOE 记录 | 复用 `sys_log`」一行, 属能力复用口径、不是当前态断言 → 不改。教训:**改引用前逐处读现文,别照自己 上一轮的笔记盲替**(这条比改中的每一处都值钱)。 - 同一批勘查还报了约 12 处「`2404` 缺『工厂默认』限定词」(AGENTS、`App/protocol/ README.md`、`Src/freertos.c` 注释、5 篇 docs、`test/host` 工具,含一张配图生成器 要连产物同批重跑)与 `测试开发标准流程.md`「像素层 60 内容页」、 `.pre-commit-config.yaml` uinav 注释「快检档 18 张黄金参考」(后者写时为真、 描述的却是钩子现有行为):**本笔刻意不做**,另开任务空间处理——那批要同时动 AGENTS.md 与 `.pre-commit-config.yaml` 两处单写者面,与并行会话撞车的代价更高。 - 端口生效语义(用户裁定 2026-10-30):产品基线写「公共地址与端口可配…改后需重启 生效」,代码 `App/task/iec104_serv.c` 是「ca 改后要重启;port 下次重 bind 才生效」 ——裁定**按代码语义要改**处理(改了端口应立刻重 bind),属固件行为变更,另开任务 且需真机验证;`.serialtest/config.json` 写死 `i104: listening on 2404` 必检锚点 是同题(现场改端口会让串口回归假失败),一并留给那条任务。 自查(任务树内) - `check_doc_links.py --sections-strict`:12 个改动 .md 逐个 rc=0; `check_mermaid.py` 改动 .md 全 rc=0;`check_generated_fresh.py` PASS (25 个生成器 / 21 一致 / 4 存量基线);`check_nested_comment.py --all` PASS (449 文件,新发 0、存量 2);`check_bom_drift.py --all` 99 个名单文件无漂移。 - 编译:`.dsh/skills/keil-build` 退出码 0(0 错误 0 警告), Program Size Code=246720 RO=29836 RW=2052 ZI=149708 —— 与本树凭证同码, 证明这批改的确实只有注释与文档。 - 字节体检:23 个文件无 CR / U+FFFD / 行尾空格 / 缺末行换行;`ui_task.c` 被编辑后 丢的 3 字节 BOM 已补回(bom-guard 复验通过);5 处「三连空行」逐文件与 HEAD 比对确认是存量(Python 生成器的 PEP8 双空行与个别 C 文件原有空行)。 - 一次性工具 `ci-artifacts/fix_docs2.py`:断言式规则表,按文件读一次、在演化文本上 逐条替换、一次写盘。第一版写成「每条规则各自从启动时快照起算」,同一文件后一条 把前一条整体覆盖,只有每文件最后一条活下来——靠「改完再全仓复扫旧形态」当场抓到, 这条判据本身已值得写进流程。 - 待办:`git add` 后需重跑 `ram_baseline.py --update`(体积凭证按 git 索引内容生成, 未 staged 的文件不在凭证里,`check_size_gate.py --local` 现在因此报红)。 - 未 commit、未 push(AGENTS.md 边界:提交属 Ask first)。
293 lines
11 KiB
Python
293 lines
11 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
FreeRTOS 任务表一致性工具。
|
||
|
||
背景
|
||
----
|
||
`README.md` 与 `AGENTS.md` 各有一张 FreeRTOS 任务表(任务名 / 栈大小 / 优先级)。
|
||
历史上这两张表全靠手写,`Src/freertos.c` 改栈后表格忘记同步——2026-09 就出现过
|
||
两张表都比源码落后一大截(写 2048/4096/4096/4096/16384/2048,实际 1024...512)。
|
||
本脚本以 `Src/freertos.c` 为**唯一事实源**,生成 / 校验这两张表。
|
||
|
||
事实源
|
||
------
|
||
解析 `Src/freertos.c` 里形如:
|
||
|
||
const osThreadAttr_t netTask_attributes = {
|
||
.name = "netTask",
|
||
.stack_size = 1024 * 4, /* 注释 */
|
||
.priority = (osPriority_t) osPriorityNormal,
|
||
};
|
||
|
||
的块,抽出 name / stack_words / priority。`stack_size` 一律按 `N * 4`
|
||
(CMSIS-RTOS2 的 stack_size 单位是字节,工程约定写成 `words * 4`)换算为 words。
|
||
|
||
用法
|
||
----
|
||
python scripts/gen_task_table.py # 检查模式(默认):不一致即退出码 1
|
||
python scripts/gen_task_table.py --check # 同上
|
||
python scripts/gen_task_table.py --write # 就地重写 README/AGENTS 中的任务表
|
||
python scripts/gen_task_table.py --diff # 只打印期望表格,不写文件
|
||
|
||
pre-commit 用法:`python scripts/gen_task_table.py --check`,
|
||
任一被跟踪文档里的任务表与 `Src/freertos.c` 不一致即拦截。
|
||
|
||
退出码:0 通过;1 不一致 / 解析失败。
|
||
"""
|
||
import argparse
|
||
import pathlib
|
||
import re
|
||
import sys
|
||
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||
FREERTOS_C = REPO_ROOT / "Src" / "freertos.c"
|
||
|
||
# 需要保持同步的文档及其任务表锚点
|
||
# anchor_begin / anchor_end 之间即为「受管表格区」,脚本只替换这一段,
|
||
# 表格外的说明文字(含各文档自己的职责描述/口径注记)原样保留。
|
||
TARGETS = [
|
||
{
|
||
"path": REPO_ROOT / "README.md",
|
||
"begin": "<!-- TASK_TABLE:BEGIN generated by scripts/gen_task_table.py -->",
|
||
"end": "<!-- TASK_TABLE:END -->",
|
||
"header": "| 任务 | 栈大小 | 优先级 | 职责 |\n"
|
||
"|------|--------|--------|------|",
|
||
},
|
||
{
|
||
"path": REPO_ROOT / "AGENTS.md",
|
||
"begin": "<!-- TASK_TABLE:BEGIN generated by scripts/gen_task_table.py -->",
|
||
"end": "<!-- TASK_TABLE:END -->",
|
||
"header": "| Task | 栈 (words) | 优先级 |\n|---|---|---|",
|
||
},
|
||
]
|
||
|
||
# 文档中的展示顺序(与 README/AGENTS 既有一致;不在表内的任务按源码顺序追加)。
|
||
# 不用源码顺序:freertos.c 里 timeSyncTask 定义在最前,但文档按「主任务→辅助任务」排列。
|
||
DISPLAY_ORDER = ["defaultTask", "netTask", "ftpTask", "testTask", "uiTask",
|
||
"timeSyncTask", "adcTask", "iec104Task", "protectTask"]
|
||
|
||
# 各任务在文档里的「职责」描述(README 需要,AGENTS 不需要)。
|
||
# 只影响展示文案,不参与一致性判定;未知任务回落为空。
|
||
ROLE = {
|
||
"defaultTask": "FTL + FatFS 挂载、配置引擎属主、syslog 唯一写者",
|
||
"netTask": "网络初始化 + CH395F 轮询(消费 NET_RECONFIG 按代次重建监听)",
|
||
"ftpTask": "FTP 服务(等待网络+存储就绪)",
|
||
"testTask": "硬件测试入口",
|
||
"uiTask": "LVGL UI 界面",
|
||
"timeSyncTask": "墙钟时间同步",
|
||
"adcTask": "TPAFE5160 采样(TIM3 1kHz 事件驱动 + 原子快照 + STM32 ADC DMA,纯消费者)",
|
||
"iec104Task": "IEC 60870-5-104 上行(工厂默认 :2404 监听,遥信/遥测/定值区)",
|
||
"protectTask": "保护判据 20ms 节拍(保护判据元件 + 采样故障闭锁,B5)",
|
||
}
|
||
|
||
|
||
def sort_tasks(tasks):
|
||
"""按 DISPLAY_ORDER 排序;未列出的任务保持源码相对顺序排在最后。"""
|
||
idx = {n: i for i, n in enumerate(DISPLAY_ORDER)}
|
||
return sorted(tasks, key=lambda t: (idx.get(t[0], len(idx)),))
|
||
|
||
# 任务块:const osThreadAttr_t <id>_attributes = { ... };
|
||
BLOCK_RE = re.compile(
|
||
r"const\s+osThreadAttr_t\s+(?P<id>\w+)_attributes\s*=\s*\{(?P<body>.*?)\};",
|
||
re.S,
|
||
)
|
||
STACK_RE = re.compile(r"\.stack_size\s*=\s*(?P<expr>[^,]+),")
|
||
PRIO_RE = re.compile(r"\.priority\s*=\s*\(osPriority_t\)\s*(?P<prio>\w+)")
|
||
|
||
# 优先级枚举 → 文档里使用的短名(去掉 osPriority 前缀)
|
||
PRIO_SHORT = lambda p: p[len("osPriority"):] if p.startswith("osPriority") else p
|
||
|
||
|
||
def parse_tasks(text: str):
|
||
"""从 freertos.c 文本解析任务列表,按出现顺序返回。
|
||
|
||
stack_size 允许以下写法(工程约定):
|
||
1024 * 4 -> 1024 words
|
||
(1024 * 4) -> 1024 words
|
||
4096 -> 1024 words(按 bytes/4 折算,并给出提示)
|
||
返回 [(name, words, prio, raw_expr), ...]
|
||
"""
|
||
tasks = []
|
||
for m in BLOCK_RE.finditer(text):
|
||
body = m.group("body")
|
||
sm = STACK_RE.search(body)
|
||
pm = PRIO_RE.search(body)
|
||
if not (sm and pm):
|
||
continue
|
||
expr = sm.group("expr").strip()
|
||
name_lit = re.search(r'\.name\s*=\s*"([^"]+)"', body)
|
||
name = name_lit.group(1) if name_lit else m.group("id")
|
||
|
||
# 去掉注释后求值:`1024 * 4` -> 4096 (bytes)
|
||
clean = re.sub(r"/\*.*?\*/", "", expr, flags=re.S).strip()
|
||
clean = clean.strip("()").strip()
|
||
try:
|
||
num = eval(clean, {"__builtins__": {}}, {}) # noqa: S307 (受控字面量)
|
||
except Exception:
|
||
print(f"[WARN] 无法解析 {name} 的 stack_size 表达式:{expr!r}",
|
||
file=sys.stderr)
|
||
continue
|
||
|
||
# 工程约定 = words*4,故 words = 字节/4
|
||
words = num // 4 if num % 4 == 0 else num
|
||
tasks.append((name, words, PRIO_SHORT(pm.group("prio")), expr))
|
||
return tasks
|
||
|
||
|
||
def render_table(tasks, header: str, with_role: bool) -> str:
|
||
lines = [header]
|
||
for name, words, prio, _expr in tasks:
|
||
if with_role:
|
||
lines.append(f"| {name} | {words} words | {prio} | {ROLE.get(name, '')} |")
|
||
else:
|
||
lines.append(f"| {name} | {words} | {prio} |")
|
||
return "\n".join(lines)
|
||
|
||
|
||
def replace_managed(text: str, target: dict, table: str):
|
||
"""替换 <!-- TASK_TABLE:BEGIN --> ... <!-- TASK_TABLE:END --> 之间的内容。
|
||
|
||
返回 (新文本, 是否变化)。若锚点不存在则原样返回(由调用方决定是否补锚点)。
|
||
"""
|
||
b, e = target["begin"], target["end"]
|
||
if b not in text or e not in text:
|
||
return text, False
|
||
start = text.index(b) + len(b)
|
||
end = text.index(e)
|
||
old = text[start:end]
|
||
new = "\n" + table + "\n"
|
||
if old == new:
|
||
return text, False
|
||
return text[:start] + new + text[end:], True
|
||
|
||
|
||
def extract_managed(text: str, target: dict):
|
||
b, e = target["begin"], target["end"]
|
||
if b not in text or e not in text:
|
||
return None
|
||
start = text.index(b) + len(b)
|
||
end = text.index(e)
|
||
return text[start:end].strip()
|
||
|
||
|
||
def read_doc(path: pathlib.Path):
|
||
"""读文档,返回 (文本, 原换行符, 是否有 BOM)。
|
||
|
||
本仓库 core.autocrlf=true 且无 .gitattributes,工作区换行混杂
|
||
(README=LF、AGENTS=CRLF);带 BOM 文件(App/cfg/cfg_file.c 等)另有铁律。
|
||
--write 必须**按原样回写**,否则整文件行尾/BOM 漂移 → 巨型无意义 diff。
|
||
"""
|
||
raw = path.read_bytes()
|
||
bom = raw.startswith(b"\xef\xbb\xbf")
|
||
body = raw[3:] if bom else raw
|
||
crlf = body.count(b"\r\n")
|
||
lf_only = body.count(b"\n") - crlf
|
||
newline = "\r\n" if crlf > lf_only else "\n"
|
||
text = body.decode("utf-8", errors="replace")
|
||
# 统一成 \n 供内部处理
|
||
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
||
return text, newline, bom
|
||
|
||
|
||
def write_doc(path: pathlib.Path, text: str, newline: str, bom: bool):
|
||
"""按原换行符 / BOM 状态回写(text 内部为 \n)。"""
|
||
out = text if newline == "\n" else text.replace("\n", newline)
|
||
data = out.encode("utf-8")
|
||
if bom:
|
||
data = b"\xef\xbb\xbf" + data
|
||
path.write_bytes(data)
|
||
|
||
|
||
def main(argv):
|
||
ap = argparse.ArgumentParser(description="FreeRTOS 任务表生成/校验")
|
||
g = ap.add_mutually_exclusive_group()
|
||
g.add_argument("--check", action="store_true", help="检查模式(默认)")
|
||
g.add_argument("--write", action="store_true", help="就地重写受管表格区")
|
||
g.add_argument("--diff", action="store_true", help="只打印期望表格")
|
||
args = ap.parse_args(argv[1:])
|
||
|
||
if not FREERTOS_C.exists():
|
||
print(f"[FAIL] 找不到事实源:{FREERTOS_C}", file=sys.stderr)
|
||
return 1
|
||
c_text, _nl, _bom = read_doc(FREERTOS_C)
|
||
tasks = parse_tasks(c_text)
|
||
if not tasks:
|
||
print("[FAIL] 未能从 Src/freertos.c 解析出任何任务,请检查正则与源码格式。",
|
||
file=sys.stderr)
|
||
return 1
|
||
tasks = sort_tasks(tasks)
|
||
|
||
if args.diff:
|
||
for t in TARGETS:
|
||
with_role = t["path"].name == "README.md"
|
||
print(f"=== {t['path'].name} ===")
|
||
print(render_table(tasks, t["header"], with_role))
|
||
print()
|
||
return 0
|
||
|
||
changed, missing, problems = [], [], []
|
||
for t in TARGETS:
|
||
if not t["path"].exists():
|
||
problems.append(f"{t['path'].name} 不存在")
|
||
continue
|
||
text, newline, bom = read_doc(t["path"])
|
||
with_role = t["path"].name == "README.md"
|
||
table = render_table(tasks, t["header"], with_role)
|
||
|
||
if args.write:
|
||
new_text, did = replace_managed(text, t, table)
|
||
if did:
|
||
write_doc(t["path"], new_text, newline, bom)
|
||
changed.append(t["path"].name)
|
||
else:
|
||
got = extract_managed(text, t)
|
||
if got is None:
|
||
missing.append(t["path"].name)
|
||
else:
|
||
# 提取区已归一为 \n;与期望表格比较时同样归一
|
||
if got.replace("\r\n", "\n") != table.strip():
|
||
changed.append(t["path"].name)
|
||
|
||
if args.write:
|
||
if missing:
|
||
print("[WARN] 以下文档缺少受管锚点,未写入:"
|
||
+ ", ".join(missing))
|
||
print(f" 请在任务表上下各加一行锚点:\n"
|
||
f" {TARGETS[0]['begin']}\n {TARGETS[0]['end']}")
|
||
if changed:
|
||
print("已更新任务表:" + ", ".join(changed))
|
||
if not changed and not missing:
|
||
print("任务表已是最新,无需改动。")
|
||
return 0
|
||
|
||
if problems:
|
||
for p in problems:
|
||
print(f"[FAIL] {p}", file=sys.stderr)
|
||
return 1
|
||
|
||
if missing or changed:
|
||
if missing:
|
||
print(f"[FAIL] 以下文档缺少受管锚点 "
|
||
f"({TARGETS[0]['begin']} / {TARGETS[0]['end']}):")
|
||
for n in missing:
|
||
print(f" - {n}")
|
||
if changed:
|
||
print(f"[FAIL] 以下文档的任务表与 Src/freertos.c 不一致:")
|
||
for n in changed:
|
||
print(f" - {n}")
|
||
print("\n事实源(Src/freertos.c):")
|
||
for name, words, prio, _ in tasks:
|
||
print(f" {name}: {words} words / {prio}")
|
||
print("\n期望表格(README 口径):")
|
||
print(render_table(tasks, TARGETS[0]["header"], True))
|
||
print("\n修复:运行 `python scripts/gen_task_table.py --write`")
|
||
return 1
|
||
|
||
print(f"任务表一致({len(tasks)} 个任务,"
|
||
f"{len(TARGETS)} 个文档)。")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main(sys.argv))
|