4c74cfa825
批次二文档半(道 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 存量收尾与它们同批提交。
301 lines
13 KiB
Python
301 lines
13 KiB
Python
#!/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()
|