#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""生成 docs/images/采样有效性位图与掩码.svg —— 采样有效性校验出口位图与消费者掩码对照(纯标准库)。
为什么不用 Mermaid:本图的核心信息是**位号对齐**(同一 bit 在三张位图与四个消费者掩码
里落在同一列),必须等宽网格逐列对齐;Mermaid 无法控制列位。
用法(仓库根目录,任意 python3):
python tools/arch_diagram/gen_adc_health_bitmap_svg.py
输出:
docs/images/采样有效性位图与掩码.svg
改内容时:编辑本文件顶部的 GROUPS 数据后重跑。
"""
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
ROW_LABEL_FONT = 12.5
BIT_FONT = 11
TAG_FONT = 10.5
NOTE_FONT = 11.5
HEAD_FONT = 12.5
LABEL_X, LABEL_W = PAD, 262
GRID_X = PAD + LABEL_W + 8
BIT_W, BIT_H, BIT_GAP = 94, 46, 4
NOTE_W = 320
GROUP_GAP = 26
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
INK, MUTED = "#0f172a", "#64748b"
C_LEVEL = ("#0d9488", "#f0fdfa") # 电平(可自愈)
C_LATCH = ("#e11d48", "#fff1f2") # 锁存(不自愈)
C_EVENT = ("#d97706", "#fffbeb") # 仅边沿事件
C_MASK = ("#7c3aed", "#f5f3ff") # 消费者掩码命中
C_NONE = ("#94a3b8", "#f8fafc") # 未使用 / 不适用
# ----------------------------- 数据(改这里) -----------------------------
# kind: level / latch / event / mask / none
GROUPS = [
dict(head="判定层写出的三张位图(adc_health.c)", rows=[
dict(label="flags 电平位", sub="adc_health_flags(),当前活跃",
note="protectTask 每 20 ms 读它做自检失败闭锁;bit8 由本任务自己判老后 OR 入(判定层不写)",
bits=[("bit0", "TPAFE_STALL", "level"), ("bit1", "STM32_STALL", "level"),
("bit2", "SEQ_BACKWARD", "latch"), ("bit3", "—", "none"),
("bit4", "—", "none"), ("bit5", "—", "none"),
("bit6", "—", "none"), ("bit7", "—", "none"),
("bit8", "STALE", "level")]),
dict(label="sticky 锁存位", sub="adc_health_sticky(),直到 init 才清",
note="只留痕不闭锁:曾停摆/曾出错必须可追溯",
bits=[("bit0", "TPAFE_STALL", "latch"), ("bit1", "STM32_STALL", "latch"),
("bit2", "SEQ_BACKWARD", "latch"), ("bit3", "FRSTDATA", "latch"),
("bit4", "STM32_ERR", "latch"), ("bit5", "HW_ERR", "latch"),
("bit6", "不入锁存", "none"), ("bit7", "不入锁存", "none"),
("bit8", "不入锁存", "none")]),
dict(label="events 事件位", sub="adc_health_check() 出参,仅当拍有效",
note="adc_health_report() 的唯一输入:按位分三级处置",
bits=[("bit0", "—", "none"), ("bit1", "—", "none"), ("bit2", "—", "none"),
("bit3", "FRSTDATA", "event"), ("bit4", "STM32_ERR", "event"),
("bit5", "HW_ERR", "event"), ("bit6", "TPAFE_GAP", "event"),
("bit7", "STM32_GAP", "event"), ("bit8", "—", "none")]),
]),
dict(head="消费者侧的掩码与记忆位图", rows=[
dict(label="PROT_HF_LOCK_MASK", sub="protect_task.c 编译期常量",
note="命中即 prot_elm_init() 复位元件 + 本拍不 eval;SOE 5/6 带位图",
bits=[("bit0", "闭锁", "mask"), ("bit1", "闭锁", "mask"),
("bit2", "闭锁", "mask"), ("bit3", "不闭锁", "none"),
("bit4", "不闭锁", "none"), ("bit5", "不闭锁", "none"),
("bit6", "—", "none"), ("bit7", "—", "none"),
("bit8", "闭锁", "mask")]),
dict(label="serious 严重事件", sub="adc_task.c 的 adc_health_report()",
note="三类计数增量恒应为 0 → 每次 DBG_ERROR + 自检留痕",
bits=[("bit0", "—", "none"), ("bit1", "—", "none"), ("bit2", "—", "none"),
("bit3", "FRSTDATA", "mask"), ("bit4", "STM32_ERR", "mask"),
("bit5", "HW_ERR", "mask"), ("bit6", "—", "none"),
("bit7", "—", "none"), ("bit8", "—", "none")]),
dict(label="gap 丢帧事件", sub="同一函数里的丢帧分支",
note="刻意不告警:超拍是常态(启动突发期即累计 23 次),只累计",
bits=[("bit0", "—", "none"), ("bit1", "—", "none"), ("bit2", "—", "none"),
("bit3", "—", "none"), ("bit4", "—", "none"), ("bit5", "—", "none"),
("bit6", "TPAFE_GAP", "mask"), ("bit7", "STM32_GAP", "mask"),
("bit8", "—", "none")]),
dict(label="s_health_logged", sub="编排层记忆位图(每类一次留痕)",
note=" |= (serious | rise):用同一位号把 SYSLOG 压成每类每次开机一条",
bits=[("bit0", "记过", "mask"), ("bit1", "记过", "mask"),
("bit2", "记过", "mask"), ("bit3", "记过", "mask"),
("bit4", "记过", "mask"), ("bit5", "记过", "mask"),
("bit6", "不记", "none"), ("bit7", "不记", "none"),
("bit8", "不记", "none")]),
]),
]
FOOT = ("bit3 / bit4 / bit5 在 events 与 sticky 中刻意用同一位号 → 消费者能把事件位直接"
"套在锁存位图上做每类一次留痕;bit8 = 保护侧兜底门专用(判定层不写它)。"
"详见 docs/算法/采样有效性校验设计说明.md")
KIND_COLOR = dict(level=C_LEVEL, latch=C_LATCH, event=C_EVENT, mask=C_MASK, none=C_NONE)
# ----------------------------- 工具函数 -----------------------------
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 build():
# 位数列数由数据推导(曾硬编码 8 列,加 bit8 后备注框把新列盖住了)
NB = max(len(row["bits"]) for g in GROUPS for row in g["rows"])
body = []
addb = body.append
for g in GROUPS:
fit(g["head"], HEAD_FONT, W - 2 * PAD, "group head")
for r in g["rows"]:
fit(r["label"], ROW_LABEL_FONT, LABEL_W - 16, "row label")
fit(r["sub"], TAG_FONT, LABEL_W - 16, "row sub")
for ln in wrap(r["note"], NOTE_FONT, NOTE_W - 24):
fit(ln, NOTE_FONT, NOTE_W - 24, "row note")
for b, tag, kind in r["bits"]:
fit(tag, TAG_FONT, BIT_W - 12, "bit tag " + b)
fit(FOOT, NOTE_FONT, W - 2 * PAD - 32, "foot")
for w in WARN:
print("OVERFLOW WARN " + w)
row_h = BIT_H + 10
y = 178
for g in GROUPS:
addb('%s'
% (PAD, y, HEAD_FONT, esc(g["head"])))
y += 22
for r in g["rows"]:
cy = y
addb('%s'
% (PAD, cy + 20, ROW_LABEL_FONT, INK, esc(r["label"])))
addb('%s'
% (PAD, cy + 38, TAG_FONT, MUTED, esc(r["sub"])))
for i, (b, tag, kind) in enumerate(r["bits"]):
fg, bg = KIND_COLOR[kind]
x = GRID_X + i * (BIT_W + BIT_GAP)
addb(''
% (x, cy, BIT_W, BIT_H, bg, fg, "0.55" if kind != "none" else "0.35"))
addb(''
'%s' % (x + 10, cy + 19, BIT_FONT, fg, esc(b)))
addb('%s'
% (x + 10, cy + 36, TAG_FONT, esc(tag)))
nx = GRID_X + NB * (BIT_W + BIT_GAP) + 12
addb('' % (nx, cy, NOTE_W, BIT_H))
lines = wrap(r["note"], NOTE_FONT, NOTE_W - 24)
ty = cy + (BIT_H - (len(lines) - 1) * 15 + 4) / 2.0
for ln in lines:
addb('%s'
% (nx + 12, ty, NOTE_FONT, esc(ln)))
ty += 15
y += row_h
y += GROUP_GAP
# 图例
lx, ly = GRID_X, 118
for label, kind in [("电平位(可自愈)", "level"), ("锁存位(不自愈)", "latch"),
("边沿事件位(仅当拍)", "event"), ("消费者掩码命中 / 记忆位", "mask"),
("不适用", "none")]:
fg, bg = KIND_COLOR[kind]
addb('' % (lx, ly - 12, bg, fg))
lx += 22
addb('%s'
% (lx, ly, esc(label)))
lx += text_w(label, 12) + 30
total_h = y + 74
foot_y = total_h - 58
body.insert(0, '' % (W, total_h))
head = ['%s'
% (PAD, TITLE_FONT, INK, esc("采样有效性校验 · 出口位图与消费者掩码(逐位对齐)")),
'%s'
% (PAD, SUB_FONT, MUTED,
esc("同一 bit 在三张位图与四个掩码里始终落在同一列:位号对齐 = 消费者可以位图代打表"))]
svg = ('\n'
% (W, total_h, W, total_h, FONT, "\n".join(head), "\n".join(body),
PAD, foot_y, W - 2 * PAD, PAD + 16, foot_y + 27, NOTE_FONT, esc(FOOT)))
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))
def wrap(s, fs, avail):
"""按估算宽度做中文换行(无空格语言:逐字累计)。"""
lines, cur = [], ""
for ch in s:
if text_w(cur + ch, fs) > avail and cur:
lines.append(cur)
cur = ""
cur += ch
if cur:
lines.append(cur)
return lines
if __name__ == "__main__":
build()