Files
STM32F4-Base/tools/arch_diagram/gen_net_instability_svg.py
T
wandering1 d770e36c7c refactor(NET): 源码注释去阶段化 + 网络两份文档重构 + 不稳根因图更新
- 源码注释:删去自愈收敛阶段
1/1.5/3等 scaffolding 称谓,改为语义引用
  (E-02 收口 / F-NET-2/3/4 / docs/网络/网络自愈架构设计.md §5.2 E13);
  涉及 net_socket/.h、net_events、net_listener、net_pending、net_types、
  net_config、ch395f、time_sync、iec104_serv、ui_net_sink、lftpd、
  simulator/sim/net_socket_sim、board/host 测试与探针
- 文档重构:网络自愈架构设计.md 与 Net层阻塞模型架构设计.md 改为现状口径
  (删历史节与阶段叙事),NET/README.md、docs/README.md 同步
- 工具与配图:gen_net_instability_svg.py 与
  docs/images/网络不稳根因总览.svg 随源码口径一并更新
- 门禁:@build 0 错误 0 警告
2026-10-14 22:05:53 +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 丢弃式读一遍=拔全楼门铃线"],
"总闸唯一读者",
["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:唯一周期重建决策者(消费者即时重建三处保留,台架实测)"]),
("⑤ 传达员被按在椅子上", "队头阻塞",
["症状:FTP banner 迟到 25~30 s(E-02 实测),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(E-02 定案)",
"机制:芯片 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()