156 lines
6.6 KiB
Python
156 lines
6.6 KiB
Python
#!/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()
|