#!/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%s\n%s\n' '\n' '%s\n\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()