2525c2d948
用户裁定两条:① 剩余裸写端口的地方加「工厂默认」限定词,不改固件行为; ② 生效语义按需求口径「保存即生效」定,现实现(端口下次重 bind 才生效)是 要改的一侧 → 建任务登记,本笔不动代码行为。 限定词(24 处 / 17 文件):AGENTS 数据主线、App/protocol/README、根 README 文档索引、Src/freertos.c 三处任务注释、docs/README、项目启动顺序管理、存储 架构说明、软件架构与运行时规格(2)、RAM 使用分析、产品需求设计文档(5)、 项目架构总览(3)、test/host/README、iec104_master 流程注释、IEC104 上行设 计说明、继电保护功能复现规划。配图两张同批重跑:gen_arch_svg.py 与 gen_memory_svg.py(脚本与产物同一笔)。 两处陈旧计数(都做过双源核证): - 测试开发标准流程 像素层「60 内容页」→ 内容页黄金参考 66 张(64 页); 双源 = simulator/tests/ref/shot_*.bin 实数 66 与 check_content_pages.py 的 PAGES 66 条(含两个 _p2)。 - .pre-commit-config.yaml 的 uinav-selftest 注释「18 张黄金参考」→ 写成 「接线当时 18 张、现 66 张」:git log -S 定到 6a904c1b(2026-10-25),那 天 ref 目录确实 18 张,写时为真,但它描述的是钩子现有行为,属当前态。 口径修正与登记: - 页面扩充详细方案「通讯设置 = 无配置模型(端口与 CA 都是 #define)」→ 标 「当时」,并写明现已在 /config.ini 的 [comm] 段可配、UI 通讯设置页可改。 - 产品需求 §通讯配置:删掉「改后需重启生效」这句把两件事混为一谈的话,改成 需求要求「保存即生效」+ 现实现(公共地址重启、端口下次重 bind)+ 指向登记簿。 - docs/任务规划/登记簿.md §1 新增「104 端口改后立刻重 bind」:含隐性耦合 —— .serialtest/config.json 的必检锚点写死 listening on 2404,端口一改串口 回归必假失败;改行为时要么锚点只查 listening on,要么由脚本注入端口。 复扫(旧形态归零判据)剩 20 处未带限定词,逐条定性为刻意不改:默认值定义处、 出厂模板与断言、usage 与 argparse default、UI 黄金参考显示值、台架主站地址 (同数不同义)、串口锚点、叙事句、引用关系图坐标。逐条理由见一次性台账。 门禁:check_doc_links --sections-strict 对 16 篇改动 .md 逐个 rc=0; check_mermaid --all 31 文件 62 围栏新发 0;check_nested_comment --all 449 文件新发 0;check_bom_drift --all 99 个名单内文件无漂移; check_generated_fresh PASS(25 生成器 / 21 一致 / 4 只减不增基线);字节体检 无新增 BOM / CR / U+FFFD / 行尾空格(三连空行与 HEAD 逐项相同 = 存量)。 /keil-build 0 错 0 警,Program Size Code=246720 RO=29836 RW=2052 ZI=149708 与凭证同码:本笔只碰注释与配图文字,无固件行为变更。
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()
|