Files
STM32F4-Base/tools/arch_diagram/gen_net_instability_svg.py
T
wandering1 c7459b393f docs(N34): 网络不稳根因复盘总览(比喻版)+ 全景图 SVG 生成器
- 新增 网络不稳根因总览.md:七个误会(传达室比喻+机制+证据出处)→
  环↔拆法↔落点对照表 → E13 僵尸会话 Mermaid 时序图 → 一句话总结
- 新增 tools/arch_diagram/gen_net_instability_svg.py(draw-diagram 规范,
  纯标准库)与产物 docs/images/网络不稳根因总览.svg(headless Edge 目检两轮)
- N34 README:实施状态表补全阶段 1.5/2/3 与 E6 插桩行(原「阶段 2/3
  未开始」陈旧口径修正);挂复盘总览为第一入口;§5.56 改紧邻写法
- 门禁:check_doc_links --sections-strict 本批 4 文件通过
2026-10-13 17:12:36 +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 —— N34 复盘配图(纯标准库)。
布局:左列 = 七个误会(问题环,玫红),右列 = 对应拆法(绿),行间水平箭头;
左列环间竖向细箭头 = "叠加累积";底部两个总结盒。
为什么不用 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 22:丢中断",
["症状:UI 复连永久拒连、FTP 第 29 轮自己死掉,全程不报一个错",
"机制:快速复连时 INT# 门铃哑火,netTask 只靠门铃干活 → 状态静默滞留"],
"100ms 巡逻对账",
["单一 reconciler:判据读驱动状态缓存(E2 背书 18.1µs),分级去抖",
"门铃丢了不要紧,每 100ms 亲自数一遍人头"]),
("② 门铃总闸看一眼就清零", "Trap 25 B3",
["症状:别的 Socket 的 CONNECT/DISCONNECT 从此永久失踪",
"机制:全局中断寄存器整体读即清,open/relisten 丢弃式读一遍=拔全楼门铃线"],
"总闸唯一读者",
["F-NET-7:全局寄存器唯一读者=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:唯一周期重建决策者(消费者即时重建三处保留,P2a 实测)"]),
("⑤ 传达员被按在椅子上", "队头阻塞",
["症状:FTP banner 迟到 25~30 s(E-02 实测),netTask 频繁冻结半秒",
"机制:30 s 阻塞收 + 500 ms 盲等忙等,而 netTask 是全楼唯一传达员"],
"传达员永不离开传达室",
["忙等统一为 net_sock_reset_sync 有界排渣(600 ms 硬上限),阻塞消息化",
"阶段 1.5: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(E-02 定案)",
"机制:芯片 close 只发 FIN;对端内核存活但应用挂死 = 回 ACK 却不起身"],
"知道他不轰人,就别指望他轰人",
["定案写入 Trap 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 = ["逐环拆掉 = 稳(N34 阶段 0~3 已实施)",
"不依赖门铃 · 唯一主人 · 永不冻结 · 100ms 对账 · 开张前排渣——台架回归全绿"]
FOOT = ("证据链:docs/问题记录/CH395F_Trap_Records.md(Trap 07/09/14/17/22/23/25) · "
"docs/网络/N34-自愈审计/实验方案.md(E1~E13) · 修复落地见 docs/网络/N34-自愈审计/实施方案.md")
# ----------------------------- 工具函数 -----------------------------
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("为什么网络「一直不稳」——七个误会与逐个拆掉(N34 复盘)")))
add('<text x="%d" y="%d" font-size="%d" fill="#64748b">%s</text>'
% (PAD, 62, SUB_FONT,
esc("左列 = 根因(比喻 + 机制);右列 = N34 阶段 0~3 的对应拆法;环与环叠加才构成「玄学」")))
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()