Files
STM32F4-Base/tools/arch_diagram/gen_adc_health_timeline_svg.py
T
wandering1 4c74cfa825 refactor(编号·批次二): 文档侧 194 处保护功能号改成功能名,定义表 7 张首格改语义标识
批次二文档半(道 gate-docs 执行)。先纠三个数的口径账,别再拿任何旧数当准:**234 处是准数** —— 卡片写
52(Lead 用了门禁 Checker 的引用集合,它**刻意排除属主文档内部的定义位**)、道里先报 265(裸正则把
`SDL2-2.32.10` 里的 `L2-2` 当成功能号,误报 10 处)。最终边界口径 = 带左边界 + 跳过反引号代码段。

· 19 只文件 HEAD 共 234 处 → 现 40 处,**40 处 100% 落在变更记录行**(冻结历史,按 Q3 一条没动),
  非变更记录残留 **0**;改写 163 行 / 16 只文档。站点最多:规划文档 110→37、相量层单测说明 25→1、
  采集与数据处理短板评测 21→0、App/algo/README 26→0。
  (`App/algo/README.md` 由上一笔代码提交带上车,路径归属已在该笔正文注明,内容是本批改写)
· **定义表首列由编号改成语义标识**(编号规范 §3.3「表首格就是短语」的完全形态):首列原为编号的表
  共 **7 张**(不是 5 张),删掉与之重复的「语义名」列;表头标签按域词区分——三张 L3 保护**元件**表
  保留 `| 元件 |`,四张 L1/L2 表用 `| 功能名 |`(把「元件」统一洗成功能名会丢掉领域信息)。
  七张表逐张核:表头竖线数 == 分隔行 == 首条数据行 == 列数+1;与 HEAD 逐块对拍**净新增不一致块 0**,
  并且正是这个对拍抓到第一版「三张表数据行已删列、表头没删」的破表。
· 7 处删号留下的残词/重复解释括号人工收口(如「L1-5(通道标定)」→「通道标定(一次系数 + 零漂)」),
  复扫待收口站点 0;措辞逐字取自映射表,无第二种说法。
· tools/arch_diagram 三只生成器 4 处(3 处 docstring + 1 处图形文案)与产物同批:重跑后**只有位图那张
  变 2 个 text 节点的换行位置**,画布 1580x740 与总行数不变,另两张逐字节未变;改前先干跑对照证明三张
  svg 原本与生成器一致(不在 stale 基线里)——`check_generated_fresh --only docs-svg` PASS。

本笔另含 **F-* 半批的文档面**(同一轮 `git add docs tools/arch_diagram AGENTS.md` 一并收进
,非误伤):BSD
函数调用关系里 9 处反引号紧邻指针、属主文档 §7 的停发句与导读行(**12 条定义行原样保留、不删登记族**)、
以及 `gen_net_instability_svg.py` 与它的产物 `网络不稳根因总览.svg`(「唯一读者红线」那条的框宽自适应
526 → 551,右缘 1411 未越界,画布 1560x1468 不变)——该半批的**代码注释面**在下一笔。

⚠ 因 `docs/任务规划/登记簿.md` 与 `docs/任务规划/UI补齐.md` 正被另一张卡(UI 叶数复核)改动,本笔**不含
那两只文件**,批次二的 §9 存量收尾与它们同批提交。
2026-10-27 09:05:24 +08:00

301 lines
13 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 —— 采样有效性校验四个工况的时序与门限(纯标准库)。
为什么不用 Mermaid:本图要表达**时间与门限的比例关系**(100 ms 门限线、静默计时线、
电平位/锁存位的存续区间),必须按比例画坐标与条形;Mermaid 只画拓扑、不保比例。
用法(仓库根目录,任意 python3):
python tools/arch_diagram/gen_adc_health_timeline_svg.py
输出:
docs/images/采样有效性判定时间轴.svg
改内容时:编辑本文件顶部的 PANELS 数据后重跑。
"""
import html
import os
import xml.etree.ElementTree as ET
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
OUT = os.path.join(ROOT, "docs", "images", "采样有效性判定时间轴.svg")
W = 1580
PAD = 40
TITLE_FONT = 21
SUB_FONT = 13
AXIS_FONT = 10.5
TXT_FONT = 11.5
HEAD_FONT = 13.5
PLOT_X0, PLOT_X1 = 430, 1120
NOTE_X, NOTE_W = 1150, 390
T_MAX = 200.0
V_MAX = 150.0 # 纵轴:静默计时 ms(面板 C 的第二序列另按自身标度)
PLOT_H = 96
STRIP_H = 15
PANEL_H = 186
FIRST_Y = 220
PLOT_W = PLOT_X1 - PLOT_X0
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
INK, MUTED = "#0f172a", "#64748b"
C_OK = "#16a34a"
C_LEVEL = "#0d9488"
C_LATCH = "#e11d48"
C_EVENT = "#d97706"
C_AXIS = "#cbd5e1"
C_TH = "#e11d48"
BG_STRIP = "#f1f5f9"
# ----------------------------- 数据(改这里) -----------------------------
THRESHOLD = 100.0
PANELS = [
dict(
title="A 对照:健康 1 kHz(d 恒为 1)",
seq=["seq: 1000 1001 1002 ... 每拍恰好 +1",
"每拍都刷新最后前进时刻 adv_ms"],
verdict="elapsed = now - adv_ms 恒为 0 → 永不进门限带",
elapsed=[[(0, 0), (200, 0)]],
strips=[],
notes=["判定层对健康态**零输出**:events=0、flags=0、",
"sticky=0。主机单测 TC-ADCHEALTH-002 用 2000 拍",
"扫描把这个结论钉死(健康不误报)。"],
),
dict(
title="B TPAFE 静默:100 ms 检出,恢复只清电平位",
seq=["seq: 1000 1001 ... 冻结在 1050",
"STM32 车道照常前进(分车道独立判定)"],
verdict="elapsed 每拍 +1,达到 100 ms 那拍即置位",
elapsed=[[(0, 0), (100, 100), (150, 150)], [(150, 0), (200, 0)]],
strips=[("flags", "TPAFE_STALL 电平", C_LEVEL, 100, 150),
("sticky", "TPAFE_STALL 锁存留痕", C_LATCH, 100, 200)],
marks=[(100, 100, "1"), (150, 150, "2")],
notes=["1 t=100 ms:ADC_HF_TPAFE_STALL 置位 + 进 sticky;",
" 闭锁路径 = protectTask 下一拍(间隔 <= 20 ms)",
" 命中掩码 → prot_elm_init() + 本拍不 eval",
"2 t=150 ms:序号重新前进 → 电平位清、sticky 仍留痕"],
),
dict(
title="C 消费者超拍:d=3 只算丢帧,永不判静默",
seq=["seq: 1000 1003 1006 ... 每拍 +3",
"生产者活着,消费者没跟上"],
verdict="d >= 1 → 刷新 adv_ms,elapsed 恒 0;tgap += d-1",
elapsed=[[(0, 0), (200, 0)]],
series=dict(label="tpafe_gaps 累计点数(右轴 0~8)", color=C_EVENT,
points=[(0, 0), (20, 0), (20, 2), (60, 2), (60, 4),
(100, 4), (100, 6), (200, 6)]),
strips=[("events", "ADC_HE_TPAFE_GAP 仅当拍", C_EVENT, 20, 22),
("events", "同上", C_EVENT, 60, 62),
("events", "同上", C_EVENT, 100, 102)],
marks=[(20, 9, "1"), (60, 9, "2")],
notes=["1 t=20 ms:d=3 → tgap += 2 + 当拍事件,下一拍事件消失",
" (边沿语义,不是电平)",
"2 丢帧不入锁存、不告警、不闭锁;两条车道同单位:",
" tgap 与 adc1_gap 都按帧点数(d-1 加权)累计,心跳里可",
" 直接对比。台架日志:启动突发期 23 点,稳态每 10 s +1~2 点"],
),
dict(
title="D 序号倒退:锁存不可自愈,代价是永久闭锁",
seq=["seq: 1050 1051 ... 跳到 995",
"d = 995 - 1051 < 0(有符号差)"],
verdict="置位后重新对齐基线,不重复触发;位永不自动清",
elapsed=[[(0, 0), (200, 0)]],
strips=[("flags", "SEQ_BACKWARD 不自动清", C_LATCH, 60, 200),
("sticky", "SEQ_BACKWARD 锁存", C_LATCH, 60, 200)],
marks=[(60, 9, "1"), (194, 9, "2")],
notes=["1 t=60 ms:倒退被检出 → 重新对齐避免每拍重复置位",
"2 电平位与锁存位同时置位 → 保护永久退出,直到",
" 复位或显式 adc_health_init();这是刻意取舍:",
" 计数被改写时相量不可信,宁可拒动不误动"],
),
]
FOOT = ("横轴 = 距最后一次序号前进的时间(ms,tick 回绕由有符号差值吸收);红线 = ADC_HEALTH_STALL_MS = 100 ms。"
"详见 docs/算法/采样有效性校验设计说明.md")
# ----------------------------- 工具函数 -----------------------------
def esc(s):
return html.escape(s, quote=True)
def text_w(s, fs):
return sum(fs * (1.0 if ord(ch) >= 0x2E80 else 0.58) for ch in s)
WARN = []
def fit(s, fs, avail, where):
if text_w(s, fs) > avail:
WARN.append("%s: %.0f > %.0f [%s]" % (where, text_w(s, fs), avail, s))
def X(t):
return PLOT_X0 + PLOT_W * (t / T_MAX)
def Y(v):
return v
# ----------------------------- 生成 -----------------------------
def build():
b = []
add = b.append
for pn in PANELS:
fit(pn["title"], HEAD_FONT, PLOT_W - 40, "panel title")
for s in pn["seq"] + [pn["verdict"]]:
fit(s, TXT_FONT, 360, "panel seq/verdict")
for s in pn["notes"]:
fit(s, TXT_FONT, NOTE_W - 24, "panel note")
for w in WARN:
print("OVERFLOW WARN " + w)
total_h = FIRST_Y + len(PANELS) * PANEL_H + 74
add('<text x="%d" y="44" font-size="%d" font-weight="700" fill="%s">%s</text>'
% (PAD, TITLE_FONT, INK, esc("采样有效性判定时间轴 · 四个工况的 elapsed 与位存续区间")))
add('<text x="%d" y="70" font-size="%d" fill="%s">%s</text>'
% (PAD, SUB_FONT, MUTED,
esc("elapsed = (int32_t)(now_ms - 最后一次序号前进的时刻);条带 = 位为 1 的存续区间")))
# 图例
lx, ly = PLOT_X0, 116
for label, color in [("电平位 flags(可自愈)", C_LEVEL), ("锁存位 sticky / 不自愈", C_LATCH),
("边沿事件 events(仅当拍)", C_EVENT), ("静默门限 100 ms", C_TH)]:
add('<rect x="%d" y="%d" width="16" height="12" rx="3" fill="%s"/>' % (lx, ly - 10, color))
lx += 22
add('<text x="%d" y="%d" font-size="12" fill="#475569">%s</text>' % (lx, ly, esc(label)))
lx += text_w(label, 12) + 30
# 列头
add('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="#334155">%s</text>'
% (PAD, 178, HEAD_FONT, esc("工况与计数")))
add('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="#334155">%s</text>'
% (PLOT_X0, 178, HEAD_FONT, esc("静默计时 elapsed(ms)与位存续")))
add('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="#334155">%s</text>'
% (NOTE_X, 178, HEAD_FONT, esc("关键时刻与判读")))
for pi, pn in enumerate(PANELS):
ptop = FIRST_Y + pi * PANEL_H
base = ptop + PLOT_H
strip_y0 = base + 12
# 左侧说明块
add('<rect x="%d" y="%d" width="%d" height="%d" rx="10" fill="#f8fafc" '
'stroke="#64748b" stroke-opacity="0.25"/>' % (PAD, ptop, 370, PLOT_H + 46))
add('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="%s">%s</text>'
% (PAD + 14, ptop + 26, HEAD_FONT, INK, esc(pn["title"])))
yy = ptop + 50
for s in pn["seq"]:
add('<text x="%d" y="%d" font-size="%d" fill="#475569">%s</text>'
% (PAD + 14, yy, TXT_FONT, esc(s)))
yy += 17
add('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="#0d9488">%s</text>'
% (PAD + 14, yy + 6, TXT_FONT, esc(pn["verdict"])))
# 坐标框 + 网格
add('<rect x="%d" y="%d" width="%d" height="%d" fill="#ffffff" stroke="%s" '
'stroke-width="1"/>' % (PLOT_X0, ptop + 4, PLOT_W, PLOT_H, C_AXIS))
for gv in (0, 50, 100, 150):
gy = base - PLOT_H * (gv / V_MAX)
add('<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="%s" stroke-width="0.8" '
'stroke-dasharray="3 4"/>' % (PLOT_X0, gy, PLOT_X1, gy, C_AXIS))
add('<text x="%d" y="%.1f" font-size="%d" fill="%s" text-anchor="end">%s</text>'
% (PLOT_X0 - 6, gy + 4, AXIS_FONT, MUTED, str(gv)))
for tv in (0, 50, 100, 150, 200):
tx = X(tv)
add('<line x1="%.1f" y1="%d" x2="%.1f" y2="%d" stroke="%s" stroke-width="1"/>'
% (tx, base, tx, base + 4, C_AXIS))
add('<text x="%.1f" y="%d" font-size="%d" fill="%s" text-anchor="middle">%s</text>'
% (tx, base + 16, AXIS_FONT, MUTED, str(tv)))
# 门限线
ty = base - PLOT_H * (THRESHOLD / V_MAX)
add('<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="%s" stroke-width="2" '
'stroke-dasharray="8 5"/>' % (PLOT_X0, ty, PLOT_X1, ty, C_TH))
add('<text x="%d" y="%.1f" font-size="%d" fill="%s">%s</text>'
% (PLOT_X1 - 176, ty - 6, AXIS_FONT, C_TH, esc("ADC_HEALTH_STALL_MS = 100")))
# elapsed 折线
for seg in pn["elapsed"]:
pts = " ".join("%.1f,%.1f" % (X(t), base - PLOT_H * (v / V_MAX)) for t, v in seg)
add('<polyline points="%s" fill="none" stroke="%s" stroke-width="2.6" '
'stroke-linejoin="round"/>' % (pts, C_LEVEL))
# 第二序列(丢帧累计,右轴标度)
sd = pn.get("series")
if sd:
vmax = max(v for _, v in sd["points"]) or 1.0
pts = " ".join("%.1f,%.1f" % (X(t), base - PLOT_H * (v / (vmax * 1.15)))
for t, v in sd["points"])
add('<polyline points="%s" fill="none" stroke="%s" stroke-width="2.4" '
'stroke-linejoin="round"/>' % (pts, sd["color"]))
add('<text x="%d" y="%d" font-size="%d" fill="%s">%s</text>'
% (PLOT_X0 + 8, ptop + 20, AXIS_FONT, sd["color"], esc(sd["label"])))
# 标记点
for t, v, tag in pn.get("marks", []):
mx, my = X(t), base - PLOT_H * (v / V_MAX)
add('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%d" stroke="%s" stroke-width="1" '
'stroke-dasharray="3 3"/>' % (mx, my, mx, base + 4, MUTED))
add('<circle cx="%.1f" cy="%.1f" r="9" fill="%s"/>' % (mx, my, INK))
add('<text x="%.1f" y="%.1f" font-size="10.5" font-weight="700" fill="#ffffff" '
'text-anchor="middle">%s</text>' % (mx, my + 3.6, esc(tag)))
# 位存续条带
for i in range(2):
add('<rect x="%d" y="%d" width="%d" height="%d" rx="4" fill="%s" '
'stroke-opacity="0"/>' % (PLOT_X0, strip_y0 + i * (STRIP_H + 4), PLOT_W,
STRIP_H, BG_STRIP))
add('<text x="%d" y="%d" font-size="%d" fill="%s" text-anchor="end">%s</text>'
% (PLOT_X0 - 6, strip_y0 + 11, AXIS_FONT, MUTED, esc("flags")))
add('<text x="%d" y="%d" font-size="%d" fill="%s" text-anchor="end">%s</text>'
% (PLOT_X0 - 6, strip_y0 + STRIP_H + 15, AXIS_FONT, MUTED, esc("sticky/events")))
for name, label, color, t0, t1 in pn["strips"]:
row = 0 if name == "flags" else 1
sx = X(t0)
sw = max(4.0, X(t1) - X(t0))
sy = strip_y0 + row * (STRIP_H + 4)
add('<rect x="%.1f" y="%.1f" width="%.1f" height="%d" rx="4" fill="%s" '
'fill-opacity="0.85"/>' % (sx, sy, sw, STRIP_H, color))
lab_x = sx + sw + 8
if lab_x + text_w(label, AXIS_FONT) > PLOT_X1 + 4:
lab_x = max(PLOT_X0 + 6, sx - text_w(label, AXIS_FONT) - 8)
add('<text x="%.1f" y="%.1f" font-size="%d" fill="%s">%s</text>'
% (lab_x, sy + 11, AXIS_FONT, color, esc(label)))
# 右侧判读
ny = ptop + 18
for s in pn["notes"]:
add('<text x="%d" y="%d" font-size="%d" fill="#334155">%s</text>'
% (NOTE_X, ny, TXT_FONT, esc(s)))
ny += 17
fy = total_h - 56
add('<rect x="%d" y="%d" width="%d" height="42" rx="10" fill="#fffbeb" '
'stroke="#d97706" stroke-opacity="0.45"/>' % (PAD, fy, W - 2 * PAD))
add('<text x="%d" y="%d" font-size="%d" fill="#7c2d12">%s</text>'
% (PAD + 16, fy + 26, TXT_FONT, esc(FOOT)))
svg = ('<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" '
'viewBox="0 0 %d %d" font-family="%s">\n<rect x="0" y="0" width="%d" height="%d" '
'fill="#ffffff"/>\n%s\n</svg>\n'
% (W, total_h, W, total_h, FONT, W, total_h, "\n".join(b)))
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)
print("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, total_h, OUT))
if len(WARN):
print("!! %d 处文字可能溢出" % len(WARN))
if __name__ == "__main__":
build()