Files
STM32F4-Base/tools/arch_diagram/gen_net_instability_svg.py
T
wandering1 4c74cfa825 refactor(编号·批次二): 文档侧 194 处保护功能号改成功能名,定义表 7 张首格改语义标识
批次二文档半(道 gate-docs 执行)。先纠三个数的口径账,别再拿任何旧数当准:**234 处是准数** —— 卡片写
52(Lead 用了门禁 Checker 的引用集合,它**刻意排除属主文档内部的定义位**)、道里先报 265(裸正则把
`SDL2-2.32.10` 里的 `L2-2` 当成功能号,误报 10 处)。最终边界口径 = 带左边界 + 跳过反引号代码段。

· 19 只文件 HEAD 共 234 处 → 现 40 处,**40 处 100% 落在变更记录行**(冻结历史,按 Q3 一条没动),
  非变更记录残留 **0**;改写 163 行 / 16 只文档。站点最多:规划文档 110→37、相量层单测说明 25→1、
  采集与数据处理短板评测 21→0、App/algo/README 26→0。
  (`App/algo/README.md` 由上一笔代码提交带上车,路径归属已在该笔正文注明,内容是本批改写)
· **定义表首列由编号改成语义标识**(编号规范 §3.3「表首格就是短语」的完全形态):首列原为编号的表
  共 **7 张**(不是 5 张),删掉与之重复的「语义名」列;表头标签按域词区分——三张 L3 保护**元件**表
  保留 `| 元件 |`,四张 L1/L2 表用 `| 功能名 |`(把「元件」统一洗成功能名会丢掉领域信息)。
  七张表逐张核:表头竖线数 == 分隔行 == 首条数据行 == 列数+1;与 HEAD 逐块对拍**净新增不一致块 0**,
  并且正是这个对拍抓到第一版「三张表数据行已删列、表头没删」的破表。
· 7 处删号留下的残词/重复解释括号人工收口(如「L1-5(通道标定)」→「通道标定(一次系数 + 零漂)」),
  复扫待收口站点 0;措辞逐字取自映射表,无第二种说法。
· tools/arch_diagram 三只生成器 4 处(3 处 docstring + 1 处图形文案)与产物同批:重跑后**只有位图那张
  变 2 个 text 节点的换行位置**,画布 1580x740 与总行数不变,另两张逐字节未变;改前先干跑对照证明三张
  svg 原本与生成器一致(不在 stale 基线里)——`check_generated_fresh --only docs-svg` PASS。

本笔另含 **F-* 半批的文档面**(同一轮 `git add docs tools/arch_diagram AGENTS.md` 一并收进
,非误伤):BSD
函数调用关系里 9 处反引号紧邻指针、属主文档 §7 的停发句与导读行(**12 条定义行原样保留、不删登记族**)、
以及 `gen_net_instability_svg.py` 与它的产物 `网络不稳根因总览.svg`(「唯一读者红线」那条的框宽自适应
526 → 551,右缘 1411 未越界,画布 1560x1468 不变)——该半批的**代码注释面**在下一笔。

⚠ 因 `docs/任务规划/登记簿.md` 与 `docs/任务规划/UI补齐.md` 正被另一张卡(UI 叶数复核)改动,本笔**不含
那两只文件**,批次二的 §9 存量收尾与它们同批提交。
2026-10-27 09:05:24 +08:00

261 lines
12 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 —— 自愈架构复盘配图(纯标准库)。
布局:左列 = 七个误会(问题环,玫红),右列 = 对应拆法(绿),行间水平箭头;
左列环间竖向细箭头 = "叠加累积";底部两个总结盒。
为什么不用 Mermaid:双列对齐 + 水平映射箭头需要精确布局。
用法(仓库根目录,任意 python3):
python tools/arch_diagram/gen_net_instability_svg.py
输出:
docs/images/网络不稳根因总览.svg
改内容时:编辑本文件顶部的 RINGS / SUMMARY 数据后重跑即可。
"""
import html
import os
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
OUT = os.path.join(ROOT, "docs", "images", "网络不稳根因总览.svg")
# ----------------------------- 画布与尺寸参数 -----------------------------
W = 1560
PAD = 26
TITLE_FONT = 20
SUB_FONT = 13
BAND_TITLE_FONT = 15
BAND_SUB_FONT = 11.5
CHIP_FONT = 13
CHIP_H = 34 # 芯片盒高
CHIP_GAP = 10 # 芯片水平间距
CHIP_ROW_GAP = 8 # 芯片行间距
CHIP_PAD = 14 # 芯片内左右留白
BAND_PAD_X = 16 # 框内左右留白
BAND_TITLE_H = 32 # 框内标题区高
BAND_PAD_BOTTOM = 12
COL_GAP = 128 # 左右列间距(放映射箭头)
ROW_V_GAP = 40 # 行间距(放"叠加"竖向箭头)
TITLE_H = 78 # 顶部标题区高
SUM_H = 78 # 底部总结盒高
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
C_PROB = "#e11d48" # 问题列(玫红)
C_PROB_BG = "#fff1f2"
C_FIX = "#16a34a" # 拆法列(绿)
C_FIX_BG = "#f0fdf4"
C_GRAY = "#94a3b8"
# ----------------------------- 数据(改这里) -----------------------------
# (问题标题, 问题副标, [问题芯片], 拆法标题, [拆法芯片])
RINGS = [
("① 门铃听不见", "TRAP-NET-17:事件未收到(E14 复核:非芯片丢,是被读清消费)",
["症状:UI 复连永久拒连、FTP 第 29 轮自己死掉,全程不报一个错",
"机制:快速复连时门铃事件被读清消费,netTask 只靠门铃干活 → 状态静默滞留"],
"100ms 巡逻对账",
["单一 reconciler:判据读驱动状态缓存(E2 背书 18.1µs),分级去抖",
"门铃听不见不要紧,每 100ms 亲自数一遍人头"]),
("② 门铃总闸看一眼就清零", "TRAP-NET-20 B3",
["症状:别的 Socket 的 CONNECT/DISCONNECT 从此永久失踪",
"机制:全局中断寄存器整体读即清,open/relisten 丢弃式读一遍=拔全楼门铃线"],
"总闸唯一读者",
["唯一读者红线:全局寄存器唯一读者=net_poll;open/relisten 只清本 Socket SINT",
"查自己门铃时,不许拔别人家的线"]),
("③ 柜台和货梯是同一台", "单连接模型",
["症状:会话 EOF 之后端口「连接被拒」",
"机制:监听 fd == 数据 fd,应用在 EOF 时 net_close = 把柜台连电梯一起拆"],
"送客走正门",
["铁律:EOF 不得 close 监听 fd;新 API net_drop_conn(拆会话保柜台)",
"net_drop_conn = 优雅 FIN + 本地拆除 + relisten,幂等"]),
("④ 三个临时工都在修电梯", "多所有者 relisten",
["症状:重建风暴、慢消费者的 EOF 被吞、新客户端数据被旧会话吸收",
"机制:accept/recv/pending 三处都重建监听,谁先看到断连谁动手互相踩脚"],
"修电梯的只剩一个人",
["重建收敛到唯一所有者(net_listener + reconciler),其余位置只置事实不动手",
"不变量 II:唯一周期重建决策者(消费者即时重建三处保留,台架实测)"]),
("⑤ 传达员被按在椅子上", "队头阻塞",
["症状:FTP banner 迟到 25~30 s(台架实测),netTask 频繁冻结半秒",
"机制:30 s 阻塞收 + 500 ms 盲等忙等,而 netTask 是全楼唯一传达员"],
"传达员永不离开传达室",
["忙等统一为 net_sock_reset_sync 有界排渣(600 ms 硬上限),阻塞消息化",
"recv 语义主流化:net_recv 默认无限阻塞 + net_recv_nb + 60 s tripwire 日志"]),
("⑥ 僵尸客人赖着不走", "E13:网卡闪断",
["症状:设备网络「莫名死了」、新连接被拒——在和幽灵做生意",
"机制:PC 网卡闪断,内核悄悄撕掉 socket 不发 FIN,芯片侧仍 ESTABLISHED 占死柜台"],
"查岗处理幽灵",
["keepalive 60/5/3(E11 实测 ≈30 s 判死)+ 芯片 TIMEOUT → QUARANTINE → 重建",
"自愈 30~90 s,不需要冷启动(E13 台架收口:用户判断正确)"]),
("⑦ 只会礼貌送客不会轰人", "close 无 RST(手册定案)",
["症状:FIN_WAIT_2 长尾,监听恢复受限于对端 close(台架定案)",
"机制:芯片 close 只发 FIN;对端内核存活但应用挂死 = 回 ACK 却不起身"],
"知道他不轰人,就别指望他轰人",
["定案写入 TRAP-NET-07:兜底靠查岗(keepalive/TIMEOUT),不靠 RST",
"net_drop_conn 本地拆除 + reconciler 形态③④ 认领僵尸态"]),
("⑧ 垃圾不清越扫越脏", "残渣风暴态",
["症状:同一份固件故障率 2%~54% 摆动(E8 实测),看起来像「玄学」",
"机制:迟到 DISCONNECT 锁存 / 半死 Socket / 粘滞中断位,被带进下一条连接"],
"开张前把垃圾扫干净",
["sock_reset 标准排渣:读空 RX → CLOSE → 排空事件 → 确认落地(byte0=00H)",
"M3 矛盾态门 + M5 drain-until-quiescent(风暴态免疫)"]),
]
SUM_LEFT = ["七环叠加 =「玄学不稳」",
"随机 · 无错误日志 · 时好时坏——每个环节单看都「正常工作」"]
SUM_RIGHT = ["逐环拆掉 = 稳(现状机制已全部落地)",
"不依赖门铃 · 唯一主人 · 永不冻结 · 100ms 对账 · 开张前排渣——台架回归全绿"]
FOOT = ("证据链:docs/问题记录/CH395F_Trap_Records.md(TRAP-NET-07/-09/-12/-17/-18/-20) · "
"架构现状 docs/网络/网络自愈架构设计.md §2~§4 · 实证锚点同文档 §5.2(E0~E19)")
# ----------------------------- 工具函数 -----------------------------
def esc(s):
return html.escape(s, quote=True)
def text_w(s, fs):
"""粗略估算文本像素宽:CJK/全角/圈码/箭头 ≈ 1.0em,其余 ≈ 0.58em(宁大勿小)。"""
w = 0.0
for ch in s:
w += fs * (1.0 if ord(ch) >= 0x2460 else 0.58)
return w
def chip_w(label):
return text_w(label, CHIP_FONT) + 2 * CHIP_PAD
def wrap_chips(chips, avail):
rows, cur, cur_w = [], [], 0.0
for c in chips:
cw = chip_w(c)
need = cw if not cur else cur_w + CHIP_GAP + cw
if cur and need > avail:
rows.append(cur)
cur, cur_w = [c], cw
else:
cur.append(c)
cur_w = need
if cur:
rows.append(cur)
return rows
def band_h(rows):
return BAND_TITLE_H + len(rows) * CHIP_H + (len(rows) - 1) * CHIP_ROW_GAP + BAND_PAD_BOTTOM
# ----------------------------- 生成 -----------------------------
def draw_band(out, title, sub, chips, bx, by, bw, bh, color, bg):
add = out.append
add('<rect x="%d" y="%.1f" width="%d" height="%.1f" rx="10" fill="%s" stroke="%s" '
'stroke-opacity="0.45"/>' % (bx, by, bw, bh, bg, color))
add('<text x="%d" y="%.1f" font-size="%d" font-weight="700" fill="%s">%s</text>'
% (bx + BAND_PAD_X, by + 22, BAND_TITLE_FONT, color, esc(title)))
if sub:
tw = text_w(title, BAND_TITLE_FONT)
add('<text x="%.0f" y="%.1f" font-size="%s" fill="#64748b">%s</text>'
% (bx + BAND_PAD_X + tw + 10, by + 22, BAND_SUB_FONT, esc(sub)))
cy = by + BAND_TITLE_H
for row in chips:
cx = bx + BAND_PAD_X
for c in row:
cw = chip_w(c)
add('<rect x="%d" y="%.1f" width="%d" height="%d" rx="7" fill="#ffffff" '
'stroke="%s" stroke-opacity="0.55"/>' % (cx, cy, cw, CHIP_H, color))
add('<text x="%d" y="%.1f" font-size="%d" fill="#1e293b" text-anchor="middle">%s</text>'
% (cx + cw / 2, cy + CHIP_H / 2 + CHIP_FONT * 0.35, CHIP_FONT, esc(c)))
cx += cw + CHIP_GAP
cy += CHIP_H + CHIP_ROW_GAP
def build():
out = []
add = out.append
lw = (W - 2 * PAD - COL_GAP) // 2
rw = W - 2 * PAD - COL_GAP - lw
avail_l = lw - 2 * BAND_PAD_X
avail_r = rw - 2 * BAND_PAD_X
# 预排:每行高度 = 左右两框较高者
placed = []
y = TITLE_H
for (pt, ps, pc, ft, fc) in RINGS:
lrows = wrap_chips(pc, avail_l)
rrows = wrap_chips(fc, avail_r)
h = max(band_h(lrows), band_h(rrows))
placed.append((pt, ps, lrows, ft, rrows, y, h))
y += h + ROW_V_GAP
sum_y = y - ROW_V_GAP + 26
total_h = sum_y + SUM_H + 46
add('<rect x="0" y="0" width="%d" height="%d" fill="#ffffff"/>' % (W, total_h))
add('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="#0f172a">%s</text>'
% (PAD, 40, TITLE_FONT, esc("为什么网络「一直不稳」——七个误会与逐个拆掉(自愈架构复盘)")))
add('<text x="%d" y="%d" font-size="%d" fill="#64748b">%s</text>'
% (PAD, 62, SUB_FONT,
esc("左列 = 根因(比喻 + 机制);右列 = 现状机制的对应拆法;环与环叠加才构成「玄学」")))
for (pt, ps, lrows, ft, rrows, by, h) in placed:
draw_band(out, pt, ps, lrows, PAD, by, lw, h, C_PROB, C_PROB_BG)
draw_band(out, ft, "", rrows, PAD + lw + COL_GAP, by, rw, h, C_FIX, C_FIX_BG)
# 水平映射箭头(问题 → 拆法)
ay = by + h / 2
x0 = PAD + lw + 6
x1 = PAD + lw + COL_GAP - 10
add('<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="%s" stroke-width="2.5"/>'
% (x0, ay, x1, ay, C_GRAY))
add('<path d="M %d %.1f l -9 -6 v 12 z" fill="%s"/>' % (x1, ay, C_GRAY))
add('<text x="%d" y="%.1f" font-size="11" fill="#64748b" text-anchor="middle">%s</text>'
% (PAD + lw + COL_GAP / 2, ay - 8, esc("拆掉")))
# 左列环间竖向箭头(叠加累积)
for i in range(len(placed) - 1):
y0 = placed[i][5] + placed[i][6] + 5
y1 = placed[i + 1][5] - 7
ax = PAD + 60
add('<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="%s" stroke-width="1.6" '
'stroke-dasharray="4 3" stroke-opacity="0.7"/>' % (ax, y0, ax, y1, C_PROB))
add('<path d="M %d %.1f l -4.5 -7 h 9 z" fill="%s" fill-opacity="0.7"/>' % (ax, y1, C_PROB))
add('<text x="%d" y="%.1f" font-size="11" fill="%s" transform="rotate(-90 %d %.1f)">%s</text>'
% (PAD - 14, placed[3][5] + placed[3][6] / 2, C_PROB, PAD - 14,
placed[3][5] + placed[3][6] / 2, esc("叠加累积 → 越扫越脏")))
# 底部总结盒
add('<rect x="%d" y="%d" width="%d" height="%d" rx="10" fill="%s" stroke="%s" '
'stroke-opacity="0.5"/>' % (PAD, sum_y, lw, SUM_H, C_PROB_BG, C_PROB))
add('<text x="%d" y="%d" font-size="15" font-weight="700" fill="%s">%s</text>'
% (PAD + BAND_PAD_X, sum_y + 28, C_PROB, esc(SUM_LEFT[0])))
add('<text x="%d" y="%d" font-size="12" fill="#334155">%s</text>'
% (PAD + BAND_PAD_X, sum_y + 52, esc(SUM_LEFT[1])))
rx = PAD + lw + COL_GAP
add('<rect x="%d" y="%d" width="%d" height="%d" rx="10" fill="%s" stroke="%s" '
'stroke-opacity="0.5"/>' % (rx, sum_y, rw, SUM_H, C_FIX_BG, C_FIX))
add('<text x="%d" y="%d" font-size="15" font-weight="700" fill="%s">%s</text>'
% (rx + BAND_PAD_X, sum_y + 28, C_FIX, esc(SUM_RIGHT[0])))
add('<text x="%d" y="%d" font-size="12" fill="#334155">%s</text>'
% (rx + BAND_PAD_X, sum_y + 52, esc(SUM_RIGHT[1])))
add('<text x="%d" y="%d" font-size="12" fill="#94a3b8">%s</text>'
% (PAD, total_h - 16, 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)))
return svg, total_h
def main():
svg, h = build()
os.makedirs(os.path.dirname(OUT), exist_ok=True)
with open(OUT, "w", encoding="utf-8", newline="\n") as f:
f.write(svg)
import xml.etree.ElementTree as ET
ET.fromstring(svg)
print("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, h, OUT))
if __name__ == "__main__":
main()