Files
STM32F4-Base/tools/arch_diagram/gen_data_trust_ladder_svg.py
T
wandering1 163b933715 docs: 数据可信与 ADC 通道映射只留现状,清历史与悬空引用
数据可信.md 与 ADC通道映射说明.md 按「只写现状」口径重写:
删历史沿革、推理过程与已删小节的残留指针,保留现行判据。

- 数据可信:§2.7 删「当时的证据」六行表;§4.6.1 标题改「范围与
  幅度上限」、四行改现状陈述;§4.6.2 删轮次名历史映射注;§4.6.3
  段文件行去过程语;删节尾孤立排程段;§4.4 负零点段收成现状结论。
- 数据可信:把「系数合法带」从已删的 §8 风险表搬回 §4.1(代码头注释
  仍指向它,属搬回不是新发明)。
- 数据可信:行号纠错 adc_task.c:382 -> :425、protect_task.c:245 ->
  :268,逐个 grep 核对。
- 跨文档与代码:12 处指向已删 §4.6.4/§4.6.5/§8/§9/§10 的悬空引用改到
  现存小节或内联结论,涉 6 处 App 源文件头注释与 6 篇文档。
- 两个 SVG 生成器文案同步重跑,产物同笔提交。

check_doc_links --all 通过(221 篇);pre-commit 全绿。
2026-11-06 17:41:11 +08:00

249 lines
9.6 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 —— 「数据可信」的五级台阶:现在踩在第 3 级,
第 4、5 级(端子二次值 / 精度验收)在等外部资源(纯标准库)。
为什么不用 Mermaid:本图要读的是"台阶的高度差 + 每一级踩没踩上去"——Mermaid 会把
五级压成同一高度,"还差一级"这件事就看不出来了。
用法(仓库根目录,任意 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 = "「数据可信」是五级台阶 —— 现在踩在第 3 级"
SUB1 = ("每升一级只多答一个问题:通路活 → 静态读数对 → 动态波形对 → 换算到端子 → 精度达标;"
"台阶的高度差 = 下游能多信多少")
SUB2 = ("第 3 级已台架闭环(2026-11-02 / 11-04 两轮),用的是手边发生器 + 已有的注入点;"
"第 4、5 级在等子板变比与标准源")
# 每列:(编号, 台阶名, 状态, 主色, 底色, 这一级多答的问题, 用什么证 / 什么挡路)
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", "动态引脚波形", "已证 · 2026-11-04", C_GREEN, BG_GREEN,
"注 1 V 正弦,算出来还是不是 1 V?",
"幅值 / 波形 / 相位 / 测频 / 削顶 / 谐波六轮全跑;测频轮以「如实报不可测」通过"),
("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()