Files
STM32F4-Base/tools/arch_diagram/gen_cfg_arch_svg.py
T
wandering1 a6cdebde62 文档体系重组与 D-7 一致性门禁落地,清理芯片手册孤儿图片
- D-6 目录规范化:docs 根目录散落文档归入 UI设计/子系统设计/架构设计/审查记录/
  环境与部署/网络 等子目录,去掉目录名空格,重命名 嵌入式C语言代码规范_V1.0.md
- D-7 三档一致性门禁:新增 scripts/check_doc_links.py(链接存在性)、
  scripts/gen_task_table.py(FreeRTOS 任务表数值)、scripts/check_mem_numbers.py
  (内存/Flash 体积),均支持 --check/--write/--diff,pre-commit 钩子同步注册
- D-5 文档索引:新建 docs/README.md,新建 docs/文档规范.md 约束目录命名与链接写法
- D-1 芯片手册瘦身:删除 1069 张无引用孤儿图片(77.06 MB),
  docs/芯片手册 120 MB → 41 MB,已引用图片零断链
- 同步 C 源码注释中的文档路径引用(D-6 重组后),构建 0 错误 0 警告
2026-09-26 00:51:53 +08:00

220 lines
8.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""生成 docs/images/配置文件分层架构.svg —— 通用配置文件(/config.ini)分层架构图(纯标准库,无需第三方依赖)。
由 gen_net_stack_svg.py 复制改数据区:配置体系六层结构
(docs/子系统设计/配置文件设计说明.md §4.1):
PC 端(编辑器/FTP 客户端)→ lftpd FTP 服务 → /config.ini 文件 → cfg_file 引擎 → 段适配(键表)→ 消费者。
用法(仓库根目录,任意 python3):
python tools/arch_diagram/gen_cfg_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 = 1200
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_PC = dict(
title="PC 端(外部)", sub="文本编辑器 + FTP 客户端", color="#475569", bg="#f1f5f9",
chips=["文本编辑器:改 /config.ini(注释 / 加段 / 改值)",
"FTP 客户端:RETR 下载 / STOR 上传(端口 21)"],
)
L_FTP = dict(
title="lftpd FTP 服务(端口 21)", sub="ftpTask · 单连接 · root = /(= 卷根)", color="#7c3aed", bg="#f5f3ff",
chips=["RETR / STOR / LIST / DELE / CWD(lftpd 现状能力)",
"STOR = 原地覆盖(无 tmp+rename)",
"路径规范化封死 .. 穿越"],
)
L_FILE = dict(
title="/config.ini(通用配置文件)", sub="FatFS 卷根(NAND GD5F2GQ5UE · dhara FTL)", color="#0d9488", bg="#f0fdfa",
chips=["INI 文本:[段] / key=value / ; 注释",
"CRLF · UTF-8(容忍 BOM)· ≤ 4KB",
"设备写:tmp + rename(原子);变更识别:size + mtime"],
)
L_ENGINE = dict(
title="cfg_file(配置文件引擎)", sub="App/cfg_file.c · 双端共享", color="#2563eb", bg="#eff6ff",
chips=["INI 解析 / 序列化(逐键校验 · 容错 · 自愈)",
"外科手术式保存(保留注释 / 未知段 → 降级安全)",
"2s 变更轮询(f_stat)+ 段变更回调",
"出厂模板生成 · 旧 NCFG blob 迁移"],
)
L_SECT = dict(
title="段适配(键表)", sub="段 RAM 副本 = 模块静态结构体(稳态 0 增量)", color="#16a34a", bg="#f0fdf4",
chips=["net_cfg [network]:ip / mask / gateway / mac",
"未来:[adc] · [rs485] · [ftp] ...(三步接入)"],
)
L_CONS = dict(
title="消费者", sub="读 / 写段 RAM 副本 + 应用", color="#e11d48", bg="#fff1f2",
chips=["netTask:组网 / 重建(代次号自愈)",
"UI 网口设置页:显示 / 编辑 / 保存(零改动)",
"未来模块:cfg_file_section_data() 取段数据"],
)
# 每一行可放多个"同级并列"的层(上层只依赖下方各层)
ROWS = [
[L_PC],
[L_FTP],
[L_FILE],
[L_ENGINE],
[L_SECT],
[L_CONS],
]
FOOT = "稳态 RAM 零增量(段数据 = 模块既有结构体);暂态堆缓冲 ≤9KB(仅 load/save 时);FF_FS_REENTRANT=1 卷锁串行化多任务 FS 访问;模拟器端 = 宿主文件 sim_config.ini(同核心)"
# ----------------------------- 工具函数 -----------------------------
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("通用配置文件分层架构(/config.ini)")))
add('<text x="%d" y="%d" font-size="%d" fill="#64748b">%s</text>'
% (PAD, 62, SUB_FONT,
esc("INI 文本文件 · FTP 上传/下载 · 段键表引擎 · MCU/模拟器双端共享")))
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()