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)。
228 lines
9.7 KiB
Python
228 lines
9.7 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""生成 docs/images/项目架构总览.svg —— 分层架构图(纯标准库,无需第三方依赖)。
|
||
|
||
为什么不用 Mermaid:自动布局不可控,多节点/跨层依赖会画成交叉的"意大利面"。
|
||
本脚本手工排布:每层一个圆角框(标题在框内顶部 + 模块芯片自动换行),
|
||
同一"行"可放多个并列层(如 应用层 与 UI 层 同级并排),行间单向下箭头。
|
||
|
||
用法(仓库根目录,任意 python3):
|
||
python tools/arch_diagram/gen_arch_svg.py
|
||
输出:
|
||
docs/images/项目架构总览.svg
|
||
改架构时:编辑本文件顶部的 LAYERS / ROWS 数据后重跑即可。
|
||
"""
|
||
import html
|
||
import os
|
||
|
||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
OUT = os.path.join(ROOT, "docs", "images", "项目架构总览.svg")
|
||
|
||
# ----------------------------- 画布与尺寸参数 -----------------------------
|
||
W = 1560
|
||
PAD = 26
|
||
TITLE_FONT = 20
|
||
SUB_FONT = 13
|
||
BAND_TITLE_FONT = 17
|
||
BAND_SUB_FONT = 12
|
||
CHIP_FONT = 13
|
||
|
||
CHIP_H = 38 # 芯片盒高
|
||
CHIP_GAP = 10 # 芯片水平间距
|
||
CHIP_ROW_GAP = 10 # 芯片行间距
|
||
CHIP_PAD = 16 # 芯片内左右留白
|
||
BAND_PAD_X = 18 # 框内左右留白
|
||
BAND_TITLE_H = 38 # 框内标题区高
|
||
BAND_PAD_BOTTOM = 16
|
||
BAND_H_GAP = 26 # 同一行内并列框的间距
|
||
BAND_V_GAP = 44 # 行间距(放箭头)
|
||
TITLE_H = 78 # 顶部标题区高
|
||
|
||
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
|
||
|
||
# ----------------------------- 架构数据(改这里) -----------------------------
|
||
L_BIZ = dict(
|
||
title="① 业务层", sub="顶层共享配置 · 被 UI 与基础服务共用", color="#2563eb", bg="#eff6ff",
|
||
chips=["net_cfg(网口配置:IP/掩码/网关/MAC · /config.ini [network] 段)"],
|
||
)
|
||
L_UI = dict(
|
||
title="② UI 层", sub="Drivers/BSP/UI · 一份显存三出(模拟器/屏板/远程)", color="#7c3aed", bg="#f5f3ff",
|
||
chips=["ui_port(显示端口)", "ui_menu / ui_menu_view(级联菜单)", "ui_menu_tab(菜单树生成物)",
|
||
"ui_content_page(内容页框架)", "ui_page_registry(页面注册表 · 生成物)", "ui_page_*(20+ 页插件)",
|
||
"ui_data(数据供给契约)", "ui_net_sink(远程显示 :7003)", "ui_standby(待机屏)",
|
||
"ui_font_cn_12(中文字体)", "ui_deco(浅 V 装饰)"],
|
||
)
|
||
L_SVC = dict(
|
||
title="③ 基础服务层", sub="应用侧服务 / 编排", color="#0d9488", bg="#f0fdfa",
|
||
chips=["app_main(启动编排)", "sys_events(就绪屏障)", "task/(9 任务)",
|
||
"algo/(语义取数与有效性~保护判据 · 遥测快照 · 录波数据面)",
|
||
"protocol/iec104(104 上行 :2404)", "ui_data_feed(App→UI 装配)",
|
||
"sys_log · soe(记录引擎)", "console_log(UART5 控制台单写者)",
|
||
"cfg_file · prot_set_store(配置引擎)",
|
||
"lftpd(FTP :21)", "time_sync(TCP 对时 :8888)"],
|
||
)
|
||
L_MW = dict(
|
||
title="④ 中间件层", sub="通用库 / 协议栈", color="#16a34a", bg="#f0fdf4",
|
||
chips=["net_socket / net_select / net_events / net_listener / net_pending(BSD API + 阻塞模型)",
|
||
"FatFs(文件系统)", "nand_ftl(dhara FTL)", "LVGL v9.3(图形库)",
|
||
"sys_clock(墙钟基准)", "task_watchdog(IWDG 判定)"],
|
||
)
|
||
L_DRV = dict(
|
||
title="⑤ 驱动层", sub="Drivers/BSP · 芯片时序/中断", color="#d97706", bg="#fffbeb",
|
||
chips=["ch395f(SPI2 + DMA)", "gd5f2gq5ue(SPI1)", "tpafe5160(并行 16 位)",
|
||
"sd2506(I2C1)", "rs485(UART4 + DE)",
|
||
"sb_link · sb_proto(屏板链路 · USART1 700 kbaud)"],
|
||
)
|
||
L_KERN = dict(
|
||
title="⑥ 内核 / HAL", sub="调度 · 外设抽象", color="#475569", bg="#f1f5f9",
|
||
chips=["FreeRTOS V10.3.1(CMSIS-RTOS2 · heap_4)", "STM32 HAL(时基 TIM7)",
|
||
"CMSIS / 启动文件"],
|
||
)
|
||
L_HW = dict(
|
||
title="⑦ 硬件", sub="STM32F407ZGTx @168MHz", color="#e11d48", bg="#fff1f2",
|
||
chips=["CH395F 以太网(SPI2)", "GD5F2GQ5UE NAND(SPI1)", "TPAFE5160 ADC(并行)",
|
||
"SD2506 RTC(I2C1)", "RS-485(UART4 · 未接线)", "屏板(USART1 · 700 kbaud)",
|
||
"RS232#1 日志(UART5 · 115200)", "6x LED"],
|
||
)
|
||
|
||
# 每一行可放多个"同级并列"的层(上层只依赖下方各层)
|
||
ROWS = [
|
||
[L_BIZ], # 顶层:业务层(仅共享配置 net_cfg)
|
||
[L_UI, L_SVC], # UI 层与基础服务层同级并排,均使用 net_cfg
|
||
[L_MW], # 中间件层(通用库 / 协议栈)
|
||
[L_DRV],
|
||
[L_KERN],
|
||
[L_HW],
|
||
]
|
||
|
||
FOOT = "上层只依赖相邻下层(单向);UI 层与基础服务层同级并列,均使用顶层 net_cfg。跨层调用细节见 docs/架构设计/项目架构总览.md §1.2"
|
||
|
||
|
||
# ----------------------------- 工具函数 -----------------------------
|
||
def esc(s):
|
||
return html.escape(s, quote=True)
|
||
|
||
|
||
def text_w(s, fs):
|
||
"""粗略估算文本像素宽:CJK/全角/圈码/箭头 ≈ 1.0em,其余 ≈ 0.58em。
|
||
|
||
取偏大估计(宁可芯片略宽,不可文字溢出盒外)。
|
||
"""
|
||
w = 0.0
|
||
for ch in s:
|
||
w += fs * (1.0 if ord(ch) >= 0x2460 else 0.58)
|
||
return w
|
||
|
||
|
||
def chip_w(label):
|
||
return text_w(label, CHIP_FONT) + 2 * CHIP_PAD
|
||
|
||
|
||
def wrap_chips(chips, avail):
|
||
rows, cur, cur_w = [], [], 0.0
|
||
for c in chips:
|
||
cw = chip_w(c)
|
||
need = cw if not cur else cur_w + CHIP_GAP + cw
|
||
if cur and need > avail:
|
||
rows.append(cur)
|
||
cur, cur_w = [c], cw
|
||
else:
|
||
cur.append(c)
|
||
cur_w = need
|
||
if cur:
|
||
rows.append(cur)
|
||
return rows
|
||
|
||
|
||
# ----------------------------- 生成 -----------------------------
|
||
def build():
|
||
out = []
|
||
add = out.append
|
||
|
||
# 布局:逐行排布(行内可并列多个框,行高取该行最高框)
|
||
placements = [] # (layer, chip_rows, x, y, w, h)
|
||
y = TITLE_H
|
||
for row in ROWS:
|
||
n = len(row)
|
||
bw = (W - 2 * PAD - BAND_H_GAP * (n - 1)) // n
|
||
bands = []
|
||
for i, layer in enumerate(row):
|
||
bx = PAD + i * (bw + BAND_H_GAP)
|
||
avail = bw - 2 * BAND_PAD_X
|
||
rows = wrap_chips(layer["chips"], avail)
|
||
bh = BAND_TITLE_H + len(rows) * CHIP_H + (len(rows) - 1) * CHIP_ROW_GAP + BAND_PAD_BOTTOM
|
||
bands.append([layer, rows, bx, y, bw, bh])
|
||
row_h = max(b[5] for b in bands)
|
||
for b in bands:
|
||
b[5] = row_h # 同行等高(视觉"同级")
|
||
placements.extend(bands)
|
||
y += row_h + BAND_V_GAP
|
||
total_h = y - BAND_V_GAP + 54
|
||
|
||
add('<rect x="0" y="0" width="%d" height="%d" fill="#ffffff"/>' % (W, total_h))
|
||
add('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="#0f172a">%s</text>'
|
||
% (PAD, 40, TITLE_FONT, esc("STM32F4-Base 项目分层架构")))
|
||
add('<text x="%d" y="%d" font-size="%d" fill="#64748b">%s</text>'
|
||
% (PAD, 62, SUB_FONT,
|
||
esc("STM32F407ZGTx @168MHz · FreeRTOS V10.3.1 (CMSIS-RTOS2) · 上层只依赖相邻下层")))
|
||
|
||
for (layer, rows, bx, by, bw, bh) in placements:
|
||
# 框背景 + 边框
|
||
add('<rect x="%d" y="%d" width="%d" height="%d" rx="12" fill="%s" stroke="%s" '
|
||
'stroke-opacity="0.45"/>' % (bx, by, bw, bh, layer["bg"], layer["color"]))
|
||
# 标题(层名 + 目录/职责说明)
|
||
add('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="%s">%s</text>'
|
||
% (bx + BAND_PAD_X, by + 26, BAND_TITLE_FONT, layer["color"], esc(layer["title"])))
|
||
tw = text_w(layer["title"], BAND_TITLE_FONT)
|
||
add('<text x="%.0f" y="%d" font-size="%s" fill="#64748b">%s</text>'
|
||
% (bx + BAND_PAD_X + tw + 12, by + 26, BAND_SUB_FONT, esc(layer["sub"])))
|
||
# 芯片
|
||
cy = by + BAND_TITLE_H
|
||
for row in rows:
|
||
cx = bx + BAND_PAD_X
|
||
for c in row:
|
||
cw = chip_w(c)
|
||
add('<rect x="%d" y="%d" width="%d" height="%d" rx="8" fill="#ffffff" '
|
||
'stroke="%s" stroke-opacity="0.55"/>' % (cx, cy, cw, CHIP_H, layer["color"]))
|
||
add('<text x="%d" y="%d" font-size="%d" fill="#1e293b" text-anchor="middle">%s</text>'
|
||
% (cx + cw / 2, cy + CHIP_H / 2 + CHIP_FONT * 0.35, CHIP_FONT, esc(c)))
|
||
cx += cw + CHIP_GAP
|
||
cy += CHIP_H + CHIP_ROW_GAP
|
||
|
||
# 行间箭头:每个上层框向下引一条(顶层两框 → 两条,体现"都依赖下层")
|
||
row_first = [] # 每行首个 placement 的下标
|
||
idx = 0
|
||
for row in ROWS:
|
||
row_first.append(idx)
|
||
idx += len(row)
|
||
for r in range(len(ROWS) - 1):
|
||
y0 = placements[row_first[r]][3] + placements[row_first[r]][5] + 6
|
||
y1 = placements[row_first[r + 1]][3] - 8
|
||
for i in range(row_first[r], row_first[r] + len(ROWS[r])):
|
||
(_, _, bx, _, bw, _) = placements[i]
|
||
ax = bx + bw / 2
|
||
add('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="#94a3b8" '
|
||
'stroke-width="2.5"/>' % (ax, y0, ax, y1 - 7))
|
||
add('<path d="M %.1f %.1f l -6 -9 h 12 z" fill="#94a3b8"/>' % (ax, y1))
|
||
|
||
add('<text x="%d" y="%d" font-size="12" fill="#94a3b8">%s</text>'
|
||
% (PAD, total_h - 18, esc(FOOT)))
|
||
|
||
svg = ('<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" '
|
||
'viewBox="0 0 %d %d" font-family="%s">\n%s\n</svg>\n'
|
||
% (W, total_h, W, total_h, FONT, "\n".join(out)))
|
||
return svg, total_h
|
||
|
||
|
||
def main():
|
||
svg, h = build()
|
||
with open(OUT, "w", encoding="utf-8", newline="\n") as f:
|
||
f.write(svg)
|
||
import xml.etree.ElementTree as ET
|
||
ET.fromstring(svg)
|
||
print("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, h, OUT))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|