docs(algo): 新增 adc_health 单文件深设计与三张配图
- 新建 docs/算法/采样有效性校验设计说明.md(15 章):判据推导
(100 ms 门限的三条独立依据)、电平/锁存/事件三种位语义与
位号对齐的收益代价、闭锁与 IWDG 兜底责任矩阵、观测盲区、
主机侧验证分工(11 组 TC / 31 断言映射)、现场判读手册、
自测问题与改前必读清单
- 配图走 draw-diagram 生成器路线(脚本 + 产物一并提交):
tools/arch_diagram/gen_adc_health_flow_svg.py 等三个脚本 →
docs/images/ 下三张 SVG;文内另有 2 张 Mermaid(判定主干
flowchart、静默位生命周期 stateDiagram-v2,沿用仓内既有方言)
- 登记与指针:docs/README.md §5.2 加一行 + 变更记录;
docs/算法/采集与算法层设计说明.md §4 与 §13 加深读指针;
本文只写「为什么」,接口表仍由该文档属主,不重复
- 读码确认并如实登记三项(本提交不改代码,待裁):
1) adc_task_init() 全库无调用点,adc_health_init() 因此不在
产品路径,判定正确只依赖 .bss 清零 + 首拍建基线
2) tgap 计帧点数(d-1 加权),adc1_gap 计次数——两侧单位
不对称,心跳里不可直接对比
3) TIM3 停摆时判定层随消费者一起停,最坏约 800 ms + 2498 ms
窗口内 protectTask 仍以旧相量判据(细节见 §10 / §14)
- 门禁自查:pre-commit 目标集全绿(doc-links / id-check /
空白 / EOF / 大文件);主机单测 adc_health 31/31 复跑通过;
9 个新增或改动文件 BOM=无、CR=0、U+FFFD=0
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""生成 docs/images/采样有效性位图与掩码.svg —— L1-6 出口位图与消费者掩码对照(纯标准库)。
|
||||
|
||||
为什么不用 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 = 112, 46, 4
|
||||
NOTE_W = 340
|
||||
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 读它做 L5-4 闭锁;不看事件位",
|
||||
bits=[("bit0", "TPAFE_STALL", "level"), ("bit1", "STM32_STALL", "level"),
|
||||
("bit2", "SEQ_BACKWARD", "latch"), ("bit3", "—", "none"),
|
||||
("bit4", "—", "none"), ("bit5", "—", "none"),
|
||||
("bit6", "—", "none"), ("bit7", "—", "none")]),
|
||||
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")]),
|
||||
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")]),
|
||||
]),
|
||||
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")]),
|
||||
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")]),
|
||||
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")]),
|
||||
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")]),
|
||||
]),
|
||||
]
|
||||
|
||||
FOOT = ("bit3 / bit4 / bit5 在 events 与 sticky 中刻意用同一位号 → 消费者能把事件位直接"
|
||||
"套在锁存位图上做每类一次留痕;bit8 以上全 32 位保留未用。"
|
||||
"详见 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():
|
||||
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('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="#334155">%s</text>'
|
||||
% (PAD, y, HEAD_FONT, esc(g["head"])))
|
||||
y += 22
|
||||
for r in g["rows"]:
|
||||
cy = y
|
||||
addb('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="%s">%s</text>'
|
||||
% (PAD, cy + 20, ROW_LABEL_FONT, INK, esc(r["label"])))
|
||||
addb('<text x="%d" y="%d" font-size="%d" fill="%s">%s</text>'
|
||||
% (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('<rect x="%d" y="%d" width="%d" height="%d" rx="8" fill="%s" '
|
||||
'stroke="%s" stroke-opacity="%s"/>'
|
||||
% (x, cy, BIT_W, BIT_H, bg, fg, "0.55" if kind != "none" else "0.35"))
|
||||
addb('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="%s">'
|
||||
'%s</text>' % (x + 10, cy + 19, BIT_FONT, fg, esc(b)))
|
||||
addb('<text x="%d" y="%d" font-size="%d" fill="#334155">%s</text>'
|
||||
% (x + 10, cy + 36, TAG_FONT, esc(tag)))
|
||||
nx = GRID_X + 8 * (BIT_W + BIT_GAP) + 12
|
||||
addb('<rect x="%d" y="%d" width="%d" height="%d" rx="8" fill="#f8fafc" '
|
||||
'stroke="#64748b" stroke-opacity="0.25"/>' % (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('<text x="%d" y="%d" font-size="%d" fill="#475569">%s</text>'
|
||||
% (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('<rect x="%d" y="%d" width="16" height="16" rx="4" fill="%s" stroke="%s" '
|
||||
'stroke-width="1.6"/>' % (lx, ly - 12, bg, fg))
|
||||
lx += 22
|
||||
addb('<text x="%d" y="%d" font-size="12" fill="#475569">%s</text>'
|
||||
% (lx, ly, esc(label)))
|
||||
lx += text_w(label, 12) + 30
|
||||
|
||||
total_h = y + 74
|
||||
foot_y = total_h - 58
|
||||
body.insert(0, '<rect x="0" y="0" width="%d" height="%d" fill="#ffffff"/>' % (W, total_h))
|
||||
head = ['<text x="%d" y="44" font-size="%d" font-weight="700" fill="%s">%s</text>'
|
||||
% (PAD, TITLE_FONT, INK, esc("采样有效性校验 · 出口位图与消费者掩码(逐位对齐)")),
|
||||
'<text x="%d" y="70" font-size="%d" fill="%s">%s</text>'
|
||||
% (PAD, SUB_FONT, MUTED,
|
||||
esc("同一 bit 在三张位图与四个掩码里始终落在同一列:位号对齐 = 消费者可以位图代打表"))]
|
||||
|
||||
svg = ('<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" '
|
||||
'viewBox="0 0 %d %d" font-family="%s">\n%s\n%s\n'
|
||||
'<rect x="%d" y="%d" width="%d" height="44" rx="10" fill="#fffbeb" '
|
||||
'stroke="#d97706" stroke-opacity="0.45"/>\n'
|
||||
'<text x="%d" y="%d" font-size="%d" fill="#7c2d12">%s</text>\n</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()
|
||||
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""生成 docs/images/采样有效性校验数据流.svg —— L1-6「观测计数 → 快照 → 判定 → 位图 → 消费者」(纯标准库)。
|
||||
|
||||
为什么不用 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++:无跳变帧累计(E-44)",
|
||||
"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++:完成计数一次前进 >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()
|
||||
@@ -0,0 +1,300 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""生成 docs/images/采样有效性判定时间轴.svg —— L1-6 四个工况的时序与门限(纯标准库)。
|
||||
|
||||
为什么不用 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",
|
||||
" 计帧点数(d-1 加权),adc1_gap 计次数(每次 +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()
|
||||
Reference in New Issue
Block a user