c377fc8c15
- 采集与算法层设计说明第 5 节由单段说明拆成 5.1~5.5:为什么需要这一层 (三个缺口 + 两个被否决的替代方案)、设计思想(五条不变量)、环内布局 与 start 推导、API 契约、要点与禁令。 - 新增两张配图(生成器 + 产物同批提交): 采样环缓冲_为什么需要.svg —— 上游 1 kHz 覆盖写只交最新一帧 vs 下游 20 点滑窗,缺口 19 点,以及三个消费者必须同源; 采样环缓冲_环形布局.svg —— 40 格环里哪 20 格是本窗、写指针指「下一格」、 回绕方向与三条设计不变量。 - 5.5 补一条「读者与写者必须同任务」,与故障录波设计说明的取窗竞态口径一致。 - 门禁:doc-links --sections-strict 通过(28 处章节引用全部可判定); id-check 新增行无未登记编号;generated-fresh 21 个生成器 16 一致、0 新漂移。
277 lines
9.8 KiB
Python
277 lines
9.8 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""生成 docs/images/采样环缓冲_环形布局.svg —— 采样环缓冲的设计思想落在图上的三件事:
|
||
写指针指「下一格」而不是「最后一格」、窗永远紧贴指针身后、整窗随每次写入右移一格。
|
||
|
||
为什么不用 Mermaid:本图要读的是"40 个格子里哪 20 格是窗、指针停在第几格"——
|
||
Mermaid 画不出等宽格子与指针位置,盒子宽度随文本变,"窗贴着指针"这个关系就没了。
|
||
|
||
用法(仓库根目录,任意 python3):
|
||
python tools/arch_diagram/gen_sample_ring_layout_svg.py
|
||
输出:
|
||
docs/images/采样环缓冲_环形布局.svg
|
||
改内容:容量(周波数 / 每周波点数)或窗长变了才重画;调坐标 / 文案编辑下方常量后重跑。
|
||
"""
|
||
import html
|
||
import os
|
||
import sys
|
||
import xml.etree.ElementTree as ET
|
||
|
||
|
||
def emit(s):
|
||
"""控制台打印按目标编码替换,绝不因 GBK 终端遇上 ²/− 之类字符而崩
|
||
(generated-fresh 钩子会把本脚本拷进临时目录重跑,崩了就是一片假红)。"""
|
||
enc = getattr(sys.stdout, "encoding", None) or "ascii"
|
||
sys.stdout.write(s.encode(enc, "replace").decode(enc, "replace") + "\n")
|
||
|
||
|
||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
OUT = os.path.join(ROOT, "docs", "images", "采样环缓冲_环形布局.svg")
|
||
|
||
# ----------------------------- 画布与尺寸 -----------------------------
|
||
W = 1060
|
||
H = 580
|
||
PAD = 46
|
||
RIGHT = W - PAD
|
||
|
||
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
|
||
|
||
TITLE_FONT = 21
|
||
SUB_FONT = 13
|
||
CARD_FONT = 13.5
|
||
BODY_FONT = 11.5
|
||
TINY_FONT = 11
|
||
SLOT_FONT = 9
|
||
|
||
INK = "#0f172a"
|
||
LINE = "#334155"
|
||
MUTED = "#64748b"
|
||
FAINT = "#94a3b8"
|
||
RULE = "#cbd5e1"
|
||
WHITE = "#ffffff"
|
||
|
||
C_BLUE = "#2563eb"
|
||
BG_BLUE = "#eff6ff"
|
||
C_AMBER = "#d97706"
|
||
BG_AMBER = "#fffbeb"
|
||
C_GREEN = "#16a34a"
|
||
BG_GREEN = "#f0fdf4"
|
||
C_ROSE = "#e11d48"
|
||
BG_GREY = "#f1f5f9"
|
||
|
||
# ----------------------------- 数据(改这里) -----------------------------
|
||
DEPTH = 40 # SAMPLE_RING_DEPTH
|
||
WIN_N = 20 # 示例窗长(一周波)
|
||
HEAD = 30 # 示例写指针位置
|
||
START = (HEAD + DEPTH - WIN_N) % DEPTH # = 10
|
||
|
||
TITLE = "40 格环:写指针指「下一格」,窗永远紧贴它身后"
|
||
SUB1 = "DEPTH = 2 周波 × 20 点 = 40;窗长 n 取 1~40,取的是 [head−n, head−1] 这 n 格。"
|
||
SUB2 = "图例:蓝 = 本窗 20 点,灰 = 更旧的 10 点,琥珀 = 最旧的 10 点(下一批被覆盖)。"
|
||
SUB3 = ("窗首 start = (head + 40 − n) % 40 —— 先加 40 再减,减法就不落到负数;"
|
||
"每写一格整窗右移一格。")
|
||
|
||
MARK_START = "start(最老点)"
|
||
MARK_NEW = "head−1(最新点)"
|
||
MARK_HEAD = "head(下一拍写这里)"
|
||
WRAP_NOTE = "写满后回绕:39 的下一格是 0 —— 窗可能跨过环尾,所以取数必须逐点取模"
|
||
|
||
CARDS = [
|
||
("整帧写完才推进 head", C_GREEN, BG_GREEN,
|
||
"9 路语义值一次写齐、再动指针。单帧内不存在「半新半旧」,"
|
||
"消费侧取到的窗永远落在整帧边界上。"),
|
||
("只存不算是分层契约", C_BLUE, BG_BLUE,
|
||
"同刻性归上游 adc_input 负责;本层不假设、不校正,也不做任何跨通道运算——"
|
||
"所以它能脱离硬件做主机侧单测。"),
|
||
("容量取 2 周波", C_AMBER, BG_AMBER,
|
||
"1 周波就够算全周傅氏;另 1 周波是给 1.5~2 周波长窗(抗衰减直流)留的余量,"
|
||
"改容量只动一个宏。"),
|
||
]
|
||
FOOTNOTE = ("n = 40 时 start == head:环满时「下一拍要覆盖的那一格」恰好就是最老点 —— 不是特例,是唯一自洽解。"
|
||
"同理 n = 20 时窗正好是 head 身后的 20 格,不多不少。")
|
||
|
||
# ----------------------------- 布局 -----------------------------
|
||
SLOT_W = 22
|
||
SLOT_GAP = 2
|
||
SLOT_H = 54
|
||
SLOT_X0 = PAD
|
||
ROW_Y = 200
|
||
|
||
MARK_Y = 176 # 标记文字基线
|
||
STEM_Y1 = 184 # 标记引线
|
||
STEM_Y2 = ROW_Y
|
||
|
||
WRAP_Y = 286
|
||
WRAP_TEXT_Y = 308
|
||
|
||
CARD_TOP = 340
|
||
CARD_W = 300
|
||
CARD_GAP = 19
|
||
CARD_H = 120
|
||
|
||
|
||
# ----------------------------- 工具函数 -----------------------------
|
||
def esc(s):
|
||
return html.escape(s, quote=True)
|
||
|
||
|
||
def text_w(s, fs):
|
||
"""粗略估算文本像素宽:CJK/全角 ≈ 1.0em,其余 ≈ 0.58em(宁可偏大)。"""
|
||
return sum(fs * (1.0 if ord(ch) >= 0x2E80 else 0.58) for ch in s)
|
||
|
||
|
||
OVERFLOW = []
|
||
|
||
|
||
def check_fit(s, fs, avail, where):
|
||
if text_w(s, fs) > avail:
|
||
OVERFLOW.append("%s: %.0f > %.0f [%s]" % (where, text_w(s, fs), avail, s))
|
||
|
||
|
||
def txt(x, y, s, fs, fill, weight=400, anchor=None):
|
||
a = "" if anchor is None else ' text-anchor="%s"' % anchor
|
||
w = "" if weight == 400 else ' font-weight="%d"' % weight
|
||
return ('<text x="%.1f" y="%.1f" font-size="%g"%s%s fill="%s">%s</text>'
|
||
% (x, y, fs, a, w, fill, esc(s)))
|
||
|
||
|
||
def line(x1, y1, x2, y2, color=LINE, width=2.0, dash=None):
|
||
d = "" if dash is None else ' stroke-dasharray="%s"' % dash
|
||
return ('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="%g"%s/>'
|
||
% (x1, y1, x2, y2, color, width, d))
|
||
|
||
|
||
def rect(x, y, w, h, fill, stroke=None, rx=8, sw=1.6, dash=None):
|
||
s = "" if stroke is None else ' stroke="%s" stroke-width="%g"' % (stroke, sw)
|
||
d = "" if dash is None else ' stroke-dasharray="%s"' % dash
|
||
return '<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="%g" fill="%s"%s%s/>' % (
|
||
x, y, w, h, rx, fill, s, d)
|
||
|
||
|
||
def tri(points, fill):
|
||
return '<polygon points="%s" fill="%s"/>' % (
|
||
" ".join("%.1f,%.1f" % (px, py) for px, py in points), fill)
|
||
|
||
|
||
def wrap(s, fs, avail, max_lines, where=""):
|
||
"""按估算宽度折行(CJK 逐字可断)。超出 max_lines 用省略号收尾——
|
||
宁可提示文案太长,也不让字溢出盒子。"""
|
||
if text_w(s, fs) <= avail:
|
||
return [s]
|
||
lines = []
|
||
cur = ""
|
||
for ch in s:
|
||
if text_w(cur + ch, fs) > avail:
|
||
lines.append(cur)
|
||
cur = ch
|
||
else:
|
||
cur += ch
|
||
if cur:
|
||
lines.append(cur)
|
||
if len(lines) > max_lines:
|
||
OVERFLOW.append("%s: 折行 %d > %d" % (where, len(lines), max_lines))
|
||
lines = lines[:max_lines]
|
||
lines[max_lines - 1] = lines[max_lines - 1][:-1] + "…"
|
||
return lines
|
||
|
||
|
||
def slot_x(i):
|
||
return SLOT_X0 + i * (SLOT_W + SLOT_GAP)
|
||
|
||
|
||
def slot_c(i):
|
||
"""第 i 格的横向中心。"""
|
||
return slot_x(i) + SLOT_W / 2.0
|
||
|
||
|
||
# ----------------------------- 生成 -----------------------------
|
||
def build():
|
||
out = []
|
||
add = out.append
|
||
|
||
avail = RIGHT - PAD
|
||
for s, fs, where in ((TITLE, TITLE_FONT, "title"), (SUB1, SUB_FONT, "sub1"),
|
||
(SUB2, SUB_FONT, "sub2"), (SUB3, SUB_FONT, "sub3")):
|
||
check_fit(s, fs, avail, where)
|
||
if OVERFLOW:
|
||
for o in OVERFLOW:
|
||
emit("OVERFLOW WARN " + o)
|
||
|
||
add('<rect x="0" y="0" width="%d" height="%d" fill="%s"/>' % (W, H, WHITE))
|
||
|
||
# ---- 标题区 ----
|
||
add(txt(PAD, 42, TITLE, TITLE_FONT, INK, weight=700))
|
||
add(txt(PAD, 68, SUB1, SUB_FONT, MUTED))
|
||
add(txt(PAD, 88, SUB2, SUB_FONT, MUTED))
|
||
add(txt(PAD, 108, SUB3, SUB_FONT, MUTED))
|
||
|
||
# ---- 40 个格子 ----
|
||
win_lo = START
|
||
win_hi = HEAD - 1
|
||
for i in range(DEPTH):
|
||
x = slot_x(i)
|
||
if win_lo <= i <= win_hi:
|
||
fill, stroke, sw = BG_BLUE, C_BLUE, 2.0
|
||
elif i >= HEAD:
|
||
fill, stroke, sw = BG_AMBER, C_AMBER, 1.4
|
||
else:
|
||
fill, stroke, sw = BG_GREY, FAINT, 1.2
|
||
add(rect(x, ROW_Y, SLOT_W, SLOT_H, fill, stroke, rx=3, sw=sw))
|
||
add(txt(slot_c(i), ROW_Y + SLOT_H / 2.0 + 3.5, str(i), SLOT_FONT, MUTED, anchor="middle"))
|
||
|
||
# ---- 三个关键位置标记 ----
|
||
marks = [
|
||
(slot_c(START), MARK_START, C_BLUE, "start"),
|
||
(slot_c(HEAD - 1), MARK_NEW, C_BLUE, "end"),
|
||
(slot_c(HEAD), MARK_HEAD, C_AMBER, "start"),
|
||
]
|
||
for cx, label, color, anchor in marks:
|
||
add(line(cx, STEM_Y1, cx, STEM_Y2 - 2, color, 1.4))
|
||
add(tri([(cx, STEM_Y2), (cx - 4.5, STEM_Y2 - 8), (cx + 4.5, STEM_Y2 - 8)], color))
|
||
check_fit(label, TINY_FONT, 240, "mark %s" % label)
|
||
add(txt(cx, MARK_Y, label, TINY_FONT, color, weight=700, anchor=anchor))
|
||
|
||
# ---- 回绕提示:39 的下一格是 0 ----
|
||
x_from = slot_c(DEPTH - 1)
|
||
x_to = slot_c(0)
|
||
add(line(x_from, WRAP_Y, x_to + 8, WRAP_Y, C_ROSE, 1.6, dash="7 5"))
|
||
add(tri([(x_to, WRAP_Y), (x_to + 9, WRAP_Y - 4.5), (x_to + 9, WRAP_Y + 4.5)], C_ROSE))
|
||
for ln in wrap(WRAP_NOTE, TINY_FONT, avail, 1, "wrap note"):
|
||
add(txt(PAD, WRAP_TEXT_Y, ln, TINY_FONT, C_ROSE, weight=700))
|
||
|
||
# ---- 三条设计不变量 ----
|
||
inner = CARD_W - 24
|
||
for i, (name, c, bg, body) in enumerate(CARDS):
|
||
x = PAD + i * (CARD_W + CARD_GAP)
|
||
add(rect(x, CARD_TOP, CARD_W, CARD_H, bg, c, rx=8, sw=1.8))
|
||
check_fit(name, CARD_FONT, inner, "card %d name" % i)
|
||
add(txt(x + 12, CARD_TOP + 24, name, CARD_FONT, INK, weight=700))
|
||
y = CARD_TOP + 46
|
||
for ln in wrap(body, BODY_FONT, inner, 5, "card %d body" % i):
|
||
add(txt(x + 12, y, ln, BODY_FONT, MUTED))
|
||
y += 15
|
||
|
||
# ---- 底部说明 ----
|
||
fy = CARD_TOP + CARD_H + 30
|
||
add(line(PAD, fy, RIGHT, fy, RULE, 1.0))
|
||
for i, ln in enumerate(wrap(FOOTNOTE, SUB_FONT, avail, 2, "foot wrap")):
|
||
add(txt(PAD, fy + 24 + i * 19, ln, SUB_FONT, MUTED))
|
||
|
||
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, H, W, H, FONT, "\n".join(out)))
|
||
|
||
d = os.path.dirname(OUT)
|
||
if not os.path.isdir(d):
|
||
os.makedirs(d)
|
||
with open(OUT, "w", encoding="utf-8", newline="\n") as fp:
|
||
fp.write(svg)
|
||
ET.fromstring(svg)
|
||
emit("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, H, OUT))
|
||
if OVERFLOW:
|
||
emit("!! %d overflow warnings" % len(OVERFLOW))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
build()
|