Files
STM32F4-Base/tools/arch_diagram/gen_net_instability_svg.py
T
wandering1 a12367981d fix(N34): E14 丢中断复核——芯片不丢事件;listen 排事件挪至 LISTEN 前+全局读收口+全库归因更正
48 轮直读 SINT 矩阵(8 周期×6 变体)复核历史"丢中断"说法:芯片侧事件
丢失 = 0。归类:drain 读清消费 3/48(drain-int 探针现场抓获)、门控
竞态 2/48、主机同步 1/48、幽灵态 0/48 未复现。

根除性修复:net_listen_locked / net_listener_try 的排事件读清从
listen 后挪至 LISTEN 命令之前——新连接 CONNECT 落进该窗即被本方读清
吞掉,即"数据被吞"的生产侧自伤机制(台架 3/48 为同形态放大版)。

全局读收口:全局中断寄存器唯一读者 = net_poll(I7);生产/测试/台架
游离全局读全部删除,ch395f.h 声明处加护栏注释。

归因更正:17 文件约 40 处"丢中断"措辞改为"事件未收到/读清消费竞态"
(机制全部保留,仅归因更正);设计文档 §5.2 新增 E14 锚点;Trap
Records TRAP-NET-17/-20/B3 归因更正段;配图生成器与 SVG 重生成。

台架:事件未收到复核探针保留(drain-int / SINT 补读×3 / 超时归类),
下次跑矩阵自动归类;实验注册(main.c/uvprojx)已完全还原。

验证:@build 0 错误 0 警告;ci_local L0 32/0 + gitlint + L0.5
953/953;doc-links --all --sections-strict 通过;@test PASS(无
[ERR],net init 619ms);回归 :7003 300ms×10 = 10/10、:21 n30×50 =
0/50 失败。
2026-10-14 02:48:50 +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:唯一周期重建决策者(消费者即时重建三处保留,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-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 = ["逐环拆掉 = 稳(自愈收敛阶段 0~3 已实施)",
"不依赖门铃 · 唯一主人 · 永不冻结 · 100ms 对账 · 开张前排渣——台架回归全绿"]
FOOT = ("证据链:docs/问题记录/CH395F_Trap_Records.md(TRAP-NET-07/-09/-12/-17/-18/-20) · "
"架构现状 docs/网络/网络自愈架构设计.md §2~§4 · 实证锚点同文档 §5.2(E1~E13)")
# ----------------------------- 工具函数 -----------------------------
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("左列 = 根因(比喻 + 机制);右列 = 自愈收敛阶段 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()