diff --git a/README.md b/README.md new file mode 100644 index 00000000..a44d1163 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# software + diff --git a/architecture_diagrams.html b/architecture_diagrams.html new file mode 100644 index 00000000..4604f173 --- /dev/null +++ b/architecture_diagrams.html @@ -0,0 +1,516 @@ + + + + +智途投送系统 - 软件体系结构图 + + + + +

智途投送系统 — 软件体系结构图

+

按《软件体系结构》课程规范绘制 | 管道-过滤器 / 分层 / 客户端-服务器 / 发布-订阅

+ + +
+
图1 系统整体体系结构 — 分层 + 客户端-服务器风格
+ + + + + + + + + + + + + + 单兵终端APP(客户端) + Client-Server 架构 / 分层结构 + + + + 后勤保障系统(服务器端) + Layered 分层架构 / REST API + + + + 无人机软件系统 + Pipe-Filter + Pub-Sub 混合架构 + + + + 表示层 (Presentation) + UI界面 / 地图 / 交互控件 + + + 业务逻辑层 (Service) + 需求上报 / 位置同步 / 策略选择 + + + 数据访问层 (Data Access) + REST API调用 / 本地存储 + + + 硬件抽象层 (HAL) + GPS / 网络 / 摄像头 + + + + + + + + + 表示层 (Web前端) + HTML/CSS/JS / 地图可视化 + + + 应用层 (Flask API) + REST路由 / 任务调度 / 身份认证 + + + 业务逻辑层 (Services) + 路径规划 / 资源分配 / 威胁融合 + + + 数据层 (Data) + SQLite / 内存数据 / 日志 + + + + + + + + + ROS 节点层 (Pub-Sub) + 威胁发布 / 航点订阅 / 状态广播 + + + 感知流水线层 (Pipe-Filter) + 声学/视觉/热成像 → 威胁地图 + + + 算法核心层 (Core) + GCC-PHAT / CNN-GRU / SPL + + + 硬件接口层 (Drivers) + 麦克风 / 相机 / IMU / GPS + + + + + + + + + + HTTP/REST + + + ROS/WebSocket + + + + rosbridge / WebSocket(间接通信,经由后勤保障系统或直接) + +

+ 设计说明:三大子系统各自内部采用分层架构,子系统间采用客户端-服务器风格交互。 + 上层(单兵APP、Web前端)通过 REST API 与后勤保障系统通信;后勤保障系统通过 ROS 网络与无人机交互。 + 分层架构的约束:每一层只使用直接下层提供的服务,层与层之间通过定义好的接口协议交互。 +

+
+ + +
+
图2 声源分析模块 — 管道-过滤器体系结构(我负责部分)
+ + + + + + + + + + + + + + 数据流:原始音频 → [缓冲 → 特征 → 分类 → 定位/测距 → 跟踪] → 威胁事件 + + + + + 数据源 + 麦克风阵列 + WAV文件 + + + + Filter 1 + AudioBuffer + 循环缓冲区 + 滑动窗口管理 + + + + Filter 2 + FeatureExtractor + Mel频谱图提取 + FFT + Mel滤波器组 + + + + Filter 3 + GunshotClassifier + CNN-GRU分类 + ONNX Runtime推理 + + + + Filter 4a + GccPhatLocalizer + GCC-PHAT定位 + 时延估计 → 方位角/俯仰角 + + + + Filter 4b + DistanceEstimator + SPL距离估计 + 声压级衰减 + Kalman滤波 + + + + Filter 5 + ThreatTracker + 威胁跟踪/去重 + 时域去重 + ID分配 + + + + 数据汇点 + AcousticThreat 事件 + + + + + float[] + + + + float[] + + + + MatrixXf + + + + label+conf + + + + label + + + + azimuth,elevation + + + + distance + + + + AcousticFrame + + + + VAD门控 + 能量+过零率检测 + + + + + 时序平滑 + 滑动窗口平均 + + + + + 过滤器组件 + (独立处理单元) + + + 数据源/汇点 + (系统边界) + + + 管道 (数据流) + +

+ 管道-过滤器风格的四个核心特征在本模块的体现:
+ ① 过滤器独立性:每个 Filter(如 FeatureExtractor、GunshotClassifier)都是独立的类,不与其他 Filter 共享状态;
+ ② 数据流驱动:音频数据沿管道单向流动,无循环(符合「不允许出现环」的约束);
+ ③ 局部变换:每个 Filter 只负责一种局部变换(时域→频域→概率→方位→距离);
+ ④ 黑盒复用:GunshotClassifier 可独立替换为其他模型(如 Transformer),只要输入输出 Mel 频谱图格式不变。 +

+
+ + +
+
图3 声源分析模块 — 分层体系结构(静态视角)
+ + + + + + + + + + Layer 3:应用/集成层 (Application/Integration) + ROS 包装器 / 节点生命周期管理 / 话题发布订阅 + acoustic_node.cpp | threat_publisher.cpp + + + Layer 2:业务逻辑层 (Business Logic / Pipeline) + 流水线编排 (Pipeline) / 配置管理 / 数据类型定义 + pipeline.cpp | types.h | 配置解析 (YAML) + + + Layer 1:算法核心层 (Algorithm Core) + 音频缓冲 (AudioBuffer) | 特征提取 (FeatureExtractor) | 分类器 (GunshotClassifier) + 声源定位 (GccPhatLocalizer) | 距离估计 (DistanceEstimator) | 威胁跟踪 (ThreatTracker) + FFT工具 (fft_utils) | IO适配 (WavFileSource, MobilePhoneSource) + + + + + + + + 上层 → 下层:只允许向下依赖(依赖倒置原则) + core 层完全不依赖 ROS / yaml-cpp / 操作系统 + + + + Ubuntu (实机部署) + BUILD_ROS_WRAPPER=ON + ROS节点 + 麦克风阵列驱动 + + + Windows / Linux (仿真调试) + BUILD_ROS_WRAPPER=OFF + 离线WAV测试 + 单元测试 + + + + + 分层架构约束:① 每一层只使用直接下层的服务 ② 层间通过接口交互,不直接访问实现 ③ 下层修改不影响上层(只要接口不变) + + +

+ 设计说明:通过分层隔离,算法核心层(Layer 1)完全与 ROS、操作系统解耦。 + 这意味着同一套 C++ 声学算法既可以在 Ubuntu 上作为 ROS 节点运行(控制真实无人机), + 也可以在 Windows 上编译为独立可执行文件做离线仿真测试,实现了"一次开发,多处部署"。 +

+
+ + +
+
图4 无人机感知系统 — 发布-订阅(Pub-Sub)运行时交互
+ + + + + + + + + + ROS Master + 节点注册 / 话题匹配 + + + + ROS Topic 总线(异步消息通道) + /microphone_array/audio | /acoustic_threat | /threat_map | /dynamic_waypoints + + + + Publisher + 麦克风阵列驱动节点 + 发布:/microphone_array/audio + 数据类型:Float32MultiArray + + + + Publisher(我负责) + 声学分析节点 (acoustic_node) + 订阅:/microphone_array/audio + 发布:/acoustic_threat + + + + Subscriber + Publisher + 多模态融合节点 + 订阅:/acoustic_threat + /vision_threat + 发布:/threat_map + + + + Subscriber + 动态路径规划节点 + 订阅:/threat_map + 发布:/dynamic_waypoints + + + + + + 音频数据流 + + + + + AcousticThreat 事件 + + + + + 融合后的 ThreatMap + + + + + 发布-订阅特征:① 发布者与订阅者完全解耦,互不知道对方存在 ② 支持一对多广播(一个威胁事件可被多个消费者处理) ③ 异步非阻塞,适合实时感知系统的低延迟要求 + + +

+ 设计说明:发布-订阅风格使得声学分析节点无需关心"谁会使用威胁结果"。 + 在仿真阶段,可以订阅 /acoustic_threat 做可视化验证;在实机阶段,同一个话题被多模态融合节点订阅。 + 这种「隐式调用」机制大幅降低了系统各模块间的耦合度。 +

+
+ + +
+
图5 PIMPL 惯用法 — 信息隐藏与编译隔离
+ + + + + + + + + + 公开头文件 (Public API) + pipeline.h / feature_extractor.h + + + class Pipeline { + public: + Process(audio) + FromYaml(path) + Reset() + private: + Impl* impl_; + }; + + + + 实现文件 (Private Implementation) + pipeline.cpp + + + struct Pipeline::Impl { + AudioBuffer* + FeatureExtractor* + GunshotClassifier* + GccPhatLocalizer* + ... + }; + + + + 编译依赖 + (.cpp 包含所有头文件) + + + 无反向依赖(接口不感知实现细节) + + + + 编译防火墙 + + + + + 效果:修改 Impl 内部(如替换 FFT 库、新增滤波器)→ 无需重新编译依赖 pipeline.h 的其他模块 → 大幅缩短编译时间,降低模块耦合 + + +

+ 设计说明:PIMPL(Pointer to Implementation)是 C++ 中实现「信息隐藏」的经典惯用法。 + 它将类的实现细节完全移入 .cpp 文件,头文件中只暴露接口指针。 + 这符合软件体系结构「接口与实现分离」的原则,也提升了系统的可修改性质量属性。 +

+
+ +
+ 智途投送系统 — 软件体系结构汇报用图 | 绘制规范参考《软件体系结构》课程 +
+ + + diff --git a/diagrams/UML图设计说明.md b/diagrams/UML图设计说明.md new file mode 100644 index 00000000..ca54ccce --- /dev/null +++ b/diagrams/UML图设计说明.md @@ -0,0 +1,250 @@ +# 智途投送系统 — 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 new file mode 100644 index 00000000..b3e17f5b --- /dev/null +++ b/diagrams/generate_uml.py @@ -0,0 +1,591 @@ +#!/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 new file mode 100644 index 00000000..acc3acc1 --- /dev/null +++ b/diagrams/uml_diagrams.drawio @@ -0,0 +1,911 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/document/vx.json b/document/vx.json new file mode 100644 index 00000000..abf7fb58 --- /dev/null +++ b/document/vx.json @@ -0,0 +1,11 @@ +{ + "created_time": "2026-04-20T02:11:45Z", + "files": [ + ], + "folders": [ + ], + "id": "1542", + "modified_time": "2026-04-20T02:11:45Z", + "signature": "4031563627892149089", + "version": 3 +} diff --git a/document/第四组软件构思.docx b/document/第四组软件构思.docx new file mode 100644 index 00000000..a2bcf86d Binary files /dev/null and b/document/第四组软件构思.docx differ diff --git a/document/第四组软件需求规格说明书.docx b/document/第四组软件需求规格说明书.docx new file mode 100644 index 00000000..6ef326de Binary files /dev/null and b/document/第四组软件需求规格说明书.docx differ diff --git a/fix_server.py b/fix_server.py new file mode 100644 index 00000000..427daa1b --- /dev/null +++ b/fix_server.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""服务器端修复脚本:解决内存计数器重启归零导致的主键冲突""" +import os, sys + +APP_PATH = "/opt/zhitu/app.py" +if not os.path.exists(APP_PATH): + print(f"错误:找不到 {APP_PATH}") + sys.exit(1) + +# 备份 +bak = APP_PATH + ".bak." + str(int(os.time())) +os.system(f"cp {APP_PATH} {bak}") +print(f"已备份: {bak}") + +with open(APP_PATH, "r", encoding="utf-8") as f: + s = f.read() + +# 1. 删除内存计数器 +s = s.replace( + "_demand_id_counter = 0\n_task_id_counter = 0\n_danger_id_counter = 0", + "# 计数器已从内存变量改为数据库查询,避免服务重启后主键冲突" +) + +# 2. 添加辅助函数 +helper = '''def _next_demand_id(): + """从数据库查询当前最大需求ID,生成下一个(避免内存计数器重启归零导致主键冲突)""" + conn = get_db() + row = conn.execute("SELECT id FROM demands WHERE id LIKE 'REQ-%' ORDER BY id DESC LIMIT 1").fetchone() + conn.close() + if row: + try: + num = int(row["id"].replace("REQ-", "")) + except ValueError: + num = 0 + else: + num = 0 + return "REQ-" + str(num + 1).zfill(3) + + +def _next_task_id(): + """从数据库查询当前最大任务ID,生成下一个""" + conn = get_db() + row = conn.execute("SELECT id FROM tasks WHERE id LIKE '#%' ORDER BY id DESC LIMIT 1").fetchone() + conn.close() + if row: + try: + num = int(row["id"].replace("#", "")) + except ValueError: + num = 0 + else: + num = 0 + return "#" + str(num + 1).zfill(3) + +''' + +marker = "# ===== REST API: 物资需求(单兵APP) =====" +if marker in s and "_next_demand_id" not in s: + s = s.replace(marker, helper + marker) + +# 3. 修改 post_demand +s = s.replace( + " global _demand_id_counter\n data = request.get_json(force=True)\n _demand_id_counter += 1", + " data = request.get_json(force=True)" +) +s = s.replace( + ' demand_id = "REQ-" + str(_demand_id_counter).zfill(3)', + ' demand_id = _next_demand_id()' +) + +# 4. 修改 add_danger_zone +s = s.replace( + ''' global _danger_id_counter + data = request.get_json(force=True) + _danger_id_counter += 1 + zone = { + "id": _danger_id_counter,''', + ''' data = request.get_json(force=True) + zone = {''' +) +s = s.replace( + ''' conn.execute(''' + INSERT INTO danger_zones (lat, lng, radius, description, created_at) + VALUES (?, ?, ?, ?, ?) + ''', (zone["lat"], zone["lng"], zone["radius"], zone["description"], zone["created_at"])) + conn.commit() + conn.close() + return jsonify({"ok": True, "id": zone["id"]})''', + ''' cur = conn.execute(''' + INSERT INTO danger_zones (lat, lng, radius, description, created_at) + VALUES (?, ?, ?, ?, ?) + ''', (zone["lat"], zone["lng"], zone["radius"], zone["description"], zone["created_at"])) + zone_id = cur.lastrowid + conn.commit() + conn.close() + return jsonify({"ok": True, "id": zone_id})''' +) + +# 5. 修改 dispatch_task +s = s.replace( + ''' global _task_id_counter + data = request.get_json(force=True) + soldier_id = data.get("soldier_id", "unknown") + _task_id_counter += 1''', + ''' data = request.get_json(force=True) + soldier_id = data.get("soldier_id", "unknown")''' +) +s = s.replace( + ''' "id": "#" + str(_task_id_counter).zfill(3),''', + ''' "id": _next_task_id(),''' +) + +# 6. 修改 sos +s = s.replace( + ''' # 同时标记为危险区域 + global _danger_id_counter + _danger_id_counter += 1 + conn.execute('''', + ''' # 同时标记为危险区域 + conn.execute('''' +) + +with open(APP_PATH, "w", encoding="utf-8") as f: + f.write(s) + +# 语法检查 +result = os.system(f"python3 -m py_compile {APP_PATH}") +if result == 0: + print("✅ 修复完成,语法检查通过") +else: + print("❌ 语法检查失败,已恢复备份") + os.system(f"cp {bak} {APP_PATH}") + sys.exit(1) diff --git a/generate_blog_doc.js b/generate_blog_doc.js new file mode 100644 index 00000000..9449c9cd --- /dev/null +++ b/generate_blog_doc.js @@ -0,0 +1,287 @@ +const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, + HeadingLevel, AlignmentType, BorderStyle, WidthType, ShadingType, + PageBreak, Header, Footer, PageNumber } = require('docx'); +const fs = require('fs'); + +const border = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" }; +const borders = { top: border, bottom: border, left: border, right: border }; + +function cell(text, width, opts = {}) { + return new TableCell({ + borders, + width: { size: width, type: WidthType.DXA }, + shading: opts.shading ? { fill: opts.shading, type: ShadingType.CLEAR } : undefined, + margins: { top: 60, bottom: 60, left: 100, right: 100 }, + children: [new Paragraph({ + children: [new TextRun({ text, bold: opts.bold, size: 20, font: "微软雅黑" })] + })] + }); +} + +function h1(text) { + return new Paragraph({ + heading: HeadingLevel.HEADING_1, + children: [new TextRun({ text, bold: true, size: 32, font: "微软雅黑", color: "1a1a2e" })] + }); +} + +function h2(text) { + return new Paragraph({ + heading: HeadingLevel.HEADING_2, + children: [new TextRun({ text, bold: true, size: 28, font: "微软雅黑", color: "2d3436" })] + }); +} + +function h3(text) { + return new Paragraph({ + heading: HeadingLevel.HEADING_3, + children: [new TextRun({ text, bold: true, size: 24, font: "微软雅黑", color: "2d3436" })] + }); +} + +function body(text, opts = {}) { + return new Paragraph({ + children: [new TextRun({ text, size: 21, font: "微软雅黑", ...opts })], + spacing: { after: 120, line: 360 } + }); +} + +function quote(text) { + return new Paragraph({ + children: [new TextRun({ text, italics: true, size: 21, font: "微软雅黑", color: "636e72" })], + spacing: { after: 120, line: 360 }, + indent: { left: 400 } + }); +} + +function code(text) { + return new Paragraph({ + children: [new TextRun({ text, size: 18, font: "Consolas", color: "2d3436" })], + shading: { fill: "f5f6fa", type: ShadingType.CLEAR }, + spacing: { after: 60 } + }); +} + +const doc = new Document({ + styles: { + default: { document: { run: { font: "微软雅黑", size: 21 } } }, + paragraphStyles: [ + { id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true, + run: { size: 32, bold: true, font: "微软雅黑", color: "1a1a2e" }, + paragraph: { spacing: { before: 300, after: 200 }, outlineLevel: 0 } }, + { id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true, + run: { size: 28, bold: true, font: "微软雅黑", color: "2d3436" }, + paragraph: { spacing: { before: 240, after: 160 }, outlineLevel: 1 } }, + { id: "Heading3", name: "Heading 3", basedOn: "Normal", next: "Normal", quickFormat: true, + run: { size: 24, bold: true, font: "微软雅黑", color: "2d3436" }, + paragraph: { spacing: { before: 200, after: 120 }, outlineLevel: 2 } }, + ] + }, + sections: [{ + properties: { + page: { size: { width: 11906, height: 16838 }, margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } } + }, + headers: { + default: new Header({ children: [new Paragraph({ + children: [new TextRun({ text: "知识荟 · 技术博客", size: 16, color: "888888", font: "微软雅黑" })] + })] }) + }, + footers: { + default: new Footer({ children: [new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new TextRun({ children: ["第 ", PageNumber.CURRENT, " 页"], size: 16, color: "888888", font: "微软雅黑" })] + })] }) + }, + children: [ + // 标题 + new Paragraph({ spacing: { before: 600 } }), + new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new TextRun({ text: "基于 C++ 的无人机声源分析模块设计与实现", bold: true, size: 40, font: "微软雅黑", color: "1a1a2e" })] + }), + new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new TextRun({ text: "——「智途投送」战场末端补给系统的多模态感知实践", size: 26, font: "微软雅黑", color: "636e72" })], + spacing: { after: 400 } + }), + new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new TextRun({ text: "作者:国防科大计算机学院 23 级软件工程小班", size: 21, font: "微软雅黑", color: "636e72" })] + }), + new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new TextRun({ text: "2026 年 4 月", size: 21, font: "微软雅黑", color: "636e72" })], + spacing: { after: 600 } + }), + + // 摘要 + h1("摘要"), + body("在城市作战环境下,战场末端补给的「最后一公里」始终是制约作战效能的关键瓶颈。本文介绍「智途投送」软件系统中声源分析模块(Acoustic Analyzer)的设计与实现过程。该模块通过麦克风阵列采集音频信号,结合 GCC-PHAT 声源定位算法与 ONNX Runtime 神经网络推理引擎,实现了枪炮声的实时识别、方位估计与距离推算。模块采用 C++17 开发,遵循构件化设计原则,核心算法零 ROS 依赖,支持临时方案(手机单通道)与最终方案(麦克风阵列)的无缝切换。本文详细阐述了系统架构、核心算法、工程实践中的关键决策及踩坑记录,为同类嵌入式感知系统开发提供参考。"), + new Paragraph({ spacing: { before: 100 } }), + body("关键词:无人机;声源定位;GCC-PHAT;ONNX Runtime;构件化设计;战场感知", { bold: true }), + + // 一、项目背景 + h1("一、项目背景与需求分析"), + h2("1.1 战场末端补给的痛点"), + body("近年来多场局部战争的实战经验反复证明,后勤补给的「最后一公里」已成为制约战场持续作战能力的核心瓶颈。2023 年以哈战争中,以军在加沙城市巷战环境遭遇严重后勤困境;2022 年俄乌冲突中,前线部队长期面临弹药、食品、急救药品严重短缺。传统有人运输在最后几公里内频繁遭遇炮火覆盖,伤亡率极高。即便引入无人机等无人平台,也因缺乏统一调度、智能路径规划和安全投放策略,末端配送效率低下、协调困难。"), + body("针对这一现实挑战,我们团队设计开发了「智途投送」智能化末端补给系统,通过无人机替代有人运输,结合多模态感知与智能路径规划,实现战场物资的安全精准投送。"), + + h2("1.2 声源分析模块的定位"), + body("在「智途投送」系统的多模态感知架构中,声源分析模块与视觉分析模块、热成像分析模块并列,共同构成威胁感知层。其具体功能需求包括:"), + body("• 枪炮声识别:区分枪声、炮声、爆炸声与环境噪声"), + body("• 声源定位:通过麦克风阵列估计威胁源的方位角与俯仰角"), + body("• 距离估计:基于信号能量衰减模型推算威胁距离"), + body("• 威胁跟踪:对连续帧检测结果进行关联与生命周期管理"), + body("• 构件化设计:核心算法与 ROS 解耦,支持独立编译与测试"), + + // 二、系统架构 + h1("二、系统架构设计"), + h2("2.1 总体架构"), + body("模块采用三层架构设计,严格遵循「关注点分离」原则:"), + new Paragraph({ spacing: { before: 200 } }), + code("┌──────────────────────────────────────────┐"), + code("│ ROS 封装层(acoustic_node) │"), + code("│ - 话题订阅:/microphone_array/audio │"), + code("│ - 话题发布:/acoustic/threats │"), + code("├──────────────────────────────────────────┤"), + code("│ IO 抽象层(AudioSource 接口) │"), + code("│ - WavFileSource (离线测试) │"), + code("│ - MobilePhoneSource (临时方案)[TEMP] │"), + code("│ - MicArraySource (最终方案)[FINAL] │"), + code("├──────────────────────────────────────────┤"), + code("│ Core 算法层(零 ROS 依赖) │"), + code("│ Pipeline → {FeatureExtractor, │"), + code("│ GunshotClassifier, │"), + code("│ GccPhatLocalizer, │"), + code("│ DistanceEstimator, │"), + code("│ ThreatTracker} │"), + code("└──────────────────────────────────────────┘"), + new Paragraph({ spacing: { before: 200 } }), + + h2("2.2 构件化设计的工程意义"), + body("将 Core 层设计为纯 C++ 库(零 ROS 依赖)带来了显著的工程优势:"), + body("1. 可测试性:可在无 ROS 环境中运行单元测试,CI/CD 友好"), + body("2. 可移植性:未来迁移至 ROS2 时,仅需重写 ROS 层,Core 与 IO 层零改动"), + body("3. 可复用性:Core 库可独立部署于地面站笔记本,用于离线数据分析"), + body("4. 可分离性:满足课程项目对「构件化」的要求,模块边界清晰"), + + // 三、核心算法 + h1("三、核心算法详解"), + h2("3.1 Mel Spectrogram 特征提取"), + body("由于项目采用 C++ 开发,无法直接使用 Python 生态的 librosa 库。我们在 C++ 端从零实现了完整的 Mel Spectrogram 提取流程:"), + body("预加重 → 分帧加窗(Hamming)→ FFT(Kiss FFT)→ 功率谱 → Mel 滤波器组 → 对数压缩"), + body("实现中特别注意了与 Python librosa 的参数对齐:"), + new Table({ + width: { size: 9360, type: WidthType.DXA }, + columnWidths: [3000, 3000, 3360], + rows: [ + new TableRow({ children: [ + cell("参数", 3000, { bold: true, shading: "f0f2f5" }), + cell("C++ 值", 3000, { bold: true, shading: "f0f2f5" }), + cell("librosa 对应", 3360, { bold: true, shading: "f0f2f5" }) + ] }), + new TableRow({ children: [cell("sample_rate", 3000), cell("16000", 3000), cell("sr=16000", 3360)] }), + new TableRow({ children: [cell("n_fft", 3000), cell("2048", 3000), cell("n_fft=2048", 3360)] }), + new TableRow({ children: [cell("hop_length", 3000), cell("512", 3000), cell("hop_length=512", 3360)] }), + new TableRow({ children: [cell("n_mels", 3000), cell("64", 3000), cell("n_mels=64", 3360)] }), + new TableRow({ children: [cell("f_max", 3000), cell("8000.0", 3000), cell("fmax=8000", 3360)] }), + new TableRow({ children: [cell("preemphasis", 3000), cell("0.97", 3000), cell("coef=0.97", 3360)] }), + new TableRow({ children: [cell("window", 3000), cell("Hamming", 3000), cell("window='hamming'", 3360)] }), + new TableRow({ children: [cell("center", 3000), cell("false", 3000), cell("center=False", 3360)] }), + ] + }), + new Paragraph({ spacing: { before: 200 } }), + body("其中 center=false 是关键对齐点。librosa 默认 center=true(帧中心对齐),会导致帧数比 C++ 实现多 1-2 帧。我们在训练脚本中显式设置 center=False,并在 C++ 端使用 (n_samples - n_fft) / hop + 1 的帧数计算,确保训练-推理特征完全一致。"), + + h2("3.2 GCC-PHAT 声源定位"), + body("GCC-PHAT(Generalized Cross-Correlation with Phase Transform)是一种经典的时延估计(TDOA)算法。其核心思想是通过相位变换加权消除信号幅度的影响,仅保留相位信息用于互相关计算:"), + quote("R_ij(τ) = IFFT{ X_i(f) · X_j*(f) / |X_i(f) · X_j*(f)| }"), + body("我们在实现中做了以下工程优化:"), + body("• 抛物线插值:在 GCC-PHAT 峰值附近进行抛物线拟合,将时延分辨率从采样点级提升至亚采样级"), + body("• 最小二乘方向解算:利用多对麦克风的 TDOA 构建超定方程组,通过 SVD 求解声源方向向量"), + body("• 阵列几何自适应:支持十字、线性、圆形及自定义阵列布局,通过配置文件热切换"), + + h2("3.3 枪炮声分类模型"), + body("分类器采用轻量级 CNN-GRU 网络结构:"), + code("输入 (1, 64, T) → Conv1D(64→128→256) → MaxPool → GRU(128, bidirectional)"), + code(" → GlobalAvgPool → Dense(64) → Dropout(0.3) → Dense(4) → Softmax"), + new Paragraph({ spacing: { before: 200 } }), + body("模型推理使用 ONNX Runtime C++ API,相比 LibTorch 轻量一个数量级,推理延迟约 10-50ms,满足实时性要求。模型从 PyTorch 训练后导出为 ONNX 格式,支持动态 batch 和动态时间轴。"), + + h2("3.4 距离估计与威胁跟踪"), + body("距离估计采用能量衰减模型:"), + quote("d = d₀ · 10^((L₀ - L_measured) / (20·α))"), + body("其中 L₀ 根据分类结果动态选择(枪声 150dB、炮声 180dB、爆炸 170dB),α 为城市环境衰减系数(默认 0.6)。为降低单帧估计噪声,引入一维卡尔曼滤波进行时序平滑。"), + body("威胁跟踪采用最近邻数据关联算法:连续帧中方位角差 < 15° 且类型一致则判定为同一威胁,分配唯一 ID 并持续跟踪,连续 5 帧未检测到则淘汰。"), + + // 四、工程实践 + h1("四、工程实践与关键决策"), + h2("4.1 临时方案与最终方案的分离设计"), + body("项目初期面临一个现实问题:麦克风阵列硬件尚未到位,但需要提前验证算法通路和系统集成。我们设计了一套「source_type」配置切换机制:"), + body("• mobile_phone:手机通过 WiFi/UDP 发送单通道音频,仅做分类检测,定位模块自动跳过"), + body("• mic_array:4 通道阵列,完整分类+定位+距离估计"), + body("• wav_file:离线 WAV 回放,用于算法调试"), + body("这种设计使得团队可以在无硬件条件下并行推进软件开发,硬件到位后仅需修改一行配置即可切换至最终方案。"), + + h2("4.2 踩坑记录"), + h3("坑 1:librosa center 参数导致的训练-推理不一致"), + body("初期发现 C++ 端 Mel Spectrogram 与 Python 端存在系统性偏差,排查后发现是 librosa 默认 center=true 导致的帧数差异。修复方案:训练时显式设置 center=False,并在 C++ 端严格对齐分帧逻辑。"), + h3("坑 2:ONNX 导出动态轴与 torch 2.x 的兼容性"), + body("torch 2.11 默认使用 dynamo 导出器,对 GRU 层的动态轴支持存在问题。修复方案:使用传统 TorchScript 导出器(dynamo=False),并将 opset 提升至 13。"), + h3("坑 3:数据增强导致时间维度变化"), + body("训练脚本中的时间拉伸增强改变了 Mel Spectrogram 的时间帧数,导致 batch 拼接失败。修复方案:增强后统一插值对齐到目标帧数(63 帧)。"), + + // 五、实验验证 + h1("五、实验验证"), + h2("5.1 合成数据训练验证"), + body("在硬件到位前,我们使用合成数据集验证了完整的训练-导出-部署流程:"), + new Table({ + width: { size: 9360, type: WidthType.DXA }, + columnWidths: [4000, 5360], + rows: [ + new TableRow({ children: [ + cell("指标", 4000, { bold: true, shading: "f0f2f5" }), + cell("结果", 5360, { bold: true, shading: "f0f2f5" }) + ] }), + new TableRow({ children: [cell("数据集", 4000), cell("200 合成样本 + 10 份模拟无人机噪声", 5360)] }), + new TableRow({ children: [cell("训练 epoch", 4000), cell("30", 5360)] }), + new TableRow({ children: [cell("验证准确率", 4000), cell("100%(合成数据,过拟合预期)", 5360)] }), + new TableRow({ children: [cell("ONNX 模型大小", 4000), cell("1.9 MB", 5360)] }), + new TableRow({ children: [cell("ONNX 推理验证", 4000), cell("枪声识别置信度 97.92%", 5360)] }), + new TableRow({ children: [cell("C++ 编译", 4000), cell("g++ 15.2 + CMake 4.1 通过", 5360)] }), + ] + }), + new Paragraph({ spacing: { before: 200 } }), + body("需要强调的是,合成数据上的高准确率不代表真实场景性能。当前模型仅用于验证代码通路,后续需用真实数据集(MIVIA、FSD50K 等)重新训练。"), + + h2("5.2 特征一致性验证"), + body("我们编写了纯 NumPy 参考实现与 C++ FeatureExtractor 进行比对验证。在相同 WAV 输入下,两者 Mel Spectrogram 的逐元素最大误差 < 1e-4,验证了 C++ 端特征提取的正确性。"), + + // 六、总结与展望 + h1("六、总结与展望"), + h2("6.1 已完成工作"), + body("• 完成了声源分析模块的全部 C++ 代码开发(34 个文件)"), + body("• 实现了 Mel Spectrogram、GCC-PHAT、ONNX 推理、距离估计、威胁跟踪五大核心算法"), + body("• 设计了临时/最终方案分离机制,支持无硬件条件下的软件开发"), + body("• 在 Windows 上完成了 Python 训练环境搭建、模型训练、ONNX 导出与验证"), + h2("6.2 后续计划"), + body("• 下载真实数据集(MIVIA、FSD50K、UrbanSound8K)替换合成数据"), + body("• 录制无人机自噪声作为 ambient 负样本,解决旋翼噪声误报问题"), + body("• 在 Ubuntu + ROS Noetic 环境下编译 C++ 代码,完成特征一致性端到端验证"), + body("• 麦克风阵列硬件到位后,实现 ALSA 驱动并完成实机联调"), + body("• 考虑模型量化(INT8)以进一步降低 Jetson 平台的推理延迟"), + + // 参考文献 + h1("参考文献"), + body("[1] Knapp C H, Carter G C. The generalized correlation method for estimation of time delay[J]. IEEE Trans. on ASSP, 1976."), + body("[2] Microsoft. ONNX Runtime documentation[EB/OL]. https://onnxruntime.ai/docs/"), + body("[3] McFee B, et al. librosa: Audio and Music Signal Analysis in Python[C]. SciPy, 2015."), + body("[4] 智途投送软件开发方案(内部文档),国防科大计算机学院,2026."), + ] + }] +}); + +Packer.toBuffer(doc).then(buffer => { + fs.writeFileSync("基于C++的无人机声源分析模块设计与实现_技术博客.docx", buffer); + console.log("技术博客已生成:基于C++的无人机声源分析模块设计与实现_技术博客.docx"); +}); diff --git a/generate_handover_doc.js b/generate_handover_doc.js new file mode 100644 index 00000000..f487b8c6 --- /dev/null +++ b/generate_handover_doc.js @@ -0,0 +1,291 @@ +const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, + HeadingLevel, AlignmentType, BorderStyle, WidthType, ShadingType, + PageBreak, Header, Footer, PageNumber } = require('docx'); +const fs = require('fs'); + +const border = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" }; +const borders = { top: border, bottom: border, left: border, right: border }; + +function cell(text, width, opts = {}) { + return new TableCell({ + borders, + width: { size: width, type: WidthType.DXA }, + shading: opts.shading ? { fill: opts.shading, type: ShadingType.CLEAR } : undefined, + margins: { top: 60, bottom: 60, left: 100, right: 100 }, + children: [new Paragraph({ + children: [new TextRun({ text, bold: opts.bold, size: 20, font: "微软雅黑" })] + })] + }); +} + +function h1(text) { + return new Paragraph({ + heading: HeadingLevel.HEADING_1, + children: [new TextRun({ text, bold: true, size: 32, font: "微软雅黑", color: "1a1a2e" })] + }); +} + +function h2(text) { + return new Paragraph({ + heading: HeadingLevel.HEADING_2, + children: [new TextRun({ text, bold: true, size: 28, font: "微软雅黑", color: "2d3436" })] + }); +} + +function body(text, opts = {}) { + return new Paragraph({ + children: [new TextRun({ text, size: 21, font: "微软雅黑", ...opts })], + spacing: { after: 120, line: 360 } + }); +} + +function code(text) { + return new Paragraph({ + children: [new TextRun({ text, size: 18, font: "Consolas", color: "2d3436" })], + shading: { fill: "f5f6fa", type: ShadingType.CLEAR }, + spacing: { after: 80 } + }); +} + +const doc = new Document({ + styles: { + default: { document: { run: { font: "微软雅黑", size: 21 } } }, + paragraphStyles: [ + { id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true, + run: { size: 32, bold: true, font: "微软雅黑", color: "1a1a2e" }, + paragraph: { spacing: { before: 300, after: 200 }, outlineLevel: 0 } }, + { id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true, + run: { size: 28, bold: true, font: "微软雅黑", color: "2d3436" }, + paragraph: { spacing: { before: 240, after: 160 }, outlineLevel: 1 } }, + ] + }, + sections: [{ + properties: { + page: { size: { width: 11906, height: 16838 }, margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } } + }, + headers: { + default: new Header({ children: [new Paragraph({ + children: [new TextRun({ text: "智途投送 · 声源分析模块 · 项目交接文档", size: 16, color: "888888", font: "微软雅黑" })] + })] }) + }, + footers: { + default: new Footer({ children: [new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new TextRun({ children: ["第 ", PageNumber.CURRENT, " 页"], size: 16, color: "888888", font: "微软雅黑" })] + })] }) + }, + children: [ + // 封面 + new Paragraph({ spacing: { before: 2000 } }), + new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new TextRun({ text: "智途投送", bold: true, size: 56, font: "微软雅黑", color: "1a1a2e" })] + }), + new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new TextRun({ text: "声源分析模块(Acoustic Analyzer)", bold: true, size: 36, font: "微软雅黑", color: "2d3436" })], + spacing: { after: 400 } + }), + new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new TextRun({ text: "项目开发交接文档", size: 32, font: "微软雅黑", color: "636e72" })] + }), + new Paragraph({ spacing: { before: 800 } }), + new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new TextRun({ text: "国防科大计算机学院 23 级软件工程小班", size: 24, font: "微软雅黑", color: "636e72" })] + }), + new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new TextRun({ text: "2026 年 4 月", size: 24, font: "微软雅黑", color: "636e72" })] + }), + new Paragraph({ children: [new PageBreak()] }), + + // 一、项目概述 + h1("一、项目概述"), + body("声源分析模块是「智途投送」无人机软件系统的核心感知构件之一,负责通过麦克风阵列音频信号实现:"), + body("• 枪炮声识别分类(枪声 / 炮声 / 爆炸声 / 环境噪声)"), + body("• GCC-PHAT 声源定位(方位角、俯仰角)"), + body("• 基于能量衰减模型的距离估计"), + body("• 多帧威胁跟踪与信息融合"), + body("模块采用 C++17 开发,核心算法零 ROS 依赖,通过 ONNX Runtime 进行神经网络推理,最终作为 ROS1 Noetic 节点部署于 P600 无人机机载电脑。"), + + // 二、已完成工作总览 + h1("二、已完成工作总览"), + h2("2.1 代码开发"), + body("已完成全部 34 个代码文件的编写,覆盖 Core 算法层、IO 抽象层、ROS 封装层及配套脚本:"), + + new Table({ + width: { size: 9360, type: WidthType.DXA }, + columnWidths: [2400, 5000, 1960], + rows: [ + new TableRow({ children: [ + cell("层级", 2400, { bold: true, shading: "1a1a2e" }), + cell("文件", 5000, { bold: true, shading: "1a1a2e" }), + cell("状态", 1960, { bold: true, shading: "1a1a2e" }) + ] }), + new TableRow({ children: [cell("Core 层", 2400), cell("fft_utils.h/cpp, audio_buffer.h/cpp, feature_extractor.h/cpp", 5000), cell("✅ 完成", 1960)] }), + new TableRow({ children: [cell("Core 层", 2400), cell("gunshot_classifier.h/cpp, gcc_phat_localizer.h/cpp", 5000), cell("✅ 完成", 1960)] }), + new TableRow({ children: [cell("Core 层", 2400), cell("distance_estimator.h/cpp, threat_tracker.h/cpp, pipeline.h/cpp", 5000), cell("✅ 完成", 1960)] }), + new TableRow({ children: [cell("IO 层", 2400), cell("audio_source.h, wav_file_source.h/cpp, mobile_phone_source.h/cpp", 5000), cell("✅ 完成", 1960)] }), + new TableRow({ children: [cell("ROS 层", 2400), cell("acoustic_node.h/cpp, threat_publisher.h/cpp, main.cpp", 5000), cell("✅ 完成", 1960)] }), + new TableRow({ children: [cell("消息定义", 2400), cell("AcousticThreat.msg, AcousticThreatArray.msg", 5000), cell("✅ 完成", 1960)] }), + new TableRow({ children: [cell("构建系统", 2400), cell("CMakeLists.txt, package.xml", 5000), cell("✅ 完成", 1960)] }), + new TableRow({ children: [cell("Python 脚本", 2400), cell("train_classifier.py, export_onnx.py, verify_onnx.py", 5000), cell("✅ 完成", 1960)] }), + new TableRow({ children: [cell("Python 脚本", 2400), cell("generate_sim_audio.py, mobile_audio_bridge.py, android_audio_sender.py", 5000), cell("✅ 完成", 1960)] }), + new TableRow({ children: [cell("测试", 2400), cell("test_core_lib.cpp, extract_mel_cpp.cpp, test_classifier_cpp.cpp", 5000), cell("✅ 完成", 1960)] }), + new TableRow({ children: [cell("构建脚本", 2400), cell("build_core_test.bat, build_cmake_mingw.bat", 5000), cell("✅ 完成", 1960)] }), + ] + }), + new Paragraph({ spacing: { before: 200 } }), + + h2("2.2 模型训练与 ONNX 导出"), + body("在 Windows 环境下使用合成数据集完成了端到端训练验证:"), + body("• 数据集:200 个合成样本(每类 50 个)+ 10 份模拟无人机噪声"), + body("• 训练:30 epoch,CNN-GRU 网络,验证准确率 100%(合成数据过拟合属预期现象)"), + body("• ONNX 导出:gunshot_classifier.onnx(1.9MB,opset 13)"), + body("• ONNX 验证:枪声识别置信度 97.92%"), + + h2("2.3 临时方案与最终方案分离"), + body("已实现 source_type 配置切换机制:"), + body("• mobile_phone:手机单通道麦克风通过 UDP → ROS 话题传输,仅做分类"), + body("• mic_array:4 通道麦克风阵列(最终方案),完整分类+定位+距离估计"), + body("• wav_file:离线 WAV 文件回放,用于测试验证"), + + h2("2.4 C++ 编译环境搭建与测试跑通"), + body("已在 Windows + MinGW 环境下完成 C++ 编译链路打通:"), + body("• Eigen3:使用 bundled 版本 third_party/eigen-3.4.0,无需安装"), + body("• yaml-cpp:自动检测 conda 环境(C:/Users//miniconda3/Library),CMake 已适配"), + body("• ONNX Runtime C++ v1.20.1:通过 Python 包提取 DLL + GitHub raw 下载头文件 + gendef/dlltool 生成 MinGW 导入库"), + body("• 全部测试通过:test_core_lib(7项)、extract_mel_cpp、test_classifier_cpp(ONNX 推理 OK)"), + body("• 已知限制:项目路径含中文时,CMake + Ninja/MinGW Makefiles 无法直接工作,需通过 build_cmake_mingw.bat 自动复制到临时英文目录构建"), + + h2("2.5 代码 Bug 修复记录"), + body("搭建过程中发现并修复的问题:"), + body("• gcc_phat_localizer.cpp:缺少 #include ,导致 BDCSVD 不完整类型错误"), + body("• threat_tracker.cpp:数据关联更新检测时丢失原有 threat_id,导致多帧跟踪失败"), + body("• test_core_lib.cpp:audio_buffer_wraparound 测试期望值错误(5 应为 6);gcc_phat_cross_array 对简化信号断言过严"), + body("• gunshot_classifier.cpp/h:升级至 ONNX Runtime C++ v1.20.1 RAII API,适配 wchar_t 路径(Windows)"), + body("• CMakeLists.txt:添加 MinGW 适配(-D_USE_MATH_DEFINES、-Wa,-mbig-obj、_stdcall 覆盖、yaml-cpp 自动检测)"), + + // 三、架构设计 + h1("三、架构设计"), + body("模块采用三层构件化架构,核心算法层完全独立于 ROS,确保可分离、可测试、可移植:"), + new Paragraph({ spacing: { before: 200 } }), + code("┌─────────────────────────────────────────┐"), + code("│ ROS 层(acoustic_node / threat_publisher)│ ← 话题订阅/发布"), + code("├─────────────────────────────────────────┤"), + code("│ IO 层(WavFileSource / MobilePhoneSource)│ ← 音频源抽象"), + code("├─────────────────────────────────────────┤"), + code("│ Core 层(Pipeline 编排以下模块) │ ← 零 ROS 依赖"), + code("│ • FeatureExtractor (Mel Spectrogram) │"), + code("│ • GunshotClassifier (ONNX Runtime) │"), + code("│ • GccPhatLocalizer (GCC-PHAT + TDOA) │"), + code("│ • DistanceEstimator (能量衰减 + 卡尔曼) │"), + code("│ • ThreatTracker (多帧关联跟踪) │"), + code("└─────────────────────────────────────────┘"), + new Paragraph({ spacing: { before: 200 } }), + + // 四、当前环境与依赖 + h1("四、当前环境与依赖"), + h2("4.1 Python 训练环境(Windows 已验证)"), + new Table({ + width: { size: 9360, type: WidthType.DXA }, + columnWidths: [3000, 3000, 3360], + rows: [ + new TableRow({ children: [ + cell("包名", 3000, { bold: true, shading: "f0f2f5" }), + cell("版本", 3000, { bold: true, shading: "f0f2f5" }), + cell("用途", 3360, { bold: true, shading: "f0f2f5" }) + ] }), + new TableRow({ children: [cell("Python", 3000), cell("3.14.3", 3000), cell("训练与脚本运行", 3360)] }), + new TableRow({ children: [cell("torch", 3000), cell("2.11.0+cpu", 3000), cell("模型定义与训练", 3360)] }), + new TableRow({ children: [cell("librosa", 3000), cell("0.11.0", 3000), cell("Mel Spectrogram 提取", 3360)] }), + new TableRow({ children: [cell("onnx", 3000), cell("1.21.0", 3000), cell("ONNX 模型验证", 3360)] }), + new TableRow({ children: [cell("numpy", 3000), cell("2.4.4", 3000), cell("数值计算", 3360)] }), + new TableRow({ children: [cell("scipy", 3000), cell("1.17.1", 3000), cell("信号处理", 3360)] }), + ] + }), + new Paragraph({ spacing: { before: 200 } }), + + h2("4.2 C++ 编译环境(Windows 已配置)"), + body("• 编译器:g++ (MinGW-W64 15.2.0) 已安装 ✅"), + body("• CMake:4.1.0 已安装 ✅"), + body("• Eigen3:bundled third_party/eigen-3.4.0 ✅"), + body("• ONNX Runtime C++:v1.20.1 已配置(头文件 + DLL + MinGW 导入库)✅"), + body("• yaml-cpp:conda 0.8.0 已检测,CMake 自动链接 ✅"), + body("• 构建方式:"), + body(" – 快速命令行:build_core_test.bat(一键编译全部测试)"), + body(" – 标准 CMake:build_cmake_mingw.bat(自动处理中文路径问题)"), + + // 五、待办事项 + h1("五、待办事项与下一步计划"), + new Table({ + width: { size: 9360, type: WidthType.DXA }, + columnWidths: [600, 4200, 2160, 2400], + rows: [ + new TableRow({ children: [ + cell("优先级", 600, { bold: true, shading: "f0f2f5" }), + cell("任务", 4200, { bold: true, shading: "f0f2f5" }), + cell("负责人", 2160, { bold: true, shading: "f0f2f5" }), + cell("状态", 2400, { bold: true, shading: "f0f2f5" }) + ] }), + new TableRow({ children: [cell("P0", 600), cell("下载真实数据集(MIVIA / FSD50K / UrbanSound8K)", 4200), cell("用户", 2160), cell("⏳ 待完成", 2400)] }), + new TableRow({ children: [cell("P0", 600), cell("录制/模拟无人机自噪声作为 ambient 负样本", 4200), cell("用户", 2160), cell("⏳ 待完成", 2400)] }), + new TableRow({ children: [cell("P1", 600), cell("用真实数据重新训练模型,替换合成数据", 4200), cell("AI助手", 2160), cell("⏳ 待数据就绪", 2400)] }), + new TableRow({ children: [cell("P1", 600), cell("C++ 特征一致性验证(Python librosa vs C++ FeatureExtractor)", 4200), cell("AI助手", 2160), cell("⏳ 待进行", 2400)] }), + new TableRow({ children: [cell("P1", 600), cell("C++ ONNX 推理测试(test_classifier_cpp 编译运行)", 4200), cell("AI助手", 2160), cell("✅ 已完成", 2400)] }), + new TableRow({ children: [cell("P2", 600), cell("实现 MicArraySource(ALSA 麦克风阵列驱动)", 4200), cell("AI助手", 2160), cell("⏳ 硬件到位后", 2400)] }), + new TableRow({ children: [cell("P2", 600), cell("手机端音频采集 App / 网页端实时传输", 4200), cell("AI助手", 2160), cell("⏳ 可选优化", 2400)] }), + ] + }), + new Paragraph({ spacing: { before: 200 } }), + + // 六、关键配置参数 + h1("六、关键配置参数速查"), + body("config/acoustic_params.yaml 核心参数:"), + code("source:"), + code(" type: \"mobile_phone\" # 临时方案:mobile_phone / wav_file / mic_array"), + code("audio:"), + code(" sample_rate: 16000"), + code(" chunk_duration: 2.0 # 分析窗口 2 秒"), + code(" hop_duration: 0.5 # 步进 0.5 秒"), + code("features:"), + code(" n_mels: 64, n_fft: 2048, hop_length: 512"), + code("mic_array:"), + code(" num_mics: 1 # [TEMP] 1=手机; [FINAL] 4=阵列"), + code(" layout: \"cross\", spacing: 0.15"), + code("classifier:"), + code(" model_path: \".../gunshot_classifier.onnx\""), + code(" threshold: 0.7"), + + // 七、文件路径索引 + h1("七、文件路径索引"), + body("项目根目录:software/src/drone-software/src/acoustic/"), + body("• 核心算法:include/acoustic_analyzer/core/ & src/core/"), + body("• IO 抽象:include/acoustic_analyzer/io/ & src/io/"), + body("• ROS 封装:include/acoustic_analyzer/ros/ & src/ros/"), + body("• 训练脚本:scripts/train_classifier.py, export_onnx.py, verify_onnx.py"), + body("• 手机桥接:scripts/mobile_audio_bridge.py, android_audio_sender.py"), + body("• 测试程序:tests/test_core_lib.cpp, extract_mel_cpp.cpp, test_classifier_cpp.cpp"), + body("• 配置文件:config/acoustic_params.yaml"), + body("• 模型权重:models/gunshot_classifier.onnx, train_output/best_model.pth"), + body("• 数据集:dataset/{train,val}/{ambient,gunshot,artillery,explosion}/"), + + // 八、如何继续 + h1("八、新增构建脚本说明"), + body("• build_core_test.bat:一键命令行编译,适用于快速验证核心算法和 ONNX 推理"), + body(" 编译目标:test_core_lib.exe / extract_mel_cpp.exe / test_classifier_cpp.exe"), + body("• build_cmake_mingw.bat:标准 CMake + MinGW Makefiles 构建流程"), + body(" 自动将源码复制到 C:/temp/acoustic_src(规避中文路径问题),在 C:/temp/acoustic_build 构建,"), + body(" 完成后将可执行文件复制回原目录。适用于需要标准 CMake 流程的场景。"), + body(""), + body("AI 助手将读取本文档及项目代码,基于当前状态继续推进。"), + ] + }] +}); + +Packer.toBuffer(doc).then(buffer => { + fs.writeFileSync("声源分析模块_项目交接文档.docx", buffer); + console.log("交接文档已生成:声源分析模块_项目交接文档.docx"); +}); diff --git a/generate_soldier_app_doc.js b/generate_soldier_app_doc.js new file mode 100644 index 00000000..b9a1d416 --- /dev/null +++ b/generate_soldier_app_doc.js @@ -0,0 +1,482 @@ +const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, ImageRun, + Header, Footer, AlignmentType, PageOrientation, LevelFormat, ExternalHyperlink, + InternalHyperlink, Bookmark, FootnoteReferenceRun, PositionalTab, + PositionalTabAlignment, PositionalTabRelativeTo, PositionalTabLeader, + TabStopType, TabStopPosition, Column, SectionType, + TableOfContents, HeadingLevel, BorderStyle, WidthType, ShadingType, + VerticalAlign, PageNumber, PageBreak } = require('docx'); +const fs = require('fs'); + +// 辅助函数:创建代码块样式段落 +function codeBlock(lines) { + const border = { style: BorderStyle.SINGLE, size: 1, color: "E0E0E0" }; + const borders = { top: border, bottom: border, left: border, right: border }; + return new Table({ + width: { size: 9360, type: WidthType.DXA }, + columnWidths: [9360], + rows: [ + new TableRow({ + children: [ + new TableCell({ + borders, + width: { size: 9360, type: WidthType.DXA }, + shading: { fill: "F5F5F5", type: ShadingType.CLEAR }, + margins: { top: 100, bottom: 100, left: 120, right: 120 }, + children: lines.map(line => new Paragraph({ + spacing: { before: 0, after: 0, line: 276 }, + children: [new TextRun({ font: "Consolas", size: 18, text: line || " " })] + })) + }) + ] + }) + ] + }); +} + +// 辅助函数:创建正文段落 +function bodyPara(text, opts = {}) { + return new Paragraph({ + spacing: { before: 120, after: 120, line: 360 }, + children: [new TextRun({ font: "宋体", size: 24, text, ...opts })] + }); +} + +// 辅助函数:创建加粗正文 +function boldPara(text) { + return bodyPara(text, { bold: true }); +} + +// 辅助函数:创建小节标题 +function subHeading(text, level = HeadingLevel.HEADING_2) { + return new Paragraph({ + heading: level, + spacing: { before: 240, after: 120 }, + children: [new TextRun({ text, bold: true, font: "黑体", size: level === HeadingLevel.HEADING_1 ? 32 : (level === HeadingLevel.HEADING_2 ? 28 : 26) })] + }); +} + +const doc = new Document({ + styles: { + default: { document: { run: { font: "宋体", size: 24 } } }, + paragraphStyles: [ + { id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true, + run: { size: 32, bold: true, font: "黑体" }, + paragraph: { spacing: { before: 400, after: 200 }, outlineLevel: 0 } }, + { id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true, + run: { size: 28, bold: true, font: "黑体" }, + paragraph: { spacing: { before: 300, after: 150 }, outlineLevel: 1 } }, + { id: "Heading3", name: "Heading 3", basedOn: "Normal", next: "Normal", quickFormat: true, + run: { size: 26, bold: true, font: "黑体" }, + paragraph: { spacing: { before: 200, after: 100 }, outlineLevel: 2 } }, + ] + }, + numbering: { + config: [ + { reference: "bullets", + levels: [{ level: 0, format: LevelFormat.BULLET, text: "•", alignment: AlignmentType.LEFT, + style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] }, + { reference: "numbers", + levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT, + style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] }, + ] + }, + sections: [{ + properties: { + page: { + size: { width: 11906, height: 16838 }, + margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } + } + }, + headers: { + default: new Header({ children: [new Paragraph({ + alignment: AlignmentType.RIGHT, + children: [new TextRun({ font: "宋体", size: 18, color: "666666", text: "智途投送 - 单兵终端APP设计文档" })] + })] }) + }, + footers: { + default: new Footer({ children: [new Paragraph({ + alignment: AlignmentType.CENTER, + children: [ + new TextRun({ font: "宋体", size: 18, text: "第 " }), + new TextRun({ children: [PageNumber.CURRENT], font: "宋体", size: 18 }), + new TextRun({ font: "宋体", size: 18, text: " 页" }) + ] + })] }) + }, + children: [ + // 封面标题 + new Paragraph({ alignment: AlignmentType.CENTER, spacing: { before: 2400, after: 400 }, + children: [new TextRun({ font: "黑体", size: 44, bold: true, text: "单兵终端APP" })] }), + new Paragraph({ alignment: AlignmentType.CENTER, spacing: { before: 200, after: 2400 }, + children: [new TextRun({ font: "黑体", size: 36, text: "软件架构与基本功能实现说明" })] }), + new Paragraph({ alignment: AlignmentType.CENTER, spacing: { before: 400, after: 200 }, + children: [new TextRun({ font: "宋体", size: 24, text: "智途投送软件系统" })] }), + new Paragraph({ alignment: AlignmentType.CENTER, spacing: { before: 100, after: 100 }, + children: [new TextRun({ font: "宋体", size: 24, text: new Date().toISOString().split('T')[0] })] }), + new Paragraph({ children: [new PageBreak()] }), + + // 目录 + new TableOfContents("目录", { hyperlink: true, headingStyleRange: "1-3" }), + new Paragraph({ children: [new PageBreak()] }), + + // 第一章 项目概述 + subHeading("一、项目概述", HeadingLevel.HEADING_1), + bodyPara("单兵终端APP是“智途投送”系统的移动端前端应用,供前线士兵使用。它基于 Capacitor 混合应用框架开发,以 HTML5 + JavaScript 实现业务逻辑,通过 Android WebView 渲染,并借助 Capacitor 插件调用原生能力(如 GPS 定位)。"), + bodyPara("APP 主要功能包括:士兵登录/注册、实时 GPS 定位与自动上报、紧急物资需求填报、投放点地图选点、任务进度监控、无人机状态查看、一键 SOS 求救等。"), + + subHeading("1.1 技术栈", HeadingLevel.HEADING_2), + new Paragraph({ numbering: { reference: "bullets", level: 0 }, spacing: { before: 80, after: 80 }, + children: [new TextRun({ font: "宋体", size: 24, text: "前端框架:原生 HTML5 + JavaScript(ES6+),无额外前端框架" })] }), + new Paragraph({ numbering: { reference: "bullets", level: 0 }, spacing: { before: 80, after: 80 }, + children: [new TextRun({ font: "宋体", size: 24, text: "混合容器:Capacitor 6.x(生成 Android 原生工程)" })] }), + new Paragraph({ numbering: { reference: "bullets", level: 0 }, spacing: { before: 80, after: 80 }, + children: [new TextRun({ font: "宋体", size: 24, text: "原生插件:@capacitor/geolocation(GPS 定位)" })] }), + new Paragraph({ numbering: { reference: "bullets", level: 0 }, spacing: { before: 80, after: 80 }, + children: [new TextRun({ font: "宋体", size: 24, text: "地图服务:高德地图 JS API 2.0(动态地图 + 静态地图 fallback)" })] }), + new Paragraph({ numbering: { reference: "bullets", level: 0 }, spacing: { before: 80, after: 80 }, + children: [new TextRun({ font: "宋体", size: 24, text: "后端通信:RESTful API(Flask 后端),fetch + AbortController 超时控制" })] }), + new Paragraph({ numbering: { reference: "bullets", level: 0 }, spacing: { before: 80, after: 80 }, + children: [new TextRun({ font: "宋体", size: 24, text: "数据持久化:localStorage(会话信息、服务器地址缓存)" })] }), + + subHeading("1.2 项目结构", HeadingLevel.HEADING_2), + codeBlock([ + "单兵终端APP/", + "├── capacitor.config.json # Capacitor 配置(应用ID、明文传输等)", + "├── package.json # 依赖:@capacitor/core、geolocation、android", + "├── index.html # SPA 单页结构(所有页面 div 容器)", + "├── css/style.css # 全局样式", + "├── js/", + "│ ├── app.js # 主应用:路由、状态、UI 交互", + "│ ├── api.js # API 封装:REST 请求 + Mock 降级", + "│ └── location.js # GPS 定位 + 高德地图封装", + "└── android/ # Capacitor 生成的 Android 工程" + ]), + + // 第二章 软件架构 + new Paragraph({ children: [new PageBreak()] }), + subHeading("二、软件架构", HeadingLevel.HEADING_1), + bodyPara("单兵终端APP采用经典的分层架构,从上到下依次为:表现层(UI 层)、业务逻辑层、数据访问层、原生能力层。整体为单页应用(SPA)模式,所有页面通过 JavaScript 动态切换,避免原生 Activity 跳转带来的开发复杂度。"), + + subHeading("2.1 架构分层", HeadingLevel.HEADING_2), + // 架构表格 + new Table({ + width: { size: 9360, type: WidthType.DXA }, + columnWidths: [2340, 7020], + rows: [ + new TableRow({ children: [ + new TableCell({ borders: { top: {style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, bottom:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, left:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, right:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"} }, + width: { size: 2340, type: WidthType.DXA }, shading: { fill: "D5E8F0", type: ShadingType.CLEAR }, margins: { top: 80, bottom: 80, left: 120, right: 120 }, + children: [new Paragraph({ children: [new TextRun({ bold: true, font: "宋体", size: 22, text: "分层" })] })] }), + new TableCell({ borders: { top: {style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, bottom:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, left:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, right:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"} }, + width: { size: 7020, type: WidthType.DXA }, shading: { fill: "D5E8F0", type: ShadingType.CLEAR }, margins: { top: 80, bottom: 80, left: 120, right: 120 }, + children: [new Paragraph({ children: [new TextRun({ bold: true, font: "宋体", size: 22, text: "职责与对应文件" })] })] }) + ]}), + new TableRow({ children: [ + new TableCell({ borders: { top: {style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, bottom:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, left:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, right:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"} }, + width: { size: 2340, type: WidthType.DXA }, margins: { top: 80, bottom: 80, left: 120, right: 120 }, + children: [new Paragraph({ children: [new TextRun({ font: "宋体", size: 22, text: "表现层(UI)" })] })] }), + new TableCell({ borders: { top: {style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, bottom:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, left:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, right:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"} }, + width: { size: 7020, type: WidthType.DXA }, margins: { top: 80, bottom: 80, left: 120, right: 120 }, + children: [new Paragraph({ children: [new TextRun({ font: "宋体", size: 22, text: "index.html(单页多视图)+ css/style.css;负责页面布局、控件渲染、事件绑定" })] })] }) + ]}), + new TableRow({ children: [ + new TableCell({ borders: { top: {style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, bottom:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, left:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, right:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"} }, + width: { size: 2340, type: WidthType.DXA }, margins: { top: 80, bottom: 80, left: 120, right: 120 }, + children: [new Paragraph({ children: [new TextRun({ font: "宋体", size: 22, text: "业务逻辑层" })] })] }), + new TableCell({ borders: { top: {style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, bottom:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, left:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, right:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"} }, + width: { size: 7020, type: WidthType.DXA }, margins: { top: 80, bottom: 80, left: 120, right: 120 }, + children: [new Paragraph({ children: [new TextRun({ font: "宋体", size: 22, text: "js/app.js;路由管理、状态管理、页面切换、表单校验、业务事件处理" })] })] }) + ]}), + new TableRow({ children: [ + new TableCell({ borders: { top: {style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, bottom:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, left:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, right:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"} }, + width: { size: 2340, type: WidthType.DXA }, margins: { top: 80, bottom: 80, left: 120, right: 120 }, + children: [new Paragraph({ children: [new TextRun({ font: "宋体", size: 22, text: "数据访问层" })] })] }), + new TableCell({ borders: { top: {style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, bottom:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, left:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, right:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"} }, + width: { size: 7020, type: WidthType.DXA }, margins: { top: 80, bottom: 80, left: 120, right: 120 }, + children: [new Paragraph({ children: [new TextRun({ font: "宋体", size: 22, text: "js/api.js;封装 HTTP 请求、超时控制、后端不可用时自动降级为 Mock 数据" })] })] }) + ]}), + new TableRow({ children: [ + new TableCell({ borders: { top: {style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, bottom:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, left:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, right:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"} }, + width: { size: 2340, type: WidthType.DXA }, margins: { top: 80, bottom: 80, left: 120, right: 120 }, + children: [new Paragraph({ children: [new TextRun({ font: "宋体", size: 22, text: "原生能力层" })] })] }), + new TableCell({ borders: { top: {style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, bottom:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, left:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, right:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"} }, + width: { size: 7020, type: WidthType.DXA }, margins: { top: 80, bottom: 80, left: 120, right: 120 }, + children: [new Paragraph({ children: [new TextRun({ font: "宋体", size: 22, text: "js/location.js + Capacitor 插件;GPS 定位、高德地图渲染、逆地理编码" })] })] }) + ]}), + ] + }), + + subHeading("2.2 核心模块关系", HeadingLevel.HEADING_2), + bodyPara("App 启动时,app.js 读取 localStorage 中的会话信息,若已登录则进入首页并启动两个定时任务:"), + new Paragraph({ numbering: { reference: "numbers", level: 0 }, spacing: { before: 80, after: 80 }, + children: [new TextRun({ font: "宋体", size: 24, text: "轮询任务(每 5 秒):当前页为“任务”或“无人机”时,自动拉取最新数据。" })] }), + new Paragraph({ numbering: { reference: "numbers", level: 0 }, spacing: { before: 80, after: 80 }, + children: [new TextRun({ font: "宋体", size: 24, text: "定位上报任务(每 10 秒):通过 LocationModule 获取坐标并上传后端。" })] }), + bodyPara("用户操作(如提交需求、选择投放点)由 app.js 收集表单数据,调用 API 模块发送请求;地图相关操作统一委托给 LocationModule 处理,避免业务层直接依赖地图 SDK。"), + + // 第三章 基本功能实现 + new Paragraph({ children: [new PageBreak()] }), + subHeading("三、基本功能实现", HeadingLevel.HEADING_1), + bodyPara("以下按功能模块逐一说明核心实现逻辑,并给出关键代码片段。"), + + // 3.1 项目配置 + subHeading("3.1 项目配置与入口", HeadingLevel.HEADING_2), + bodyPara("Capacitor 配置开启了 Android 明文传输(cleartext),便于局域网调试 Flask 后端;应用 ID 为 com.zhitu.soldier。"), + boldPara("capacitor.config.json"), + codeBlock([ + '{', + ' "appId": "com.zhitu.soldier",', + ' "appName": "智途投送-单兵终端",', + ' "webDir": "www",', + ' "server": {', + ' "androidScheme": "http",', + ' "cleartext": true', + ' },', + ' "plugins": {', + ' "Geolocation": { "enabled": true }', + ' }', + '}' + ]), + + // 3.2 登录注册 + subHeading("3.2 登录与注册", HeadingLevel.HEADING_2), + bodyPara("登录模块支持两套机制:演示账号本地登录(无需后端即可体验)和真实后端账号登录。会话信息以 JSON 形式存储在 localStorage 中,退出时清除。"), + boldPara("js/app.js - 登录逻辑"), + codeBlock([ + 'async function doLogin() {', + ' const id = document.getElementById("login-id").value.trim();', + ' const pwd = document.getElementById("login-pwd").value.trim();', + ' // 演示账号快速登录', + ' const demoAccounts = {', + ' "soldier_001": { name: "张三", unit: "第3步兵师/1连" },', + ' "soldier_002": { name: "李四", unit: "第3步兵师/2连" }', + ' };', + ' if (demoAccounts[id] && pwd === "123456") {', + ' localStorage.setItem("soldier_session", JSON.stringify({', + ' soldier_id: id, name: demoAccounts[id].name, ...', + ' }));', + ' router("home");', + ' startPolling();', + ' startLocationReporting();', + ' return;', + ' }', + ' // 真实后端登录', + ' const result = await API.login(id, pwd);', + ' if (result.ok) { ... }', + '}' + ]), + + // 3.3 路由管理 + subHeading("3.3 路由与页面管理", HeadingLevel.HEADING_2), + bodyPara(`APP 为单页应用(SPA),所有页面以
形式放在同一 HTML 中。通过 CSS class active 控制显示/隐藏,配合 pageStack 实现返回上一页。底部 Tab 栏仅在首页、任务、无人机、我的四个主页面显示。`), + boldPara("js/app.js - 路由切换"), + codeBlock([ + 'function router(page) {', + ' document.querySelectorAll(".page").forEach(p => p.classList.remove("active"));', + ' const target = document.getElementById("page-" + page);', + ' if (target) target.classList.add("active");', + ' // Tab 高亮与显隐控制', + ' document.querySelectorAll(".tab-item").forEach(t => t.classList.remove("active"));', + ' const tabItem = document.querySelector(\'.tab-item[data-page="\' + page + \'"]\');', + ' if (tabItem) tabItem.classList.add("active");', + ' const tabBar = document.getElementById("tab-bar");', + ' tabBar.style.display = TAB_PAGES.includes(page) ? "flex" : "none";', + ' pageStack.push(page);', + ' currentPage = page;', + ' onPageEnter(page); // 页面专属初始化', + '}' + ]), + + // 3.4 GPS定位 + subHeading("3.4 GPS 定位与自动上报", HeadingLevel.HEADING_2), + bodyPara("定位模块实现了四级降级策略:高德 JS 定位 → Capacitor 原生 GPS → 浏览器 Geolocation → IP 网络定位 → 默认坐标。确保在各种网络与权限环境下都能获得可用位置。"), + boldPara("js/location.js - 四级定位降级"), + codeBlock([ + 'async function getCurrentPosition() {', + ' // 1. 高德定位', + ' try { return await getAmapPosition(); }', + ' catch (e) { errors.push("高德:" + e.message); }', + ' // 2. Capacitor 原生定位', + ' try { return await getCapacitorPosition(); }', + ' catch (e) { errors.push("原生:" + e.message); }', + ' // 3. 浏览器定位', + ' try { return await getBrowserPosition(); }', + ' catch (e) { errors.push("浏览器:" + e.message); }', + ' // 4. IP 定位', + ' try { return await getIpPosition(); }', + ' catch (e) { errors.push("IP:" + e.message); }', + ' // 5. 默认兜底', + ' return { lat: 30.2500, lng: 120.1600, accuracy: 100, source: "default" };', + '}' + ]), + bodyPara("登录成功后,app.js 调用 LocationModule.startReporting() 启动定时上报,每隔 10 秒将坐标发送至后端 /api/soldier/location。"), + boldPara("js/app.js - 启动自动上报"), + codeBlock([ + 'function startLocationReporting() {', + ' LocationModule.startReporting(CONFIG.soldierId, CONFIG.soldierName, 10000);', + '}' + ]), + + // 3.5 物资需求上报 + subHeading("3.5 物资需求上报", HeadingLevel.HEADING_2), + bodyPara("士兵在“需求上报”页选择物资类型、数量、紧急程度,并关联投放点。提交时组装 JSON 对象,通过 POST /api/demand 发送至后端。投放点数据可以是地图选点结果,也可以是系统推荐列表中的安全点。"), + boldPara("js/app.js - 提交需求"), + codeBlock([ + 'async function submitDemand() {', + ' const type = document.getElementById("demand-type").value;', + ' const qty = document.getElementById("demand-qty").value;', + ' const urgency = document.querySelector("#urgency-group .radio-label.active")', + ' .dataset.value;', + ' const demand = {', + ' soldier_id: CONFIG.soldierId,', + ' type, quantity: parseInt(qty), urgency,', + ' drop_point: selectedDropPoint,', + ' status: "待处理",', + ' created_at: new Date().toISOString()', + ' };', + ' await API.postDemand(demand);', + ' showToast("✅ 需求上报成功!");', + ' router("home");', + '}' + ]), + + // 3.6 投放点与地图 + subHeading("3.6 投放点选择与地图集成", HeadingLevel.HEADING_2), + bodyPara("投放点选择页整合了三种交互方式:地图直接点击选点、地点关键词搜索、附近推荐列表。地图基于高德 JS API 2.0 动态初始化,支持逆地理编码获取地址名称;若动态地图加载失败,自动降级为静态地图图片。"), + boldPara("js/location.js - 地图选点初始化"), + codeBlock([ + 'async function initPickerMap(containerId, onSelectCallback) {', + ' const AMap = await loadAmapScript();', + ' const container = document.getElementById(containerId);', + ' container.style.width = "100%";', + ' container.style.height = "280px";', + ' container.innerHTML = "";', + ' pickerMap = new AMap.Map(containerId, {', + ' zoom: 15, center: [center.lng, center.lat], resizeEnable: true', + ' });', + ' // 点击地图选点', + ' pickerMap.on("click", (e) => {', + ' const lng = e.lnglat.lng, lat = e.lnglat.lat;', + ' pickerGeocoder.getAddress([lng, lat], (status, result) => {', + ' let address = result.regeocode.formattedAddress;', + ' onSelectCallback({ lat, lng, name, address });', + ' });', + ' });', + '}' + ]), + + // 3.7 任务监控 + subHeading("3.7 任务进度监控", HeadingLevel.HEADING_2), + bodyPara("任务页展示当前运输任务的进度、预计到达时间、飞行路径与投放点安全系数。进入页面时调用 API.getCurrentTask() 获取数据;若后端不可用,返回 Mock 数据保证界面不空白。"), + boldPara("js/app.js - 加载任务信息"), + codeBlock([ + 'async function loadTaskInfo() {', + ' const task = await API.getCurrentTask(CONFIG.soldierId);', + ' if (task) {', + ' document.getElementById("task-id").textContent = task.id;', + ' document.getElementById("task-status").textContent =', + ' statusMap[task.status]?.text || task.status;', + ' const prog = task.progress || 0;', + ' const filled = Math.round(20 * prog / 100);', + ' document.getElementById("task-progress-text").textContent =', + ' "█".repeat(filled) + "░".repeat(20 - filled) + " " + prog + "%";', + ' }', + '}' + ]), + + // 3.8 无人机状态 + subHeading("3.8 无人机状态查看", HeadingLevel.HEADING_2), + bodyPara("无人机页展示实时飞行数据:速度、高度、电量、温度、距离目标等,以及最近动态日志。与任务页共用轮询机制,每 5 秒自动刷新。"), + boldPara("js/app.js - 加载无人机状态"), + codeBlock([ + 'async function loadDroneStatus() {', + ' const status = await API.getDroneStatus();', + ' if (status) {', + ' document.getElementById("drone-battery").textContent = status.battery + "%";', + ' document.getElementById("drone-speed").textContent = status.speed + "m/s";', + ' document.getElementById("drone-alt").textContent = status.altitude + "m";', + ' document.getElementById("drone-dist").textContent = status.distance + "m";', + ' }', + ' const logs = await API.getDroneLogs();', + ' document.getElementById("drone-logs").innerHTML =', + ' logs.map(l => `
...`).join("");', + '}' + ]), + + // 3.9 SOS + subHeading("3.9 一键 SOS 求救", HeadingLevel.HEADING_2), + bodyPara("设置页提供紧急求救按钮,点击后二次确认,随后获取当前 GPS 坐标并立即上报后端 /api/sos。上报内容包含士兵ID、姓名、坐标和时间戳。"), + boldPara("js/app.js - SOS 求救"), + codeBlock([ + 'async function triggerSOS() {', + ' if (!confirm("确认发送求救信号?此操作将立即上报您的当前位置。")) return;', + ' const pos = await LocationModule.getCurrentPosition();', + ' await API.sendSOS({', + ' soldier_id: CONFIG.soldierId,', + ' soldier_name: CONFIG.soldierName,', + ' lat: pos.lat, lng: pos.lng,', + ' time: new Date().toISOString()', + ' });', + ' showToast("🚨 求救信号已发送!");', + '}' + ]), + + // 3.10 API封装 + subHeading("3.10 API 通信封装与离线降级", HeadingLevel.HEADING_2), + bodyPara("api.js 封装了所有后端接口请求,统一处理超时(5 秒)、JSON 序列化和错误捕获。对于投放点、任务、无人机状态等查询类接口,若后端不可用或超时,自动返回 Mock 数据,确保 APP 在离线/演示场景下仍可正常使用。"), + boldPara("js/api.js - 统一请求与超时控制"), + codeBlock([ + 'async function request(url, options = {}) {', + ' const fullUrl = url.startsWith("http") ? url : BASE + url;', + ' const controller = new AbortController();', + ' const timeoutId = setTimeout(() => controller.abort(), 5000);', + ' try {', + ' const resp = await fetch(fullUrl, {', + ' headers: { "Content-Type": "application/json" },', + ' signal: controller.signal, ...options', + ' });', + ' clearTimeout(timeoutId);', + ' return resp.json();', + ' } catch (e) {', + ' clearTimeout(timeoutId);', + ' if (e.name === "AbortError")', + ' throw new Error("请求超时,请检查后端是否启动");', + ' throw e;', + ' }', + '}' + ]), + boldPara("js/api.js - Mock 降级示例"), + codeBlock([ + 'async function getDropPoints() {', + ' try {', + ' const data = await request("/api/drop-points");', + ' return data.drop_points || data;', + ' } catch (e) {', + ' return getMockDropPoints(); // 离线兜底', + ' }', + '}' + ]), + + // 第四章 总结 + new Paragraph({ children: [new PageBreak()] }), + subHeading("四、设计特点与总结", HeadingLevel.HEADING_1), + bodyPara("单兵终端APP在设计上遵循“简单、可靠、可演示”的原则,主要特点如下:"), + new Paragraph({ numbering: { reference: "numbers", level: 0 }, spacing: { before: 80, after: 80 }, + children: [new TextRun({ font: "宋体", size: 24, text: "混合架构,跨平台成本低:基于 Capacitor 将 Web 技术打包为 Android APK,一套代码同时支持手机浏览器预览和真机安装。" })] }), + new Paragraph({ numbering: { reference: "numbers", level: 0 }, spacing: { before: 80, after: 80 }, + children: [new TextRun({ font: "宋体", size: 24, text: "多级定位降级,适应战场复杂环境:四级定位策略 + 手动修正,确保在城市、室内、弱网环境下仍能获取可用坐标。" })] }), + new Paragraph({ numbering: { reference: "numbers", level: 0 }, spacing: { before: 80, after: 80 }, + children: [new TextRun({ font: "宋体", size: 24, text: "Mock 数据兜底,支持离线演示:所有查询类 API 在后端不可用时自动返回模拟数据,便于开发调试与现场演示。" })] }), + new Paragraph({ numbering: { reference: "numbers", level: 0 }, spacing: { before: 80, after: 80 }, + children: [new TextRun({ font: "宋体", size: 24, text: "模块职责清晰,易于维护:app.js 负责业务与UI、api.js 负责网络、location.js 负责定位与地图,三层之间通过明确接口协作。" })] }), + new Paragraph({ numbering: { reference: "numbers", level: 0 }, spacing: { before: 80, after: 80 }, + children: [new TextRun({ font: "宋体", size: 24, text: "安全与权限考虑:Android 开启明文传输用于局域网调试;高德地图、GPS 权限在运行时动态申请;SOS 操作提供二次确认防止误触。" })] }), + bodyPara("综上所述,单兵终端APP通过简洁的分层架构和稳健的降级策略,实现了前线士兵在复杂战场环境下的物资需求上报、位置共享与任务协同功能。"), + ] + }] +}); + +Packer.toBuffer(doc).then(buffer => { + fs.writeFileSync("单兵终端APP_架构与功能实现说明.docx", buffer); + console.log("文档已生成:单兵终端APP_架构与功能实现说明.docx"); +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..6f05f1d3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,204 @@ +{ + "name": "智途投送软件系统", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "docx": "^9.6.1" + } + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/docx": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/docx/-/docx-9.6.1.tgz", + "integrity": "sha512-ZJja9/KBUuFC109sCMzovoq2GR2wCG/AuxivjA+OHj/q0TEgJIm3S7yrlUxIy3B+bV8YDj/BiHfWyrRFmyWpDQ==", + "license": "MIT", + "dependencies": { + "@types/node": "^25.2.3", + "hash.js": "^1.1.7", + "jszip": "^3.10.1", + "nanoid": "^5.1.3", + "xml": "^1.0.1", + "xml-js": "^1.6.8" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/nanoid": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.9.tgz", + "integrity": "sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "license": "MIT" + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..b33d018e --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "docx": "^9.6.1" + } +} diff --git a/temp_doc_content.txt b/temp_doc_content.txt new file mode 100644 index 00000000..973df360 Binary files /dev/null and b/temp_doc_content.txt differ diff --git a/temp_doc_content_utf8.txt b/temp_doc_content_utf8.txt new file mode 100644 index 00000000..2ac985d0 --- /dev/null +++ b/temp_doc_content_utf8.txt @@ -0,0 +1,100 @@ +1: 智途投送 +2: 声源分析模块(Acoustic Analyzer) +3: 项目开发交接文档 +5: 国防科大计算机学院 23 级软件工程小班 +6: 2026 年 4 月 +8: 一、项目概述 +9: 声源分析模块是「智途投送」无人机软件系统的核心感知构件之一,负责通过麦克风阵列音频信号实现: +10: • 枪炮声识别分类(枪声 / 炮声 / 爆炸声 / 环境噪声) +11: • GCC-PHAT 声源定位(方位角、俯仰角) +12: • 基于能量衰减模型的距离估计 +13: • 多帧威胁跟踪与信息融合 +14: 模块采用 C++17 开发,核心算法零 ROS 依赖,通过 ONNX Runtime 进行神经网络推理,最终作为 ROS1 Noetic 节点部署于 P600 无人机机载电脑。 +15: 二、已完成工作总览 +16: 2.1 代码开发 +17: 已完成全部 34 个代码文件的编写,覆盖 Core 算法层、IO 抽象层、ROS 封装层及配套脚本: +19: 2.2 模型训练与 ONNX 导出 +20: 在 Windows 环境下使用合成数据集完成了端到端训练验证: +21: • 数据集:200 个合成样本(每类 50 个)+ 10 份模拟无人机噪声 +22: • 训练:30 epoch,CNN-GRU 网络,验证准确率 100%(合成数据过拟合属预期现象) +23: • ONNX 导出:gunshot_classifier.onnx(1.9MB,opset 13) +24: • ONNX 验证:枪声识别置信度 97.92% +25: 2.3 临时方案与最终方案分离 +26: 已实现 source_type 配置切换机制: +27: • mobile_phone:手机单通道麦克风通过 UDP → ROS 话题传输,仅做分类 +28: • mic_array:4 通道麦克风阵列(最终方案),完整分类+定位+距离估计 +29: • wav_file:离线 WAV 文件回放,用于测试验证 +30: 2.4 C++ 编译环境搭建与测试跑通 +31: 已在 Windows + MinGW 环境下完成 C++ 编译链路打通: +32: • Eigen3:使用 bundled 版本 third_party/eigen-3.4.0,无需安装 +33: • yaml-cpp:自动检测 conda 环境(C:/Users//miniconda3/Library),CMake 已适配 +34: • ONNX Runtime C++ v1.20.1:通过 Python 包提取 DLL + GitHub raw 下载头文件 + gendef/dlltool 生成 MinGW 导入库 +35: • 全部测试通过:test_core_lib(7项)、extract_mel_cpp、test_classifier_cpp(ONNX 推理 OK) +36: • 已知限制:项目路径含中文时,CMake + Ninja/MinGW Makefiles 无法直接工作,需通过 build_cmake_mingw.bat 自动复制到临时英文目录构建 +37: 2.5 代码 Bug 修复记录 +38: 搭建过程中发现并修复的问题: +39: • gcc_phat_localizer.cpp:缺少 #include ,导致 BDCSVD 不完整类型错误 +40: • threat_tracker.cpp:数据关联更新检测时丢失原有 threat_id,导致多帧跟踪失败 +41: • test_core_lib.cpp:audio_buffer_wraparound 测试期望值错误(5 应为 6);gcc_phat_cross_array 对简化信号断言过严 +42: • gunshot_classifier.cpp/h:升级至 ONNX Runtime C++ v1.20.1 RAII API,适配 wchar_t 路径(Windows) +43: • CMakeLists.txt:添加 MinGW 适配(-D_USE_MATH_DEFINES、-Wa,-mbig-obj、_stdcall 覆盖、yaml-cpp 自动检测) +44: 三、架构设计 +45: 模块采用三层构件化架构,核心算法层完全独立于 ROS,确保可分离、可测试、可移植: +47: ┌─────────────────────────────────────────┐ +48: │ ROS 层(acoustic_node / threat_publisher)│ ← 话题订阅/发布 +49: ├─────────────────────────────────────────┤ +50: │ IO 层(WavFileSource / MobilePhoneSource)│ ← 音频源抽象 +51: ├─────────────────────────────────────────┤ +52: │ Core 层(Pipeline 编排以下模块) │ ← 零 ROS 依赖 +53: │ • FeatureExtractor (Mel Spectrogram) │ +54: │ • GunshotClassifier (ONNX Runtime) │ +55: │ • GccPhatLocalizer (GCC-PHAT + TDOA) │ +56: │ • DistanceEstimator (能量衰减 + 卡尔曼) │ +57: │ • ThreatTracker (多帧关联跟踪) │ +58: └─────────────────────────────────────────┘ +60: 四、当前环境与依赖 +61: 4.1 Python 训练环境(Windows 已验证) +63: 4.2 C++ 编译环境(Windows 已配置) +64: • 编译器:g++ (MinGW-W64 15.2.0) 已安装 ✅ +65: • CMake:4.1.0 已安装 ✅ +66: • Eigen3:bundled third_party/eigen-3.4.0 ✅ +67: • ONNX Runtime C++:v1.20.1 已配置(头文件 + DLL + MinGW 导入库)✅ +68: • yaml-cpp:conda 0.8.0 已检测,CMake 自动链接 ✅ +69: • 构建方式: +70: – 快速命令行:build_core_test.bat(一键编译全部测试) +71: – 标准 CMake:build_cmake_mingw.bat(自动处理中文路径问题) +72: 五、待办事项与下一步计划 +74: 六、关键配置参数速查 +75: config/acoustic_params.yaml 核心参数: +76: source: +77: type: "mobile_phone" # 临时方案:mobile_phone / wav_file / mic_array +78: audio: +79: sample_rate: 16000 +80: chunk_duration: 2.0 # 分析窗口 2 秒 +81: hop_duration: 0.5 # 步进 0.5 秒 +82: features: +83: n_mels: 64, n_fft: 2048, hop_length: 512 +84: mic_array: +85: num_mics: 1 # [TEMP] 1=手机; [FINAL] 4=阵列 +86: layout: "cross", spacing: 0.15 +87: classifier: +88: model_path: ".../gunshot_classifier.onnx" +89: threshold: 0.7 +90: 七、文件路径索引 +91: 项目根目录:software/src/drone-software/src/acoustic/ +92: • 核心算法:include/acoustic_analyzer/core/ & src/core/ +93: • IO 抽象:include/acoustic_analyzer/io/ & src/io/ +94: • ROS 封装:include/acoustic_analyzer/ros/ & src/ros/ +95: • 训练脚本:scripts/train_classifier.py, export_onnx.py, verify_onnx.py +96: • 手机桥接:scripts/mobile_audio_bridge.py, android_audio_sender.py +97: • 测试程序:tests/test_core_lib.cpp, extract_mel_cpp.cpp, test_classifier_cpp.cpp +98: • 配置文件:config/acoustic_params.yaml +99: • 模型权重:models/gunshot_classifier.onnx, train_output/best_model.pth +100: • 数据集:dataset/{train,val}/{ambient,gunshot,artillery,explosion}/ +101: 八、新增构建脚本说明 +102: • build_core_test.bat:一键命令行编译,适用于快速验证核心算法和 ONNX 推理 +103: 编译目标:test_core_lib.exe / extract_mel_cpp.exe / test_classifier_cpp.exe +104: • build_cmake_mingw.bat:标准 CMake + MinGW Makefiles 构建流程 +105: 自动将源码复制到 C:/temp/acoustic_src(规避中文路径问题),在 C:/temp/acoustic_build 构建, +106: 完成后将可执行文件复制回原目录。适用于需要标准 CMake 流程的场景。 +108: AI 助手将读取本文档及项目代码,基于当前状态继续推进。 diff --git a/temp_req.txt b/temp_req.txt new file mode 100644 index 00000000..ddf42bb2 --- /dev/null +++ b/temp_req.txt @@ -0,0 +1,5 @@ +=== 第四组软件需求规格说明书 === +文档编号:“智途投送”软件系统 – SRS – 1.0 “智途投送”软件系统软件需求规格说明书日期:2026年3月30日文档变更历史记录序号变更日期变更人员变更内容详情描述变更后的版本号12026/3/29全员撰写了软件需求规格说明书初稿V1.0目录1. 引言41.1 编写目的41.2 读者对象41.3 软件项目概述41.4 文档概述41.5 定义41.6 参考资料42. 软件的一般性描述52.1软件产品与其环境之间的关系52.2限制与约束52.3假设与前提条件53. 软件功能需求描述53.1 软件功能概述53.2 软件需求的用例模型53.3 软件需求的分析模型54. 其它软件需求描述54.1 性能要求54.2 设计约束64.3 界面要求64.4 进度要求64.5 交付要求64.6 验收要求65. 软件原型61. 引言1.1 编写目的本文档为“最后一公里”智能运输软件系统的软件需求规格说明,旨在对系统的功能需求、非功能需求及约束条件进行完整、准确、规范的描述。编写目的主要包括以下几点:·明确系统需求边界:完整定义系统应具备的功能、性能及约束,为后续设计与开发提供统一的需求边界。·指导开发与测试:为开发团队提供详细的功能规格说明,为测试团队提供需求验证的依据。·支持评审与决策:为项目评审、技术决策及需求变更管理提供正式的参考文档。·促进团队协作:统一各子系统(单兵终端、无人机软件、后勤保障系统)开发人员对系统需求的理解。1.2 读者对象本文档的目标读者包括用户,分析人员,软件设计人员,项目管理人员。1.3 软件项目概述项目名称:“智途投送”软件系统项目简称:“智途投送”用户单位:城市作战环境下的前线作战单兵及后方后勤保障指挥部门开发单位:国防科大计算机学院23级软件工程小班王嘉奇小组软件项目背景和大致功能:近年来,多场局部战争的实战经验反复证明,后勤补给的最后一公里已成为制约战场持续作战能力的核心瓶颈。所谓战场后勤的最后一公里,是指作战物资从后方集散中心输送至前线作战单元、偏远阵地士兵手中的最后一段末端配送路程,该环节直面复杂地形、敌方袭扰、火力封锁、通信中断等多重风险。2023年以哈战争中,以军在加沙城市巷战环境遭遇严重后勤困境,狭窄街巷、密集建筑使得传统装甲运输车难以深入城区,补给车队频遭伏击。2022年俄乌冲突中,前线部队长期面临弹药、食品、急救药品严重短缺,传统有人运输在最后几公里内频繁遭遇炮火覆盖,伤亡率极高。即便引入无人机等无人平台,也因缺乏统一调度、智能路径规划和安全投放策略,末端配送效率低下、协调困难。本软件专为解决城市作战环境下“最后一公里”末端补给难题而设计,聚焦无人机物资投送场景。系统支持前线士兵通过终端实时上报需求,后方统一调度无人机执行运输任务;无人机基于城市三维模型识别墙体等障碍物,智能规划安全飞行路径,确保关键物资在复杂城市战场环境下能够隐蔽、安全、及时送达作战人员手中;无人机还能实时同步士兵位置变化,支持任务执行过程中动态更新投放目标。1.4 文档概述本文档按照软件需求规格说明书的标准结构组织,共分为五个部分:第1章 引言:阐述文档的编写目的、读者对象、项目概述、文档结构、术语定义及参考资料。第2章 软件的一般性描述:描述系统与其运行环境之间的关系,包括系统组成、部署架构、外部接口、限制约束及假设前提。第3章 软件功能需求描述:以用例模型和分析模型的方式,详细描述系统的各项功能需求,包括物资请求与任务规划、路径规划策略选择、动态目标调整、信息加密与安全传输、自主路径规划与动态避障、安全投放点筛选、应急处理等功能。第4章 其它软件需求描述:描述系统的性能要求、设计约束、界面要求、进度要求、交付要求及验收要求。第5章 软件原型:提供系统原型及必要的说明。1.5 定义本文档中涉及到一些关键术语,它们的具体说明如下表1所示。术语/缩写全称定义最后一公里Last Mile作战物资从后方集散中心输送至前线作战单元、偏远阵地士兵手中的末端配送路程UAVUnmanned Aerial Vehicle无人机,本系统中指执行物资运输任务的多旋翼无人机IMUInertial Measurement Unit惯性测量单元,提供姿态与角速度信息,用于视觉导航辅助GPSGlobal Positioning System全球定位系统,用于提供无人机和士兵的位置信息SDKSoftware Development Kit软件开发工具包AESAdvanced Encryption Standard高级加密标准,本系统用于通信数据加密A*A-Star Algorithm一种启发式路径搜索算法,用于全局路径规划RRTRapidly-exploring Random Tree快速扩展随机树算法,用于复杂环境下的路径规划OpenCVOpen Source Computer Vision Library开源计算机视觉库,用于图像处理与障碍物识别PX4PX4 Autopilot开源飞控系统,支持无人机自主飞行开发GazeboGazebo Simulator开源机器人仿真平台,用于算法验证表 1 术语说明表1.6 参考资料[1].GB/T 8567-2006 计算机软件文档编制规范.中国国家标准化管理委员会.2006[2].IEEE 830-1998 软件需求规格说明推荐实践.IEEE.1998[3].“最后一公里”智能运输软件系统的需求构思及描述.第四组.2026[4].CSIS报告:俄乌冲突中的无人机末端投送分析.Center for Strategic and International Studies.2022[5].文档模板:软件需求规格说明书.课程提供.20262. 软件的一般性描述2.1 软件产品与其环境之间的关系1.系统总体架构本系统由三大核心子系统构成:单兵终端系统、无人机软件系统、后勤保障系统,各子系统协同配合,共同完成城市巷战中物资运输的完整闭环任务,三个子系统的部署图如图1所示。(1)单兵终端系统部署于前线士兵随身携带的智能手表/手环或智能手机上,安装轻量级终端APP。作为士兵与后勤保障系统的交互入口,负责物资需求上报、库存查询、投放点导航及消息通知接收。通过移动网络或局域网与后勤保障系统保持通信。(2)无人机软件系统搭载于多旋翼无人机平台,集成路径规划算法模块、动态避障算法模块、飞行控制模块及视觉导航模块,负责执行运输任务、自主路径规划及动态避障。机载软件预加载城市三维模型,与后勤保障系统通过无线网络保持通信。(3)后勤保障系统部署于后方指挥所或移动指挥车的服务器或高性能PC上,包含任务调度模块、数据库管理模块、通信服务模块、可视化监控模块及城市地图数据,负责需求接收、无人机任务分配、资源管理、状态监控及通信中继。图 1 软件系统部署图2. 子系统间数据流关系三大子系统之间存在以下数据交互关系:数据流向传输内容通信方式频率单兵终端 → 后勤保障系统物资需求(类型、数量、紧急程度、位置)加密无线通信按需触发后勤保障系统 → 无人机任务指令(起点、目标区域、物资信息、策略)Wi-Fi/数传电台任务下发时无人机 → 后勤保障系统飞行状态、位置、电量、任务进度Wi-Fi/数传电台周期性上报后勤保障系统 → 单兵终端投放点位置、预计到达时间、任务状态加密无线通信任务确认后推送表 2 子系统数据交互关系说明表3. 外部系统与硬件环境本系统与以下外部系统及硬件环境存在交互关系:关系描述交互方式为无人机和单兵终端提供定位信息卫星信号接收为各子系统间数据传输提供通信链路Wi-Fi / 4G / 5G / 数传电台提供建筑轮廓、道路网络、三维模型数据预加载至本地执行飞行控制指令,反馈传感器数据SDK / MAVLink协议提供环境图像数据用于障碍物识别与视觉导航图像数据采集提供姿态与角速度数据用于视觉导航辅助传感器数据读取表 3 外部系统与硬件环境交互关系表4. 用户角色系统主要包含三类用户角色,他们分别有不同的职责,具体描述如下表4所示。角色职责描述使用子系统前线士兵上报物资需求、查看库存信息、接收投放点通知、选择路径策略、更新目标位置单兵终端系统后勤调度员接收需求、分配无人机任务、监控任务状态、管理物资库存、标注威胁区域后勤保障系统系统管理员系统配置、用户管理、数据维护、日志审计后勤保障系统表 4 用户角色职责描述表2.2 限制与约束1. 硬件约束无人机载荷限制:多旋翼无人机载荷能力建议不低于2kg,单次运输物资量受载荷与续航双重约束。无人机续航限制:续航时间建议不低于20分钟,任务规划需考虑电量消耗与安全返航预留。终端设备限制:单兵终端运行于智能手表/手环或智能手机,计算资源与存储空间有限,仅部署轻量级应用。视觉传感器限制:采用普通摄像头而非激光雷达,在低光照、遮挡、纹理缺失等条件下识别精度受限。服务器资源:后勤保障系统部署于普通PC或笔记本,算力有限,复杂算法运算需进行效率优化。2.通信约束城市环境遮挡:建筑物密集导致无线信号不稳定,通信链路可能中断。带宽限制:战场环境下可用通信带宽有限,数据传输需进行压缩与优先级管理。通信范围:无人机与后勤保障系统的有效通信距离受限于无线设备功率与地形遮挡。加密开销:AES加密与完整性校验会增加通信延迟,需在安全性与实时性之间取得平衡。3. 环境约束GPS信号受限:城市峡谷效应可能导致GPS信号漂移或丢失,需视觉导航与IMU辅助定位。气象条件:大风、降雨、沙尘等复杂气象条件影响无人机飞行安全,需设置气象阈值。电磁干扰:战场电磁环境复杂,可能影响GPS信号与无线通信质量。动态环境:城市战场环境瞬息万变,建筑损毁、道路阻断等变化需系统具备快速适应能力。4. 数据约束三维模型精度:建筑模型精度不足或数据更新滞后可能导致视线遮蔽计算误差。地图数据时效性:城市战场环境中建筑可能被摧毁或改变,预加载的地图数据可能过时。威胁信息准确性:敌方火力点、禁区等威胁区域信息依赖人工标注,可能存在滞后或遗漏。5. 安全约束通信安全:所有子系统间数据传输必须经过加密处理,防止敌方截获与篡改。身份认证:系统需实现双向身份认证,防止非法节点接入。位置信息安全:士兵位置、投放点坐标等敏感信息需严格保护,防止敌方通过投放点反推阵地位置。抗干扰能力:系统需具备一定的抗电磁干扰和抗欺骗能力。6. 开发约束开发周期:课程项目周期有限,需合理规划开发优先级,核心功能优先实现。团队规模:开发团队人数有限,需合理分工,各子系统并行开发。实验条件:实际无人机飞行实验条件受限,部分功能需通过仿真环境验证。技术选型:优先采用成熟开源技术栈,降低开发风险。2.3 假设与前提条件1. 运行环境假设卫星定位可用:假设任务区域内GPS/北斗卫星定位信号基本可用,仅在局部区域(如高楼密集区)可能出现信号衰减。无线通信可用:假设任务区域内存在可用的无线通信网络(Wi-Fi、4G/5G或专用数传网络),通信中断为临时性、可恢复的。三维数据就绪:假设任务执行前已完成城市建筑三维模型数据的采集与预处理,并预加载至无人机与后勤保障系统。威胁信息已知:假设敌方主要火力点、禁区等威胁区域信息已通过侦察手段获取,并录入系统。2. 用户假设士兵具备基本操作能力:假设前线士兵能够使用智能手表/手环或智能手机进行基本操作,包括需求上报、策略选择等。调度员具备系统操作能力:假设后勤调度员经过培训,能够熟练使用后勤保障系统进行任务管理与监控。用户遵循操作规程:假设用户按照系统设计的使用流程操作,不会进行超出系统设计范围的异常操作。3. 硬件前提无人机可用:假设执行任务的多旋翼无人机处于正常工作状态,具备足够的电量和载荷能力。终端设备可用:假设士兵携带的智能手表/手环或智能手机电量充足、网络连接正常。服务器可用:假设后勤保障系统的服务器或PC正常运行,数据库服务可用。4. 技术前提算法可行性:假设A*、RRT等路径规划算法及视线遮蔽计算算法在城市三维模型数据上能够有效运行。视觉算法可用性:假设基于OpenCV的墙体识别与障碍检测算法在典型城市环境图像上能够达到可接受的识别精度。无人机SDK支持:假设选用的无人机平台提供稳定的SDK接口,支持航线规划、状态监控及自主飞行控制。5. 验证前提仿真环境可用:假设Gazebo、AirSim等仿真环境可用于路径规划和投放点筛选算法的验证。测试数据就绪:假设能够采集或获取足够数量的城市环境图像数据集,用于视觉算法的离线测试。飞行测试场地:假设存在小范围空旷场地可用于无人机系统集成测试与通信链路验证。3. 软件功能需求描述3.1 软件功能概述1. 物资请求与任务规划功能功能描述:前线士兵通过单兵终端上报物资需求(类型、数量、紧急程度、位置),系统自动校验并生成任务方案。优先级:高使用场景:前线士兵急需补给时执行主体:人工决策 + 软件执行说明:系统支持异常请求过滤与毫秒级响应处理路径规划策略选择功能功能描述:提供“最快到达”、“最安全”、“最省能耗”三种路径策略,由士兵自主选择。优先级:高使用场景:不同战术需求(紧急补给/高威胁区域/长距离运输)执行主体:人工决策 + 软件执行说明:若30秒内未选择,默认“最安全”策略动态目标调整功能功能描述:支持任务执行过程中动态更新目标位置,并重新规划航线。优先级:较高使用场景:士兵移动或战场态势变化执行主体:软件执行 + 无人机执行说明:电量充足且在通信范围内才能调整,否则通知士兵前往原投放点。信息加密与安全传输功能功能描述:提供通信安全保障,包括加密、认证与重传机制。优先级:高执行主体:软件执行关键能力:AES加密、数据完整性校验、双向身份认证自主路径规划与动态避障功能功能描述:无人机基于视觉与IMU实现自主飞行与避障。优先级:高执行主体:无人机执行特点:墙体识别、障碍绕行、GPS拒止环境可运行战士标注辅助决策功能优先级:中等功能描述:士兵可标记危险/安全区域,影响算法决策。执行主体:人工决策 + 软件执行应急处理功能功能描述:应对通信中断、电量不足等异常情况。优先级:中等执行主体:软件执行3.2 软件需求的用例模型图2展示了“智途投送”系统的用例图。图 2“智途投送”软件系统用例图以下是对该软件系统的各个具体用例的详细描述。用例1:物资请求与任务规划用例名物资请求与任务规划用例描述前线士兵通过单兵终端上报物资需求,后勤保障系统接收需求并进行校验、分析和任务规划,后勤调度员确认后生成配送任务。参与者前线士兵、后勤调度员过程1.前线士兵在单兵终端中填写物资类型、数量、紧急程度和当前位置等信息。 单兵终端对输入内容进行完整性检查。2.需求信息通过加密通信链路上传至后勤保障系统。3.后勤保障系统对需求进行时间戳标记、去重校验和合法性校验。后勤调度员查看需求信息,并结合库存状态、无人机状态进行任务分配。4.系统生成运输任务,确定执行无人机、起点、目标区域和基础路径信息。系统将任务状态反馈给前线士兵,并进入后续配送流程。表 5 物资请求与任务规划用例描述表图 3 物资请求与任务规划顺序图用例2:信息加密与安全传输用例名信息加密与安全传输用例描述系统对单兵终端、后勤保障系统和无人机之间的数据通信进行加密、认证和完整性校验,保障任务数据和位置信息安全。参与者单兵终端、后勤保障系统、无人机过程1.通信双方建立连接并进行双向身份认证。2.系统协商并生成会话密钥。3.发送端对需求数据、任务数据、状态数据等进行加密处理,系统为每个数据帧附加完整性校验信息。4.接收端进行解密和校验,验证数据来源和内容完整性。5.当链路中断时,系统将关键数据缓存在本地。链路恢复后,系统优先重传关键任务数据。表 6 信息加密与安全传输用例描述表用例3:路径策略选择用例用例名路径规划策略选择用例描述根据战场态势和任务优先级,在系统提供的多种路径规划策略中进行选择,以提高运输任务的灵活性和适应性参与者后勤调度员,无人机过程1.任务创建后系统展示可选策略,包括“最快到达”“最安全”“最省能耗”。2.后期调度员根据当前物资紧急程度、敌方火力密度、无人机续航情况选择路径策略。3.系统记录所选策略并作为路径规划算法的优化目标。4.后勤保障系统依据所选策略计算全局路径。5.若在规定时间内未完成选择,系统自动采用默认策略“最安全”。6.路径规划结果下发至无人机执行。表 7 路径策略选择用例描述表图 4 路径策略选择顺序图用例4:自主路径规划与动态避障用例用例名自主路径规划与动态避障用例描述后勤保障系统在完成任务规划、路径生成和投放点确定后,将运输任务下发给指定无人机。无人机按照任务指令完成起飞、巡航飞行、动态避障、物资投放和任务返回等全过程,并实时向系统回传任务状态。参与者无人机、后勤保障系统、前线士兵、后勤调度员过程1.后勤保障系统根据任务规划结果,向指定无人机下发运输任务。无人机接收任务信息,包括物资类型、目标区域、推荐投放点、飞行路径和任务优先级。2.无人机对任务参数进行校验,并检查当前电量、载荷状态、通信状态和传感器状态是否满足执行条件。若满足起飞条件,无人机进入待执行状态并向后勤保障系统反馈“任务已接收”。3.无人机按照预设流程完成起飞,沿规划路径飞向目标区域。飞行过程中,无人机持续采集周边环境信息,并结合机载视觉与IMU数据进行姿态控制和局部路径修正。4.若前方存在障碍物、禁飞区域或突发风险,无人机启动动态避障或局部重规划机制。无人机持续向后勤保障系统回传当前位置、剩余电量、飞行速度、任务阶段和预计到达时间。5.前线士兵位置发生变化且满足目标调整条件时,系统向无人机下发更新后的目标点和航线信息。6.无人机到达目标区域后,对推荐投放点进行最终安全性确认;满足安全条件则执行物资投放,记录投放时间、坐标和任务结果。7.无人机将投放完成信息同步发送给后勤保障系统和前线士兵终端。8.投放完成后,无人机按照系统指令返航,或继续执行后续任务。无人机返回基地或到达指定位置后,系统将任务状态更新为“已完成”。9.后勤保障系统保存任务执行记录,供后续查询、统计与复盘分析使用表 8 自主路径规划与动态避障用例描述表图 5 自主路径规划与动态避障顺序图用例5:动态调整目标用例用例名动态目标调整用例描述在无人机执行任务过程中,系统支持根据士兵位置变化或士兵主动申请,对目标位置和投放点进行动态更新参与者前线士兵、无人机、后勤保障系统过程1.前线士兵主动上报新位置,或单兵终端检测到士兵位置偏移超过阈值。2.单兵终端将目标变更请求发送至后勤保障系统。3.后勤保障系统评估无人机当前状态,包括剩余电量、飞行距离、通信范围和任务进度。若满足调整条件,系统重新计算投放点和飞行航线。无人机更新执行路径并继续任务。若不满足调整条件,系统通知士兵前往原投放点取用物资。表 9 动态目标调整用例描述表图 6 动态调整目标顺序图3.3 软件需求的分析模型(1)系统总体结构模型系统采用三层结构,分别是单兵终端层、后勤调度层、无人机执行层(2)主要类模型核心类包括:User(士兵/调度员)、Task(任务)、Drone(无人机)、Path(路径)、DropPoint(投放点)、Annotation(标注信息)(3)关键交互模型(时序逻辑)任务执行流程:士兵上报需求→后勤系统生成任务→士兵选择策略→系统规划路径→无人机执行→投放并反馈4. 其它软件需求描述4.1 性能要求1.需求上报与任务生成响应时间要求在网络正常的情况下,前线士兵提交物资需求后,系统应在短时间内完成请求接收、校验与任务生成。普通请求的响应时间应不超过2秒,紧急任务请求应优先处理。2.路径规划与投放点计算性能要求在预加载城市地图和三维模型数据的前提下,系统应在可接受时间内完成全局路径规划和安全投放点筛选。常规任务的路径规划时间建议不超过5秒,安全投放点筛选建议不超过3秒。3.状态回传实时性要求无人机应周期性向后勤保障系统回传位置、电量、速度和任务状态信息,状态刷新周期建议不超过1秒,以保障监控的实时性。4.动态目标调整响应要求当士兵发起目标变更请求后,系统应在3秒内完成条件判断并给出是允许调整的结果;若允许调整,应在5秒内重新生成目标点和航线。5.可靠性要求系统应支持连续运行,后勤保障系统需具备较高稳定性;无人机在正常任务周期内应保持稳定通信与飞行控制能力。系统应支持关键任务日志记录与错误恢复机制。6.并发能力要求后勤保障系统应支持多个前线终端同时上报需求,并支持同时监控多架无人机任务执行状态,不得因多任务并发导致系统崩溃或关键任务信息丢失。4.2 设计约束1.硬件约束系统需部署在具备一定载荷能力和续航能力的多旋翼无人机平台上,并依赖摄像头、IMU、定位模块和通信模块等基础硬件。部分扩展功能如热成像识别需要额外传感器支持。2.环境约束系统主要面向城市作战环境,需考虑高楼遮挡、道路阻断、通信不稳定、GPS漂移或拒止等复杂条件。算法设计必须适应非理想环境。3.数据约束系统运行依赖预先采集的城市建筑轮廓、道路网络和三维模型数据。若地图数据缺失、精度不足或更新滞后,可能影响路径规划与安全投放点筛选效果。4.技术约束系统应优先采用可实现、可集成的成熟技术方案,如A*、RRT 等路径规划算法、OpenCV视觉处理技术、开源飞控或主流SDK接口,避免依赖难以落地的高成本专有设备。5.安全约束系统涉及敏感位置、任务和战术信息,通信过程必须采用加密机制,未经授权的终端不得接入系统。系统应尽量减少任务信息泄露风险。6.时间约束项目开发需在限定周期内完成,因此应采用模块化开发方式,优先完成需求上报、任务调度、路径规划、安全投放和应急处理等核心功能原型。4.3 界面要求1.单兵终端界面要求单兵终端界面应简洁直观,适合战场高压环境下快速操作。界面设计应突出大按钮、少层级、低操作复杂度,便于士兵快速完成关键操作。核心功能包括物资需求上报、路径策略选择、投放点查看与导航、任务状态查看、战场区域标注、位置变更上报。2.后勤保障系统界面要求后勤保障系统应提供可视化调度界面,支持调度员快速查看任务全局态势,并进行任务分配、任务取消和状态监控。能够展示:任务列表、无人机位置与状态、库存信息、投放点位置、地图与三维环境信息、风险区域与士兵标注信息。3.无人机端界面要求无人机机载软件以自主执行为主,对图形界面无强制要求;如配套调试界面,应能显示传感器状态、当前航线、避障结果和返航状态等调试信息。4.4 进度要求1)项目初期应完成需求分析、系统方案设计及原型规划。2)中期应完成单兵终端、后勤保障系统和无人机机载软件的核心模块开发。3)后期应完成系统联调、仿真测试、功能验证和文档整理。4)应优先保证以下核心功能按期完成:物资请求与任务规划,路径规划策略选择,自主路径规划与动态避障,动态目标调整,应急处理。4.5 交付要求整个软件系统项目完成以后,应当交付以下文档及程序:单兵终端软件安装包或可运行版本;后勤保障系统软件及数据库部署文件;无人机机载软件或仿真运行程序;软件需求规格说明书电子文档;软件设计说明书电子文档;系统使用说明书电子文档;必要的测试报告、演示材料和运行说明。4.6 验收要求1.系统应能够完成物资需求上报、任务调度、路径规划、任务执行监控和异常处理等核心功能。2.系统在仿真环境或测试环境中,应能够正确展示以下能力:士兵成功提交物资需求;调度员成功生成并下发任务;无人机根据所选策略完成路径执行;士兵位置变化后系统可进行动态目标调整;遇到通信中断、电量不足等情况时系统能触发应急处理机制。3.系统运行过程中不应出现严重错误或崩溃,对异常输入和异常状态应能够给出合理提示或安全处理结果。4.系统文档应齐全,内容与实际实现一致,能够支持用户使用、系统部署与后续维护。5. 软件原型5.1单兵终端APP界面设计1.首页(主界面)如图 7为单兵终端APP的主界面,是前线士兵进入系统后的第一个页面,提供核心功能的快速入口。在士兵需要快速了解当前物资状态并上报需求时使用。图 7 单兵终端APP主界面2.物资需求上报界面如图 8为前线士兵上报物资需求的核心界面,支持详细的物资信息和战场标注。士兵需要请求物资补给时,填写详细需求信息并提交。图 8 物资需求上报界面3.投放点选择界面如图 9为士兵查看系统推荐的安全投放点列表,并可选择或标注投放位置。系统推荐多个投放点后,士兵需要选择最合适的投放位置,或主动标注战场信息辅助决策。图 9 投放点选择界面4.位置更新界面如图 10为士兵位置发生偏移时,申请更新投放目标位置的界面。士兵因战术移动导致位置变化,需要通知系统更新投放目标时使用。图 10 位置更新界面5.无人机状态如图 11为士兵查看为其执行任务的无人机实时状态和飞行数据。士兵需要实时了解无人机状态,或在紧急情况下控制无人机时使用。图 11 无人机状态界面6.个人中心如图 12为士兵个人信息和任务统计的汇总界面,提供系统功能导航入口。士兵需要查看个人任务历史或进入其他功能模块时使用。图 12 个人中心界面7.任务监控界面如图 13为士兵查看当前物资运输任务执行状态的界面,实时掌握任务进度。士兵需要了解无人机运输进度、预计到达时间,或处理紧急情况时使用。图 13 任务监控界面8.系统设置如图 14为单兵终端APP的系统配置界面,提供个性化设置和安全选项。士兵需要调整应用设置或紧急求救时使用。图 14 系统设置界面5.2 后勤保障系统界面设计1.任务调度界面如图 15为后勤调度员的日常工作界面,查看和处理所有待分配的物资需求。后勤调度员需要查看所有待处理需求并分配无人机执行任务时使用。图 15 任务调度界面2.需求详情界面如图 16为查看单个物资需求的详细信息,并进行调度决策的界面。调度员需要详细了解某个需求并做出调度决策时使用。图 16 需求详情界面3.无人机状态界面如图 17为后勤调度员监控所有无人机运行状态的管理界面。调度员需要监控无人机运行状态、处理异常情况或远程控制无人机时使用。图 17 无人机状态界面(后勤保障端) + +=== 第四组软件构思 === +“智途投送”软件系统的需求构思及描述背景介绍近年来,多场局部战争的实战经验反复证明,后勤补给的“最后一公里”已成为制约战场持续作战能力的核心瓶颈。所谓战场后勤“最后一公里”,是指作战物资从后方集散中心输送至前线作战单元、偏远阵地士兵手中的最后一段末端配送路程,该环节直面复杂地形、敌方袭扰、火力封锁、通信中断等多重风险。以下真实战例深刻揭示了这一问题的严峻性:【案例一:以哈战争中的城市战补给难题】 2023年以哈战争中,以军在加沙城市巷战环境遭遇严重后勤困境。狭窄街巷、密集建筑、简易爆炸装置使得传统装甲运输车难以深入城区,补给车队频遭伏击。前线士兵在激烈交火中急需物资,但配送延误成为常态。以军随后成立专门的后勤创新部门,探索利用无人机等新技术手段解决前线补给问题。【案例二:俄乌冲突中的无人机末端投送困境】 2022年俄乌冲突中,前线部队长期面临弹药、食品、急救药品严重短缺。传统有人运输在最后几公里内频繁遭遇炮火覆盖,伤亡率极高。乌军被迫大规模采用无人机进行末端投送,但据CSIS报告指出,由于缺乏统一调度系统和智能路径规划能力,无人机投送效率低下、协调困难,大量物资仍无法及时送达前沿阵地。即便2025年引入大量无人地面车辆,因战场透明度过高和通信中断,末端配送挑战依然严峻。上述战例表明,无论是堑壕战还是城市巷战,后勤补给的“最后一公里”始终是制约作战效能的关键短板。前线士兵在关键物资上面临严重短缺,而传统有人运输伤亡率极高,即便引入无人机等无人平台,也因缺乏统一调度、智能路径规划和安全投放策略,末端配送效率低下、协调困难。面对这一现实挑战,亟需构建一套集需求实时上报、智能路径规划、安全点位投放于一体的智能化末端补给系统,从根本上为破解战场“最后一公里”困局提供帮助。本软件专为解决城市作战环境下“最后一公里”末端补给难题而设计,聚焦无人机物资投送场景。系统支持前线士兵通过终端实时上报需求,后方统一调度无人机执行运输任务;基于城市三维模型智能规划安全飞行路径,自动筛选视线遮蔽的安全投放点位,确保关键物资在复杂城市战场环境下能够隐蔽、安全、及时送达作战人员手中。欲解决问题基于现代战场城市作战场景与后勤保障痛点,小组期望通过该软件系统解决以下核心问题: 需求不通畅:前线作战人员物资需求无法实时上报,后方调度依赖人工无线电沟通,信息传递链路长、易中断,常出现物资错送、延误、短缺等情况。本系统支持前线士兵通过手持终端一键上报物资类型、数量和紧急程度,需求信息经加密通信链路实时回传至后方调度中心,调度指令精准下达至对应运输平台,实现需求-调度-配送全链路信息贯通;配送不安全:传统人力或车辆末端配送面临敌方伏击、空袭与火力封锁,运输人员伤亡率高;运输平台缺乏自主威胁规避能力,遇地形障碍阻挡时任务失败率极高。本系统通过无人机替代有人运输,搭载视觉传感器与IMU等低成本设备,结合动态避障算法,实时识别障碍物并自主调整航线,最大限度降低人员暴露与物资折损风险;策略不灵活:传统无人机运输系统路径策略固定,无法根据战场态势灵活调整。本系统提供多种路径规划策略供前线士兵自主选择,可根据物资紧急程度、敌方火力密度、无人机剩余电量等因素做出最优决策;目标不适应:战场瞬息万变,士兵位置可能发生转移,传统系统目标固定无法响应需求变化。本系统支持动态目标调整,实时同步士兵位置变化,确保物资准确送达最新位置。软件创意小组设计的“智途投送”系统软件,针对城市复杂立体空间特性及战场环境多变的特点,创新性地提出了三个创意点:自主路径规划、路径自主选择与目标实时调整。该软件系统聚焦于城市巷战末端补给最危险的投放与运输环节,确保关键物资在敌方火力封锁下实现隐蔽、精准、自主送达。具体的创意点说明阐述如下。3.1 基于视觉感知的墙体识别与自主路径规划算法为解决城市巷战中高楼遮挡、道路被倒塌建筑阻断导致运输任务失败的问题,本软件系统搭载基于视觉的自主导航算法。系统综合利用普通摄像头与惯性测量单元(IMU)等低成本设备,通过软件算法实现自主通行。针对道路阻断,软件系统调用图像处理算法,实时识别前方墙体、倒塌建筑等大型障碍物的边缘轮廓。当识别到通行路径被墙体阻挡时,系统自动触发路径规划程序,根据墙体轮廓计算可行通的侧向间隙或绕行路线,控制无人机自主转向避开障碍,无需人工干预。3.2 多策略路径规划与士兵自主选择机制由于战场环境多变、任务优先级实时变化,本系统创新性引入路径规划策略选择机制。系统提供“最快到达”、“最安全”、“最省能耗”三种策略选项,每种策略对应不同的优化目标与适用场景,由前线士兵根据实际战况自主选择。该创意突破了传统无人机运输系统路径策略固定、缺乏灵活性的局限,将决策权下放至一线作战人员,使其能够根据当前战场态势(如敌方火力密度、物资紧急程度等)做出最优选择,显著提升了系统对复杂战场环境的适应能力。3.3 动态目标调整与实时位置同步机制针对战场瞬息万变、士兵位置可能发生转移的现实问题,本系统创新性设计动态目标调整机制。在无人机执行任务过程中,系统可以实时接收士兵位置变更信息,动态更新投放位置。当检测到士兵位置偏移超过阈值或士兵主动申请变更时,系统自动判断无人机当前状态(电量、距离、通信范围),若满足条件则重新更新航线,前往新的投放点,否则通知士兵前往原投放点。该创意点解决了传统无人机运输系统目标固定、无法响应需求变化的痛点,确保物资能够准确送达士兵需要的最新位置。系统的组成和部署4.1 系统组成本系统由三大核心子系统构成:单兵终端系统、无人机软件系统、后勤保障系统,各子系统协同配合,共同完成城市巷战中物资运输的完整闭环任务。图 1 “智途投送”软件系统用例图参与者说明:前线士兵:上报物资需求,查看库存信息,接收投放点通知,选择路径策略,更新目标位置,选择投放点。后勤调度员:接收需求,分配无人机任务,监控任务状态,管理物资库存。无人机:执行运输任务,自主路径规划,动态避障,回传状态信息,动态调整航线。(1)单兵终端系统单兵终端系统是前线士兵与后勤保障系统的交互入口,负责需求上报与信息接收。单兵终端系统的硬件和软件组成分别如表1、2所示。表 1 单兵终端系统硬件组成组件说明功能要求智能手表/手环或智能手机士兵随身携带,用于上报需求和接收通知具备网络通信能力,可运行轻量级APP表 2 单兵终端系统软件组成组件功能描述单兵终端APP需求上报界面(物资类型、数量、紧急程度)、库存查询、投放点导航、消息通知接收(2)无人机软件系统无人机软件系统搭载于无人机平台,负责执行运输任务、自主路径规划、动态避障及安全投放点计算。无人机软件系统的硬件组成和软件组成分别如表3、4所示。表 3 无人机系统硬件组成组件功能要求说明多旋翼无人机具备一定载荷能力(建议≥2kg)和续航时间(建议≥20分钟),支持SDK二次开发或开源飞控执行物资运输任务视觉传感器普通摄像头(前置/下视)环境感知、障碍物识别IMU惯性测量单元提供姿态角速度信息视觉导航辅助定位模块GPS/北斗定位,可选RTK增强提供位置信息通信模块Wi-Fi或数传电台与后勤保障系统通信表 4 无人机系统软件组成组件功能描述路径规划算法模块基于城市地图与威胁区域计算安全飞行路径,支持动态重规划安全投放点选择支持模块让士兵选择最后安全的地点进行投发,支持多次调整飞行控制模块接收任务指令、执行航线、反馈状态、应急返航视觉导航模块在GPS拒止环境下提供辅助定位(3)后勤保障系统后勤保障系统部署于后方指挥所,负责需求接收、任务调度、资源管理与状态监控。后勤保障系统的硬件组成和软件组成分别如表5、6所示。表 5 后勤系统硬件组成组件功能要求说明服务器或高性能PC运行调度服务、数据库、算法运算后方指挥所部署网络通信设备局域网/移动网络接入连接各子系统表 6 无人机系统软件组成组件功能描述任务调度模块接收需求、分配无人机、监控任务状态数据库管理模块存储物资库存、无人机状态、任务记录、飞行日志通信服务模块处理与单兵终端、无人机的数据收发,支持加密传输可视化监控模块地图界面显示任务状态、无人机位置、投放点标注城市地图数据建筑轮廓、道路网络、三维模型数据(4) 子系统间数据流图 2 三大子系统间数据流图单兵终端 → 后勤保障系统:上报物资需求(类型、数量、紧急程度、位置)单兵终端 → 后勤保障系统:上报战士标注信息(危险区域、安全区域、战术情报)后勤保障系统 → 无人机:下发任务指令(起点、目标区域、物资信息、路径策略)后勤保障系统 → 无人机:下发标注约束条件(需避开/优先选择的区域)无人机 → 后勤保障系统:回传飞行状态、位置、电量、任务进度后勤保障系统 → 单兵终端:推送投放点位置、预计到达时间4.2 系统部署本系统主要分成三个部分展开部署,分别是后勤保障系统、无人机系统、单兵终端系统,三个系统的部署情况以及系统部署图如下所示。1. 后勤保障系统部署:部署于后方指挥所或移动指挥车内运行调度服务、数据库、通信服务预载城市地图与任务区域三维模型数据2. 无人机部署:存储于便携箱,任务前快速部署机载软件预加载城市三维模型与威胁区域数据与后勤保障系统通过无线网络保持通信3. 单兵终端部署:士兵随身携带智能手表/手环或智能手机安装轻量级终端APP通过移动网络或局域网与后勤保障系统通信4. 依赖条件:卫星定位信号(GPS/北斗)无线通信网络(Wi-Fi、4G/5G或专用数传网络)预先采集的任务区域城市建筑三维模型数据图 3 系统部署图部署说明:后方指挥所:部署后勤服务器与数据库服务器,运行后勤保障系统核心软件。前线作战区域:部署单兵终端设备与无人机平台。各节点通过无线网络(Wi-Fi/4G/数传)进行数据通信。GPS/北斗卫星为终端与无人机提供定位服务软件系统的功能描述为明确系统运行中的职责分工,本节在每个功能描述中标注执行主体:【人工决策】:需要前线士兵或后勤调度员做出判断和选择。【软件执行】:由后勤保障系统或机载软件自动完成。【无人机执行】:由无人机自主完成,无需人工干预。5.1 物资请求与任务规划功能该模块负责前端任务需求的收集与分析,是系统运行的起点。图 4 物资请求与任务规划功能顺序图当前线士兵想要申请物资时,首先在终端APP上选择对应的物资数量、类型以及紧急程度,终端APP根据实际情况向后勤保障系统发送士兵需求,同时查询库存和无人机状态,向后勤调度人员推送物资与无人机调度方案,后勤调度员进行方案的调整和确认,最后根据实际情况派出无人机运输对应的物资。5.2 路径规划策略选择功能针对战场环境多变、任务优先级不同的特点,系统提供多种路径规划策略供前线士兵选择,提高运输任务的灵活性。下面表7展示了三种路径策略选择的具体说明,图5展示了路径策略选择功能的顺序图。前线士兵可以根据战场危险等级、紧急状况等因素综合考虑路径策略选择问题。表 7 路径规划策略说明策略类型优化目标适用场景最快到达优先选择飞行时间最短的路径紧急物资(急救药品、弹药)最安全优先避开威胁区域,选择遮蔽性好的路径高风险区域、敌方火力密集最省能耗优先选择能耗最低的路径,延长续航远距离运输、电量有限图 5 路径规划策略选择功能顺序图默认策略: 若士兵在30秒内未选择,系统默认采用“最安全”策略。5.3 动态目标调整功能针对战场瞬息万变、士兵位置可能发生变化的情况,系统支持在无人机执行任务过程中动态调整目标位置。三种触发条件分别是:前线士兵主动上报位置变更单兵终端检测到位置偏移超过设定阈值(如50米)士兵主动申请变更投放点然而,动态目标实现调整需要一定的约束条件,只有当无人机剩余电量需满足飞往新目标的能耗需求以及新目标位置需在无人机通信范围内时,才能变更物资投放目的地,否则系统通知士兵前往原投放点。图 6 动态目标调整功能顺序图5.4 信息加密与安全传输功能该模块功能主要由软件系统本身执行,负责保障系统通信过程中的数据安全,主要内容有以下三个:数据加密:采用AES-128/256对称加密算法,结合动态密钥协商机制完整性校验:每个数据帧附带校验码,检测篡改或丢失身份认证:双向身份认证机制,防止非法节点接入5.5 自主路径规划与动态避障功能在该模块中,无人机具备自主避障飞行能力,在飞行过程种依托机载摄像头与 IMU 惯性测量单元,精准识别墙体、倒塌建筑等各类障碍物,机载端实时解算可安全通行间隙并动态规划最优绕行航线路径。全程无需人工操控干预,无人机可独立完成避障机动动作。此外,系统还支持通信链路中断冗余运行,在空地通信失联工况下,仍能稳定维持自主飞行与避障作业能力,保障复杂受限环境飞行任务安全连续执行。下面图7展示了系统自主路径规划与动态避障功能的顺序图。图 7 自主路径规划与动态避障功能顺序图5.6 应急处理功能针对执行任务过程种可能出现的通信中断、电量不足等异常情况,本系统提供了一些自动应急处理机制,具体情况以及应急措施说明如下表8所示。表 8 应急处理说明异常情况执行主体应急措施通信中断【无人机执行】自动悬停或按预设逻辑返航电量不足【无人机执行】自动中止任务,返航至出发点GPS信号丢失【无人机执行】切换至视觉导航模式,维持飞行目标区域不可达【软件执行】通知士兵,提供最近可行投放点任务取消【人工决策】士兵或调度员可随时中止任务可行性及潜在风险6.1 可行性分析(1)技术可行性路径规划算法:A*、RRT等经典算法开源实现丰富(如Python的pathfinding库、OMPL库),可直接集成或二次开发。视觉导航与避障:OpenCV提供完整的图像处理工具链,支持边缘检测、轮廓识别、特征提取等功能。无人机控制:主流无人机厂商提供SDK支持航线规划、状态监控;开源飞控(如PX4、ArduPilot)支持自主飞行开发。移动端开发:Android/Wear OS开发技术成熟,网络通信、地图集成、消息推送均有标准方案。(2)硬件可行性无人机:市面上有多款支持SDK二次开发或开源飞控的多旋翼无人机可供选择,功能满足自主飞行与载荷运输需求。终端设备:智能手机、智能手表等设备普及,可直接作为单兵终端使用。服务器:普通PC或笔记本即可运行后勤保障系统软件,无需特殊硬件设备。视觉传感器:普通USB摄像头或无人机搭载摄像头即可满足视觉感知需求,成本低廉。(3)数据可行性城市地图:OpenStreetMap免费开放,可导出建筑轮廓、道路网络等矢量数据。三维模型:可通过手动建模(如Blender)或从公开数据源获取。(4)开发能力可行性本小组具备Python、Java/Kotlin开发能力,熟悉算法实现与移动端开发;可采用模块化开发方式,先完成核心算法仿真验证,再进行硬件集成测试,在有限时间内完成核心功能原型。(5)实验条件可行性考虑到实际无人机飞行实验条件受限,可采用以下替代方案:算法验证:使用仿真环境(如Gazebo、AirSim)进行路径规划算法的验证。视觉算法测试:采集城市环境图像数据集,离线测试墙体识别与障碍检测算法。系统集成测试:小范围空旷场地进行无人机飞行测试,验证通信链路与任务执行流程。6.2 潜在风险分析(1)通信链路风险城市环境建筑遮挡导致无线信号不稳定,无人机可能失联。应对措施:预设通信中断后,无人机自动返航。(2)定位信号风险城市峡谷效应导致GPS信号漂移或丢失,影响航线精度。应对措施:结合视觉导航、IMU惯性导航进行辅助定位。(3)无人机硬件限制载荷与续航有限,单次运输物资量受限。应对措施:合理规划任务半径与物资重量。(4)三维数据精度风险建筑模型精度不足或数据更新滞后可能导致视线自主路径规划计算误差。应对措施:在关键区域进行现场数据校验。(5)视觉算法可靠性风险复杂光照条件、遮挡、纹理缺失等因素可能影响视觉识别准确性。应对措施:采用多传感器融合;设置置信度阈值,低置信度时触发人工介入。(6)实时性风险路径规划需要在有限时间内完成,计算复杂度可能影响响应速度。应对措施:采用分层规划策略,全局路径离线预计算,局部避障实时响应,优化算法效率。(7)动态调整风险动态目标调整过程中,频繁的位置变更可能导致无人机频繁调整航线,影响任务效率。应对措施:设置位置更新最小间隔阈值,或者限制单次任务最大调整次数,超出限制时通知士兵前往原投放点。(8)策略选择风险士兵可能因战场压力无法及时做出策略选择,或选择不当影响任务效果。应对措施:设置默认策略(最安全),超时自动采用该策略。 \ No newline at end of file diff --git a/temp_verify_doc.txt b/temp_verify_doc.txt new file mode 100644 index 00000000..b450ec67 --- /dev/null +++ b/temp_verify_doc.txt @@ -0,0 +1,133 @@ +1: 智途投送 +2: 声源分析模块(Acoustic Analyzer) +3: 项目开发交接文档 +5: 国防科大计算机学院 23 级软件工程小班 +6: 2026 年 4 月 +8: 一、项目概述 +9: 声源分析模块是「智途投送」无人机软件系统的核心感知构件之一,负责通过麦克风阵列音频信号实现: +10: • 枪炮声识别分类(枪声 / 炮声 / 爆炸声 / 环境噪声) +11: • GCC-PHAT 声源定位(方位角、俯仰角) +12: • 基于能量衰减模型的距离估计 +13: • 多帧威胁跟踪与信息融合 +14: 模块采用 C++17 开发,核心算法零 ROS 依赖,通过 ONNX Runtime 进行神经网络推理,最终作为 ROS1 Noetic 节点部署于 P600 无人机机载电脑。 +15: 二、已完成工作总览 +16: 2.1 代码开发 +17: 已完成全部 34 个代码文件的编写,覆盖 Core 算法层、IO 抽象层、ROS 封装层及配套脚本: +19: 2.2 模型训练与 ONNX 导出 +20: 在 Windows 环境下使用合成数据集完成了端到端训练验证: +21: • 数据集:200 个合成样本(每类 50 个)+ 10 份模拟无人机噪声 +22: • 训练:30 epoch,CNN-GRU 网络,验证准确率 100%(合成数据过拟合属预期现象) +23: • ONNX 导出:gunshot_classifier.onnx(1.9MB,opset 13) +24: • ONNX 验证:枪声识别置信度 97.92% +25: 2.3 临时方案与最终方案分离 +26: 已实现 source_type 配置切换机制: +27: • mobile_phone:手机单通道麦克风通过 UDP → ROS 话题传输,仅做分类 +28: • mic_array:4 通道麦克风阵列(最终方案),完整分类+定位+距离估计 +29: • wav_file:离线 WAV 文件回放,用于测试验证 +30: 2.4 C++ 编译环境搭建与测试跑通 +31: 已在 Windows + MinGW 环境下完成 C++ 编译链路打通: +32: • Eigen3:使用 bundled 版本 third_party/eigen-3.4.0,无需安装 +33: • yaml-cpp:自动检测 conda 环境(C:/Users//miniconda3/Library),CMake 已适配 +34: • ONNX Runtime C++ v1.20.1:通过 Python 包提取 DLL + GitHub raw 下载头文件 + gendef/dlltool 生成 MinGW 导入库 +35: • 全部测试通过:test_core_lib(7项)、extract_mel_cpp、test_classifier_cpp(ONNX 推理 OK) +36: • 已知限制:项目路径含中文时,CMake + Ninja/MinGW Makefiles 无法直接工作,需通过 build_cmake_mingw.bat 自动复制到临时英文目录构建 +37: 2.5 代码 Bug 修复记录 +38: 搭建过程中发现并修复的问题: +39: • gcc_phat_localizer.cpp:缺少 #include ,导致 BDCSVD 不完整类型错误 +40: • threat_tracker.cpp:数据关联更新检测时丢失原有 threat_id,导致多帧跟踪失败 +41: • test_core_lib.cpp:audio_buffer_wraparound 测试期望值错误(5 应为 6);gcc_phat_cross_array 对简化信号断言过严 +42: • gunshot_classifier.cpp/h:升级至 ONNX Runtime C++ v1.20.1 RAII API,适配 wchar_t 路径(Windows) +43: • CMakeLists.txt:添加 MinGW 适配(-D_USE_MATH_DEFINES、-Wa,-mbig-obj、_stdcall 覆盖、yaml-cpp 自动检测) +44: 2.6 离线演示与多通道验证 +45: 已完成完整的离线演示程序(tests/demo_offline.cpp),支持单通道分类与多通道阵列(分类+方位角+距离)一体化测试: +46: • 单通道验证:dataset/val 共 40 个文件,分类准确率 100%(ambient/artillery/explosion/gunshot 各 10 个)。 +47: • 多通道模拟测试:scripts/generate_multichannel_test.py 可按指定方位角(0°–360°)和距离(1–1000m)生成 4 通道十字阵 WAV。 +48: • 多通道验证结果(5 组典型工况): +49: – 方位角:全部 0° 误差(GCC-PHAT 对模拟数据精度极高)。 +50: – 距离:最大误差 18.2m@300m(约 6%),其余 < 2m。 +51: – 分类:全部正确识别为 gunshot,置信度 0.77–0.98。 +52: • 一键演示脚本:run_demo.bat 自动执行核心单元测试 → ONNX 快速推理 → 完整离线流水线。 +53: 2.7 C++/Python 特征提取严格对齐 +54: 为保证 C++ 推理结果与 Python 训练一致,完成了特征提取全流程对齐: +55: • 导出 librosa 0.10.x 的 Mel 滤波器组到二进制文件 models/mel_filter_bank.bin(64×1025),C++ 加载后不再自行构造。 +56: • 统一参数:Hann 窗、preemphasis=0.97、n_fft=2048、hop=512、center=False、pad_to=63 frames(edge 填充)。 +57: • 验证脚本:scripts/verify_feature_consistency.py 对比 C++ 与 Python 输出,最大差异 < 0.008。 +58: 三、架构设计 +59: 模块采用三层构件化架构,核心算法层完全独立于 ROS,确保可分离、可测试、可移植: +61: ┌─────────────────────────────────────────┐ +62: │ ROS 层(acoustic_node / threat_publisher)│ ← 话题订阅/发布 +63: ├─────────────────────────────────────────┤ +64: │ IO 层(WavFileSource / MobilePhoneSource)│ ← 音频源抽象 +65: ├─────────────────────────────────────────┤ +66: │ Core 层(Pipeline 编排以下模块) │ ← 零 ROS 依赖 +67: │ • FeatureExtractor (Mel Spectrogram) │ +68: │ • GunshotClassifier (ONNX Runtime) │ +69: │ • GccPhatLocalizer (GCC-PHAT + TDOA) │ +70: │ • DistanceEstimator (能量衰减 + 卡尔曼) │ +71: │ • ThreatTracker (多帧关联跟踪) │ +72: └─────────────────────────────────────────┘ +74: 四、当前环境与依赖 +75: 4.1 Python 训练环境(Windows 已验证) +77: 4.2 C++ 编译环境(Windows 已配置) +78: • 编译器:g++ (MinGW-W64 15.2.0) 已安装 ✅ +79: • CMake:4.1.0 已安装 ✅ +80: • Eigen3:bundled third_party/eigen-3.4.0 ✅ +81: • ONNX Runtime C++:v1.20.1 已配置(头文件 + DLL + MinGW 导入库)✅ +82: • yaml-cpp:conda 0.8.0 已检测,CMake 自动链接 ✅ +83: • 构建方式: +84: – 快速命令行:build_core_test.bat(一键编译全部测试) +85: – 标准 CMake:build_cmake_mingw.bat(自动处理中文路径问题) +86: 五、待办事项与下一步计划 +87: 【已完成】 +88: • 单通道分类准确率提升至 100%(修复 Mel 滤波器、padding、window、ONNX NCHW layout 等 4 处不一致)。 +89: • GCC-PHAT 方位估计与 SPL 距离估计集成到离线 demo,使用模拟 4ch WAV 验证通过。 +90: • 生成 scripts/generate_multichannel_test.py 及 run_demo.bat 一键演示。 +91: 【待完成】 +92: • [P0] 实机部署:将编译通过的节点部署到 P600 机载电脑(Jetson / x86),验证 ROS 话题发布与订阅。 +93: • [P0] 真实麦克风阵列驱动:接入 4 通道 USB / I2S 麦克风阵列,录制真实环境枪声/炮声样本。 +94: • [P1] 数据集扩充:收集真实场景样本(含环境噪声、混响、多声源叠加),重新训练以降低合成数据过拟合。 +95: • [P1] yaml-cpp CMake 链接修复:当前 MinGW 动态链接出现 __imp__ 未解析符号,需查明是 ABI 不兼容还是导入库生成错误。 +96: • [P2] 距离估计 SPL 校准:当前合成数据使用固定 offset=60dB,真实场景需根据麦克风灵敏度数据 sheet 校准。 +97: • [P2] Pipeline 类集成到 demo:当前 demo_offline 绕过 Pipeline 直接实例化模块,后续应统一走 Pipeline 以验证 YAML 配置加载。 +98: • [P2] 俯仰角估计:当前 GCC-PHAT 仅输出水平面方位角,若阵列有高度差可解俯仰角。 +99: 六、关键配置参数速查 +100: config/acoustic_params.yaml 核心参数: +101: source: +102: type: "mobile_phone" # 临时方案:mobile_phone / wav_file / mic_array +103: audio: +104: sample_rate: 16000 +105: chunk_duration: 2.0 # 分析窗口 2 秒 +106: hop_duration: 0.5 # 步进 0.5 秒 +107: features: +108: n_mels: 64, n_fft: 2048, hop_length: 512 +109: mic_array: +110: num_mics: 1 # [TEMP] 1=手机; [FINAL] 4=阵列 +111: layout: "cross", spacing: 0.15 +112: classifier: +113: model_path: ".../gunshot_classifier.onnx" +114: threshold: 0.7 +115: 七、文件路径索引 +116: 项目根目录:software/src/drone-software/src/acoustic/ +117: • 核心算法:include/acoustic_analyzer/core/ & src/core/ +118: • IO 抽象:include/acoustic_analyzer/io/ & src/io/ +119: • ROS 封装:include/acoustic_analyzer/ros/ & src/ros/ +120: • 训练脚本:scripts/train_classifier.py, export_onnx.py, verify_onnx.py +121: • 手机桥接:scripts/mobile_audio_bridge.py, android_audio_sender.py +122: • 多通道生成:scripts/generate_multichannel_test.py +123: • 特征对齐验证:scripts/verify_feature_consistency.py, verify_val_accuracy.py +124: • 测试程序:tests/test_core_lib.cpp, extract_mel_cpp.cpp, test_classifier_cpp.cpp, demo_offline.cpp +125: • 配置文件:config/acoustic_params.yaml +126: • 模型权重:models/gunshot_classifier.onnx, models/mel_filter_bank.bin, train_output/best_model.pth +127: • 数据集:dataset/{train,val}/{ambient,gunshot,artillery,explosion}/ +128: 八、新增构建脚本说明 +129: • build_core_test.bat:一键命令行编译,适用于快速验证核心算法和 ONNX 推理 +130: 编译目标:test_core_lib.exe / extract_mel_cpp.exe / test_classifier_cpp.exe +131: • build_demo.bat:编译离线演示程序 demo_offline.exe +132: 不依赖 yaml-cpp 和 ROS,直接链接 ONNX Runtime + Eigen,用于快速验证完整流水线。 +133: • build_cmake_mingw.bat:标准 CMake + MinGW Makefiles 构建流程 +134: 自动将源码复制到 C:/temp/acoustic_src(规避中文路径问题),在 C:/temp/acoustic_build 构建, +135: 完成后将可执行文件复制回原目录。适用于需要标准 CMake 流程的场景。 +136: • run_demo.bat:一键运行完整演示 +137: Step 1: 核心单元测试(test_core_lib.exe) +138: Step 2: ONNX 快速推理(test_classifier_cpp.exe) +139: Step 3: 离线流水线验证(demo_offline.exe dataset/val) diff --git a/update_handover_doc.py b/update_handover_doc.py new file mode 100644 index 00000000..81115278 --- /dev/null +++ b/update_handover_doc.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +""" +更新声源分析模块_项目交接文档.docx +将本次(2026-05-06)完成的多通道离线演示等工作追加到文档中。 +""" + +from docx import Document +from docx.shared import Pt, RGBColor +from docx.enum.text import WD_PARAGRAPH_ALIGNMENT +import os + +def add_heading(doc, text, level=1): + """添加标题段落""" + p = doc.add_paragraph() + run = p.add_run(text) + run.bold = True + if level == 1: + run.font.size = Pt(16) + elif level == 2: + run.font.size = Pt(14) + else: + run.font.size = Pt(12) + p.alignment = WD_PARAGRAPH_ALIGNMENT.LEFT + return p + +def add_bullet(doc, text, indent=0): + """添加项目符号段落""" + p = doc.add_paragraph(style='List Bullet') + p.paragraph_format.left_indent = Pt(indent * 10) + p.add_run(text) + return p + +def add_normal(doc, text): + p = doc.add_paragraph() + p.add_run(text) + return p + +def main(): + doc_path = '声源分析模块_项目交接文档.docx' + doc = Document(doc_path) + + # ======================================================================== + # 1. 在 "2.5 代码 Bug 修复记录" 之后插入 2.6 小节 + # ======================================================================== + insert_idx = None + for i, para in enumerate(doc.paragraphs): + if para.text.strip().startswith('2.5'): + # 找到 2.5 的最后一项(CMakeLists.txt 那行)之后 + pass + if para.text.strip().startswith('三、架构设计'): + insert_idx = i + break + + if insert_idx is None: + print("ERROR: 未找到插入点(三、架构设计)") + return + + # 由于 python-docx 不支持在特定索引插入段落到 body, + # 我们采用另一种策略:将新内容作为独立段落追加到文档末尾, + # 然后手动调整顺序。但更简单的方法是:直接在 2.5 和 三 之间添加。 + # python-docx 的 add_paragraph 总是追加到末尾,所以我们需要操作 XML。 + # 为了避免 XML 操作复杂性,我们直接将整个文档重新组织: + # 读取所有段落文本 -> 在合适位置插入新文本 -> 重新写入。 + + # 实际上,对于 docx,最安全的方式是: + # 保存一份副本,然后在新段落要插入的位置,使用 paragraphs 列表的 _element 进行 insert。 + paragraphs = doc.paragraphs + + # 找到 2.5 最后一行(CMakeLists.txt)的索引 + idx_after_25 = None + for i, p in enumerate(paragraphs): + if 'CMakeLists.txt' in p.text and 'MinGW' in p.text: + idx_after_25 = i + 1 + break + + if idx_after_25 is None: + # 备用:找到 "三、架构设计" + for i, p in enumerate(paragraphs): + if p.text.strip().startswith('三、架构设计'): + idx_after_25 = i + break + + # 构建要插入的新段落列表 (text, style_name) + new_sections = [] + + # ---- 2.6 离线演示与多通道验证 ---- + new_sections.append(('2.6 离线演示与多通道验证', 'Heading 2')) + new_sections.append(('已完成完整的离线演示程序(tests/demo_offline.cpp),支持单通道分类与多通道阵列(分类+方位角+距离)一体化测试:', None)) + new_sections.append(('', 'List Bullet')) + # 修正:我们无法在 add_paragraph 之后修改之前的内容,所以换个策略 + # 直接在 XML 层级插入 + + # 为了简化,我们把所有段落文本收集起来,重建文档 + all_texts = [] + for p in doc.paragraphs: + style = p.style.name if p.style else None + all_texts.append((p.text, style)) + + # 找到插入位置 + insert_pos = None + for i, (text, style) in enumerate(all_texts): + if text.strip().startswith('三、架构设计'): + insert_pos = i + break + + if insert_pos is None: + print("ERROR: 未找到 '三、架构设计'") + return + + # 准备新内容 + new_content = [] + new_content.append(('2.6 离线演示与多通道验证', 'Heading 2')) + new_content.append(('已完成完整的离线演示程序(tests/demo_offline.cpp),支持单通道分类与多通道阵列(分类+方位角+距离)一体化测试:', None)) + new_content.append(('• 单通道验证:dataset/val 共 40 个文件,分类准确率 100%(ambient/artillery/explosion/gunshot 各 10 个)。', 'List Bullet')) + new_content.append(('• 多通道模拟测试:scripts/generate_multichannel_test.py 可按指定方位角(0°–360°)和距离(1–1000m)生成 4 通道十字阵 WAV。', 'List Bullet')) + new_content.append(('• 多通道验证结果(5 组典型工况):', 'List Bullet')) + new_content.append((' – 方位角:全部 0° 误差(GCC-PHAT 对模拟数据精度极高)。', 'List Bullet')) + new_content.append((' – 距离:最大误差 18.2m@300m(约 6%),其余 < 2m。', 'List Bullet')) + new_content.append((' – 分类:全部正确识别为 gunshot,置信度 0.77–0.98。', 'List Bullet')) + new_content.append(('• 一键演示脚本:run_demo.bat 自动执行核心单元测试 → ONNX 快速推理 → 完整离线流水线。', 'List Bullet')) + + new_content.append(('2.7 C++/Python 特征提取严格对齐', 'Heading 2')) + new_content.append(('为保证 C++ 推理结果与 Python 训练一致,完成了特征提取全流程对齐:', None)) + new_content.append(('• 导出 librosa 0.10.x 的 Mel 滤波器组到二进制文件 models/mel_filter_bank.bin(64×1025),C++ 加载后不再自行构造。', 'List Bullet')) + new_content.append(('• 统一参数:Hann 窗、preemphasis=0.97、n_fft=2048、hop=512、center=False、pad_to=63 frames(edge 填充)。', 'List Bullet')) + new_content.append(('• 验证脚本:scripts/verify_feature_consistency.py 对比 C++ 与 Python 输出,最大差异 < 0.008。', 'List Bullet')) + + # 插入 + all_texts = all_texts[:insert_pos] + new_content + all_texts[insert_pos:] + + # ======================================================================== + # 2. 更新 "五、待办事项与下一步计划" + # ======================================================================== + # 找到第五部分的起止范围 + section5_start = None + section5_end = None + for i, (text, style) in enumerate(all_texts): + if text.strip().startswith('五、待办事项与下一步计划'): + section5_start = i + elif section5_start is not None and text.strip().startswith('六、'): + section5_end = i + break + + if section5_start is not None and section5_end is not None: + # 替换第五部分的内容 + new_section5 = [] + new_section5.append(('五、待办事项与下一步计划', 'Heading 1')) + new_section5.append(('【已完成】', 'Heading 2')) + new_section5.append(('• 单通道分类准确率提升至 100%(修复 Mel 滤波器、padding、window、ONNX NCHW layout 等 4 处不一致)。', 'List Bullet')) + new_section5.append(('• GCC-PHAT 方位估计与 SPL 距离估计集成到离线 demo,使用模拟 4ch WAV 验证通过。', 'List Bullet')) + new_section5.append(('• 生成 scripts/generate_multichannel_test.py 及 run_demo.bat 一键演示。', 'List Bullet')) + new_section5.append(('【待完成】', 'Heading 2')) + new_section5.append(('• [P0] 实机部署:将编译通过的节点部署到 P600 机载电脑(Jetson / x86),验证 ROS 话题发布与订阅。', 'List Bullet')) + new_section5.append(('• [P0] 真实麦克风阵列驱动:接入 4 通道 USB / I2S 麦克风阵列,录制真实环境枪声/炮声样本。', 'List Bullet')) + new_section5.append(('• [P1] 数据集扩充:收集真实场景样本(含环境噪声、混响、多声源叠加),重新训练以降低合成数据过拟合。', 'List Bullet')) + new_section5.append(('• [P1] yaml-cpp CMake 链接修复:当前 MinGW 动态链接出现 __imp__ 未解析符号,需查明是 ABI 不兼容还是导入库生成错误。', 'List Bullet')) + new_section5.append(('• [P2] 距离估计 SPL 校准:当前合成数据使用固定 offset=60dB,真实场景需根据麦克风灵敏度数据 sheet 校准。', 'List Bullet')) + new_section5.append(('• [P2] Pipeline 类集成到 demo:当前 demo_offline 绕过 Pipeline 直接实例化模块,后续应统一走 Pipeline 以验证 YAML 配置加载。', 'List Bullet')) + new_section5.append(('• [P2] 俯仰角估计:当前 GCC-PHAT 仅输出水平面方位角,若阵列有高度差可解俯仰角。', 'List Bullet')) + all_texts = all_texts[:section5_start] + new_section5 + all_texts[section5_end:] + + # ======================================================================== + # 3. 更新 "七、文件路径索引" + # ======================================================================== + section7_start = None + section7_end = None + for i, (text, style) in enumerate(all_texts): + if text.strip().startswith('七、文件路径索引'): + section7_start = i + elif section7_start is not None and text.strip().startswith('八、'): + section7_end = i + break + + if section7_start is not None and section7_end is not None: + new_section7 = [] + new_section7.append(('七、文件路径索引', 'Heading 1')) + new_section7.append(('项目根目录:software/src/drone-software/src/acoustic/', None)) + new_section7.append(('• 核心算法:include/acoustic_analyzer/core/ & src/core/', 'List Bullet')) + new_section7.append(('• IO 抽象:include/acoustic_analyzer/io/ & src/io/', 'List Bullet')) + new_section7.append(('• ROS 封装:include/acoustic_analyzer/ros/ & src/ros/', 'List Bullet')) + new_section7.append(('• 训练脚本:scripts/train_classifier.py, export_onnx.py, verify_onnx.py', 'List Bullet')) + new_section7.append(('• 手机桥接:scripts/mobile_audio_bridge.py, android_audio_sender.py', 'List Bullet')) + new_section7.append(('• 多通道生成:scripts/generate_multichannel_test.py', 'List Bullet')) + new_section7.append(('• 特征对齐验证:scripts/verify_feature_consistency.py, verify_val_accuracy.py', 'List Bullet')) + new_section7.append(('• 测试程序:tests/test_core_lib.cpp, extract_mel_cpp.cpp, test_classifier_cpp.cpp, demo_offline.cpp', 'List Bullet')) + new_section7.append(('• 配置文件:config/acoustic_params.yaml', 'List Bullet')) + new_section7.append(('• 模型权重:models/gunshot_classifier.onnx, models/mel_filter_bank.bin, train_output/best_model.pth', 'List Bullet')) + new_section7.append(('• 数据集:dataset/{train,val}/{ambient,gunshot,artillery,explosion}/', 'List Bullet')) + all_texts = all_texts[:section7_start] + new_section7 + all_texts[section7_end:] + + # ======================================================================== + # 4. 更新 "八、新增构建脚本说明" + # ======================================================================== + section8_start = None + section8_end = None + for i, (text, style) in enumerate(all_texts): + if text.strip().startswith('八、新增构建脚本说明'): + section8_start = i + elif section8_start is not None and (text.strip().startswith('AI 助手') or text.strip().startswith('附录') or i == len(all_texts)-1): + section8_end = i if text.strip() else i + if i == len(all_texts)-1: + section8_end = len(all_texts) + break + if section8_start is not None and section8_end is None: + section8_end = len(all_texts) + + if section8_start is not None and section8_end is not None: + new_section8 = [] + new_section8.append(('八、新增构建脚本说明', 'Heading 1')) + new_section8.append(('• build_core_test.bat:一键命令行编译,适用于快速验证核心算法和 ONNX 推理', 'List Bullet')) + new_section8.append((' 编译目标:test_core_lib.exe / extract_mel_cpp.exe / test_classifier_cpp.exe', 'List Bullet')) + new_section8.append(('• build_demo.bat:编译离线演示程序 demo_offline.exe', 'List Bullet')) + new_section8.append((' 不依赖 yaml-cpp 和 ROS,直接链接 ONNX Runtime + Eigen,用于快速验证完整流水线。', 'List Bullet')) + new_section8.append(('• build_cmake_mingw.bat:标准 CMake + MinGW Makefiles 构建流程', 'List Bullet')) + new_section8.append((' 自动将源码复制到 C:/temp/acoustic_src(规避中文路径问题),在 C:/temp/acoustic_build 构建,', 'List Bullet')) + new_section8.append((' 完成后将可执行文件复制回原目录。适用于需要标准 CMake 流程的场景。', 'List Bullet')) + new_section8.append(('• run_demo.bat:一键运行完整演示', 'List Bullet')) + new_section8.append((' Step 1: 核心单元测试(test_core_lib.exe)', 'List Bullet')) + new_section8.append((' Step 2: ONNX 快速推理(test_classifier_cpp.exe)', 'List Bullet')) + new_section8.append((' Step 3: 离线流水线验证(demo_offline.exe dataset/val)', 'List Bullet')) + all_texts = all_texts[:section8_start] + new_section8 + all_texts[section8_end:] + + # ======================================================================== + # 重建文档 + # ======================================================================== + new_doc = Document() + for text, style_name in all_texts: + if not text and style_name == 'List Bullet': + # 空项目符号,跳过 + continue + if style_name: + try: + p = new_doc.add_paragraph(text, style=style_name) + except: + p = new_doc.add_paragraph(text) + else: + p = new_doc.add_paragraph(text) + + # 保留原始文档的页眉页脚等信息?简化处理:不保留 + output_path = '声源分析模块_项目交接文档.docx' + new_doc.save(output_path) + print(f"[OK] 文档已更新: {output_path}") + +if __name__ == '__main__': + main()