#!/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('' % (W, total_h)) add('%s' % (PAD, 40, TITLE_FONT, esc("STM32F4-Base 项目分层架构"))) add('%s' % (PAD, 62, SUB_FONT, esc("STM32F407ZGTx @168MHz · FreeRTOS V10.3.1 (CMSIS-RTOS2) · 上层只依赖相邻下层"))) for (layer, rows, bx, by, bw, bh) in placements: # 框背景 + 边框 add('' % (bx, by, bw, bh, layer["bg"], layer["color"])) # 标题(层名 + 目录/职责说明) add('%s' % (bx + BAND_PAD_X, by + 26, BAND_TITLE_FONT, layer["color"], esc(layer["title"]))) tw = text_w(layer["title"], BAND_TITLE_FONT) add('%s' % (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('' % (cx, cy, cw, CHIP_H, layer["color"])) add('%s' % (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('' % (ax, y0, ax, y1 - 7)) add('' % (ax, y1)) add('%s' % (PAD, total_h - 18, esc(FOOT))) svg = ('\n%s\n\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()