Files
STM32F4-Base/tools/arch_diagram/gen_ac_rounds_svg.py
T
wandering1 ae09c5737a docs: 删任务规划两份已完成文档,数据可信内容迁入算法层/硬件平台/登记簿
任务规划目录从 5 个文件收敛到 3 个:只留导航(README)、待办(登记簿)、
暂缓专项(暂缓-RS485)。

删除的两个文件都不再有现行读者:
- UI补齐.md 执行清单 2026-10-28 清空、上板判据 2026-11-06 台架轮已复核;
  叶数口径迁 docs/UI设计/菜单页面设计.md §3.9,三条内容页硬规矩迁
  docs/UI设计/内容页架构.md §7.1,两条未闭环待办迁登记簿 §7/§8。
- 数据可信.md 不是完成记录,而是被约 130 处代码头注释引用的设计约束,
  因此按内容分三处落位后删除,不整篇丢弃。

数据可信内容的新归属(旧节号 → 新家):
- §2/§4 设计口径 → docs/算法/采集与算法层设计说明.md 新增 §13(13 小节),
  §7 前置决策表 → §13.12 已定案的前置决策。
- §3 台阶与波次退出判据 → docs/硬件平台/ADC通道映射说明.md 新增 §9。
- §5 任务卡 → docs/任务规划/登记簿.md §7(拆成变比确认 / 两点标定两行)。

引用改写 34 文件 77 处路径引用 + 14 文件 24 处昵称引用 + 双重节号 35 处,
覆盖代码头注释、测试、生成器与文档;归档区历史路径改纯文本提及。
四个 SVG 生成器文案同步并重跑,产物随脚本同批提交。

同批处理两件事:
- 9 个 UTF-16 LE 证据文件转 UTF-8。end-of-file-fixer 按字节判末尾换行,
  UTF-16 的换行是 0A 00、末字节 00,钩子会追加裸 0x0a 使文件变奇数长度,
  8 个已实际损坏。转码后逐文件比对 HEAD 原文验证正文零丢失。
- 修回 App/cfg/cfg_file.c 被剥掉的 UTF-8 BOM(补 3 字节,未整文件重写)。

验证:/keil-build 0 错误 0 警告(Code=255160);check_doc_links --all
--sections-strict 221 篇全通过、无法判定 0;pre-commit 全量全 Passed。
2026-11-09 16:13:57 +08:00

324 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""生成 docs/images/交流六轮速查.svg —— 交流(正弦)动态验证六轮:每一轮注什么信号、
验什么、数字从哪儿读,一张图先给全局轮廓,再进 §8.5 逐轮执行卡。
为什么不用 Mermaid:本图的语义重心是「每轮注入信号的波形形状」与「三块小面板里
信号缩略图 + 判据 + 读数的固定排版对位」——Mermaid 画不出正弦/方波/三角/削顶/谐波
叠加这些波形,也无法把六格对齐成 3x2 卡片。它只适合关系拓扑(那部分另在
docs/测试说明/专项验证/ADC前端注入验证流程.md §8.3 / §8.5 用 Mermaid 表达)。
用法(仓库根目录,任意 python3):
python tools/arch_diagram/gen_ac_rounds_svg.py
输出:
docs/images/交流六轮速查.svg
改内容:轮次事实(接法 / 判据 / 读数)变了才重画;调坐标 / 文案编辑下方常量后重跑。
"""
import html
import math
import os
import sys
import xml.etree.ElementTree as ET
def emit(s):
"""控制台打印按目标编码替换,绝不因 GBK 终端遇上 ≤/≈ 之类字符而崩
(generated-fresh 钩子会把本脚本拷进临时目录重跑,崩了就是一片假红)。"""
enc = getattr(sys.stdout, "encoding", None) or "ascii"
sys.stdout.write(s.encode(enc, "replace").decode(enc, "replace") + "\n")
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
OUT = os.path.join(ROOT, "docs", "images", "交流六轮速查.svg")
# ----------------------------- 画布与尺寸 -----------------------------
W = 1060
H = 680
PAD = 46
RIGHT = W - PAD
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
TITLE_FONT = 21
SUB_FONT = 13
HEAD_FONT = 15
LABEL_FONT = 11
BODY_FONT = 11.5
TINY_FONT = 10.5
INK = "#0f172a"
LINE = "#334155"
MUTED = "#64748b"
FAINT = "#94a3b8"
RULE = "#cbd5e1"
WHITE = "#ffffff"
C_BLUE = "#2563eb"
BG_BLUE = "#eff6ff"
C_TEAL = "#0d9488"
BG_TEAL = "#f0fdfa"
C_GREEN = "#16a34a"
BG_GREEN = "#f0fdf4"
C_AMBER = "#d97706"
BG_AMBER = "#fffbeb"
C_ROSE = "#e11d48"
BG_ROSE = "#fff1f2"
C_PURPLE = "#7c3aed"
BG_PURPLE = "#f5f3ff"
# ----------------------------- 布局 -----------------------------
COLS = 3
CW = 314
CH = 246
GAPX = 12
GAPY = 14
X0 = PAD
Y0 = 106
THUMB_H = 86
LABEL_W = 36 # 「注入 / 判据 / 读数」标签列宽
# ----------------------------- 数据(改这里) -----------------------------
TITLE = "交流动态验证六轮 —— 每一轮注什么信号、读哪个口"
SUB1 = ("一卡一轮:注入信号缩略图 → 接法与设定 → 判据一句话 → 数字从哪儿读。"
"先看图建立全局轮廓,再按 §8.5 的对应执行卡逐轮跑")
SUB2 = ("六轮共用同一套接线与通用动作;过判据的属主是 采集与算法层设计说明.md §13.10,"
"本图只是导航,不含新判据")
# 每卡:(轮名, 角标, 主色, 底色, 波形类型, 注入, 判据, 读数的地方)
CARDS = [
("幅值轮", "先做 · 三档幅度", C_BLUE, BG_BLUE, "amp",
"UA(R31)端子侧,正弦 50.000 Hz,三档 Vpp 2.828 / 1.414 / 0.566",
"每档 ua_mv ≈ 万用表 × 1000,偏差 ≤2 %",
"meas: ua_mv 为主(1 mV 分辨率)"),
("波形轮", "真有效值 ≠ 基波", C_TEAL, BG_TEAL, "three",
"同设 Vpp 2.828 V 改方波 / 三角(面板有效值各异)",
"读数只跟基波:方波 ≈1278、三角 ≈817,±2 %",
"meas: ua_mv;抬头记万用表响应类型"),
("相位轮", "定量 · 录波", C_GREEN, BG_GREEN, "phase",
"UA(R31)与 UN(R28)同源同幅 Vpp 2.828 V",
"同段内相位差 ≈0°,B 通道改 90° 应跟到 ≈90°,±2°",
"wave_read.py fft 定量;phasor(hb) 只核象限"),
("测频轮", "测不出数 = 过", C_AMBER, BG_AMBER, "sweep",
"UA 同幅值轮接法,扫 45 / 50 / 55 Hz",
"如实报不可测(现状固件):valid=0、UI 显 --;报 50.00 反而是 bug",
"freq(hb) 0.01 Hz;UI Hz"),
("削顶轮", "负向 · 单独日志", C_ROSE, BG_ROSE, "clip",
"UB(R52):DC −1.00 V 保持 ≥5 s,或 2.0 Hz 正弦",
"连续 20 拍贴底 → rail / edge 锁存;50 Hz 正弦不锁存 = 预期",
"cal: rail / edge;录波再跑 shape 对表"),
("谐波轮", "定量 · 录波", C_PURPLE, BG_PURPLE, "harm",
"IA(R19):分三注,基波 Vpp 2.263、二五次各 Vpp 0.453",
"Ia2 / Ia5 各读数 ≈0.160 V,±2 %(最坏峰值 1.58 V 留余量)",
"wave_read.py fft --ch 4;UI Ia2 / Ia5 目检旁证"),
]
FOOTNOTE = ("六轮已跑完(2026-11-02 / 11-04):实测结论见 docs/硬件平台/ADC通道映射说明.md §8.2,"
"谐波 B 行已归因板端前端 RC(fc≈796 Hz)。波形缩略图只表形状,不代表真实幅度刻度。")
# ----------------------------- 工具函数 -----------------------------
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 txt(x, y, s, fs, fill, weight=400, anchor=None):
a = "" if anchor is None else ' text-anchor="%s"' % anchor
w = "" if weight == 400 else ' font-weight="%d"' % weight
return ('<text x="%.1f" y="%.1f" font-size="%g"%s%s fill="%s">%s</text>'
% (x, y, fs, a, w, fill, esc(s)))
def line(x1, y1, x2, y2, color=LINE, width=2.0, dash=None):
d = "" if dash is None else ' stroke-dasharray="%s"' % dash
return ('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="%g"%s/>'
% (x1, y1, x2, y2, color, width, d))
def rect(x, y, w, h, fill, stroke=None, rx=8, sw=1.6):
s = "" if stroke is None else ' stroke="%s" stroke-width="%g"' % (stroke, sw)
return '<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="%g" fill="%s"%s/>' % (
x, y, w, h, rx, fill, s)
def poly(pts, color, sw=2.0, dash=None):
d = "" if dash is None else ' stroke-dasharray="%s"' % dash
p = " ".join("%.1f,%.1f" % (x, y) for x, y in pts)
return ('<polyline points="%s" fill="none" stroke="%s" stroke-width="%g" '
'stroke-linejoin="round"%s/>' % (p, color, sw, d))
def wrap(s, fs, avail, max_lines, where=""):
"""按估算宽度折行(CJK 逐字可断)。超出 max_lines 用省略号收尾——
宁可提示文案太长,也不让字溢出盒子。"""
if text_w(s, fs) <= avail:
return [s]
lines = []
cur = ""
for ch in s:
if text_w(cur + ch, fs) > avail:
lines.append(cur)
cur = ch
else:
cur += ch
if cur:
lines.append(cur)
if len(lines) > max_lines:
OVERFLOW.append("%s: 折行 %d > %d" % (where, len(lines), max_lines))
lines = lines[:max_lines]
lines[max_lines - 1] = lines[max_lines - 1][:-1] + "…"
return lines
# ----------------------------- 波形缩略图 -----------------------------
def _sample(fv, x, y0, w, h, n=140):
"""把 [-1,1] 的值函数 fv(t in 0..1) 画进框 (x, y0, w, h)。"""
yc = y0 + h / 2.0
amp = h * 0.36
pts = []
for i in range(n):
t = float(i) / (n - 1)
pts.append((x + w * t, yc - amp * fv(t)))
return pts
def thumb(x, y, w, h, kind, c):
out = [rect(x, y, w, h, WHITE, RULE, rx=6, sw=1.0)]
pad = 10.0
ix, iw, ih = x + pad, w - 2 * pad, h - 2 * pad
if kind == "amp":
out.append(line(x + 4, y + h / 2, x + w - 4, y + h / 2, FAINT, 0.8, dash="3 3"))
yc = y + h / 2.0
amp = ih * 0.36
out.append(line(x + pad, yc - amp, x + w - pad, yc - amp, c, 1.0, dash="5 4"))
out.append(line(x + pad, yc + amp, x + w - pad, yc + amp, c, 1.0, dash="5 4"))
out.append(poly(_sample(lambda t: math.sin(2 * math.pi * 2 * t), ix, y + pad, iw, ih), c))
out.append(txt(x + pad, y + pad - 1, "峰 ±1.414 V · 基波有效值 1.000 V", TINY_FONT, MUTED))
elif kind == "three":
yc = y + h / 2.0
amp = ih * 0.36
out.append(line(x + pad, yc - amp, x + w - pad, yc - amp, FAINT, 0.9, dash="5 4"))
slot = iw / 3.0
kinds = [lambda t: math.sin(4 * math.pi * t),
lambda t: 1.0 if math.sin(4 * math.pi * t) >= 0 else -1.0,
lambda t: (2.0 / math.pi) * math.asin(math.sin(4 * math.pi * t))]
names = ["正弦", "方波", "三角"]
for i, fv in enumerate(kinds):
sx = ix + i * slot
out.append(poly(_sample(fv, sx + 6, y + pad, slot - 12, ih, n=120), c, 1.8))
out.append(txt(sx + slot / 2, y + h - 2, names[i], TINY_FONT, MUTED, anchor="middle"))
out.append(txt(x + pad, y + pad - 1, "同设 Vpp 2.828 V(不是同有效值)", TINY_FONT, MUTED))
elif kind == "phase":
out.append(line(x + 4, y + h / 2, x + w - 4, y + h / 2, FAINT, 0.8, dash="3 3"))
out.append(poly(_sample(lambda t: math.sin(2 * math.pi * 1.5 * t), ix, y + pad, iw, ih),
c, 2.0))
out.append(poly(_sample(lambda t: math.sin(2 * math.pi * 1.5 * t + math.pi / 2),
ix, y + pad, iw, ih), C_ROSE, 1.6, dash="6 4"))
out.append(txt(x + pad, y + h - 2, "UA 0° 实线", TINY_FONT, c, weight=700))
out.append(txt(x + pad + 78, y + h - 2, "UN +90° 红虚线", TINY_FONT, C_ROSE, weight=700))
elif kind == "sweep":
out.append(line(x + 4, y + h / 2, x + w - 4, y + h / 2, FAINT, 0.8, dash="3 3"))
slot = iw / 3.0
cyc = [0.9, 1.4, 2.1]
labs = ["45 Hz", "50 Hz", "55 Hz"]
for i in range(3):
sx = ix + i * slot
out.append(poly(_sample(lambda t, cc=cyc[i]: math.sin(2 * math.pi * cc * t),
sx + 5, y + pad, slot - 10, ih, n=100), c, 1.8))
out.append(txt(sx + slot / 2, y + h - 2, labs[i], TINY_FONT, MUTED, anchor="middle"))
elif kind == "clip":
floor_y = y + pad + ih * 0.86
out.append(line(x + pad, floor_y, x + w - pad, floor_y, C_ROSE, 1.2, dash="5 4"))
yc = y + pad + ih * 0.40
amp = ih * 0.46
pts = []
n = 160
for i in range(n):
t = float(i) / (n - 1)
v = math.sin(2 * math.pi * 2 * t)
vv = max(v, 0.0) * 2.0 - 1.0
pts.append((ix + iw * t, yc - amp * vv))
out.append(poly(pts, c, 2.0))
out.append(txt(x + pad, y + pad - 1, "负半周钉 0(贴下轨)", TINY_FONT, MUTED))
elif kind == "harm":
out.append(line(x + 4, y + h / 2, x + w - 4, y + h / 2, FAINT, 0.8, dash="3 3"))
def fv(t):
w1 = 2 * math.pi * 1.5 * t
return (math.sin(w1) + 0.2 * math.sin(2 * w1) + 0.2 * math.sin(5 * w1)) / 1.35
out.append(poly(_sample(fv, ix, y + pad, iw, ih), c, 2.0))
out.append(txt(x + pad, y + pad - 1, "基波 + 2 次 + 5 次", TINY_FONT, MUTED))
return out
# ----------------------------- 生成 -----------------------------
def build():
out = []
add = out.append
avail = RIGHT - PAD
for s, fs, where in ((TITLE, TITLE_FONT, "title"), (SUB1, SUB_FONT, "sub1"),
(SUB2, SUB_FONT, "sub2")):
check_fit(s, fs, avail, where)
body_x = X0 + 12 + LABEL_W
body_avail = CW - 24 - LABEL_W
for idx, (name, tag, c, bg, kind, inj, rule, ex) in enumerate(CARDS):
cx = X0 + (idx % COLS) * (CW + GAPX)
cy = Y0 + (idx // COLS) * (CH + GAPY)
body_x = cx + 12 + LABEL_W
check_fit(name + tag, HEAD_FONT, CW - 24, "card %s head" % name)
for s, where in ((inj, "inj"), (rule, "rule"), (ex, "exit")):
check_fit(s[:14], BODY_FONT, body_avail, "card %s %s firstline" % (name, where))
add(rect(cx, cy, CW, CH, bg, c, rx=10, sw=2.0))
add(txt(cx + 12, cy + 26, name, HEAD_FONT, INK, weight=700))
add(txt(cx + CW - 12, cy + 26, tag, TINY_FONT, c, weight=700, anchor="end"))
for el in thumb(cx + 12, cy + 36, CW - 24, THUMB_H, kind, c):
add(el)
ty = cy + 36 + THUMB_H + 16
for label, s, mx in (("注入", inj, 2), ("判据", rule, 2), ("读数", ex, 2)):
add(txt(cx + 12, ty, label, LABEL_FONT, c, weight=700))
for ln in wrap(s, BODY_FONT, body_avail, mx, "card %s %s" % (name, label)):
add(txt(body_x, ty, ln, BODY_FONT, INK if label == "判据" else MUTED))
ty += 16
ty += 4
fy = Y0 + 2 * CH + GAPY + 22
add(line(PAD, fy, RIGHT, fy, RULE, 1.0))
for i, ln in enumerate(wrap(FOOTNOTE, SUB_FONT, avail, 2, "foot wrap")):
add(txt(PAD, fy + 22 + i * 19, ln, SUB_FONT, MUTED))
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, H, W, 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)
emit("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, H, OUT))
if OVERFLOW:
for o in OVERFLOW:
emit("OVERFLOW WARN " + o)
emit("!! %d overflow warnings" % len(OVERFLOW))
if __name__ == "__main__":
build()