Files
STM32F4-Base/tools/arch_diagram/gen_cfg_arch_svg.py
T

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()