From a328093dbba8ae4acbb5d8d310652159b8c8b598 Mon Sep 17 00:00:00 2001 From: AetherPendragon Date: Sun, 24 May 2026 01:06:16 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=A4=9A=E4=BD=99=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- diagrams/UML图设计说明.md | 250 --------- diagrams/generate_uml.py | 591 --------------------- diagrams/uml_diagrams.drawio | 911 --------------------------------- 3 files changed, 1752 deletions(-) delete mode 100644 diagrams/UML图设计说明.md delete mode 100644 diagrams/generate_uml.py delete mode 100644 diagrams/uml_diagrams.drawio diff --git a/diagrams/UML图设计说明.md b/diagrams/UML图设计说明.md deleted file mode 100644 index ca54ccce..00000000 --- a/diagrams/UML图设计说明.md +++ /dev/null @@ -1,250 +0,0 @@ -# 智途投送系统 — UML 设计规格说明书 - -> 本文档配合 `uml_diagrams.drawio` 使用,共包含 6 张 UML 图,覆盖「声源分析模块」和「单兵终端APP」两个核心子系统。 -> 所有 UML 图均按《软件体系结构》课程规范绘制,可直接导入 draw.io(diagrams.net)编辑。 - ---- - -## 📁 文件说明 - -| 文件名 | 说明 | -|--------|------| -| `uml_diagrams.drawio` | draw.io 源文件(6 个 Page),直接拖拽到 https://app.diagrams.net 即可打开 | -| `UML图设计说明.md` | 本文件,解释每张图的设计意图和体系结构映射 | - ---- - -## 图1:类图 — 声源分析模块核心类结构 - -### 为什么画这张图? - -**类图(Class Diagram)** 是面向对象设计的核心静态结构图。软件设计规格说明书(SDS)必须包含类图,因为它: -- 展示系统的**静态结构**——有哪些类、类的属性和方法 -- 展示类之间的关系——**组合、依赖、继承** -- 是编码实现的直接依据,体现了「从设计到代码」的映射 - -### 这张图展示了什么设计思路? - -#### 1. 组合关系(Composition)体现「管道-过滤器」架构 - -- `Pipeline` 通过 **组合**(菱形实心箭头)持有 6 个核心子模块: - - `AudioBuffer`(音频循环缓冲) - - `FeatureExtractor`(Mel 频谱特征提取) - - `GunshotClassifier`(枪声分类器) - - `GccPhatLocalizer`(GCC-PHAT 声源定位) - - `DistanceEstimator`(SPL 距离估计) - - `ThreatTracker`(威胁跟踪去重) - -> **设计意图**:组合关系表明这些子模块的生命周期由 `Pipeline` 管理,`Pipeline` 销毁时子模块也随之销毁。这与「管道-过滤器」架构中「过滤器由管道统一管理」的思想一致。 - -#### 2. PIMPL 惯用法的信息隐藏 - -- `Pipeline` 和 `FeatureExtractor` 的私有属性只有 `-impl_: Impl*`,真正的实现细节被隐藏在 `.cpp` 文件中。 - -> **设计意图**:类图只暴露公共接口(`Process`、`FromYaml`、`Reset`),隐藏实现细节。这符合**信息隐藏原则**(Information Hiding),降低模块间的耦合度,提升可修改性。 - -#### 3. 跨平台分层设计 - -- `AcousticNode`(ROS 层)依赖 `Pipeline`(业务层),但不直接依赖 `AudioBuffer` 等核心类 -- `WavFileSource` 作为独立 IO 类,可被 `AcousticNode` 直接组合 - -> **设计意图**:通过引入独立的 IO 适配层,核心算法与数据源解耦。同一套算法既可以读取 WAV 文件做离线测试,也可以接收 ROS Topic 做在线推理。 - ---- - -## 图2:顺序图 — Pipeline::Process 音频处理调用链 - -### 为什么画这张图? - -**顺序图(Sequence Diagram)** 展示对象之间的**动态交互**和**消息时序**。在 SDS 中,顺序图用于: -- 验证类图设计的可行性——类之间能否协作完成业务 -- 展示关键用例的详细流程——一次音频分析涉及哪些对象、调用顺序如何 -- 发现设计缺陷——是否存在循环依赖、重复调用、性能瓶颈 - -### 这张图展示了什么设计思路? - -#### 1. 同步阻塞调用链确保数据一致性 - -从 `AcousticNode` → `Pipeline::Process` → 各子模块的方法调用都是**同步阻塞**的(实线箭头)。 - -> **设计意图**:单线程同步执行避免了多线程竞争条件和锁开销,对于「实时音频流处理」场景,简化了并发控制,确保了数据在流水线中的顺序一致性。 - -#### 2. 严格的单向数据流 - -消息严格从上到下传递,不存在对象 A 调用 B、B 又回调 A 的循环。 - -> **设计意图**:符合管道-过滤器架构「无环」的约束。数据从源端(麦克风)单向流向汇点(威胁事件),每个过滤器只接收上游输出、产生下游输入,不反向依赖。 - -#### 3. 返回值与发布解耦 - -`Pipeline::Process` 返回 `AcousticFrame` 后,`AcousticNode` 再调用 `ThreatPublisher::Publish`。 - -> **设计意图**:「计算」与「通信」分离。Pipeline 只负责纯计算逻辑,不关心 ROS 通信细节;AcousticNode 作为适配层,负责将计算结果转换为 ROS 消息发布。这实现了**关注点分离**(Separation of Concerns)。 - ---- - -## 图3:组件图 — 声源分析模块分层组件结构 - -### 为什么画这张图? - -**组件图(Component Diagram)** 展示系统的**物理/逻辑组件**及其依赖关系。在 SDS 中,组件图用于: -- 展示系统的模块化划分——系统由哪些可替换的组件构成 -- 展示组件依赖——哪个组件依赖哪个组件、依赖哪些外部库 -- 指导构建系统——CMake/Makefile 的模块划分依据 - -### 这张图展示了什么设计思路? - -#### 1. 三层组件隔离 - -| 组件 | 职责 | 可替换性 | -|------|------|---------| -| `acoustic_analyzer_core` | 核心算法,平台无关 | 可在任何操作系统编译 | -| `acoustic_analyzer_io` | 音频输入适配 | 新增音频源只需扩展此层 | -| `acoustic_analyzer_ros` | ROS 集成包装 | 换用 DDS 时只需替换此层 | - -> **设计意图**:严格的分层使得「替换 ROS 框架」或「新增音频源」不会影响核心算法。这是**分层架构**的核心价值——修改局部,不影响全局。 - -#### 2. 依赖外部库通过「虚线箭头」标注 - -- `core` → ONNX Runtime(模型推理) -- `core` → Eigen 3.4.0(矩阵运算) -- `core` → yaml-cpp(配置解析) -- `ros` → ROS (roscpp)(通信框架) - -> **设计意图**:虚线箭头表示「依赖/使用」关系(UML 中的 `«use»`)。这些外部库是第三方组件,不是自研代码,但需要明确标注以说明编译依赖和环境要求。 - -#### 3. 接口抽象(IAcousticSource) - -`acoustic_analyzer_io` 组件向外暴露 `IAcousticSource` 接口,`core` 层通过该接口获取音频数据。 - -> **设计意图**:依赖倒置原则(DIP)——高层模块(core)不依赖低层模块的具体实现(WavFileSource),而是依赖抽象接口。这支持了「开闭原则」:新增音频源不需要修改 core 代码。 - ---- - -## 图4:用例图 — 单兵终端APP功能需求 - -### 为什么画这张图? - -**用例图(Use Case Diagram)** 是需求分析阶段的核心产物,在 SDS 中用于: -- 从**用户视角**描述系统功能边界——系统能做什么、为谁做 -- 识别参与者(Actor)和用例(Use Case)的交互关系 -- 作为功能验收的基准——每个用例对应一个可测试的场景 - -### 这张图展示了什么设计思路? - -#### 1. 明确的系统边界 - -用一个大矩形框住所有用例,标注「单兵终端APP」,表示这是系统的功能边界。框外的「前线士兵」是参与者,框内是用例。 - -> **设计意图**:软件体系结构设计的第一步是定义系统边界。用例图清晰地表达了「单兵终端APP的职责范围」——它不负责无人机控制、不负责路径规划计算,只负责「前端交互和信息上报」。 - -#### 2. `«include»` 关系:必要子流程 - -「上报物资需求」`«include»`「选择投放点」。 - -> **设计意图**:include 表示被包含用例是主用例的**必要组成部分**。士兵上报需求时,**必须**选择投放点,这不是可选的。这对应了代码中 `submitDemand()` 必须调用投放点选择逻辑。 - -#### 3. `«extend»` 关系:可选扩展 - -「SOS一键求救」`«extend»`「实时位置上报」。 - -> **设计意图**:extend 表示扩展用例在特定条件下才会触发。SOS 求救时,系统自动触发位置上报(扩展行为),但位置上报本身也可以独立运行。这对应了代码中 `triggerSOS()` 内部调用 `LocationModule.getCurrentPosition()` 的设计。 - ---- - -## 图5:活动图 — 物资需求上报业务流程 - -### 为什么画这张图? - -**活动图(Activity Diagram)** 描述业务过程或操作的工作流。在 SDS 中,活动图用于: -- 展示用例的**内部流程**——用例图只说了「能做什么」,活动图说「怎么做」 -- 识别分支、合并、并发——哪些步骤有判断条件、哪些可以并行 -- 发现异常路径——失败时如何处理、是否有回退机制 - -### 这张图展示了什么设计思路? - -#### 1. 支持多种投放点选择模式 - -从「查看推荐投放点」分支到「地图选点/搜索」: - -> **设计意图**:活动图展示了投放点选择的两种入口——列表推荐(后端计算)和地图自由选点(高德 API)。这体现了**灵活性**设计:既给士兵提供「一键选择安全点」的便捷,也支持「自定义精确位置」的精细化需求。 - -#### 2. 失败处理与降级策略 - -在「提交成功?」判断分支: -- **成功分支**:显示成功提示,跳转首页 -- **失败分支**:显示错误提示,**支持重试** - -> **设计意图**:体现了**可用性**质量属性。APP 在网络不稳定时不会崩溃,而是给出明确反馈并提供重试路径。代码中 `API.postDemand()` 被 try-catch 包裹,且内置 Mock 数据保证演示可用性。 - -#### 3. 回退路径(Cancel → Return) - -如果士兵点击「否」(不确认提交),流程回退到「选择物资类型」步骤。 - -> **设计意图**:活动图中的回退边(从 Cancel 回到 Type)展示了系统的**容错性**。用户可以在提交前任意步骤返回修改,不会丢失已填写的信息(因为状态保存在内存中)。 - ---- - -## 图6:部署图 — 系统物理部署拓扑 - -### 为什么画这张图? - -**部署图(Deployment Diagram)** 展示系统的**物理节点**和**运行时部署**。在 SDS 中,部署图用于: -- 展示软件构件运行在哪些硬件/操作系统上 -- 展示节点间的通信协议和网络拓扑 -- 指导运维部署——需要哪些机器、装什么系统、开放哪些端口 - -### 这张图展示了什么设计思路? - -#### 1. 三层物理分离 - -| 节点 | 部署位置 | 运行环境 | -|------|---------|---------| -| 士兵手机 | 前线 | Android 12+ / Capacitor WebView | -| 后方服务器 | 指挥所 | Ubuntu 22.04 / Python Flask | -| 无人机机载计算机 | 无人机 | Ubuntu 20.04 / ROS Noetic | - -> **设计意图**:物理分离对应了**逻辑子系统分离**。单兵APP、后勤系统、无人机软件分别运行在不同硬件上,通过定义好的网络协议通信。这种分离确保了「前线设备轻量化」(手机即可)、「后端集中化」(服务器统一管理)、「机载实时化」(ROS 硬实时)。 - -#### 2. 服务器作为「消息中转站」 - -单兵APP 不直接与无人机通信,而是通过 HTTP/REST 与服务器交互,服务器再通过 ROS/WebSocket 与无人机交互。 - -> **设计意图**:这是**中介者模式**(Mediator Pattern)在物理层的体现。服务器作为中介者,解耦了前线士兵与无人机控制: -> - 士兵不需要知道无人机的 IP 地址或 ROS 网络配置 -> - 无人机不需要暴露接口给外部互联网 -> - 服务器可以做身份认证、日志审计、任务调度 - -#### 3. 可选直连通道(虚线) - -单兵APP → 无人机之间有一条虚线标注「rosbridge(可选直连)」。 - -> **设计意图**:虚线表示「非必须但支持的通信路径」。在演示或局域网环境下,前端可以通过 rosbridge 直接订阅无人机状态,绕过 Flask 后端,降低延迟。这是**灵活性**与**安全性**的权衡——平时走服务器(安全),紧急时直连(快速)。 - ---- - -## 📊 六张图的体系结构映射总结 - -| UML 图 | 视角 | 体系结构知识点 | 在我们项目中的体现 | -|--------|------|--------------|-----------------| -| **类图** | 静态结构 | 信息隐藏、组合复用 | Pipeline 组合 6 个子模块,PIMPL 隐藏实现 | -| **顺序图** | 动态交互 | 管道-过滤器约束 | 同步阻塞调用链,单向无环数据流 | -| **组件图** | 模块组织 | 分层架构、依赖倒置 | core/io/ros 三层,依赖抽象接口 | -| **用例图** | 用户功能 | 系统边界、include/extend | 单兵APP功能边界,SOS扩展位置上报 | -| **活动图** | 业务流程 | 可用性、容错设计 | 失败重试、回退路径、Mock降级 | -| **部署图** | 物理拓扑 | 中介者模式、物理分离 | 服务器中转、三层硬件分离、可选直连 | - ---- - -## 🎯 汇报建议 - -下节课汇报时,建议按以下顺序展示这 6 张图: - -1. **先放部署图(图6)** —— 让听众快速理解「系统由哪些部分组成、运行在哪里」 -2. **再放用例图(图4)** —— 说明「单兵终端APP能做什么、为谁服务」 -3. **聚焦声源分析模块,放类图(图1)** —— 展示核心类的静态结构 -4. **用顺序图(图2)解释类如何协作** —— 动态验证静态设计的可行性 -5. **用组件图(图3)总结分层设计** —— 强调跨平台可移植性和可替换性 -6. **用活动图(图5)展示单兵APP的典型流程** —— 以「物资需求上报」为例,说明用户体验设计 - -> 每张图讲 1-2 分钟即可,重点突出「**这张图对应课程中的哪个知识点**」以及「**我们在实践中如何体现这个知识点**」。 diff --git a/diagrams/generate_uml.py b/diagrams/generate_uml.py deleted file mode 100644 index b3e17f5b..00000000 --- a/diagrams/generate_uml.py +++ /dev/null @@ -1,591 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -生成 draw.io (diagrams.net) 可用的 UML 图 XML 文件 -包含 6 张图:类图、顺序图、组件图、用例图、活动图、部署图 -""" - -import xml.etree.ElementTree as ET -import uuid -import html - -def make_mxfile(pages): - root = ET.Element("mxfile") - root.set("host", "app.diagrams.net") - root.set("modified", "2026-05-19T00:00:00.000Z") - root.set("agent", "PythonScript") - root.set("version", "24.0.0") - root.set("type", "device") - root.set("pages", str(len(pages))) - for i, (name, graph_model) in enumerate(pages): - diag = ET.SubElement(root, "diagram") - diag.set("name", name) - diag.set("id", str(uuid.uuid4())) - diag.append(graph_model) - return root - -def make_graph_model(cells, w=1200, h=900): - gm = ET.Element("mxGraphModel") - gm.set("dx", "1434") - gm.set("dy", "780") - gm.set("grid", "1") - gm.set("gridSize", "10") - gm.set("guides", "1") - gm.set("tooltips", "1") - gm.set("connect", "1") - gm.set("arrows", "1") - gm.set("fold", "1") - gm.set("page", "1") - gm.set("pageScale", "1") - gm.set("pageWidth", str(w)) - gm.set("pageHeight", str(h)) - gm.set("math", "0") - gm.set("shadow", "0") - root = ET.SubElement(gm, "root") - ET.SubElement(root, "mxCell", {"id":"0"}) - ET.SubElement(root, "mxCell", {"id":"1", "parent":"0"}) - for cell in cells: - root.append(cell) - return gm - -def cell_style(shape, extras=""): - base = { - "class": "swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;", - "class_attr": "text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;", - "actor": "shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;", - "usecase": "ellipse;whiteSpace=wrap;html=1;", - "boundary": "shape=note;whiteSpace=wrap;html=1;backgroundOutline=1;darkOpacity=0.05;", - "rectangle": "rounded=0;whiteSpace=wrap;html=1;", - "rounded": "rounded=1;whiteSpace=wrap;html=1;", - "lifeline": "shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;collapsible=0;recursiveResize=0;outlineConnect=0;", - "activation": "shape=umlDestroy;whiteSpace=wrap;html=1;strokeWidth=3;", - "activation2": "rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;", - "component": "shape=component;align=left;spacingLeft=36;verticalAlign=top;whiteSpace=wrap;html=1;", - "interface": "shape=providedRequiredInterface;verticalAlign=top;spacingTop=0;whiteSpace=wrap;html=1;", - "start": "ellipse;whiteSpace=wrap;html=1;fillColor=#000000;", - "end": "ellipse;whiteSpace=wrap;html=1;fillColor=#000000;strokeColor=#ff0000;", - "activity": "rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;", - "decision": "rhombus;whiteSpace=wrap;html=1;fillColor=#ffffcc;strokeColor=#b3b3b3;", - "node": "shape=cube;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;darkOpacity=0.05;", - "artifact": "shape=note;whiteSpace=wrap;html=1;backgroundOutline=1;darkOpacity=0.05;", - "arrow": "edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;", - "dashed_arrow": "edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;", - "open_arrow": "edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=open;endFill=0;", - "diamond": "edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=diamondThin;endFill=1;", - "async": "edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;", - "message": "edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;verticalAlign=bottom;", - "return": "edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;verticalAlign=bottom;", - "text": "text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;", - "title": "text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=16;fontStyle=1", - } - s = base.get(shape, shape) - if extras: - s += extras - return s - -def add_cell(cells, cid, parent="1", style="", value="", x=0, y=0, w=0, h=0, source=None, target=None, edge=False): - attrs = {"id": str(cid), "parent": str(parent), "style": style, "value": value} - if edge: - attrs["edge"] = "1" - if source: attrs["source"] = str(source) - if target: attrs["target"] = str(target) - else: - attrs["vertex"] = "1" - if x is not None: attrs["x"] = str(x) - if y is not None: attrs["y"] = str(y) - if w is not None: attrs["width"] = str(w) - if h is not None: attrs["height"] = str(h) - cell = ET.Element("mxCell", attrs) - if edge: - geo = ET.SubElement(cell, "mxGeometry", {"relative":"1", "as":"geometry"}) - if source and target: - ET.SubElement(geo, "Array", {"as":"points"}) - else: - geo = ET.SubElement(cell, "mxGeometry", {"x":str(x), "y":str(y), "width":str(w), "height":str(h), "as":"geometry"}) - cells.append(cell) - return cid - -_class_id_counter = 1000 -def add_uml_class(cells, cid, name, attrs, methods, x, y, w=200, h=None): - global _class_id_counter - line_h = 18 - attr_h = len(attrs) * line_h if attrs else line_h - meth_h = len(methods) * line_h if methods else line_h - sep = 6 - total_h = 26 + attr_h + sep + meth_h + 10 - if h and h > total_h: - total_h = h - # class box - add_cell(cells, cid, "1", cell_style("class"), name, x, y, w, total_h) - # separator line - _class_id_counter += 1 - add_cell(cells, f"{cid}_sep1", cid, cell_style("rectangle","fillColor=none;strokeColor=none;"), "", 0, 26, w, 0) - # attrs - ay = 26 - for i, a in enumerate(attrs): - _class_id_counter += 1 - add_cell(cells, f"{cid}_attr{i}", cid, cell_style("class_attr"), a, 0, ay, w, line_h) - ay += line_h - if not attrs: - _class_id_counter += 1 - add_cell(cells, f"{cid}_attr0", cid, cell_style("class_attr"), "", 0, ay, w, line_h) - ay += line_h - # separator - _class_id_counter += 1 - add_cell(cells, f"{cid}_sep2", cid, cell_style("rectangle","fillColor=none;strokeColor=none;"), "", 0, ay, w, 0) - ay += sep - # methods - for i, m in enumerate(methods): - _class_id_counter += 1 - add_cell(cells, f"{cid}_meth{i}", cid, cell_style("class_attr"), m, 0, ay, w, line_h) - ay += line_h - if not methods: - _class_id_counter += 1 - add_cell(cells, f"{cid}_meth0", cid, cell_style("class_attr"), "", 0, ay, w, line_h) - return cid - -# ============================================================ -# 图1:类图 — 声源分析模块 -# ============================================================ -def build_class_diagram(): - cells = [] - # title - add_cell(cells, "title1", "1", cell_style("title"), - "图1 类图 — 声源分析模块核心类结构(静态视角)", 20, 10, 600, 30) - - # Pipeline (center) - add_uml_class(cells, "Pipeline", "Pipeline", - ["-impl_: Impl*"], - ["+Process(audio): AcousticFrame", "+FromYaml(path): PipelineConfig", "+Reset()", "+Config(): const PipelineConfig&"], - 420, 80, 260, 160) - - # AudioBuffer - add_uml_class(cells, "AudioBuffer", "AudioBuffer", - ["-capacity_frames_: size_t", "-num_channels_: size_t", "-buffer_: vector<float>", "-head_, tail_, size_: size_t"], - ["+Push(samples): size_t", "+Pop(n): vector<float>", "+Get(off,n): vector<float>", "+Size(): size_t", "+Clear()"], - 40, 80, 200, 170) - - # FeatureExtractor - add_uml_class(cells, "FeatureExtractor", "FeatureExtractor", - ["-impl_: Impl*"], - ["+MelSpectrogram(audio): MatrixXf", "+MelSpectrogramMultiChannel(audio,n): vector<MatrixXf>"], - 40, 300, 220, 110) - - # GunshotClassifier - add_uml_class(cells, "GunshotClassifier", "GunshotClassifier", - ["-session_: Ort::Session*", "-env_: Ort::Env*", "-labels_: vector<string>"], - ["+Predict(mel): pair<string,float>", "+Labels(): const vector<string>&"], - 300, 300, 220, 110) - - # GccPhatLocalizer - add_uml_class(cells, "GccPhatLocalizer", "GccPhatLocalizer", - ["-mic_config_: MicArrayConfig", "-sample_rate_: int", "-max_tdoa_: float"], - ["+Localize(audio_mat): pair<float,float>"], - 560, 300, 200, 90) - - # DistanceEstimator - add_uml_class(cells, "DistanceEstimator", "DistanceEstimator", - ["-config_: DistanceConfig", "-kalman_state_: float"], - ["+ComputeSpl(audio): float", "+Estimate(spl,label): float", "+UpdateKalman(d): float", "+Reset()"], - 800, 300, 220, 110) - - # ThreatTracker - add_uml_class(cells, "ThreatTracker", "ThreatTracker", - ["-min_interval_: float", "-history_: vector<AcousticThreat>"], - ["+Update(threats): vector<AcousticThreat>", "+Reset()"], - 800, 80, 200, 90) - - # AcousticNode (ROS) - add_uml_class(cells, "AcousticNode", "AcousticNode (ROS层)", - ["-nh_, pnh_: NodeHandle", "-pipeline_: Pipeline*", "-source_type_: string"], - ["+run()", "-on_mic_array_audio(msg)", "-process_wav_source()", "-load_params()"], - 40, 480, 240, 120) - - # WavFileSource - add_uml_class(cells, "WavFileSource", "WavFileSource", - ["-file_path_: string", "-sample_rate_: int", "-num_channels_: int"], - ["+open(): bool", "+read(audio,n): size_t", "+num_channels(): int"], - 340, 480, 200, 100) - - # Types - add_uml_class(cells, "AcousticThreat", "AcousticThreat (struct)", - ["+timestamp: Timestamp", "+threat_id: string", "+sound_type: string", "+confidence: float", "+azimuth: float", "+elevation: float", "+distance: float"], - [], - 600, 480, 220, 130) - - # PipelineConfig - add_uml_class(cells, "PipelineConfig", "PipelineConfig (struct)", - ["+sample_rate: uint32_t", "+chunk_duration: float", "+n_mels: uint32_t", "+classifier: ClassifierConfig", "+mic_array: MicArrayConfig", "+distance: DistanceConfig"], - [], - 860, 480, 220, 130) - - # Relationships (edges) - # Pipeline --diamond--> components - add_cell(cells, "e1", "1", cell_style("diamond"), "", edge=True, source="Pipeline", target="AudioBuffer") - add_cell(cells, "e2", "1", cell_style("diamond"), "", edge=True, source="Pipeline", target="FeatureExtractor") - add_cell(cells, "e3", "1", cell_style("diamond"), "", edge=True, source="Pipeline", target="GunshotClassifier") - add_cell(cells, "e4", "1", cell_style("diamond"), "", edge=True, source="Pipeline", target="GccPhatLocalizer") - add_cell(cells, "e5", "1", cell_style("diamond"), "", edge=True, source="Pipeline", target="DistanceEstimator") - add_cell(cells, "e6", "1", cell_style("diamond"), "", edge=True, source="Pipeline", target="ThreatTracker") - - # AcousticNode --> Pipeline - add_cell(cells, "e7", "1", cell_style("open_arrow"), "", edge=True, source="AcousticNode", target="Pipeline") - - # AcousticNode --> WavFileSource - add_cell(cells, "e8", "1", cell_style("open_arrow"), "", edge=True, source="AcousticNode", target="WavFileSource") - - # Pipeline --> PipelineConfig (dependency) - add_cell(cells, "e9", "1", cell_style("dashed_arrow"), "uses", edge=True, source="Pipeline", target="PipelineConfig") - - # ThreatTracker --> AcousticThreat - add_cell(cells, "e10", "1", cell_style("open_arrow"), "", edge=True, source="ThreatTracker", target="AcousticThreat") - - # Labels for relationships - add_cell(cells, "l1", "1", cell_style("text"), "组合", 230, 90, 50, 20) - add_cell(cells, "l2", "1", cell_style("text"), "组合", 230, 310, 50, 20) - add_cell(cells, "l3", "1", cell_style("text"), "组合", 520, 310, 50, 20) - add_cell(cells, "l4", "1", cell_style("text"), "组合", 780, 310, 50, 20) - add_cell(cells, "l5", "1", cell_style("text"), "组合", 780, 100, 50, 20) - - return make_graph_model(cells, w=1200, h=700) - -# ============================================================ -# 图2:顺序图 — Pipeline::Process 调用链 -# ============================================================ -def build_sequence_diagram(): - cells = [] - add_cell(cells, "title2", "1", cell_style("title"), - "图2 顺序图 — Pipeline::Process 音频处理调用链(动态视角)", 20, 10, 700, 30) - - # Lifelines - lx = 60 - lifelines = [ - ("AcousticNode", lx), - ("Pipeline", lx+180), - ("AudioBuffer", lx+340), - ("FeatureExtractor", lx+500), - ("GunshotClassifier", lx+660), - ("GccPhatLocalizer", lx+820), - ("DistanceEstimator", lx+980), - ("ThreatTracker", lx+1140), - ("ThreatPublisher", lx+1300), - ] - - for name, x in lifelines: - add_cell(cells, f"ll_{name}", "1", cell_style("lifeline"), name, x, 60, 100, 520) - - # Messages - y = 100 - def msg(cid, src, tgt, text, ypos, dashed=False): - style = cell_style("return") if dashed else cell_style("message") - add_cell(cells, cid, "1", style, text, edge=True, source=f"ll_{src}", target=f"ll_{tgt}") - # label - add_cell(cells, f"{cid}_lab", "1", cell_style("text"), text, - (lifelines_dict[src] + lifelines_dict[tgt])//2 - 60, ypos-15, 120, 20) - - lifelines_dict = {name:x for name,x in lifelines} - - msg("m1", "AcousticNode", "Pipeline", "Process(audio_samples)", y) - y += 50 - msg("m2", "Pipeline", "AudioBuffer", "Push(samples)", y) - y += 40 - msg("m3", "Pipeline", "AudioBuffer", "Get(offset, chunk)", y) - y += 40 - msg("m4", "Pipeline", "FeatureExtractor", "MelSpectrogramMultiChannel(...)", y) - y += 40 - msg("m5", "Pipeline", "GunshotClassifier", "Predict(avg_mel)", y) - y += 40 - msg("m6", "Pipeline", "GccPhatLocalizer", "Localize(audio_mat)", y) - y += 40 - msg("m7", "Pipeline", "DistanceEstimator", "Estimate(spl, label)", y) - y += 40 - msg("m8", "Pipeline", "ThreatTracker", "Update(threat)", y) - y += 40 - msg("m9", "Pipeline", "AcousticNode", "return AcousticFrame", y, dashed=True) - y += 40 - msg("m10", "AcousticNode", "ThreatPublisher", "Publish(frame)", y) - - # activation bars (simplified as small rectangles) - for name, x in lifelines: - add_cell(cells, f"act_{name}", "1", cell_style("activation2"), "", x+40, 100, 20, y-60) - - # note - add_cell(cells, "note1", "1", cell_style("boundary"), - "设计思路:每个调用都是同步阻塞调用,数据沿调用链逐层传递。这种设计确保了单线程内数据一致性,简化了实时系统的并发控制。", - 40, y+40, 400, 60) - - return make_graph_model(cells, w=1500, h=700) - -# ============================================================ -# 图3:组件图 — 声源分析模块分层 -# ============================================================ -def build_component_diagram(): - cells = [] - add_cell(cells, "title3", "1", cell_style("title"), - "图3 组件图 — 声源分析模块分层组件结构", 20, 10, 600, 30) - - # Core component - add_cell(cells, "comp_core", "1", cell_style("component"), - "«component»\nacoustic_analyzer_core\n\n• AudioBuffer\n• FeatureExtractor\n• GunshotClassifier\n• GccPhatLocalizer\n• DistanceEstimator\n• ThreatTracker\n• Pipeline", - 80, 80, 220, 200) - - # IO component - add_cell(cells, "comp_io", "1", cell_style("component"), - "«component»\nacoustic_analyzer_io\n\n• AudioSource (interface)\n• WavFileSource\n• MobilePhoneSource", - 80, 320, 220, 120) - - # ROS component - add_cell(cells, "comp_ros", "1", cell_style("component"), - "«component»\nacoustic_analyzer_ros\n\n• AcousticNode\n• ThreatPublisher", - 400, 80, 200, 100) - - # External libraries - add_cell(cells, "ext_onnx", "1", cell_style("artifact"), - "«library»\nONNX Runtime", 400, 220, 140, 60) - add_cell(cells, "ext_eigen", "1", cell_style("artifact"), - "«library»\nEigen 3.4.0", 400, 300, 140, 60) - add_cell(cells, "ext_yaml", "1", cell_style("artifact"), - "«library»\nyaml-cpp", 400, 380, 140, 60) - add_cell(cells, "ext_ros", "1", cell_style("artifact"), - "«framework»\nROS (roscpp)", 680, 80, 140, 60) - - # Dependencies - add_cell(cells, "d1", "1", cell_style("open_arrow"), "", edge=True, source="comp_ros", target="comp_core") - add_cell(cells, "d2", "1", cell_style("open_arrow"), "", edge=True, source="comp_ros", target="comp_io") - add_cell(cells, "d3", "1", cell_style("dashed_arrow"), "uses", edge=True, source="comp_core", target="ext_onnx") - add_cell(cells, "d4", "1", cell_style("dashed_arrow"), "uses", edge=True, source="comp_core", target="ext_eigen") - add_cell(cells, "d5", "1", cell_style("dashed_arrow"), "uses", edge=True, source="comp_core", target="ext_yaml") - add_cell(cells, "d6", "1", cell_style("dashed_arrow"), "uses", edge=True, source="comp_ros", target="ext_ros") - - # Interface port - add_cell(cells, "iface1", "1", cell_style("interface"), "IAcousticSource", 300, 350, 40, 30) - add_cell(cells, "d7", "1", cell_style("open_arrow"), "", edge=True, source="comp_io", target="iface1") - add_cell(cells, "d8", "1", cell_style("open_arrow"), "", edge=True, source="iface1", target="comp_core") - - # note - add_cell(cells, "note3", "1", cell_style("boundary"), - "设计思路:core 层仅依赖第三方数学库(Eigen/ONNX),完全不依赖 ROS 和 yaml-cpp。\n这使得核心算法可以在 Windows/Linux 上独立编译测试,实现跨平台复用。", - 680, 200, 320, 70) - - return make_graph_model(cells, w=1100, h=520) - -# ============================================================ -# 图4:用例图 — 单兵终端APP -# ============================================================ -def build_usecase_diagram(): - cells = [] - add_cell(cells, "title4", "1", cell_style("title"), - "图4 用例图 — 单兵终端APP功能需求(用户视角)", 20, 10, 600, 30) - - # Actor - add_cell(cells, "actor", "1", cell_style("actor"), "前线士兵", 60, 200, 40, 80) - - # System boundary - add_cell(cells, "boundary", "1", cell_style("rectangle","fillColor=#f5f5f5;strokeColor=#666;"), - "单兵终端APP", 150, 80, 500, 420) - - usecases = [ - ("UC1", "登录认证", 260, 120), - ("UC2", "上报物资需求", 400, 120), - ("UC3", "选择投放点", 260, 200), - ("UC4", "查看任务状态", 400, 200), - ("UC5", "查看无人机状态", 260, 280), - ("UC6", "实时位置上报", 400, 280), - ("UC7", "SOS一键求救", 260, 360), - ("UC8", "服务器配置", 400, 360), - ] - - for uid, label, x, y in usecases: - add_cell(cells, uid, "1", cell_style("usecase"), label, x, y, 120, 50) - - # Include / Extend relationships - add_cell(cells, "inc1", "1", cell_style("dashed_arrow"), "«include»", edge=True, source="UC2", target="UC3") - add_cell(cells, "ext1", "1", cell_style("dashed_arrow"), "«extend»", edge=True, source="UC7", target="UC6") - - # Actor connections - for uid in ["UC1","UC2","UC3","UC4","UC5","UC6","UC7","UC8"]: - add_cell(cells, f"a_{uid}", "1", cell_style("arrow"), "", edge=True, source="actor", target=uid) - - # note - add_cell(cells, "note4", "1", cell_style("boundary"), - "设计思路:用例图从用户视角描述了系统功能边界。\n"+ - "「上报物资需求」包含「选择投放点」(必须),\n"+ - "「SOS求救」扩展「实时位置上报」(自动触发位置发送)。", - 700, 120, 280, 80) - - return make_graph_model(cells, w=1100, h=600) - -# ============================================================ -# 图5:活动图 — 物资需求上报流程 -# ============================================================ -def build_activity_diagram(): - cells = [] - add_cell(cells, "title5", "1", cell_style("title"), - "图5 活动图 — 物资需求上报业务流程(行为视角)", 20, 10, 600, 30) - - y = 60 - # start - add_cell(cells, "a_start", "1", cell_style("start"), "", 400, y, 20, 20) - y += 40 - add_cell(cells, "a_login", "1", cell_style("activity"), "登录认证", 360, y, 100, 40) - y += 60 - add_cell(cells, "a_home", "1", cell_style("activity"), "进入首页", 360, y, 100, 40) - y += 60 - add_cell(cells, "a_dec1", "1", cell_style("decision"), "选择功能", 360, y, 100, 50) - y += 70 - - # Branch: submit demand - add_cell(cells, "a_type", "1", cell_style("activity"), "选择物资类型", 180, y, 120, 40) - add_cell(cells, "a_demand", "1", cell_style("activity"), "输入数量/紧急程度", 340, y, 140, 40) - add_cell(cells, "a_drop", "1", cell_style("activity"), "查看推荐投放点", 520, y, 140, 40) - add_cell(cells, "a_map", "1", cell_style("activity"), "地图选点/搜索", 700, y, 140, 40) - - # Edges from decision - add_cell(cells, "e_d1", "1", cell_style("arrow"), "上报需求", edge=True, source="a_dec1", target="a_type") - add_cell(cells, "e_d2", "1", cell_style("arrow"), "", edge=True, source="a_type", target="a_demand") - add_cell(cells, "e_d3", "1", cell_style("arrow"), "", edge=True, source="a_demand", target="a_drop") - add_cell(cells, "e_d4", "1", cell_style("arrow"), "", edge=True, source="a_drop", target="a_map") - - y += 60 - add_cell(cells, "a_confirm", "1", cell_style("decision"), "确认提交?", 360, y, 100, 50) - add_cell(cells, "e_d5", "1", cell_style("arrow"), "", edge=True, source="a_map", target="a_confirm") - - y += 70 - add_cell(cells, "a_submit", "1", cell_style("activity"), "调用 API.postDemand()", 340, y, 140, 40) - add_cell(cells, "a_cancel", "1", cell_style("activity"), "返回修改", 560, y, 100, 40) - add_cell(cells, "e_yes", "1", cell_style("arrow"), "是", edge=True, source="a_confirm", target="a_submit") - add_cell(cells, "e_no", "1", cell_style("arrow"), "否", edge=True, source="a_confirm", target="a_cancel") - add_cell(cells, "e_back", "1", cell_style("arrow"), "", edge=True, source="a_cancel", target="a_type") - - y += 60 - add_cell(cells, "a_dec2", "1", cell_style("decision"), "提交成功?", 360, y, 100, 50) - add_cell(cells, "e_d6", "1", cell_style("arrow"), "", edge=True, source="a_submit", target="a_dec2") - - y += 70 - add_cell(cells, "a_success", "1", cell_style("activity"), "显示成功提示\n跳转首页", 180, y, 140, 50) - add_cell(cells, "a_fail", "1", cell_style("activity"), "显示错误提示\n支持重试", 520, y, 140, 50) - add_cell(cells, "e_ok", "1", cell_style("arrow"), "成功", edge=True, source="a_dec2", target="a_success") - add_cell(cells, "e_err", "1", cell_style("arrow"), "失败", edge=True, source="a_dec2", target="a_fail") - - y += 70 - # merge - add_cell(cells, "a_merge", "1", cell_style("activity"), "结束", 360, y, 100, 40) - add_cell(cells, "e_m1", "1", cell_style("arrow"), "", edge=True, source="a_success", target="a_merge") - add_cell(cells, "e_m2", "1", cell_style("arrow"), "", edge=True, source="a_fail", target="a_merge") - - y += 60 - add_cell(cells, "a_end", "1", cell_style("end"), "", 400, y, 20, 20) - add_cell(cells, "e_end", "1", cell_style("arrow"), "", edge=True, source="a_merge", target="a_end") - - # Swimlane labels - add_cell(cells, "sw1", "1", cell_style("text","fontStyle=1;fontSize=12;"), "【士兵操作】", 40, 80, 100, 20) - add_cell(cells, "sw2", "1", cell_style("text","fontStyle=1;fontSize=12;"), "【APP处理】", 40, 200, 100, 20) - add_cell(cells, "sw3", "1", cell_style("text","fontStyle=1;fontSize=12;"), "【后端交互】", 40, 400, 100, 20) - - # note - add_cell(cells, "note5", "1", cell_style("boundary"), - "设计思路:活动图展示了物资需求上报的完整业务流程。\n"+ - "关键设计:① 投放点选择支持「列表推荐」和「地图自由选点」两种模式;\n"+ - "② API 调用失败时返回模拟数据(Mock),保证演示可用性;\n"+ - "③ 全流程有明确的成功/失败分支和回退路径。", - 40, 520, 420, 80) - - return make_graph_model(cells, w=900, h=700) - -# ============================================================ -# 图6:部署图 — 系统物理部署 -# ============================================================ -def build_deployment_diagram(): - cells = [] - add_cell(cells, "title6", "1", cell_style("title"), - "图6 部署图 — 智途投送系统物理部署拓扑", 20, 10, 600, 30) - - # Node 1: Soldier Phone - add_cell(cells, "node_phone", "1", cell_style("node"), - "«device»\n士兵手机\nAndroid 12+", 60, 80, 180, 120) - add_cell(cells, "art_app", "1", cell_style("artifact"), - "单兵终端APP\n(Capacitor/WebView)", 80, 130, 140, 50) - - # Node 2: Server - add_cell(cells, "node_server", "1", cell_style("node"), - "«device»\n后方指挥所服务器\nUbuntu 22.04", 340, 80, 220, 140) - add_cell(cells, "art_flask", "1", cell_style("artifact"), - "Flask 后端\n(Python 3.10)", 360, 130, 180, 40) - add_cell(cells, "art_web", "1", cell_style("artifact"), - "Web 监控界面\n(HTML/JS/Leaflet)", 360, 180, 180, 30) - - # Node 3: UAV - add_cell(cells, "node_uav", "1", cell_style("node"), - "«device»\n无人机机载计算机\nUbuntu 20.04 + ROS Noetic", 660, 80, 240, 180) - add_cell(cells, "art_ros", "1", cell_style("artifact"), - "ROS 节点网络\n(roscore + 多节点)", 680, 130, 200, 40) - add_cell(cells, "art_acoustic", "1", cell_style("artifact"), - "声源分析模块\n(C++17 / ONNX)", 680, 180, 200, 40) - add_cell(cells, "art_other", "1", cell_style("artifact"), - "视觉/热成像/路径规划节点", 680, 230, 200, 30) - - # Communication links - add_cell(cells, "c1", "1", cell_style("arrow"), - "HTTP/REST\n(4G/WiFi)", edge=True, source="node_phone", target="node_server") - add_cell(cells, "c2", "1", cell_style("arrow"), - "ROS Topic\n(WebSocket/局域网)", edge=True, source="node_server", target="node_uav") - add_cell(cells, "c3", "1", cell_style("dashed_arrow"), - "rosbridge\n(可选直连)", edge=True, source="node_phone", target="node_uav") - - # Hardware inside UAV - add_cell(cells, "hw_mic", "1", cell_style("rectangle","fillColor=#e0e0e0;"), - "麦克风阵列", 700, 300, 80, 40) - add_cell(cells, "hw_cam", "1", cell_style("rectangle","fillColor=#e0e0e0;"), - "可见光相机", 800, 300, 80, 40) - add_cell(cells, "hw_gps", "1", cell_style("rectangle","fillColor=#e0e0e0;"), - "GPS/IMU", 700, 360, 80, 40) - - add_cell(cells, "c_hw1", "1", cell_style("arrow"), "", edge=True, source="hw_mic", target="node_uav") - add_cell(cells, "c_hw2", "1", cell_style("arrow"), "", edge=True, source="hw_cam", target="node_uav") - add_cell(cells, "c_hw3", "1", cell_style("arrow"), "", edge=True, source="hw_gps", target="node_uav") - - # note - add_cell(cells, "note6", "1", cell_style("boundary"), - "设计思路:部署图展示了系统的物理分布和通信路径。\n"+ - "关键设计:① 单兵APP通过 4G/WiFi 与后方服务器通信,不直接依赖无人机;\n"+ - "② 服务器作为「消息中转站」,解耦了前线士兵与无人机控制;\n"+ - "③ 无人机机载端运行 Ubuntu+ROS,通过局域网与机载传感器直连。", - 60, 400, 520, 80) - - return make_graph_model(cells, w=1000, h=560) - -# ============================================================ -# Main -# ============================================================ -pages = [ - ("01-类图-声源分析模块", build_class_diagram()), - ("02-顺序图-Pipeline调用链", build_sequence_diagram()), - ("03-组件图-声源分析分层", build_component_diagram()), - ("04-用例图-单兵终端APP", build_usecase_diagram()), - ("05-活动图-物资需求上报", build_activity_diagram()), - ("06-部署图-系统物理拓扑", build_deployment_diagram()), -] - -root = make_mxfile(pages) - -# Pretty print -def indent(elem, level=0): - i = "\n" + level*" " - if len(elem): - if not elem.text or not elem.text.strip(): - elem.text = i + " " - if not elem.tail or not elem.tail.strip(): - elem.tail = i - for child in elem: - indent(child, level+1) - if not child.tail or not child.tail.strip(): - child.tail = i - else: - if level and (not elem.tail or not elem.tail.strip()): - elem.tail = i - -indent(root) - -tree = ET.ElementTree(root) -tree.write("uml_diagrams.drawio", encoding="utf-8", xml_declaration=True) -print("Generated: uml_diagrams.drawio (6 pages)") diff --git a/diagrams/uml_diagrams.drawio b/diagrams/uml_diagrams.drawio deleted file mode 100644 index acc3acc1..00000000 --- a/diagrams/uml_diagrams.drawio +++ /dev/null @@ -1,911 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -