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 存量收尾与它们同批提交。
257 lines
11 KiB
Python
257 lines
11 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""生成 docs/images/采样有效性校验数据流.svg —— 采样有效性校验「观测计数 → 快照 → 判定 → 位图 → 消费者」(纯标准库)。
|
||
|
||
为什么不用 Mermaid:本图要表达**三条量车道(TPAFE / STM32 / 时基)平行流过五个阶段**,
|
||
Mermaid 自动布局会把三条车道挤成交叉线、也保不住"同一拍取数"的横向对齐。
|
||
这里按车道手工排布:每车道一行,箭头全部水平,行内可直接横向读一遍判据。
|
||
|
||
用法(仓库根目录,任意 python3):
|
||
python tools/arch_diagram/gen_adc_health_flow_svg.py
|
||
输出:
|
||
docs/images/采样有效性校验数据流.svg
|
||
改内容时:只编辑本文件顶部的 STAGES / LANES 数据后重跑。
|
||
"""
|
||
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
|
||
HEAD_FONT = 13
|
||
CELL_TITLE_FONT = 12.5
|
||
CELL_NOTE_FONT = 11
|
||
LANE_FONT = 12
|
||
FOOT_FONT = 12
|
||
|
||
CELL_H = 148
|
||
LANE_GAP = 34
|
||
MAIN_Y = 214
|
||
LANE_LABEL_W = 96
|
||
COLS = [
|
||
(PAD + LANE_LABEL_W, 268), # 阶段 1:观测计数(ISR / 消费侧)
|
||
(PAD + LANE_LABEL_W + 288, 236), # 阶段 2:本拍快照
|
||
(PAD + LANE_LABEL_W + 544, 276), # 阶段 3:判定(纯逻辑)
|
||
(PAD + LANE_LABEL_W + 840, 264), # 阶段 4:出口位图
|
||
(PAD + LANE_LABEL_W + 1124, 250), # 阶段 5:消费者
|
||
]
|
||
|
||
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
|
||
C_LANE = ["#0d9488", "#2563eb", "#7c3aed"]
|
||
BG_LANE = ["#f0fdfa", "#eff6ff", "#f5f3ff"]
|
||
C_HEAD, BG_HEAD = "#475569", "#f1f5f9"
|
||
INK, MUTED = "#0f172a", "#64748b"
|
||
|
||
# ----------------------------- 数据(改这里) -----------------------------
|
||
STAGES = [
|
||
"1. 观测计数(ISR / 驱动侧)",
|
||
"2. 本拍计数快照",
|
||
"3. 判定(adc_health.c 纯逻辑)",
|
||
"4. 出口位图(32 位)",
|
||
"5. 消费者与处置",
|
||
]
|
||
|
||
# 每条车道 = 一行;cells 与 STAGES 一一对应
|
||
LANES = [
|
||
dict(
|
||
name="TPAFE",
|
||
color=0,
|
||
cells=[
|
||
dict(title="tpafe5160.c 的 BUSY EXTI",
|
||
lines=["s_frame_seq++:每读完一帧 +1",
|
||
"s_frstdata_err++:无跳变帧累计计数",
|
||
"ISR 内只加计数,不打日志"] ),
|
||
dict(title="adcTask 每拍 poll 之后取值",
|
||
lines=["tpafe_seq",
|
||
"tpafe_frst_err(uint16 放宽)",
|
||
"⚠ 必须取本拍 poll 之后的值"] ),
|
||
dict(title="序号差值 d = seq - 上次",
|
||
lines=["d < 0 → 置倒退位并重新对齐",
|
||
"d > 1 → tgap += d-1 + 当拍事件",
|
||
"d >= 1 → 刷新「最后前进时刻」"] ),
|
||
dict(title="本车道出口",
|
||
lines=["bit0 ADC_HF_TPAFE_STALL(自愈)",
|
||
"bit2 ADC_HF_SEQ_BACKWARD(锁存)",
|
||
"bit6/bit3 ADC_HE_TPAFE_GAP / FRSTDATA"] ),
|
||
dict(title="闭锁 + 留痕 + 计数",
|
||
lines=["protectTask 每 20 ms 读 bit0/bit2",
|
||
"gap / FRSTDATA 事件 → 编排层日志",
|
||
"tgap → 10 s 心跳字段"] ),
|
||
],
|
||
),
|
||
dict(
|
||
name="STM32",
|
||
color=1,
|
||
cells=[
|
||
dict(title="adc_input.c(DMA2 Stream4)",
|
||
lines=["s_dma_seq++:半区/整区完成各一次",
|
||
"s_stm32_gaps += d-1:丢帧点数",
|
||
"s_stm32_errs++:连续 >=2 拍无新帧",
|
||
"s_stm32_hw_errs++:HAL 错误回调"] ),
|
||
dict(title="同一拍的四个 STM32 计数",
|
||
lines=["stm32_seq / stm32_errs",
|
||
"stm32_gaps / stm32_hw_errs",
|
||
"gap 细账不重复取(L1 已算)"] ),
|
||
dict(title="序号差值 d + 三处计数增量",
|
||
lines=["d < 0 → 同一个 SEQ_BACKWARD 位",
|
||
"d >= 1 → 刷新最后前进时刻",
|
||
"增量 != 0 → 事件 + 锁存"] ),
|
||
dict(title="本车道出口",
|
||
lines=["bit1 ADC_HF_STM32_STALL(自愈)",
|
||
"bit2 共用(两车道同一位)",
|
||
"bit7/bit4/bit5 GAP / ERR / HW_ERR"] ),
|
||
dict(title="闭锁 + 严重事件",
|
||
lines=["bit1/bit2 → 同左闭锁路径",
|
||
"STM32_ERR / HW_ERR:恒 0 增量",
|
||
"出现即 DBG_ERROR + 自检留痕"] ),
|
||
],
|
||
),
|
||
dict(
|
||
name="时基",
|
||
color=2,
|
||
cells=[
|
||
dict(title="osKernelGetTickCount()",
|
||
lines=["HAL 时基 TIM7,tick = 1 ms",
|
||
"2^32 ms 后自然回绕"] ),
|
||
dict(title="now_ms:本拍唯一一份",
|
||
lines=["健康判定与心跳上报共用",
|
||
"不在判定层内再取一次"] ),
|
||
dict(title="差值法 (int32_t)(now - adv)",
|
||
lines=["回绕安全(取有符号差)",
|
||
">= ADC_HEALTH_STALL_MS(100)",
|
||
"可判定窗 ±2^31 ≈ ±24.85 天"] ),
|
||
dict(title="tpafe_stall_ms",
|
||
lines=["静默期内给出停滞时长",
|
||
"正常态恒为 0",
|
||
"STM32 侧未上报(观测盲区)"] ),
|
||
dict(title="上升沿日志可读性",
|
||
lines=["health: flags=… stall_ms=…",
|
||
"回答到底停了多少毫秒"] ),
|
||
],
|
||
),
|
||
]
|
||
|
||
FOOT = ("判定层零平台依赖(只 include <stdint.h>、不碰寄存器 / 不取数 / 不打日志)→ 主机侧以 #include 原样纳入、免桩全覆盖单测。"
|
||
"详见 docs/算法/采样有效性校验设计说明.md")
|
||
|
||
|
||
# ----------------------------- 工具函数 -----------------------------
|
||
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 build():
|
||
out = []
|
||
add = out.append
|
||
|
||
n_cells = max(len(l["cells"]) for l in LANES)
|
||
for li, lane in enumerate(LANES):
|
||
for ci, cell in enumerate(lane["cells"]):
|
||
_, cw = COLS[ci]
|
||
avail = cw - 26
|
||
check_fit(cell["title"], CELL_TITLE_FONT, avail,
|
||
"lane %s col %d title" % (lane["name"], ci + 1))
|
||
for ln in cell["lines"]:
|
||
check_fit(ln, CELL_NOTE_FONT, avail,
|
||
"lane %s col %d line" % (lane["name"], ci + 1))
|
||
for ci, st in enumerate(STAGES):
|
||
check_fit(st, HEAD_FONT, COLS[ci][1] - 20, "stage head %d" % (ci + 1))
|
||
if len(OVERFLOW):
|
||
for o in OVERFLOW:
|
||
print("OVERFLOW WARN " + o)
|
||
|
||
lane_h = CELL_H + LANE_GAP
|
||
bottom = MAIN_Y + len(LANES) * lane_h - LANE_GAP
|
||
total_h = bottom + 92
|
||
|
||
add('<rect x="0" y="0" width="%d" height="%d" fill="#ffffff"/>' % (W, total_h))
|
||
add('<text x="%d" y="44" font-size="%d" font-weight="700" fill="%s">%s</text>'
|
||
% (PAD, TITLE_FONT, INK, esc("采样有效性校验(adc_health)· 数据流与出口")))
|
||
add('<text x="%d" y="70" font-size="%d" fill="%s">%s</text>'
|
||
% (PAD, SUB_FONT, MUTED,
|
||
esc("三条量车道平行流过五个阶段;判定层每拍一次、常数次整数运算,输入只是计数快照")))
|
||
|
||
# ---- 阶段标题 ----
|
||
for ci, st in enumerate(STAGES):
|
||
x, cw = COLS[ci]
|
||
add('<rect x="%d" y="150" width="%d" height="34" rx="8" fill="%s" stroke="%s" '
|
||
'stroke-opacity="0.35"/>' % (x, cw, BG_HEAD, C_HEAD))
|
||
add('<text x="%d" y="172" font-size="%d" font-weight="700" fill="#334155">%s</text>'
|
||
% (x + 12, HEAD_FONT, esc(st)))
|
||
|
||
# ---- 车道 ----
|
||
for li, lane in enumerate(LANES):
|
||
color, bg = C_LANE[lane["color"]], BG_LANE[lane["color"]]
|
||
y0 = MAIN_Y + li * lane_h
|
||
# 车道标签
|
||
add('<rect x="%d" y="%d" width="%d" height="%d" rx="10" fill="%s" stroke="%s" '
|
||
'stroke-opacity="0.5"/>' % (PAD, y0, LANE_LABEL_W - 14, CELL_H, bg, color))
|
||
add('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="%s" '
|
||
'text-anchor="middle">%s</text>'
|
||
% (PAD + (LANE_LABEL_W - 14) / 2.0, y0 + CELL_H / 2.0 + 5, LANE_FONT,
|
||
color, esc(lane["name"])))
|
||
|
||
for ci, cell in enumerate(lane["cells"]):
|
||
x, cw = COLS[ci]
|
||
add('<rect x="%d" y="%d" width="%d" height="%d" rx="10" fill="%s" stroke="%s" '
|
||
'stroke-opacity="0.45"/>' % (x, y0, cw, CELL_H, bg, color))
|
||
add('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="%s">%s</text>'
|
||
% (x + 13, y0 + 30, CELL_TITLE_FONT, color, esc(cell["title"])))
|
||
yy = y0 + 56
|
||
for ln in cell["lines"]:
|
||
add('<text x="%d" y="%d" font-size="%d" fill="#334155">%s</text>'
|
||
% (x + 13, yy, CELL_NOTE_FONT, esc(ln)))
|
||
yy += 22
|
||
# 水平箭头到下一列
|
||
if ci + 1 < len(lane["cells"]):
|
||
nx = COLS[ci + 1][0]
|
||
cy = y0 + CELL_H / 2.0
|
||
add('<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="%s" '
|
||
'stroke-width="2.2"/>' % (x + cw, cy, nx - 9, cy, color))
|
||
add('<path d="M %.1f %.1f L %.1f %.1f L %.1f %.1f z" fill="%s"/>'
|
||
% (nx, cy, nx - 11, cy - 5.5, nx - 11, cy + 5.5, color))
|
||
|
||
# ---- 底部说明 ----
|
||
add('<rect x="%d" y="%d" width="%d" height="46" rx="10" fill="#fffbeb" '
|
||
'stroke="#d97706" stroke-opacity="0.45"/>' % (PAD, bottom + 30, W - 2 * PAD))
|
||
add('<text x="%d" y="%d" font-size="%d" fill="#7c2d12">%s</text>'
|
||
% (PAD + 16, bottom + 58, FOOT_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%s\n</svg>\n'
|
||
% (W, total_h, W, total_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)
|
||
print("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, total_h, OUT))
|
||
if len(OVERFLOW):
|
||
print("!! %d 处文字可能溢出,见上方 WARN" % len(OVERFLOW))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
build()
|