Files
STM32F4-Base/tools/arch_diagram/gen_data_trust_ladder_svg.py
T
wandering1 b46e44d92c docs(采集可信): 数据可信文档台阶化重构并立项故障录波
两份工作互锁(互相引用),故同一笔提交。

数据可信:全文按「五级台阶」重排(§1 全局 → §2 信度现状 → §3 M4 退出判据逐条 →
§4 设计约束 → §5 任务卡 → §6 顺序 → §7 决策 → §8 风险 → §9 边界 → §10 关系)。
恢复 §2.2 量纲错配(该节此前被瘦身删掉,test/host/prot_dim 的两处引用一直悬空);
新增 §2.7 与 §4.6:ADC 只验过直流静态标定,正弦动态波形一次也没采过——用信号发生
器在既有注入点注 ±2 V 以内的正弦即可验全周傅氏、测频、快照与削顶路由,不必往端子
加压、也不需要标准源。§6 手绘框图改 Mermaid。章节编号未重排:§2 / §4 / §7 / §10 被
App 与 test 的头注释当规范引用。

故障录波:用户裁定 2026-10-29 录波先行,三条口径——1 kHz(每周波 20 点)、8 路
电气量全录、一屏只画一路共 8 路可切换。新增设计说明(与产品需求 80 点/周波口径的
偏离显式登记;存原始码值而非工程量,绕开未标定闭锁与测频幅值门;四个所有者的跨
任务边界与三条并发陷阱;段文件字节布局;单通道回放显示契约;约 5.9 KB 静态增量
触发 R6)与卡片登记簿(数据面最小闭环 → 手动与保护启动触发 → 现场单通道回放页,
二期的 4 kHz、突变量启动、COMTRADE、104 文件传输不返工的结构约束写进设计文档)。

两张旧配图与一张新配图改由 tools/arch_diagram/ 的生成器产出,脚本与产物同笔提交。
代码一行未动:本波只交付文档与设计。
2026-10-29 13:18:28 +08:00

251 lines
9.8 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/数据可信台阶.svg —— 「数据可信」的五级台阶:现在踩在第 2 级,
第 3 级(动态正弦波形)用手边设备就能跨(纯标准库)。
为什么不用 Mermaid:本图要读的是"台阶的高度差 + 每一级踩没踩上去"——Mermaid 会把
五级压成同一高度,"还差一级"这件事就看不出来了。谁挡谁的顺序拓扑另有一张 Mermaid
图,在 docs/任务规划/数据可信.md §6(本图刻意不重复那张的语义)。
用法(仓库根目录,任意 python3):
python tools/arch_diagram/gen_data_trust_ladder_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 = 712
PAD = 46
RIGHT = W - PAD
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
TITLE_FONT = 21
SUB_FONT = 13
HEAD_FONT = 14.5
Q_FONT = 12.5
HOW_FONT = 11.5
TINY_FONT = 11
INK = "#0f172a"
LINE = "#334155"
MUTED = "#64748b"
FAINT = "#94a3b8"
RULE = "#cbd5e1"
WHITE = "#ffffff"
C_GREEN = "#16a34a"
BG_GREEN = "#f0fdf4"
C_AMBER = "#d97706"
BG_AMBER = "#fffbeb"
C_ROSE = "#e11d48"
BG_ROSE = "#fff1f2"
C_BLUE = "#2563eb"
BG_BLUE = "#eff6ff"
# ----------------------------- 数据(改这里) -----------------------------
TITLE = "「数据可信」是五级台阶 —— 现在踩在第 2 级"
SUB1 = ("每升一级只多答一个问题:通路活 → 静态读数对 → 动态波形对 → 换算到端子 → 精度达标;"
"台阶的高度差 = 下游能多信多少")
SUB2 = ("第 3 级不要标准源、也不要往端子加压:一台信号发生器 + 已有的注入点就能做"
"(判据见 docs/任务规划/数据可信.md §4.6)")
# 每列:(编号, 台阶名, 状态, 主色, 底色, 这一级多答的问题, 用什么证 / 什么挡路)
STEPS = [
("1", "通路活", "已证 · 2026-10-27", C_GREEN, BG_GREEN,
"9 路都在转换?没串道?极性没反?",
"开机自检 + 帧计数仍 1000 Hz;逐路注入只动本路"),
("2", "静态引脚电压", "已证 · 2026-10-27", C_GREEN, BG_GREEN,
"注 1 V 直流,读数是不是 1 V?",
"万用表 + 电位器三点线性:斜率 6544.2 vs 理论 6553.6 码/V,差 −0.14 %"),
("3", "动态引脚波形", "待做 · 本文新增", C_ROSE, BG_ROSE,
"注 1 V 正弦,算出来还是不是 1 V?",
"注正弦验全周傅氏幅值 / 相位 / 测频 / 削顶 / 谐波抑制。"
"挡路:FREQ_AMP_MIN = 50 V 在引脚域恒不过 → 测频只能读到 IV"),
("4", "端子二次值", "等外部资源", C_AMBER, BG_AMBER,
"引脚上这 1 V 对应端子侧几 V?",
"④ 子板 / 端子板变比:仓内无原理图,须拿图纸或万用表实测"),
("5", "精度等级验收", "等外部资源", C_AMBER, BG_AMBER,
"额定工况下整条链误差够几级?",
"标准源两点标定写 [adc_cal] 置 VALID → 保护自动解卡 → 送检"),
]
BAND_LABEL_1 = "M4 承诺边界(§2.6):蓝底这三列都还在「ADC 引脚」这一侧,eng = 0、单位是引脚电压 V"
GATE_LABEL = "eng 升档门:④ 变比到手 + 电气 8 路 VALID 全置位 → eng 才升 1"
FOOTNOTE = ("第 2、3 级共用同一个注入点与同一套读数出口,但证的是两件事:"
"直流证「一条水平线的高度」,正弦证「整条时间轴的形状与节拍」——第 3 级不能由第 2 级代证。")
# ----------------------------- 布局 -----------------------------
STEP_W = 196
STEP_GAP = 6
X0 = PAD
FIRST_H = 96 # 第 1 级台阶高度
RISE = 64 # 每高一级加这么多
BASE_Y = 466 # 地面线
GATE_X = X0 + 3 * (STEP_W + STEP_GAP) - STEP_GAP - 3 # 第 3/4 列之间
BAND_TOP = 106
BAND_BOTTOM = BASE_Y + 172
# ----------------------------- 工具函数 -----------------------------
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):
s = "" if stroke is None else ' stroke="%s" stroke-width="%g"' % (stroke, sw)
return '<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="%g" fill="%s"%s/>' % (
x, y, w, h, rx, fill, s)
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 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")):
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(rect(PAD - 12, BAND_TOP, GATE_X - PAD + 3, BAND_BOTTOM - BAND_TOP, BG_BLUE, None, rx=12))
# ---- 台阶 ----
inner_w = STEP_W - 20
for i, (no, name, status, c, bg, q, how) in enumerate(STEPS):
x = X0 + i * (STEP_W + STEP_GAP)
h = FIRST_H + i * RISE
top = BASE_Y - h
add(rect(x, top, STEP_W, h, bg, c, rx=8, sw=2.0))
add('<circle cx="%.1f" cy="%.1f" r="13" fill="%s"/>' % (x + 24, top + 26, c))
add(txt(x + 24, top + 31, no, 13, WHITE, weight=700, anchor="middle"))
check_fit(name, HEAD_FONT, inner_w - 30, "step %s name" % no)
add(txt(x + 44, top + 32, name, HEAD_FONT, INK, weight=700))
add(txt(x + 11, top + 58, status, TINY_FONT, c, weight=700))
# ---- 地面线 ----
add(line(PAD, BASE_Y, RIGHT, BASE_Y, LINE, 2.2))
# ---- 地面下的明细(问题 + 用什么证),逐列一行 ----
for i, (no, name, status, c, bg, q, how) in enumerate(STEPS):
x = X0 + i * (STEP_W + STEP_GAP)
y = BASE_Y + 24
for ln in wrap(q, Q_FONT, inner_w, 2, "step %s q" % no):
add(txt(x + 10, y, ln, Q_FONT, INK, weight=700))
y += 17
y += 3
for ln in wrap(how, HOW_FONT, inner_w, 5, "step %s how" % no):
add(txt(x + 10, y, ln, HOW_FONT, MUTED))
y += 15
# ---- eng 升档门 ----
add(line(GATE_X, BAND_TOP - 6, GATE_X, BAND_BOTTOM, C_ROSE, 2.0, dash="7 5"))
gy = BAND_TOP + 12
for ln in wrap(GATE_LABEL, TINY_FONT, STEP_W - 16, 4, "gate label"):
add(txt(GATE_X + 8, gy, ln, TINY_FONT, C_ROSE, weight=700))
gy += 15
# ---- 蓝底标签(在台阶上方的空白区)----
bl = wrap(BAND_LABEL_1, HOW_FONT, GATE_X - PAD - 24, 2, "band label")
for i, ln in enumerate(bl):
add(txt(PAD, BAND_TOP + 26 + i * 17, ln, HOW_FONT, C_BLUE, weight=700))
# ---- 底部说明 ----
fy = BAND_BOTTOM + 22
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()