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

156 lines
6.6 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/RemoDispBus帧格式.svg —— 协议帧结构图(纯标准库,无需第三方依赖)。
为什么不用 Mermaid / HTML:协议"帧"是等宽字节的连续分段,需要"字段宽 ∝ 字节数"的
精确比例与逐字段标注。Mermaid 盒子按内容自适应宽度、还会自动加箭头(把连续字节流画成
有向边,失真);HTML 要截图才能变成可移植图片。SVG 可精确定位(像素 ∝ 字节)、矢量无损、
纯文本可 diff / 可 lint、浏览器直接用系统字体渲染、Markdown 里 ![]() 即可嵌入。
本脚本沿用 tools/arch_diagram/ 既有约定(纯标准库拼 SVG + ElementTree 校验 +
UTF-8/LF 落盘 + 顶部数据改后重跑)。
用法(仓库根目录,任意 python3):
python tools/arch_diagram/gen_frame_svg.py
输出:
docs/images/RemoDispBus帧格式.svg
改字段 / 配色:编辑本文件顶部的 FIELDS / TITLE / SUB / FOOT 后重跑。
画别的帧:复制本文件,改 FIELDS 与 OUT / TITLE(build() 与 FIELDS 解耦,通用)。
"""
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", "RemoDispBus帧格式.svg")
# ----------------------------- 画布与尺寸参数 -----------------------------
PAD = 40 # 左右留白
PX_PER_BYTE = 52 # 每字节像素宽(1B 字段 = 52px;须足以容纳最长字段名 / 标注)
DATA_UNITS = 5 # 变长 DATA 段的"代表宽度"(按 N 字节画,仅示意,标注 LEN B)
BAR_Y = 116 # 字段盒顶 y(标题 / 副标题 / 偏移标尺在其上方)
BAR_H = 74 # 字段盒高
NAME_DY = 30 # 字段名在盒内的 y 偏移
SIZE_DY = 54 # 尺寸标注在盒内的 y 偏移
RULER_DY = 20 # 偏移标尺线在盒顶上方
TITLE_FONT = 20
SUB_FONT = 13
NAME_FONT = 15
SIZE_FONT = 12
RULER_FONT = 12
FOOT_FONT = 12
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
# ----------------------------- 帧字段(改这里) -----------------------------
# name=字段名 bytes=字节数(None=变长段,按 DATA_UNITS 画) label=尺寸标注 color/bg=配色
FIELDS = [
dict(name="TAG", bytes=1, label="1 B", color="#2563eb", bg="#eff6ff"),
dict(name="CMD", bytes=1, label="1 B", color="#2563eb", bg="#eff6ff"),
dict(name="LEN_H", bytes=1, label="1 B", color="#0d9488", bg="#f0fdfa"),
dict(name="LEN_L", bytes=1, label="1 B", color="#0d9488", bg="#f0fdfa"),
dict(name="DATA", bytes=None, label="LEN B · 变长", color="#d97706", bg="#fffbeb"),
dict(name="CRC", bytes=1, label="1 B", color="#16a34a", bg="#f0fdf4"),
]
TITLE = "RemoDispBus 协议帧格式"
SUB = "整帧 = 4 + LEN + 1 字节 · 大端(网络字节序)· CRC = DATA 段逐字节异或(不含 TAG/CMD/LEN)"
FOOT = ("TAG:0xAA=客户端帧 / 0xBB=服务端帧 · DATA 为变长段(LEN 字节,0–255,图中按 5 字节示意)· "
"详见 docs/RemoDispBus协议与接收设计说明.md §1")
# ----------------------------- 工具函数 -----------------------------
def esc(s):
return html.escape(s, quote=True)
def field_w(f):
return (f["bytes"] if f["bytes"] is not None else DATA_UNITS) * PX_PER_BYTE
def offset_labels(fields):
"""按字段字节数累计边界偏移;遇变长段后,其后偏移记为 '<前缀>+LEN[+n]'。"""
labs, off, expr = ["0"], 0, None
for f in fields:
if f["bytes"] is None:
expr = ("%d+LEN" % off) if off is not None else ("%s+LEN" % (expr or "0"))
off = None
labs.append(expr)
elif off is not None:
off += f["bytes"]
labs.append(str(off))
else:
expr = "%s+%d" % (expr, f["bytes"])
labs.append(expr)
return labs
# ----------------------------- 生成 -----------------------------
def build():
out = []
add = out.append
frame_w = sum(field_w(f) for f in FIELDS)
W = frame_w + 2 * PAD
# 预计算各字段 x / 宽
segs, x = [], PAD
for f in FIELDS:
w = field_w(f)
segs.append([f, x, w])
x += w
labels = offset_labels(FIELDS)
total_h = BAR_Y + BAR_H + 56
add('<rect x="0" y="0" width="%d" height="%d" fill="#ffffff"/>' % (W, total_h))
# 标题 + 副标题
add('<text x="%d" y="40" font-size="%d" font-weight="700" fill="#0f172a">%s</text>'
% (PAD, TITLE_FONT, esc(TITLE)))
add('<text x="%d" y="64" font-size="%d" fill="#64748b">%s</text>'
% (PAD, SUB_FONT, esc(SUB)))
# 偏移标尺(每个字段边界一个刻度 + 偏移标注)
ruler_y = BAR_Y - RULER_DY
add('<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#cbd5e1" stroke-width="1"/>'
% (PAD, ruler_y, PAD + frame_w, ruler_y))
bounds = [PAD] + [s[1] + s[2] for s in segs]
for i, bx in enumerate(bounds):
add('<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#94a3b8" stroke-width="1.5"/>'
% (bx, ruler_y - 4, bx, ruler_y + 4))
lab = labels[i] if i < len(labels) else "?"
add('<text x="%d" y="%d" font-size="%d" fill="#94a3b8" text-anchor="middle">%s</text>'
% (bx, ruler_y - 8, RULER_FONT, esc(lab)))
# 字段盒(名在上、尺寸在下)
for (f, bx, bw) in segs:
add('<rect x="%d" y="%d" width="%d" height="%d" fill="%s" stroke="%s" stroke-width="1.5"/>'
% (bx, BAR_Y, bw, BAR_H, f["bg"], f["color"]))
cx = bx + bw / 2.0
add('<text x="%.1f" y="%d" font-size="%d" font-weight="700" fill="#0f172a" '
'text-anchor="middle">%s</text>'
% (cx, BAR_Y + NAME_DY, NAME_FONT, esc(f["name"])))
add('<text x="%.1f" y="%d" font-size="%d" fill="%s" text-anchor="middle">%s</text>'
% (cx, BAR_Y + SIZE_DY, SIZE_FONT, f["color"], esc(f["label"])))
# 脚注
add('<text x="%d" y="%d" font-size="%d" fill="#94a3b8">%s</text>'
% (PAD, total_h - 16, FOOT_FONT, 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, W, total_h
def main():
svg, W, 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()