Files
STM32F4-Base/tools/arch_diagram/gen_ac_median_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

275 lines
11 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 —— 交流轮为什么必须「连取 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 是上送节拍、不是平均窗(§13.9)——"
"单行可能恰好抓到波峰或波谷,毛刺行会拖偏算术平均,中位数对两头都钝感")
# 十行 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()