docs(测试说明): 交流动态验证轮次加四张导读图(两 SVG 生成器 + 两 Mermaid)
§8 开头插「六轮速查卡」:3x2 面板,每轮注入信号缩略图数据驱动
(正弦 / 同峰值方波三角 / UA·UN 异相 / 扫频 / 负半周钉 0 / 谐波叠
加),一卡给接法、判据一句话、主读数出口,先立轮廓再进执行卡。
§8.5 插「为什么连取 10 行取中位数」概念图:左栏机理(定宽 20 拍
窗逐窗滑动 → 读数拍频跳),右栏操作(中位数 / 平均 / 真值三线
错位对照 + 毛刺行标注)——取数口径 4e4d597e 刚定,此图补原理。
§8.3 / §8.5 各加一张 Mermaid:接线拓扑(端接高阻、万用表并接、
单点共地、STM32 五路仅削顶轮支线);单轮执行主干(采集链与偏差
两级决策、分诊虚线回环,汇于关输出填表)。
新增生成器 gen_ac_rounds_svg.py / gen_ac_median_svg.py(纯标准库、
OVERFLOW 预检归零、generated-fresh 自动在册),README 补两行台账。
验证:两产物连跑两次 sha1 不变;check_generated_fresh rc=0 无
DIFF / NEW、漂移基线不增;mermaid-syntax 2 围栏新发 0;headless
Chrome 截图回读三轮,修掉两类只有渲染才暴露的真错(正文 x 在卡
循环外算导致第 2、3 列叠进第 1 列;三角波闭式值域 −2..4 画越界
尖峰)。六文件 BOM / CR / U+FFFD 均零。
注:当前 check_doc_links --all 报 27 处,全属并发会话在改的产品
需求三份文档,与本笔无关、未触碰。
This commit is contained in:
@@ -25,6 +25,8 @@
|
||||
| `gen_sample_ring_why_svg.py` | 采样环缓冲_为什么需要 | 上游 1 kHz 覆盖写只交最新一帧 vs 下游要 20 点滑窗:缺口 19 点与三个消费者 |
|
||||
| `gen_sample_ring_layout_svg.py` | 采样环缓冲_环形布局 | 40 格环里哪 20 格是本窗、写指针指「下一格」、回绕与三条设计不变量 |
|
||||
| `gen_sample_ring_start_svg.py` | 采样环缓冲_窗首下标 | 窗首 = head − n 的圆周走法,与「直接减 / 16 位回绕 / 先加 DEPTH」三种写法对照 |
|
||||
| `gen_ac_rounds_svg.py` | 交流六轮速查 | 执行手册 §8 六轮总览卡:每轮注什么波形(正弦/方波三角/异相/扫频/削顶/谐波叠加缩略图)、判据一句话、主读数出口 |
|
||||
| `gen_ac_median_svg.py` | 交流定数取中位数 | 「连取 10 行取中位数」的机理(全周窗滑动 → 读数拍频跳)与操作(中位数 vs 平均 vs 单行对真值)两栏数值示意 |
|
||||
|
||||
## 使用方式
|
||||
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""生成 docs/images/交流定数取中位数.svg —— 交流轮为什么必须「连取 10 行心跳、取中位数」:
|
||||
左半讲机理(全周窗逐拍滑动 → 单行带拍频跳动),右半讲操作(10 行里任何单行都可能撞
|
||||
波峰波谷、毛刺行拖偏算术平均,中位数对两头都钝感)。
|
||||
|
||||
为什么不用 Mermaid:本图是两幅带刻度的数值示意图(正弦 + 窗框、散点 + 三条水平基准线),
|
||||
坐标与数值就是语义本身;Mermaid 只能画盒子和箭头,画不出「中位数线贴着密集簇、
|
||||
平均线被毛刺拽走」这个高度差对比。
|
||||
|
||||
用法(仓库根目录,任意 python3):
|
||||
python tools/arch_diagram/gen_ac_median_svg.py
|
||||
输出:
|
||||
docs/images/交流定数取中位数.svg
|
||||
改内容:`vals` / 文案变了才重跑;调坐标编辑下方常量。数值一律硬编码,保证逐字节幂等。
|
||||
"""
|
||||
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 = 566
|
||||
PAD = 46
|
||||
RIGHT = W - PAD
|
||||
|
||||
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
|
||||
TITLE_FONT = 21
|
||||
SUB_FONT = 12.5
|
||||
HEAD_FONT = 13.5
|
||||
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_GREEN = "#16a34a"
|
||||
C_ROSE = "#e11d48"
|
||||
BG_ROSE = "#fff1f2"
|
||||
C_AMBER = "#d97706"
|
||||
|
||||
# ----------------------------- 数据(改这里) -----------------------------
|
||||
TITLE = "交流轮为什么连取 10 行、取中位数 —— 机理与操作"
|
||||
SUB1 = ("meas: 是瞬时基波有效值透传:200 ms 是上送节拍、不是平均窗(数据可信 §4.5)——"
|
||||
"单行可能恰好抓到波峰或波谷,毛刺行会拖偏算术平均,中位数对两头都钝感")
|
||||
|
||||
# 十行 ua_mv(1 mV 分辨率):第 9 行是偶发毛刺行
|
||||
VALS = [998, 1003, 996, 1001, 999, 1006, 997, 1004, 1002, 1030]
|
||||
GLITCH_I = 9
|
||||
TRUE_V = 1000.0
|
||||
YLO, YHI = 988.0, 1036.0
|
||||
|
||||
OPS_NOTE = ("操作:稳定 ≥30 s 后,把连续 10 行心跳的 ua_mv 记进记录表(备注顺手记跳动区间 min–max),"
|
||||
"排序取第 5、6 个的平均 = 本条代表数;偏差 % 用它对万用表 × 1000。")
|
||||
FOOTNOTE = ("非整 50 Hz 时 meas: 来回跳是预期(§8.8 分诊同一条);扫频时读 freq(hb),别读 meas:。"
|
||||
"左图频差为示意刻意夸大,实拍跳动只有几个 LSB 的量级。")
|
||||
|
||||
# ----------------------------- 布局 -----------------------------
|
||||
LPX, LPW = PAD, 424 # 左面板(机理)
|
||||
RPX, RPW = 522, RIGHT - 522 # 右面板(操作)
|
||||
PLOT_T, PLOT_B = 176.0, 404.0
|
||||
HDR_Y = 146.0
|
||||
|
||||
# ----------------------------- 工具函数 -----------------------------
|
||||
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=""):
|
||||
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 mechanism():
|
||||
out = []
|
||||
add = out.append
|
||||
x0, x1 = LPX, LPX + LPW - 40
|
||||
per = 24.0 # 示意:信号周期 24 拍(真实约 20 拍;频差夸大)
|
||||
win = 20.0 # 全周窗固定 20 拍
|
||||
tmax = 44.0
|
||||
def X(t):
|
||||
return x0 + (x1 - x0) * t / tmax
|
||||
yc = (PLOT_T + PLOT_B) / 2 - 14
|
||||
amp = 74.0
|
||||
|
||||
add(txt(LPX, HDR_Y, "机理:窗定宽 20 拍,截到的相位逐窗滑动", HEAD_FONT, INK, weight=700))
|
||||
# 信号
|
||||
pts = []
|
||||
for i in range(221):
|
||||
t = 44.0 * i / 220.0
|
||||
pts.append((X(t), yc - amp * math.sin(2 * math.pi * t / per)))
|
||||
add(poly(pts, LINE, 1.8))
|
||||
# 两个窗
|
||||
for (t0, c, dash, name) in ((0.0, C_BLUE, None, "窗 A"),
|
||||
(20.0, C_ROSE, "6 4", "窗 B(下一行心跳)")):
|
||||
add(rect(X(t0), yc - amp - 16, X(t0 + win) - X(t0), 2 * amp + 32, "none", c, rx=6,
|
||||
sw=1.8 if dash is None else 1.4))
|
||||
add(txt(X(t0) + 4, yc - amp - 22, name, TINY_FONT, c, weight=700))
|
||||
# 拍刻度(每拍一根,只在窗内画,避免刷屏)
|
||||
for k in range(0, 41):
|
||||
add(line(X(k), PLOT_B - 30, X(k), PLOT_B - 24, FAINT, 0.8))
|
||||
add(line(x0, PLOT_B - 24, x1 + 24, PLOT_B - 24, RULE, 1.0))
|
||||
add(txt(x1 + 24, PLOT_B - 10, "拍(1 kHz)", TINY_FONT, MUTED, anchor="end"))
|
||||
# 尾部相位差说明
|
||||
add(txt(x0, PLOT_B + 6,
|
||||
"发生器频率与 1 kHz 时基不严格同步:每过一个窗,尾部多算 / 少算一小段 →",
|
||||
BODY_FONT, MUTED))
|
||||
add(txt(x0, PLOT_B + 24,
|
||||
"基波值围绕真值上下小幅摆动(拍频式)→ 每一行心跳落在摆动带的不同位置",
|
||||
BODY_FONT, MUTED))
|
||||
return out
|
||||
|
||||
|
||||
# ----------------------------- 右:取数操作 -----------------------------
|
||||
def operation():
|
||||
out = []
|
||||
add = out.append
|
||||
add(txt(RPX, HDR_Y, "操作:这 10 行里取哪个数当代表", HEAD_FONT, INK, weight=700))
|
||||
ax0, ax1 = RPX, RPX + RPW - 150
|
||||
ay0, ay1 = PLOT_T + 8, PLOT_B - 8
|
||||
def Y(v):
|
||||
return ay1 - (v - YLO) / (YHI - YLO) * (ay1 - ay0)
|
||||
def X(i):
|
||||
return ax0 + 24 + i * ((ax1 - ax0 - 40) / 9.0)
|
||||
|
||||
add(line(ax0, ay0 - 12, ax0, ay1 + 6, LINE, 1.6))
|
||||
add(line(ax0, ay1 + 6, ax1, ay1 + 6, LINE, 1.6))
|
||||
for v in (990, 1000, 1010, 1030):
|
||||
add(line(ax0 - 4, Y(v), ax1, Y(v), "#e2e8f0" if v != 990 else RULE, 0.8))
|
||||
add(txt(ax0 - 8, Y(v) + 4, str(v), TINY_FONT, MUTED, anchor="end"))
|
||||
add(txt(ax0 - 8, ay0 - 16, "ua_mv", TINY_FONT, MUTED, anchor="end"))
|
||||
|
||||
srt = sorted(VALS)
|
||||
med = (srt[4] + srt[5]) / 2.0
|
||||
mean = sum(VALS) / float(len(VALS))
|
||||
|
||||
# 三条水平基准:右侧标签逐条错开(线位太近,同高必叠字)
|
||||
lx = ax1 + 8
|
||||
add(line(ax0 + 6, Y(TRUE_V), ax1, Y(TRUE_V), MUTED, 1.2, dash="6 4"))
|
||||
add(txt(lx, Y(TRUE_V) + 15, "真值(万用表 × 1000)", TINY_FONT, MUTED))
|
||||
check_fit("真值(万用表 × 1000)", TINY_FONT, RIGHT - lx, "lbl true")
|
||||
add(line(ax0 + 6, Y(med), ax1, Y(med), C_GREEN, 2.4))
|
||||
add(txt(lx, Y(med) - 3, "中位数 %g ← 本条取它" % med, TINY_FONT, C_GREEN, weight=700))
|
||||
check_fit("中位数 %g ← 本条取它" % med, TINY_FONT, RIGHT - lx, "lbl med")
|
||||
add(line(ax0 + 6, Y(mean), ax1, Y(mean), C_BLUE, 1.4, dash="2 3"))
|
||||
add(txt(lx, Y(mean) - 20, "平均 %g(被拖高)" % mean, TINY_FONT, C_BLUE))
|
||||
check_fit("平均 %g(被拖高)" % mean, TINY_FONT, RIGHT - lx, "lbl mean")
|
||||
|
||||
# 十个点
|
||||
for i, v in enumerate(VALS):
|
||||
c = C_ROSE if i == GLITCH_I else LINE
|
||||
r = 4.6 if i == GLITCH_I else 3.6
|
||||
add('<circle cx="%.1f" cy="%.1f" r="%.1f" fill="%s"/>' % (X(i), Y(v), r, c))
|
||||
add(txt(X(i), ay1 + 22, str(i + 1), TINY_FONT, MUTED, anchor="middle"))
|
||||
add(txt(X(GLITCH_I), Y(VALS[GLITCH_I]) - 10, "毛刺行", TINY_FONT, C_ROSE, weight=700,
|
||||
anchor="middle"))
|
||||
add(txt(ax0 + 24, ay1 + 40, "心跳行号(每行间隔 200 ms)", TINY_FONT, MUTED))
|
||||
return out
|
||||
|
||||
|
||||
# ----------------------------- 生成 -----------------------------
|
||||
def build():
|
||||
avail = RIGHT - PAD
|
||||
check_fit(TITLE, TITLE_FONT, avail, "title")
|
||||
check_fit(SUB1, SUB_FONT, avail, "sub1")
|
||||
|
||||
out = ['<rect x="0" y="0" width="%d" height="%d" fill="%s"/>' % (W, H, WHITE)]
|
||||
out.append(txt(PAD, 42, TITLE, TITLE_FONT, INK, weight=700))
|
||||
out.append(txt(PAD, 68, SUB1, SUB_FONT, MUTED))
|
||||
|
||||
out.extend(mechanism())
|
||||
out.extend(operation())
|
||||
|
||||
# 竖向分隔
|
||||
out.append(line(497.0, 120.0, 497.0, 452.0, RULE, 1.0))
|
||||
|
||||
ny = 462
|
||||
for i, ln in enumerate(wrap(OPS_NOTE, BODY_FONT, avail, 2, "ops")):
|
||||
out.append(txt(PAD, ny + i * 17, ln, BODY_FONT, INK, weight=700))
|
||||
fy = ny + 40
|
||||
out.append(line(PAD, fy, RIGHT, fy, RULE, 1.0))
|
||||
for i, ln in enumerate(wrap(FOOTNOTE, SUB_FONT, avail, 2, "foot")):
|
||||
out.append(txt(PAD, fy + 20 + i * 18, 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()
|
||||
@@ -0,0 +1,323 @@
|
||||
#!/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 = ("六轮共用同一套接线与通用动作;过判据的属主是 docs/任务规划/数据可信.md §4.6.2,"
|
||||
"本图只是导航,不含新判据")
|
||||
|
||||
# 每卡:(轮名, 角标, 主色, 底色, 波形类型, 注入, 判据, 出口)
|
||||
CARDS = [
|
||||
("幅值轮", "先做 · 主用例", C_BLUE, BG_BLUE, "amp",
|
||||
"UA(R31)端子侧,正弦 50.000 Hz、1.000 Vrms",
|
||||
"ua_mv 中位数 ≈ 万用表 × 1000,偏差 ≤2 %",
|
||||
"meas: ua_mv 为主;104 0x4011 只作交叉读数"),
|
||||
("波形轮", "真 RMS ≠ 基波", C_TEAL, BG_TEAL, "three",
|
||||
"同峰值 1.414 V 改方波 / 三角(面板有效值各异)",
|
||||
"读数只跟基波:方波 ≈1278、三角 ≈817,±2 %",
|
||||
"meas: ua_mv;抬头记万用表响应类型"),
|
||||
("相位轮", "定量 · 取段", C_GREEN, BG_GREEN, "phase",
|
||||
"UA(R31)与 UN(R28)同源同幅 1.000 Vrms",
|
||||
"同段内相位差 ≈0°,B 通道改 90° 应跟到 ≈90°,±2°",
|
||||
"wave_read.py fft 定量;phasor(hb) 只核象限"),
|
||||
("测频轮", "A 档无数 = 过", C_AMBER, BG_AMBER, "sweep",
|
||||
"UA 同幅值轮接法,扫 45 / 50 / 55 Hz",
|
||||
"A 档如实报不可测:valid=0、UI 显 --;报 50.00 反而是 bug",
|
||||
"freq(hb) 0.01 Hz;UI Hz、104 0x401D"),
|
||||
("削顶轮", "负向 · 单独日志", C_ROSE, BG_ROSE, "clip",
|
||||
"UB(R52):DC −1.00 V 保持 ≥5 s,或 2.0 Hz 正弦",
|
||||
"连续 20 拍贴底 → rail / edge 锁存 + 104 该路 IV 清零;50 Hz 正弦不锁存 = 预期",
|
||||
"cal: rail / edge;取段再跑 shape 对表"),
|
||||
("谐波轮", "定量 · 取段", C_PURPLE, BG_PURPLE, "harm",
|
||||
"IA(R19):基波 0.800 + 二次 0.160 + 五次 0.160 Vrms",
|
||||
"Ia2 / Ia5 各 ≈0.160 Vrms,±2 %(最坏峰值 1.58 V 留余量)",
|
||||
"wave_read.py fft --ch 4;UI Ia2 / Ia5 目检旁证"),
|
||||
]
|
||||
|
||||
FOOTNOTE = ("顺序与耗时见 §8.6(约 135 min 一趟 session);[ERR] 判读见 §8.7,失败先查 §8.8 分诊表。"
|
||||
"波形缩略图只表形状,不代表真实幅度刻度。")
|
||||
|
||||
# ----------------------------- 工具函数 -----------------------------
|
||||
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 · 基波 RMS 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, "同峰值对齐(不是同 Vrms)", 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()
|
||||
Reference in New Issue
Block a user