merge: task/archsvg-arch-e2e-svg 合入 v2-on-ui

任务 archsvg(1 个提交)经 task-worktree 回收:
- docs(配图): 端到端链路鸟瞰进生成器体系,补大图碰撞审计判据

备注:把 archify 端到端架构视图转成仓内生成器配图
This commit is contained in:
2026-10-30 16:00:39 +08:00
7 changed files with 919 additions and 2 deletions
+40 -1
View File
@@ -73,6 +73,41 @@ whenToUse: 需要新增/修改 docs/ 文档的技术配图(协议帧、分层
5. 跑 `& '.venv\Scripts\python.exe' tools/arch_diagram/gen_<主题>_svg.py`,**把 OVERFLOW 告警打到 0 再进目检**——预检能在渲染前就报出 `foot: 974 > 968` 这类溢出(本窗实测两张图首跑各报一条脚注超宽),比靠眼睛反复量便宜一个数量级。 5. 跑 `& '.venv\Scripts\python.exe' tools/arch_diagram/gen_<主题>_svg.py`,**把 OVERFLOW 告警打到 0 再进目检**——预检能在渲染前就报出 `foot: 974 > 968` 这类溢出(本窗实测两张图首跑各报一条脚注超宽),比靠眼睛反复量便宜一个数量级。
6. 新脚本落库时**同批**把这一行加进 `tools/arch_diagram/README.md` 的「脚本一览」表。 6. 新脚本落库时**同批**把这一行加进 `tools/arch_diagram/README.md` 的「脚本一览」表。
## 三b、大图(几十框几十边)加一道碰撞审计,别只靠眼睛
边数一多,像素回读就查不全(本窗实测:42 框 37 边的端到端链路图,缩略 PNG 看着干净,
审计器一眼报出 23 条:边标签压节点、走线穿框、标签出画布)。做法是**解析自己生成的 SVG**
而不是解析渲染结果——`ET.parse` 产物,按 `font-size`/`fill` 分诊文字,估宽口径直接复用生成器的
`text_w()`(两边同一把尺子才不会假绿),然后判三件事:
1. **边标签的「文字盒」vs 节点框**(不是白片盒):交集 > 6 px² 就算问题——节点在边之后画,
压上去的**文字**会被节点白底吃掉半个字(本窗症状:「串行执行」渲染成「行执行」)。
白片本身压到框边角是**无害**的(白片先画、节点后画,只是盖住一点白底,不改字)。
除「有没有压上」,还要打印**文字与最近框的最小水平余量**并按 < 3px 报问题:
本窗「TRGO 触发」余量 0.7px、「帧 B 下发」1.5px,都没压上却随时会被真实字宽吃笔画,
缩短标签后升到 3.5px 以上才算安全。
2. **走线段 vs 节点框**(内缩 2px 再判,排除贴边端点):线穿框是硬伤,改边的出入框方位或独占通道。
3. **标签互压 / 出画布**:白片两两求交;`box[3] > H-2` 查脚注贴底。
4. **零余量复扫(生成器侧,别只跑 SVG 侧那一遍)**:SVG 反推不出哪两个框是本边端点,
走线只能内缩着查 → 1~3px 的切边照不到。用生成器自己的 `Box` / `route()` / `label_at()`
真值再扫一遍:框不膨胀不内缩、只跳本边两端。跑 SVG 侧与真值侧两遍都归零才算收工。
脚本骨架与判据见 `reference/svg-collision-audit.py`(可直接拷进 `ci-artifacts/` 当一次性工具用,
**别放 `tools/arch_diagram/`**——那里只放生成器,非 `gen_*.py` 的脚本混进去会让人以为在册)。
配套的两条几何纪律(都是本窗踩出来的):
- **同轴直连不要插退化折点**:同行两框若按「同轴 → 加一段 gx 走廊」的通用规则生成,会产出
`[(195,176),(219,176),(219,176),(243,176)]` 这种点集——线还是直的,但**标签落点算成第一半的中点**,
整格偏移半个通道 → 压框。正解:出框轴与入框轴同轴且已对齐时直接返回两点线。
- **标签落点用弧长中点,不用「最长段中点」**:多段折线里最长段可能有多条(对称 Z 字),
取第一条会把标签推到拐角上;沿总弧长取 50% 再定位,配合 `seg`/`t` 旋钮微调。
**先验审计器本身,再信它的「0 条」**:本窗把 `hi > y0` 复制粘贴成 `hi > x0`,零余量那一遍
凭空报出 3 条「穿框」,差点为此重排三条边的走线。判据脚本的假阳性比漏报更贵——它会让你
去改一张本来没问题的图。规矩:写完后挑一条**已知干净**的边(例如两框正对的直连边)单独跑一遍,
确认它不报,再跑全量。
## 三、目检(Windows:本机没有任何现成 SVG 渲染器) ## 三、目检(Windows:本机没有任何现成 SVG 渲染器)
**先探测,别照抄 cairosvg 命令**:本仓 `.venv` 无 `cairosvg`(`ModuleNotFoundError: No module named 'cairosvg'`),`rsvg-convert` 不存在,只有 Edge / Chrome: **先探测,别照抄 cairosvg 命令**:本仓 `.venv` 无 `cairosvg`(`ModuleNotFoundError: No module named 'cairosvg'`),`rsvg-convert` 不存在,只有 Edge / Chrome:
@@ -89,7 +124,9 @@ Test-Path "C:\Program Files\Google\Chrome\Application\chrome.exe"
Test-Path "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" Test-Path "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
``` ```
截图(**`--screenshot=` 必须给绝对 Windows 路径 + 绝对 `file:///` 输入**): 截图(**`--screenshot=` 必须给绝对 Windows 路径 + 绝对 `file:///` 输入**,并且**固定用
`--headless=new`**:本窗用旧 `--headless` 同一套命令时成时不成——一次写出了 168 KB,
下一次静默不写文件;也别把 stderr 丢进 `/dev/null`,成功标志就是它自己打印的那行字数):
```bash ```bash
mkdir -p ci-artifacts # Chrome 没有建目录的参数,先自己建 mkdir -p ci-artifacts # Chrome 没有建目录的参数,先自己建
@@ -152,6 +189,7 @@ sha1sum docs/images/<主题>.svg
- **照着现文 / `docs/架构设计/图形绘制流程.md` §四 跑 `pip install cairosvg` 或 `rsvg-convert`**:这台机器上没有(前者 `ModuleNotFoundError`、后者根本没有这个命令)→ 先按 §三探测,改用 headless Chrome/Edge。**别在没渲染出 PNG 的情况下声称"已目检"。** - **照着现文 / `docs/架构设计/图形绘制流程.md` §四 跑 `pip install cairosvg` 或 `rsvg-convert`**:这台机器上没有(前者 `ModuleNotFoundError`、后者根本没有这个命令)→ 先按 §三探测,改用 headless Chrome/Edge。**别在没渲染出 PNG 的情况下声称"已目检"。**
- **`--screenshot=ci-artifacts/ladder.png`(相对路径)** → `EXIT 2` + `Failed to write file ci-artifacts/ladder.png: 系统找不到指定的路径。 (0x3)`,PNG 压根没落盘;换绝对 Windows 路径后写出 95549 bytes。这条报错**不区分**"相对路径"与"目录不存在"两种成因(实测 `mkdir -p ci-artifacts` 已先跑过、仍报同一句),排查时两条都排。 - **`--screenshot=ci-artifacts/ladder.png`(相对路径)** → `EXIT 2` + `Failed to write file ci-artifacts/ladder.png: 系统找不到指定的路径。 (0x3)`,PNG 压根没落盘;换绝对 Windows 路径后写出 95549 bytes。这条报错**不区分**"相对路径"与"目录不存在"两种成因(实测 `mkdir -p ci-artifacts` 已先跑过、仍报同一句),排查时两条都排。
- **只做预检 + 缩略目检就收工(大图)**:`check_fit` 归零、PNG 看着干净,仍可能有二十来条标签压框——节点白底后画会吃掉压上去的标签;大图必须走 §三b 的解析式审计。
- **只做预检就收工**:`check_fit` / `OVERFLOW` 归零也不代表干净——本窗实测"门标签压第 5 级盒、蓝底标签挤、多余虚线"三类碰撞只有 PNG 回读看得见;反过来只目检不预检,脚注溢出就得靠眼睛反复量。**两者都要 0**。 - **只做预检就收工**:`check_fit` / `OVERFLOW` 归零也不代表干净——本窗实测"门标签压第 5 级盒、蓝底标签挤、多余虚线"三类碰撞只有 PNG 回读看得见;反过来只目检不预检,脚注溢出就得靠眼睛反复量。**两者都要 0**。
- **手改 SVG 成品**:等于制造「不是我改的」diff,且 `generated-fresh` 立刻 DIFF;也**不要退化成 ASCII 框图**(AGENTS.md 明令勿手绘)。一切修改都落在生成器常量上再重跑。 - **手改 SVG 成品**:等于制造「不是我改的」diff,且 `generated-fresh` 立刻 DIFF;也**不要退化成 ASCII 框图**(AGENTS.md 明令勿手绘)。一切修改都落在生成器常量上再重跑。
- **批量改生成器时 `old_string` 不匹配**:一批 `edit` 会在中途抛错,但**前面的编辑已经落盘** → 半成品生成器(本窗实测:`emit` helper 缺失却仍打印 OK,几轮之后才查出)。正解:改前先 `read` 出精确原文,逐条改,末尾统一复验(`grep -n "def emit"`、`grep -c print`、再重跑看 rc 与 OK 行)。 - **批量改生成器时 `old_string` 不匹配**:一批 `edit` 会在中途抛错,但**前面的编辑已经落盘** → 半成品生成器(本窗实测:`emit` helper 缺失却仍打印 OK,几轮之后才查出)。正解:改前先 `read` 出精确原文,逐条改,末尾统一复验(`grep -n "def emit"`、`grep -c print`、再重跑看 rc 与 OK 行)。
@@ -163,6 +201,7 @@ sha1sum docs/images/<主题>.svg
- [ ] 生成器输出 `OK … bytes, canvas WxH`,**无** `OVERFLOW WARN` / `!! N overflow warnings`;`grep -n "def emit"` 命中、`grep -c print` = 0。 - [ ] 生成器输出 `OK … bytes, canvas WxH`,**无** `OVERFLOW WARN` / `!! N overflow warnings`;`grep -n "def emit"` 命中、`grep -c print` = 0。
- [ ] 绝对路径 PNG 存在、尺寸 = 画布,`read_image` 回读确认:无文字压框 / 溢出、无标签互撞、无 `…` 截断收尾、配色在调色板内。 - [ ] 绝对路径 PNG 存在、尺寸 = 画布,`read_image` 回读确认:无文字压框 / 溢出、无标签互撞、无 `…` 截断收尾、配色在调色板内。
- [ ] 连跑两次产物 sha1 不变;`scripts/check_generated_fresh.py` rc=0(无 DIFF / NEW,`[BASELINE]` 未增加)。 - [ ] 连跑两次产物 sha1 不变;`scripts/check_generated_fresh.py` rc=0(无 DIFF / NEW,`[BASELINE]` 未增加)。
- [ ] 大图(≥20 边):跑一遍 §三b 的碰撞审计,报数归零(标签压框 / 走线穿框 / 标签出画布)。
- [ ] 引用它的 `.md` 相对路径正确(根目录 `images/`,子目录 `../images/`);`tools/arch_diagram/README.md` 已加脚本行。 - [ ] 引用它的 `.md` 相对路径正确(根目录 `images/`,子目录 `../images/`);`tools/arch_diagram/README.md` 已加脚本行。
- [ ] 脚本 + 产物 `.svg` + `.md` + README 行一并暂存;`pre-commit run --files …` 全过(无乱码 / 行尾空白 / 编码 OK)。 - [ ] 脚本 + 产物 `.svg` + `.md` + README 行一并暂存;`pre-commit run --files …` 全过(无乱码 / 行尾空白 / 编码 OK)。
@@ -0,0 +1,154 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""一次性碰撞审计(拷进 `ci-artifacts/` 用,**不要放进 `tools/arch_diagram/`**):
解析自己生成的 SVG,报「边标签压节点 / 走线穿框 / 标签互压 / 文字出画布 / 节点文字出框 /
标签余量过窄」,并打印最紧的三条「标签文字 ↔ 最近框」水平余量。
为什么需要它:`check_fit` 预检只量**框内**文字;几十框几十边的大图,
标签压框与走线穿框在缩略 PNG 里根本看不全(实测 42 框 37 边报出 23 条),
而像素回读又慢又主观。解析产物是确定性的。
依赖:待审图对应的生成器模块必须能被 import(脚本靠它复用同一把估宽尺子 `text_w()`
与几何常量 `CW` / `CH` / `EDGE_FONT` / `ECOL`,两边同尺子才不会假绿)。
生成器侧要求:边标签白片用 `fill-opacity="0.94"`、节点框用 `fill="#ffffff"` 且
`width==CW`、`height==CH`(本脚本靠这两条分诊);文字用 `<text font-size fill>`。
**这一遍照不到的东西**(别把它当唯一判据):
- 走线检查必须把框**内缩**几像素,否则合法的「线接在框边上」会全报成穿框——从 SVG
反推不出哪两个框是本边的端点。代价:1~3px 的切边也照不到。要零余量就在**生成器侧**
再用 `Box` / `route()` / `label_at()` 的真值扫一遍(线段 vs 不膨胀的框,只跳本边两端)。
- 白片比通道宽一点、被节点白底盖住边角:无害(白片先画、节点后画),不用改;
要盯的是**文字**的余量,上面那三条数就是。
- 自写的判据脚本先拿一条**已知干净**的边验它不报,再信它的「0 条」:实测一条
`hi > x0` 写成 `hi > y0` 的复制粘贴错,凭空报出 3 条「穿框」,差点为此重排走线。
用法(仓库根):
python ci-artifacts/label_audit.py <生成器模块名> [svg 路径]
# 例:python ci-artifacts/label_audit.py gen_arch_endtoend_svg
# python ci-artifacts/label_audit.py gen_wave_record_flow_svg docs/images/故障录波数据流.svg
退出码:0 = 无问题,1 = 有问题(打印明细)。
别的生成器若没有同名常量(多数用 `CHIP_FONT` / 自己的线色变量),改脚本顶部 `EDGE_COLORS` 与 `edge_labels` 那两处筛选即可——估宽必须继续用该生成器自己的 `text_w()`,别换一把尺子。
"""
import sys
import xml.etree.ElementTree as ET
sys.path.insert(0, "tools/arch_diagram")
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
mod_name = sys.argv[1]
G = __import__(mod_name)
svg_path = sys.argv[2] if len(sys.argv) > 2 else G.OUT
MASK_OP = "0.94" # 边标签白片的 fill-opacity(生成器侧约定)
EDGE_COLORS = set(G.ECOL.values()) # 边标签颜色(生成器侧的线色表)
def box_of_text(el):
"""<text> → (x0, y0, x1, y1, 文本, 字号, 颜色)。baseline y 之上约 0.85em、之下 0.22em。"""
x, y = float(el.get("x")), float(el.get("y"))
fs = float(el.get("font-size"))
s = "".join(el.itertext())
w = G.text_w(s, fs)
a = el.get("text-anchor", "start")
x0 = x if a == "start" else (x - w / 2 if a == "middle" else x - w)
return (x0, y - fs * 0.85, x0 + w, y + fs * 0.22, s, fs, el.get("fill"))
def area(a, b):
ix = min(a[2], b[2]) - max(a[0], b[0])
iy = min(a[3], b[3]) - max(a[1], b[1])
return ix * iy if (ix > 0.5 and iy > 0.5) else 0.0
def segs_of(d):
n = [float(v) for v in d.replace("M", " ").replace("L", " ").replace(",", " ").split()]
pts = list(zip(n[0::2], n[1::2]))
return list(zip(pts, pts[1:]))
def seg_hits_rect(sg, r, m=2.0):
"""轴对齐线段 vs 矩形(内缩 m 排除贴边的合法端点)。斜段一律算可疑。"""
(x0, y0), (x1, y1) = sg
rx0, ry0, rx1, ry1 = r[0] + m, r[1] + m, r[2] - m, r[3] - m
if abs(y0 - y1) < 0.5:
return ry0 < y0 < ry1 and min(x0, x1) < rx1 - m and max(x0, x1) > rx0 + m
if abs(x0 - x1) < 0.5:
return rx0 < x0 < rx1 and min(y0, y1) < ry1 - m and max(y0, y1) > ry0 + m
return True
root = ET.parse(svg_path).getroot()
W, H = float(root.get("width")), float(root.get("height"))
texts, nodes, masks, paths = [], [], [], []
for el in root.iter():
tag = el.tag.split("}")[-1] # 去命名空间,否则一个都匹配不上
if tag == "text":
texts.append(box_of_text(el))
elif tag == "rect":
w, h = float(el.get("width")), float(el.get("height"))
x, y = float(el.get("x")), float(el.get("y"))
if el.get("fill") == "#ffffff" and abs(w - G.CW) < 0.6 and abs(h - G.CH) < 0.6:
nodes.append((x, y, x + w, y + h, "node"))
elif el.get("fill") == "#ffffff" and el.get("fill-opacity") == MASK_OP:
masks.append((x, y, x + w, y + h, "mask"))
elif tag == "path" and el.get("fill") == "none":
paths.append(segs_of(el.get("d")))
edge_labels = [t for t in texts if t[6] in EDGE_COLORS and t[5] == G.EDGE_FONT]
bad = []
for lb in edge_labels:
b = lb[:4]
for n in nodes:
a = area(b, n)
if a > 6: # 压到框边 6 px² 以上就会被节点吃掉半个字
bad.append("标签压节点 %-12s 压 node@(%.0f,%.0f) %.0f px²" % (lb[4][:12], n[0], n[1], a))
for ob in edge_labels:
if ob is not lb and area(b, ob[:4]) > 6:
bad.append("标签互压 %s / %s" % (lb[4][:10], ob[4][:10]))
if b[0] < 2 or b[2] > W - 2 or b[3] > H - 2 or b[1] < 2:
bad.append("标签出画布 %s box=(%.0f,%.0f,%.0f,%.0f) 画布 %.0fx%.0f"
% (lb[4][:12], b[0], b[1], b[2], b[3], W, H))
for segs in paths:
for sg in segs:
for n in nodes:
if seg_hits_rect(sg, n):
bad.append("走线穿框 (%d,%d)-(%d,%d) 压 node@(%.0f,%.0f)"
% (sg[0][0], sg[0][1], sg[1][0], sg[1][1], n[0], n[1]))
break
# 「节点文字出框」只能按几何判:文字中心落在某个节点框里,却没被该框完整包住。
# 别拿 font-size 当筛选——本仓惯例里副题字号 = 边标签字号 = 脚注字号(9.5),按字号筛会全灭。
for t in texts:
cx, cy = (t[0] + t[2]) / 2, (t[1] + t[3]) / 2
host = [n for n in nodes if n[0] <= cx <= n[2] and n[1] <= cy <= n[3]]
for n in host:
if not (t[0] >= n[0] - 1 and t[2] <= n[2] + 1 and t[1] >= n[1] - 1 and t[3] <= n[3] + 1):
bad.append("节点文字出框 %r 超 node@(%.0f,%.0f)" % (t[4][:16], n[0], n[1]))
break
# 余量比「有没有压上」更早暴露问题:本仓约定边标签整段落在框间通道里,
# 只要文字离最近框 < 3px,字宽估算稍偏(本脚本与生成器共用同一把尺子,
# 但真实字体度量和它有出入)就会被后画的节点吃掉笔画。
MIN_CLEAR = 3.0
clears = []
for lb in edge_labels:
b = lb[:4]
worst, wid = None, None
for n in nodes:
if min(b[3], n[3]) - max(b[1], n[1]) <= 0: # 竖直方向不相干
continue
c = max(n[0] - b[2], b[0] - n[2]) # 正=间距,负=压进去多深
if worst is None or c < worst:
worst, wid = c, (n[0], n[1])
if worst is not None:
clears.append((worst, lb[4][:12], wid))
if worst < MIN_CLEAR:
bad.append("标签余量过窄 %r 距 node@%s 仅 %.1fpx(< %.1f)"
% (lb[4][:12], wid, worst, MIN_CLEAR))
seen = []
for b in bad:
if b not in seen:
seen.append(b)
print("%s:边标签 %d / 节点 %d / 文字 %d / 路径 %d"
% (svg_path, len(edge_labels), len(nodes), len(texts), len(paths)))
for c in sorted(clears)[:3]:
print(" 最紧余量 %.1fpx 标签 %r 邻 node@%s" % (c[0], c[1], c[2]))
for b in seen:
print(" ! " + b)
print("合计问题 %d 条" % len(seen))
sys.exit(1 if seen else 0)
@@ -0,0 +1,284 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1192" height="1042" viewBox="0 0 1192 1042" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif">
<defs><marker id="ar-d" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#334155"/></marker><marker id="ar-c" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#64748b"/></marker><marker id="ar-e" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#7c3aed"/></marker></defs>
<rect x="0" y="0" width="1192" height="1042" fill="#ffffff"/>
<text x="34.0" y="34.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="20" fill="#0f172a" text-anchor="start" font-weight="bold">STM32F407ZGTx 工业 DTU 平台 · 端到端链路总览</text>
<text x="34.0" y="55.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="12.5" fill="#475569" text-anchor="start" font-weight="normal">一个电气量从引脚到 104 报文 / 显存 / 记录文件穿过的框,与「谁在什么时候被谁唤醒」;分层归属与层内模块清单见 项目架构总览.svg,本图只画链路与握手</text>
<rect x="34.0" y="104.0" width="174.0" height="874.0" rx="10" fill="#f1f5f9" stroke="#475569" stroke-width="1.1" fill-opacity="0.50"/>
<text x="44.0" y="123.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="13.5" fill="#475569" text-anchor="start" font-weight="bold">片外硬件</text>
<text x="44.0" y="137.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="10.5" fill="#475569" text-anchor="start" font-weight="normal" fill-opacity="0.85">板级器件 · 不走软件分层</text>
<rect x="230.0" y="104.0" width="174.0" height="874.0" rx="10" fill="#fffbeb" stroke="#d97706" stroke-width="1.1" fill-opacity="0.50"/>
<text x="240.0" y="123.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="13.5" fill="#d97706" text-anchor="start" font-weight="bold">驱动层 Drivers/BSP</text>
<text x="240.0" y="137.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="10.5" fill="#d97706" text-anchor="start" font-weight="normal" fill-opacity="0.85">手写芯片驱动与总线</text>
<rect x="426.0" y="104.0" width="174.0" height="874.0" rx="10" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.1" fill-opacity="0.50"/>
<text x="436.0" y="123.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="13.5" fill="#16a34a" text-anchor="start" font-weight="bold">中间件与算法</text>
<text x="436.0" y="137.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="10.5" fill="#16a34a" text-anchor="start" font-weight="normal" fill-opacity="0.85">取数 → 相量 → 快照</text>
<rect x="622.0" y="104.0" width="174.0" height="874.0" rx="10" fill="#f0fdfa" stroke="#0d9488" stroke-width="1.1" fill-opacity="0.50"/>
<text x="632.0" y="123.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="13.5" fill="#0d9488" text-anchor="start" font-weight="bold">任务编排与握手</text>
<text x="632.0" y="137.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="10.5" fill="#0d9488" text-anchor="start" font-weight="normal" fill-opacity="0.85">App/task · 就绪屏障</text>
<rect x="818.0" y="104.0" width="174.0" height="874.0" rx="10" fill="#fff1f2" stroke="#e11d48" stroke-width="1.1" fill-opacity="0.50"/>
<text x="828.0" y="123.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="13.5" fill="#e11d48" text-anchor="start" font-weight="bold">平台服务与数据落点</text>
<text x="828.0" y="137.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="10.5" fill="#e11d48" text-anchor="start" font-weight="normal" fill-opacity="0.85">记录 / 配置 / 网络门面</text>
<path d="M195.0 176.0 L243.0 176.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="200.5" y="168.8" width="37.0" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="219.0" y="179.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">8 路帧</text>
<path d="M243.0 268.0 L195.0 268.0" fill="none" stroke="#64748b" stroke-width="1.25" stroke-dasharray="5 3" marker-end="url(#ar-c)"/>
<rect x="204.5" y="260.8" width="29.0" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="219.0" y="271.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#64748b" text-anchor="middle">TRGO</text>
<path d="M391.0 176.0 L439.0 176.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="392.5" y="168.8" width="45.0" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="415.0" y="179.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">原子快照</text>
<path d="M349.6 239.0 L349.6 222.0 L480.4 222.0 L480.4 205.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="386.2" y="214.8" width="57.5" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="415.0" y="225.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">DMA 双缓冲</text>
<path d="M513.0 205.0 L513.0 239.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="490.5" y="214.8" width="45.0" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="513.0" y="225.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">语义通道</text>
<path d="M513.0 297.0 L513.0 331.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="500.0" y="306.8" width="26.0" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="513.0" y="317.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">相量</text>
<path d="M709.0 205.0 L709.0 239.0" fill="none" stroke="#64748b" stroke-width="1.25" stroke-dasharray="5 3" marker-end="url(#ar-c)"/>
<rect x="667.7" y="214.8" width="82.6" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="709.0" y="225.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#64748b" text-anchor="middle">1 kHz → 20 ms</text>
<path d="M483.4 389.0 L483.4 413.0 L709.0 413.0 L709.0 423.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="566.7" y="405.8" width="45.0" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="589.2" y="416.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">同一上游</text>
<path d="M685.3 331.0 L685.3 297.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="672.3" y="306.8" width="26.0" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="685.3" y="317.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">定值</text>
<path d="M735.6 423.0 L735.6 389.0" fill="none" stroke="#64748b" stroke-width="1.25" stroke-dasharray="5 3" marker-end="url(#ar-c)"/>
<rect x="714.4" y="398.8" width="42.5" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="735.6" y="409.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#64748b" text-anchor="middle">YK 请求</text>
<path d="M783.0 452.0 L831.0 452.0" fill="none" stroke="#64748b" stroke-width="1.25" stroke-dasharray="5 3" marker-end="url(#ar-c)"/>
<rect x="794.0" y="444.8" width="26.0" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="807.0" y="455.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#64748b" text-anchor="middle">脏段</text>
<path d="M783.0 268.0 L831.0 268.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="784.5" y="260.8" width="45.0" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="807.0" y="271.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">边沿投递</text>
<path d="M783.0 636.0 L831.0 636.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="784.5" y="628.8" width="45.0" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="807.0" y="639.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">唯一写者</text>
<path d="M762.3 665.0 L762.3 682.0 L869.5 682.0 L869.5 699.0" fill="none" stroke="#64748b" stroke-width="1.25" stroke-dasharray="5 3" marker-end="url(#ar-c)"/>
<rect x="791.9" y="674.8" width="48.0" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="815.9" y="685.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#64748b" text-anchor="middle">try 语义</text>
<path d="M195.0 544.0 L243.0 544.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="195.0" y="536.8" width="48.0" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="219.0" y="547.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">SPI 事务</text>
<path d="M391.0 544.0 L439.0 544.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="392.5" y="536.8" width="45.0" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="415.0" y="547.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">集中读清</text>
<path d="M587.0 544.0 L635.0 544.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="588.5" y="536.8" width="45.0" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="611.0" y="547.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">串行执行</text>
<path d="M783.0 544.0 L831.0 544.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="789.2" y="536.8" width="35.5" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="807.0" y="547.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">单连接</text>
<path d="M979.0 530.1 L1115.8 530.1 L1115.8 481.0" fill="none" stroke="#64748b" stroke-width="1.25" stroke-dasharray="5 3" marker-end="url(#ar-c)"/>
<rect x="1047.9" y="522.8" width="48.0" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="1071.9" y="533.5" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#64748b" text-anchor="middle">TCP 对端</text>
<path d="M195.0 636.0 L243.0 636.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="201.2" y="628.8" width="35.5" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="219.0" y="639.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">页读写</text>
<path d="M391.0 636.0 L439.0 636.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="397.2" y="628.8" width="35.5" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="415.0" y="639.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">逻辑块</text>
<path d="M587.0 636.0 L635.0 636.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="598.0" y="628.8" width="26.0" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="611.0" y="639.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">挂载</text>
<path d="M195.0 728.0 L243.0 728.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="204.5" y="720.8" width="29.0" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="219.0" y="731.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">I2C1</text>
<path d="M391.0 728.0 L439.0 728.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="402.0" y="720.8" width="26.0" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="415.0" y="731.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">墙钟</text>
<path d="M635.0 728.0 L587.0 728.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="588.5" y="720.8" width="45.0" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="611.0" y="731.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">收包对时</text>
<path d="M439.0 820.0 L391.0 820.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="401.2" y="812.8" width="27.5" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="415.0" y="823.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">帧 B</text>
<path d="M635.0 820.0 L587.0 820.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="598.0" y="812.8" width="26.0" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="611.0" y="823.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">灌数</text>
<path d="M783.0 820.0 L831.0 820.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="784.5" y="812.8" width="45.0" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="807.0" y="823.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">同一显存</text>
<path d="M243.0 820.0 L195.0 820.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="199.0" y="812.8" width="40.1" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="219.0" y="823.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">700000</text>
<path d="M673.5 849.0 L673.5 861.0 L486.4 861.0 L486.4 883.0" fill="none" stroke="#64748b" stroke-width="1.25" stroke-dasharray="5 3" marker-end="url(#ar-c)"/>
<rect x="537.9" y="853.8" width="74.1" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="574.9" y="864.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#64748b" text-anchor="middle">100 ms 喂心跳</text>
<path d="M726.8 849.0 L726.8 866.0 L709.0 866.0 L709.0 883.0" fill="none" stroke="#64748b" stroke-width="1.25" stroke-dasharray="5 3" marker-end="url(#ar-c)"/>
<rect x="700.1" y="858.8" width="35.5" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="717.9" y="869.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#64748b" text-anchor="middle">等就绪</text>
<path d="M783.0 912.0 L831.0 912.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="784.2" y="904.8" width="45.6" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="807.0" y="915.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">WaitAll</text>
<path d="M979.0 820.0 L1027.0 820.0" fill="none" stroke="#7c3aed" stroke-width="1.25" stroke-dasharray="1.5 3" marker-end="url(#ar-e)"/>
<rect x="980.5" y="812.8" width="45.0" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="1003.0" y="823.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#7c3aed" text-anchor="middle">同源像素</text>
<path d="M560.4 389.0 L560.4 400.0 L1118.8 400.0 L1118.8 389.0" fill="none" stroke="#7c3aed" stroke-width="1.25" stroke-dasharray="1.5 3" marker-end="url(#ar-e)"/>
<rect x="812.3" y="392.8" width="54.5" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="839.6" y="403.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#7c3aed" text-anchor="middle">纯逻辑直跑</text>
<path d="M709.0 941.0 L709.0 954.0 L317.0 954.0 L317.0 941.0" fill="none" stroke="#7c3aed" stroke-width="1.25" stroke-dasharray="1.5 3" marker-end="url(#ar-e)"/>
<rect x="490.5" y="946.8" width="45.0" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="513.0" y="957.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#7c3aed" text-anchor="middle">唤醒发送</text>
<path d="M979.0 464.8 L1008.8 464.8 L1008.8 636.0 L979.0 636.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="981.5" y="543.1" width="54.5" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="1008.8" y="553.8" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">唯一落盘者</text>
<path d="M88.4 883.0 L88.4 861.0 L284.4 861.0 L284.4 849.0" fill="none" stroke="#334155" stroke-width="1.35" marker-end="url(#ar-d)"/>
<rect x="158.9" y="853.8" width="45.0" height="14.5" rx="3" fill="#ffffff" fill-opacity="0.94"/><text x="181.4" y="864.4" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="middle">回调宿主</text>
<rect x="47.0" y="147.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#475569" stroke-width="1.3"/>
<rect x="47.0" y="147.0" width="3.5" height="58.0" rx="1.75" fill="#475569" stroke="#475569" stroke-width="0"/>
<text x="58.0" y="171.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">TPAFE5160 前端</text>
<text x="58.0" y="187.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">并行总线 · 8 路</text>
<rect x="47.0" y="239.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#475569" stroke-width="1.3"/>
<rect x="47.0" y="239.0" width="3.5" height="58.0" rx="1.75" fill="#475569" stroke="#475569" stroke-width="0"/>
<text x="58.0" y="263.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">片内 ADC1 + DMA</text>
<text x="58.0" y="279.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">TIM3 TRGO 乒乓</text>
<rect x="47.0" y="515.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#475569" stroke-width="1.3"/>
<rect x="47.0" y="515.0" width="3.5" height="58.0" rx="1.75" fill="#475569" stroke="#475569" stroke-width="0"/>
<text x="58.0" y="539.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">CH395F 以太网</text>
<text x="58.0" y="555.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">SPI 串行化</text>
<rect x="47.0" y="607.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#475569" stroke-width="1.3"/>
<rect x="47.0" y="607.0" width="3.5" height="58.0" rx="1.75" fill="#475569" stroke="#475569" stroke-width="0"/>
<text x="58.0" y="631.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">GD5F SPI NAND</text>
<text x="58.0" y="647.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">2 KB 页 · ECC</text>
<rect x="47.0" y="699.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#475569" stroke-width="1.3"/>
<rect x="47.0" y="699.0" width="3.5" height="58.0" rx="1.75" fill="#475569" stroke="#475569" stroke-width="0"/>
<text x="58.0" y="723.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">SD2506 RTC</text>
<text x="58.0" y="739.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">I2C1 硬件亚秒</text>
<rect x="47.0" y="791.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#475569" stroke-width="1.3"/>
<rect x="47.0" y="791.0" width="3.5" height="58.0" rx="1.75" fill="#475569" stroke="#475569" stroke-width="0"/>
<text x="58.0" y="815.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">屏板 160×160</text>
<text x="58.0" y="831.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">USART1 700000</text>
<rect x="47.0" y="883.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#475569" stroke-width="1.3"/>
<rect x="47.0" y="883.0" width="3.5" height="58.0" rx="1.75" fill="#475569" stroke="#475569" stroke-width="0"/>
<text x="58.0" y="907.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">RS-485 总线</text>
<text x="58.0" y="923.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">UART4 · 未接线(暂缓)</text>
<rect x="243.0" y="147.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#d97706" stroke-width="1.3"/>
<rect x="243.0" y="147.0" width="3.5" height="58.0" rx="1.75" fill="#d97706" stroke="#d97706" stroke-width="0"/>
<text x="254.0" y="171.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">TPAFE 驱动</text>
<text x="254.0" y="187.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">原子快照 snapshot</text>
<rect x="243.0" y="239.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#d97706" stroke-width="1.3"/>
<rect x="243.0" y="239.0" width="3.5" height="58.0" rx="1.75" fill="#d97706" stroke="#d97706" stroke-width="0"/>
<text x="254.0" y="263.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">ADC · TIM3 · DMA</text>
<text x="254.0" y="279.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">手写上电自校准</text>
<rect x="243.0" y="515.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#d97706" stroke-width="1.3"/>
<rect x="243.0" y="515.0" width="3.5" height="58.0" rx="1.75" fill="#d97706" stroke="#d97706" stroke-width="0"/>
<text x="254.0" y="539.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">CH395F 驱动</text>
<text x="254.0" y="555.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">中断集中读清</text>
<rect x="243.0" y="607.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#d97706" stroke-width="1.3"/>
<rect x="243.0" y="607.0" width="3.5" height="58.0" rx="1.75" fill="#d97706" stroke="#d97706" stroke-width="0"/>
<text x="254.0" y="631.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">NAND 驱动 + FTL</text>
<text x="254.0" y="647.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">ECC · BBT</text>
<rect x="243.0" y="699.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#d97706" stroke-width="1.3"/>
<rect x="243.0" y="699.0" width="3.5" height="58.0" rx="1.75" fill="#d97706" stroke="#d97706" stroke-width="0"/>
<text x="254.0" y="723.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">RTC 驱动 + 墙钟</text>
<text x="254.0" y="739.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">sys_clock epoch</text>
<rect x="243.0" y="791.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#d97706" stroke-width="1.3"/>
<rect x="243.0" y="791.0" width="3.5" height="58.0" rx="1.75" fill="#d97706" stroke="#d97706" stroke-width="0"/>
<text x="254.0" y="815.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">屏板链路驱动</text>
<text x="254.0" y="831.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">借道 RS485</text>
<rect x="243.0" y="883.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#d97706" stroke-width="1.3"/>
<rect x="243.0" y="883.0" width="3.5" height="58.0" rx="1.75" fill="#d97706" stroke="#d97706" stroke-width="0"/>
<text x="254.0" y="907.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">控制台 UART5</text>
<text x="254.0" y="923.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">4 KB 环形</text>
<rect x="439.0" y="147.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#16a34a" stroke-width="1.3"/>
<rect x="439.0" y="147.0" width="3.5" height="58.0" rx="1.75" fill="#16a34a" stroke="#16a34a" stroke-width="0"/>
<text x="450.0" y="171.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">语义取数层</text>
<text x="450.0" y="187.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">有效性 · 标定</text>
<rect x="439.0" y="239.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#16a34a" stroke-width="1.3"/>
<rect x="439.0" y="239.0" width="3.5" height="58.0" rx="1.75" fill="#16a34a" stroke="#16a34a" stroke-width="0"/>
<text x="450.0" y="263.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">全周傅氏相量</text>
<text x="450.0" y="279.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">ring → phasor</text>
<rect x="439.0" y="331.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#16a34a" stroke-width="1.3"/>
<rect x="439.0" y="331.0" width="3.5" height="58.0" rx="1.75" fill="#16a34a" stroke="#16a34a" stroke-width="0"/>
<text x="450.0" y="355.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">遥测快照双区</text>
<text x="450.0" y="371.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">测频 200 ms</text>
<rect x="439.0" y="515.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#16a34a" stroke-width="1.3"/>
<rect x="439.0" y="515.0" width="3.5" height="58.0" rx="1.75" fill="#16a34a" stroke="#16a34a" stroke-width="0"/>
<text x="450.0" y="539.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">NET 中间件</text>
<text x="450.0" y="555.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">socket · 挂起</text>
<rect x="439.0" y="607.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#16a34a" stroke-width="1.3"/>
<rect x="439.0" y="607.0" width="3.5" height="58.0" rx="1.75" fill="#16a34a" stroke="#16a34a" stroke-width="0"/>
<text x="450.0" y="631.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">FTL + FatFs</text>
<text x="450.0" y="647.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">dhara · 8.3 短名</text>
<rect x="439.0" y="699.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#16a34a" stroke-width="1.3"/>
<rect x="439.0" y="699.0" width="3.5" height="58.0" rx="1.75" fill="#16a34a" stroke="#16a34a" stroke-width="0"/>
<text x="450.0" y="723.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">对时协议</text>
<text x="450.0" y="739.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">推送写 RTC</text>
<rect x="439.0" y="791.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#16a34a" stroke-width="1.3"/>
<rect x="439.0" y="791.0" width="3.5" height="58.0" rx="1.75" fill="#16a34a" stroke="#16a34a" stroke-width="0"/>
<text x="450.0" y="815.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">LVGL + ui_port</text>
<text x="450.0" y="831.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">160×160 1bpp</text>
<rect x="439.0" y="883.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#16a34a" stroke-width="1.3"/>
<rect x="439.0" y="883.0" width="3.5" height="58.0" rx="1.75" fill="#16a34a" stroke="#16a34a" stroke-width="0"/>
<text x="450.0" y="907.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">IWDG + Fault</text>
<text x="450.0" y="923.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">心跳掩码</text>
<rect x="635.0" y="147.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#0d9488" stroke-width="1.3"/>
<rect x="635.0" y="147.0" width="3.5" height="58.0" rx="1.75" fill="#0d9488" stroke="#0d9488" stroke-width="0"/>
<text x="646.0" y="171.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">adcTask</text>
<text x="646.0" y="187.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">1 kHz 消费者</text>
<rect x="635.0" y="239.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#0d9488" stroke-width="1.3"/>
<rect x="635.0" y="239.0" width="3.5" height="58.0" rx="1.75" fill="#0d9488" stroke="#0d9488" stroke-width="0"/>
<text x="646.0" y="263.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">protectTask</text>
<text x="646.0" y="279.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">20 ms 判据</text>
<rect x="635.0" y="331.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#0d9488" stroke-width="1.3"/>
<rect x="635.0" y="331.0" width="3.5" height="58.0" rx="1.75" fill="#0d9488" stroke="#0d9488" stroke-width="0"/>
<text x="646.0" y="355.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">定值层</text>
<text x="646.0" y="371.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">prot_set 校验</text>
<rect x="635.0" y="423.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#0d9488" stroke-width="1.3"/>
<rect x="635.0" y="423.0" width="3.5" height="58.0" rx="1.75" fill="#0d9488" stroke="#0d9488" stroke-width="0"/>
<text x="646.0" y="447.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">iec104Task</text>
<text x="646.0" y="463.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">100 ms · :2404</text>
<rect x="635.0" y="515.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#0d9488" stroke-width="1.3"/>
<rect x="635.0" y="515.0" width="3.5" height="58.0" rx="1.75" fill="#0d9488" stroke="#0d9488" stroke-width="0"/>
<text x="646.0" y="539.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">netTask</text>
<text x="646.0" y="555.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">MsgQueue 深 8</text>
<rect x="635.0" y="607.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#0d9488" stroke-width="1.3"/>
<rect x="635.0" y="607.0" width="3.5" height="58.0" rx="1.75" fill="#0d9488" stroke="#0d9488" stroke-width="0"/>
<text x="646.0" y="631.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">defaultTask</text>
<text x="646.0" y="647.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">记录唯一写者</text>
<rect x="635.0" y="699.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#0d9488" stroke-width="1.3"/>
<rect x="635.0" y="699.0" width="3.5" height="58.0" rx="1.75" fill="#0d9488" stroke="#0d9488" stroke-width="0"/>
<text x="646.0" y="723.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">timeSyncTask</text>
<text x="646.0" y="739.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">等 NET_READY</text>
<rect x="635.0" y="791.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#0d9488" stroke-width="1.3"/>
<rect x="635.0" y="791.0" width="3.5" height="58.0" rx="1.75" fill="#0d9488" stroke="#0d9488" stroke-width="0"/>
<text x="646.0" y="815.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">uiTask</text>
<text x="646.0" y="831.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">唯一写侧</text>
<rect x="635.0" y="883.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#0d9488" stroke-width="1.3"/>
<rect x="635.0" y="883.0" width="3.5" height="58.0" rx="1.75" fill="#0d9488" stroke="#0d9488" stroke-width="0"/>
<text x="646.0" y="907.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">就绪屏障</text>
<text x="646.0" y="923.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">位组 · NoClear</text>
<rect x="831.0" y="239.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#e11d48" stroke-width="1.3"/>
<rect x="831.0" y="239.0" width="3.5" height="58.0" rx="1.75" fill="#e11d48" stroke="#e11d48" stroke-width="0"/>
<text x="842.0" y="263.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">SOE 事件</text>
<text x="842.0" y="279.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">soe.log 定长</text>
<rect x="831.0" y="331.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#e11d48" stroke-width="1.3"/>
<rect x="831.0" y="331.0" width="3.5" height="58.0" rx="1.75" fill="#e11d48" stroke="#e11d48" stroke-width="0"/>
<text x="842.0" y="355.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">定值落盘</text>
<text x="842.0" y="371.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">/prot_set.bin</text>
<rect x="831.0" y="423.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#e11d48" stroke-width="1.3"/>
<rect x="831.0" y="423.0" width="3.5" height="58.0" rx="1.75" fill="#e11d48" stroke="#e11d48" stroke-width="0"/>
<text x="842.0" y="447.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">配置引擎</text>
<text x="842.0" y="463.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">net_cfg · ftp_cfg</text>
<rect x="831.0" y="515.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#e11d48" stroke-width="1.3"/>
<rect x="831.0" y="515.0" width="3.5" height="58.0" rx="1.75" fill="#e11d48" stroke="#e11d48" stroke-width="0"/>
<text x="842.0" y="539.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">TCP 服务面</text>
<text x="842.0" y="555.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">:21 / :2404</text>
<rect x="831.0" y="607.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#e11d48" stroke-width="1.3"/>
<rect x="831.0" y="607.0" width="3.5" height="58.0" rx="1.75" fill="#e11d48" stroke="#e11d48" stroke-width="0"/>
<text x="842.0" y="631.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">文件系统落点</text>
<text x="842.0" y="647.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">/log · /wave</text>
<rect x="831.0" y="699.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#e11d48" stroke-width="1.3"/>
<rect x="831.0" y="699.0" width="3.5" height="58.0" rx="1.75" fill="#e11d48" stroke="#e11d48" stroke-width="0"/>
<text x="842.0" y="723.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">卷锁 + 账本</text>
<text x="842.0" y="739.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">三路 try 协定</text>
<rect x="831.0" y="791.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#e11d48" stroke-width="1.3"/>
<rect x="831.0" y="791.0" width="3.5" height="58.0" rx="1.75" fill="#e11d48" stroke="#e11d48" stroke-width="0"/>
<text x="842.0" y="815.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">一份显存三出</text>
<text x="842.0" y="831.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">屏板 · :7003</text>
<rect x="831.0" y="883.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#e11d48" stroke-width="1.3"/>
<rect x="831.0" y="883.0" width="3.5" height="58.0" rx="1.75" fill="#e11d48" stroke="#e11d48" stroke-width="0"/>
<text x="842.0" y="907.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">ftpTask</text>
<text x="842.0" y="923.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">lftpd 根 :21</text>
<rect x="1027.0" y="331.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#2563eb" stroke-width="1.3"/>
<rect x="1027.0" y="331.0" width="3.5" height="58.0" rx="1.75" fill="#2563eb" stroke="#2563eb" stroke-width="0"/>
<text x="1038.0" y="355.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">主机侧第三端</text>
<text x="1038.0" y="371.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">直打生产 .c</text>
<rect x="1027.0" y="423.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#2563eb" stroke-width="1.3"/>
<rect x="1027.0" y="423.0" width="3.5" height="58.0" rx="1.75" fill="#2563eb" stroke="#2563eb" stroke-width="0"/>
<text x="1038.0" y="447.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">上位侧对端</text>
<text x="1038.0" y="463.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">104 主站</text>
<rect x="1027.0" y="791.0" width="148.0" height="58.0" rx="7" fill="#ffffff" stroke="#2563eb" stroke-width="1.3"/>
<rect x="1027.0" y="791.0" width="3.5" height="58.0" rx="1.75" fill="#2563eb" stroke="#2563eb" stroke-width="0"/>
<text x="1038.0" y="815.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="11.5" fill="#0f172a" text-anchor="start" font-weight="bold">PC 模拟器</text>
<text x="1038.0" y="831.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#475569" text-anchor="start" font-weight="normal">SDL2 同源码</text>
<line x1="34" y1="994.0" x2="1162" y2="994.0" stroke="#cbd5e1" stroke-width="1"/>
<line x1="34.0" y1="997.0" x2="60.0" y2="997.0" stroke="#334155" stroke-width="1.35"/>
<text x="65.0" y="1001.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="start" font-weight="normal">数据 / 读写</text>
<line x1="143.5" y1="997.0" x2="169.5" y2="997.0" stroke="#64748b" stroke-width="1.35" stroke-dasharray="5 3"/>
<text x="174.5" y="1001.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="start" font-weight="normal">命令 · 请求 · 配置</text>
<line x1="296.6" y1="997.0" x2="322.6" y2="997.0" stroke="#7c3aed" stroke-width="1.35" stroke-dasharray="1.5 3"/>
<text x="327.6" y="1001.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="start" font-weight="normal">屏障位 · 心跳 · 同源镜像</text>
<rect x="478.1" y="991.0" width="11.0" height="11.0" rx="2.5" fill="#ffffff" stroke="#475569" stroke-width="1.2"/>
<text x="493.1" y="1001.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="start" font-weight="normal">片外硬件</text>
<rect x="547.1" y="991.0" width="11.0" height="11.0" rx="2.5" fill="#ffffff" stroke="#d97706" stroke-width="1.2"/>
<text x="562.1" y="1001.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="start" font-weight="normal">驱动</text>
<rect x="597.1" y="991.0" width="11.0" height="11.0" rx="2.5" fill="#ffffff" stroke="#16a34a" stroke-width="1.2"/>
<text x="612.1" y="1001.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="start" font-weight="normal">中间件与算法</text>
<rect x="685.1" y="991.0" width="11.0" height="11.0" rx="2.5" fill="#ffffff" stroke="#0d9488" stroke-width="1.2"/>
<text x="700.1" y="1001.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="start" font-weight="normal">任务与握手</text>
<rect x="763.6" y="991.0" width="11.0" height="11.0" rx="2.5" fill="#ffffff" stroke="#e11d48" stroke-width="1.2"/>
<text x="778.6" y="1001.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="start" font-weight="normal">服务与落点</text>
<rect x="842.1" y="991.0" width="11.0" height="11.0" rx="2.5" fill="#ffffff" stroke="#2563eb" stroke-width="1.2"/>
<text x="857.1" y="1001.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#334155" text-anchor="start" font-weight="normal">对外 / 同构端</text>
<text x="34.0" y="1016.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#64748b" text-anchor="start" font-weight="normal">口径:AGENTS.md「数据主线」+ docs/架构设计/项目架构总览.md(行号级证据在那份文档与代码里)。RS-485 采集未接线:rs485_task 有实现但不建任务,接线后数据进语义取数层;其余行为条件(未标定闭锁 / 就绪位 NoClear / CH395F 单连接 / V0.</text>
<text x="34.0" y="1028.0" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" font-size="9.5" fill="#64748b" text-anchor="start" font-weight="normal">1 双转换器无共同采样触发)见正文,它们只改变判据与端口、不改变本图的框与连线。</text>
</svg>

After

Width:  |  Height:  |  Size: 46 KiB

+2
View File
@@ -13,6 +13,7 @@
|---|---|---| |---|---|---|
| 协议帧 / 字节布局(精确比例) | **SVG**(`gen_frame_svg.py`) | 字段宽须严格 ∝ 字节数 + 逐字段标注 + 偏移标尺 | | 协议帧 / 字节布局(精确比例) | **SVG**(`gen_frame_svg.py`) | 字段宽须严格 ∝ 字节数 + 逐字段标注 + 偏移标尺 |
| 闪存 / 内存分区布局(精确比例) | **SVG**(`gen_partition_svg.py`) | 帧图生成器的 MB 计量副本(分区宽 ∝ MB 数 + 偏移标尺) | | 闪存 / 内存分区布局(精确比例) | **SVG**(`gen_partition_svg.py`) | 帧图生成器的 MB 计量副本(分区宽 ∝ MB 数 + 偏移标尺) |
| 端到端链路鸟瞰(一条数据走到底) | **SVG**(`gen_arch_endtoend_svg.py`) | 42 框 37 边、长边各占一条独占通道;标签白片必须落在框间通道内,否则被后画的节点吃掉半字 → 判据见 **`draw-diagram` skill** §三b |
| 分层架构 / 模块依赖 | **SVG**(`gen_arch_svg.py`) | 多节点跨层依赖,Mermaid 会画成"意大利面",手工排布更清晰;单层单职责的栈图用其副本 `gen_storage_stack_svg.py` | | 分层架构 / 模块依赖 | **SVG**(`gen_arch_svg.py`) | 多节点跨层依赖,Mermaid 会画成"意大利面",手工排布更清晰;单层单职责的栈图用其副本 `gen_storage_stack_svg.py` |
| 就绪屏障 / 生产者-消费者 | **SVG**(`gen_barrier_svg.py`) | 三栏对齐 + 正交连线,避免交叉 | | 就绪屏障 / 生产者-消费者 | **SVG**(`gen_barrier_svg.py`) | 三栏对齐 + 正交连线,避免交叉 |
| 流程图 / 时序 / 状态机 / ER | **Mermaid**(Markdown 内嵌) | 拓扑关系类图,节点少、强调连接;渲染器原生支持 | | 流程图 / 时序 / 状态机 / ER | **Mermaid**(Markdown 内嵌) | 拓扑关系类图,节点少、强调连接;渲染器原生支持 |
@@ -45,6 +46,7 @@
| `gen_storage_stack_svg.py` | `docs/images/存储栈分层.svg` | `L_*` / `ROWS` / `TITLE` / `SUB` / `FOOT` | | `gen_storage_stack_svg.py` | `docs/images/存储栈分层.svg` | `L_*` / `ROWS` / `TITLE` / `SUB` / `FOOT` |
| `gen_partition_svg.py` | `docs/images/物理分区布局.svg` | `FIELDS` / `TITLE` / `SUB` / `FOOT`(MB 计量) | | `gen_partition_svg.py` | `docs/images/物理分区布局.svg` | `FIELDS` / `TITLE` / `SUB` / `FOOT`(MB 计量) |
| `gen_net_pending_svg.py` | `docs/images/Net层阻塞模型.svg` | `CALLER_STEPS` / `LOOP_STEPS` / `PHASES` | | `gen_net_pending_svg.py` | `docs/images/Net层阻塞模型.svg` | `CALLER_STEPS` / `LOOP_STEPS` / `PHASES` |
| `gen_arch_endtoend_svg.py` | `docs/images/架构总览_端到端链路.svg` | `NODES`(id / 列 / 行 / 标题 / 副题)+ `EDGES`(出入框方位与 f / g / my / gx 旋钮)+ `BANDS` 纵带;三条独占通道 `CHAN_B` / `CHAN_78` / `CHAN_SLOTS` 与走廊 `GUT_C4C5` 落在几何区 |
| `gen_cfg_arch_svg.py` | `docs/images/配置文件分层架构.svg` | 配置引擎分层 | | `gen_cfg_arch_svg.py` | `docs/images/配置文件分层架构.svg` | 配置引擎分层 |
| `gen_net_stack_svg.py` | `docs/images/BSD网络栈分层.svg` | BSD 网络栈分层 | | `gen_net_stack_svg.py` | `docs/images/BSD网络栈分层.svg` | BSD 网络栈分层 |
| `gen_memory_svg.py` | `docs/images/内存布局_*.svg` | RAM / FLASH 布局 | | `gen_memory_svg.py` | `docs/images/内存布局_*.svg` | RAM / FLASH 布局 |
+9 -1
View File
@@ -1,6 +1,6 @@
# STM32F4-Base 项目架构总览 # STM32F4-Base 项目架构总览
> 版本:v1.2 日期:2026-10-30 > 版本:v1.3 日期:2026-10-30
> 平台:STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2)+ FreeRTOS V10.3.1(CMSIS-RTOS2) > 平台:STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2)+ FreeRTOS V10.3.1(CMSIS-RTOS2)
> 用途:一页看清项目的分层、任务、数据流与目录组织。现状能力与功能缺口的权威口径在 AGENTS.md「项目概况」,细节以各模块专项文档为准(见 §9)。 > 用途:一页看清项目的分层、任务、数据流与目录组织。现状能力与功能缺口的权威口径在 AGENTS.md「项目概况」,细节以各模块专项文档为准(见 §9)。
@@ -48,6 +48,14 @@
| `time_sync` / `sys_clock` → `sd2506` | 墙钟基准与写回 RTC | | `time_sync` / `sys_clock` → `sd2506` | 墙钟基准与写回 RTC |
| `ui_port` / `ui_page_*` → `LVGL` | UI 全部绘制走 LVGL(1bpp 离屏渲染到内存帧缓冲) | | `ui_port` / `ui_page_*` → `LVGL` | UI 全部绘制走 LVGL(1bpp 离屏渲染到内存帧缓冲) |
### 1.3 端到端链路鸟瞰(另一视角:一个电气量走到底)
![一个电气量从片外前端穿过驱动与算法,经任务交给 104 / 显存 / 记录文件;跨任务握手走就绪屏障](../images/架构总览_端到端链路.svg)
> 上图由 `tools/arch_diagram/gen_arch_endtoend_svg.py` 生成(42 框 / 37 边 / 5 条纵带,线型三类:实线 = 数据与读写、短虚线 = 命令·请求·配置、紫点线 = 屏障位·心跳·同源镜像)。
> **与 §1 分层图的分工**:§1 回答「谁属于哪一层、层间单向依赖」,本图回答「一条数据从引脚走到 104 报文 / 显存 / 记录文件要穿过哪些框、谁在什么时候被谁唤醒」。图里的五条纵带只是**列分组**(便于对齐),不是依赖声明——依赖方向仍以 §1.1 表与 `layer-deps` 门禁为准。
> 影响链路走向的两条行为条件已画进图内(RS-485 未接线 → 暂缓接语义取数层;跨任务握手集中经 `就绪屏障`);其余行为条件(未标定闭锁、`osFlagsNoClear`、CH395F 单连接、V0.1 双转换器无共同采样触发)不在图上重复,见 AGENTS.md「项目概况 / 平台约束」与 §2.2、§5。
> `*` `rs485_task.c` 任务体已实现,但**当前未在 `freertos.c` 创建**(B2 暂缓,见 `docs/任务规划/暂缓-RS485.md`);`adc_task.c` 已于 2026-10-10 接入创建(`adcTask`)。 > `*` `rs485_task.c` 任务体已实现,但**当前未在 `freertos.c` 创建**(B2 暂缓,见 `docs/任务规划/暂缓-RS485.md`);`adc_task.c` 已于 2026-10-10 接入创建(`adcTask`)。
> 串口三分(2026-10-29 屏板批 0 后的定态):**USART1 = 屏板链路 700 kbaud**、**UART4 = RS-485(115200,未接线)**、**UART5 = 控制台日志 115200**——日志口原在 USART1,屏板占用后迁至 UART5(RS232#1 台架线改接日志口)。 > 串口三分(2026-10-29 屏板批 0 后的定态):**USART1 = 屏板链路 700 kbaud**、**UART4 = RS-485(115200,未接线)**、**UART5 = 控制台日志 115200**——日志口原在 USART1,屏板占用后迁至 UART5(RS232#1 台架线改接日志口)。
+1
View File
@@ -18,6 +18,7 @@
| `gen_watchdog_svg.py` | 看门狗心跳模型图 | IWDG + 任务心跳位图(置位→判定→喂狗 + 时间轴) | | `gen_watchdog_svg.py` | 看门狗心跳模型图 | IWDG + 任务心跳位图(置位→判定→喂狗 + 时间轴) |
| `gen_adc_frontend_svg.py` | ADC 前端电路图 | 端子 → 串阻 → 转换节点 → 转换器引脚;含差分负端与 STM32 侧对照 | | `gen_adc_frontend_svg.py` | ADC 前端电路图 | 端子 → 串阻 → 转换节点 → 转换器引脚;含差分负端与 STM32 侧对照 |
| `gen_adc_code_scale_svg.py` | ADC 码值刻度图 | 「码」的直观定义:两片转换器的刻度尺对照(16 位双极性 vs 12 位单极性) | | `gen_adc_code_scale_svg.py` | ADC 码值刻度图 | 「码」的直观定义:两片转换器的刻度尺对照(16 位双极性 vs 12 位单极性) |
| `gen_arch_endtoend_svg.py` | 架构总览端到端链路图 | 端到端链路鸟瞰:片外硬件 → 驱动 → 算法 → 任务 → 落点 → 对外/同构端,42 框 37 边 5 条纵带;线型分数据 / 命令 / 屏障心跳三类,长边各占一条独占通道(与 §1 分层图分工:那张答「谁在哪一层」,这张答「一条数据走到底要穿过谁」) |
| `gen_data_trust_ladder_svg.py` | 数据可信台阶图 | 采集数据可信度的五级台阶:现在踩在哪一级、下一级挡什么 | | `gen_data_trust_ladder_svg.py` | 数据可信台阶图 | 采集数据可信度的五级台阶:现在踩在哪一级、下一级挡什么 |
| `gen_data_trust_dc_ac_svg.py` | ADC 直流标定与交流验证图 | 上排直流注入链、下排信号发生器正弦验证,中间列只有正弦能证的六件事 | | `gen_data_trust_dc_ac_svg.py` | ADC 直流标定与交流验证图 | 上排直流注入链、下排信号发生器正弦验证,中间列只有正弦能证的六件事 |
| `gen_wave_record_flow_svg.py` | 故障录波数据流图 | 录波段数据的四个所有者与四条「不许」:谁独占写、谁只能读、标志怎么过界 | | `gen_wave_record_flow_svg.py` | 故障录波数据流图 | 录波段数据的四个所有者与四条「不许」:谁独占写、谁只能读、标志怎么过界 |
+429
View File
@@ -0,0 +1,429 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""生成 docs/images/架构总览_端到端链路.svg —— 端到端链路鸟瞰(纯标准库)。
这张图回答什么问题:一个电气量从芯片引脚走到 104 报文 / 显存 / 记录文件,
中间要穿过哪些框;以及「谁在什么时候被谁唤醒、谁独占写、谁只是同一份代码的
另一颗脑袋」——即**数据流与控制握手**,不是层间依赖清单。
与同目录另两张总览图的分工:
gen_arch_svg.py → 架构分层图:六层 + 每层有哪些模块(回答「谁属于哪一层」)
gen_barrier_svg.py → 就绪屏障图:Event Flags 模型细节
本脚本 → 端到端链路:片外硬件 → 驱动 → 算法 → 任务 → 落点 → 对外/同构端
线型三类:实线 = 数据与读写;短虚线 = 命令 / 请求 / 配置;紫点线 = 屏障位、心跳与同源镜像。
为什么不用 Mermaid:本图 42 框 37 边,含跨行长边(遥测快照 → 主机侧第三端独占一条行通道)、
同列相邻框之间的并行双线(定值下行 / YK 上行各占一个出框位置)、绕底槽的握手边
(就绪屏障 → 控制台要跨过整行),还要把五条分层纵带画成等宽对齐的框。
Mermaid 自动布局既不可控(多节点跨层依赖会画成"意大利面"),也表达不了
「一条边独占一条通道」这类判据。
用法(仓库根目录,任意 python3):
python tools/arch_diagram/gen_arch_endtoend_svg.py
输出:
docs/images/架构总览_端到端链路.svg
改内容:编辑 NODES / EDGES / BANDS / 文案常量后重跑。
调坐标:先改顶部几何常量(CW / CH / PX / PY 与三条独占通道 Y、一条走廊 X),
单边微调走 EDGES 尾部旋钮 f / g(出框、入框位置 0~1)与 my / gx(独占通道坐标)。
产物内容零不确定:**不得**写入时间戳 / 提交号 / 绝对路径(generated-fresh 逐字节比对)。
"""
import html
import os
import sys
import xml.etree.ElementTree as ET
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
OUT = os.path.join(ROOT, "docs", "images", "架构总览_端到端链路.svg")
# ----------------------------- 画布与几何 -----------------------------
PAD_L = 34
PAD_R = 30
TITLE_H = 74
LEGEND_H = 50
FOOT_GAP = 14
CW = 148
CH = 58
COLS = 6
ROWS = 9
PX = 196
PY = 92
BAND_PAD = 13
BAND_TITLE_H = 30
BAND_BOTTOM_PAD = 24 # 带内底部专用走廊(第 8 行下方绕行的握手边走这里)
GRID_W = (COLS - 1) * PX + CW
W = PAD_L + GRID_W + PAD_R
X0 = PAD_L + BAND_PAD
Y0 = TITLE_H + 30 + BAND_PAD + BAND_TITLE_H
H = Y0 + (ROWS - 1) * PY + CH + BAND_PAD + BAND_BOTTOM_PAD + FOOT_GAP + LEGEND_H
# 独占通道 / 走廊(长边走这里;数值落在通道内,改动后回看有无重叠)
CHAN_B = Y0 + 2 * PY + CH + 11 # 第 2、3 行之间(遥测快照 → 主机侧第三端)
CHAN_B2 = Y0 + 2 * PY + CH + 24 # 同一条走廊的下半股(遥测快照 → iec104Task,两条平行不共线)
CHAN_78 = Y0 + 7 * PY + CH + 12 # 第 7、8 行之间(RS-485 → 屏板链路)
CHAN_SLOTS = Y0 + 8 * PY + CH + 13 # 第 8 行下方底槽(就绪屏障 → 控制台,带内独占)
GUT_C4C5 = X0 + 4 * PX + CW + (PX - CW) * 0.62 # 第 4、5 列之间垂直走廊(配置引擎 → 落点)
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
TITLE_FONT = 20
SUB_FONT = 12.5
BAND_FONT = 13.5
BAND_SUB_FONT = 10.5
NODE_FONT = 11.5
NODE_SUB_FONT = 9.5
EDGE_FONT = 9.5
FOOT_FONT = 9.5
# 调色板(沿用 docs/架构设计/图形绘制流程.md;紫色整族让给「屏障 / 事件」,纵带改用玫红)
C_HW = ("#475569", "#f1f5f9")
C_DRV = ("#d97706", "#fffbeb")
C_MID = ("#16a34a", "#f0fdf4")
C_TASK = ("#0d9488", "#f0fdfa")
C_SVC = ("#e11d48", "#fff1f2")
C_EXT = ("#2563eb", "#eff6ff")
E_DATA = "#334155"
E_CTRL = "#64748b"
E_EVT = "#7c3aed"
GRID_LINE = "#cbd5e1"
TITLE = "STM32F407ZGTx 工业 DTU 平台 · 端到端链路总览"
SUBTITLE = ("一个电气量从引脚到 104 报文 / 显存 / 记录文件穿过的框,与「谁在什么时候被谁唤醒」;"
"分层归属与层内模块清单见 项目架构总览.svg,本图只画链路与握手")
FOOTNOTE = ("口径:AGENTS.md「数据主线」+ docs/架构设计/项目架构总览.md(行号级证据在那份文档与代码里)。"
"RS-485 采集未接线:rs485_task 有实现但不建任务,接线后数据进语义取数层;"
"其余行为条件(未标定闭锁 / 就绪位 NoClear / CH395F 单连接 / V0.1 双转换器无共同采样触发)见正文,"
"它们只改变判据与端口、不改变本图的框与连线。")
# ----------------------------- 纵带(一列 = 一条带) -----------------------------
BANDS = [
(0, "片外硬件", "板级器件 · 不走软件分层", C_HW),
(1, "驱动层 Drivers/BSP", "手写芯片驱动与总线", C_DRV),
(2, "中间件与算法", "取数 → 相量 → 快照", C_MID),
(3, "任务编排与握手", "App/task · 就绪屏障", C_TASK),
(4, "平台服务与数据落点", "记录 / 配置 / 网络门面", C_SVC),
]
# ----------------------------- 节点:(id, 列, 行, 标题, 副题) -----------------------------
NODES = [
("tpafe", 0, 0, "TPAFE5160 前端", "并行总线 · 8 路"),
("adc1", 0, 1, "片内 ADC1 + DMA", "TIM3 TRGO 乒乓"),
("eth", 0, 4, "CH395F 以太网", "SPI 串行化"),
("nandchip", 0, 5, "GD5F SPI NAND", "2 KB 页 · ECC"),
("rtc", 0, 6, "SD2506 RTC", "I2C1 硬件亚秒"),
("panel", 0, 7, "屏板 160×160", "USART1 700000"),
("rs485hw", 0, 8, "RS-485 总线", "UART4 · 未接线(暂缓)"),
("drv_tpafe", 1, 0, "TPAFE 驱动", "原子快照 snapshot"),
("drv_adc", 1, 1, "ADC · TIM3 · DMA", "手写上电自校准"),
("drv_ch395", 1, 4, "CH395F 驱动", "中断集中读清"),
("drv_nand", 1, 5, "NAND 驱动 + FTL", "ECC · BBT"),
("drv_rtc", 1, 6, "RTC 驱动 + 墙钟", "sys_clock epoch"),
("sb_link", 1, 7, "屏板链路驱动", "借道 RS485"),
("console", 1, 8, "控制台 UART5", "4 KB 环形"),
("adc_input", 2, 0, "语义取数层", "有效性 · 标定"),
("phasor", 2, 1, "全周傅氏相量", "ring → phasor"),
("meas", 2, 2, "遥测快照双区", "测频 200 ms"),
("netmw", 2, 4, "NET 中间件", "socket · 挂起"),
("fs", 2, 5, "FTL + FatFs", "dhara · 8.3 短名"),
("timeproto", 2, 6, "对时协议", "推送写 RTC"),
("lvgl", 2, 7, "LVGL + ui_port", "160×160 1bpp"),
("selfheal", 2, 8, "IWDG + Fault", "心跳掩码"),
("adc_task", 3, 0, "adcTask", "1 kHz 消费者"),
("protect_task", 3, 1, "protectTask", "20 ms 判据"),
("protset", 3, 2, "定值层", "prot_set 校验"),
("iec104_task", 3, 3, "iec104Task", "100 ms · :2404"),
("net_task", 3, 4, "netTask", "MsgQueue 深 8"),
("default_task", 3, 5, "defaultTask", "记录唯一写者"),
("time_sync_task", 3, 6, "timeSyncTask", "等 NET_READY"),
("ui_task", 3, 7, "uiTask", "唯一写侧"),
("barrier", 3, 8, "就绪屏障", "位组 · NoClear"),
("soe", 4, 1, "SOE 事件", "soe.log 定长"),
("setfile", 4, 2, "定值落盘", "/prot_set.bin"),
("cfgplane", 4, 3, "配置引擎", "net_cfg · ftp_cfg"),
("services", 4, 4, "TCP 服务面", ":21 / :2404"),
("files", 4, 5, "文件系统落点", "/log · /wave"),
("vollock", 4, 6, "卷锁 + 账本", "三路 try 协定"),
("disp", 4, 7, "一份显存三出", "屏板 · :7003"),
("ftp_task", 4, 8, "ftpTask", "lftpd 根 :21"),
("hosttest", 5, 2, "主机侧第三端", "直打生产 .c"),
("peer", 5, 3, "上位侧对端", "104 主站"),
("sim", 5, 7, "PC 模拟器", "SDL2 同源码"),
]
# ----------------------------- 边 -----------------------------
# (起点, 终点, 标签, 类别, 出框, 入框, 旋钮)
# 类别 d=数据/读写 c=命令/请求/配置 e=屏障·心跳·同源
# 出/入框 t b l r;出框轴与入框轴同向时自动加一段(水平用 gx,垂直用 my)
# 旋钮 f/g = 该边上的位置 0~1(默认 0.5);my/gx = 独占通道坐标;seg/t = 标签落点
EDGES = [
("tpafe", "drv_tpafe", "8 路帧", "d", "r", "l", {}),
("drv_adc", "adc1", "TRGO", "c", "l", "r", {}),
("drv_tpafe", "adc_input", "原子快照", "d", "r", "l", {}),
("drv_adc", "adc_input", "DMA 双缓冲", "d", "t", "b", {"f": 0.72, "g": 0.28}),
("adc_input", "phasor", "语义通道", "d", "b", "t", {}),
("phasor", "meas", "相量", "d", "b", "t", {}),
("adc_task", "protect_task", "1 kHz → 20 ms", "c", "b", "t", {}),
("meas", "iec104_task", "同一上游", "d", "b", "t", {"f": 0.3, "g": 0.5, "my": CHAN_B2}),
("protset", "protect_task", "定值", "d", "t", "b", {"f": 0.34, "g": 0.34}),
("iec104_task", "protset", "YK 请求", "c", "t", "b", {"f": 0.68, "g": 0.68}),
("iec104_task", "cfgplane", "脏段", "c", "r", "l", {}),
("protect_task", "soe", "边沿投递", "d", "r", "l", {}),
("default_task", "files", "唯一写者", "d", "r", "l", {}),
("default_task", "vollock", "try 语义", "c", "b", "t", {"f": 0.86, "g": 0.26}),
("eth", "drv_ch395", "SPI 事务", "d", "r", "l", {}),
("drv_ch395", "netmw", "集中读清", "d", "r", "l", {}),
("netmw", "net_task", "串行执行", "d", "r", "l", {}),
("net_task", "services", "单连接", "d", "r", "l", {}),
("services", "peer", "TCP 对端", "c", "r", "b", {"f": 0.26, "g": 0.6}),
("nandchip", "drv_nand", "页读写", "d", "r", "l", {}),
("drv_nand", "fs", "逻辑块", "d", "r", "l", {}),
("fs", "default_task", "挂载", "d", "r", "l", {}),
("rtc", "drv_rtc", "I2C1", "d", "r", "l", {}),
("drv_rtc", "timeproto", "墙钟", "d", "r", "l", {}),
("time_sync_task", "timeproto", "收包对时", "d", "l", "r", {}),
("lvgl", "sb_link", "帧 B", "d", "l", "r", {}),
("ui_task", "lvgl", "灌数", "d", "l", "r", {}),
("ui_task", "disp", "同一显存", "d", "r", "l", {}),
("sb_link", "panel", "700000", "d", "l", "r", {}),
("ui_task", "selfheal", "100 ms 喂心跳", "c", "b", "t", {"f": 0.26, "g": 0.32, "my": CHAN_78}),
("ui_task", "barrier", "等就绪", "c", "b", "t", {"f": 0.62}),
("barrier", "ftp_task", "WaitAll", "d", "r", "l", {}),
("disp", "sim", "同源像素", "e", "r", "l", {}),
("meas", "hosttest", "纯逻辑直跑", "e", "b", "b", {"f": 0.82, "g": 0.62, "my": CHAN_B}),
("barrier", "console", "唤醒发送", "e", "b", "b", {"my": CHAN_SLOTS}),
("cfgplane", "files", "唯一落盘者", "d", "r", "r", {"f": 0.72, "g": 0.5, "gx": GUT_C4C5}),
("rs485hw", "sb_link", "回调宿主", "d", "t", "b", {"f": 0.28, "g": 0.28, "my": CHAN_78}),
]
OVERFLOW = []
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")
def esc(s):
return html.escape(s, quote=True)
def text_w(s, fs):
"""估宽:CJK / 全角 / 箭头 ≈ 1em,其余 ≈ 0.58em(宁大勿小)。"""
w = 0.0
for ch in s:
o = ord(ch)
if o > 0x2E80 or ch in "→×·…—":
w += 1.0
else:
w += 0.58
return w * fs
def check_fit(s, fs, avail, where):
got = text_w(s, fs)
if got > avail:
OVERFLOW.append("%s: %.0f > %.0f [%s]" % (where, got, avail, s))
def wrap(s, fs, avail, max_lines, where):
"""按估宽折行;超行数截断并加 … 记一条 OVERFLOW。"""
lines, cur = [], ""
for ch in s:
if cur and text_w(cur + ch, fs) > avail:
lines.append(cur)
cur = ch
else:
cur += ch
if cur:
lines.append(cur)
if len(lines) > max_lines:
lines = lines[:max_lines]
lines[-1] = lines[-1].rstrip()[:-1] + "…"
OVERFLOW.append("%s: 折行超 %d 行 [%s]" % (where, max_lines, s))
return lines
def rect(x, y, w, h, fill, stroke, rx=7, sw=1.2, opacity=1.0):
o = ' fill-opacity="%.2f"' % opacity if opacity != 1.0 else ""
return ('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="%g" '
'fill="%s" stroke="%s" stroke-width="%g"%s/>' % (x, y, w, h, rx, fill, stroke, sw, o))
def txt(x, y, s, fs, fill, anchor="start", weight="normal", opacity=1.0):
o = ' fill-opacity="%.2f"' % opacity if opacity != 1.0 else ""
return ('<text x="%.1f" y="%.1f" font-family="%s" font-size="%g" fill="%s" '
'text-anchor="%s" font-weight="%s"%s>%s</text>'
% (x, y, FONT, fs, fill, anchor, weight, o, esc(s)))
ECOL = {"d": E_DATA, "c": E_CTRL, "e": E_EVT}
EDASH = {"d": None, "c": "5 3", "e": "1.5 3"}
def poly(points, cls):
d = " ".join(("M" if i == 0 else "L") + "%.1f %.1f" % p for i, p in enumerate(points))
dash = EDASH[cls]
dd = ' stroke-dasharray="%s"' % dash if dash else ""
w = 1.35 if cls == "d" else 1.25
return ('<path d="%s" fill="none" stroke="%s" stroke-width="%g"%s '
'marker-end="url(#ar-%s)"/>' % (d, ECOL[cls], w, dd, cls))
def elabel(x, y, s, cls):
"""边标签:先铺白色底片,避免与线或其他标签叠字。"""
w = text_w(s, EDGE_FONT) + 7
h = EDGE_FONT + 5
return ('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="3" fill="#ffffff" '
'fill-opacity="0.94"/><text x="%.1f" y="%.1f" font-family="%s" font-size="%g" '
'fill="%s" text-anchor="middle">%s</text>'
% (x - w / 2, y - h / 2, w, h, x, y + EDGE_FONT * 0.36, FONT, EDGE_FONT,
ECOL[cls], esc(s)))
class Box(object):
def __init__(self, nid, col, row, label, sub):
self.id = nid
self.col = col
self.row = row
self.label = label
self.sub = sub
self.x = float(X0 + col * PX)
self.y = float(Y0 + row * PY)
self.w = float(CW)
self.h = float(CH)
def side(self, sd, f):
"""出框点:sd ∈ t b l r,f 沿该边位置(0 靠左/上,1 靠右/下)。"""
if sd == "t":
return (self.x + self.w * f, self.y)
if sd == "b":
return (self.x + self.w * f, self.y + self.h)
if sd == "l":
return (self.x, self.y + self.h * f)
return (self.x + self.w, self.y + self.h * f)
def route(a, b, ex, en, k):
"""折线点集:出框轴与入框轴垂直 → 3 点;同轴 → 4 点(加一段 gx 或 my)。"""
f = k.get("f", 0.5)
g = k.get("g", 0.5)
pa = a.side(ex, f)
pb = b.side(en, g)
ax = ex in "lr"
bx = en in "lr"
if ax != bx:
if ax: # 水平出、垂直入
return [pa, (pb[0], pa[1]), pb]
return [pa, (pa[0], pb[1]), pb]
if ax: # 都水平
if abs(pa[1] - pb[1]) < 0.6:
return [pa, pb] # 同一行:直连(别插退化折点,否则标签落点偏半格)
gx = k.get("gx", (pa[0] + pb[0]) / 2)
return [pa, (gx, pa[1]), (gx, pb[1]), pb]
if abs(pa[0] - pb[0]) < 0.6: # 同一列:直连
return [pa, pb]
my = k.get("my", (pa[1] + pb[1]) / 2) # 都垂直:走 my 水平通道
return [pa, (pa[0], my), (pb[0], my), pb]
def label_at(points, k):
"""标签落点:默认整条折线的弧长中点(不会退化成「第一半的中点」);
seg = 指定段序号、t = 该段(或整条线)上的比例。"""
if "seg" in k:
i = k["seg"]
t = k.get("t", 0.5)
x0, y0 = points[i]
x1, y1 = points[i + 1]
return (x0 + (x1 - x0) * t, y0 + (y1 - y0) * t)
lens = [abs(points[i + 1][0] - points[i][0]) + abs(points[i + 1][1] - points[i][1])
for i in range(len(points) - 1)]
total = sum(lens) or 1.0
want = total * k.get("t", 0.5)
acc = 0.0
for i, L in enumerate(lens):
if acc + L >= want or i == len(lens) - 1:
u = 0.5 if L <= 0 else min(1.0, max(0.0, (want - acc) / L))
x0, y0 = points[i]
x1, y1 = points[i + 1]
return (x0 + (x1 - x0) * u, y0 + (y1 - y0) * u)
acc += L
return points[0]
def build():
boxes = [Box(*n) for n in NODES]
nodes = dict((b.id, b) for b in boxes)
col_band = dict((c, cb) for c, _t, _s, cb in BANDS)
check_fit(TITLE, TITLE_FONT, W - PAD_L - PAD_R, "title")
p = ['<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" '
'viewBox="0 0 %d %d" font-family="%s">' % (W, H, W, H, FONT)]
defs = ['<defs>']
for cls in ("d", "c", "e"):
defs.append('<marker id="ar-%s" viewBox="0 0 10 10" refX="9" refY="5" '
'markerWidth="6" markerHeight="6" orient="auto-start-reverse">'
'<path d="M0 0 L10 5 L0 10 z" fill="%s"/></marker>' % (cls, ECOL[cls]))
defs.append("</defs>")
p.append("".join(defs))
p.append('<rect x="0" y="0" width="%d" height="%d" fill="#ffffff"/>' % (W, H))
p.append(txt(PAD_L, 34, TITLE, TITLE_FONT, "#0f172a", weight="bold"))
for i, ln in enumerate(wrap(SUBTITLE, SUB_FONT, W - PAD_L - PAD_R, 2, "subtitle")):
p.append(txt(PAD_L, 55 + i * 15, ln, SUB_FONT, "#475569"))
band_y = Y0 - BAND_PAD - BAND_TITLE_H
band_h = (ROWS - 1) * PY + CH + 2 * BAND_PAD + BAND_BOTTOM_PAD + BAND_TITLE_H
for col, title, sub, (fg, bg) in BANDS:
x = X0 + col * PX - BAND_PAD
w = CW + 2 * BAND_PAD
p.append(rect(x, band_y, w, band_h, bg, fg, rx=10, sw=1.1, opacity=0.5))
check_fit(title, BAND_FONT, w - 14, "band[%d]" % col)
p.append(txt(x + 10, band_y + 19, title, BAND_FONT, fg, weight="bold"))
for j, ln in enumerate(wrap(sub, BAND_SUB_FONT, w - 20, 1, "band-sub[%d]" % col)):
p.append(txt(x + 10, band_y + 33, ln, BAND_SUB_FONT, fg, opacity=0.85))
for src, dst, label, cls, ex, en, k in EDGES:
pts = route(nodes[src], nodes[dst], ex, en, k)
p.append(poly(pts, cls))
lx, ly = label_at(pts, k)
p.append(elabel(lx, ly, label, cls))
for b in boxes:
fg, _bg = col_band.get(b.col, C_EXT)
p.append(rect(b.x, b.y, b.w, b.h, "#ffffff", fg, rx=7, sw=1.3))
p.append(rect(b.x, b.y, 3.5, b.h, fg, fg, rx=1.75, sw=0))
check_fit(b.label, NODE_FONT, b.w - 16, "node[%s]" % b.id)
p.append(txt(b.x + 11, b.y + 24, b.label, NODE_FONT, "#0f172a", weight="bold"))
for j, ln in enumerate(wrap(b.sub, NODE_SUB_FONT, b.w - 18, 2, "node-sub[%s]" % b.id)):
p.append(txt(b.x + 11, b.y + 40 + j * 12, ln, NODE_SUB_FONT, "#475569"))
ly = H - LEGEND_H + 4
p.append('<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="%s" stroke-width="1"/>'
% (PAD_L, band_y + band_h + 16, W - PAD_R, band_y + band_h + 16, GRID_LINE))
lx = PAD_L
for label, cls, kind in [("数据 / 读写", "d", None), ("命令 · 请求 · 配置", "c", "5 3"),
("屏障位 · 心跳 · 同源镜像", "e", "1.5 3")]:
dd = ' stroke-dasharray="%s"' % kind if kind else ""
p.append('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" '
'stroke-width="1.35"%s/>' % (lx, ly + 1, lx + 26, ly + 1, ECOL[cls], dd))
p.append(txt(lx + 31, ly + 5, label, FOOT_FONT, "#334155"))
lx += 31 + text_w(label, FOOT_FONT) + 24
for label, (fg, _bg) in [("片外硬件", C_HW), ("驱动", C_DRV), ("中间件与算法", C_MID),
("任务与握手", C_TASK), ("服务与落点", C_SVC), ("对外 / 同构端", C_EXT)]:
p.append(rect(lx, ly - 5, 11, 11, "#ffffff", fg, rx=2.5, sw=1.2))
p.append(txt(lx + 15, ly + 5, label, FOOT_FONT, "#334155"))
lx += 15 + text_w(label, FOOT_FONT) + 16
if lx > W - PAD_R:
OVERFLOW.append("legend: 图例排到 %.0f 超出右边界 %.0f" % (lx, W - PAD_R))
for j, ln in enumerate(wrap(FOOTNOTE, FOOT_FONT, W - PAD_L - PAD_R, 2, "footnote")):
p.append(txt(PAD_L, H - 26 + j * 12, ln, FOOT_FONT, "#64748b"))
p.append("</svg>")
svg = "\n".join(p) + "\n"
ET.fromstring(svg)
d = os.path.dirname(OUT)
if not os.path.isdir(d):
os.makedirs(d)
with open(OUT, "wb") as f:
f.write(svg.encode("utf-8"))
emit("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, H, OUT))
if OVERFLOW:
for w in OVERFLOW:
emit("OVERFLOW WARN " + w)
emit("!! %d overflow warnings" % len(OVERFLOW))
if __name__ == "__main__":
build()