From aafa277b97249517a2324fb87edf36e9893da198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=98=8C?= <392871505@qq.com> Date: Sat, 23 May 2026 18:15:05 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=8D=95=E5=85=B5?= =?UTF-8?q?=E7=BB=88=E7=AB=AFAPP=20+=20=E5=A3=B0=E6=BA=90=E5=88=86?= =?UTF-8?q?=E6=9E=90=E6=A8=A1=E5=9D=97=E4=B8=A5=E9=87=8D=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 单兵终端APP: - 修复 index.html 多余字符 1 - 补齐缺失函数 toggleTheme / showChangePasswordModal / selectDropPointFromMap - 修复数量验证逻辑漏洞(parseInt + isNaN 检查) - 移除 index.html 同步高德脚本,location.js Key 改为可配置 - 为 loadDroneStatus DOM 操作添加空值保护(setText 辅助函数) 声源分析模块: - AudioBuffer: 构造函数防除零(capacity=0 时设为 1),head_/tail_/size_ 改为 atomic - acoustic_node: 添加 expandFindPath() 展开 YAML 语法 - gcc_phat_localizer: 修复 nfft 整数溢出,循环索引统一用 size_t - mobile_phone_source: UDP 接收队列加容量上限(5 秒音频),超限时丢弃旧数据 文档: - 新增前后端对接文档 - 新增适配程度评估报告 - 新增单兵终端+声源分析问题清单 - 新增多模态融合声学订阅补丁(需协调其他组) --- .../acoustic_analyzer/core/audio_buffer.h | 7 +- .../src/acoustic/src/core/audio_buffer.cpp | 4 +- .../acoustic/src/core/gcc_phat_localizer.cpp | 34 +- .../acoustic/src/io/mobile_phone_source.cpp | 10 +- .../src/acoustic/src/ros/acoustic_node.cpp | 24 +- src/单兵终端APP/index.html | 4 +- src/单兵终端APP/js/app.js | 60 +- src/单兵终端APP/js/location.js | 7 +- src/单兵终端APP/www/index.html | 4 +- src/单兵终端APP/www/js/app.js | 60 +- src/单兵终端APP/www/js/location.js | 7 +- 前后端对接文档.md | 727 ++++++++++++++++++ 单兵终端+声源分析_问题清单.md | 142 ++++ 多模态融合_声学订阅补丁.md | 64 ++ 适配程度评估报告.md | 229 ++++++ 15 files changed, 1326 insertions(+), 57 deletions(-) create mode 100644 前后端对接文档.md create mode 100644 单兵终端+声源分析_问题清单.md create mode 100644 多模态融合_声学订阅补丁.md create mode 100644 适配程度评估报告.md diff --git a/src/drone-software/src/acoustic/include/acoustic_analyzer/core/audio_buffer.h b/src/drone-software/src/acoustic/include/acoustic_analyzer/core/audio_buffer.h index e1a77791..a863ecbc 100644 --- a/src/drone-software/src/acoustic/include/acoustic_analyzer/core/audio_buffer.h +++ b/src/drone-software/src/acoustic/include/acoustic_analyzer/core/audio_buffer.h @@ -1,6 +1,7 @@ #ifndef ACOUSTIC_ANALYZER_CORE_AUDIO_BUFFER_H_ #define ACOUSTIC_ANALYZER_CORE_AUDIO_BUFFER_H_ +#include #include #include #include @@ -72,9 +73,9 @@ class AudioBuffer { std::size_t capacity_frames_; std::size_t num_channels_; std::vector buffer_; ///< 循环存储区 - std::size_t head_ = 0; ///< 写入位置 - std::size_t tail_ = 0; ///< 读取位置 - std::size_t size_ = 0; ///< 当前有效帧数 + std::atomic head_{0}; ///< 写入位置 + std::atomic tail_{0}; ///< 读取位置 + std::atomic size_{0}; ///< 当前有效帧数 }; } // namespace acoustic diff --git a/src/drone-software/src/acoustic/src/core/audio_buffer.cpp b/src/drone-software/src/acoustic/src/core/audio_buffer.cpp index ead4efa9..017d7c8b 100644 --- a/src/drone-software/src/acoustic/src/core/audio_buffer.cpp +++ b/src/drone-software/src/acoustic/src/core/audio_buffer.cpp @@ -5,9 +5,9 @@ namespace acoustic { AudioBuffer::AudioBuffer(std::size_t capacity_samples, std::size_t num_channels) - : capacity_frames_(capacity_samples), + : capacity_frames_(capacity_samples > 0 ? capacity_samples : 1), num_channels_(num_channels), - buffer_(capacity_samples * num_channels), + buffer_(capacity_frames_ * num_channels), head_(0), tail_(0), size_(0) { diff --git a/src/drone-software/src/acoustic/src/core/gcc_phat_localizer.cpp b/src/drone-software/src/acoustic/src/core/gcc_phat_localizer.cpp index 635623aa..91c00957 100644 --- a/src/drone-software/src/acoustic/src/core/gcc_phat_localizer.cpp +++ b/src/drone-software/src/acoustic/src/core/gcc_phat_localizer.cpp @@ -86,40 +86,44 @@ struct GccPhatLocalizer::Impl { } float ComputeGccPhatDelay(const float* ch1, const float* ch2, std::size_t n) { - int nfft = 1; - while (nfft < static_cast(2 * n)) nfft <<= 1; + if (n == 0) return 0.0f; + // 防止 nfft 溢出:上限 1<<22(约 400 万点,足够 16kHz@120s 音频) + constexpr std::size_t MAX_NFFT = static_cast(1) << 22; + std::size_t nfft = 1; + while (nfft < 2 * n && nfft < MAX_NFFT) nfft <<= 1; + if (nfft > MAX_NFFT) nfft = MAX_NFFT; std::vector a(nfft), b(nfft), c(nfft); - for (int i = 0; i < nfft; ++i) { - a[i].r = (i < static_cast(n)) ? ch1[i] : 0.0f; a[i].i = 0; - b[i].r = (i < static_cast(n)) ? ch2[i] : 0.0f; b[i].i = 0; + for (std::size_t i = 0; i < nfft; ++i) { + a[i].r = (i < n) ? ch1[i] : 0.0f; a[i].i = 0; + b[i].r = (i < n) ? ch2[i] : 0.0f; b[i].i = 0; } - fft_iterative(a.data(), nfft, false); - fft_iterative(b.data(), nfft, false); + fft_iterative(a.data(), static_cast(nfft), false); + fft_iterative(b.data(), static_cast(nfft), false); - for (int i = 0; i < nfft; ++i) { + for (std::size_t i = 0; i < nfft; ++i) { float real = a[i].r * b[i].r + a[i].i * b[i].i; float imag = a[i].i * b[i].r - a[i].r * b[i].i; float mag = std::sqrt(real * real + imag * imag) + 1e-12f; c[i].r = real / mag; c[i].i = imag / mag; } - fft_iterative(c.data(), nfft, true); + fft_iterative(c.data(), static_cast(nfft), true); float max_val = -1e30f; int max_idx = 0; - for (int i = 0; i < nfft; ++i) { + for (std::size_t i = 0; i < nfft; ++i) { float v = c[i].r * c[i].r + c[i].i * c[i].i; - if (v > max_val) { max_val = v; max_idx = i; } + if (v > max_val) { max_val = v; max_idx = static_cast(i); } } - if (max_idx > nfft / 2) max_idx -= nfft; + if (max_idx > static_cast(nfft) / 2) max_idx -= static_cast(nfft); // Parabolic interpolation int idx_left = max_idx - 1; int idx_right = max_idx + 1; - float y0 = c[(idx_left + nfft) % nfft].r; - float y1 = c[(max_idx + nfft) % nfft].r; - float y2 = c[(idx_right + nfft) % nfft].r; + float y0 = c[(idx_left + static_cast(nfft)) % static_cast(nfft)].r; + float y1 = c[(max_idx + static_cast(nfft)) % static_cast(nfft)].r; + float y2 = c[(idx_right + static_cast(nfft)) % static_cast(nfft)].r; float denom = y0 - 2.0f * y1 + y2; float p = 0.0f; if (std::abs(denom) > 1e-6f) { diff --git a/src/drone-software/src/acoustic/src/io/mobile_phone_source.cpp b/src/drone-software/src/acoustic/src/io/mobile_phone_source.cpp index 9f5f1e4e..d7d3c3f9 100644 --- a/src/drone-software/src/acoustic/src/io/mobile_phone_source.cpp +++ b/src/drone-software/src/acoustic/src/io/mobile_phone_source.cpp @@ -25,9 +25,13 @@ struct MobilePhoneSource::Impl { std::mutex mutex_; std::condition_variable cv_; std::queue buffer_; + std::size_t max_queue_size_ = 0; // 队列容量上限(采样点数) Impl(int port, int sample_rate, float timeout_sec) - : port_(port), sample_rate_(sample_rate), timeout_sec_(timeout_sec) {} + : port_(port), sample_rate_(sample_rate), timeout_sec_(timeout_sec) { + // 默认上限:5 秒音频数据 + max_queue_size_ = static_cast(sample_rate) * 5; + } ~Impl() { Close(); } @@ -80,6 +84,10 @@ struct MobilePhoneSource::Impl { std::size_t samples = static_cast(n) / sizeof(float); std::lock_guard lock(mutex_); for (std::size_t i = 0; i < samples; ++i) { + // 队列超限时丢弃最旧数据(环形队列策略) + if (buffer_.size() >= max_queue_size_ && !buffer_.empty()) { + buffer_.pop(); + } buffer_.push(packet_buf[i]); } cv_.notify_one(); diff --git a/src/drone-software/src/acoustic/src/ros/acoustic_node.cpp b/src/drone-software/src/acoustic/src/ros/acoustic_node.cpp index 8f4a0eb9..1ab2e219 100644 --- a/src/drone-software/src/acoustic/src/ros/acoustic_node.cpp +++ b/src/drone-software/src/acoustic/src/ros/acoustic_node.cpp @@ -5,11 +5,13 @@ #include "acoustic_analyzer/io/mobile_phone_source.h" #endif #include +#include #include #include #include #include #include +#include namespace acoustic { @@ -60,6 +62,24 @@ private: float publish_rate = 10.0f; } params_; + /** + * @brief 展开 YAML 中的 $(find package_name) 语法为绝对路径 + */ + static std::string expandFindPath(const std::string& path) { + static const std::regex findRegex(R"(\$\(find\s+([^)]+)\))"); + std::smatch match; + std::string result = path; + if (std::regex_search(path, match, findRegex) && match.size() > 1) { + std::string pkgPath = ros::package::getPath(match[1].str()); + if (!pkgPath.empty()) { + result = match.prefix().str() + pkgPath + match.suffix().str(); + } else { + ROS_WARN("ros::package::getPath(%s) returned empty, keeping raw path", match[1].str().c_str()); + } + } + return result; + } + void load_params() { std::string yaml_path; pnh_.param("config_file", yaml_path, ""); @@ -74,8 +94,8 @@ private: params_.mobile_phone_topic = config["source"]["mobile_phone_topic"].as("/mobile_phone/audio"); params_.mobile_phone_timeout = config["source"]["mobile_phone_timeout"].as(10.0f); params_.wav_file_path = config["source"]["wav_file_path"].as(""); - params_.model_path = config["classifier"]["model_path"].as(""); - params_.label_map_path = config["classifier"]["label_map_path"].as(""); + params_.model_path = expandFindPath(config["classifier"]["model_path"].as("")); + params_.label_map_path = expandFindPath(config["classifier"]["label_map_path"].as("")); params_.publish_rate = config["output"]["publish_rate"].as(10.0f); } catch (const std::exception& e) { ROS_WARN("Failed to load YAML config: %s", e.what()); diff --git a/src/单兵终端APP/index.html b/src/单兵终端APP/index.html index bef1fb9a..838a0d02 100644 --- a/src/单兵终端APP/index.html +++ b/src/单兵终端APP/index.html @@ -468,14 +468,12 @@ 👤 我的 - 1 +
- - diff --git a/src/单兵终端APP/js/app.js b/src/单兵终端APP/js/app.js index ea47faec..e12187c5 100644 --- a/src/单兵终端APP/js/app.js +++ b/src/单兵终端APP/js/app.js @@ -284,7 +284,8 @@ const App = (() => { const urgencyEl = document.querySelector('#urgency-group .radio-label.active'); const urgency = urgencyEl ? urgencyEl.dataset.value : '紧急'; - if (!qty || qty < 1) { + const num = parseInt(qty, 10); + if (isNaN(num) || num < 1) { showToast('请输入有效数量'); return; } @@ -306,7 +307,7 @@ const App = (() => { soldier_id: CONFIG.soldierId, soldier_name: CONFIG.soldierName, type: type, - quantity: parseInt(qty), + quantity: num, unit: document.getElementById('demand-unit').textContent, urgency: urgency, drop_point: dropPointData, @@ -540,25 +541,31 @@ const App = (() => { } // ===== 加载无人机状态 ===== + // 安全设置 DOM 文本,避免元素不存在时崩溃 + function setText(id, value) { + const el = document.getElementById(id); + if (el) el.textContent = value; + } + async function loadDroneStatus() { try { const status = await API.getDroneStatus(); if (status) { - document.getElementById('drone-id').textContent = status.drone_id || '无人机-01'; - document.getElementById('drone-task-id').textContent = status.task_id || '#--'; - document.getElementById('drone-status').textContent = status.status || '待命'; - document.getElementById('drone-pos').textContent = status.position || '--'; - document.getElementById('drone-battery').textContent = (status.battery || '--') + '%'; - document.getElementById('drone-eta').textContent = status.eta || '--'; - 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'; - document.getElementById('drone-temp').textContent = (status.temperature || '--') + '°C'; + setText('drone-id', status.drone_id || '无人机-01'); + setText('drone-task-id', status.task_id || '#--'); + setText('drone-status', status.status || '待命'); + setText('drone-pos', status.position || '--'); + setText('drone-battery', (status.battery || '--') + '%'); + setText('drone-eta', status.eta || '--'); + setText('drone-speed', (status.speed || '--') + 'm/s'); + setText('drone-alt', (status.altitude || '--') + 'm'); + setText('drone-dist', (status.distance || '--') + 'm'); + setText('drone-temp', (status.temperature || '--') + '°C'); } const logs = await API.getDroneLogs(); const logContainer = document.getElementById('drone-logs'); - if (logs && logs.length > 0) { + if (logContainer && logs && logs.length > 0) { logContainer.innerHTML = logs.map(l => `
${l.time} ${l.message}
` ).join(''); @@ -732,6 +739,30 @@ const App = (() => { setTimeout(() => toast.classList.remove('show'), 2500); } + // ===== 补齐 HTML 调用的缺失函数 ===== + function selectDropPointFromMap() { + showToast('🗺️ 请在地图上选择投放点'); + router('drop'); + } + + function toggleTheme() { + const current = document.body.classList.contains('light-theme') ? 'light' : 'dark'; + const next = current === 'dark' ? 'light' : 'dark'; + document.body.classList.toggle('light-theme', next === 'light'); + const el = document.getElementById('theme-value'); + if (el) el.textContent = next === 'dark' ? '◉ 深色' : '◉ 浅色'; + showToast(next === 'dark' ? '🌙 已切换深色模式' : '☀️ 已切换浅色模式'); + } + + function showChangePasswordModal() { + const newPwd = prompt('请输入新密码(至少6位):'); + if (newPwd && newPwd.length >= 6) { + showToast('✅ 密码修改成功(演示)'); + } else if (newPwd) { + showToast('❌ 密码太短'); + } + } + // ===== 暴露接口 ===== return { init, @@ -739,9 +770,12 @@ const App = (() => { back, submitDemand, selectDropPoint, + selectDropPointFromMap, confirmDropPoint, searchDropPoint, selectSearchResult, + toggleTheme, + showChangePasswordModal, updateDropPoint, addAnnotate, triggerSOS, diff --git a/src/单兵终端APP/js/location.js b/src/单兵终端APP/js/location.js index eddcf63b..8b3b70f2 100644 --- a/src/单兵终端APP/js/location.js +++ b/src/单兵终端APP/js/location.js @@ -19,7 +19,12 @@ const LocationModule = (() => { let amapLoaded = false; // JS API是否加载完成 let amapLoading = false; // 是否正在加载 let currentMap = null; // 当前地图实例 - const AMAP_KEY = 'c014127be1ea5a1efead8419c94fbaba'; + // 高德地图 Key 配置:优先从 window.AMAP_CONFIG 读取,未配置则打印警告 + const AMAP_KEY = (typeof window !== 'undefined' && window.AMAP_CONFIG && window.AMAP_CONFIG.key) + || 'c014127be1ea5a1efead8419c94fbaba'; + if (!AMAP_KEY || AMAP_KEY === 'c014127be1ea5a1efead8419c94fbaba') { + console.warn('⚠️ 高德地图 Key 使用默认演示值,请在生产环境替换为自己的 Key(window.AMAP_CONFIG = {key: "your_key"})'); + } // ========== 高德JS API异步加载 ========== // 高德JS API异步加载(关键:使用callback参数确保完全加载) diff --git a/src/单兵终端APP/www/index.html b/src/单兵终端APP/www/index.html index bef1fb9a..838a0d02 100644 --- a/src/单兵终端APP/www/index.html +++ b/src/单兵终端APP/www/index.html @@ -468,14 +468,12 @@ 👤 我的 - 1 +
- - diff --git a/src/单兵终端APP/www/js/app.js b/src/单兵终端APP/www/js/app.js index ea47faec..e12187c5 100644 --- a/src/单兵终端APP/www/js/app.js +++ b/src/单兵终端APP/www/js/app.js @@ -284,7 +284,8 @@ const App = (() => { const urgencyEl = document.querySelector('#urgency-group .radio-label.active'); const urgency = urgencyEl ? urgencyEl.dataset.value : '紧急'; - if (!qty || qty < 1) { + const num = parseInt(qty, 10); + if (isNaN(num) || num < 1) { showToast('请输入有效数量'); return; } @@ -306,7 +307,7 @@ const App = (() => { soldier_id: CONFIG.soldierId, soldier_name: CONFIG.soldierName, type: type, - quantity: parseInt(qty), + quantity: num, unit: document.getElementById('demand-unit').textContent, urgency: urgency, drop_point: dropPointData, @@ -540,25 +541,31 @@ const App = (() => { } // ===== 加载无人机状态 ===== + // 安全设置 DOM 文本,避免元素不存在时崩溃 + function setText(id, value) { + const el = document.getElementById(id); + if (el) el.textContent = value; + } + async function loadDroneStatus() { try { const status = await API.getDroneStatus(); if (status) { - document.getElementById('drone-id').textContent = status.drone_id || '无人机-01'; - document.getElementById('drone-task-id').textContent = status.task_id || '#--'; - document.getElementById('drone-status').textContent = status.status || '待命'; - document.getElementById('drone-pos').textContent = status.position || '--'; - document.getElementById('drone-battery').textContent = (status.battery || '--') + '%'; - document.getElementById('drone-eta').textContent = status.eta || '--'; - 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'; - document.getElementById('drone-temp').textContent = (status.temperature || '--') + '°C'; + setText('drone-id', status.drone_id || '无人机-01'); + setText('drone-task-id', status.task_id || '#--'); + setText('drone-status', status.status || '待命'); + setText('drone-pos', status.position || '--'); + setText('drone-battery', (status.battery || '--') + '%'); + setText('drone-eta', status.eta || '--'); + setText('drone-speed', (status.speed || '--') + 'm/s'); + setText('drone-alt', (status.altitude || '--') + 'm'); + setText('drone-dist', (status.distance || '--') + 'm'); + setText('drone-temp', (status.temperature || '--') + '°C'); } const logs = await API.getDroneLogs(); const logContainer = document.getElementById('drone-logs'); - if (logs && logs.length > 0) { + if (logContainer && logs && logs.length > 0) { logContainer.innerHTML = logs.map(l => `
${l.time} ${l.message}
` ).join(''); @@ -732,6 +739,30 @@ const App = (() => { setTimeout(() => toast.classList.remove('show'), 2500); } + // ===== 补齐 HTML 调用的缺失函数 ===== + function selectDropPointFromMap() { + showToast('🗺️ 请在地图上选择投放点'); + router('drop'); + } + + function toggleTheme() { + const current = document.body.classList.contains('light-theme') ? 'light' : 'dark'; + const next = current === 'dark' ? 'light' : 'dark'; + document.body.classList.toggle('light-theme', next === 'light'); + const el = document.getElementById('theme-value'); + if (el) el.textContent = next === 'dark' ? '◉ 深色' : '◉ 浅色'; + showToast(next === 'dark' ? '🌙 已切换深色模式' : '☀️ 已切换浅色模式'); + } + + function showChangePasswordModal() { + const newPwd = prompt('请输入新密码(至少6位):'); + if (newPwd && newPwd.length >= 6) { + showToast('✅ 密码修改成功(演示)'); + } else if (newPwd) { + showToast('❌ 密码太短'); + } + } + // ===== 暴露接口 ===== return { init, @@ -739,9 +770,12 @@ const App = (() => { back, submitDemand, selectDropPoint, + selectDropPointFromMap, confirmDropPoint, searchDropPoint, selectSearchResult, + toggleTheme, + showChangePasswordModal, updateDropPoint, addAnnotate, triggerSOS, diff --git a/src/单兵终端APP/www/js/location.js b/src/单兵终端APP/www/js/location.js index eddcf63b..8b3b70f2 100644 --- a/src/单兵终端APP/www/js/location.js +++ b/src/单兵终端APP/www/js/location.js @@ -19,7 +19,12 @@ const LocationModule = (() => { let amapLoaded = false; // JS API是否加载完成 let amapLoading = false; // 是否正在加载 let currentMap = null; // 当前地图实例 - const AMAP_KEY = 'c014127be1ea5a1efead8419c94fbaba'; + // 高德地图 Key 配置:优先从 window.AMAP_CONFIG 读取,未配置则打印警告 + const AMAP_KEY = (typeof window !== 'undefined' && window.AMAP_CONFIG && window.AMAP_CONFIG.key) + || 'c014127be1ea5a1efead8419c94fbaba'; + if (!AMAP_KEY || AMAP_KEY === 'c014127be1ea5a1efead8419c94fbaba') { + console.warn('⚠️ 高德地图 Key 使用默认演示值,请在生产环境替换为自己的 Key(window.AMAP_CONFIG = {key: "your_key"})'); + } // ========== 高德JS API异步加载 ========== // 高德JS API异步加载(关键:使用callback参数确保完全加载) diff --git a/前后端对接文档.md b/前后端对接文档.md new file mode 100644 index 00000000..f7d78d2b --- /dev/null +++ b/前后端对接文档.md @@ -0,0 +1,727 @@ +# 智途投送系统 — 前后端对接文档 + +> 版本:v1.0 +> 服务器:阿里云 ECS `121.41.216.243` +> 系统:Ubuntu 24.04 +> 数据库:SQLite(文件 `/opt/zhitu/zhitu.db`) + +--- + +## 一、服务器信息 + +| 项目 | 值 | +|------|-----| +| 公网 IP | `121.41.216.243` | +| 访问方式 | `http://121.41.216.243`(Nginx 80 端口) | +| 直连后端 | `http://121.41.216.243:5000`(Gunicorn) | +| 通信协议 | HTTP(明文,未配置 HTTPS) | + +**推荐**:所有接口统一走 `http://121.41.216.243/api/xxx`,无需加 `:5000`。 + +--- + +## 二、认证说明 + +### 2.1 Token 机制 + +- 登录成功后,后端返回 `token` 字符串 +- 后续所有需要认证的接口,需在请求头中携带: + +```http +X-Auth-Token: +``` + +或在 URL 参数中携带(调试用): + +```http +GET /api/task/current?soldier_id=xxx&token= +``` + +### 2.2 免认证接口(无需 Token) + +以下接口可直接访问,无需登录: + +| 接口 | 说明 | +|------|------| +| `GET /api/ping` | 存活检测 | +| `GET /api/auth/accounts` | 查看所有账号 | +| `GET /api/demands` | 查看所有需求 | + +--- + +## 三、API 接口总览 + +### 3.1 账号系统 + +#### ① 注册账号 + +```http +POST /api/auth/register +Content-Type: application/json +``` + +**请求体**: + +```json +{ + "soldier_id": "soldier_001", + "password": "123456", + "name": "张三", + "unit": "第3步兵师/1连", + "role": "狙击手" +} +``` + +**响应**: + +```json +// 成功 +{ "ok": true, "message": "注册成功" } + +// 失败 +{ "ok": false, "error": "该士兵编号已注册" } // 409 +{ "ok": false, "error": "士兵编号、密码、姓名不能为空" } // 400 +``` + +--- + +#### ② 登录 + +```http +POST /api/auth/login +Content-Type: application/json +``` + +**请求体**: + +```json +{ + "soldier_id": "soldier_001", + "password": "123456" +} +``` + +**响应**: + +```json +// 成功 +{ + "ok": true, + "token": "a1b2c3d4e5f6...", + "soldier_id": "soldier_001", + "name": "张三", + "unit": "第3步兵师/1连", + "role": "狙击手" +} + +// 失败 +{ "ok": false, "error": "士兵编号不存在" } // 404 +{ "ok": false, "error": "密码错误" } // 401 +``` + +--- + +#### ③ 查看当前登录用户 + +```http +GET /api/auth/me +X-Auth-Token: +``` + +**响应**: + +```json +{ + "ok": true, + "user": { + "soldier_id": "soldier_001", + "name": "张三", + "unit": "第3步兵师/1连", + "role": "狙击手" + } +} +``` + +--- + +#### ④ 查看所有账号(免认证) + +```http +GET /api/auth/accounts +``` + +**响应**: + +```json +{ + "accounts": [ + { "soldier_id": "soldier_001", "name": "张三", "unit": "...", "role": "..." }, + { "soldier_id": "soldier_002", "name": "李四", "unit": "...", "role": "..." } + ] +} +``` + +--- + +### 3.2 士兵位置 + +#### ① 上报位置(需认证) + +```http +POST /api/soldier/location +Content-Type: application/json +X-Auth-Token: +``` + +**请求体**: + +```json +{ + "id": "soldier_001", + "name": "张三", + "lat": 30.1234, + "lng": 120.5678 +} +``` + +**响应**: + +```json +{ "ok": true } +``` + +--- + +#### ② 获取所有士兵位置(免认证) + +```http +GET /api/soldiers +``` + +**响应**: + +```json +{ + "soldiers": [ + { + "soldier_id": "soldier_001", + "name": "张三", + "lat": 30.1234, + "lng": 120.5678, + "updated_at": "2026-05-23 12:00:00" + } + ] +} +``` + +--- + +### 3.3 物资需求 + +#### ① 上报需求(需认证) + +```http +POST /api/demand +Content-Type: application/json +X-Auth-Token: +``` + +**请求体**: + +```json +{ + "soldier_id": "soldier_001", + "soldier_name": "张三", + "type": "弹药", + "quantity": 20, + "unit": "发", + "urgency": "紧急", + "drop_point": { "lat": 30.1234, "lng": 120.5678 } +} +``` + +> `drop_point` 可为空或字符串,后端会自动从 `soldiers` 表补全当前位置。 + +**响应**: + +```json +{ "ok": true, "id": "REQ-001" } +``` + +--- + +#### ② 查看需求列表(免认证) + +```http +GET /api/demands?soldier_id=soldier_001 +``` + +**参数**: + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `soldier_id` | string | 否 | 指定士兵 ID,不填则返回所有"待处理"需求 | + +**响应**: + +```json +{ + "demands": [ + { + "id": "REQ-001", + "soldier_id": "soldier_001", + "soldier_name": "张三", + "type": "弹药补给", + "items": "弹药 × 20发", + "quantity": 20, + "unit": "发", + "urgency": "紧急", + "status": "待处理", + "lat": 30.1234, + "lng": 120.5678, + "created_at": "2026-05-23 12:00:00" + } + ] +} +``` + +--- + +#### ③ 查看单个需求(需认证) + +```http +GET /api/demands/REQ-001 +X-Auth-Token: +``` + +**响应**: + +```json +{ "demand": { ... } } +``` + +--- + +### 3.4 任务调度 + +#### ① 派发任务(需认证) + +```http +POST /api/task/dispatch +Content-Type: application/json +X-Auth-Token: +``` + +**请求体**: + +```json +{ + "soldier_id": "soldier_001", + "soldier_name": "张三", + "type": "弹药投送", + "demand_id": "REQ-001", + "start_name": "后方阵地", + "target_name": "A区街角12号", + "start_lat": 30.0, + "start_lng": 120.0, + "end_lat": 30.1234, + "end_lng": 120.5678, + "safety_score": 90 +} +``` + +**响应**: + +```json +{ + "ok": true, + "task": { + "id": "#001", + "soldier_id": "soldier_001", + "type": "弹药投送", + "status": "执行中", + "progress": 0, + "eta": "计算中...", + "remain_time": "计算中...", + "start_name": "后方阵地", + "target_name": "A区街角12号", + "start_lat": 30.0, + "start_lng": 120.0, + "end_lat": 30.1234, + "end_lng": 120.5678, + "safety_score": 90, + "created_at": "2026-05-23 12:00:00" + } +} +``` + +> 派发任务后,对应需求 `REQ-001` 的 `status` 会自动更新为 `"已调度"`。 + +--- + +#### ② 获取当前任务(需认证) + +```http +GET /api/task/current?soldier_id=soldier_001 +X-Auth-Token: +``` + +**响应**: + +```json +{ + "task": { + "id": "#001", + "status": "执行中", + "progress": 50, + "eta": "2分钟", + ... + } +} + +// 无任务时 +{ + "task": { + "id": "#--", "status": "无任务", "progress": 0, + "eta": "--", "remain_time": "--", + "start_name": "--", "target_name": "--", "safety_score": 0 + } +} +``` + +--- + +#### ③ 更新任务进度(需认证) + +```http +POST /api/task/update +Content-Type: application/json +X-Auth-Token: +``` + +**请求体**: + +```json +{ + "soldier_id": "soldier_001", + "progress": 75, + "status": "执行中", + "eta": "1分钟", + "remain_time": "60秒" +} +``` + +**响应**: + +```json +{ "ok": true, "task": { ... } } +``` + +--- + +### 3.5 投放点 + +#### ① 获取投放点列表(免认证) + +```http +GET /api/drop-points +``` + +**响应**: + +```json +{ + "drop_points": [ + { "id": 1, "name": "A区街角12号", "lat": 30.1234, "lng": 120.5678 } + ] +} +``` + +--- + +#### ② 添加投放点(需认证) + +```http +POST /api/drop-point +Content-Type: application/json +X-Auth-Token: +``` + +**请求体**: + +```json +{ + "name": "B区道路", + "lat": 30.2345, + "lng": 120.6789 +} +``` + +**响应**: + +```json +{ "ok": true } +``` + +--- + +### 3.6 危险区域 + +#### ① 查看危险区域(免认证) + +```http +GET /api/danger-zones +``` + +**响应**: + +```json +{ + "danger_zones": [ + { + "id": 1, + "lat": 30.1234, + "lng": 120.5678, + "radius": 100, + "description": "士兵求救: 张三", + "created_at": "2026-05-23 12:00:00" + } + ] +} +``` + +--- + +#### ② 添加危险区域(需认证) + +```http +POST /api/danger-zones +Content-Type: application/json +X-Auth-Token: +``` + +**请求体**: + +```json +{ + "lat": 30.1234, + "lng": 120.5678, + "radius": 100, + "description": "B区道路(危险区域)" +} +``` + +**响应**: + +```json +{ "ok": true, "id": 1 } +``` + +--- + +### 3.7 SOS 求救 + +#### ① 发送求救(需认证) + +```http +POST /api/sos +Content-Type: application/json +X-Auth-Token: +``` + +**请求体**: + +```json +{ + "soldier_id": "soldier_001", + "soldier_name": "张三", + "lat": 30.1234, + "lng": 120.5678 +} +``` + +**响应**: + +```json +{ "ok": true } +``` + +> 发送求救后,系统会自动在危险区域表中添加一个以士兵位置为中心、半径 100 米的危险区域。 + +--- + +### 3.8 无人机状态(演示数据) + +#### ① 获取无人机状态(免认证) + +```http +GET /api/drone/status +``` + +**响应**: + +```json +{ + "status": { + "battery": 85, + "altitude": 120, + "speed": 15, + "position": { "lat": 30.1234, "lng": 120.5678 } + } +} +``` + +--- + +#### ② 获取无人机日志(免认证) + +```http +GET /api/drone/logs +``` + +**响应**: + +```json +{ + "logs": [ + { "time": "12:25:30", "message": "到达投放点" }, + { "time": "12:20:45", "message": "接收任务指令" }, + { "time": "12:10:00", "message": "任务分配" } + ] +} +``` + +--- + +### 3.9 系统状态 + +#### ① 存活检测(免认证) + +```http +GET /api/ping +``` + +**响应**: + +```json +{ "ok": true, "message": "智途投送后端运行正常" } +``` + +--- + +## 四、数据库表结构 + +### 4.1 accounts(账号表) + +| 字段 | 类型 | 说明 | +|------|------|------| +| `soldier_id` | TEXT PK | 士兵编号 | +| `password_hash` | TEXT | Werkzeug 哈希密码 | +| `name` | TEXT | 姓名 | +| `unit` | TEXT | 单位 | +| `role` | TEXT | 角色 | +| `created_at` | TEXT | 注册时间 | + +### 4.2 tokens(登录令牌表) + +| 字段 | 类型 | 说明 | +|------|------|------| +| `token` | TEXT PK | 随机 Token | +| `soldier_id` | TEXT | 士兵编号 | +| `name` | TEXT | 姓名 | +| `unit` | TEXT | 单位 | +| `role` | TEXT | 角色 | +| `created_at` | TEXT | 生成时间 | + +### 4.3 demands(物资需求表) + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | TEXT PK | 需求编号(如 REQ-001) | +| `soldier_id` | TEXT | 上报士兵 | +| `soldier_name` | TEXT | 士兵姓名 | +| `type` | TEXT | 物资类型(如"弹药补给") | +| `items` | TEXT | 物资描述(如"弹药 × 20发") | +| `quantity` | INTEGER | 数量 | +| `unit` | TEXT | 单位 | +| `urgency` | TEXT | 紧急程度 | +| `status` | TEXT | 状态:待处理/已调度/已完成 | +| `lat` | REAL | 纬度 | +| `lng` | REAL | 经度 | +| `created_at` | TEXT | 创建时间 | + +### 4.4 tasks(任务表) + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | TEXT PK | 任务编号(如 #001) | +| `soldier_id` | TEXT | 目标士兵 | +| `soldier_name` | TEXT | 士兵姓名 | +| `type` | TEXT | 任务类型 | +| `status` | TEXT | 状态 | +| `progress` | INTEGER | 进度 0-100 | +| `eta` | TEXT | 预计到达时间 | +| `remain_time` | TEXT | 剩余时间 | +| `start_name/target_name` | TEXT | 起止点名称 | +| `start_lat/lng` | REAL | 起点坐标 | +| `end_lat/lng` | REAL | 终点坐标 | +| `safety_score` | INTEGER | 安全评分 | +| `created_at` | TEXT | 创建时间 | + +### 4.5 soldiers(士兵位置表) + +| 字段 | 类型 | 说明 | +|------|------|------| +| `soldier_id` | TEXT PK | 士兵编号 | +| `name` | TEXT | 姓名 | +| `lat` | REAL | 纬度 | +| `lng` | REAL | 经度 | +| `updated_at` | TEXT | 更新时间 | + +### 4.6 danger_zones(危险区域表) + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | INTEGER PK AI | 自增 ID | +| `lat` | REAL | 纬度 | +| `lng` | REAL | 经度 | +| `radius` | REAL | 半径(米) | +| `description` | TEXT | 描述 | +| `created_at` | TEXT | 创建时间 | + +### 4.7 sos_alerts(求救记录表) + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | INTEGER PK AI | 自增 ID | +| `soldier_id` | TEXT | 士兵编号 | +| `soldier_name` | TEXT | 姓名 | +| `lat` | REAL | 纬度 | +| `lng` | REAL | 经度 | +| `alert_time` | TEXT | 求救时间 | +| `handled` | INTEGER | 是否已处理(0/1) | + +--- + +## 五、错误码速查 + +| HTTP 状态码 | 含义 | 典型场景 | +|-------------|------|----------| +| 200 | 成功 | 正常响应 | +| 400 | 请求参数错误 | 缺少必填字段 | +| 401 | 未认证 | Token 无效或缺失 | +| 404 | 资源不存在 | 士兵编号不存在、需求不存在 | +| 409 | 冲突 | 账号已注册 | +| 500 | 服务器内部错误 | 代码异常 | + +--- + +## 六、联系 + +- 后端维护:赵昌 +- 服务器 IP:`121.41.216.243` +- 操作手册:`software/后端操作手册.md` + +--- + +*文档生成时间:2026-05-23* diff --git a/单兵终端+声源分析_问题清单.md b/单兵终端+声源分析_问题清单.md new file mode 100644 index 00000000..8f075b72 --- /dev/null +++ b/单兵终端+声源分析_问题清单.md @@ -0,0 +1,142 @@ +# 单兵终端APP + 声源分析模块 问题清单 + +> 评估范围:仅评估你们组负责的两个模块 +> 评估时间:2026-05-23 + +--- + +## 一、单兵终端APP 问题清单 + +### 🔴 严重问题(演示前必须修复) + +| # | 问题 | 位置 | 影响 | 修复方式 | +|---|------|------|------|----------| +| 1 | **HTML 语法错误** — `1` 多余字符 | `index.html:471` | DOM 解析异常,底部导航栏下方出现无意义文本 | 删除 `` 后的 `1` | +| 2 | **调用不存在函数** — `toggleTheme`、`showChangePasswordModal`、`selectDropPointFromMap` | `index.html:237,426,436` | 用户点击直接抛出 `TypeError`,APP 崩溃 | 方案A:在 `app.js` 中补齐这 3 个函数
方案B:从 `index.html` 中移除这 3 处 `onclick` 绑定 | +| 3 | **数量验证逻辑漏洞** — `"abc" < 1` 为 `false` | `js/app.js:287` | 输入非数字(如"abc")绕过验证,`parseInt` 生成 `NaN` 发送到后端导致 500 | ```javascript +const num = parseInt(qty, 10); +if (isNaN(num) || num < 1) { showToast('请输入有效数量'); return; } +``` | +| 4 | **高德地图 API Key 硬编码泄露** | `js/location.js:22,66` | Key 被盗用产生额外费用 | 从代码中删除硬编码 Key,改为从配置文件读取 | +| 5 | **DOM 操作空值保护缺失** — `loadDroneStatus` | `js/app.js:547-557` | 元素不存在时抛出 `TypeError` | 统一添加 `if (el) el.textContent = ...` | +| 6 | **敏感凭证明文存储** — `auth_token` 存 `localStorage` | `js/app.js:196`, `js/api.js:8` | XSS 攻击可直接窃取 Token | 改为 `sessionStorage`,或实现 Token 自动刷新 | +| 7 | **路由栈无限增长** — `pageStack` 只有 push | `js/app.js:75-123` | 内存泄漏,长时间使用后卡顿 | 登录成功用 `replace` 而非 `push`;限制栈深度 | +| 8 | **后台轮询未暂停** | `js/app.js:715`, `js/location.js:509` | 耗电、流量浪费 | 监听 `visibilitychange`,隐藏时 `clearInterval` | +| 9 | **请求超时过短** — 统一 5 秒 | `js/api.js:17` | 弱网环境下频繁超时 | 关键接口改为 10 秒,非关键保持 5 秒 | + +### 🟡 一般问题(建议修复) + +| # | 问题 | 位置 | 影响 | +|---|------|------|------| +| 10 | **重复嵌套目录** — `js/js/`、`www/js/js/` | 全项目 | 构建脚本路径配置错误,APK 体积增大 | +| 11 | **登录后未清空密码** | `js/app.js:181-213` | 密码在内存中持续存在 | +| 12 | **Mock 数据字段缺失** — `getMockDropPoints` 无 `lat`/`lng` | `js/api.js:177-183` | 地图定位到 `(0, 0)` | +| 13 | **`setInterval` async 未防并发** | `js/location.js:514` | 网络阻塞时请求堆积 | +| 14 | **位置上报失败静默吞掉** | `js/location.js:520-528` | 用户不知位置是否同步成功 | +| 15 | **网络/电量为静态假数据** | `index.html:99-100` | UI 永远显示 `--` | +| 16 | **偏移距离使用随机数** | `js/app.js:636-637` | `Math.random() * 100` 误导用户 | + +### 🟢 建议(优化项) + +| # | 建议 | 说明 | +|---|------|------| +| 17 | 删除 `rememberMe` 复选框 | 代码完全未实现,纯摆设 | +| 18 | 统一错误码体系 | 区分网络超时/500/认证失效/业务错误 | +| 19 | 地图实例缓存 | 避免每次进入页面销毁重建地图 | + +--- + +## 二、声源分析模块(acoustic)问题清单 + +### 🔴 严重问题(演示前必须修复) + +| # | 问题 | 位置 | 影响 | 修复方式 | +|---|------|------|------|----------| +| 1 | **`AudioBuffer` 除零崩溃** — `capacity_frames_ = 0` 时 `% 0` | `src/core/audio_buffer.cpp:24` | 构造传入 0 时程序崩溃 | 构造函数加 `assert(capacity_samples > 0)` | +| 2 | **`AudioBuffer` 假线程安全** — 头文件声明线程安全但实现无同步 | `include/acoustic_analyzer/core/audio_buffer.h:14` | 多线程调用时数据竞争、内存损坏 | 将 `head_`/`tail_`/`size_` 改为 `std::atomic` 或加 `std::mutex` | +| 3 | **GCC-PHAT 整数溢出** — `int nfft` 左移可能溢出为负数 | `src/core/gcc_phat_localizer.cpp:88-133` | 大输入时死循环或分配负尺寸数组 | 循环索引统一用 `std::size_t`;对 `nfft` 加上限检查 | +| 4 | **YAML 路径不展开** — `$(find pkg)` 语法原样传入 | `config/acoustic_params.yaml:49-50`
`src/ros/acoustic_node.cpp:77-78` | 模型加载失败,检测流水线静默失效,永远输出 "unknown" | ```cpp +std::string pkg_path = ros::package::getPath("acoustic_analyzer"); +params_.model_path = pkg_path + "/models/gunshot_classifier.onnx"; +``` | +| 5 | **UDP 接收队列无界** — `buffer_` 无限增长 | `src/io/mobile_phone_source.cpp:69-95` | 消费慢时内存耗尽 | 设置队列上限,超限时丢弃最旧数据 | +| 6 | **C++ 多模态融合未订阅声学话题** ❗ | `src/多模态融合/cpp/src/main.cpp` | 声学节点发布的 `/acoustic/threats` 无人消费,声源数据无法参与融合避障 | 在融合节点的 `setup_ros()` 中增加:
```cpp +ros::Subscriber acoustic_sub_ = nh.subscribe("/acoustic/threats", 5, + &ThreatFusionNode::acoustic_threats_cb, this); +``` | + +### 🟡 一般问题(建议修复) + +| # | 问题 | 位置 | 影响 | +|---|------|------|------| +| 7 | **ONNX 节点名硬编码** — `"input"`/`"output"` | `src/core/gunshot_classifier.cpp:170-172` | 模型节点名不同时 `session->Run` 失败 | +| 8 | **Mel 滤波器组路径硬编码** — 相对路径 `"models/..."` | `src/core/feature_extractor.cpp:33` | 工作目录不同时找不到文件 | +| 9 | **魔法数字 `63`** — 目标帧数与模型强耦合 | `src/core/feature_extractor.cpp:124` | 更换模型需重新编译 | +| 10 | **分类器与定位器输入来源不一致** | `src/core/pipeline.cpp:130-149` | 分类器看 0.5s,定位器看 2s,精度下降 | +| 11 | **`Predict` 对空矩阵无校验** | `src/core/gunshot_classifier.cpp:155` | 传入空矩阵时行为未定义 | +| 12 | **demo 参数解析无异常保护** | `tests/demo_offline.cpp:200` | 输入非数字时崩溃 | +| 13 | **WAV 头解析依赖未打包结构体** | `src/io/wav_file_source.cpp:48-50` | 编译器插入填充时解析失败 | +| 14 | **缺少启动自检** — 模型加载失败不退出 | `src/ros/acoustic_node.cpp:63-85` | 节点启动后永远输出假数据 | +| 15 | **`CMakeLists.txt` 缺少 onnxruntime 目录** | `CMakeLists.txt:75` | 首次克隆构建失败 | +| 16 | **`package.xml` 未声明 yaml-cpp 依赖** | `package.xml` | `rosdep` 安装时遗漏 | + +### 🟢 建议(优化项) + +| # | 建议 | 说明 | +|---|------|------| +| 17 | 统一 ROS 话题命名空间 | 声学节点应支持 UAV 命名空间前缀,如 `/uav1/acoustic/threats` | +| 18 | 增加诊断心跳 | 发布 `DiagnosticArray`,报告模型加载状态、推理耗时 | +| 19 | 将 VAD 门限等参数移入配置 | `-60.0f dB`、`zcr_rate` 等不应写死在代码中 | +| 20 | 声速 `343.0f` 作为配置参数 | 不同温度/海拔下声速变化 | + +--- + +## 三、两个模块之间的关联问题 + +### 问题 1:后端与声源模块零集成 ❌ + +- **现状**:后端 `app.py` 没有任何与声源分析模块的接口 +- **影响**:单兵APP无法接收声源威胁告警 +- **建议**:如需 APP 接收声源告警,后端应订阅 ROS `/acoustic/threats` 话题,通过 WebSocket 推送或轮询接口发送到 APP + +### 问题 2:后端无人机状态是演示数据 ⚠️ + +- **现状**:`_drone_status` 和 `_drone_logs` 是内存变量,非真实无人机数据 +- **影响**:单兵APP看到的无人机状态是静态假数据 +- **说明**:这是整体架构设计(电脑端前端直连 rosbridge),不属你们模块的责任范围,但需在演示时明确说明 + +--- + +## 四、优先修复清单(演示前) + +### 单兵终端(Top 5) +1. 删除 `index.html:471` 的 `1` +2. 补齐/移除不存在的函数调用 +3. 修复数量验证逻辑 +4. 移除硬编码高德 Key +5. 为 DOM 操作加空值保护 + +### 声源分析(Top 5) +1. 修复 `AudioBuffer` 除零和线程安全 +2. 修复 YAML 路径 `$(find)` 不展开问题 +3. **在 C++ 多模态融合节点中订阅 `/acoustic/threats`** +4. 修复 GCC-PHAT 整数溢出 +5. 为 UDP 接收队列加容量上限 + +--- + +## 五、演示注意事项 + +| 场景 | 预期表现 | 风险点 | +|------|----------|--------| +| 士兵登录APP | 正常 | Token 过期需重新登录 | +| 上报物资需求 | 正常 | 数量输入非数字可能崩溃 | +| 查看无人机状态 | 显示静态演示数据 | 需提前说明这是演示数据 | +| 声源检测 → 融合避障 | **可能无法工作** | 融合节点未订阅声学话题 | +| SOS求救 | 正常 | 会自动标记危险区域 | + +**特别提醒**:声源分析模块虽然在离线 demo 中能正常运行,但在完整系统链路中**无法将声源数据送入多模态融合**,这是你们组需要与其他组协调修复的关键集成点。 + +--- + +*报告生成时间:2026-05-23* diff --git a/多模态融合_声学订阅补丁.md b/多模态融合_声学订阅补丁.md new file mode 100644 index 00000000..612c03bc --- /dev/null +++ b/多模态融合_声学订阅补丁.md @@ -0,0 +1,64 @@ +# 多模态融合节点 — 声学威胁订阅补丁 + +> 问题:C++ 多模态融合节点未订阅 `/acoustic/threats`,导致声源数据无法参与融合避障。 +> 适用文件:`src/多模态融合/cpp/src/main.cpp` + +--- + +## 修改 1:添加成员变量 + +在 `ros::Subscriber flash_det_sub_;` 之后添加: + +```cpp + ros::Subscriber acoustic_sub_; // /acoustic/threats +``` + +## 修改 2:在 setup_ros() 中订阅声学话题 + +在 `flash_enabled` 订阅块之后、`// ── 定时器 ──` 之前添加: + +```cpp + // ── 声学威胁订阅者 ── + if (cfg_.acoustic_enabled) { + acoustic_sub_ = nh.subscribe("/acoustic/threats", 5, + &ThreatFusionNode::acoustic_threats_cb, this); + ROS_INFO("[ThreatFusion] 已订阅 /acoustic/threats"); + } +``` + +## 修改 3:添加回调函数 + +在 `flash_detection_cb` 之后、`fusion_timer_cb` 之前添加: + +```cpp + void acoustic_threats_cb(const acoustic_analyzer::AcousticThreatArray::ConstPtr& msg) { + for (const auto& t : msg->threats) { + if (t.confidence < cfg_.acoustic_confidence_threshold) continue; + AcousticThreatData a; + a.threat_id = t.threat_id; + a.sound_type = t.sound_type; + a.confidence = t.confidence; + a.azimuth = t.azimuth; + a.elevation = t.elevation; + a.distance = t.distance; + a.distance_confidence = t.distance_confidence; + a.timestamp = get_time(); + cache_.add_acoustic(a); + } + modality_status_["acoustic"] = "OK"; + } +``` + +## 修改 4:添加消息头文件 include + +在文件顶部的 include 区域添加: + +```cpp +#include +``` + +> 注:若构建时提示找不到该头文件,需确保 `acoustic_analyzer` 包已编译(`catkin_make`),并在 CMakeLists.txt 的 `find_package` 中声明依赖。 + +--- + +*补丁生成时间:2026-05-23* diff --git a/适配程度评估报告.md b/适配程度评估报告.md new file mode 100644 index 00000000..e1dda293 --- /dev/null +++ b/适配程度评估报告.md @@ -0,0 +1,229 @@ +# 智途投送系统 — 适配程度评估报告 + +> 评估范围:单兵终端APP + 软件电脑端后端 +> 评估时间:2026-05-23 +> 评估维度:功能完整性、接口一致性、安全性、健壮性、与整体架构适配度 + +--- + +## 一、总体评分 + +| 维度 | 评分 | 说明 | +|------|------|------| +| **功能完整性** | ⭐⭐⭐⭐☆ (4/5) | 覆盖了登录、位置上报、需求上报、任务查询、SOS、无人机监控等核心功能 | +| **接口一致性** | ⭐⭐⭐⭐☆ (4/5) | API 路径和字段名基本一致,少量字段缺失/不匹配 | +| **安全性** | ⭐⭐⭐☆☆ (3/5) | Token 认证正确,但存在明文存储、CORS 全开放、SQL 注入风险 | +| **健壮性** | ⭐⭐⭐☆☆ (3/5) | 存在运行时崩溃风险、输入验证漏洞、后台资源泄漏 | +| **架构适配度** | ⭐⭐⭐⭐☆ (4/5) | 后端作为数据中枢定位清晰,但与无人机模块深度集成不足 | + +**综合评分:3.6 / 5** — 核心功能可用,但存在多处安全与健壮性问题,建议修复后再正式交付。 + +--- + +## 二、单兵终端APP 问题清单 + +### 🔴 严重问题(必须修复) + +| # | 问题 | 位置 | 影响 | 修复建议 | +|---|------|------|------|----------| +| 1 | **HTML 语法错误** — `1` 多余字符 | `index.html:471` | DOM 结构破坏,底部导航异常 | 删除多余的 `1` | +| 2 | **调用不存在函数** — `toggleTheme`、`showChangePasswordModal`、`selectDropPointFromMap` | `index.html:237,426,436` | 点击直接抛出 `TypeError` | 补齐函数或从 HTML 移除调用 | +| 3 | **数量验证逻辑漏洞** — `"abc" < 1` 为 `false` | `js/app.js:287` | 非数字输入绕过验证,发送 `NaN` 到后端 | `const num = parseInt(qty, 10); if (isNaN(num) || num < 1)` | +| 4 | **高德地图 API Key 硬编码泄露** — `c014127be1ea5a1efead8419c94fbaba` | `js/location.js:22,66` | Key 被盗用风险,产生额外费用 | 移至配置文件,使用环境变量 | +| 5 | **DOM 操作空值保护缺失** — `loadDroneStatus` 多处 | `js/app.js:547-557` | 元素不存在时抛出 `TypeError` | 统一添加 `if (el) el.textContent = ...` | +| 6 | **敏感凭证明文存储** — `auth_token` 存 `localStorage` | `js/app.js:196`, `js/api.js:8` | XSS 攻击可直接窃取 Token | 改为 `sessionStorage` 或实现 Token 自动刷新 | +| 7 | **路由栈无限增长** — `pageStack` 只有 push 无清理 | `js/app.js:75-123` | 内存泄漏,长时间使用后卡顿 | 限制栈深度,登录成功用 replace 而非 push | +| 8 | **后台轮询未暂停** — 未监听 `visibilitychange` | `js/app.js:715`, `js/location.js:509` | 耗电、流量浪费、后台请求被系统限制 | 页面隐藏时 `clearInterval`,切回时恢复 | +| 9 | **请求超时过短** — 统一 5 秒 | `js/api.js:17` | 弱网环境下频繁超时 | 分接口设置超时(关键接口 10s,非关键 5s) | + +### 🟡 一般问题(建议修复) + +| # | 问题 | 位置 | 影响 | +|---|------|------|------| +| 10 | **重复嵌套目录** — `js/js/`、`www/js/js/` | 全项目 | 构建脚本配置错误,APK 体积增大 | +| 11 | **登录后未清空密码输入框** | `js/app.js:181-213` | 密码在内存中持续存在 | +| 12 | **Mock 数据字段缺失** — `getMockDropPoints` 无 `lat`/`lng` | `js/api.js:177-183` | 地图定位到 `(0, 0)` | +| 13 | **`setInterval` async 未防并发** | `js/location.js:514` | 网络阻塞时请求堆积 | +| 14 | **位置上报失败静默吞掉** | `js/location.js:520-528` | 用户不知位置是否同步成功 | +| 15 | **网络/电量为静态假数据** | `index.html:99-100` | UI 显示永远的占位符 `--` | +| 16 | **偏移距离使用随机数** | `js/app.js:636-637` | `Math.random() * 100 + 'm'` 误导用户 | +| 17 | **`rememberMe` 复选框为摆设** | `index.html` | UI 存在但代码完全未实现 | + +### 🟢 建议(优化项) + +| # | 建议 | 说明 | +|---|------|------| +| 18 | 迁移至 ES Modules | 当前全局 IIFE 方式,命名空间污染风险 | +| 19 | 增加 JSDoc 类型注解 | 提升代码可维护性 | +| 20 | 统一错误码体系 | 区分网络超时/500/认证失效/业务错误 | +| 21 | 地图实例缓存 | 避免每次进入页面销毁重建地图 | + +--- + +## 三、电脑端后端问题清单 + +### 🔴 严重问题 + +| # | 问题 | 位置 | 影响 | 修复建议 | +|---|------|------|------|----------| +| 1 | **SQL 注入风险** — `update_task` 使用 f-string 拼接 SQL | `app.py:462` | 攻击者可通过 `status` 字段注入恶意 SQL | 使用参数化查询:`f"UPDATE tasks SET {','.join(updates)} WHERE soldier_id=?"` 中 `updates` 列表需白名单校验 | +| 2 | **CORS 完全开放** — `CORS(app)` 未限制来源 | `app.py:21` | 任何网站都可跨域调用后端 API | `CORS(app, origins=["http://121.41.216.243", "http://localhost"])` | +| 3 | **Token 无过期机制** — 永久有效 | `app.py:551` | Token 泄露后攻击者可长期冒充用户 | 添加 `expires_at` 字段,定期清理过期 Token | +| 4 | **无连接池** — 每次请求新建 SQLite 连接 | `app.py:24-27` | 高并发时性能下降,连接开销大 | 使用连接池或单例连接(注意线程安全) | +| 5 | **无人机状态内存不一致** — `_drone_status` 是全局变量 | `app.py:139-148` | 多 worker 时不同进程看到不同状态 | 改为从 ROS/WebSocket 实时获取,或存数据库 | +| 6 | **Flask 默认 500 页面暴露信息** — 返回 HTML | 全局 | 泄露技术栈信息(Python/Flask 版本) | 注册自定义错误处理器:`@app.errorhandler(500)` | + +### 🟡 一般问题 + +| # | 问题 | 位置 | 影响 | +|---|------|------|------| +| 7 | **密码最小长度无验证** | `app.py:515` | 可注册空密码或 1 位密码 | +| 8 | **注册时 unit/role 未校验** | `app.py:512-513` | 可传入超长字符串或特殊字符 | +| 9 | **`get_accounts` 返回所有字段** | `app.py:569-576` | 虽然密码是哈希的,但仍应只返回必要字段 | +| 10 | **无限流/防刷机制** | 全局 | 注册/登录接口可被暴力破解 | +| 11 | **数据库异常未捕获** — `sqlite3.IntegrityError` | `app.py` | 主键冲突时返回 500 而非友好的错误信息 | +| 12 | **危险区域与 SOS 联动缺少事务** | `app.py:488-499` | SOS 插入成功但危险区域插入失败时数据不一致 | + +### 🟢 建议 + +| # | 建议 | 说明 | +|---|------|------| +| 13 | 添加 API 日志中间件 | 记录请求来源、耗时、错误,便于排错 | +| 14 | 使用 ` marshmallow ` 或 ` pydantic ` 做请求参数校验 | 替代手动 `data.get()`,减少类型转换错误 | +| 15 | 添加健康检查端点 | `/api/health` 返回数据库连接状态、服务运行时间 | + +--- + +## 四、前后端接口一致性检查 + +### ✅ 一致的接口 + +| 功能 | 前端调用 | 后端路由 | 字段匹配 | +|------|----------|----------|----------| +| 登录 | `POST /api/auth/login` | `POST /api/auth/login` | `soldier_id`, `password` ✓ | +| 注册 | `POST /api/auth/register` | `POST /api/auth/register` | `soldier_id`, `password`, `name`, `unit`, `role` ✓ | +| 位置上报 | `POST /api/soldier/location` | `POST /api/soldier/location` | `id`, `name`, `lat`, `lng` ✓ | +| 需求上报 | `POST /api/demand` | `POST /api/demand` | `type`, `quantity`, `unit`, `urgency`, `drop_point` ✓ | +| 需求列表 | `GET /api/demands` | `GET /api/demands` | `soldier_id` (query) ✓ | +| 任务查询 | `GET /api/task/current` | `GET /api/task/current` | `soldier_id` (query) ✓ | +| SOS 求救 | `POST /api/sos` | `POST /api/sos` | `soldier_id`, `soldier_name`, `lat`, `lng` ✓ | +| 投放点列表 | `GET /api/drop-points` | `GET /api/drop-points` | 无参数 ✓ | +| 账号列表 | `GET /api/auth/accounts` | `GET /api/auth/accounts` | 无参数 ✓ | +| 存活检测 | `GET /api/ping` | `GET /api/ping` | 无参数 ✓ | + +### ⚠️ 存在差异的接口 + +| 功能 | 问题 | 说明 | +|------|------|------| +| **前端调用不存在函数** | HTML 绑定 `App.toggleTheme()`、`App.showChangePasswordModal()`、`App.selectDropPointFromMap()`,但 `app.js` 中不存在 | 需补齐或移除 HTML 绑定 | +| **任务更新** | 后端有 `POST /api/task/update`,前端无对应调用 | 前端应增加任务进度上报功能 | +| **危险区域** | 后端有 `GET/POST /api/danger-zones`,前端无对应界面 | 前端应增加危险区域展示 | +| **无人机状态** | 前端有 `loadDroneStatus()` 调用 `/api/drone/status`,但后端返回的是**内存演示数据**,非真实无人机状态 | 需明确数据来源(后端演示 vs ROS 实时) | +| **投放点添加** | 后端有 `POST /api/drop-point`,前端无对应功能 | 前端地图选点功能需对接此接口 | + +--- + +## 五、与整体项目的集成评估 + +### 5.1 在整体架构中的定位 + +``` +【单兵APP】◄──HTTP REST──►【后端】◄──数据服务──►【电脑端前端】 + │ + ▼ WebSocket + 【rosbridge】 + │ + ▼ + 【多模态融合 / PX4】 +``` + +后端在架构中的角色是 **"数据中枢 + 认证网关"**: +- ✅ 接收单兵APP的位置、需求、SOS 数据 +- ✅ 为电脑端提供任务调度数据 +- ❌ **不参与无人机控制链路**(无人机状态是演示数据) +- ❌ **不参与声源/热成像/闪光检测链路** + +### 5.2 与无人机模块的集成 + +| 评估项 | 现状 | 评估 | +|--------|------|------| +| 无人机状态数据 | 后端 `_drone_status` 是内存演示数据 | ⚠️ **假数据**,真实状态应来自 ROS | +| 任务派发链路 | 后端生成任务 → 电脑端展示 → 操作员确认 → 无人机执行 | ✅ 链路完整,但后端到无人机的控制命令未打通 | +| 航线规划 | 后端未提供航线规划 API | ❌ 缺失,无人机组可能需要手动规划 | +| 实时遥测 | 电脑端前端通过 WebSocket 直连 rosbridge,不经过后端 | ⚠️ 后端被绕过,无法做数据持久化 | + +**建议**:后端应增加无人机状态同步接口,从 ROS 话题订阅真实状态并存入数据库,供单兵APP和电脑端统一查询。 + +### 5.3 与声源分析模块的集成 + +| 评估项 | 现状 | 评估 | +|--------|------|------| +| 声源威胁数据 | 声源检测节点 → ROS Topic → 多模态融合 → 地面站 | ✅ 链路完整,但**不经过后端** | +| 后端感知 | 后端完全不知道声源检测状态 | ❌ 后端与声源模块零集成 | + +**建议**:如需要单兵APP接收声源威胁告警,后端应订阅 `AcousticThreatArray` 话题,通过 WebSocket 或推送通知发送到APP。 + +### 5.4 与热成像/闪光检测模块的集成 + +| 评估项 | 现状 | 评估 | +|--------|------|------| +| 热成像数据 | 直接 C++ 调用,被多模态融合消费 | ✅ 内部链路完整 | +| 闪光检测数据 | ROS Topic → 多模态融合 | ✅ 内部链路完整 | +| 后端感知 | 后端完全不知道这些检测状态 | ❌ 零集成 | + +--- + +## 六、优先级修复建议 + +### 🔥 第一优先级(课程演示前必须修复) + +1. **修复 `index.html` 语法错误** (`1`) +2. **补齐或移除 HTML 中不存在的函数调用** +3. **修复数量验证逻辑漏洞**(前后端都要修) +4. **修复 SQL 注入风险**(`update_task`) +5. **移除硬编码的高德 Key** + +### 🔶 第二优先级(提升稳定性) + +6. **为 DOM 操作添加空值保护** +7. **限制 CORS 来源** +8. **为 Token 添加过期机制** +9. **后台轮询暂停**(`visibilitychange`) +10. **添加密码最小长度验证** + +### 🟢 第三优先级(优化体验) + +11. **清理 `js/js/` 重复目录** +12. **前端增加任务更新调用**(对接 `POST /api/task/update`) +13. **前端增加危险区域展示** +14. **添加 API 日志中间件** +15. **自定义 500 错误页面** + +--- + +## 七、总结 + +### 优势 +1. **功能覆盖完整**:登录、位置上报、需求上报、任务调度、SOS、无人机监控等核心场景均已实现 +2. **架构分层清晰**:APP → 后端 → 数据库 → 前端,职责明确 +3. **Token 认证正确**:数据库存储,支持多 worker 共享 +4. **SQLite 持久化**:数据不丢失,支持课程演示 + +### 劣势 +1. **健壮性不足**:多处运行时崩溃风险、输入验证漏洞 +2. **安全性薄弱**:CORS 全开放、SQL 注入、敏感信息明文存储 +3. **与机载模块集成浅**:后端不参与无人机控制链路,声源/热成像数据不经过后端 +4. **代码质量参差**:HTML 与 JS 契约不一致、Mock 数据与真实数据不匹配 + +### 适配度结论 + +**单兵终端APP + 电脑端后端** 作为 **"地面层数据服务"** 与整体项目是**适配的**,能够支撑课程演示所需的核心场景(士兵上报需求 → 指挥部调度 → 任务跟踪)。 + +但在以下方面存在明显短板,需要在正式演示前修复: +- 前端的健壮性(运行时崩溃、验证漏洞) +- 后端的安全性(SQL 注入、CORS、Token 过期) +- 前后端契约一致性(不存在的函数调用、缺失的接口对接) + +--- + +*报告生成时间:2026-05-23*