#!/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/UI设计/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('' % (W, total_h))
# 标题 + 副标题
add('%s'
% (PAD, TITLE_FONT, esc(TITLE)))
add('%s'
% (PAD, SUB_FONT, esc(SUB)))
# 偏移标尺(每个字段边界一个刻度 + 偏移标注)
ruler_y = BAR_Y - RULER_DY
add(''
% (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(''
% (bx, ruler_y - 4, bx, ruler_y + 4))
lab = labels[i] if i < len(labels) else "?"
add('%s'
% (bx, ruler_y - 8, RULER_FONT, esc(lab)))
# 字段盒(名在上、尺寸在下)
for (f, bx, bw) in segs:
add(''
% (bx, BAR_Y, bw, BAR_H, f["bg"], f["color"]))
cx = bx + bw / 2.0
add('%s'
% (cx, BAR_Y + NAME_DY, NAME_FONT, esc(f["name"])))
add('%s'
% (cx, BAR_Y + SIZE_DY, SIZE_FONT, f["color"], esc(f["label"])))
# 脚注
add('%s'
% (PAD, total_h - 16, FOOT_FONT, esc(FOOT)))
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()