#!/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%s\n\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()