No Multible

This commit is contained in:
ZhangJinLong 2025-09-11 10:49:07 +08:00
parent f87f090542
commit 42250dff0e
58 changed files with 6066 additions and 6408 deletions

1
.idea/.name Normal file
View File

@ -0,0 +1 @@
Cmake_project_test

View File

@ -2,16 +2,8 @@
<project version="4"> <project version="4">
<component name="deploymentTargetSelector"> <component name="deploymentTargetSelector">
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="Unnamed">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-09-02T08:19:09.762084200Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\29096\.android\avd\Medium_Phone.avd" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState> </SelectionState>
</selectionStates> </selectionStates>
</component> </component>

View File

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "interactive"
}

View File

@ -1,200 +0,0 @@
# 蓝牙连接问题解决方案
## 🔍 问题分析
你遇到的问题是:应用一直在扫描蓝牙设备,但没有找到设备可以连接。
## 🎯 解决方案
### 1. 页面布局优化 ✅
**已完成的改进:**
- **添加了图表标题**:显示"ECG实时监测"
- **优化了布局比例**图表区域占70%文本区域占30%
- **添加了默认显示内容**:在没有数据时显示提示信息和示例波形
**现在的显示效果:**
```
┌─────────────────────────────────────────┐
│ [连接蓝牙] [启动程序] │
│ [停止程序] [陷波滤波] │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ ECG实时监测 │
│ ┌─────────────────────────────────────┐ │
│ │ 等待数据... │ │
│ │ 请先连接蓝牙设备 │ │
│ │ 然后点击'启动程序' │ │
│ │ [示例波形图] │ │
│ └─────────────────────────────────────┘ │
│ ┌─────────────────────────────────────┐ │
│ │ 等待数据... │ │
│ │ 请先连接蓝牙设备 │ │
│ │ 然后点击'启动程序' │ │
│ │ [示例波形图] │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 状态信息区域 │
└─────────────────────────────────────────┘
```
### 2. 蓝牙扫描优化 ✅
**已完成的改进:**
- **移除设备名称过滤**:现在会扫描并显示所有发现的蓝牙设备
- **支持更多设备类型**:包括手机、耳机、手表等所有蓝牙设备
- **双模式扫描**优先使用BLE回退到传统蓝牙
**扫描逻辑:**
```kotlin
// 之前:只显示包含"ECG"、"心电"关键词的设备
if (device.name?.contains("ECG", ignoreCase = true) == true) {
addDiscoveredDevice(device)
}
// 现在:显示所有发现的设备
addDiscoveredDevice(device)
```
## 🚀 使用步骤
### 第一步:测试蓝牙扫描
1. **确保蓝牙已开启**
- 进入系统设置 → 蓝牙 → 开启蓝牙
2. **点击"连接蓝牙"按钮**
- 系统会申请权限(如果还没有)
- 开始扫描附近设备
3. **观察扫描结果**
```
蓝牙状态: 正在扫描蓝牙设备...
发现设备: iPhone (00:11:22:33:44:55)
发现设备: AirPods (AA:BB:CC:DD:EE:FF)
发现设备: 小米手环 (11:22:33:44:55:66)
扫描完成,找到 3 个设备
```
### 第二步:选择测试设备
1. **扫描完成后会弹出设备选择对话框**
2. **选择任意一个设备进行测试**(比如你的手机或耳机)
3. **点击设备名称进行连接**
### 第三步:测试连接功能
1. **连接过程观察**
```
正在连接设备: iPhone
设备已连接
服务发现成功
数据通道已建立
```
2. **连接状态验证**
- 按钮文字变为"断开蓝牙"
- 按钮颜色变为红色
## 🔧 测试建议
### 1. 使用模拟数据测试
如果没有真实的心电设备,可以:
**方法一:使用其他蓝牙设备**
- 连接你的手机、耳机、手表等
- 验证连接功能是否正常
- 观察数据接收情况
**方法二:使用蓝牙调试工具**
- 下载蓝牙调试应用如nRF Connect
- 模拟发送数据包
- 测试数据接收功能
### 2. 检查设备兼容性
```bash
# 查看蓝牙相关日志
adb logcat | grep BluetoothManager
# 查看权限状态
adb shell dumpsys package com.example.cmake_project_test | grep permission
```
### 3. 常见设备类型
- **手机**iPhone, Samsung, Huawei, Xiaomi
- **耳机**AirPods, Sony, Bose, Sennheiser
- **手表**Apple Watch, Samsung Galaxy Watch
- **手环**:小米手环, 华为手环, Fitbit
## ⚠️ 注意事项
### 1. 权限要求
确保已授予以下权限:
- `BLUETOOTH_SCAN`
- `BLUETOOTH_CONNECT`
- `ACCESS_FINE_LOCATION`
- `ACCESS_COARSE_LOCATION`
### 2. 设备距离
- 确保设备在蓝牙有效范围内通常10米内
- 确保设备处于可发现状态
### 3. 连接限制
- 某些设备可能不支持GATT连接
- 某些设备可能需要配对密码
- 某些设备可能被其他应用占用
## 🎯 下一步测试
### 1. 功能验证
- [ ] 蓝牙扫描正常工作
- [ ] 设备选择对话框显示
- [ ] 设备连接成功
- [ ] 连接状态正确显示
- [ ] 断开连接功能正常
### 2. 界面验证
- [ ] 图表区域显示默认内容
- [ ] 提示信息清晰可见
- [ ] 示例波形正常显示
- [ ] 布局比例合理
### 3. 性能验证
- [ ] 扫描响应及时
- [ ] 连接过程流畅
- [ ] 界面无卡顿
- [ ] 内存使用合理
## 🔮 后续优化
### 1. 设备过滤选项
可以添加一个开关,让用户选择是否过滤设备:
```kotlin
// 添加过滤开关
private var enableDeviceFilter = false
// 条件过滤
if (!enableDeviceFilter || device.name?.contains("ECG", ignoreCase = true) == true) {
addDiscoveredDevice(device)
}
```
### 2. 设备类型识别
可以自动识别设备类型并分类显示:
```kotlin
// 设备类型识别
enum class DeviceType {
ECG_DEVICE, // 心电设备
PHONE, // 手机
HEADPHONES, // 耳机
WATCH, // 手表
OTHER // 其他
}
```
### 3. 连接历史
保存最近连接的设备列表:
```kotlin
// 保存连接历史
private val connectionHistory = mutableListOf<BluetoothDevice>()
```
现在你可以测试蓝牙连接功能了!建议先连接一个普通的蓝牙设备(如手机或耳机)来验证连接功能是否正常工作。🎉

View File

@ -1,173 +0,0 @@
# 蓝牙数据流处理指南
## 🎯 数据流处理架构
### 数据流向
```
蓝牙设备 → BluetoothManager → MainActivity → DataManager → ECG图表显示
```
### 处理流程
1. **蓝牙数据接收**: `BluetoothManager.onCharacteristicChanged`
2. **数据传递**: `MainActivity.onDataReceived`
3. **数据解析**: `DataManager.onBleNotify`
4. **信号处理**: `DataManager.processStreamingData`
5. **图表更新**: `MainActivity.onProcessedDataAvailable`
## 🔧 已完成的修改
### 1. BluetoothManager ✅
- **Nordic UART Service (NUS)** 协议支持
- **自动UUID匹配**: `6e400001-b5a3-f393-e0a9-68716563686f`
- **数据接收**: `onCharacteristicChanged` 回调
- **状态管理**: 连接状态、错误处理
### 2. MainActivity ✅
- **蓝牙回调实现**: `BluetoothManager.BluetoothCallback`
- **数据接收处理**: `onDataReceived` 方法
- **实时数据回调**: `DataManager.RealTimeDataCallback`
- **图表更新**: `onProcessedDataAvailable` 方法
### 3. DataManager ✅
- **蓝牙数据处理**: `onBleNotify` 方法
- **流式数据处理**: `processStreamingData` 方法
- **信号处理**: 高通滤波 → 低通滤波 → 陷波滤波
- **实时回调**: `onProcessedDataAvailable` 回调
### 4. ECG图表 ✅
- **实时更新**: `ECGRhythmView``ECGWaveformView`
- **数据缓冲**: 性能优化的数据缓冲机制
- **双视图显示**: 10秒节奏视图 + 2.5秒波形视图
## 📱 测试步骤
### 第一步:连接蓝牙设备
1. **点击"连接蓝牙"按钮**
2. **等待扫描完成**
3. **选择你的ECG设备**
4. **等待连接成功**
### 第二步:验证数据接收
1. **观察状态信息**
```
蓝牙状态: 设备已连接
蓝牙状态: 服务发现成功
蓝牙状态: 发现服务: 6e400001-b5a3-f393-e0a9-68716563686f
蓝牙状态: 发现特征: 6e400002-b5a3-f393-e0a9-68716563686f
蓝牙状态: 数据通道已建立,开始接收数据
```
2. **检查数据接收**
```
接收到蓝牙数据: X 字节
解析出 X 个数据包
```
### 第三步:验证图表显示
1. **ECG图表变为可见**
2. **实时波形开始显示**
3. **双视图同步更新**
- **节奏视图**: 10秒显示窗口
- **波形视图**: 2.5秒显示窗口
### 第四步:验证信号处理
1. **观察处理进度**
```
=== 实时流式处理 ===
处理进度: XX%
总样本数: XXX
已处理样本: XXX
实时曲线图已更新 ✓
```
2. **检查滤波效果**
- 信号应该更加平滑
- 噪声应该被有效抑制
- 波形应该清晰可见
## 🔍 关键代码位置
### 1. 蓝牙数据接收
```kotlin
// BluetoothManager.kt - 第468行
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
val data = characteristic.value
callback?.onDataReceived(data)
}
```
### 2. 数据传递处理
```kotlin
// MainActivity.kt - 第656行
override fun onDataReceived(data: ByteArray) {
Thread {
dataManager.onBleNotify(data)
runOnUiThread {
binding.ecgChartContainer.visibility = View.VISIBLE
}
}.start()
}
```
### 3. 数据解析处理
```kotlin
// DataManager.kt - 第73行
fun onBleNotify(chunk: ByteArray) {
ensureParser()
nativeCallback.streamParserAppend(parserHandle, chunk)
val packets = nativeCallback.streamParserDrainPackets(parserHandle)
processStreamingData(packets)
}
```
### 4. 图表更新
```kotlin
// MainActivity.kt - 第558行
override fun onProcessedDataAvailable(channelIndex: Int, processedData: List<Float>) {
binding.ecgRhythmView.updateData(processedData)
binding.ecgWaveformView.updateData(processedData)
}
```
## 🎉 预期效果
### 连接成功时:
- ✅ 蓝牙设备连接成功
- ✅ 数据通道建立
- ✅ 开始接收ECG数据
### 数据处理时:
- ✅ 实时数据解析
- ✅ 信号滤波处理
- ✅ 通道映射完成
### 图表显示时:
- ✅ ECG图表可见
- ✅ 实时波形显示
- ✅ 双视图同步更新
- ✅ 平滑的动画效果
### 性能优化:
- ✅ 后台数据处理
- ✅ UI线程不阻塞
- ✅ 数据缓冲机制
- ✅ 实时回调更新
## ⚠️ 注意事项
### 1. 数据格式
- **Nordic UART Service**: 原始字节流
- **数据解析**: 自动协议解析
- **通道映射**: 8通道 → 12通道
### 2. 性能考虑
- **后台处理**: 避免UI阻塞
- **数据缓冲**: 优化内存使用
- **更新频率**: 平衡实时性和性能
### 3. 错误处理
- **连接失败**: 自动重试机制
- **数据异常**: 错误日志记录
- **UI异常**: 异常捕获处理
现在你的应用已经完全支持蓝牙数据流处理可以实时接收、处理和显示ECG数据了🚀

View File

@ -1,228 +0,0 @@
# 蓝牙功能完整特性说明
## 🚀 功能概述
本应用现已具备完整的蓝牙连接功能,支持真实的心电设备连接和数据接收。
## 🔧 核心特性
### 1. 双模式蓝牙扫描
- **BLE扫描**:优先使用低功耗蓝牙扫描,更快速、更节能
- **传统蓝牙扫描**兼容不支持BLE的设备
- **自动切换**:根据设备能力自动选择扫描模式
### 2. 智能设备过滤
- **名称过滤**:自动识别包含"ECG"、"心电"等关键词的设备
- **设备分类**区分BLE和传统蓝牙设备
- **重复检测**:避免重复添加相同设备
### 3. 实时状态管理
- **连接状态**:实时显示连接、断开、扫描等状态
- **权限检查**:动态检查蓝牙和位置权限
- **错误处理**:友好的错误提示和状态恢复
### 4. 数据流处理
- **实时接收**:自动接收蓝牙设备发送的数据
- **数据解析**:将接收的数据传递给现有的数据处理管道
- **实时显示**在ECG曲线图上实时显示接收的数据
## 📱 用户界面
### 按钮布局优化
```
┌─────────────────────────────────────────┐
│ [连接蓝牙] [启动程序] │
│ [停止程序] [陷波滤波] │
└─────────────────────────────────────────┘
```
### 状态指示
- **连接蓝牙**:绿色(未连接)→ 橙色(扫描中)→ 红色(已连接)
- **启动程序**:启用/禁用状态管理
- **停止程序**:仅在程序运行时启用
- **陷波滤波**:仅在程序运行时启用
## 🔍 技术实现
### 扫描机制
```kotlin
// BLE扫描设置
val scanSettings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
// 设备过滤
if (device.name?.contains("ECG", ignoreCase = true) == true ||
device.name?.contains("心电", ignoreCase = true) == true) {
addDiscoveredDevice(device)
}
```
### 权限管理
```kotlin
private val REQUIRED_PERMISSIONS = arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
```
### 数据流处理
```kotlin
override fun onDataReceived(data: ByteArray) {
// 将蓝牙数据传递给DataManager
dataManager.onBleNotify(data)
updateStatus("接收到蓝牙数据: ${data.size} 字节")
}
```
## 🎯 使用流程
### 1. 权限申请
1. 首次点击"连接蓝牙"按钮
2. 系统弹出权限申请对话框
3. 选择"允许"授予所有权限
### 2. 设备扫描
1. 权限授予后自动开始扫描
2. 扫描持续10秒
3. 自动过滤心电相关设备
### 3. 设备选择
1. 扫描完成后显示设备列表
2. 选择目标心电设备
3. 等待连接建立
### 4. 数据采集
1. 连接成功后点击"启动程序"
2. 观察ECG曲线图显示实时数据
3. 查看状态信息确认数据接收
## 🔧 配置参数
### 蓝牙参数
```kotlin
// 服务UUID (根据设备协议调整)
private val SERVICE_UUID = UUID.fromString("0000fff0-0000-1000-8000-00805f9b34fb")
// 特征UUID (根据设备协议调整)
private val CHARACTERISTIC_UUID = UUID.fromString("0000fff1-0000-1000-8000-00805f9b34fb")
// 设备名称前缀
private const val DEVICE_NAME_PREFIX = "ECG"
```
### 扫描参数
- **扫描时间**10秒
- **扫描模式**:低延迟模式
- **设备过滤**:包含"ECG"、"心电"关键词
## 📊 状态监控
### 连接状态
```
蓝牙状态: 正在扫描蓝牙设备...
发现设备: ECG_Device_001 (00:11:22:33:44:55)
蓝牙设备已连接: ECG_Device_001
接收到蓝牙数据: 128 字节
```
### 权限状态
```
权限状态:
BLUETOOTH_SCAN: ✓
BLUETOOTH_CONNECT: ✓
ACCESS_FINE_LOCATION: ✓
ACCESS_COARSE_LOCATION: ✓
```
## 🔍 调试功能
### 日志输出
- **扫描过程**:记录设备发现和扫描状态
- **连接过程**:记录连接建立和断开
- **数据接收**:记录数据包大小和频率
- **错误处理**:记录详细错误信息
### 调试命令
```bash
# 查看蓝牙相关日志
adb logcat | grep BluetoothManager
# 查看权限相关日志
adb logcat | grep MainActivity
```
## ⚠️ 常见问题
### 1. 扫描无结果
**可能原因**
- 设备不在范围内
- 设备未开启或未处于可发现状态
- 权限未正确授予
**解决方法**
- 确认设备距离和状态
- 检查权限设置
- 重启设备和应用
### 2. 连接失败
**可能原因**
- 设备被其他应用占用
- 设备电池电量不足
- 服务UUID不匹配
**解决方法**
- 关闭其他蓝牙应用
- 检查设备电量
- 确认设备协议
### 3. 数据接收异常
**可能原因**
- 数据格式不匹配
- 连接不稳定
- 设备发送频率过高
**解决方法**
- 检查数据解析逻辑
- 优化连接稳定性
- 调整数据处理频率
## 🔮 未来改进
### 1. 功能增强
- **自动重连**:连接断开时自动尝试重连
- **多设备支持**:同时连接多个设备
- **数据缓存**:本地缓存数据防止丢失
### 2. 用户体验
- **设备管理**:保存常用设备列表
- **连接优化**:优化连接速度和稳定性
- **界面美化**:更直观的状态显示
### 3. 技术优化
- **协议适配**:支持更多设备协议
- **性能优化**:减少电池消耗
- **兼容性**支持更多Android版本
## 📝 注意事项
1. **位置权限必需**:蓝牙扫描需要位置权限
2. **设备兼容性**确保设备支持BLE或传统蓝牙
3. **数据格式**:确保设备发送的数据格式与解析器兼容
4. **连接稳定性**:保持设备在有效范围内
5. **电池管理**:注意设备电池电量,避免连接中断
## 🎉 总结
现在你的应用已经具备了完整的蓝牙连接功能:
**双模式扫描**支持BLE和传统蓝牙
**智能过滤**:自动识别心电设备
**权限管理**:完整的权限申请和检查
**实时数据**:实时接收和显示数据
**状态监控**:详细的状态和错误提示
**界面优化**:合理的按钮布局和状态指示
现在可以连接真实的心电设备并开始数据采集了!🎉

View File

@ -1,144 +0,0 @@
# 蓝牙连接功能使用说明
## 📱 功能概述
本应用已集成蓝牙连接功能,支持连接十二导联心电设备并实时接收数据。
## 🔧 功能特性
### 1. 蓝牙设备管理
- **自动扫描**:点击"连接蓝牙"按钮自动扫描附近设备
- **设备选择**:扫描完成后显示设备列表供用户选择
- **状态显示**:实时显示连接状态和数据接收情况
### 2. 数据流处理
- **实时接收**:自动接收蓝牙设备发送的数据
- **数据处理**:将接收的数据传递给现有的数据处理管道
- **实时显示**在ECG曲线图上实时显示接收的数据
### 3. 用户界面
- **状态指示**:按钮颜色和文字显示当前连接状态
- **日志记录**:详细记录蓝牙操作和状态变化
- **错误处理**:友好的错误提示和状态恢复
## 🚀 使用方法
### 第一步:启用蓝牙
1. 确保设备蓝牙已启用
2. 确保心电设备已开启并处于可发现状态
### 第二步:连接设备
1. 点击"连接蓝牙"按钮
2. 等待扫描完成约2-3秒
3. 在弹出的设备列表中选择目标设备
4. 等待连接建立
### 第三步:开始数据采集
1. 连接成功后,点击"启动程序"按钮
2. 观察ECG曲线图开始显示实时数据
3. 查看状态信息确认数据接收正常
## 🔧 技术配置
### 蓝牙参数设置
```kotlin
// 服务UUID (根据设备协议调整)
private val SERVICE_UUID = UUID.fromString("0000fff0-0000-1000-8000-00805f9b34fb")
// 特征UUID (根据设备协议调整)
private val CHARACTERISTIC_UUID = UUID.fromString("0000fff1-0000-1000-8000-00805f9b34fb")
// 设备名称前缀
private const val DEVICE_NAME_PREFIX = "ECG"
```
### 权限配置
```xml
<!-- 蓝牙权限 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
```
## 📊 状态指示
### 按钮状态
| 状态 | 按钮文字 | 按钮颜色 | 说明 |
|------|----------|----------|------|
| 未连接 | "连接蓝牙" | 绿色 | 可以开始连接 |
| 扫描中 | "扫描中..." | 橙色 | 正在扫描设备 |
| 连接中 | "连接中..." | 橙色 | 正在连接设备 |
| 已连接 | "断开蓝牙" | 红色 | 可以断开连接 |
### 日志信息
- `[时间] 蓝牙状态: 正在扫描蓝牙设备...`
- `[时间] 发现设备: ECG_Device_001`
- `[时间] 蓝牙设备已连接: ECG_Device_001`
- `[时间] 接收到蓝牙数据: 128 字节`
- `[时间] 蓝牙设备已断开`
## 🔍 故障排除
### 常见问题
#### 1. 蓝牙未启用
**症状**:点击连接按钮无反应
**解决**:在系统设置中启用蓝牙
#### 2. 权限不足
**症状**:提示"缺少蓝牙扫描权限"
**解决**:在应用设置中授予蓝牙权限
#### 3. 设备未发现
**症状**:扫描完成但未找到设备
**解决**
- 确认设备已开启
- 确认设备处于可发现状态
- 检查设备距离是否过远
#### 4. 连接失败
**症状**:选择设备后连接失败
**解决**
- 确认设备未被其他应用占用
- 重新启动设备
- 检查设备电池电量
### 调试信息
应用会在日志中记录详细的调试信息,包括:
- 蓝牙初始化状态
- 设备扫描过程
- 连接建立过程
- 数据接收情况
## 🔄 数据流程
```
蓝牙设备 → BluetoothManager → MainActivity → DataManager → 信号处理 → 指标计算 → UI显示
```
1. **蓝牙设备**:发送原始心电数据
2. **BluetoothManager**:接收数据并传递给应用
3. **MainActivity**:处理蓝牙回调和数据转发
4. **DataManager**:解析和处理数据
5. **信号处理**:滤波和信号增强
6. **指标计算**计算心率和HRV指标
7. **UI显示**:在曲线图上显示结果
## 📝 注意事项
1. **首次使用**:需要授予蓝牙权限
2. **设备兼容性**确保设备支持BLE协议
3. **数据格式**:确保设备发送的数据格式与解析器兼容
4. **连接稳定性**:保持设备在有效范围内
5. **电池管理**:注意设备电池电量,避免连接中断
## 🔮 未来改进
1. **自动重连**:连接断开时自动尝试重连
2. **多设备支持**:同时连接多个设备
3. **数据缓存**:本地缓存数据防止丢失
4. **连接优化**:优化连接速度和稳定性
5. **设备管理**:保存常用设备列表

View File

@ -1,143 +0,0 @@
# 蓝牙UUID调试指南
## 🎯 问题描述
连接蓝牙设备后出现"未找到目标服务"错误这是因为设备的UUID与预设的不匹配。
## 🔧 解决方案
### 1. 自动UUID匹配 ✅
- **实现**支持多种常见ECG设备UUID
- **效果**自动尝试匹配不同的服务UUID和特征UUID
- **UUID列表**
```
服务UUID:
- 0000fff0-0000-1000-8000-00805f9b34fb (默认)
- 0000ffe0-0000-1000-8000-00805f9b34fb (变体1)
- 0000ffe5-0000-1000-8000-00805f9b34fb (变体2)
- 0000ff00-0000-1000-8000-00805f9b34fb (变体3)
- 0000ff10-0000-1000-8000-00805f9b34fb (变体4)
特征UUID:
- 0000fff1-0000-1000-8000-00805f9b34fb (默认)
- 0000ffe1-0000-1000-8000-00805f9b34fb (变体1)
- 0000ffe6-0000-1000-8000-00805f9b34fb (变体2)
- 0000ff01-0000-1000-8000-00805f9b34fb (变体3)
- 0000ff11-0000-1000-8000-00805f9b34fb (变体4)
```
### 2. 详细调试信息 ✅
- **实现**:打印所有可用服务和特征
- **效果**显示设备实际提供的UUID
- **日志输出**
```
D/BluetoothManager: 设备提供的服务数量: 3
D/BluetoothManager: 发现服务: 0000fff0-0000-1000-8000-00805f9b34fb
D/BluetoothManager: 服务 0000fff0-0000-1000-8000-00805f9b34fb 的特征数量: 2
D/BluetoothManager: 发现特征: 0000fff1-0000-1000-8000-00805f9b34fb
D/BluetoothManager: 发现特征: 0000fff2-0000-1000-8000-00805f9b34fb
```
## 🚀 测试步骤
### 第一步:连接设备
1. **点击"连接蓝牙"按钮**
2. **选择你的ECG设备**
3. **等待连接完成**
### 第二步:查看调试信息
1. **观察Logcat输出**
```
蓝牙状态: 服务发现成功
发现服务: 0000fff0-0000-1000-8000-00805f9b34fb
发现特征: 0000fff1-0000-1000-8000-00805f9b34fb
```
2. **检查状态信息**
- ✅ 如果显示"数据通道已建立,开始接收数据" → 成功
- ❌ 如果显示"未找到匹配的服务或特征" → 需要手动配置
### 第三步手动配置UUID如果需要
如果自动匹配失败,请:
1. **查看可用服务UUID**
```
状态信息: 可用服务: 0000xxxx-0000-1000-8000-00805f9b34fb, ...
```
2. **记录你的设备UUID**
- 服务UUID: `0000xxxx-0000-1000-8000-00805f9b34fb`
- 特征UUID: `0000yyyy-0000-1000-8000-00805f9b34fb`
3. **修改代码**
```kotlin
private val SERVICE_UUIDS = listOf(
UUID.fromString("你的服务UUID"), // 添加你的UUID
UUID.fromString("0000fff0-0000-1000-8000-00805f9b34fb"),
// ... 其他UUID
)
```
## 📱 常见ECG设备UUID
### 1. 标准ECG设备
```
服务UUID: 0000fff0-0000-1000-8000-00805f9b34fb
特征UUID: 0000fff1-0000-1000-8000-00805f9b34fb
```
### 2. 心电监护仪
```
服务UUID: 0000ffe0-0000-1000-8000-00805f9b34fb
特征UUID: 0000ffe1-0000-1000-8000-00805f9b34fb
```
### 3. 便携式ECG
```
服务UUID: 0000ffe5-0000-1000-8000-00805f9b34fb
特征UUID: 0000ffe6-0000-1000-8000-00805f9b34fb
```
## 🔍 调试技巧
### 1. 使用Logcat过滤
```
adb logcat | grep BluetoothManager
```
### 2. 查看设备信息
```
adb logcat | grep "发现服务\|发现特征"
```
### 3. 检查连接状态
```
adb logcat | grep "设备已连接\|服务发现成功"
```
## ⚠️ 注意事项
### 1. UUID格式
- UUID必须是标准格式`xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`
- 16位UUID会自动扩展为128位
### 2. 权限要求
- 需要`BLUETOOTH_CONNECT`权限
- Android 12+需要额外权限
### 3. 设备兼容性
- 不同厂商的ECG设备可能使用不同UUID
- 需要根据实际设备调整
## 🎉 预期效果
### 成功连接:
- ✅ 显示"数据通道已建立,开始接收数据"
- ✅ 开始接收ECG数据
- ✅ 数据传递给数据处理模块
### 需要调试:
- ❌ 显示"未找到匹配的服务或特征"
- 显示所有可用服务的UUID
- 🔧 需要手动添加正确的UUID
现在请重新连接你的设备查看调试信息告诉我你的设备实际使用的UUID🎯

View File

@ -1,198 +0,0 @@
# 按钮和对话框修复测试指南
## 🎯 修复内容
### 1. 按钮位置修复 ✅
- **问题**:按钮在屏幕最上面,显示不全
- **解决**:添加顶部边距,让按钮下移
- **实现**
```xml
android:layout_marginTop="50dp" <!-- 添加50dp顶部边距 -->
```
### 2. 对话框性能优化 ✅
- **问题**:设备选择对话框很卡
- **解决**:简化对话框实现,限制设备数量
- **实现**
- 限制最多显示8个设备
- 使用简单的文本列表而不是ListView
- 提供快速选择按钮
## 📱 现在的UI布局
### 启动时的界面
```
┌─────────────────────────────────────────┐
│ │
│ [状态栏区域] │
│ │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ [连接蓝牙] [启动程序] │
│ [停止程序] [陷波滤波] │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 状态信息区域 │
│ [应用已就绪,可以开始使用] │
│ [权限状态信息] │
│ [点击"连接蓝牙"按钮开始蓝牙连接...] │
└─────────────────────────────────────────┘
```
### 设备选择对话框
```
┌─────────────────────────────────────────┐
│ 选择蓝牙设备 │
├─────────────────────────────────────────┤
│ 找到 5 个设备: │
│ │
│ • iPhone (00:11:22:33:44:55) │
│ • AirPods (AA:BB:CC:DD:EE:FF) │
│ • 小米手环 (11:22:33:44:55:66) │
│ • Samsung Galaxy (22:33:44:55:66:77) │
│ • Huawei Watch (33:44:55:66:77:88) │
│ │
│ 请选择要连接的设备: │
├─────────────────────────────────────────┤
│ [选择第一个设备] [选择第二个设备] [取消] │
└─────────────────────────────────────────┘
```
## 🚀 测试步骤
### 第一步:验证按钮位置
1. **启动应用**
2. **确认按钮位置**
- ✅ 按钮不在屏幕最上面
- ✅ 按钮完全可见
- ✅ 按钮与状态栏有足够距离
### 第二步:测试蓝牙扫描
1. **点击"连接蓝牙"按钮**
2. **观察扫描过程**
```
蓝牙状态: 正在扫描蓝牙设备...
发现设备: iPhone (00:11:22:33:44:55)
发现设备: AirPods (AA:BB:CC:DD:EE:FF)
发现设备: 小米手环 (11:22:33:44:55:66)
```
### 第三步:验证对话框性能
1. **扫描完成后**
- ✅ 对话框应该快速弹出
- ✅ 对话框不卡顿
- ✅ 设备列表清晰显示
### 第四步:测试设备选择
1. **选择设备**
- 点击"选择第一个设备"按钮
- 或点击"选择第二个设备"按钮
- 观察连接过程
### 第五步:验证连接状态
1. **连接成功后**
- ✅ 按钮文字变为"断开蓝牙"
- ✅ 按钮颜色变为红色
- ✅ 状态信息更新
## 🔧 关键代码修改
### 1. 按钮位置修复
```xml
<!-- 控制按钮区域 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp"
android:background="#E8E8E8"
android:layout_marginTop="50dp"> <!-- 添加顶部边距 -->
```
### 2. 对话框性能优化
```kotlin
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(requireContext())
builder.setTitle("选择蓝牙设备")
if (devices.isEmpty()) {
builder.setMessage("未找到蓝牙设备")
builder.setPositiveButton("重新扫描") { _, _ -> dismiss() }
builder.setNegativeButton("取消") { _, _ -> dismiss() }
} else {
// 限制设备数量,避免列表过长
val limitedDevices = devices.take(8) // 最多显示8个设备
// 创建设备列表字符串
val deviceListText = limitedDevices.joinToString("\n") { device ->
"• ${device.name ?: "未知设备"} (${device.address})"
}
val message = if (devices.size > 8) {
"找到 ${devices.size} 个设备显示前8个\n\n$deviceListText\n\n请选择要连接的设备"
} else {
"找到 ${devices.size} 个设备:\n\n$deviceListText\n\n请选择要连接的设备"
}
builder.setMessage(message)
// 创建选择按钮
builder.setPositiveButton("选择第一个设备") { _, _ ->
if (limitedDevices.isNotEmpty()) {
onDeviceSelectedListener?.invoke(limitedDevices[0])
}
}
// 如果有多个设备,添加更多选择按钮
if (limitedDevices.size > 1) {
builder.setNeutralButton("选择第二个设备") { _, _ ->
onDeviceSelectedListener?.invoke(limitedDevices[1])
}
}
builder.setNegativeButton("取消") { _, _ -> dismiss() }
}
return builder.create()
}
```
## ⚠️ 注意事项
### 1. 按钮位置
- 添加了50dp的顶部边距
- 确保按钮在状态栏下方
- 适应不同屏幕尺寸
### 2. 对话框性能
- 限制最多显示8个设备
- 使用简单的文本列表
- 提供快速选择按钮
### 3. 设备选择
- 第一个设备:点击"选择第一个设备"
- 第二个设备:点击"选择第二个设备"
- 更多设备:可以修改代码添加更多按钮
## 🎉 预期效果
### 启动时:
- ✅ 按钮位置合理,完全可见
- ✅ 界面布局清晰
- ✅ 状态信息正常显示
### 扫描时:
- ✅ 扫描过程流畅
- ✅ 设备发现信息及时更新
### 对话框:
- ✅ 快速弹出,无卡顿
- ✅ 设备列表清晰显示
- ✅ 选择按钮响应及时
### 连接后:
- ✅ 连接状态正确显示
- ✅ 按钮状态正确更新
- ✅ 可以开始数据处理
现在可以测试修复后的按钮位置和对话框性能了!🎉

View File

@ -1,218 +0,0 @@
# 设备选择功能改进测试指南
## 🎯 改进内容
### 1. 显示所有扫描到的设备 ✅
- **问题**之前只显示前8个设备
- **解决**使用RecyclerView显示所有扫描到的设备
- **实现**:移除设备数量限制,显示完整设备列表
### 2. 点击任意设备连接 ✅
- **问题**:之前只能选择前两个设备
- **解决**:每个设备都可以点击连接
- **实现**RecyclerView的每个item都可以点击
### 3. 扫描完成后立即弹出对话框 ✅
- **问题**:扫描完成后可能延迟弹出对话框
- **解决**:扫描停止时立即触发回调
- **实现**:在`stopBleScan()`中立即调用`onScanComplete`
## 📱 现在的UI布局
### 设备选择对话框
```
┌─────────────────────────────────────────┐
│ 选择蓝牙设备 (找到 5 个设备) │
├─────────────────────────────────────────┤
│ ┌─────────────────────────────────────┐ │
│ │ iPhone │ │
│ │ 00:11:22:33:44:55 │ │
│ └─────────────────────────────────────┘ │
│ ┌─────────────────────────────────────┐ │
│ │ AirPods │ │
│ │ AA:BB:CC:DD:EE:FF │ │
│ └─────────────────────────────────────┘ │
│ ┌─────────────────────────────────────┐ │
│ │ 小米手环 │ │
│ │ 11:22:33:44:55:66 │ │
│ └─────────────────────────────────────┘ │
│ ┌─────────────────────────────────────┐ │
│ │ Samsung Galaxy │ │
│ │ 22:33:44:55:66:77 │ │
│ └─────────────────────────────────────┘ │
│ ┌─────────────────────────────────────┐ │
│ │ Huawei Watch │ │
│ │ 33:44:55:66:77:88 │ │
│ └─────────────────────────────────────┘ │
├─────────────────────────────────────────┤
│ [取消] │
└─────────────────────────────────────────┘
```
## 🚀 测试步骤
### 第一步:启动蓝牙扫描
1. **点击"连接蓝牙"按钮**
2. **观察扫描过程**
```
蓝牙状态: 正在扫描蓝牙设备...
发现设备: iPhone (00:11:22:33:44:55)
发现设备: AirPods (AA:BB:CC:DD:EE:FF)
发现设备: 小米手环 (11:22:33:44:55:66)
```
### 第二步:验证扫描完成提示
1. **扫描完成后**
- ✅ 显示"扫描完成,找到 X 个设备"
- ✅ 立即弹出设备选择对话框
- ✅ 对话框标题显示设备数量
### 第三步:验证设备列表
1. **检查设备列表**
- ✅ 显示所有扫描到的设备
- ✅ 每个设备显示名称和地址
- ✅ 列表可以滚动(如果设备很多)
### 第四步:测试设备选择
1. **点击任意设备**
- ✅ 点击第一个设备
- ✅ 点击中间的设备
- ✅ 点击最后一个设备
- ✅ 每个设备都能正常连接
### 第五步:验证连接过程
1. **连接状态变化**
- ✅ 按钮文字变为"连接中..."
- ✅ 按钮颜色变为橙色
- ✅ 状态信息更新
## 🔧 关键代码修改
### 1. 设备适配器
```kotlin
private inner class DeviceAdapter : RecyclerView.Adapter<DeviceAdapter.DeviceViewHolder>() {
override fun onBindViewHolder(holder: DeviceViewHolder, position: Int) {
val device = devices[position]
val deviceName = device.name ?: "未知设备"
val deviceAddress = device.address
holder.textView.text = "$deviceName\n$deviceAddress"
holder.itemView.setOnClickListener {
onDeviceSelectedListener?.invoke(device)
dismiss()
}
}
override fun getItemCount(): Int = devices.size
}
```
### 2. 对话框实现
```kotlin
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(requireContext())
builder.setTitle("选择蓝牙设备 (找到 ${devices.size} 个设备)")
if (devices.isEmpty()) {
builder.setMessage("未找到蓝牙设备")
builder.setPositiveButton("重新扫描") { _, _ -> dismiss() }
builder.setNegativeButton("取消") { _, _ -> dismiss() }
} else {
// 创建RecyclerView显示所有设备
val recyclerView = RecyclerView(requireContext()).apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = DeviceAdapter()
// 设置固定高度,避免对话框过大
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
600 // 固定高度600dp
)
}
builder.setView(recyclerView)
builder.setNegativeButton("取消") { _, _ -> dismiss() }
}
return builder.create()
}
```
### 3. 扫描完成回调
```kotlin
override fun onScanComplete(devices: List<BluetoothDevice>) {
runOnUiThread {
updateStatus("扫描完成,找到 ${devices.size} 个设备")
if (devices.isNotEmpty()) {
// 立即显示设备选择对话框
val dialog = BluetoothDeviceDialog.newInstance(devices)
dialog.setOnDeviceSelectedListener { device ->
// 连接选中的设备
bluetoothManager.connectToDevice(device)
binding.bluetoothButton.text = "连接中..."
binding.bluetoothButton.setBackgroundColor(Color.parseColor("#FF9800"))
updateStatus("正在连接设备: ${device.name ?: device.address}")
}
dialog.show(supportFragmentManager, "BluetoothDeviceDialog")
} else {
updateStatus("未找到蓝牙设备,请重试")
binding.bluetoothButton.text = "连接蓝牙"
binding.bluetoothButton.setBackgroundColor(Color.parseColor("#4CAF50"))
}
}
}
```
### 4. BLE扫描停止
```kotlin
private fun stopBleScan() {
try {
bluetoothLeScanner?.stopScan(bleScanCallback)
Log.d(TAG, "BLE扫描已停止")
// 扫描完成后立即触发回调
callback?.onStatusChanged("扫描完成,找到 ${discoveredDevices.size} 个设备")
callback?.onScanComplete(discoveredDevices.toList())
} catch (e: Exception) {
Log.e(TAG, "停止BLE扫描失败: ${e.message}")
}
}
```
## ⚠️ 注意事项
### 1. 设备显示
- 显示所有扫描到的设备,无数量限制
- 每个设备显示名称和地址
- 支持滚动查看(如果设备很多)
### 2. 连接选择
- 点击任意设备都可以连接
- 连接后对话框自动关闭
- 支持取消操作
### 3. 性能优化
- 使用RecyclerView提高性能
- 固定对话框高度避免过大
- 扫描完成后立即弹出
## 🎉 预期效果
### 扫描过程:
- ✅ 显示扫描进度
- ✅ 实时显示发现的设备
- ✅ 扫描完成后立即提示
### 设备选择:
- ✅ 显示所有扫描到的设备
- ✅ 每个设备都可以点击
- ✅ 对话框响应流畅
### 连接过程:
- ✅ 点击设备后立即开始连接
- ✅ 连接状态正确显示
- ✅ 支持连接任意设备
现在你可以扫描所有设备并点击任意设备进行连接了!🎉

601
DynamicChartView_backup.kt Normal file
View File

@ -0,0 +1,601 @@
package com.example.cmake_project_test
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.ScaleGestureDetector
import kotlin.math.max
import kotlin.math.min
/**
* 可调节的通用生理信号图表视图
* 支持多种设备类型和通道数量的动态显示具有缩放平移时间窗口调节等功能
*/
class DynamicChartView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paint = Paint().apply {
color = Color.BLUE
strokeWidth = 2f
style = Paint.Style.STROKE
isAntiAlias = false // 关闭抗锯齿,提高绘制性能
}
private val gridPaint = Paint().apply {
color = Color.LTGRAY
strokeWidth = 1f
style = Paint.Style.STROKE
alpha = 80
}
private val textPaint = Paint().apply {
color = Color.BLACK
textSize = 20f
isAntiAlias = true
}
private val controlPaint = Paint().apply {
color = Color.RED
strokeWidth = 3f
style = Paint.Style.STROKE
isAntiAlias = true
}
private val path = Path()
private var dataPoints = mutableListOf<Float>()
private var maxDataPoints = 1000 // 默认数据点数量
private var minValue = Float.MAX_VALUE
private var maxValue = Float.MIN_VALUE
private var isDataAvailable = false
// 图表配置
private var chartTitle = "通道"
private var channelIndex = 0
private var deviceType = "未知设备"
private var sampleRate = 250f // 默认采样率
private var timeWindow = 4f // 默认时间窗口(秒)
// 性能优化:真正的逐点绘制
private val dataBuffer = mutableListOf<Float>()
// 多通道数据支持用于PPG等设备
private val multiChannelDataPoints = mutableListOf<MutableList<Float>>()
private val multiChannelDataBuffers = mutableListOf<MutableList<Float>>()
private var isMultiChannelMode = false
private val channelColors = listOf(Color.RED, Color.BLUE) // 通道颜色
private var lastUpdateTime = 0L
private val updateInterval = 2L // 2ms更新间隔实现500Hz显示
private var lastDrawTime = 0L
private val drawInterval = 1L // 1ms绘制间隔实现1000Hz绘制频率
// 交互控制参数
private var scaleFactor = 1.0f
private var translateX = 0f
private var translateY = 0f
private var viewportStart = 0f
private var viewportEnd = 1.0f
private var isDragging = false
private var lastTouchX = 0f
private var lastTouchY = 0f
// 手势检测器
private val scaleGestureDetector = ScaleGestureDetector(context, ScaleListener())
// 控制按钮区域
private val controlButtonRect = RectF()
private val fullscreenButtonRect = RectF()
// 全屏模式相关
private var isFullscreenMode = false
private var originalLayoutParams: android.view.ViewGroup.LayoutParams? = null
/**
* 设置图表配置
*/
fun setChartConfig(
title: String,
channelIdx: Int,
device: String,
maxPoints: Int,
sampleRateHz: Float,
timeWindowSec: Float
) {
chartTitle = title
channelIndex = channelIdx
deviceType = device
maxDataPoints = maxPoints
sampleRate = sampleRateHz
timeWindow = timeWindowSec
resetViewport()
invalidate()
}
/**
* 设置多通道模式用于PPG等设备
*/
fun setMultiChannelMode(channelCount: Int) {
isMultiChannelMode = true
multiChannelDataPoints.clear()
multiChannelDataBuffers.clear()
// 初始化每个通道的数据结构
for (i in 0 until channelCount) {
multiChannelDataPoints.add(mutableListOf())
multiChannelDataBuffers.add(mutableListOf())
}
}
fun updateData(newData: List<Float>) {
if (newData.isEmpty()) return
isDataAvailable = true
// 批量添加到缓冲区
dataBuffer.addAll(newData)
val currentTime = System.currentTimeMillis()
// 批量处理 - 每次处理更多点,提高数据累积速度
if (currentTime - lastUpdateTime >= updateInterval) {
if (dataBuffer.isNotEmpty()) {
// 每次处理多个点,提高效率
val batchSize = minOf(10, dataBuffer.size) // 每次最多处理10个点
val batch = dataBuffer.take(batchSize)
dataBuffer.removeAll(batch)
dataPoints.addAll(batch)
// 限制数据点数量
if (dataPoints.size > maxDataPoints) {
dataPoints = dataPoints.takeLast(maxDataPoints).toMutableList()
}
// 更新数据范围 - 使用批量数据
updateDataRange(batch)
lastUpdateTime = currentTime
}
}
// 超高频绘制
if (currentTime - lastDrawTime >= drawInterval) {
invalidate()
lastDrawTime = currentTime
}
}
/**
* 更新多通道数据用于PPG等设备
*/
fun updateMultiChannelData(channelData: List<List<Float>>) {
if (channelData.isEmpty() || !isMultiChannelMode) {
Log.d("DynamicChartView", "跳过多通道更新: channelData.isEmpty=${channelData.isEmpty()}, isMultiChannelMode=$isMultiChannelMode")
return
}
Log.d("DynamicChartView", "多通道数据更新: 通道数=${channelData.size}, 每个通道数据点=${channelData.map { it.size }}")
isDataAvailable = true
val currentTime = System.currentTimeMillis()
// 更新每个通道的数据
for (channelIndex in channelData.indices) {
if (channelIndex >= multiChannelDataBuffers.size) {
Log.w("DynamicChartView", "通道索引超出范围: $channelIndex >= ${multiChannelDataBuffers.size}")
continue
}
val channelBuffer = multiChannelDataBuffers[channelIndex]
channelBuffer.addAll(channelData[channelIndex])
Log.d("DynamicChartView", "通道$channelIndex 添加了${channelData[channelIndex].size}个数据点")
}
// 批量处理数据
if (currentTime - lastUpdateTime >= updateInterval) {
var hasDataToProcess = false
for (channelIndex in multiChannelDataBuffers.indices) {
val buffer = multiChannelDataBuffers[channelIndex]
if (buffer.isNotEmpty()) {
// 每次处理多个点
val batchSize = minOf(10, buffer.size)
val batch = buffer.take(batchSize)
buffer.removeAll(batch)
multiChannelDataPoints[channelIndex].addAll(batch)
// 限制数据点数量
if (multiChannelDataPoints[channelIndex].size > maxDataPoints) {
multiChannelDataPoints[channelIndex] = multiChannelDataPoints[channelIndex].takeLast(maxDataPoints).toMutableList()
}
hasDataToProcess = true
}
}
if (hasDataToProcess) {
// 更新数据范围 - 计算所有通道的数据范围
updateMultiChannelDataRange()
lastUpdateTime = currentTime
Log.d("DynamicChartView", "多通道数据处理完成: 通道数据点=${multiChannelDataPoints.map { it.size }}, 范围=${minValue}-${maxValue}")
}
}
// 超高频绘制
if (currentTime - lastDrawTime >= drawInterval) {
invalidate()
lastDrawTime = currentTime
}
}
private fun updateDataRange(batch: List<Float>) {
// 使用全部数据点计算范围,提供更准确的范围
if (dataPoints.isEmpty()) return
// 计算全部数据点的范围
minValue = dataPoints.minOrNull() ?: 0f
maxValue = dataPoints.maxOrNull() ?: 0f
// 确保有足够的显示范围
val range = maxValue - minValue
if (range < 0.1f) {
val center = (maxValue + minValue) / 2
minValue = center - 0.05f
maxValue = center + 0.05f
} else {
val margin = range * 0.1f
minValue -= margin
maxValue += margin
}
}
/**
* 更新多通道数据范围
*/
private fun updateMultiChannelDataRange() {
if (multiChannelDataPoints.isEmpty()) {
Log.w("DynamicChartView", "多通道数据点为空,无法计算范围")
return
}
// 计算所有通道的最小值和最大值
var globalMin = Float.MAX_VALUE
var globalMax = Float.MIN_VALUE
var hasValidData = false
for (channelIndex in multiChannelDataPoints.indices) {
val channelData = multiChannelDataPoints[channelIndex]
if (channelData.isNotEmpty()) {
val channelMin = channelData.minOrNull() ?: 0f
val channelMax = channelData.maxOrNull() ?: 0f
globalMin = minOf(globalMin, channelMin)
globalMax = maxOf(globalMax, channelMax)
hasValidData = true
Log.d("DynamicChartView", "通道$channelIndex: 范围=${channelMin}-${channelMax}, 数据点=${channelData.size}")
}
}
if (!hasValidData) {
Log.w("DynamicChartView", "没有有效的多通道数据")
return
}
minValue = globalMin
maxValue = globalMax
Log.d("DynamicChartView", "多通道全局范围: ${minValue}-${maxValue}")
// 确保有足够的显示范围
val range = maxValue - minValue
if (range < 0.1f) {
val center = (maxValue + minValue) / 2
minValue = center - 0.05f
maxValue = center + 0.05f
} else {
val margin = range * 0.1f
minValue -= margin
maxValue += margin
}
Log.d("DynamicChartView", "多通道显示范围: ${minValue}-${maxValue}")
}
fun clearData() {
dataPoints.clear()
dataBuffer.clear()
// 清除多通道数据
for (channelData in multiChannelDataPoints) {
channelData.clear()
}
for (channelBuffer in multiChannelDataBuffers) {
channelBuffer.clear()
}
isDataAvailable = false
resetViewport()
invalidate()
}
/**
* 重置视口到默认状态
*/
fun resetViewport() {
scaleFactor = 1.0f
translateX = 0f
translateY = 0f
viewportStart = 0f
viewportEnd = 1.0f
invalidate()
}
override fun onTouchEvent(event: MotionEvent): Boolean {
scaleGestureDetector.onTouchEvent(event)
when (event.action) {
MotionEvent.ACTION_DOWN -> {
val x = event.x
val y = event.y
// 开始拖拽
isDragging = true
lastTouchX = x
lastTouchY = y
return true
}
MotionEvent.ACTION_MOVE -> {
if (isDragging) {
val deltaX = event.x - lastTouchX
val deltaY = event.y - lastTouchY
translateX += deltaX
translateY += deltaY
// 限制平移范围
val maxTranslate = width * 0.5f
translateX = translateX.coerceIn(-maxTranslate, maxTranslate)
translateY = translateY.coerceIn(-maxTranslate, maxTranslate)
lastTouchX = event.x
lastTouchY = event.y
invalidate()
return true
}
}
MotionEvent.ACTION_UP -> {
isDragging = false
return true
}
}
return super.onTouchEvent(event)
}
private inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
scaleFactor *= detector.scaleFactor
scaleFactor = scaleFactor.coerceIn(0.2f, 5.0f)
invalidate()
return true
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val width = width.toFloat()
val height = height.toFloat()
// 应用变换
canvas.save()
canvas.translate(translateX, translateY)
canvas.scale(scaleFactor, scaleFactor, width / 2, height / 2)
// 绘制网格
drawGrid(canvas, width, height)
// 绘制标题和通道信息
drawTitle(canvas, width, height)
// 绘制数据统计
drawStats(canvas, width, height)
// 绘制全屏按钮
drawFullscreenButton(canvas, width, height)
// 如果没有数据,显示默认内容
if (!isDataAvailable) {
drawDefaultContent(canvas, width, height)
} else {
// 绘制曲线 - 检查单通道或多通道数据
val hasDataToDraw = if (isMultiChannelMode) {
multiChannelDataPoints.any { it.size > 1 }
} else {
dataPoints.size > 1
}
if (hasDataToDraw) {
drawCurve(canvas, width, height)
}
}
canvas.restore()
}
private fun drawGrid(canvas: Canvas, width: Float, height: Float) {
val padding = 60f
val drawWidth = width - 2 * padding
val drawHeight = height - 2 * padding
// 绘制水平网格线
val gridLines = 5
for (i in 0..gridLines) {
val y = padding + (drawHeight * i / gridLines)
canvas.drawLine(padding, y, width - padding, y, gridPaint)
}
// 绘制垂直网格线
for (i in 0..gridLines) {
val x = padding + (drawWidth * i / gridLines)
canvas.drawLine(x, padding, x, height - padding, gridPaint)
}
}
private fun drawTitle(canvas: Canvas, width: Float, height: Float) {
val title = "$chartTitle $channelIndex"
canvas.drawText(title, 10f, 25f, textPaint)
val deviceInfo = "$deviceType (${sampleRate.toInt()}Hz)"
canvas.drawText(deviceInfo, 10f, 50f, textPaint)
// 显示当前缩放和时间窗口信息
val scaleInfo = "缩放: ${String.format("%.1f", scaleFactor)}x"
canvas.drawText(scaleInfo, width - 150f, 25f, textPaint)
val timeInfo = "时间窗口: ${timeWindow.toInt()}"
canvas.drawText(timeInfo, width - 150f, 50f, textPaint)
}
private fun drawStats(canvas: Canvas, width: Float, height: Float) {
val totalDataPoints = if (isMultiChannelMode) {
multiChannelDataPoints.sumOf { it.size }
} else {
dataPoints.size
}
val statsText = if (isMultiChannelMode) {
"数据点: ${totalDataPoints} (${multiChannelDataPoints.size}通道)"
} else {
"数据点: ${dataPoints.size}"
}
canvas.drawText(statsText, 10f, height - 35f, textPaint)
if (totalDataPoints > 0) {
val rangeText = "范围: ${String.format("%.2f", minValue)} - ${String.format("%.2f", maxValue)}"
canvas.drawText(rangeText, 10f, height - 15f, textPaint)
}
}
private fun drawDefaultContent(canvas: Canvas, width: Float, height: Float) {
val centerX = width / 2
val centerY = height / 2
val hintPaint = Paint().apply {
color = Color.GRAY
textSize = 16f
textAlign = Paint.Align.CENTER
isAntiAlias = true
}
canvas.drawText("等待数据...", centerX, centerY - 20f, hintPaint)
canvas.drawText("请先连接蓝牙设备", centerX, centerY + 20f, hintPaint)
canvas.drawText("然后点击'启动程序'显示数据", centerX, centerY + 60f, hintPaint)
}
private fun drawCurve(canvas: Canvas, width: Float, height: Float) {
val padding = 60f
val drawWidth = width - 2 * padding
val drawHeight = height - 2 * padding
if (isMultiChannelMode && multiChannelDataPoints.isNotEmpty()) {
Log.d("DynamicChartView", "多通道绘制: 通道数=${multiChannelDataPoints.size}, 数据点=${multiChannelDataPoints.map { it.size }}")
// 多通道绘制模式
for (channelIndex in multiChannelDataPoints.indices) {
val channelData = multiChannelDataPoints[channelIndex]
if (channelData.isEmpty()) {
Log.d("DynamicChartView", "通道$channelIndex 数据为空,跳过绘制")
continue
}
path.reset()
// 设置通道颜色
val channelPaint = Paint(paint).apply {
color = channelColors.getOrElse(channelIndex) { Color.GRAY }
strokeWidth = 2f
}
val xStep = if (channelData.size > 1) drawWidth / (channelData.size - 1) else drawWidth
for (i in channelData.indices) {
val x = padding + i * xStep
val normalizedValue = (channelData[i] - minValue) / (maxValue - minValue)
val y = padding + (0.1f + normalizedValue * 0.8f) * drawHeight
if (i == 0) {
path.moveTo(x, y)
} else {
path.lineTo(x, y)
}
}
canvas.drawPath(path, channelPaint)
}
} else {
// 单通道绘制模式
if (dataPoints.isEmpty()) return
path.reset()
val xStep = if (dataPoints.size > 1) drawWidth / (dataPoints.size - 1) else drawWidth
for (i in dataPoints.indices) {
val x = padding + i * xStep
val normalizedValue = (dataPoints[i] - minValue) / (maxValue - minValue)
val y = padding + (0.1f + normalizedValue * 0.8f) * drawHeight
if (i == 0) {
path.moveTo(x, y)
} else {
path.lineTo(x, y)
}
}
canvas.drawPath(path, paint)
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val desiredHeight = 200 // 每个图表的高度
val height = resolveSize(desiredHeight, heightMeasureSpec)
setMeasuredDimension(widthMeasureSpec, height)
}
/**
* 获取当前图表数据
*/
fun getCurrentData(): List<Float> {
return dataPoints.toList()
}
/**
* 获取数据统计信息
*/
fun getDataStats(): Map<String, Any> {
val totalDataPoints = if (isMultiChannelMode) {
multiChannelDataPoints.sumOf { it.size }
} else {
dataPoints.size
}
return mapOf(
"dataPoints" to totalDataPoints,
"multiChannelMode" to isMultiChannelMode,
"channelCount" to if (isMultiChannelMode) multiChannelDataPoints.size else 1,
"minValue" to minValue,
"maxValue" to maxValue,
"range" to (maxValue - minValue),
"isDataAvailable" to isDataAvailable,
"scaleFactor" to scaleFactor,
"timeWindow" to timeWindow
)
}
}

View File

@ -1,141 +0,0 @@
# JNI函数名不匹配问题修复说明
## 问题描述
在代码重构过程中,将原生方法从`MainActivity`移动到`DataManager`类后,出现了以下错误:
```
Cannot resolve corresponding JNI function Java_com_example_cmake_1project_1test_DataManager_createStreamParser
```
## 问题原因
### JNI函数命名规则
JNI函数名遵循以下格式
```
Java_{包名}_{类名}_{方法名}
```
其中:
- 包名中的点(.)用下划线(_)替换
- 类名中的下划线用`_1`替换
### 具体变化
**重构前的JNI函数名**在MainActivity中
```
Java_com_example_cmake_1project_1test_MainActivity_createStreamParser
Java_com_example_cmake_1project_1test_MainActivity_destroyStreamParser
Java_com_example_cmake_1project_1test_MainActivity_streamParserAppend
Java_com_example_cmake_1project_1test_MainActivity_streamParserDrainPackets
```
**重构后的JNI函数名**在DataManager中
```
Java_com_example_cmake_1project_1test_DataManager_createStreamParser
Java_com_example_cmake_1project_1test_DataManager_destroyStreamParser
Java_com_example_cmake_1project_1test_DataManager_streamParserAppend
Java_com_example_cmake_1project_1test_DataManager_streamParserDrainPackets
```
## 解决方案
### 方案1回调接口模式已实现
通过创建回调接口,让`DataManager`通过`MainActivity`来调用原生方法保持原有的JNI函数名。
#### 实现步骤
1. **创建回调接口**
```kotlin
interface NativeMethodCallback {
fun createStreamParser(): Long
fun destroyStreamParser(handle: Long)
fun streamParserAppend(handle: Long, chunk: ByteArray)
fun streamParserDrainPackets(handle: Long): List<SensorData>?
}
```
2. **修改DataManager构造函数**
```kotlin
class DataManager(private val nativeCallback: NativeMethodCallback)
```
3. **MainActivity实现接口**
```kotlin
class MainActivity : AppCompatActivity(), DataManager.NativeMethodCallback
```
4. **保持原生方法在MainActivity中**
```kotlin
// 原生方法声明 - 保持原来的JNI函数名
external fun createStreamParser(): Long
external fun destroyStreamParser(handle: Long)
external fun streamParserAppend(handle: Long, chunk: ByteArray)
external fun streamParserDrainPackets(handle: Long): List<SensorData>?
```
#### 优势
- 保持原有的JNI函数名无需修改C++代码
- 维持了代码重构的架构优势
- 清晰的职责分离
#### 劣势
- 增加了接口依赖
- 稍微增加了代码复杂度
### 方案2修改C++代码中的JNI函数名
如果您有权限修改C++代码可以将C++中的JNI函数名改为新的名称。
#### 需要修改的C++函数名
```cpp
// 原来的函数名
JNIEXPORT jlong JNICALL Java_com_example_cmake_1project_1test_MainActivity_createStreamParser
// 改为
JNIEXPORT jlong JNICALL Java_com_example_cmake_1project_1test_DataManager_createStreamParser
```
#### 优势
- 完全符合重构后的架构
- 无需额外的接口层
#### 劣势
- 需要修改C++代码
- 可能影响其他依赖项目
## 推荐方案
**推荐使用方案1回调接口模式**,原因如下:
1. **无需修改C++代码**保持现有C++代码的稳定性
2. **维持重构优势**:仍然保持了代码的职责分离
3. **向后兼容**如果将来需要可以轻松切换到方案2
4. **风险较低**:不会引入新的编译或链接问题
## 修复后的架构
```
MainActivity (实现NativeMethodCallback)
DataManager (通过回调调用原生方法)
原生C++库 (保持原有JNI函数名)
```
## 注意事项
1. **JNI函数名一致性**确保Kotlin中的`external`方法名与C++中的JNI函数名完全匹配
2. **库加载**:确保在`MainActivity`的`companion object`中正确加载原生库
3. **接口实现**MainActivity必须实现`NativeMethodCallback`接口的所有方法
4. **依赖注入**DataManager的构造函数现在需要传入回调接口实例
## 验证修复
修复完成后,您应该能够:
1. 成功编译项目
2. 不再看到JNI函数名不匹配的错误
3. 保持原有的功能正常运行
4. 享受重构后的代码结构优势

View File

@ -1,88 +0,0 @@
# MAC地址格式说明
## 问题解决
您遇到的错误 "60-E9-AA-30-0B-0A is not a valid Bluetooth address" 是因为MAC地址格式问题。
## 正确的MAC地址格式
### 支持的格式:
1. **冒号分隔符**(推荐):`60:E9:AA:30:8B:0A`
2. **连字符分隔符**`60-E9-AA-30-8B-0A`
### 您的电脑MAC地址
- **正确格式**`60:E9:AA:30:8B:0A`
- **注意**:您之前输入的是 `60-E9-AA-30-0B-0A`,其中 `0B` 应该是 `8B`
## 常见MAC地址格式错误
### 1. 分隔符错误
- ❌ 错误:`60.E9.AA.30.8B.0A`(点号分隔符)
- ❌ 错误:`60 E9 AA 30 8B 0A`(空格分隔符)
- ✅ 正确:`60:E9:AA:30:8B:0A`(冒号分隔符)
### 2. 字符错误
- ❌ 错误:`60-E9-AA-30-0B-0A`0B 应该是 8B
- ✅ 正确:`60:E9:AA:30:8B:0A`
### 3. 长度错误
- ❌ 错误:`60:E9:AA:30:8B`(缺少两位)
- ❌ 错误:`60:E9:AA:30:8B:0A:FF`(多出两位)
- ✅ 正确:`60:E9:AA:30:8B:0A`6组每组2位
## 如何获取正确的MAC地址
### Windows系统
1. 打开命令提示符cmd
2. 输入:`ipconfig /all`
3. 查找"物理地址"或"Physical Address"
4. 格式类似:`60-E9-AA-30-8B-0A`
### 转换为冒号格式:
- 将连字符 `-` 替换为冒号 `:`
- `60-E9-AA-30-8B-0A``60:E9:AA:30:8B:0A`
## 测试步骤
### 1. 确认MAC地址
1. 在Windows命令提示符中输入`ipconfig /all`
2. 找到您的蓝牙适配器的物理地址
3. 确认地址格式正确
### 2. 使用应用连接
1. 启动应用
2. **长按"连接蓝牙"按钮**
3. 输入正确的MAC地址`60:E9:AA:30:8B:0A`
4. 点击"连接"
### 3. 验证连接
1. 观察连接状态
2. 查看日志信息
3. 如果仍有问题,尝试扫描连接
## 常见问题
### Q: 为什么需要冒号分隔符?
A: Android蓝牙API更推荐使用冒号分隔符兼容性更好。
### Q: 我的MAC地址是连字符格式怎么办
A: 应用现在支持两种格式,但推荐使用冒号格式。
### Q: 连接仍然失败怎么办?
A:
1. 确认MAC地址正确
2. 检查蓝牙权限
3. 确保设备在范围内
4. 尝试扫描连接方式
## 调试技巧
### 查看日志:
在Android Studio的Logcat中查看
- `BluetoothManager` 标签的详细错误信息
- 确认MAC地址格式验证结果
### 测试连接:
1. 先使用扫描功能找到设备
2. 记录正确的MAC地址
3. 再使用直接连接功能

View File

@ -1,136 +0,0 @@
# Nordic UART Service (NUS) 适配指南
## 🎯 设备协议识别
从你的设备日志可以看出你的ECG设备使用的是 **Nordic UART Service (NUS)** 协议:
```
发现服务: 6e400001-b5a3-f393-e0a9-68716563686f // NUS Service
发现特征: 6e400002-b5a3-f393-e0a9-68716563686f // NUS TX Characteristic
```
### Nordic UART Service 协议说明
**NUS (Nordic UART Service)** 是一个标准的BLE服务常用于
- ECG设备数据传输
- 传感器数据流
- 串口通信模拟
- 医疗设备通信
### UUID 含义
- **Service UUID**: `6e400001-b5a3-f393-e0a9-68716563686f`
- 这是Nordic UART Service的标准UUID
- 用于标识NUS服务
- **TX Characteristic UUID**: `6e400002-b5a3-f393-e0a9-68716563686f`
- 这是NUS的TX发送特征
- 用于接收设备发送的数据
- 支持通知Notify属性
## 🔧 适配完成
### 1. UUID配置 ✅
```kotlin
// 服务UUID - 已添加到列表首位
UUID.fromString("6e400001-b5a3-f393-e0a9-68716563686f")
// 特征UUID - 已添加到列表首位
UUID.fromString("6e400002-b5a3-f393-e0a9-68716563686f")
```
### 2. 优先级设置 ✅
- **NUS UUID已放在列表首位**
- **优先匹配你的设备协议**
- **确保快速连接**
### 3. 数据接收 ✅
- **自动启用特征通知**
- **开始接收ECG数据流**
- **数据传递给处理模块**
## 📱 测试步骤
### 第一步:重新连接设备
1. **断开当前连接**
2. **重新点击"连接蓝牙"**
3. **选择你的设备**
### 第二步:验证连接
1. **观察连接过程**
```
蓝牙状态: 设备已连接
蓝牙状态: 服务发现成功
蓝牙状态: 发现服务: 6e400001-b5a3-f393-e0a9-68716563686f
蓝牙状态: 发现特征: 6e400002-b5a3-f393-e0a9-68716563686f
```
2. **检查UUID匹配**
```
找到匹配的服务: 6e400001-b5a3-f393-e0a9-68716563686f
找到匹配的特征: 6e400002-b5a3-f393-e0a9-68716563686f
已启用特征通知
数据通道已建立,开始接收数据
```
### 第三步:验证数据接收
1. **检查状态信息**
- ✅ "数据通道已建立,开始接收数据"
- ✅ "接收到数据: X 字节"
2. **检查ECG图表**
- ✅ 图表变为可见
- ✅ 开始显示实时波形
## 🔍 Nordic UART Service 特点
### 1. 数据格式
- **原始字节流**:设备直接发送原始数据
- **无协议头**:通常没有复杂的协议封装
- **连续传输**:数据流连续传输
### 2. 常见用途
- **ECG数据**:心电信号实时传输
- **传感器数据**:各种生理参数
- **控制命令**:设备控制指令
### 3. 优势
- **标准化**:广泛使用的标准协议
- **兼容性好**:支持多种设备
- **实时性**:低延迟数据传输
## 🎉 预期效果
### 连接成功:
- ✅ 立即找到匹配的NUS服务
- ✅ 快速建立数据通道
- ✅ 开始接收ECG数据
### 数据处理:
- ✅ 数据传递给DataManager
- ✅ 进行通道映射和滤波
- ✅ 显示实时ECG波形
### 应用功能:
- ✅ 实时心率计算
- ✅ 信号质量评估
- ✅ 12导联ECG分析
## ⚠️ 注意事项
### 1. 数据格式
- NUS通常发送原始数据
- 可能需要特定的数据解析逻辑
- 注意字节序和数据对齐
### 2. 设备状态
- 确保设备处于数据发送状态
- 某些设备需要特定命令激活数据流
- 检查设备电池和连接状态
### 3. 数据质量
- 监控数据接收频率
- 检查数据包大小是否合理
- 观察信号质量指标
现在请重新连接你的设备应该能够成功建立数据通道并开始接收ECG数据了🎯

View File

@ -1,183 +0,0 @@
# 蓝牙权限申请指南
## 🔐 权限说明
在Android 12及以上版本使用蓝牙功能需要动态申请以下权限
### 必需权限
1. **BLUETOOTH_SCAN** - 扫描蓝牙设备
2. **BLUETOOTH_CONNECT** - 连接蓝牙设备
3. **ACCESS_FINE_LOCATION** - 精确位置(蓝牙扫描需要)
4. **ACCESS_COARSE_LOCATION** - 粗略位置(蓝牙扫描需要)
## 📱 权限申请流程
### 首次启动应用
1. 应用启动时会显示当前权限状态
2. 点击"连接蓝牙"按钮
3. 系统弹出权限申请对话框
4. 选择"允许"授予所有权限
### 权限状态显示
```
权限状态:
BLUETOOTH_SCAN: ✓
BLUETOOTH_CONNECT: ✓
ACCESS_FINE_LOCATION: ✓
ACCESS_COARSE_LOCATION: ✓
```
## ⚠️ 常见问题
### 1. 权限被拒绝
**症状**:按钮显示"权限被拒绝",无法使用蓝牙功能
**解决**
- 进入系统设置 → 应用管理 → 本应用 → 权限
- 手动开启蓝牙和位置权限
- 重新启动应用
### 2. 部分权限缺失
**症状**:提示"缺少权限: BLUETOOTH_SCAN, ACCESS_FINE_LOCATION"
**解决**
- 确保所有4个权限都已授予
- 位置权限对于蓝牙扫描是必需的
### 3. 权限申请对话框不出现
**症状**:点击按钮无反应
**解决**
- 检查应用是否被系统限制
- 重启应用或设备
- 检查系统版本是否支持
## 🔧 技术实现
### 权限检查代码
```kotlin
private fun checkAndRequestPermissions(): Boolean {
val missingPermissions = mutableListOf<String>()
for (permission in REQUIRED_PERMISSIONS) {
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
missingPermissions.add(permission)
}
}
if (missingPermissions.isNotEmpty()) {
updateStatus("需要蓝牙权限: ${missingPermissions.joinToString(", ")}")
ActivityCompat.requestPermissions(
this,
missingPermissions.toTypedArray(),
PERMISSION_REQUEST_CODE
)
return false
}
return true
}
```
### 权限结果处理
```kotlin
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
PERMISSION_REQUEST_CODE -> {
val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
if (allGranted) {
updateStatus("蓝牙权限已授予,开始扫描...")
startBluetoothScan()
} else {
updateStatus("蓝牙权限被拒绝,无法使用蓝牙功能")
binding.bluetoothButton.text = "权限被拒绝"
binding.bluetoothButton.setBackgroundColor(Color.parseColor("#9E9E9E"))
}
}
}
}
```
## 📋 权限清单
### AndroidManifest.xml
```xml
<!-- 蓝牙权限 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- 蓝牙功能声明 -->
<uses-feature android:name="android.hardware.bluetooth" android:required="true" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
```
### MainActivity.kt
```kotlin
companion object {
private const val PERMISSION_REQUEST_CODE = 1001
private val REQUIRED_PERMISSIONS = arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
// Used to load the 'cmake_project_test' library on application startup.
init {
System.loadLibrary("cmake_project_test")
}
}
```
## 🎯 最佳实践
### 1. 权限申请时机
- 在用户主动点击蓝牙功能时申请
- 避免应用启动时强制申请
- 提供清晰的权限用途说明
### 2. 用户体验
- 显示当前权限状态
- 提供权限被拒绝时的解决方案
- 友好的错误提示
### 3. 兼容性
- 支持Android 6.0+的动态权限
- 兼容Android 12+的新蓝牙权限
- 处理权限被拒绝的情况
## 🔍 调试技巧
### 检查权限状态
```kotlin
private fun checkPermissionStatus(): String {
val status = mutableListOf<String>()
for (permission in REQUIRED_PERMISSIONS) {
val granted = ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
status.add("${permission.split(".").last()}: ${if (granted) "✓" else "✗"}")
}
return status.joinToString("\n")
}
```
### 日志输出
- 权限申请过程会记录详细日志
- 可通过Logcat查看权限状态变化
- 便于调试权限相关问题
## 📝 注意事项
1. **位置权限必需**:蓝牙扫描需要位置权限,这是系统要求
2. **权限持久性**:权限一旦授予,应用重启后仍然有效
3. **系统限制**:某些系统可能限制权限申请
4. **版本兼容**不同Android版本的权限要求可能不同

View File

@ -1,132 +0,0 @@
# 蓝牙原始数据显示测试指南
## 🎯 功能说明
现在应用已经支持**立即显示蓝牙接收到的原始数据**,无需等待信号处理完成。
### 数据流处理顺序
1. **蓝牙数据接收** → 立即解析
2. **原始数据显示** → 立即画图(新增)
3. **信号处理** → 后台处理
4. **处理后数据显示** → 可选显示
## 📱 测试步骤
### 第一步:连接蓝牙设备
1. **点击"连接蓝牙"按钮**
2. **等待扫描完成**
3. **选择你的ECG设备**
4. **等待连接成功**
### 第二步:验证原始数据显示
1. **观察状态信息**
```
蓝牙状态: 设备已连接
蓝牙状态: 服务发现成功
蓝牙状态: 数据通道已建立,开始接收数据
```
2. **检查数据接收日志**
```
接收到蓝牙数据: X 字节
解析出 X 个数据包
立即发送原始数据到图表,处理 X 个数据包
发送原始数据到通道 X数据长度: X
显示原始数据到图表,通道: X数据长度: X
```
3. **观察图表显示**
- ✅ ECG图表立即变为可见
- ✅ 原始波形立即开始显示
- ✅ 双视图同步更新10秒节奏视图 + 2.5秒波形视图)
### 第三步:验证实时性
1. **数据接收延迟**:应该几乎无延迟
2. **图表更新频率**:与蓝牙数据接收频率一致
3. **波形连续性**:原始数据应该连续显示
## 🔍 关键代码位置
### 1. 原始数据发送
```kotlin
// DataManager.kt - 第97行
sendRawDataToCharts(packets)
// DataManager.kt - 第177行
private fun sendRawDataToCharts(packets: List<SensorData>) {
// 直接处理原始数据包,不进行通道映射
for (packet in packets) {
val channelData = packet.getChannelData()
if (channelData != null) {
for ((channelIndex, channel) in channelData.withIndex()) {
// 立即发送原始数据到图表
realTimeCallback?.onRawDataAvailable(channelIndex, channel)
}
}
}
}
```
### 2. 原始数据显示
```kotlin
// MainActivity.kt - 第572行
override fun onRawDataAvailable(channelIndex: Int, rawData: List<Float>) {
// 立即显示原始数据到图表
binding.ecgChartContainer.visibility = View.VISIBLE
binding.ecgRhythmView.updateData(rawData)
binding.ecgWaveformView.updateData(rawData)
}
```
## 🎉 预期效果
### 连接成功时:
- ✅ 蓝牙设备连接成功
- ✅ 数据通道建立
- ✅ 开始接收ECG数据
### 原始数据显示时:
- ✅ ECG图表立即可见
- ✅ 原始波形立即显示
- ✅ 双视图同步更新
- ✅ 无延迟实时显示
### 数据特点:
- **原始性**:未经任何滤波处理
- **实时性**:接收即显示
- **连续性**:数据流连续
- **完整性**:包含所有通道数据
## ⚠️ 注意事项
### 1. 数据格式
- **支持的数据类型**:所有已定义的数据类型
- **通道数量**:根据设备类型自动识别
- **采样率**:根据数据类型自动设置
### 2. 显示特点
- **原始信号**:包含噪声和基线漂移
- **实时更新**:与蓝牙数据接收同步
- **多通道**:支持多通道同时显示
### 3. 性能考虑
- **UI线程**:原始数据显示在主线程
- **内存使用**:图表自动管理数据缓冲
- **更新频率**:与蓝牙数据接收频率一致
## 🔧 调试信息
### 关键日志
```
DataManager: 立即发送原始数据到图表,处理 X 个数据包
DataManager: 发送原始数据到通道 X数据长度: X
MainActivity: 显示原始数据到图表,通道: X数据长度: X
```
### 状态检查
- 蓝牙连接状态
- 数据接收频率
- 图表更新状态
- 通道数据完整性
现在你的应用可以立即显示蓝牙接收到的原始数据了!连接设备后就能看到实时波形。🚀

View File

@ -1,136 +0,0 @@
# 代码重构说明
## 重构目标
将原本堆在一个MainActivity.kt文件中的所有逻辑分解为多个职责明确的Kotlin文件提高代码的可读性和可维护性。
## 重构后的文件结构
### 1. **MainActivity.kt** - 主界面控制器
**职责**: 主界面逻辑和生命周期管理
- 初始化各个管理器
- 处理按钮点击事件
- 管理Activity生命周期
- 协调各个管理器之间的交互
**主要方法**:
- `onCreate()`: 初始化UI和管理器
- `onBleNotify()`: 蓝牙数据通知处理
- `resetData()`: 重置数据
- `reloadData()`: 重新加载数据
### 2. **Constants.kt** - 常量定义
**职责**: 集中管理应用中的所有常量
- UI更新间隔
- 数据分块大小
- 缓冲区管理阈值
- 显示限制参数
**主要常量**:
- `UPDATE_INTERVAL`: UI更新间隔500ms
- `CHUNK_SIZE`: 数据分块大小64字节
- `BUFFER_CLEANUP_THRESHOLD`: 缓冲区清理阈值50
- `BUFFER_KEEP_COUNT`: 缓冲区保留数量30
### 3. **DeviceTypeHelper.kt** - 设备类型工具
**职责**: 设备类型相关的工具方法
- 设备类型名称映射
- 通道数据详情构建
**主要方法**:
- `getDeviceName()`: 根据数据类型获取设备名称
- `buildChannelDetails()`: 构建通道数据详情字符串
### 4. **DataManager.kt** - 数据管理器
**职责**: 数据解析、缓冲管理和原生方法调用
- 管理流式解析器
- 处理数据块
- 管理数据缓冲区
- 调用原生C++方法
**主要方法**:
- `ensureParser()`: 确保解析器已创建
- `onBleNotify()`: 处理蓝牙通知数据块
- `processFileData()`: 处理文件数据
- `cleanupBuffer()`: 智能清理缓冲区
- `resetData()`: 重置所有数据
**原生方法**:
- `createStreamParser()`: 创建流式解析器
- `streamParserAppend()`: 向解析器追加数据
- `streamParserDrainPackets()`: 从解析器拉取数据包
- `destroyStreamParser()`: 销毁解析器
### 5. **UiManager.kt** - UI管理器
**职责**: UI更新、统计信息构建和显示逻辑
- 管理UI更新调度
- 构建统计信息
- 构建显示内容
- 触发UI更新
**主要方法**:
- `scheduleUiUpdate()`: 计划UI更新避免频繁刷新
- `buildStatisticsString()`: 构建统计信息字符串
- `buildDisplayContent()`: 构建完整的显示内容
- `updateDisplay()`: 更新UI显示内容
### 6. **FileHelper.kt** - 文件帮助类
**职责**: 文件读取操作
- 从assets文件夹读取文件
**主要方法**:
- `readAssetFile()`: 读取assets文件到字节数组
## 重构优势
### 1. **职责分离**
- 每个类都有明确的单一职责
- 降低了类之间的耦合度
- 提高了代码的可测试性
### 2. **代码复用**
- 工具方法可以在多个地方复用
- 减少了重复代码
- 提高了开发效率
### 3. **维护性提升**
- 修改某个功能只需要修改对应的类
- 代码结构更清晰,易于理解
- 降低了修改的风险
### 4. **可扩展性**
- 新增功能可以创建新的类
- 现有类可以独立扩展
- 支持团队协作开发
## 数据流向
```
MainActivity → DataManager → 原生解析器
UiManager ← DataManager ← 数据缓冲区
UI更新
```
## 使用说明
1. **MainActivity**: 作为入口点,协调各个管理器
2. **DataManager**: 处理所有数据相关的操作
3. **UiManager**: 负责所有UI相关的更新
4. **DeviceTypeHelper**: 提供设备类型相关的工具方法
5. **Constants**: 集中管理所有常量
6. **FileHelper**: 处理文件读取操作
## 注意事项
1. **原生方法**: 所有原生C++方法现在都在DataManager中声明
2. **依赖关系**: 各个类之间有明确的依赖关系,避免循环依赖
3. **线程安全**: UI更新始终在主线程进行数据操作在后台线程进行
4. **错误处理**: 每个类都有适当的错误处理和日志记录
## 后续优化建议
1. **依赖注入**: 可以考虑使用Dagger或Koin进行依赖注入
2. **响应式编程**: 可以考虑使用RxJava或Flow进行数据流处理
3. **单元测试**: 重构后的代码更容易进行单元测试
4. **文档完善**: 可以添加更详细的API文档和示例代码

View File

@ -1,173 +0,0 @@
# 蓝牙指令使用说明
## 功能概述
本应用支持通过蓝牙连接设备后发送指令让设备开始发送单导联ECG数据然后实时显示ECG曲线图。专门优化用于处理单导联蓝牙实时数据不再读取12导联文件数据。
## 支持的协议
### 轻迅蓝牙通信协议 V1.0.1
本应用完全支持轻迅蓝牙通信协议,包括:
- 数据服务 UUID: `6e400001-b5a3-f393-e0a9-68716563686f`
- Write Characteristic UUID: `6e400002-b5a3-f393-e0a9-68716563686f`
- Notify Characteristic UUID: `6e400003-b5a3-f393-e0a9-68716563686f`
## 使用步骤
### 1. 连接蓝牙设备
- 点击"连接蓝牙"按钮
- 选择您的ECG设备
- 等待连接成功
### 2. 发送指令
连接成功后,点击"发送指令"按钮,会弹出指令选择对话框:
#### 指令类型选择:
- **轻迅协议指令**:使用轻迅蓝牙通信协议的标准指令
- **通用指令**:使用通用的字符串或十六进制指令
- **自定义指令**:输入自定义指令
### 3. 轻迅协议指令
选择"轻迅协议指令"后,会显示以下选项:
#### 采集控制:
- **开启采集**:发送功能码 `0x0001` 开启数据采集
- **停止采集**:发送功能码 `0x0001` 停止数据采集
#### 设备信息:
- **查询设备信息**:发送功能码 `0x0000` 查询设备参数
- **查询电量**:发送功能码 `0x0002` 查询设备电量
#### 滤波控制:
- **工频滤波开关**:发送功能码 `0x000A` 控制工频滤波
### 4. 通用指令
选择"通用指令"后,会显示以下选项:
- **开始发送数据**:让设备开始发送数据流
- **停止发送数据**:让设备停止发送数据
- **开始ECG测量**开始ECG信号测量
- **停止ECG测量**停止ECG信号测量
#### 常用指令格式:
- **字符串指令**`START_DATA`, `STOP`, `BEGIN`, `END`
- **十六进制指令**`0x01`, `0x02`, `0x53 0x54 0x41 0x52 0x54`
### 5. 查看实时图表
发送指令成功后应用会自动启动数据处理并立即开始绘制单导联ECG图表
- **自动启动**:发送指令后自动启动数据处理,无需手动点击"启动程序"
- **实时显示**:单导联数据接收后立即更新图表,无需等待
- **单导联处理**专门处理单导联ECG数据主要显示主导联信号
- **ECG节律视图**显示10秒的连续单导联信号
- **ECG波形视图**显示2.5秒的放大单导联信号
### 6. 其他功能
- **陷波滤波**:开启/关闭50Hz陷波滤波器
- **清空数据**:清空所有图表数据
- **停止程序**:停止数据处理
## 轻迅协议指令详解
### 采集控制指令
#### 开启采集 (功能码: 0x0001)
```
数据包格式: [功能码(2字节)] [数据长度(2字节)] [采集开关(1字节)] [时间戳(8字节)] [CRC16(2字节)]
示例: 01 00 09 00 01 [时间戳8字节] [CRC16 2字节]
```
**时间戳选项:**
- **立即开启**时间戳设为0
- **延迟开启**:时间戳设为未来时间(毫秒)
- **指定时间戳**:自定义时间戳
#### 停止采集 (功能码: 0x0001)
```
数据包格式: [功能码(2字节)] [数据长度(2字节)] [采集开关(1字节)] [时间戳(8字节)] [CRC16(2字节)]
示例: 01 00 09 00 00 [时间戳8字节] [CRC16 2字节]
```
### 设备信息指令
#### 查询设备信息 (功能码: 0x0000)
```
数据包格式: [功能码(2字节)] [数据长度(2字节)] [CRC16(2字节)]
示例: 00 00 00 00 [CRC16 2字节]
```
#### 查询电量 (功能码: 0x0002)
```
数据包格式: [功能码(2字节)] [数据长度(2字节)] [CRC16(2字节)]
示例: 02 00 00 00 [CRC16 2字节]
```
### 滤波控制指令
#### 工频滤波开关 (功能码: 0x000A)
```
数据包格式: [功能码(2字节)] [数据长度(2字节)] [开关状态(1字节)] [CRC16(2字节)]
开启示例: 0A 00 01 00 01 [CRC16 2字节]
关闭示例: 0A 00 01 00 00 [CRC16 2字节]
```
## 通用指令示例
### 开始数据流
```
START_DATA
START
0x01
```
### 停止数据流
```
STOP_DATA
STOP
0x02
```
### 开始ECG测量
```
START_ECG
ECG_START
0x03
```
### 停止ECG测量
```
STOP_ECG
ECG_STOP
0x04
```
## 注意事项
1. 确保蓝牙设备已正确连接
2. 根据您的设备协议选择合适的指令类型
3. 轻迅协议设备会自动计算CRC16校验确保数据完整性
4. 如果指令发送失败,请检查设备是否支持该指令
5. 可以尝试不同的指令格式(字符串或十六进制)
## 调试信息
应用会在状态栏显示详细的调试信息,包括:
- 蓝牙连接状态
- 指令发送结果(包括轻迅协议数据包内容)
- 数据接收情况
- 图表更新状态
## 轻迅协议特性
- **自动CRC16校验**所有指令包都包含CRC16-CCITT-FALSE校验
- **时间戳同步**:支持指定时间戳进行同步采集
- **工频滤波控制**:可动态开启/关闭工频滤波算法
- **设备信息查询**:支持查询设备状态和电量信息
- **自动数据处理**:发送指令后自动启动数据处理,无需手动操作
- **实时图表更新**:单导联数据接收后立即更新图表,提供最佳用户体验
- **单导联优化**专门优化处理单导联ECG数据不再处理12导联文件数据
## 测试功能
- **点击"启动程序"按钮**:立即生成测试数据并显示图表,用于验证图表显示功能
- **长按"启动程序"按钮**生成更复杂的模拟ECG数据
- **蓝牙数据测试**:当接收到蓝牙数据但原生解析器无法解析时,会自动生成测试数据
### 测试数据说明
应用会生成模拟的单导联ECG波形包含
- P波、QRS复合波、T波等典型ECG特征
- 适当的噪声模拟真实信号
- 500个数据点用于充分显示波形

View File

@ -1,271 +0,0 @@
# 信号处理JNI封装使用说明
## 概述
本项目已将C++信号处理功能封装成JNI接口可以在Android应用中直接调用。主要功能包括
- 各种数字滤波器(带通、低通、高通、陷波)
- 信号质量评估
- ECG信号质量指数计算
- 信号特征提取
- 信号预处理(归一化、去直流等)
- 实时信号处理
## 文件结构
```
app/src/main/
├── cpp/
│ ├── include/cpp/
│ │ └── signal_processor.h # C++头文件
│ ├── src/
│ │ └── signal_processor.cpp # C++实现文件(需要修复乱码)
│ ├── jni/
│ │ └── jni_bridge.cpp # 统一的JNI桥接文件包含所有JNI函数
│ └── CMakeLists.txt # 构建配置
└── java/
└── com/example/cmake_project_test/
├── SignalProcessorJNI.kt # Java JNI接口类
└── SignalProcessorExample.kt # 使用示例类
```
## 使用方法
### 1. 基本使用
```kotlin
// 创建信号处理器实例
val signalProcessor = SignalProcessorJNI()
if (signalProcessor.createProcessor()) {
// 使用各种信号处理功能
val filteredSignal = signalProcessor.bandpassFilter(
inputSignal,
sampleRate = 1000.0,
lowFreq = 40.0,
highFreq = 60.0
)
// 清理资源
signalProcessor.destroyProcessor()
}
```
### 2. 主要功能
#### 数字滤波
```kotlin
// 带通滤波
val bandpassResult = signalProcessor.bandpassFilter(
signal, sampleRate, lowFreq, highFreq
)
// 低通滤波
val lowpassResult = signalProcessor.lowpassFilter(
signal, sampleRate, cutoffFreq
)
// 高通滤波
val highpassResult = signalProcessor.highpassFilter(
signal, sampleRate, cutoffFreq
)
// 陷波滤波(去除工频干扰)
val notchResult = signalProcessor.notchFilter(
signal, sampleRate, notchFreq, qFactor
)
```
#### 信号质量评估
```kotlin
// 计算信号质量指数
val quality = signalProcessor.calculateSignalQuality(signal)
// 计算ECG信号质量指数
val ecgSQI = signalProcessor.calculateECGSQI(ecgSignal, sampleRate)
// 计算两个信号的相关性
val correlation = signalProcessor.calculateCorrelation(signal1, signal2)
```
#### 信号预处理
```kotlin
// 归一化信号幅度
signalProcessor.normalizeAmplitude(signal)
// 提取信号特征
val features = signalProcessor.extractFeatures(signal, sampleRate)
// 重置滤波器状态
signalProcessor.resetFilters()
```
#### 实时处理
```kotlin
// 实时处理数据块
val processedChunk = signalProcessor.processRealtimeChunk(
chunk, sampleRate
)
```
### 3. 完整示例
参考 `SignalProcessorExample.kt` 文件,其中包含了所有功能的演示代码:
```kotlin
val example = SignalProcessorExample()
// 运行所有演示
example.runAllDemonstrations()
// 清理资源
example.cleanup()
```
## 数据格式
### 输入数据
- 所有信号数据使用 `FloatArray` 格式
- 采样率使用 `Double` 类型
- 频率参数使用 `Double` 类型
### 输出数据
- 滤波结果返回 `FloatArray?`可能为null表示处理失败
- 质量指数返回 `Float` 类型0.0-1.0
- 相关性返回 `Float` 类型(-1.0到1.0
### 内部转换
- Java端使用 `ByteBuffer` 进行 `FloatArray``ByteArray` 的转换
- C++端使用 `std::vector<float>` 处理数据
- 字节序使用小端序Little Endian
## 注意事项
### 1. 资源管理
```kotlin
// 必须在使用前创建处理器
if (!signalProcessor.createProcessor()) {
// 处理创建失败的情况
return
}
// 使用完毕后必须销毁处理器
signalProcessor.destroyProcessor()
```
### 2. 错误处理
```kotlin
val result = signalProcessor.bandpassFilter(signal, sampleRate, lowFreq, highFreq)
if (result != null) {
// 处理成功
processResult(result)
} else {
// 处理失败
Log.e("SignalProcessor", "滤波处理失败")
}
```
### 3. 性能考虑
- 滤波器初始化有一定开销,建议复用处理器实例
- 大数据量处理时考虑分块处理
- 实时处理时注意内存分配
### 4. 线程安全
- JNI函数不是线程安全的
- 多线程使用时需要适当的同步机制
- 建议在主线程或专用工作线程中使用
## 构建配置
### CMakeLists.txt
确保在 `CMakeLists.txt` 中包含了信号处理库:
```cmake
# Signal processor static library
add_library(signal_processor STATIC
src/signal_processor.cpp)
target_include_directories(signal_processor PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include)
# 链接到主库
target_link_libraries(${CMAKE_PROJECT_NAME}
core_math
data_parser
signal_processor
android
log)
```
**注意**信号处理的JNI函数现在已整合到 `jni_bridge.cpp` 中,不再需要单独的 `signal_processor_jni.cpp` 文件。
### 库加载
在Java代码中确保正确加载原生库
```kotlin
companion object {
init {
System.loadLibrary("cmake_project_test")
}
}
```
## 故障排除
### 1. 编译错误
- 检查 `CMakeLists.txt` 配置
- 确保所有源文件路径正确
- 检查头文件包含路径
### 2. 运行时错误
- 检查Logcat中的错误信息
- 确保处理器已正确创建
- 检查输入数据格式和大小
### 3. 性能问题
- 使用Android Profiler分析性能瓶颈
- 考虑减少数据转换开销
- 优化滤波器参数
## 扩展功能
### 1. 添加新的滤波器
1. 在 `signal_processor.h` 中声明新方法
2. 在 `signal_processor.cpp` 中实现
3. 在 `signal_processor_jni.cpp` 中添加JNI封装
4. 在 `SignalProcessorJNI.kt` 中添加Java接口
### 2. 自定义信号处理
可以基于现有的JNI接口构建更高级的信号处理功能
```kotlin
class AdvancedSignalProcessor(private val jni: SignalProcessorJNI) {
fun adaptiveFilter(signal: FloatArray, sampleRate: Double): FloatArray? {
// 实现自适应滤波算法
// 使用JNI接口调用底层功能
}
}
```
## 总结
通过JNI封装您现在可以在Android应用中直接使用C++的高性能信号处理功能。这种架构既保持了C++的性能优势又提供了Java/Kotlin的易用性。
建议在使用前先运行示例代码,熟悉各种功能的使用方法,然后根据具体需求进行定制化开发。

View File

@ -1,151 +0,0 @@
# 智能UUID检测指南
## 🎯 问题分析
从你提供的日志可以看出:
```
发现特征: 00002a29-0000-1000-8000-00805f9b34fb
发现特征: 00002a24-0000-1000-8000-00805f9b34fb
发现特征: 00002a25-0000-1000-8000-00805f9b34fb
```
这些都是**标准蓝牙GATT服务**的特征UUID
- `00002a29` = Manufacturer Name String
- `00002a24` = Model Number String
- `00002a25` = Serial Number String
- `00002a27` = Hardware Revision String
- `00002a26` = Firmware Revision String
- `00002a28` = Software Revision String
## 🔧 解决方案
### 1. 扩展UUID列表 ✅
- **新增**14个常见ECG设备UUID变体
- **覆盖**:更多厂商的设备协议
- **范围**`0000ff00` 到 `0000ffb0`
### 2. 智能UUID检测 ✅
- **实现**:自动检测非标准蓝牙服务
- **逻辑**:跳过标准服务(`00002a`、`000018`
- **目标**:查找自定义服务和特征
### 3. 通知特征检测 ✅
- **实现**:检查特征是否支持通知
- **条件**`PROPERTY_NOTIFY` 或 `PROPERTY_INDICATE`
- **目的**:确保能接收数据
## 📱 检测流程
### 第一步预设UUID匹配
```
尝试匹配预设的ECG设备UUID
- 0000fff0-0000-1000-8000-00805f9b34fb
- 0000ffe0-0000-1000-8000-00805f9b34fb
- 0000ffe5-0000-1000-8000-00805f9b34fb
- ... (共15个变体)
```
### 第二步:智能检测
```
如果预设UUID未找到匹配
1. 扫描所有服务
2. 跳过标准蓝牙服务00002a, 000018
3. 查找自定义服务
4. 检查特征是否支持通知
5. 自动选择第一个匹配的特征
```
### 第三步:建立连接
```
找到匹配的服务和特征后:
1. 启用特征通知
2. 开始接收数据
3. 显示"数据通道已建立"
```
## 🚀 测试步骤
### 第一步:重新连接设备
1. **断开当前连接**
2. **重新点击"连接蓝牙"**
3. **选择你的设备**
### 第二步:观察调试信息
1. **查看服务发现过程**
```
蓝牙状态: 服务发现成功
设备提供的服务数量: X
发现服务: [UUID列表]
```
2. **查看智能检测过程**
```
预设UUID未找到匹配尝试智能检测
发现自定义服务: [UUID]
发现自定义特征: [UUID]
找到支持通知的特征: [UUID]
```
3. **查看连接结果**
```
数据通道已建立,开始接收数据
```
### 第三步:验证数据接收
1. **检查状态信息**
- ✅ "数据通道已建立,开始接收数据"
- ✅ "接收到数据: X 字节"
2. **检查ECG图表**
- ✅ 图表变为可见
- ✅ 开始显示实时数据
## 🔍 常见问题
### 1. 设备使用标准服务
**现象**:只显示`00002a`开头的UUID
**解决**:智能检测会自动跳过这些服务
### 2. 设备需要特定触发
**现象**:连接成功但没有数据
**解决**:可能需要发送特定命令激活数据流
### 3. 设备使用非标准UUID
**现象**UUID不在预设列表中
**解决**智能检测会自动发现自定义UUID
## 📋 设备信息收集
请提供以下信息:
### 1. 设备基本信息
- **设备名称**你的ECG设备显示的名称
- **设备型号**:如果知道的话
- **厂商信息**:设备制造商
### 2. 服务UUID信息
```
请提供完整的服务发现日志:
- 所有发现的服务UUID
- 所有发现的特征UUID
- 智能检测的结果
```
### 3. 连接状态
- **是否成功建立数据通道?**
- **是否开始接收数据?**
- **数据大小是多少字节?**
## 🎉 预期效果
### 成功情况:
- ✅ 显示"数据通道已建立,开始接收数据"
- ✅ 开始接收ECG数据
- ✅ ECG图表显示实时波形
### 需要进一步调试:
- ❌ 仍然显示"未找到匹配的服务或特征"
- 需要查看完整的服务发现日志
- 🔧 可能需要手动配置特定UUID
现在请重新连接你的设备,观察新的调试信息!🎯

View File

@ -1,295 +0,0 @@
# 完整功能测试指南
## 🎯 测试目标
验证应用的完整功能,包括:
- ✅ 蓝牙权限申请
- ✅ 蓝牙设备扫描和连接
- ✅ 实时数据接收和处理
- ✅ ECG曲线图显示
- ✅ 信号处理和指标计算
## 📱 测试环境准备
### 1. 设备要求
- **Android设备**Android 6.0+推荐Android 12+
- **蓝牙功能**支持BLE或传统蓝牙
- **位置权限**:需要位置权限用于蓝牙扫描
### 2. 心电设备
- **真实设备**支持BLE或传统蓝牙的心电设备
- **模拟设备**:可以使用蓝牙调试工具模拟数据
### 3. 开发环境
- **Android Studio**:最新版本
- **ADB工具**:用于日志查看和调试
## 🚀 测试步骤
### 第一步:应用安装和启动
1. **编译应用**
```bash
./gradlew assembleDebug
```
2. **安装到设备**
```bash
adb install app/build/outputs/apk/debug/app-debug.apk
```
3. **启动应用**
- 在设备上找到并启动应用
- 观察启动界面和权限状态显示
### 第二步:权限申请测试
1. **查看权限状态**
```
权限状态:
BLUETOOTH_SCAN: ✗
BLUETOOTH_CONNECT: ✗
ACCESS_FINE_LOCATION: ✗
ACCESS_COARSE_LOCATION: ✗
```
2. **点击"连接蓝牙"按钮**
- 系统应弹出权限申请对话框
- 选择"允许"授予所有权限
3. **验证权限状态**
```
权限状态:
BLUETOOTH_SCAN: ✓
BLUETOOTH_CONNECT: ✓
ACCESS_FINE_LOCATION: ✓
ACCESS_COARSE_LOCATION: ✓
```
### 第三步:蓝牙扫描测试
1. **开始扫描**
- 点击"连接蓝牙"按钮
- 按钮文字应变为"扫描中..."
- 按钮颜色应变为橙色
2. **观察扫描过程**
```
蓝牙状态: 正在扫描蓝牙设备...
发现设备: ECG_Device_001 (00:11:22:33:44:55)
扫描完成,找到 1 个设备
```
3. **设备选择对话框**
- 扫描完成后应弹出设备选择对话框
- 显示发现的设备列表
### 第四步:设备连接测试
1. **选择设备**
- 在设备列表中选择目标设备
- 点击设备名称进行连接
2. **连接过程**
```
正在连接设备: ECG_Device_001
设备已连接
服务发现成功
数据通道已建立
```
3. **连接状态验证**
- 按钮文字应变为"断开蓝牙"
- 按钮颜色应变为红色
### 第五步:数据接收测试
1. **启动数据采集**
- 点击"启动程序"按钮
- 观察ECG曲线图开始显示数据
2. **数据接收验证**
```
接收到蓝牙数据: 128 字节
接收到蓝牙数据: 256 字节
接收到蓝牙数据: 512 字节
```
3. **实时显示验证**
- ECG节律视图10秒应显示实时曲线
- ECG波形视图2.5秒)应显示详细波形
- 曲线应平滑滚动,无卡顿
### 第六步:信号处理测试
1. **滤波功能**
- 点击"陷波滤波"按钮切换滤波状态
- 观察曲线图的变化
2. **指标计算**
- 查看状态信息中的心率等指标
- 验证指标计算的准确性
3. **性能测试**
- 观察应用的响应速度
- 检查内存使用情况
## 🔍 调试技巧
### 1. 日志查看
```bash
# 查看蓝牙相关日志
adb logcat | grep BluetoothManager
# 查看权限相关日志
adb logcat | grep MainActivity
# 查看数据处理日志
adb logcat | grep DataManager
# 查看信号处理日志
adb logcat | grep StreamingSignalProcessor
```
### 2. 常见日志模式
```
BluetoothManager: 蓝牙初始化成功
BluetoothManager: BLE扫描已开始
BluetoothManager: 发现BLE设备: ECG_Device_001
BluetoothManager: 设备已连接
BluetoothManager: 接收到数据: 128 字节
```
### 3. 错误诊断
```bash
# 查看错误日志
adb logcat | grep -i error
# 查看警告日志
adb logcat | grep -i warning
```
## ⚠️ 常见问题排查
### 1. 权限问题
**症状**:点击按钮无反应
**排查**
```bash
# 检查权限状态
adb shell dumpsys package com.example.cmake_project_test | grep permission
```
**解决**
- 进入系统设置手动授予权限
- 重启应用
### 2. 蓝牙扫描问题
**症状**:扫描无结果
**排查**
```bash
# 检查蓝牙状态
adb shell dumpsys bluetooth
```
**解决**
- 确认设备蓝牙已开启
- 确认设备处于可发现状态
- 检查设备距离
### 3. 连接问题
**症状**:连接失败
**排查**
```bash
# 查看连接日志
adb logcat | grep -i "connection\|connect"
```
**解决**
- 确认设备未被其他应用占用
- 检查设备电池电量
- 重启设备和应用
### 4. 数据显示问题
**症状**:曲线图无数据
**排查**
```bash
# 查看数据处理日志
adb logcat | grep -i "data\|process"
```
**解决**
- 检查数据格式是否正确
- 确认数据处理管道正常工作
- 检查UI更新逻辑
## 📊 性能指标
### 1. 响应时间
- **权限申请**< 1秒
- **蓝牙扫描**< 10秒
- **设备连接**< 5秒
- **数据接收**:实时
### 2. 内存使用
- **应用启动**< 100MB
- **数据采集**< 200MB
- **长时间运行**< 300MB
### 3. 电池消耗
- **待机状态**< 5%/小时
- **数据采集**< 15%/小时
## 🎉 测试完成标准
### 功能完整性
- ✅ 权限申请正常工作
- ✅ 蓝牙扫描发现设备
- ✅ 设备连接成功
- ✅ 数据接收正常
- ✅ 实时显示流畅
- ✅ 信号处理有效
- ✅ 指标计算准确
### 用户体验
- ✅ 界面响应及时
- ✅ 状态提示清晰
- ✅ 错误处理友好
- ✅ 操作流程顺畅
### 稳定性
- ✅ 长时间运行稳定
- ✅ 内存使用合理
- ✅ 电池消耗正常
- ✅ 无崩溃或卡死
## 📝 测试报告模板
```
测试日期YYYY-MM-DD
测试设备:设备型号 + Android版本
测试人员:姓名
功能测试结果:
□ 权限申请:通过/失败
□ 蓝牙扫描:通过/失败
□ 设备连接:通过/失败
□ 数据接收:通过/失败
□ 实时显示:通过/失败
□ 信号处理:通过/失败
□ 指标计算:通过/失败
性能测试结果:
□ 响应时间:正常/异常
□ 内存使用:正常/异常
□ 电池消耗:正常/异常
问题记录:
1. 问题描述
2. 复现步骤
3. 解决方案
总体评价:
□ 优秀 □ 良好 □ 一般 □ 需要改进
```
现在你的应用已经具备了完整的功能,可以进行全面的测试了!🎉

View File

@ -1,182 +0,0 @@
# UI修复测试指南
## 🎯 修复内容
### 1. 图表隐藏功能 ✅
- **问题**ECG图表在启动时就显示占用屏幕空间
- **解决**:图表默认隐藏,只有在接收到数据时才显示
- **实现**
```xml
android:visibility="gone" <!-- 默认隐藏 -->
```
### 2. 设备选择对话框 ✅
- **问题**:扫描完成后没有显示设备选择对话框
- **解决**:确保`onScanComplete`回调正确显示对话框
- **实现**
```kotlin
override fun onScanComplete(devices: List<BluetoothDevice>) {
if (devices.isNotEmpty()) {
val dialog = BluetoothDeviceDialog.newInstance(devices)
dialog.show(supportFragmentManager, "BluetoothDeviceDialog")
}
}
```
## 📱 现在的UI布局
### 启动时的界面
```
┌─────────────────────────────────────────┐
│ [连接蓝牙] [启动程序] │
│ [停止程序] [陷波滤波] │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 状态信息区域 │
│ [应用已就绪,可以开始使用] │
│ [权限状态信息] │
│ [点击"连接蓝牙"按钮开始蓝牙连接...] │
└─────────────────────────────────────────┘
```
### 有数据时的界面
```
┌─────────────────────────────────────────┐
│ [连接蓝牙] [启动程序] │
│ [停止程序] [陷波滤波] │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ ECG实时监测 │
│ ┌─────────────────────────────────────┐ │
│ │ [实时ECG曲线图] │ │
│ └─────────────────────────────────────┘ │
│ ┌─────────────────────────────────────┐ │
│ │ [详细ECG波形图] │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 状态信息区域 │
│ [蓝牙状态信息] │
│ [数据处理状态] │
└─────────────────────────────────────────┘
```
## 🚀 测试步骤
### 第一步:验证启动界面
1. **启动应用**
2. **确认界面**
- ✅ 只显示按钮区域和状态信息区域
- ✅ ECG图表区域完全隐藏
- ✅ 按钮没有超出屏幕边界
### 第二步:测试蓝牙扫描
1. **点击"连接蓝牙"按钮**
2. **观察扫描过程**
```
蓝牙状态: 正在扫描蓝牙设备...
发现设备: [设备名称] ([地址])
发现设备: [设备名称] ([地址])
```
### 第三步:验证设备选择对话框
1. **扫描完成后**
- ✅ 应该弹出设备选择对话框
- ✅ 显示所有发现的设备列表
- ✅ 每个设备显示名称和地址
### 第四步:测试设备连接
1. **选择任意设备**
- 点击设备名称
- 观察连接过程
2. **验证连接状态**
- 按钮文字变为"断开蓝牙"
- 按钮颜色变为红色
### 第五步:测试数据显示
1. **连接成功后点击"启动程序"**
2. **观察图表显示**
- ✅ ECG图表区域应该出现
- ✅ 显示实时数据曲线
- ✅ 界面布局合理,不超出屏幕
## 🔧 关键代码修改
### 1. 布局文件修改
```xml
<!-- ECG图表区域 -->
<LinearLayout
android:id="@+id/ecg_chart_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="0.7"
android:orientation="vertical"
android:background="#F8F8F8"
android:visibility="gone"> <!-- 默认隐藏 -->
```
### 2. 数据显示逻辑
```kotlin
override fun onDataReceived(data: ByteArray) {
runOnUiThread {
updateStatus("接收到蓝牙数据: ${data.size} 字节")
dataManager.onBleNotify(data)
// 显示ECG图表
binding.ecgChartContainer.visibility = View.VISIBLE
}
}
override fun onProcessedDataAvailable(channelIndex: Int, processedData: List<Float>) {
if (channelIndex == 0) {
try {
// 显示ECG图表
binding.ecgChartContainer.visibility = View.VISIBLE
binding.ecgRhythmView.updateData(processedData)
binding.ecgWaveformView.updateData(processedData)
} catch (e: Exception) {
Log.e("MainActivity", "处理实时数据失败: ${e.message}", e)
}
}
}
```
## ⚠️ 注意事项
### 1. 设备选择对话框
- 确保`BluetoothDeviceDialog.kt`文件存在
- 确保对话框正确显示设备列表
- 确保点击设备后能正确连接
### 2. 图表显示时机
- 图表只在有数据时才显示
- 蓝牙数据接收时显示
- 处理后的数据更新时显示
### 3. 界面适配
- 按钮布局适应不同屏幕尺寸
- 图表区域合理分配空间
- 状态信息区域保持可见
## 🎉 预期效果
### 启动时:
- ✅ 界面简洁,只显示必要元素
- ✅ 按钮布局合理,不超出屏幕
- ✅ 状态信息清晰可见
### 扫描时:
- ✅ 显示扫描进度和设备发现信息
- ✅ 扫描完成后弹出设备选择对话框
### 连接后:
- ✅ 显示连接状态
- ✅ 可以启动数据处理
### 有数据时:
- ✅ ECG图表自动显示
- ✅ 实时更新数据曲线
- ✅ 界面布局完整合理
现在可以测试修复后的UI了🎉

View File

@ -12,7 +12,7 @@ android {
minSdk = 24 minSdk = 24
targetSdk = 36 targetSdk = 36
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild { externalNativeBuild {
@ -22,6 +22,15 @@ android {
} }
} }
signingConfigs {
create("release") {
storeFile = file("keystore.jks")
storePassword = "android123"
keyAlias = "key0"
keyPassword = "android123"
}
}
buildTypes { buildTypes {
release { release {
isMinifyEnabled = false isMinifyEnabled = false
@ -29,6 +38,10 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro"
) )
signingConfig = signingConfigs.getByName("release")
}
debug {
signingConfig = signingConfigs.getByName("release")
} }
} }
compileOptions { compileOptions {

BIN
app/keystore.jks Normal file

Binary file not shown.

View File

@ -15,6 +15,10 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- 文件导出权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- 蓝牙功能声明 --> <!-- 蓝牙功能声明 -->
<uses-feature android:name="android.hardware.bluetooth" android:required="true" /> <uses-feature android:name="android.hardware.bluetooth" android:required="true" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" /> <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
@ -27,16 +31,39 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Cmake_project_test"> android:theme="@style/Theme.Cmake_project_test"
android:description="@string/app_description"
android:requestLegacyExternalStorage="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true"> android:exported="true"
android:launchMode="singleTop"
android:configChanges="orientation|screenSize|keyboardHidden">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".FullscreenChartActivity"
android:exported="false"
android:theme="@style/Theme.Cmake_project_test.NoActionBar"
android:screenOrientation="landscape"
android:configChanges="orientation|screenSize|keyboardHidden"
android:parentActivityName=".MainActivity" />
<!-- FileProvider for sharing files -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application> </application>
</manifest> </manifest>

View File

@ -172,6 +172,14 @@ private:
const std::vector<std::vector<float>>& channels); const std::vector<std::vector<float>>& channels);
std::vector<std::vector<float>> apply_stethoscope_filters( std::vector<std::vector<float>> apply_stethoscope_filters(
const std::vector<std::vector<float>>& channels); const std::vector<std::vector<float>>& channels);
// 新增:单导联心电专用处理函数
std::vector<std::vector<float>> apply_single_lead_ecg_filters(
const std::vector<std::vector<float>>& channels);
std::vector<float> enhance_signal_quality(
const std::vector<float>& signal, double sample_rate);
std::vector<float> apply_smoothing(
const std::vector<float>& signal, int window_size);
}; };

View File

@ -241,13 +241,14 @@ SensorData parse_pw_ecg_sl(const uint8_t* data) {
payload += 1; payload += 1;
// 解析单通道ECG数据 (115个采样点) // 解析单通道ECG数据 (115个采样点)
auto& channel = result.channel_data.emplace<std::vector<float>>(); auto& channels = result.channel_data.emplace<std::vector<std::vector<float>>>();
channel.reserve(115); channels.resize(1); // 单通道
channels[0].reserve(115);
for (int i = 0; i < 115; ++i) { for (int i = 0; i < 115; ++i) {
int16_t adc_value = read_le<int16_t>(payload); int16_t adc_value = read_le<int16_t>(payload);
payload += 2; payload += 2;
channel.push_back(adc_value * 0.318f); // 转换为μV与12导联心电保持一致 channels[0].push_back(adc_value * 0.288f); // 转换为μV与12导联心电保持一致
} }
// 跳过预留1字节 // 跳过预留1字节

View File

@ -178,6 +178,9 @@ std::vector<std::vector<float>> SignalProcessor::apply_channel_filters(
case DataType::ECG_12LEAD: case DataType::ECG_12LEAD:
filtered_channels = apply_ecg_filters(channels); filtered_channels = apply_ecg_filters(channels);
break; break;
case DataType::PW_ECG_SL:
filtered_channels = apply_single_lead_ecg_filters(channels);
break;
case DataType::PPG: case DataType::PPG:
filtered_channels = apply_ppg_filters(channels); filtered_channels = apply_ppg_filters(channels);
break; break;
@ -1571,3 +1574,117 @@ FeatureSet SignalProcessor::extract_stethoscope_features(const SensorData& data)
return result; return result;
} }
// 新增:单导联心电专用信号处理函数
std::vector<std::vector<float>> SignalProcessor::apply_single_lead_ecg_filters(
const std::vector<std::vector<float>>& channels) {
std::vector<std::vector<float>> filtered_channels = channels;
for (auto& channel : filtered_channels) {
if (channel.empty()) continue;
try {
// 1. 去除直流分量
channel = remove_dc_offset(channel);
// 2. 0.5Hz高通滤波(去除基线漂移)
channel = filter(channel, 250.0, 0, 0.5, filtertype::highpass);
// 3. 50Hz陷波滤波去除工频干扰
channel = filter(channel, 250.0, 49.5, 50.5, filtertype::notchpass);
// 4. 运动伪迹检测和去除
channel = remove_motion_artifacts(channel, 250.0);
// 5. 信号质量评估和增强
channel = enhance_signal_quality(channel, 250.0);
} catch (const std::exception& e) {
std::cerr << "单导联心电滤波处理失败: " << e.what() << std::endl;
}
}
return filtered_channels;
}
// 新增:信号质量增强函数
std::vector<float> SignalProcessor::enhance_signal_quality(
const std::vector<float>& signal, double sample_rate) {
if (signal.empty()) return signal;
std::vector<float> enhanced_signal = signal;
try {
// 1. 计算信号质量指标
double mean_val = 0.0;
double variance = 0.0;
for (float val : signal) {
mean_val += val;
}
mean_val /= signal.size();
for (float val : signal) {
double diff = val - mean_val;
variance += diff * diff;
}
variance /= signal.size();
double std_dev = std::sqrt(variance);
// 2. 异常值检测和修正
const double threshold = 3.0 * std_dev; // 3σ原则
for (size_t i = 0; i < enhanced_signal.size(); ++i) {
if (std::abs(enhanced_signal[i] - mean_val) > threshold) {
// 使用前后点的平均值替换异常值
if (i > 0 && i < enhanced_signal.size() - 1) {
enhanced_signal[i] = (enhanced_signal[i-1] + enhanced_signal[i+1]) / 2.0f;
} else if (i > 0) {
enhanced_signal[i] = enhanced_signal[i-1];
} else if (i < enhanced_signal.size() - 1) {
enhanced_signal[i] = enhanced_signal[i+1];
}
}
}
// 3. 平滑处理(轻微)
enhanced_signal = apply_smoothing(enhanced_signal, 3);
} catch (const std::exception& e) {
std::cerr << "信号质量增强失败: " << e.what() << std::endl;
}
return enhanced_signal;
}
// 新增:平滑处理函数
std::vector<float> SignalProcessor::apply_smoothing(
const std::vector<float>& signal, int window_size) {
if (signal.empty() || window_size < 3) return signal;
std::vector<float> smoothed_signal = signal;
int half_window = window_size / 2;
for (size_t i = 0; i < signal.size(); ++i) {
double sum = 0.0;
int count = 0;
// 计算窗口内的平均值
for (int j = -half_window; j <= half_window; ++j) {
int idx = static_cast<int>(i) + j;
if (idx >= 0 && idx < static_cast<int>(signal.size())) {
sum += signal[idx];
count++;
}
}
if (count > 0) {
smoothed_signal[i] = static_cast<float>(sum / count);
}
}
return smoothed_signal;
}

View File

@ -26,6 +26,9 @@ import java.util.UUID
*/ */
class BluetoothManager(private val context: Context) { class BluetoothManager(private val context: Context) {
// PPTM命令编码器
private val pptmEncoder = PPTMCommandEncoder()
companion object { companion object {
private const val TAG = "BluetoothManager" private const val TAG = "BluetoothManager"
@ -110,61 +113,15 @@ class BluetoothManager(private val context: Context) {
// 常见的ECG设备UUID列表 // 常见的ECG设备UUID列表
private val SERVICE_UUIDS = listOf( private val SERVICE_UUIDS = listOf(
UUID.fromString("6e400001-b5a3-f393-e0a9-68716563686f"), // Nordic UART Service (NUS) - 你的设备 UUID.fromString("6e400001-b5a3-f393-e0a9-68716563686f"), // Nordic UART Service (NUS) - 你的设备
UUID.fromString("0000fff0-0000-1000-8000-00805f9b34fb"), // 默认
UUID.fromString("0000ffe0-0000-1000-8000-00805f9b34fb"), // 常见变体1
UUID.fromString("0000ffe5-0000-1000-8000-00805f9b34fb"), // 常见变体2
UUID.fromString("0000ff00-0000-1000-8000-00805f9b34fb"), // 常见变体3
UUID.fromString("0000ff10-0000-1000-8000-00805f9b34fb"), // 常见变体4
UUID.fromString("0000ff20-0000-1000-8000-00805f9b34fb"), // 常见变体5
UUID.fromString("0000ff30-0000-1000-8000-00805f9b34fb"), // 常见变体6
UUID.fromString("0000ff40-0000-1000-8000-00805f9b34fb"), // 常见变体7
UUID.fromString("0000ff50-0000-1000-8000-00805f9b34fb"), // 常见变体8
UUID.fromString("0000ff60-0000-1000-8000-00805f9b34fb"), // 常见变体9
UUID.fromString("0000ff70-0000-1000-8000-00805f9b34fb"), // 常见变体10
UUID.fromString("0000ff80-0000-1000-8000-00805f9b34fb"), // 常见变体11
UUID.fromString("0000ff90-0000-1000-8000-00805f9b34fb"), // 常见变体12
UUID.fromString("0000ffa0-0000-1000-8000-00805f9b34fb"), // 常见变体13
UUID.fromString("0000ffb0-0000-1000-8000-00805f9b34fb") // 常见变体14
) )
private val CHARACTERISTIC_UUIDS = listOf( private val CHARACTERISTIC_UUIDS = listOf(
UUID.fromString("6e400002-b5a3-f393-e0a9-68716563686f"), // Nordic UART TX Characteristic - 发送数据 UUID.fromString("6e400002-b5a3-f393-e0a9-68716563686f"), // Nordic UART TX Characteristic - 发送数据
UUID.fromString("6e400003-b5a3-f393-e0a9-68716563686f"), // Nordic UART RX Characteristic - 接收数据(Notify)
UUID.fromString("0000fff1-0000-1000-8000-00805f9b34fb"), // 默认
UUID.fromString("0000ffe1-0000-1000-8000-00805f9b34fb"), // 常见变体1
UUID.fromString("0000ffe6-0000-1000-8000-00805f9b34fb"), // 常见变体2
UUID.fromString("0000ff01-0000-1000-8000-00805f9b34fb"), // 常见变体3
UUID.fromString("0000ff11-0000-1000-8000-00805f9b34fb"), // 常见变体4
UUID.fromString("0000ff21-0000-1000-8000-00805f9b34fb"), // 常见变体5
UUID.fromString("0000ff31-0000-1000-8000-00805f9b34fb"), // 常见变体6
UUID.fromString("0000ff41-0000-1000-8000-00805f9b34fb"), // 常见变体7
UUID.fromString("0000ff51-0000-1000-8000-00805f9b34fb"), // 常见变体8
UUID.fromString("0000ff61-0000-1000-8000-00805f9b34fb"), // 常见变体9
UUID.fromString("0000ff71-0000-1000-8000-00805f9b34fb"), // 常见变体10
UUID.fromString("0000ff81-0000-1000-8000-00805f9b34fb"), // 常见变体11
UUID.fromString("0000ff91-0000-1000-8000-00805f9b34fb"), // 常见变体12
UUID.fromString("0000ffa1-0000-1000-8000-00805f9b34fb"), // 常见变体13
UUID.fromString("0000ffb1-0000-1000-8000-00805f9b34fb") // 常见变体14
) )
// Notify特征UUID列表 // Notify特征UUID列表
private val NOTIFY_CHARACTERISTIC_UUIDS = listOf( private val NOTIFY_CHARACTERISTIC_UUIDS = listOf(
UUID.fromString("6e400003-b5a3-f393-e0a9-68716563686f"), // Nordic UART RX Characteristic - 你的设备 UUID.fromString("6e400003-b5a3-f393-e0a9-68716563686f"), // Nordic UART RX Characteristic - 你的设备
UUID.fromString("0000fff2-0000-1000-8000-00805f9b34fb"), // 默认Notify
UUID.fromString("0000ffe2-0000-1000-8000-00805f9b34fb"), // 常见变体1 Notify
UUID.fromString("0000ffe7-0000-1000-8000-00805f9b34fb"), // 常见变体2 Notify
UUID.fromString("0000ff02-0000-1000-8000-00805f9b34fb"), // 常见变体3 Notify
UUID.fromString("0000ff12-0000-1000-8000-00805f9b34fb"), // 常见变体4 Notify
UUID.fromString("0000ff22-0000-1000-8000-00805f9b34fb"), // 常见变体5 Notify
UUID.fromString("0000ff32-0000-1000-8000-00805f9b34fb"), // 常见变体6 Notify
UUID.fromString("0000ff42-0000-1000-8000-00805f9b34fb"), // 常见变体7 Notify
UUID.fromString("0000ff52-0000-1000-8000-00805f9b34fb"), // 常见变体8 Notify
UUID.fromString("0000ff62-0000-1000-8000-00805f9b34fb"), // 常见变体9 Notify
UUID.fromString("0000ff72-0000-1000-8000-00805f9b34fb"), // 常见变体10 Notify
UUID.fromString("0000ff82-0000-1000-8000-00805f9b34fb"), // 常见变体11 Notify
UUID.fromString("0000ff92-0000-1000-8000-00805f9b34fb"), // 常见变体12 Notify
UUID.fromString("0000ffa2-0000-1000-8000-00805f9b34fb"), // 常见变体13 Notify
UUID.fromString("0000ffb2-0000-1000-8000-00805f9b34fb") // 常见变体14 Notify
) )
// 设备名称前缀 (根据你的设备调整) // 设备名称前缀 (根据你的设备调整)
@ -179,6 +136,14 @@ class BluetoothManager(private val context: Context) {
// 连接状态 // 连接状态
private var isConnected = false private var isConnected = false
private var isConnecting = false private var isConnecting = false
private var isClassicScanReceiverRegistered = false
// Handler和延迟任务
private val mainHandler = android.os.Handler(android.os.Looper.getMainLooper())
private var bleScanStopRunnable: Runnable? = null
// 清理状态标志
private var isCleaningUp = false
// 设备列表 // 设备列表
private val discoveredDevices = mutableListOf<BluetoothDevice>() private val discoveredDevices = mutableListOf<BluetoothDevice>()
@ -360,10 +325,27 @@ class BluetoothManager(private val context: Context) {
Log.d(TAG, "BLE扫描已开始") Log.d(TAG, "BLE扫描已开始")
callback?.onStatusChanged("📡 BLE扫描进行中...") callback?.onStatusChanged("📡 BLE扫描进行中...")
// 取消之前的延迟任务(如果存在)
bleScanStopRunnable?.let { mainHandler.removeCallbacks(it) }
// 15秒后停止扫描 // 15秒后停止扫描
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ bleScanStopRunnable = Runnable {
try {
// 检查是否正在清理
if (isCleaningUp) {
Log.d(TAG, "正在清理中跳过延迟BLE扫描停止")
return@Runnable
}
stopBleScan() stopBleScan()
}, 15000) } catch (e: Exception) {
Log.e(TAG, "延迟停止BLE扫描失败: ${e.message}")
// 如果是FragmentManager相关错误记录但不抛出异常
if (e.message?.contains("FragmentManager") == true) {
Log.w(TAG, "FragmentManager已销毁延迟BLE扫描停止操作被忽略")
}
}
}
mainHandler.postDelayed(bleScanStopRunnable!!, 15000)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "BLE扫描失败: ${e.message}") Log.e(TAG, "BLE扫描失败: ${e.message}")
@ -412,7 +394,19 @@ class BluetoothManager(private val context: Context) {
* 停止BLE扫描 * 停止BLE扫描
*/ */
private fun stopBleScan() { private fun stopBleScan() {
// 如果正在清理,直接返回
if (isCleaningUp) {
Log.d(TAG, "正在清理中跳过BLE扫描停止")
return
}
try { try {
// 取消延迟任务
bleScanStopRunnable?.let {
mainHandler.removeCallbacks(it)
bleScanStopRunnable = null
}
bluetoothLeScanner?.stopScan(bleScanCallback) bluetoothLeScanner?.stopScan(bleScanCallback)
Log.d(TAG, "BLE扫描已停止") Log.d(TAG, "BLE扫描已停止")
@ -433,6 +427,10 @@ class BluetoothManager(private val context: Context) {
callback?.onScanComplete(discoveredDevices.toList()) callback?.onScanComplete(discoveredDevices.toList())
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "停止BLE扫描失败: ${e.message}") Log.e(TAG, "停止BLE扫描失败: ${e.message}")
// 如果是FragmentManager相关错误记录但不抛出异常
if (e.message?.contains("FragmentManager") == true) {
Log.w(TAG, "FragmentManager已销毁BLE扫描停止操作被忽略")
}
} }
} }
@ -1077,7 +1075,7 @@ class BluetoothManager(private val context: Context) {
} }
/** /**
* 直接连接到指定MAC地址的设备用于测试 * 直接连接到指定MAC地址的设备
*/ */
fun connectToMacAddress(macAddress: String) { fun connectToMacAddress(macAddress: String) {
if (isConnecting || isConnected) { if (isConnecting || isConnected) {
@ -1098,11 +1096,6 @@ class BluetoothManager(private val context: Context) {
return return
} }
// 验证MAC地址格式
if (!isValidMacAddress(macAddress)) {
callback?.onError("MAC地址格式错误: $macAddress")
return
}
// 尝试获取设备 // 尝试获取设备
val device = bluetoothAdapter!!.getRemoteDevice(macAddress) val device = bluetoothAdapter!!.getRemoteDevice(macAddress)
@ -1166,14 +1159,6 @@ class BluetoothManager(private val context: Context) {
} }
} }
/**
* 验证MAC地址格式
*/
private fun isValidMacAddress(macAddress: String): Boolean {
// 支持多种格式60:E9:AA:30:8B:0A 或 60-E9-AA-30-8B-0A
val pattern = Regex("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$")
return pattern.matches(macAddress)
}
/** /**
* 计算CRC16-CCITT-FALSE校验 * 计算CRC16-CCITT-FALSE校验
@ -1557,12 +1542,23 @@ class BluetoothManager(private val context: Context) {
* 注册传统蓝牙扫描广播接收器 * 注册传统蓝牙扫描广播接收器
*/ */
private fun registerClassicScanReceiver() { private fun registerClassicScanReceiver() {
try {
if (!isClassicScanReceiverRegistered) {
val filter = android.content.IntentFilter().apply { val filter = android.content.IntentFilter().apply {
addAction(BluetoothDevice.ACTION_FOUND) addAction(BluetoothDevice.ACTION_FOUND)
addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED) addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED)
addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED) addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)
} }
context.registerReceiver(classicScanReceiver, filter) context.registerReceiver(classicScanReceiver, filter)
isClassicScanReceiverRegistered = true
Log.d(TAG, "传统蓝牙扫描广播接收器已注册")
} else {
Log.d(TAG, "传统蓝牙扫描广播接收器已注册,跳过重复注册")
}
} catch (e: Exception) {
Log.e(TAG, "注册广播接收器失败: ${e.message}")
isClassicScanReceiverRegistered = false
}
} }
/** /**
@ -1570,9 +1566,17 @@ class BluetoothManager(private val context: Context) {
*/ */
private fun unregisterClassicScanReceiver() { private fun unregisterClassicScanReceiver() {
try { try {
// 检查接收器是否已注册
if (isClassicScanReceiverRegistered) {
context.unregisterReceiver(classicScanReceiver) context.unregisterReceiver(classicScanReceiver)
isClassicScanReceiverRegistered = false
Log.d(TAG, "传统蓝牙扫描广播接收器已注销")
} else {
Log.d(TAG, "传统蓝牙扫描广播接收器未注册,跳过注销")
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "注销广播接收器失败: ${e.message}") Log.e(TAG, "注销广播接收器失败: ${e.message}")
isClassicScanReceiverRegistered = false
} }
} }
@ -1650,8 +1654,117 @@ class BluetoothManager(private val context: Context) {
* 清理资源 * 清理资源
*/ */
fun cleanup() { fun cleanup() {
try {
Log.d(TAG, "开始清理BluetoothManager资源")
// 设置清理标志,防止后续操作
isCleaningUp = true
// 取消所有延迟任务
bleScanStopRunnable?.let {
mainHandler.removeCallbacks(it)
bleScanStopRunnable = null
Log.d(TAG, "已取消BLE扫描延迟任务")
}
// 断开连接
disconnect() disconnect()
// 停止扫描现在会检查isCleaningUp标志
stopScan() stopScan()
// 注销广播接收器(如果已注册)
if (isClassicScanReceiverRegistered) {
unregisterClassicScanReceiver() unregisterClassicScanReceiver()
} }
// 清理状态
isClassicScanReceiverRegistered = false
isConnected = false
isConnecting = false
Log.d(TAG, "BluetoothManager资源清理完成")
} catch (e: Exception) {
Log.e(TAG, "清理BluetoothManager资源时发生异常: ${e.message}")
} finally {
// 确保清理标志被设置
isCleaningUp = true
}
}
// ==================== PPTM设备命令方法 ====================
/**
* 发送PPTM开始采样命令
* 使用PPTM协议编码器生成带CRC16校验的命令
*/
fun sendPptmStartSampleCmd() {
try {
val command = pptmEncoder.createPptmStartSampleCmd()
Log.d(TAG, "发送PPTM开始采样命令: ${command.joinToString(", ") { "0x%02X".format(it) }}")
sendCommand(command)
callback?.onStatusChanged("📤 已发送PPTM开始采样命令")
} catch (e: Exception) {
Log.e(TAG, "发送PPTM开始采样命令失败: ${e.message}")
callback?.onCommandSent(false, "发送PPTM开始采样命令失败: ${e.message}")
}
}
/**
* 发送PPTM停止采样命令
* 使用PPTM协议编码器生成带CRC16校验的命令
*/
fun sendPptmStopSampleCmd() {
try {
val command = pptmEncoder.createPptmStopSampleCmd()
Log.d(TAG, "发送PPTM停止采样命令: ${command.joinToString(", ") { "0x%02X".format(it) }}")
sendCommand(command)
callback?.onStatusChanged("📤 已发送PPTM停止采样命令")
} catch (e: Exception) {
Log.e(TAG, "发送PPTM停止采样命令失败: ${e.message}")
callback?.onCommandSent(false, "发送PPTM停止采样命令失败: ${e.message}")
}
}
/**
* 发送自定义PPTM命令
* @param opcode 操作码
* @param payload 负载数据
*/
fun sendCustomPptmCmd(opcode: Short, payload: ByteArray) {
try {
val command = pptmEncoder.createCustomPptmCmd(opcode, payload)
Log.d(TAG, "发送自定义PPTM命令: 操作码=0x%04X, 负载=${payload.joinToString(", ") { "0x%02X".format(it) }}".format(opcode))
sendCommand(command)
callback?.onStatusChanged("📤 已发送自定义PPTM命令: 操作码=0x%04X".format(opcode))
} catch (e: Exception) {
Log.e(TAG, "发送自定义PPTM命令失败: ${e.message}")
callback?.onCommandSent(false, "发送自定义PPTM命令失败: ${e.message}")
}
}
/**
* 验证PPTM数据包CRC16校验
* @param packet 完整数据包
* @return 校验是否通过
*/
fun verifyPptmPacket(packet: ByteArray): Boolean {
return pptmEncoder.verifyCrc16(packet)
}
/**
* 解析PPTM数据包
* @param packet 完整数据包
* @return 解析结果 (操作码, 负载数据) null
*/
fun parsePptmPacket(packet: ByteArray): Pair<Short, ByteArray>? {
return pptmEncoder.parsePacket(packet)
}
/**
* 获取PPTM编码器实例用于调试
*/
fun getPptmEncoder(): PPTMCommandEncoder {
return pptmEncoder
}
} }

View File

@ -21,21 +21,38 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
} }
private var realTimeCallback: RealTimeDataCallback? = null private var realTimeCallback: RealTimeDataCallback? = null
private val additionalCallbacks = mutableListOf<RealTimeDataCallback>()
fun setRealTimeCallback(callback: RealTimeDataCallback) { fun setRealTimeCallback(callback: RealTimeDataCallback) {
this.realTimeCallback = callback this.realTimeCallback = callback
} }
fun addRealTimeCallback(callback: RealTimeDataCallback) {
if (!additionalCallbacks.contains(callback)) {
additionalCallbacks.add(callback)
}
}
fun removeRealTimeCallback(callback: RealTimeDataCallback) {
try {
additionalCallbacks.remove(callback)
Log.d("DataManager", "已移除实时数据回调,剩余回调数: ${additionalCallbacks.size}")
} catch (e: Exception) {
Log.e("DataManager", "移除实时数据回调失败: ${e.message}", e)
}
}
private var parserHandle: Long = 0L private var parserHandle: Long = 0L
private val rawStream = ByteArrayOutputStream(4096) private val rawStream = ByteArrayOutputStream(4096)
private val packetBuffer = mutableListOf<SensorData>() private val packetBuffer = mutableListOf<SensorData>()
private val mappedPacketBuffer = mutableListOf<SensorData>() // 新增:映射后的数据包缓冲区
private var totalPacketsParsed = 0L private var totalPacketsParsed = 0L
// 信号处理相关 // 信号处理相关
private var streamingSignalProcessor: StreamingSignalProcessor? = null private var streamingSignalProcessor: StreamingSignalProcessor? = null
private var streamingSignalProcessorInitialized = false private var streamingSignalProcessorInitialized = false
private val processedPackets = mutableListOf<SensorData>() private val processedPackets = mutableListOf<SensorData>()
private val filterSettings = FilterSettings() private var filterSettings = FilterSettings()
// 通道映射相关 // 通道映射相关
private var dataMapper: DataMapper? = null private var dataMapper: DataMapper? = null
@ -65,7 +82,13 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
*/ */
fun ensureParser() { fun ensureParser() {
if (parserHandle == 0L) { if (parserHandle == 0L) {
try {
parserHandle = nativeCallback.createStreamParser() parserHandle = nativeCallback.createStreamParser()
Log.d("DataManager", "JNI解析器创建成功句柄: $parserHandle")
} catch (e: Exception) {
Log.e("DataManager", "JNI解析器创建失败: ${e.message}", e)
throw RuntimeException("无法创建JNI解析器", e)
}
} }
} }
@ -83,10 +106,22 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
ensureParser() ensureParser()
rawStream.write(chunk) rawStream.write(chunk)
try {
nativeCallback.streamParserAppend(parserHandle, chunk) nativeCallback.streamParserAppend(parserHandle, chunk)
} catch (e: Exception) {
Log.e("DataManager", "JNI数据追加失败: ${e.message}", e)
return // 不继续处理,避免进一步错误
}
// 拉取解析出的数据包 // 拉取解析出的数据包
val packets = nativeCallback.streamParserDrainPackets(parserHandle) val packets = try {
nativeCallback.streamParserDrainPackets(parserHandle)
} catch (e: Exception) {
Log.e("DataManager", "JNI数据拉取失败: ${e.message}", e)
null
}
Log.d("DataManager", "解析结果: ${if (packets != null) packets.size else 0} 个数据包") Log.d("DataManager", "解析结果: ${if (packets != null) packets.size else 0} 个数据包")
if (!packets.isNullOrEmpty()) { if (!packets.isNullOrEmpty()) {
@ -98,7 +133,7 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
// 立即发送原始数据到图表显示(优先显示原始数据) // 立即发送原始数据到图表显示(优先显示原始数据)
sendRawDataToCharts(packets) sendRawDataToCharts(packets)
// 可选:后台进行流式处理(不影响显示) // 注释掉后台处理,只显示原始数据
// processStreamingData(packets) // processStreamingData(packets)
} else { } else {
Log.w("DataManager", "没有解析出有效数据包,尝试生成测试数据") Log.w("DataManager", "没有解析出有效数据包,尝试生成测试数据")
@ -216,17 +251,98 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
} }
/** /**
* 立即发送原始数据到图表显示 - 专门处理单导联蓝牙数据 * 立即发送映射后的数据到图表显示
*/ */
private fun sendRawDataToCharts(packets: List<SensorData>) { private fun sendRawDataToCharts(packets: List<SensorData>) {
if (packets.isEmpty()) { if (packets.isEmpty()) {
return return
} }
// 快速处理:减少日志,提高速度 // 确保数据映射器已初始化
ensureDataMapper()
if (!dataMapperInitialized || dataMapper == null) {
Log.w("DataManager", "数据映射器未初始化,使用原始数据")
sendRawDataToChartsDirect(packets)
return
}
try {
Log.d("DataManager", "开始数据映射处理,输入数据包数: ${packets.size}")
// 清空之前的映射数据
mappedPacketBuffer.clear()
// 对每个数据包进行映射
val mappedPackets = mutableListOf<SensorData>()
for (packet in packets) {
val mappedPacket = dataMapper?.mapSensorData(packet)
if (mappedPacket != null) {
mappedPackets.add(mappedPacket)
mappedPacketBuffer.add(mappedPacket) // 保存到映射缓冲区
Log.d("DataManager", "数据包映射成功,原始通道数: ${packet.getChannelData()?.size ?: 0}, 映射后通道数: ${mappedPacket.getChannelData()?.size ?: 0}")
} else {
Log.w("DataManager", "数据包映射失败,使用原始数据")
mappedPackets.add(packet)
mappedPacketBuffer.add(packet)
}
}
// 发送映射后的数据到图表
sendMappedDataToCharts(mappedPackets)
} catch (e: Exception) {
Log.e("DataManager", "数据映射处理失败: ${e.message}", e)
// 映射失败时使用原始数据
sendRawDataToChartsDirect(packets)
}
}
/**
* 发送映射后的数据到图表
*/
private fun sendMappedDataToCharts(mappedPackets: List<SensorData>) {
if (mappedPackets.isEmpty()) {
return
}
var totalDataPoints = 0 var totalDataPoints = 0
// 专门处理单导联数据包 // 处理映射后的数据包
for (packet in mappedPackets) {
val channelData = packet.getChannelData()
if (channelData != null && channelData.isNotEmpty()) {
// 发送所有映射后的通道数据
for (i in channelData.indices) {
val channel = channelData[i]
if (channel.isNotEmpty()) {
// 发送给主回调
realTimeCallback?.onRawDataAvailable(i, channel)
// 发送给所有附加回调
additionalCallbacks.forEach { callback ->
callback.onRawDataAvailable(i, channel)
}
totalDataPoints += channel.size
}
}
}
}
if (totalDataPoints > 0) {
Log.d("DataManager", "发送映射后数据到图表: ${totalDataPoints} 个数据点")
}
}
/**
* 直接发送原始数据到图表备用方法
*/
private fun sendRawDataToChartsDirect(packets: List<SensorData>) {
if (packets.isEmpty()) {
return
}
var totalDataPoints = 0
// 处理原始数据包
for (packet in packets) { for (packet in packets) {
val channelData = packet.getChannelData() val channelData = packet.getChannelData()
if (channelData != null && channelData.isNotEmpty()) { if (channelData != null && channelData.isNotEmpty()) {
@ -234,10 +350,12 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
val primaryChannel = channelData[0] val primaryChannel = channelData[0]
if (primaryChannel.isNotEmpty()) { if (primaryChannel.isNotEmpty()) {
// 立即发送单导联数据到图表 // 立即发送单导联数据到图表
if (realTimeCallback != null) { realTimeCallback?.onRawDataAvailable(0, primaryChannel) // 使用通道0表示主导联
realTimeCallback!!.onRawDataAvailable(0, primaryChannel) // 使用通道0表示主导联 // 发送给所有附加回调
totalDataPoints += primaryChannel.size additionalCallbacks.forEach { callback ->
callback.onRawDataAvailable(0, primaryChannel)
} }
totalDataPoints += primaryChannel.size
// 如果有其他通道(如参考通道),也发送 // 如果有其他通道(如参考通道),也发送
if (channelData.size > 1) { if (channelData.size > 1) {
@ -245,6 +363,9 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
val additionalChannel = channelData[i] val additionalChannel = channelData[i]
if (additionalChannel.isNotEmpty()) { if (additionalChannel.isNotEmpty()) {
realTimeCallback?.onRawDataAvailable(i, additionalChannel) realTimeCallback?.onRawDataAvailable(i, additionalChannel)
additionalCallbacks.forEach { callback ->
callback.onRawDataAvailable(i, additionalChannel)
}
} }
} }
} }
@ -254,7 +375,7 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
// 只在需要时记录日志 // 只在需要时记录日志
if (totalDataPoints > 0) { if (totalDataPoints > 0) {
Log.d("DataManager", "快速发送原始数据: ${totalDataPoints} 个数据点") Log.d("DataManager", "发送原始数据到图表: ${totalDataPoints} 个数据点")
} }
} }
@ -393,12 +514,12 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
Log.d("DataManager", "发送处理后的数据到图表,长度: ${processedData.size}") Log.d("DataManager", "发送处理后的数据到图表,长度: ${processedData.size}")
Log.d("DataManager", "处理后数据前3个值: ${processedData.take(3).joinToString(", ")}") Log.d("DataManager", "处理后数据前3个值: ${processedData.take(3).joinToString(", ")}")
if (realTimeCallback != null) { realTimeCallback?.onRawDataAvailable(0, processedData)
realTimeCallback!!.onRawDataAvailable(0, processedData) // 发送给所有附加回调
Log.d("DataManager", "已发送处理后数据到图表") additionalCallbacks.forEach { callback ->
} else { callback.onRawDataAvailable(0, processedData)
Log.e("DataManager", "realTimeCallback 为空,无法发送处理后数据")
} }
Log.d("DataManager", "已发送处理后数据到图表")
} }
// 计算指标(使用第一个处理后的通道) // 计算指标(使用第一个处理后的通道)
@ -566,40 +687,6 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
} }
} }
/**
* 清理缓冲区
*/
fun cleanupBuffer() {
try {
// 清理原始数据包缓冲区保留最新的100个数据包
val maxKeepPackets = 100
if (packetBuffer.size > maxKeepPackets) {
val toRemove = packetBuffer.size - maxKeepPackets
repeat(toRemove) {
if (packetBuffer.isNotEmpty()) {
packetBuffer.removeAt(0)
}
}
Log.d("DataManager", "清理了 $toRemove 个旧数据包,保留 $maxKeepPackets 个最新数据包")
}
// 清理处理后数据包缓冲区保留最新的50个数据包
val maxKeepProcessedPackets = 50
if (processedPackets.size > maxKeepProcessedPackets) {
val toRemove = processedPackets.size - maxKeepProcessedPackets
repeat(toRemove) {
if (processedPackets.isNotEmpty()) {
processedPackets.removeAt(0)
}
}
Log.d("DataManager", "清理了 $toRemove 个旧处理后数据包,保留 $maxKeepProcessedPackets 个最新数据包")
}
} catch (e: Exception) {
Log.e("DataManager", "清理缓冲区时发生错误: ${e.message}")
}
}
/** /**
* 计算信号质量 * 计算信号质量
*/ */
@ -670,7 +757,7 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
val processedChannel = when (packet.getDataType()) { val processedChannel = when (packet.getDataType()) {
type.SensorData.DataType.EEG -> applyEEGFilters(channel) type.SensorData.DataType.EEG -> applyEEGFilters(channel)
type.SensorData.DataType.ECG_2LEAD, type.SensorData.DataType.ECG_12LEAD -> applyECGFilters(channel) type.SensorData.DataType.ECG_2LEAD, type.SensorData.DataType.ECG_12LEAD -> applyECGFilters(channel)
type.SensorData.DataType.PPG -> applyPPGFilters(channel) type.SensorData.DataType.PPG -> channel // PPG不进行滤波处理直接返回原始数据
else -> channel // 其他类型暂时不处理 else -> channel // 其他类型暂时不处理
} }
processedChannels.add(processedChannel) processedChannels.add(processedChannel)
@ -746,7 +833,8 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
val notchFiltered = streamingSignalProcessor!!.notchFilter( val notchFiltered = streamingSignalProcessor!!.notchFilter(
lowpassFiltered, lowpassFiltered,
filterSettings.sampleRate.toFloat(), filterSettings.sampleRate.toFloat(),
50.0f 50.0f,
10.0f
) )
if (notchFiltered != null) { if (notchFiltered != null) {
@ -762,6 +850,42 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
return channel // 处理失败时返回原始数据 return channel // 处理失败时返回原始数据
} }
/**
* 清理缓冲区
*/
fun cleanupBuffer() {
try {
// 清理原始数据包缓冲区保留最新的100个数据包
val maxKeepPackets = 100
if (packetBuffer.size > maxKeepPackets) {
val toRemove = packetBuffer.size - maxKeepPackets
repeat(toRemove) {
if (packetBuffer.isNotEmpty()) {
packetBuffer.removeAt(0)
}
}
Log.d("DataManager", "清理了 $toRemove 个旧数据包,保留 $maxKeepPackets 个最新数据包")
}
// 清理处理后数据包缓冲区保留最新的50个数据包
val maxKeepProcessedPackets = 50
if (processedPackets.size > maxKeepProcessedPackets) {
val toRemove = processedPackets.size - maxKeepProcessedPackets
repeat(toRemove) {
if (processedPackets.isNotEmpty()) {
processedPackets.removeAt(0)
}
}
Log.d("DataManager", "清理了 $toRemove 个旧处理后数据包,保留 $maxKeepProcessedPackets 个最新数据包")
}
} catch (e: Exception) {
Log.e("DataManager", "清理缓冲区时发生错误: ${e.message}")
}
}
/** /**
* 应用PPG滤波器 * 应用PPG滤波器
*/ */
@ -796,18 +920,44 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
return channel // 处理失败时返回原始数据 return channel // 处理失败时返回原始数据
} }
/**
* 设置滤波器参数
*/
fun setFilterSettings(settings: FilterSettings) {
try {
filterSettings = settings
Log.d("DataManager", "滤波器设置已更新: $settings")
} catch (e: Exception) {
Log.e("DataManager", "设置滤波器参数失败: ${e.message}")
}
}
/**
* 获取当前滤波器设置
*/
fun getFilterSettings(): FilterSettings {
return filterSettings
}
/** /**
* 清理资源 * 清理资源
*/ */
fun cleanup() { fun cleanup() {
try { try {
if (parserHandle != 0L) { if (parserHandle != 0L) {
try {
nativeCallback.destroyStreamParser(parserHandle) nativeCallback.destroyStreamParser(parserHandle)
Log.d("DataManager", "JNI解析器销毁成功")
} catch (e: Exception) {
Log.e("DataManager", "JNI解析器销毁失败: ${e.message}", e)
} finally {
parserHandle = 0L parserHandle = 0L
} }
}
// 清理缓冲区 // 清理缓冲区
packetBuffer.clear() packetBuffer.clear()
mappedPacketBuffer.clear() // 清理映射后的数据包缓冲区
processedPackets.clear() processedPackets.clear()
calculatedMetrics.clear() calculatedMetrics.clear()
channelBuffers.clear() channelBuffers.clear()
@ -820,9 +970,39 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
lastProcessTime = 0L lastProcessTime = 0L
totalProcessedSamples = 0L totalProcessedSamples = 0L
// 清理回调列表
additionalCallbacks.clear()
Log.d("DataManager", "资源清理完成") Log.d("DataManager", "资源清理完成")
} catch (e: Exception) { } catch (e: Exception) {
Log.e("DataManager", "清理资源时发生错误: ${e.message}") Log.e("DataManager", "清理资源时发生错误: ${e.message}")
} }
} }
/**
* 轻量级清理 - 只清理缓冲区不销毁解析器
*/
fun lightCleanup() {
try {
// 只清理缓冲区,保持解析器运行
packetBuffer.clear()
mappedPacketBuffer.clear()
processedPackets.clear()
calculatedMetrics.clear()
channelBuffers.clear()
processedChannelBuffers.clear()
rawStream.reset()
Log.d("DataManager", "轻量级清理完成,解析器保持运行")
} catch (e: Exception) {
Log.e("DataManager", "轻量级清理时发生错误: ${e.message}", e)
}
}
/**
* 获取映射后的数据包缓冲区
*/
fun getMappedPacketBuffer(): List<SensorData> {
return mappedPacketBuffer.toList()
}
} }

View File

@ -0,0 +1,392 @@
package com.example.cmake_project_test
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import android.util.Log
import type.SensorData
/**
* 动态图表管理器
* 负责根据设备类型动态创建和管理图表
*/
class DynamicChartManager(
private val context: Context,
private val chartsContainer: LinearLayout,
private val chartTitle: TextView?
) {
// 全屏按钮点击监听器
private var onChartFullscreenClickListener: ((chartIndex: Int, chart: DynamicChartView) -> Unit)? = null
private val charts = mutableListOf<DynamicChartView>()
private var currentDeviceType: SensorData.DataType? = null
private var currentChannelCount = 0
/**
* 设备配置信息
*/
data class DeviceConfig(
val deviceName: String,
val channelCount: Int,
val sampleRate: Float,
val timeWindow: Float,
val maxDataPoints: Int,
val channelNames: List<String>
)
/**
* 根据设备类型获取配置
*/
private fun getDeviceConfig(dataType: SensorData.DataType): DeviceConfig {
return when (dataType) {
SensorData.DataType.EEG -> DeviceConfig(
deviceName = "脑电设备",
channelCount = 8, // 6个EEG + 2个EOG
sampleRate = 250f,
timeWindow = 2.5f,
maxDataPoints = 625, // 250Hz * 2.5s = 625个点
channelNames = listOf("Fp1", "Fp2", "F3", "F4", "C3", "C4", "EOG1", "EOG2")
)
SensorData.DataType.ECG_2LEAD -> DeviceConfig(
deviceName = "双导联心电",
channelCount = 6, // ECG1, ECG2, EMG1, EMG2, 呼吸温度, 呼吸阻抗
sampleRate = 250f,
timeWindow = 2.5f,
maxDataPoints = 625, // 250Hz * 2.5s = 625个点
channelNames = listOf("ECG1", "ECG2", "EMG1", "EMG2", "呼吸温度", "呼吸阻抗")
)
SensorData.DataType.ECG_12LEAD -> DeviceConfig(
deviceName = "12导联心电",
channelCount = 12, // 映射后12个通道I, II, III, aVR, aVL, aVF, V1, V2, V3, V4, V5, V6
sampleRate = 250f,
timeWindow = 2.5f,
maxDataPoints = 625, // 250Hz * 2.5s = 625个点
channelNames = listOf("I", "II", "III", "aVR", "aVL", "aVF", "V1", "V2", "V3", "V4", "V5", "V6")
)
SensorData.DataType.PW_ECG_SL -> DeviceConfig(
deviceName = "单导联心电",
channelCount = 1,
sampleRate = 250f,
timeWindow = 2.5f,
maxDataPoints = 625, // 250Hz * 2.5s = 625个点
channelNames = listOf("单导联ECG")
)
SensorData.DataType.PPG -> DeviceConfig(
deviceName = "血氧设备",
channelCount = 1, // 两个通道合并为一张图
sampleRate = 100f,
timeWindow = 2.5f,
maxDataPoints = 250, // 100Hz * 2.5s = 250个点
channelNames = listOf("PPG信号") // 合并通道名称
)
SensorData.DataType.RESPIRATION -> DeviceConfig(
deviceName = "呼吸设备",
channelCount = 1,
sampleRate = 100f,
timeWindow = 2.5f,
maxDataPoints = 250, // 100Hz * 2.5s = 250个点
channelNames = listOf("呼吸气流")
)
SensorData.DataType.SNORE -> DeviceConfig(
deviceName = "鼾声设备",
channelCount = 1,
sampleRate = 1000f,
timeWindow = 2.5f,
maxDataPoints = 2500, // 1000Hz * 2.5s = 2500个点
channelNames = listOf("鼾声")
)
SensorData.DataType.STETHOSCOPE -> DeviceConfig(
deviceName = "数字听诊器",
channelCount = 2,
sampleRate = 1000f,
timeWindow = 2.5f,
maxDataPoints = 2500, // 1000Hz * 2.5s = 2500个点
channelNames = listOf("听诊通道1", "听诊通道2")
)
else -> DeviceConfig(
deviceName = "未知设备",
channelCount = 1,
sampleRate = 250f,
timeWindow = 2.5f,
maxDataPoints = 625, // 250Hz * 2.5s = 625个点
channelNames = listOf("通道")
)
}
}
/**
* 根据设备类型创建图表
*/
fun createChartsForDevice(dataType: SensorData.DataType) {
val config = getDeviceConfig(dataType)
Log.d("DynamicChartManager", "=== 开始创建图表 ===")
Log.d("DynamicChartManager", "设备类型: $dataType")
Log.d("DynamicChartManager", "设备名称: ${config.deviceName}")
Log.d("DynamicChartManager", "通道数量: ${config.channelCount}")
Log.d("DynamicChartManager", "当前设备类型: $currentDeviceType")
Log.d("DynamicChartManager", "当前通道数: $currentChannelCount")
// 如果设备类型没有变化,不需要重新创建
if (currentDeviceType == dataType && currentChannelCount == config.channelCount) {
Log.d("DynamicChartManager", "设备类型未变化,跳过创建")
return
}
Log.d("DynamicChartManager", "为设备 ${config.deviceName} 创建 ${config.channelCount} 个图表")
// 清除现有图表
clearAllCharts()
// 更新标题(如果存在)
chartTitle?.text = "${config.deviceName}实时监测"
// 特殊处理PPG设备创建多通道单图表
if (dataType == type.SensorData.DataType.PPG) {
val chartView = DynamicChartView(context)
// 设置为多通道模式,原始有两个通道
chartView.setMultiChannelMode(2)
// 设置图表配置
chartView.setChartConfig(
title = "红光PPG/红外光PPG",
channelIdx = 1,
device = config.deviceName,
maxPoints = config.maxDataPoints,
sampleRateHz = config.sampleRate,
timeWindowSec = config.timeWindow
)
// 设置布局参数
val layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
marginStart = 8
marginEnd = 8
topMargin = 4
bottomMargin = 4
}
chartsContainer.addView(chartView, layoutParams)
charts.add(chartView)
// 设置全屏按钮点击监听器
chartView.setOnFullscreenClickListener {
onChartFullscreenClickListener?.invoke(0, chartView)
}
currentDeviceType = dataType
currentChannelCount = config.channelCount
return
}
// 创建新图表
for (i in 0 until config.channelCount) {
val chartView = DynamicChartView(context)
// 设置图表配置
val channelName = if (i < config.channelNames.size) config.channelNames[i] else "通道"
chartView.setChartConfig(
title = channelName,
channelIdx = i + 1,
device = config.deviceName,
maxPoints = config.maxDataPoints,
sampleRateHz = config.sampleRate,
timeWindowSec = config.timeWindow
)
// 设置布局参数
val layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
marginStart = 8
marginEnd = 8
topMargin = 4
bottomMargin = 4
}
// 添加分隔线(除了最后一个图表)
if (i < config.channelCount - 1) {
val separator = View(context).apply {
setBackgroundColor(android.graphics.Color.parseColor("#CCCCCC"))
}
val separatorParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
2
).apply {
marginStart = 8
marginEnd = 8
topMargin = 4
bottomMargin = 4
}
chartsContainer.addView(separator, separatorParams)
}
chartsContainer.addView(chartView, layoutParams)
charts.add(chartView)
// 设置全屏按钮点击监听器
val chartIndex = i
chartView.setOnFullscreenClickListener {
onChartFullscreenClickListener?.invoke(chartIndex, chartView)
}
}
currentDeviceType = dataType
currentChannelCount = config.channelCount
Log.d("DynamicChartManager", "=== 图表创建完成 ===")
Log.d("DynamicChartManager", "成功创建 ${charts.size} 个图表")
Log.d("DynamicChartManager", "图表容器子视图数量: ${chartsContainer.childCount}")
}
/**
* 更新图表数据
*/
fun updateChartsData(channelData: List<List<Float>>) {
Log.d("DynamicChartManager", "=== 开始更新图表数据 ===")
Log.d("DynamicChartManager", "图表数量: ${charts.size}")
Log.d("DynamicChartManager", "数据通道数: ${channelData.size}")
if (charts.isEmpty() || channelData.isEmpty()) {
Log.w("DynamicChartManager", "图表为空或数据为空,跳过更新")
return
}
// 性能优化:减少日志输出
val dataChannels = channelData.size
val chartCount = charts.size
Log.d("DynamicChartManager", "数据通道: $dataChannels, 图表数量: $chartCount")
// 特殊处理PPG设备的多通道数据
if (currentDeviceType == type.SensorData.DataType.PPG && chartCount == 1) {
Log.d("DynamicChartManager", "PPG多通道数据更新: 通道数=${channelData.size}, 数据点=${channelData.map { it.size }}")
// PPG设备只有一个图表但有两个通道的数据
val chart = charts[0]
chart.updateMultiChannelData(channelData)
return
}
// 更新每个图表的数据 - 并行处理
for (i in 0 until minOf(dataChannels, chartCount)) {
val chart = charts[i]
val data = channelData[i]
chart.updateData(data)
}
// 如果数据通道少于图表,清空多余的图表
if (dataChannels < chartCount) {
for (i in dataChannels until chartCount) {
charts[i].clearData()
}
}
}
/**
* 清空所有图表
*/
fun clearAllCharts() {
try {
Log.d("DynamicChartManager", "=== 开始清空所有图表 ===")
Log.d("DynamicChartManager", "当前图表数量: ${charts.size}")
Log.d("DynamicChartManager", "容器子视图数量: ${chartsContainer.childCount}")
if (charts.isEmpty()) {
Log.d("DynamicChartManager", "图表列表为空,跳过清理")
return
}
// 清理图表数据
charts.forEachIndexed { index, chart ->
try {
Log.d("DynamicChartManager", "清理图表 $index")
Log.d("DynamicChartManager", "图表类型: ${chart.javaClass.simpleName}")
// 清理图表数据
chart.clearData()
Log.d("DynamicChartManager", "图表 $index 数据清理完成")
// 清理触摸事件处理器
chart.cleanupTouchHandlers()
Log.d("DynamicChartManager", "图表 $index 触摸处理器清理完成")
} catch (e: Exception) {
Log.e("DynamicChartManager", "清理图表 $index 失败: ${e.message}", e)
Log.e("DynamicChartManager", "异常堆栈: ${e.stackTraceToString()}")
}
}
// 清空图表列表
charts.clear()
Log.d("DynamicChartManager", "图表列表已清空")
// 移除所有子视图
chartsContainer.removeAllViews()
Log.d("DynamicChartManager", "容器子视图已移除")
// 重置状态
currentDeviceType = null
currentChannelCount = 0
Log.d("DynamicChartManager", "状态已重置")
Log.d("DynamicChartManager", "=== 清空所有图表完成 ===")
} catch (e: Exception) {
Log.e("DynamicChartManager", "清空图表时发生异常: ${e.message}", e)
Log.e("DynamicChartManager", "异常堆栈: ${e.stackTraceToString()}")
}
}
/**
* 重置所有图表视口
*/
fun resetAllCharts() {
charts.forEach { chart ->
chart.resetViewport()
}
}
/**
* 获取当前图表列表
*/
fun getCharts(): List<DynamicChartView> = charts.toList()
/**
* 获取当前设备类型
*/
fun getCurrentDeviceType(): SensorData.DataType? = currentDeviceType
/**
* 获取当前通道数量
*/
fun getCurrentChannelCount(): Int = currentChannelCount
/**
* 获取当前设备配置
*/
fun getCurrentDeviceConfig(): DeviceConfig? {
return currentDeviceType?.let { getDeviceConfig(it) }
}
/**
* 设置图表全屏按钮点击监听器
*/
fun setOnChartFullscreenClickListener(listener: (chartIndex: Int, chart: DynamicChartView) -> Unit) {
onChartFullscreenClickListener = listener
}
}

View File

@ -0,0 +1,954 @@
package com.example.cmake_project_test
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.ScaleGestureDetector
import kotlin.math.max
import kotlin.math.min
/**
* 可调节的通用生理信号图表视图
* 支持多种设备类型和通道数量的动态显示具有缩放平移时间窗口调节等功能
*/
class DynamicChartView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paint = Paint().apply {
color = Color.BLUE
strokeWidth = 2f
style = Paint.Style.STROKE
isAntiAlias = false // 关闭抗锯齿,提高绘制性能
}
private val gridPaint = Paint().apply {
color = Color.LTGRAY
strokeWidth = 1f
style = Paint.Style.STROKE
alpha = 80
}
private val textPaint = Paint().apply {
color = Color.BLACK
textSize = 20f
isAntiAlias = true
}
private val controlPaint = Paint().apply {
color = Color.RED
strokeWidth = 3f
style = Paint.Style.STROKE
isAntiAlias = true
}
private val path = Path()
private var dataPoints = mutableListOf<Float>()
private var maxDataPoints = 1000 // 默认数据点数量
private var minValue = Float.MAX_VALUE
private var maxValue = Float.MIN_VALUE
private var isDataAvailable = false
// 累积数据点计数器
private var totalDataPointsReceived = 0L
// 全屏模式和自定义Y轴范围
private var isFullscreenMode = false
private var customMinValue: Float? = null
private var customMaxValue: Float? = null
// 全屏按钮
private var fullscreenButtonRect = RectF()
private var showFullscreenButton = true
private var onFullscreenClickListener: (() -> Unit)? = null
// 图表配置
private var chartTitle = "通道"
private var channelIndex = 0
private var deviceType = "未知设备"
private var sampleRate = 250f // 默认采样率
private var timeWindow = 4f // 默认时间窗口(秒)
// 简化的数据管理:直接显示新数据
private val dataBuffer = mutableListOf<Float>()
// 多通道数据支持用于PPG等设备
private val multiChannelDataPoints = mutableListOf<MutableList<Float>>()
private val multiChannelDataBuffers = mutableListOf<MutableList<Float>>()
private var isMultiChannelMode = false
private val channelColors = listOf(Color.RED, Color.BLUE) // 通道颜色
// 交互控制参数
private var scaleFactor = 1.0f
private var translateX = 0f
private var translateY = 0f
private var viewportStart = 0f
private var viewportEnd = 1.0f
private var isDragging = false
private var lastTouchX = 0f
private var lastTouchY = 0f
// 手势检测器
private val scaleGestureDetector = ScaleGestureDetector(context, ScaleListener())
// 控制按钮区域
private val controlButtonRect = RectF()
/**
* 设置图表配置
*/
fun setChartConfig(
title: String,
channelIdx: Int,
device: String,
maxPoints: Int,
sampleRateHz: Float,
timeWindowSec: Float
) {
chartTitle = title
channelIndex = channelIdx
deviceType = device
maxDataPoints = optimizeMaxDataPoints(maxPoints, sampleRateHz)
sampleRate = sampleRateHz
timeWindow = timeWindowSec
resetViewport()
invalidate()
}
/**
* 根据设备类型和内存情况优化最大数据点数量
*/
private fun optimizeMaxDataPoints(baseMaxPoints: Int, sampleRate: Float): Int {
try {
val runtime = Runtime.getRuntime()
val maxMemoryMB = runtime.maxMemory() / 1024 / 1024
var optimizedPoints = baseMaxPoints
// 根据采样率调整
optimizedPoints = when {
sampleRate >= 8000f -> (baseMaxPoints * 0.3).toInt() // 音频设备减少到30%
sampleRate >= 1000f -> (baseMaxPoints * 0.5).toInt() // 高速设备减少到50%
sampleRate >= 250f -> baseMaxPoints // ECG等保持不变
else -> (baseMaxPoints * 1.2).toInt() // 低速设备可以稍微增加
}
// 根据内存情况调整
optimizedPoints = when {
maxMemoryMB < 128 -> (optimizedPoints * 0.5).toInt() // 低内存设备
maxMemoryMB < 256 -> (optimizedPoints * 0.75).toInt() // 中等内存设备
maxMemoryMB >= 512 -> (optimizedPoints * 1.25).toInt() // 高内存设备
else -> optimizedPoints
}
// 确保在合理范围内
optimizedPoints = optimizedPoints.coerceIn(500, 10000)
Log.d("DynamicChartView", "数据点限制优化: 原始=$baseMaxPoints, 优化后=$optimizedPoints, 采样率=${sampleRate}Hz, 内存=${maxMemoryMB}MB")
return optimizedPoints
} catch (e: Exception) {
Log.e("DynamicChartView", "数据点限制优化失败: ${e.message}")
return baseMaxPoints
}
}
/**
* 设置全屏模式
*/
fun setFullscreenMode(isFullscreen: Boolean) {
isFullscreenMode = isFullscreen
// 全屏模式下隐藏全屏按钮
showFullscreenButton = !isFullscreen
invalidate()
}
/**
* 设置自定义Y轴范围
*/
fun setCustomYRange(min: Float, max: Float) {
customMinValue = min
customMaxValue = max
invalidate()
}
/**
* 清除自定义Y轴范围恢复自动计算
*/
fun clearCustomYRange() {
customMinValue = null
customMaxValue = null
invalidate()
}
/**
* 设置全屏按钮点击监听器
*/
fun setOnFullscreenClickListener(listener: () -> Unit) {
onFullscreenClickListener = listener
}
/**
* 设置是否显示全屏按钮
*/
fun setShowFullscreenButton(show: Boolean) {
showFullscreenButton = show
invalidate()
}
/**
* 设置多通道模式用于PPG等设备
*/
fun setMultiChannelMode(channelCount: Int) {
isMultiChannelMode = true
multiChannelDataPoints.clear()
multiChannelDataBuffers.clear()
// 初始化每个通道的数据结构
for (i in 0 until channelCount) {
multiChannelDataPoints.add(mutableListOf())
multiChannelDataBuffers.add(mutableListOf())
}
}
fun updateData(newData: List<Float>) {
if (newData.isEmpty()) return
isDataAvailable = true
// 更新累积数据点计数器
totalDataPointsReceived += newData.size
// 直接添加新数据,来多少显示多少
dataPoints.addAll(newData)
// 限制数据点数量,保持滚动显示
if (dataPoints.size > maxDataPoints) {
val excessData = dataPoints.size - maxDataPoints
dataPoints = dataPoints.takeLast(maxDataPoints).toMutableList()
Log.d("DynamicChartView", "清理了 $excessData 个数据点,当前数据点: ${dataPoints.size}")
}
// 定期检查内存使用情况
checkMemoryUsage()
// 更新数据范围
updateDataRange(newData)
// 立即重绘
invalidate()
}
/**
* 检查内存使用情况必要时进行清理
*/
private fun checkMemoryUsage() {
try {
// 每1000个数据点检查一次内存使用情况
if (totalDataPointsReceived % 1000 == 0L) {
val runtime = Runtime.getRuntime()
val usedMemory = runtime.totalMemory() - runtime.freeMemory()
val maxMemory = runtime.maxMemory()
val memoryUsagePercent = (usedMemory.toDouble() / maxMemory.toDouble()) * 100
Log.d("DynamicChartView", "内存使用情况: ${usedMemory / 1024 / 1024}MB/${maxMemory / 1024 / 1024}MB (${String.format("%.1f", memoryUsagePercent)}%)")
// 如果内存使用超过80%,强制清理
if (memoryUsagePercent > 80.0) {
Log.w("DynamicChartView", "内存使用过高 (${String.format("%.1f", memoryUsagePercent)}%),执行紧急清理")
emergencyCleanup()
}
}
} catch (e: Exception) {
Log.e("DynamicChartView", "检查内存使用情况失败: ${e.message}")
}
}
/**
* 紧急内存清理
*/
private fun emergencyCleanup() {
try {
Log.d("DynamicChartView", "=== 执行紧急内存清理 ===")
// 强制清理数据点只保留最新的50%数据
val keepSize = (dataPoints.size * 0.5).toInt()
if (keepSize > 0 && dataPoints.size > keepSize) {
dataPoints = dataPoints.takeLast(keepSize).toMutableList()
Log.d("DynamicChartView", "紧急清理数据点,保留 $keepSize 个数据点")
}
// 清理多通道数据
multiChannelDataPoints.forEachIndexed { index, channelData ->
val keepChannelSize = (channelData.size * 0.5).toInt()
if (keepChannelSize > 0 && channelData.size > keepChannelSize) {
multiChannelDataPoints[index] = channelData.takeLast(keepChannelSize).toMutableList()
}
}
// 重新计算数据范围
updateDataRange(dataPoints)
// 强制垃圾回收
System.gc()
Log.d("DynamicChartView", "=== 紧急内存清理完成 ===")
} catch (e: Exception) {
Log.e("DynamicChartView", "紧急内存清理失败: ${e.message}")
}
}
/**
* 获取当前数据点数量
*/
fun getDataPointCount(): Int {
return if (isMultiChannelMode) {
multiChannelDataPoints.sumOf { it.size }
} else {
dataPoints.size
}
}
/**
* 裁剪数据到指定大小
*/
fun trimDataToSize(maxSize: Int) {
try {
if (isMultiChannelMode) {
// 多通道模式
multiChannelDataPoints.forEachIndexed { index, channelData ->
if (channelData.size > maxSize) {
multiChannelDataPoints[index] = channelData.takeLast(maxSize).toMutableList()
}
}
// 重新计算数据范围
updateMultiChannelDataRange()
} else {
// 单通道模式
if (dataPoints.size > maxSize) {
dataPoints = dataPoints.takeLast(maxSize).toMutableList()
// 重新计算数据范围
updateDataRange(dataPoints)
}
}
// 重绘
invalidate()
Log.d("DynamicChartView", "数据已裁剪到 $maxSize 个点")
} catch (e: Exception) {
Log.e("DynamicChartView", "裁剪数据失败: ${e.message}")
}
}
/**
* 更新多通道数据用于PPG等设备
*/
fun updateMultiChannelData(channelData: List<List<Float>>) {
if (channelData.isEmpty() || !isMultiChannelMode) {
Log.d("DynamicChartView", "跳过多通道更新: channelData.isEmpty=${channelData.isEmpty()}, isMultiChannelMode=$isMultiChannelMode")
return
}
Log.d("DynamicChartView", "多通道数据更新: 通道数=${channelData.size}, 每个通道数据点=${channelData.map { it.size }}")
isDataAvailable = true
// 更新累积数据点计数器(使用第一个通道的数据点数量)
if (channelData.isNotEmpty()) {
totalDataPointsReceived += channelData[0].size
}
// 直接添加新数据,来多少显示多少
for (channelIndex in channelData.indices) {
if (channelIndex >= multiChannelDataPoints.size) {
Log.w("DynamicChartView", "通道索引超出范围: $channelIndex >= ${multiChannelDataPoints.size}")
continue
}
val channelDataPoints = multiChannelDataPoints[channelIndex]
channelDataPoints.addAll(channelData[channelIndex])
// 限制数据点数量,保持滚动显示
if (channelDataPoints.size > maxDataPoints) {
multiChannelDataPoints[channelIndex] = channelDataPoints.takeLast(maxDataPoints).toMutableList()
}
Log.d("DynamicChartView", "通道$channelIndex 添加了${channelData[channelIndex].size}个数据点")
}
// 更新数据范围 - 计算所有通道的数据范围
updateMultiChannelDataRange()
Log.d("DynamicChartView", "多通道数据处理完成: 通道数据点=${multiChannelDataPoints.map { it.size }}, 范围=${minValue}-${maxValue}")
// 检查内存使用情况
checkMemoryUsage()
// 立即重绘
invalidate()
}
private fun updateDataRange(newData: List<Float>) {
// 如果设置了自定义Y轴范围使用自定义范围
if (customMinValue != null && customMaxValue != null) {
minValue = customMinValue!!
maxValue = customMaxValue!!
return
}
// 使用全部数据点计算范围,提供更准确的范围
if (dataPoints.isEmpty()) return
// 计算全部数据点的范围
minValue = dataPoints.minOrNull() ?: 0f
maxValue = dataPoints.maxOrNull() ?: 0f
// 确保有足够的显示范围
val range = maxValue - minValue
if (range < 0.1f) {
val center = (maxValue + minValue) / 2
minValue = center - 0.05f
maxValue = center + 0.05f
} else {
val margin = range * 0.1f
minValue -= margin
maxValue += margin
}
}
/**
* 更新多通道数据范围
*/
private fun updateMultiChannelDataRange() {
// 如果设置了自定义Y轴范围使用自定义范围
if (customMinValue != null && customMaxValue != null) {
minValue = customMinValue!!
maxValue = customMaxValue!!
Log.d("DynamicChartView", "使用自定义Y轴范围: ${minValue}-${maxValue}")
return
}
if (multiChannelDataPoints.isEmpty()) {
Log.w("DynamicChartView", "多通道数据点为空,无法计算范围")
return
}
// 计算所有通道的最小值和最大值
var globalMin = Float.MAX_VALUE
var globalMax = Float.MIN_VALUE
var hasValidData = false
for (channelIndex in multiChannelDataPoints.indices) {
val channelData = multiChannelDataPoints[channelIndex]
if (channelData.isNotEmpty()) {
val channelMin = channelData.minOrNull() ?: 0f
val channelMax = channelData.maxOrNull() ?: 0f
globalMin = minOf(globalMin, channelMin)
globalMax = maxOf(globalMax, channelMax)
hasValidData = true
Log.d("DynamicChartView", "通道$channelIndex: 范围=${channelMin}-${channelMax}, 数据点=${channelData.size}")
}
}
if (!hasValidData) {
Log.w("DynamicChartView", "没有有效的多通道数据")
return
}
minValue = globalMin
maxValue = globalMax
Log.d("DynamicChartView", "多通道全局范围: ${minValue}-${maxValue}")
// 确保有足够的显示范围
val range = maxValue - minValue
if (range < 0.1f) {
val center = (maxValue + minValue) / 2
minValue = center - 0.05f
maxValue = center + 0.05f
} else {
val margin = range * 0.1f
minValue -= margin
maxValue += margin
}
Log.d("DynamicChartView", "多通道显示范围: ${minValue}-${maxValue}")
}
fun clearData() {
try {
Log.d("DynamicChartView", "=== 开始清理图表数据 ===")
// 清理单通道数据
dataPoints.clear()
dataBuffer.clear()
Log.d("DynamicChartView", "单通道数据已清理")
// 清除多通道数据
multiChannelDataPoints.forEachIndexed { index, channelData ->
try {
channelData.clear()
} catch (e: Exception) {
Log.e("DynamicChartView", "清理多通道数据 $index 失败: ${e.message}", e)
}
}
multiChannelDataBuffers.forEachIndexed { index, channelBuffer ->
try {
channelBuffer.clear()
} catch (e: Exception) {
Log.e("DynamicChartView", "清理多通道缓冲区 $index 失败: ${e.message}", e)
}
}
Log.d("DynamicChartView", "多通道数据已清理")
// 重置状态
isDataAvailable = false
totalDataPointsReceived = 0L // 重置累积数据点计数器
Log.d("DynamicChartView", "数据状态已重置")
// 重置视口
resetViewport()
Log.d("DynamicChartView", "视口已重置")
// 重绘
invalidate()
Log.d("DynamicChartView", "=== 图表数据清理完成 ===")
} catch (e: Exception) {
Log.e("DynamicChartView", "清理图表数据时发生异常: ${e.message}", e)
Log.e("DynamicChartView", "异常堆栈: ${e.stackTraceToString()}")
}
}
/**
* 清理触摸事件处理器
*/
fun cleanupTouchHandlers() {
try {
Log.d("DynamicChartView", "=== 开始清理触摸事件处理器 ===")
// 重置触摸状态
isDragging = false
lastTouchX = 0f
lastTouchY = 0f
// 清理全屏按钮监听器
onFullscreenClickListener = null
Log.d("DynamicChartView", "=== 触摸事件处理器清理完成 ===")
} catch (e: Exception) {
Log.e("DynamicChartView", "清理触摸事件处理器时发生异常: ${e.message}", e)
Log.e("DynamicChartView", "异常堆栈: ${e.stackTraceToString()}")
}
}
/**
* 当视图从窗口分离时调用
*/
override fun onDetachedFromWindow() {
try {
Log.d("DynamicChartView", "=== onDetachedFromWindow 开始 ===")
// 清理触摸事件处理器
cleanupTouchHandlers()
// 清理数据
clearData()
Log.d("DynamicChartView", "=== onDetachedFromWindow 完成 ===")
} catch (e: Exception) {
Log.e("DynamicChartView", "onDetachedFromWindow 异常: ${e.message}", e)
Log.e("DynamicChartView", "异常堆栈: ${e.stackTraceToString()}")
} finally {
super.onDetachedFromWindow()
}
}
/**
* 重置视口到默认状态
*/
fun resetViewport() {
scaleFactor = 1.0f
translateX = 0f
translateY = 0f
viewportStart = 0f
viewportEnd = 1.0f
invalidate()
}
override fun onTouchEvent(event: MotionEvent): Boolean {
scaleGestureDetector.onTouchEvent(event)
when (event.action) {
MotionEvent.ACTION_DOWN -> {
val x = event.x
val y = event.y
// 检查是否点击了全屏按钮
if (showFullscreenButton && fullscreenButtonRect.contains(x, y)) {
onFullscreenClickListener?.invoke()
return true
}
// 开始拖拽
isDragging = true
lastTouchX = x
lastTouchY = y
return true
}
MotionEvent.ACTION_MOVE -> {
if (isDragging) {
val deltaX = event.x - lastTouchX
val deltaY = event.y - lastTouchY
translateX += deltaX
translateY += deltaY
// 限制平移范围
val maxTranslate = width * 0.5f
translateX = translateX.coerceIn(-maxTranslate, maxTranslate)
translateY = translateY.coerceIn(-maxTranslate, maxTranslate)
lastTouchX = event.x
lastTouchY = event.y
invalidate()
return true
}
}
MotionEvent.ACTION_UP -> {
isDragging = false
return true
}
}
return super.onTouchEvent(event)
}
private inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
scaleFactor *= detector.scaleFactor
scaleFactor = scaleFactor.coerceIn(0.2f, 5.0f)
invalidate()
return true
}
}
override fun onDraw(canvas: Canvas) {
try {
super.onDraw(canvas)
val width = width.toFloat()
val height = height.toFloat()
// 应用变换
canvas.save()
canvas.translate(translateX, translateY)
canvas.scale(scaleFactor, scaleFactor, width / 2, height / 2)
// 绘制网格
drawGrid(canvas, width, height)
// 绘制标题和通道信息
drawTitle(canvas, width, height)
// 绘制数据统计
drawStats(canvas, width, height)
// 绘制全屏按钮
if (showFullscreenButton) {
drawFullscreenButton(canvas, width, height)
}
// 如果没有数据,显示默认内容
if (!isDataAvailable) {
drawDefaultContent(canvas, width, height)
} else {
// 绘制曲线 - 检查单通道或多通道数据
val hasDataToDraw = if (isMultiChannelMode) {
multiChannelDataPoints.any { it.size > 1 }
} else {
dataPoints.size > 1
}
if (hasDataToDraw) {
drawCurve(canvas, width, height)
}
}
// 恢复画布状态
canvas.restore()
} catch (e: Exception) {
Log.e("DynamicChartView", "绘制时发生异常: ${e.message}", e)
// 确保画布状态被恢复
try {
canvas.restore()
} catch (e2: Exception) {
Log.e("DynamicChartView", "恢复画布状态失败: ${e2.message}", e2)
}
}
}
private fun drawGrid(canvas: Canvas, width: Float, height: Float) {
val padding = if (isFullscreenMode) 20f else 60f
val drawWidth = width - 2 * padding
val drawHeight = height - 2 * padding
// 绘制水平网格线和刻度
val gridLines = 5
for (i in 0..gridLines) {
val y = padding + (drawHeight * i / gridLines)
canvas.drawLine(padding, y, width - padding, y, gridPaint)
// 只在全屏模式下绘制Y轴刻度值
if (isFullscreenMode) {
val value = maxValue - (maxValue - minValue) * i / gridLines
val valueText = String.format("%.1f", value)
canvas.drawText(valueText, 5f, y + 5f, textPaint)
}
}
// 绘制垂直网格线和刻度
for (i in 0..gridLines) {
val x = padding + (drawWidth * i / gridLines)
canvas.drawLine(x, padding, x, height - padding, gridPaint)
// 只在全屏模式下绘制X轴刻度值累积数据点索引
if (isFullscreenMode) {
val dataPointIndex = if (isMultiChannelMode) {
// 多通道模式:使用累积数据点计数器
val stepSize = totalDataPointsReceived.toFloat() / gridLines
(stepSize * i).toInt()
} else {
// 单通道模式:使用累积数据点计数器
val stepSize = totalDataPointsReceived.toFloat() / gridLines
(stepSize * i).toInt()
}
val indexText = dataPointIndex.toString()
canvas.drawText(indexText, x - 10f, height - 10f, textPaint)
}
}
}
private fun drawTitle(canvas: Canvas, width: Float, height: Float) {
val title = "$chartTitle $channelIndex"
canvas.drawText(title, 10f, 25f, textPaint)
val deviceInfo = "$deviceType (${sampleRate.toInt()}Hz)"
canvas.drawText(deviceInfo, 10f, 50f, textPaint)
// 显示当前缩放和时间窗口信息
val scaleInfo = "缩放: ${String.format("%.1f", scaleFactor)}x"
canvas.drawText(scaleInfo, width - 150f, 25f, textPaint)
val timeInfo = "时间窗口: ${timeWindow.toInt()}"
canvas.drawText(timeInfo, width - 150f, 50f, textPaint)
}
private fun drawStats(canvas: Canvas, width: Float, height: Float) {
val totalDataPoints = if (isMultiChannelMode) {
multiChannelDataPoints.sumOf { it.size }
} else {
dataPoints.size
}
val statsText = if (isMultiChannelMode) {
"显示数据点: ${totalDataPoints} (${multiChannelDataPoints.size}通道) | 累积: ${totalDataPointsReceived}"
} else {
"显示数据点: ${totalDataPoints} | 累积: ${totalDataPointsReceived}"
}
canvas.drawText(statsText, 10f, height - 35f, textPaint)
if (totalDataPoints > 0) {
val rangeText = if (customMinValue != null && customMaxValue != null) {
"Y轴范围: ${String.format("%.0f", minValue)} - ${String.format("%.0f", maxValue)} (固定)"
} else {
"Y轴范围: ${String.format("%.2f", minValue)} - ${String.format("%.2f", maxValue)} (自动)"
}
canvas.drawText(rangeText, 10f, height - 15f, textPaint)
}
}
private fun drawDefaultContent(canvas: Canvas, width: Float, height: Float) {
val centerX = width / 2
val centerY = height / 2
val hintPaint = Paint().apply {
color = Color.GRAY
textSize = 16f
textAlign = Paint.Align.CENTER
isAntiAlias = true
}
canvas.drawText("等待数据...", centerX, centerY - 20f, hintPaint)
canvas.drawText("请先连接蓝牙设备", centerX, centerY + 20f, hintPaint)
canvas.drawText("然后点击'启动程序'显示数据", centerX, centerY + 60f, hintPaint)
}
private fun drawFullscreenButton(canvas: Canvas, width: Float, height: Float) {
val buttonSize = 40f
val margin = 10f
val buttonX = width - buttonSize - margin
val buttonY = margin
// 设置按钮区域
fullscreenButtonRect.set(buttonX, buttonY, buttonX + buttonSize, buttonY + buttonSize)
// 绘制按钮背景
val buttonPaint = Paint().apply {
color = Color.parseColor("#4CAF50")
style = Paint.Style.FILL
isAntiAlias = true
}
canvas.drawRoundRect(fullscreenButtonRect, 8f, 8f, buttonPaint)
// 绘制按钮边框
val borderPaint = Paint().apply {
color = Color.WHITE
style = Paint.Style.STROKE
strokeWidth = 2f
isAntiAlias = true
}
canvas.drawRoundRect(fullscreenButtonRect, 8f, 8f, borderPaint)
// 绘制全屏图标(简单的矩形)
val iconPaint = Paint().apply {
color = Color.WHITE
style = Paint.Style.STROKE
strokeWidth = 2f
isAntiAlias = true
}
val iconMargin = 8f
val iconRect = RectF(
buttonX + iconMargin,
buttonY + iconMargin,
buttonX + buttonSize - iconMargin,
buttonY + buttonSize - iconMargin
)
canvas.drawRect(iconRect, iconPaint)
// 绘制全屏文字
val textPaint = Paint().apply {
color = Color.WHITE
textSize = 10f
textAlign = Paint.Align.CENTER
isAntiAlias = true
}
canvas.drawText("全屏", buttonX + buttonSize/2, buttonY + buttonSize/2 + 3f, textPaint)
}
private fun drawCurve(canvas: Canvas, width: Float, height: Float) {
val padding = if (isFullscreenMode) 20f else 60f
val drawWidth = width - 2 * padding
val drawHeight = height - 2 * padding
if (isMultiChannelMode && multiChannelDataPoints.isNotEmpty()) {
Log.d("DynamicChartView", "多通道绘制: 通道数=${multiChannelDataPoints.size}, 数据点=${multiChannelDataPoints.map { it.size }}")
// 多通道绘制模式
for (channelIndex in multiChannelDataPoints.indices) {
val channelData = multiChannelDataPoints[channelIndex]
if (channelData.isEmpty()) {
Log.d("DynamicChartView", "通道$channelIndex 数据为空,跳过绘制")
continue
}
path.reset()
// 设置通道颜色
val channelPaint = Paint(paint).apply {
color = channelColors.getOrElse(channelIndex) { Color.GRAY }
strokeWidth = 2f
}
val xStep = if (channelData.size > 1) drawWidth / (channelData.size - 1) else drawWidth
for (i in channelData.indices) {
val x = padding + i * xStep
val normalizedValue = (channelData[i] - minValue) / (maxValue - minValue)
val y = padding + (0.1f + normalizedValue * 0.8f) * drawHeight
if (i == 0) {
path.moveTo(x, y)
} else {
path.lineTo(x, y)
}
}
canvas.drawPath(path, channelPaint)
}
} else {
// 单通道绘制模式
if (dataPoints.isEmpty()) return
path.reset()
val xStep = if (dataPoints.size > 1) drawWidth / (dataPoints.size - 1) else drawWidth
for (i in dataPoints.indices) {
val x = padding + i * xStep
val normalizedValue = (dataPoints[i] - minValue) / (maxValue - minValue)
val y = padding + (0.1f + normalizedValue * 0.8f) * drawHeight
if (i == 0) {
path.moveTo(x, y)
} else {
path.lineTo(x, y)
}
}
canvas.drawPath(path, paint)
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val desiredHeight = 200 // 每个图表的高度
val height = resolveSize(desiredHeight, heightMeasureSpec)
setMeasuredDimension(widthMeasureSpec, height)
}
/**
* 获取当前图表数据
*/
fun getCurrentData(): List<Float> {
return dataPoints.toList()
}
/**
* 获取数据统计信息
*/
fun getDataStats(): Map<String, Any> {
val totalDataPoints = if (isMultiChannelMode) {
multiChannelDataPoints.sumOf { it.size }
} else {
dataPoints.size
}
return mapOf(
"dataPoints" to totalDataPoints,
"multiChannelMode" to isMultiChannelMode,
"channelCount" to if (isMultiChannelMode) multiChannelDataPoints.size else 1,
"minValue" to minValue,
"maxValue" to maxValue,
"range" to (maxValue - minValue),
"isDataAvailable" to isDataAvailable,
"scaleFactor" to scaleFactor,
"timeWindow" to timeWindow
)
}
/**
* 获取是否是多通道模式
*/
fun isMultiChannelMode(): Boolean = isMultiChannelMode
}

View File

@ -24,7 +24,7 @@ class ECGRhythmView @JvmOverloads constructor(
color = Color.BLUE color = Color.BLUE
strokeWidth = 1.5f strokeWidth = 1.5f
style = Paint.Style.STROKE style = Paint.Style.STROKE
isAntiAlias = true isAntiAlias = false // 关闭抗锯齿,提高绘制性能
} }
private val gridPaint = Paint().apply { private val gridPaint = Paint().apply {
@ -47,10 +47,7 @@ class ECGRhythmView @JvmOverloads constructor(
private var maxValue = Float.MIN_VALUE private var maxValue = Float.MIN_VALUE
private var isDataAvailable = false // 标记是否有数据 private var isDataAvailable = false // 标记是否有数据
// 性能优化:数据缓冲和批量更新 // 简化的数据管理:直接显示新数据
private val dataBuffer = mutableListOf<Float>()
private var lastUpdateTime = 0L
private val updateInterval = 50L // 50ms更新间隔提高流畅度
// 缩放控制参数 // 缩放控制参数
private var scaleX = 1.0f private var scaleX = 1.0f
@ -95,28 +92,30 @@ class ECGRhythmView @JvmOverloads constructor(
private var isDragging = false private var isDragging = false
fun updateData(newData: List<Float>) { fun updateData(newData: List<Float>) {
if (newData.isNotEmpty()) { if (newData.isEmpty()) return
isDataAvailable = true isDataAvailable = true
Log.d("ECGRhythmView", "收到新数据: ${newData.size} 个点")
}
// 性能优化:批量更新数据 // 直接添加新数据,来多少显示多少
dataBuffer.addAll(newData) dataPoints.addAll(newData)
val currentTime = System.currentTimeMillis() // 限制数据点数量,保持滚动显示
if (currentTime - lastUpdateTime >= updateInterval) {
// 批量处理缓冲区的数据
if (dataBuffer.isNotEmpty()) {
dataPoints.addAll(dataBuffer)
dataBuffer.clear()
// 限制数据点数量
if (dataPoints.size > maxDataPoints) { if (dataPoints.size > maxDataPoints) {
dataPoints = dataPoints.takeLast(maxDataPoints).toMutableList() dataPoints = dataPoints.takeLast(maxDataPoints).toMutableList()
} }
// 计算数据范围 // 更新数据范围
if (dataPoints.isNotEmpty()) { updateDataRange(newData)
// 立即重绘
invalidate()
}
private fun updateDataRange(newData: List<Float>) {
// 使用全部数据点计算范围,提供更准确的范围
if (dataPoints.isEmpty()) return
// 计算全部数据点的范围
minValue = dataPoints.minOrNull() ?: 0f minValue = dataPoints.minOrNull() ?: 0f
maxValue = dataPoints.maxOrNull() ?: 0f maxValue = dataPoints.maxOrNull() ?: 0f
@ -134,18 +133,31 @@ class ECGRhythmView @JvmOverloads constructor(
} }
} }
lastUpdateTime = currentTime
Log.d("ECGRhythmView", "更新图表,数据点: ${dataPoints.size}, 范围: $minValue - $maxValue")
invalidate()
}
}
}
fun clearData() { fun clearData() {
dataPoints.clear() dataPoints.clear()
invalidate() invalidate()
} }
/**
* 获取当前图表数据
*/
fun getCurrentData(): List<Float> {
return dataPoints.toList()
}
/**
* 获取数据统计信息
*/
fun getDataStats(): Map<String, Any> {
return mapOf(
"dataPoints" to dataPoints.size,
"minValue" to minValue,
"maxValue" to maxValue,
"range" to (maxValue - minValue),
"isDataAvailable" to isDataAvailable
)
}
fun resetZoom() { fun resetZoom() {
scaleX = 1.0f scaleX = 1.0f
scaleY = 1.0f scaleY = 1.0f
@ -260,7 +272,7 @@ class ECGRhythmView @JvmOverloads constructor(
canvas.drawText("等待数据...", centerX, centerY - 20f, hintPaint) canvas.drawText("等待数据...", centerX, centerY - 20f, hintPaint)
canvas.drawText("请先连接蓝牙设备", centerX, centerY + 20f, hintPaint) canvas.drawText("请先连接蓝牙设备", centerX, centerY + 20f, hintPaint)
canvas.drawText("然后点击'启动程序'", centerX, centerY + 60f, hintPaint) canvas.drawText("然后点击'启动程序'显示原始数据", centerX, centerY + 60f, hintPaint)
// 绘制一个简单的示例波形 // 绘制一个简单的示例波形
drawSampleWaveform(canvas, width, height) drawSampleWaveform(canvas, width, height)
@ -304,9 +316,12 @@ class ECGRhythmView @JvmOverloads constructor(
val scaledWidth = drawWidth * scaleX val scaledWidth = drawWidth * scaleX
val scaledHeight = drawHeight * scaleY val scaledHeight = drawHeight * scaleY
// 性能优化:减少数据点绘制,提高流畅度
val stepSize = maxOf(1, dataPoints.size / 1000) // 最多绘制1000个点
val xStep = scaledWidth / (dataPoints.size - 1) val xStep = scaledWidth / (dataPoints.size - 1)
for (i in dataPoints.indices) { var firstPoint = true
for (i in dataPoints.indices step stepSize) {
val x = padding + i * xStep + offsetX val x = padding + i * xStep + offsetX
val normalizedValue = (dataPoints[i] - minValue) / (maxValue - minValue) val normalizedValue = (dataPoints[i] - minValue) / (maxValue - minValue)
// 使用0.1到0.9的范围确保曲线在中间80%的区域显示 // 使用0.1到0.9的范围确保曲线在中间80%的区域显示
@ -314,8 +329,9 @@ class ECGRhythmView @JvmOverloads constructor(
// 确保点在可见区域内 // 确保点在可见区域内
if (x >= padding && x <= width - padding && y >= padding && y <= height - padding) { if (x >= padding && x <= width - padding && y >= padding && y <= height - padding) {
if (i == 0) { if (firstPoint) {
path.moveTo(x, y) path.moveTo(x, y)
firstPoint = false
} else { } else {
path.lineTo(x, y) path.lineTo(x, y)
} }

View File

@ -47,10 +47,7 @@ class ECGWaveformView @JvmOverloads constructor(
private var maxValue = Float.MIN_VALUE private var maxValue = Float.MIN_VALUE
private var isDataAvailable = false // 标记是否有数据 private var isDataAvailable = false // 标记是否有数据
// 性能优化:数据缓冲和批量更新 // 简化的数据管理:直接显示新数据
private val dataBuffer = mutableListOf<Float>()
private var lastUpdateTime = 0L
private val updateInterval = 50L // 50ms更新间隔提高流畅度
// 缩放控制参数 // 缩放控制参数
private var scaleX = 1.0f private var scaleX = 1.0f
@ -95,28 +92,30 @@ class ECGWaveformView @JvmOverloads constructor(
private var isDragging = false private var isDragging = false
fun updateData(newData: List<Float>) { fun updateData(newData: List<Float>) {
if (newData.isNotEmpty()) { if (newData.isEmpty()) return
isDataAvailable = true isDataAvailable = true
Log.d("ECGWaveformView", "收到新数据: ${newData.size} 个点")
}
// 性能优化:批量更新数据 // 直接添加新数据,来多少显示多少
dataBuffer.addAll(newData) dataPoints.addAll(newData)
val currentTime = System.currentTimeMillis() // 限制数据点数量,保持滚动显示
if (currentTime - lastUpdateTime >= updateInterval) {
// 批量处理缓冲区的数据
if (dataBuffer.isNotEmpty()) {
dataPoints.addAll(dataBuffer)
dataBuffer.clear()
// 限制数据点数量
if (dataPoints.size > maxDataPoints) { if (dataPoints.size > maxDataPoints) {
dataPoints = dataPoints.takeLast(maxDataPoints).toMutableList() dataPoints = dataPoints.takeLast(maxDataPoints).toMutableList()
} }
// 计算数据范围 // 更新数据范围
if (dataPoints.isNotEmpty()) { updateDataRange(newData)
// 立即重绘
invalidate()
}
private fun updateDataRange(newData: List<Float>) {
// 使用全部数据点计算范围,提供更准确的范围
if (dataPoints.isEmpty()) return
// 计算全部数据点的范围
minValue = dataPoints.minOrNull() ?: 0f minValue = dataPoints.minOrNull() ?: 0f
maxValue = dataPoints.maxOrNull() ?: 0f maxValue = dataPoints.maxOrNull() ?: 0f
@ -127,25 +126,38 @@ class ECGWaveformView @JvmOverloads constructor(
minValue = center - 0.05f minValue = center - 0.05f
maxValue = center + 0.05f maxValue = center + 0.05f
} else { } else {
// 添加15%的上下边距,提供更好的放大效果 // 添加15%的上下边距
val margin = range * 0.15f val margin = range * 0.15f
minValue -= margin minValue -= margin
maxValue += margin maxValue += margin
} }
} }
lastUpdateTime = currentTime
Log.d("ECGWaveformView", "更新图表,数据点: ${dataPoints.size}, 范围: $minValue - $maxValue")
invalidate()
}
}
}
fun clearData() { fun clearData() {
dataPoints.clear() dataPoints.clear()
invalidate() invalidate()
} }
/**
* 获取当前图表数据
*/
fun getCurrentData(): List<Float> {
return dataPoints.toList()
}
/**
* 获取数据统计信息
*/
fun getDataStats(): Map<String, Any> {
return mapOf(
"dataPoints" to dataPoints.size,
"minValue" to minValue,
"maxValue" to maxValue,
"range" to (maxValue - minValue),
"isDataAvailable" to isDataAvailable
)
}
fun resetZoom() { fun resetZoom() {
scaleX = 1.0f scaleX = 1.0f
scaleY = 1.0f scaleY = 1.0f
@ -260,7 +272,7 @@ class ECGWaveformView @JvmOverloads constructor(
canvas.drawText("等待数据...", centerX, centerY - 20f, hintPaint) canvas.drawText("等待数据...", centerX, centerY - 20f, hintPaint)
canvas.drawText("请先连接蓝牙设备", centerX, centerY + 20f, hintPaint) canvas.drawText("请先连接蓝牙设备", centerX, centerY + 20f, hintPaint)
canvas.drawText("然后点击'启动程序'", centerX, centerY + 60f, hintPaint) canvas.drawText("然后点击'启动程序'显示原始数据", centerX, centerY + 60f, hintPaint)
// 绘制一个简单的示例波形 // 绘制一个简单的示例波形
drawSampleWaveform(canvas, width, height) drawSampleWaveform(canvas, width, height)

View File

@ -0,0 +1,901 @@
package com.example.cmake_project_test
import android.app.Activity
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.WindowManager
import android.widget.Button
import android.widget.SeekBar
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.lifecycle.lifecycleScope
import com.example.cmake_project_test.databinding.ActivityFullscreenBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import type.SensorData
/**
* 全屏图表显示Activity
* 支持ECG和PPG的不同Y轴范围设置
*/
class FullscreenChartActivity : AppCompatActivity(), DataManager.RealTimeDataCallback, NativeMethodCallback {
private lateinit var binding: ActivityFullscreenBinding
private var fullscreenChartManager: DynamicChartManager? = null
private var deviceType: String = "未知设备"
private var isSingleChart = false
private var chartIndex = 0
// 滤波器管理器
private var filterManager: FullscreenFilterManager? = null
// Activity退出标志
private var isExiting = false
// 数据回调清理标志
private var callbackRemoved = false
companion object {
// 静态数据管理器引用用于接收MainActivity的数据
private var mainDataManager: DataManager? = null
fun setMainDataManager(dataManager: DataManager) {
mainDataManager = dataManager
Log.d("FullscreenChartActivity", "设置主数据管理器引用")
}
fun clearMainDataManager() {
if (mainDataManager != null) {
Log.d("FullscreenChartActivity", "清理主数据管理器引用")
Log.d("FullscreenChartActivity", "注意这只是清理引用MainActivity的DataManager应该继续运行")
mainDataManager = null
} else {
Log.d("FullscreenChartActivity", "主数据管理器引用已为空,跳过清理")
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 设置全局异常处理器
setupGlobalExceptionHandler()
// 设置全屏模式
setupFullscreen()
binding = ActivityFullscreenBinding.inflate(layoutInflater)
setContentView(binding.root)
// 初始化全屏图表管理器(避免重复初始化)
if (fullscreenChartManager == null) {
fullscreenChartManager = DynamicChartManager(
context = this,
chartsContainer = binding.fullscreenChartsContainer,
chartTitle = null // 全屏模式下不需要标题
)
}
// 注册为数据回调接收器
val dataManager = mainDataManager
if (dataManager != null) {
dataManager.addRealTimeCallback(this)
Log.d("FullscreenChartActivity", "已注册为数据回调接收器")
} else {
Log.w("FullscreenChartActivity", "主数据管理器为空,无法注册回调")
}
// 获取传递的数据
val chartDataArray = intent.getSerializableExtra("chartData") as? Array<Map<String, Any>>
deviceType = intent.getStringExtra("deviceType") ?: "未知设备"
isSingleChart = intent.getBooleanExtra("isSingleChart", false)
chartIndex = intent.getIntExtra("chartIndex", 0)
if (chartDataArray != null) {
setupFullscreenCharts(chartDataArray)
} else {
Log.e("FullscreenChartActivity", "没有接收到图表数据")
finish()
}
// 设置退出全屏按钮
binding.exitFullscreenButton.setOnClickListener {
exitFullscreen()
}
// 初始化心率和血氧显示
setupVitalSignsDisplay()
// 初始化滤波器管理器
setupFilterManager()
}
/**
* 检查内存泄漏迹象
*/
private fun checkForMemoryLeaks_DISABLED(currentMemoryPercent: Double) {
try {
// 如果内存使用率持续高于75%且没有明显的数据积累,可能是内存泄漏
val chartDataCount = fullscreenChartManager?.let { manager ->
manager.getCharts().sumOf { chart -> chart.getDataPointCount() }
} ?: 0
val filterCacheCount = filterManager?.let { fm ->
// 简单估算缓存大小
try {
val originalCacheSize = fm.javaClass.getDeclaredField("originalDataCache")
.apply { isAccessible = true }
.get(fm) as Map<*, *>
val filteredCacheSize = fm.javaClass.getDeclaredField("filteredDataCache")
.apply { isAccessible = true }
.get(fm) as Map<*, *>
originalCacheSize.size + filteredCacheSize.size
} catch (e: Exception) {
0
}
} ?: 0
if (currentMemoryPercent > 75.0 && chartDataCount < 5000 && filterCacheCount < 20) {
Log.w("FullscreenChartActivity", "⚠️ 疑似内存泄漏: 内存使用率${String.format("%.1f", currentMemoryPercent)}%但数据量较少")
Log.w("FullscreenChartActivity", "图表数据点: $chartDataCount, 滤波缓存: $filterCacheCount")
}
} catch (e: Exception) {
Log.e("FullscreenChartActivity", "内存泄漏检查失败: ${e.message}")
}
}
/**
* 执行紧急内存清理
*/
private fun performEmergencyMemoryCleanup_DISABLED() {
try {
Log.d("FullscreenChartActivity", "=== 执行紧急内存清理 ===")
// 清理图表数据
fullscreenChartManager?.let { chartManager ->
val charts = chartManager.getCharts()
var totalCleanedPoints = 0
charts.forEach { chart ->
val currentPoints = chart.getDataPointCount()
if (currentPoints > 500) { // 如果数据点超过500个强制清理到50%
val keepPoints = (currentPoints * 0.5).toInt()
chart.trimDataToSize(keepPoints)
totalCleanedPoints += (currentPoints - keepPoints)
}
}
if (totalCleanedPoints > 0) {
Log.d("FullscreenChartActivity", "紧急清理释放了 $totalCleanedPoints 个数据点")
}
}
// 清理滤波器缓存
filterManager?.let { fm ->
try {
fm.cleanupMemory()
} catch (e: Exception) {
Log.e("FullscreenChartActivity", "清理滤波器缓存失败: ${e.message}")
}
}
// 强制垃圾回收
System.gc()
Log.d("FullscreenChartActivity", "=== 紧急内存清理完成 ===")
} catch (e: Exception) {
Log.e("FullscreenChartActivity", "紧急内存清理失败: ${e.message}")
}
}
/**
* 设置心率和血氧显示
*/
private fun setupVitalSignsDisplay() {
// 初始化心率和血氧显示区域
binding.heartRateValue.text = "--"
binding.spo2Value.text = "--"
binding.temperatureValue.text = "--"
// 设置标签
binding.heartRateLabel.text = "心率 (bpm)"
binding.spo2Label.text = "血氧 (%)"
binding.temperatureLabel.text = "体温 (°C)"
Log.d("FullscreenChartActivity", "心率和血氧显示区域已初始化")
}
/**
* 初始化滤波器管理器
*/
private fun setupFilterManager() {
try {
// 避免重复初始化滤波器管理器
if (filterManager == null) {
filterManager = FullscreenFilterManager(
context = this,
scope = lifecycleScope,
dataManager = mainDataManager,
chartManager = fullscreenChartManager,
deviceType = deviceType
)
}
// 初始化UI组件
filterManager?.initializeUI(filterBtn = binding.filterButton)
} catch (e: Exception) {
Log.e("FullscreenChartActivity", "初始化滤波器管理器失败: ${e.message}", e)
}
}
/**
* 更新心率和血氧显示
*/
private fun updateVitalSignsDisplay(additionalData: type.SensorData.AdditionalData?) {
if (additionalData == null) return
runOnUiThread {
try {
// 更新心率显示
val heartRate = additionalData.hr
if (heartRate > 0) {
binding.heartRateValue.text = heartRate.toString()
binding.heartRateValue.setTextColor(android.graphics.Color.GREEN)
} else {
binding.heartRateValue.text = "--"
binding.heartRateValue.setTextColor(android.graphics.Color.GRAY)
}
// 更新血氧显示
val spo2 = additionalData.spo2
if (spo2 > 0) {
binding.spo2Value.text = spo2.toString()
binding.spo2Value.setTextColor(android.graphics.Color.BLUE)
} else {
binding.spo2Value.text = "--"
binding.spo2Value.setTextColor(android.graphics.Color.GRAY)
}
// 更新体温显示
val temperature = additionalData.temperature
if (temperature > 0) {
binding.temperatureValue.text = String.format("%.1f", temperature)
binding.temperatureValue.setTextColor(android.graphics.Color.RED)
} else {
binding.temperatureValue.text = "--"
binding.temperatureValue.setTextColor(android.graphics.Color.GRAY)
}
Log.d("FullscreenChartActivity", "更新生命体征: HR=$heartRate, SpO2=$spo2, Temp=$temperature")
} catch (e: Exception) {
Log.e("FullscreenChartActivity", "更新生命体征显示失败: ${e.message}")
}
}
}
/**
* 设置全局异常处理器
*/
private fun setupGlobalExceptionHandler() {
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { thread, exception ->
Log.e("FullscreenChartActivity", "=== 未捕获的异常 ===")
Log.e("FullscreenChartActivity", "线程: ${thread.name}")
Log.e("FullscreenChartActivity", "异常类型: ${exception.javaClass.simpleName}")
Log.e("FullscreenChartActivity", "异常消息: ${exception.message}")
Log.e("FullscreenChartActivity", "异常堆栈: ${exception.stackTraceToString()}")
// 检查是否是JNI相关异常
if (exception.message?.contains("JNI") == true ||
exception.stackTrace.any { it.className.contains("native") || it.methodName.contains("native") }) {
Log.e("FullscreenChartActivity", "检测到JNI相关异常可能需要检查native代码")
}
// 检查是否是内存相关异常
if (exception is OutOfMemoryError) {
Log.e("FullscreenChartActivity", "检测到内存不足异常,建议优化内存使用")
}
// 检查是否是数组越界异常
if (exception is IndexOutOfBoundsException) {
Log.e("FullscreenChartActivity", "检测到数组越界异常,可能需要检查数据访问逻辑")
}
// 调用默认处理器
try {
defaultHandler?.uncaughtException(thread, exception)
} catch (e: Exception) {
Log.e("FullscreenChartActivity", "默认异常处理器也失败了: ${e.message}")
// 最后手段:强制重启应用
android.os.Process.killProcess(android.os.Process.myPid())
}
}
}
/**
* 设置全屏模式
*/
private fun setupFullscreen() {
// 隐藏状态栏和导航栏
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
)
// 保持屏幕常亮
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
// 设置全屏标题
supportActionBar?.hide()
}
/**
* 设置全屏图表
*/
private fun setupFullscreenCharts(chartDataArray: Array<Map<String, Any>>) {
Log.d("FullscreenChartActivity", "设置全屏图表,设备类型: $deviceType,图表数量: ${chartDataArray.size}")
// 根据设备类型创建图表
val dataType = try {
SensorData.DataType.valueOf(deviceType)
} catch (e: IllegalArgumentException) {
Log.w("FullscreenChartActivity", "未知设备类型: $deviceType,使用默认类型")
SensorData.DataType.PW_ECG_SL // 默认使用单导联心电
}
fullscreenChartManager?.createChartsForDevice(dataType)
// 设置全屏图表管理器的事件监听器
fullscreenChartManager?.setOnChartFullscreenClickListener { chartIndex, chart ->
// 在全屏模式下不需要处理全屏按钮点击
}
// 设置自定义Y轴范围和全屏布局
val charts = fullscreenChartManager?.getCharts() ?: emptyList()
for (i in charts.indices) {
val chart = charts[i]
// 设置全屏模式
chart.setFullscreenMode(true)
// 设置全屏布局参数
val layoutParams = android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
android.widget.LinearLayout.LayoutParams.MATCH_PARENT
).apply {
marginStart = 0
marginEnd = 0
topMargin = 0
bottomMargin = 0
}
chart.layoutParams = layoutParams
// 根据设备类型设置Y轴范围
when (deviceType) {
"ECG_2LEAD",
"ECG_12LEAD",
"PW_ECG_SL" -> {
// ECG设备Y轴范围 -20 到 20
chart.setCustomYRange(-20f, 20f)
Log.d("FullscreenChartActivity", "设置ECG图表$i Y轴范围: -20 到 20")
}
"PPG" -> {
// PPG设备Y轴范围 -2000 到 2000
chart.setCustomYRange(-2000f, 2000f)
Log.d("FullscreenChartActivity", "设置PPG图表$i Y轴范围: -2000 到 2000")
}
else -> {
// 其他设备:使用自动范围
chart.clearCustomYRange()
Log.d("FullscreenChartActivity", "设置图表$i 使用自动Y轴范围")
}
}
}
// 更新图表数据
val channelData = chartDataArray.map { chartData ->
@Suppress("UNCHECKED_CAST")
chartData["data"] as List<Float>
}
if (channelData.isNotEmpty()) {
fullscreenChartManager?.updateChartsData(channelData)
Log.d("FullscreenChartActivity", "已更新全屏图表数据,通道数: ${channelData.size}")
}
// 全屏模式下不需要更新标题
}
override fun onResume() {
super.onResume()
// 确保全屏模式
setupFullscreen()
}
override fun onPause() {
super.onPause()
Log.d("FullscreenChartActivity", "Activity暂停")
}
override fun onStop() {
super.onStop()
isExiting = true
Log.d("FullscreenChartActivity", "Activity停止")
}
override fun onDestroy() {
try {
Log.d("FullscreenChartActivity", "开始销毁Activity")
// 设置退出标志,阻止所有回调
isExiting = true
// 移除数据回调(防止重复移除)
if (!callbackRemoved) {
try {
mainDataManager?.removeRealTimeCallback(this)
callbackRemoved = true
Log.d("FullscreenChartActivity", "数据回调已移除")
} catch (e: Exception) {
Log.e("FullscreenChartActivity", "移除数据回调失败: ${e.message}")
}
} else {
Log.d("FullscreenChartActivity", "数据回调已移除,跳过重复移除")
}
// 清理静态引用
clearMainDataManager()
Log.d("FullscreenChartActivity", "Activity销毁完成")
} catch (e: Exception) {
Log.e("FullscreenChartActivity", "Activity销毁过程中出现异常: ${e.message}")
} finally {
super.onDestroy()
}
}
override fun onBackPressed() {
if (!isFinishing && !isDestroyed) {
exitFullscreen()
}
}
/**
* 安全退出全屏模式
*/
private fun exitFullscreen() {
try {
Log.d("FullscreenChartActivity", "开始退出全屏模式")
// 立即移除数据回调防止在finish()后仍有回调执行
if (!callbackRemoved) {
try {
mainDataManager?.removeRealTimeCallback(this)
callbackRemoved = true
Log.d("FullscreenChartActivity", "数据回调已移除")
} catch (e: Exception) {
Log.e("FullscreenChartActivity", "移除数据回调失败: ${e.message}")
}
} else {
Log.d("FullscreenChartActivity", "数据回调已移除,跳过重复移除")
}
// 设置退出标志,阻止所有回调
isExiting = true
// 设置结果
setResult(Activity.RESULT_OK)
// 使用更安全的方式结束Activity
if (!isFinishing && !isDestroyed) {
finish()
} else {
Log.w("FullscreenChartActivity", "Activity已经在销毁过程中跳过finish()")
}
} catch (e: Exception) {
Log.e("FullscreenChartActivity", "退出全屏模式失败: ${e.message}")
// 如果出错,直接结束
finish()
}
}
// 实时数据回调实现 - 从MainActivity接收数据
override fun onProcessedDataAvailable(channelIndex: Int, processedData: List<Float>) {
// 检查Activity是否还在运行或正在退出
if (isFinishing || isDestroyed || isExiting) {
return
}
// 检查资源是否可用
if (fullscreenChartManager == null || binding == null) {
return
}
// 如果是单图表模式,只更新对应的图表
if (isSingleChart && channelIndex != chartIndex) {
return
}
// 检查数据有效性
if (processedData.isEmpty()) {
return
}
runOnUiThread {
try {
// 再次检查Activity状态
if (isFinishing || isDestroyed || isExiting) {
Log.d("FullscreenChartActivity", "UI线程中Activity正在销毁忽略数据回调")
return@runOnUiThread
}
// 检查图表管理器是否有效
val chartManager = fullscreenChartManager
if (chartManager == null) {
Log.w("FullscreenChartActivity", "图表管理器为空,忽略数据回调")
return@runOnUiThread
}
// 通过滤波器管理器处理数据
val charts = chartManager.getCharts()
if (charts.isNotEmpty()) {
val chart = charts.getOrNull(0)
if (chart == null) {
Log.e("FullscreenChartActivity", "图表为空,跳过数据处理")
return@runOnUiThread
}
// 再次检查Activity状态防止在数据处理过程中Activity被销毁
if (isFinishing || isDestroyed || isExiting) {
Log.d("FullscreenChartActivity", "数据处理过程中Activity状态变化停止处理")
return@runOnUiThread
}
// 检查是否是PPG设备的多通道模式
if (deviceType == "PPG" && chart.isMultiChannelMode()) {
// PPG设备需要获取多通道数据
updatePPGData(chart)
} else {
// 单通道数据:检查是否有滤波器启用
try {
// 检查数据有效性
if (processedData.isEmpty()) {
Log.w("FullscreenChartActivity", "处理后数据为空,跳过处理")
return@runOnUiThread
}
// 检查数据是否包含异常值
val hasInvalidValues = processedData.any { it.isNaN() || it.isInfinite() }
if (hasInvalidValues) {
Log.w("FullscreenChartActivity", "处理后数据包含异常值,跳过处理")
return@runOnUiThread
}
// 检查是否有滤波器启用
val hasActiveFilters = filterManager?.let { manager ->
try {
val field = manager.javaClass.getDeclaredField("isAnyFilterEnabled")
field.isAccessible = true
field.getBoolean(manager)
} catch (e: Exception) {
false
}
} ?: false
if (hasActiveFilters) {
// 有滤波器启用时,使用滤波器处理
filterManager?.processRealTimeData("channel_$channelIndex", processedData) { filteredData ->
try {
// 再次检查Activity状态
if (isFinishing || isDestroyed) {
Log.d("FullscreenChartActivity", "Activity正在销毁跳过图表更新")
return@processRealTimeData
}
// 检查滤波后数据有效性
if (filteredData.isEmpty()) {
Log.w("FullscreenChartActivity", "滤波后数据为空,跳过更新")
return@processRealTimeData
}
chart.updateData(filteredData)
Log.d("FullscreenChartActivity", "已更新滤波后的单通道数据")
} catch (e: Exception) {
Log.e("FullscreenChartActivity", "更新单通道数据失败: ${e.message}", e)
}
}
} else {
// 没有滤波器启用时,直接更新图表
chart.updateData(processedData)
Log.d("FullscreenChartActivity", "已直接更新处理后数据(无滤波)")
}
} catch (e: Exception) {
Log.e("FullscreenChartActivity", "数据处理失败: ${e.message}", e)
// 出错时直接使用原始数据
try {
if (!isFinishing && !isDestroyed && processedData.isNotEmpty()) {
chart.updateData(processedData)
Log.d("FullscreenChartActivity", "已更新原始单通道数据")
}
} catch (e2: Exception) {
Log.e("FullscreenChartActivity", "更新原始数据也失败: ${e2.message}", e2)
}
}
}
}
} catch (e: Exception) {
Log.e("FullscreenChartActivity", "处理实时数据失败: ${e.message}", e)
}
}
}
override fun onRawDataAvailable(channelIndex: Int, rawData: List<Float>) {
// 检查Activity是否还在运行或正在退出
if (isFinishing || isDestroyed || isExiting) {
return
}
// 检查资源是否可用
if (fullscreenChartManager == null || binding == null) {
return
}
// 如果是单图表模式,只更新对应的图表
if (isSingleChart && channelIndex != chartIndex) {
return
}
// 检查数据有效性
if (rawData.isEmpty()) {
return
}
runOnUiThread {
try {
// 再次检查Activity状态和退出标志
if (isFinishing || isDestroyed || isExiting) {
Log.d("FullscreenChartActivity", "UI线程中Activity正在销毁忽略原始数据回调")
return@runOnUiThread
}
// 检查资源是否仍然可用
if (fullscreenChartManager == null || binding == null) {
return@runOnUiThread
}
// 检查图表管理器是否有效
val chartManager = fullscreenChartManager
if (chartManager == null) {
Log.w("FullscreenChartActivity", "图表管理器为空,忽略原始数据回调")
return@runOnUiThread
}
// 通过滤波器管理器处理数据
val charts = chartManager.getCharts()
if (charts.isNotEmpty()) {
val chart = charts.getOrNull(channelIndex.coerceAtMost(charts.size - 1))
if (chart == null) {
Log.e("FullscreenChartActivity", "通道${channelIndex}对应的图表为空,跳过数据处理")
return@runOnUiThread
}
// 再次检查Activity状态防止在数据处理过程中Activity被销毁
if (isFinishing || isDestroyed || isExiting) {
Log.d("FullscreenChartActivity", "原始数据处理过程中Activity状态变化停止处理")
return@runOnUiThread
}
// 检查是否是PPG设备的多通道模式
if (deviceType == "PPG" && chart.isMultiChannelMode()) {
// PPG设备需要获取多通道数据
updatePPGData(chart)
} else {
// 单通道数据:检查是否有滤波器启用
try {
// 检查数据有效性
if (rawData.isEmpty()) {
Log.w("FullscreenChartActivity", "原始数据为空,跳过处理")
return@runOnUiThread
}
// 检查数据是否包含异常值
val hasInvalidValues = rawData.any { it.isNaN() || it.isInfinite() }
if (hasInvalidValues) {
Log.w("FullscreenChartActivity", "原始数据包含异常值,跳过处理")
return@runOnUiThread
}
// 检查是否有滤波器启用
val hasActiveFilters = filterManager?.let { manager ->
// 通过反射或其他方式检查滤波器状态
try {
val field = manager.javaClass.getDeclaredField("isAnyFilterEnabled")
field.isAccessible = true
field.getBoolean(manager)
} catch (e: Exception) {
false // 如果无法检查,假设没有启用
}
} ?: false
if (hasActiveFilters) {
// 有滤波器启用时,使用滤波器处理
filterManager?.processRealTimeData("channel_$channelIndex", rawData) { filteredData ->
try {
// 再次检查Activity状态
if (isFinishing || isDestroyed) {
Log.d("FullscreenChartActivity", "Activity正在销毁跳过图表更新")
return@processRealTimeData
}
// 检查滤波后数据有效性
if (filteredData.isEmpty()) {
Log.w("FullscreenChartActivity", "滤波后数据为空,跳过更新")
return@processRealTimeData
}
chart.updateData(filteredData)
Log.d("FullscreenChartActivity", "已更新滤波后的原始数据")
} catch (e: Exception) {
Log.e("FullscreenChartActivity", "更新原始数据失败: ${e.message}", e)
}
}
} else {
// 没有滤波器启用时,直接更新图表
chart.updateData(rawData)
Log.d("FullscreenChartActivity", "已直接更新原始数据(无滤波)")
}
} catch (e: Exception) {
Log.e("FullscreenChartActivity", "数据处理失败: ${e.message}", e)
// 出错时直接使用原始数据
try {
if (!isFinishing && !isDestroyed && rawData.isNotEmpty()) {
chart.updateData(rawData)
Log.d("FullscreenChartActivity", "已更新原始数据")
}
} catch (e2: Exception) {
Log.e("FullscreenChartActivity", "更新原始数据也失败: ${e2.message}", e2)
}
}
}
}
// 尝试从DataManager获取最新的AdditionalData并更新生命体征显示
updateVitalSignsFromDataManager()
} catch (e: Exception) {
Log.e("FullscreenChartActivity", "处理原始数据失败: ${e.message}", e)
}
}
}
/**
* 从DataManager获取最新的生命体征数据并更新显示
*/
private fun updateVitalSignsFromDataManager() {
// 检查Activity状态
if (isFinishing || isDestroyed) {
Log.d("FullscreenChartActivity", "Activity正在销毁跳过生命体征更新")
return
}
try {
val dataManager = mainDataManager
if (dataManager != null) {
// 获取最新的数据包
val latestPackets = dataManager.getPacketBuffer()
if (latestPackets.isNotEmpty()) {
// 获取最新的数据包中的AdditionalData
val latestPacket = latestPackets.last()
val additionalData = latestPacket.additionalData
if (additionalData != null) {
updateVitalSignsDisplay(additionalData)
}
}
}
} catch (e: Exception) {
Log.e("FullscreenChartActivity", "获取生命体征数据失败: ${e.message}")
}
}
/**
* 更新PPG多通道数据
*/
private fun updatePPGData(chart: DynamicChartView) {
try {
// 检查Activity状态
if (isFinishing || isDestroyed) {
Log.d("FullscreenChartActivity", "Activity正在销毁跳过PPG数据更新")
return
}
// 检查图表是否有效
if (chart == null) {
Log.w("FullscreenChartActivity", "图表为空跳过PPG数据更新")
return
}
// 从主数据管理器获取PPG数据
val dataManager = mainDataManager
if (dataManager != null) {
try {
val mappedPackets = dataManager.getMappedPacketBuffer()
if (mappedPackets.isNotEmpty()) {
val latestMappedPacket = mappedPackets.last()
val channelData = latestMappedPacket.getChannelData()
if (channelData != null && channelData.isNotEmpty()) {
// 验证数据格式
if (channelData.size >= 2 && channelData[0].isNotEmpty() && channelData[1].isNotEmpty()) {
chart.updateMultiChannelData(channelData)
Log.d("FullscreenChartActivity", "更新PPG多通道数据: ${channelData.size}通道")
return
} else {
Log.w("FullscreenChartActivity", "PPG数据格式不正确: ${channelData.map { it.size }}")
}
} else {
Log.w("FullscreenChartActivity", "PPG通道数据为空")
}
} else {
Log.w("FullscreenChartActivity", "映射数据包为空")
}
} catch (e: Exception) {
Log.e("FullscreenChartActivity", "获取PPG数据失败: ${e.message}", e)
}
} else {
Log.w("FullscreenChartActivity", "数据管理器为空")
}
// 如果无法获取数据,使用模拟数据
try {
val mockPPGData = listOf(
listOf(100f, 120f, 110f, 130f, 115f), // 红光PPG
listOf(200f, 220f, 210f, 230f, 215f) // 红外光PPG
)
chart.updateMultiChannelData(mockPPGData)
Log.d("FullscreenChartActivity", "使用模拟PPG数据")
} catch (e: Exception) {
Log.e("FullscreenChartActivity", "更新模拟PPG数据失败: ${e.message}", e)
}
} catch (e: Exception) {
Log.e("FullscreenChartActivity", "更新PPG数据失败: ${e.message}", e)
}
}
override fun onStreamingProgress(progress: Int, totalSamples: Int, processedSamples: Int) {
// 检查Activity是否还在运行或正在退出
if (isFinishing || isDestroyed || isExiting) {
return
}
// 全屏模式下不需要显示进度
}
// NativeMethodCallback实现
override external fun createStreamParser(): Long
override external fun destroyStreamParser(handle: Long)
override external fun streamParserAppend(handle: Long, chunk: ByteArray)
override external fun streamParserDrainPackets(handle: Long): List<SensorData>?
}

View File

@ -0,0 +1,847 @@
package com.example.cmake_project_test
import android.app.AlertDialog
import android.content.Context
import android.text.TextUtils
import android.util.Log
import android.view.LayoutInflater
import android.widget.Button
import android.widget.EditText
import android.widget.Switch
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
/**
* 全屏模式滤波器管理器
* 管理滤波器状态UI交互和数据处理
*/
class FullscreenFilterManager(
private val context: Context,
private val scope: CoroutineScope,
private val dataManager: DataManager?,
private val chartManager: DynamicChartManager?,
private val deviceType: String = "PW_ECG_SL" // 默认设备类型
) {
// 滤波器状态数据类
data class FilterState(
var enabled: Boolean = false,
var frequency: Double = 0.0
)
/**
* 根据设备类型获取采样率
*/
private fun getSampleRate(): Double {
return when (deviceType) {
"ECG_2LEAD", "ECG_12LEAD", "PW_ECG_SL", "EEG" -> 250.0
"PPG" -> 50.0
"STETHOSCOPE", "SNORE" -> 8000.0
"RESPIRATION" -> 50.0
else -> 250.0 // 默认采样率
}
}
// 当前滤波器状态
private val lowpassFilter = FilterState(false, 40.0)
private val highpassFilter = FilterState(false, 1.0)
private val notchFilter = FilterState(false, 50.0)
// 是否有任何滤波器启用
private var isAnyFilterEnabled = false
// 内存管理:动态调整的缓冲区大小
private var maxDataPointsPerBatch = 100 // 每次最多处理100个数据点
private val dataBuffer = mutableListOf<Float>()
private var maxBufferSize = 500 // 缓冲区最大大小(动态调整)
// UI组件引用
private var filterButton: Button? = null
private var currentDialog: android.app.AlertDialog? = null
// 原始数据和滤波后数据缓存 - 改进为带时间戳的缓存
private val originalDataCache = mutableMapOf<String, CacheEntry>()
private val filteredDataCache = mutableMapOf<String, CacheEntry>()
// 缓存配置
private val maxCacheEntries = 50 // 最大缓存条目数
private val maxCacheDataSize = 10000 // 每个缓存条目的最大数据点数
private val cacheExpirationTime = 5 * 60 * 1000L // 缓存过期时间5分钟
// 缓存条目类
data class CacheEntry(
val data: MutableList<Float>,
val timestamp: Long = System.currentTimeMillis(),
var accessCount: Int = 0
)
/**
* 初始化UI组件
*/
fun initializeUI(filterBtn: Button) {
filterButton = filterBtn
// 根据设备配置动态调整缓冲区大小
optimizeBufferSizes()
setupUIListeners()
updateUIState()
}
/**
* 根据设备配置和内存情况动态优化缓冲区大小
*/
private fun optimizeBufferSizes() {
try {
val runtime = Runtime.getRuntime()
val maxMemoryMB = runtime.maxMemory() / 1024 / 1024
// 根据设备采样率调整批处理大小
val sampleRate = getSampleRate()
when {
sampleRate >= 8000.0 -> { // 音频设备
maxDataPointsPerBatch = 50
maxBufferSize = 200
}
sampleRate >= 1000.0 -> { // 高速设备
maxDataPointsPerBatch = 100
maxBufferSize = 400
}
sampleRate >= 250.0 -> { // 中速设备ECG等
maxDataPointsPerBatch = 150
maxBufferSize = 600
}
else -> { // 低速设备
maxDataPointsPerBatch = 200
maxBufferSize = 800
}
}
// 根据可用内存进一步调整
when {
maxMemoryMB < 128 -> { // 低内存设备
maxDataPointsPerBatch = (maxDataPointsPerBatch * 0.5).toInt()
maxBufferSize = (maxBufferSize * 0.5).toInt()
}
maxMemoryMB < 256 -> { // 中等内存设备
maxDataPointsPerBatch = (maxDataPointsPerBatch * 0.75).toInt()
maxBufferSize = (maxBufferSize * 0.75).toInt()
}
maxMemoryMB >= 512 -> { // 高内存设备
maxDataPointsPerBatch = (maxDataPointsPerBatch * 1.5).toInt()
maxBufferSize = (maxBufferSize * 1.5).toInt()
}
}
// 确保最小值
maxDataPointsPerBatch = maxOf(maxDataPointsPerBatch, 25)
maxBufferSize = maxOf(maxBufferSize, 100)
Log.d("FullscreenFilterManager", "缓冲区大小优化完成: 批处理=$maxDataPointsPerBatch, 缓冲区=$maxBufferSize, 内存=${maxMemoryMB}MB")
} catch (e: Exception) {
Log.e("FullscreenFilterManager", "缓冲区大小优化失败: ${e.message}")
// 使用默认值
maxDataPointsPerBatch = 100
maxBufferSize = 500
}
}
/**
* 设置UI监听器
*/
private fun setupUIListeners() {
// 滤波器按键
filterButton?.setOnClickListener {
showFilterDialog()
}
}
/**
* 显示滤波器设置对话框
*/
private fun showFilterDialog() {
try {
// Context在构造函数中传入不会为null但为了安全起见保留检查
val dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_filter_settings, null)
// 获取对话框中的组件
val lowpassSwitch = dialogView.findViewById<Switch>(R.id.lowpass_enable_switch)
val highpassSwitch = dialogView.findViewById<Switch>(R.id.highpass_enable_switch)
val notchSwitch = dialogView.findViewById<Switch>(R.id.notch_enable_switch)
val lowpassFreqInput = dialogView.findViewById<EditText>(R.id.lowpass_frequency_input)
val highpassFreqInput = dialogView.findViewById<EditText>(R.id.highpass_frequency_input)
val notchFreqInput = dialogView.findViewById<EditText>(R.id.notch_frequency_input)
val btnCancel = dialogView.findViewById<Button>(R.id.btn_cancel)
val btnApply = dialogView.findViewById<Button>(R.id.btn_apply)
// 检查组件是否获取成功
if (lowpassSwitch == null || highpassSwitch == null || notchSwitch == null ||
lowpassFreqInput == null || highpassFreqInput == null || notchFreqInput == null ||
btnCancel == null || btnApply == null) {
Log.e("FullscreenFilterManager", "对话框组件获取失败")
return
}
// 设置当前状态
setupDialogCurrentState(lowpassSwitch, highpassSwitch, notchSwitch,
lowpassFreqInput, highpassFreqInput, notchFreqInput)
// 创建对话框
val dialog = AlertDialog.Builder(context)
.setView(dialogView)
.create()
// 保存对话框引用
currentDialog = dialog
// 设置按钮监听器
btnCancel.setOnClickListener {
try {
dialog.dismiss()
currentDialog = null
} catch (e: Exception) {
Log.e("FullscreenFilterManager", "关闭对话框失败: ${e.message}")
}
}
btnApply.setOnClickListener {
try {
applyFilterFromDialog(lowpassSwitch, highpassSwitch, notchSwitch,
lowpassFreqInput, highpassFreqInput, notchFreqInput)
dialog.dismiss()
currentDialog = null
} catch (e: Exception) {
Log.e("FullscreenFilterManager", "应用滤波器设置失败: ${e.message}")
}
}
// 设置对话框取消监听器
dialog.setOnCancelListener {
Log.d("FullscreenFilterManager", "对话框被取消")
currentDialog = null
}
dialog.show()
} catch (e: Exception) {
Log.e("FullscreenFilterManager", "显示滤波器对话框失败: ${e.message}", e)
}
}
/**
* 设置对话框的当前状态
*/
private fun setupDialogCurrentState(
lowpassSwitch: Switch,
highpassSwitch: Switch,
notchSwitch: Switch,
lowpassFreqInput: EditText,
highpassFreqInput: EditText,
notchFreqInput: EditText
) {
// 设置开关状态
lowpassSwitch.isChecked = lowpassFilter.enabled
highpassSwitch.isChecked = highpassFilter.enabled
notchSwitch.isChecked = notchFilter.enabled
// 设置频率值
lowpassFreqInput.setText(lowpassFilter.frequency.toInt().toString())
highpassFreqInput.setText(highpassFilter.frequency.toInt().toString())
notchFreqInput.setText(notchFilter.frequency.toInt().toString())
}
/**
* 从对话框应用滤波器设置
*/
private fun applyFilterFromDialog(
lowpassSwitch: Switch,
highpassSwitch: Switch,
notchSwitch: Switch,
lowpassFreqInput: EditText,
highpassFreqInput: EditText,
notchFreqInput: EditText
) {
try {
// 获取开关状态
val lowpassEnabled = lowpassSwitch.isChecked
val highpassEnabled = highpassSwitch.isChecked
val notchEnabled = notchSwitch.isChecked
// 获取频率值并验证
val lowpassFreq = parseFrequency(lowpassFreqInput.text.toString(), 40.0)
val highpassFreq = parseFrequency(highpassFreqInput.text.toString(), 1.0)
val notchFreq = parseFrequency(notchFreqInput.text.toString(), 50.0)
// 更新滤波器状态
lowpassFilter.enabled = lowpassEnabled
lowpassFilter.frequency = lowpassFreq
highpassFilter.enabled = highpassEnabled
highpassFilter.frequency = highpassFreq
notchFilter.enabled = notchEnabled
notchFilter.frequency = notchFreq
// 检查是否有任何滤波器启用
isAnyFilterEnabled = lowpassEnabled || highpassEnabled || notchEnabled
// 应用滤波器
applyCurrentFilter()
updateUIState()
Log.d("FullscreenFilterManager", "滤波器设置已应用: 低通=${lowpassEnabled}(${lowpassFreq}Hz), 高通=${highpassEnabled}(${highpassFreq}Hz), 陷波=${notchEnabled}(${notchFreq}Hz)")
} catch (e: Exception) {
Log.e("FullscreenFilterManager", "应用滤波器设置失败: ${e.message}", e)
}
}
/**
* 解析频率值
*/
private fun parseFrequency(freqText: String, defaultValue: Double): Double {
return try {
val freq = freqText.toDouble()
if (freq > 0 && freq <= 1000) freq else defaultValue
} catch (e: Exception) {
Log.w("FullscreenFilterManager", "频率解析失败: $freqText, 使用默认值: $defaultValue")
defaultValue
}
}
/**
* 更新UI状态
*/
private fun updateUIState() {
// 更新滤波器按键状态
if (isAnyFilterEnabled) {
filterButton?.setBackgroundColor(0xFF4CAF50.toInt())
val enabledFilters = mutableListOf<String>()
if (lowpassFilter.enabled) enabledFilters.add("低通")
if (highpassFilter.enabled) enabledFilters.add("高通")
if (notchFilter.enabled) enabledFilters.add("陷波")
filterButton?.text = "滤波器 ✓ (${enabledFilters.joinToString(",")})"
} else {
filterButton?.setBackgroundColor(0xFF666666.toInt())
filterButton?.text = "滤波器"
}
}
/**
* 应用当前滤波器
*/
private fun applyCurrentFilter() {
scope.launch(Dispatchers.Main) {
try {
if (isAnyFilterEnabled) {
// 应用滤波器
dataManager?.setFilterSettings(createFilterSettings())
Log.d("FullscreenFilterManager", "应用叠加滤波器: 低通=${lowpassFilter.enabled}, 高通=${highpassFilter.enabled}, 陷波=${notchFilter.enabled}")
} else {
// 关闭所有滤波器
dataManager?.setFilterSettings(FilterSettings())
Log.d("FullscreenFilterManager", "关闭所有滤波器")
}
} catch (e: Exception) {
Log.e("FullscreenFilterManager", "应用滤波器失败: ${e.message}")
}
}
}
/**
* 根据当前状态创建滤波器设置
*/
private fun createFilterSettings(): FilterSettings {
return FilterSettings(
sampleRate = 1000.0,
// 根据启用的滤波器设置参数
generalLowPass = if (lowpassFilter.enabled) lowpassFilter.frequency else 100.0,
generalHighPass = if (highpassFilter.enabled) highpassFilter.frequency else 0.1,
generalNotchFreq = if (notchFilter.enabled) notchFilter.frequency else 50.0,
generalNotchQ = if (notchFilter.enabled) 30.0 else 30.0
)
}
/**
* 处理实时数据并返回滤波后的数据优化内存管理
*/
fun processRealTimeData(dataType: String, data: List<Float>, callback: (List<Float>) -> Unit) {
try {
// 检查数据有效性
if (data.isEmpty()) {
Log.w("FullscreenFilterManager", "数据为空,跳过处理")
callback(emptyList())
return
}
// 检查数据是否包含异常值
val hasInvalidValues = data.any { it.isNaN() || it.isInfinite() }
if (hasInvalidValues) {
Log.w("FullscreenFilterManager", "数据包含异常值,使用原始数据")
callback(data)
return
}
if (!isAnyFilterEnabled) {
// 无滤波时直接使用原始数据,不进行任何处理
callback(data)
return
}
// 内存管理:限制单次处理的数据量(仅在滤波时)
val dataToProcess = if (data.size > maxDataPointsPerBatch) {
data.takeLast(maxDataPointsPerBatch) // 只处理最新的数据点
} else {
data
}
// 应用滤波器处理数据
scope.launch(Dispatchers.Default) {
try {
// 定期清理内存
cleanupMemory()
val filteredData = applyOverlayFiltersToData(dataToProcess)
scope.launch(Dispatchers.Main) {
try {
callback(filteredData)
} catch (e: Exception) {
Log.e("FullscreenFilterManager", "回调执行失败: ${e.message}", e)
}
}
} catch (e: Exception) {
Log.e("FullscreenFilterManager", "数据处理失败: ${e.message}", e)
// 出错时使用原始数据
scope.launch(Dispatchers.Main) {
try {
callback(dataToProcess)
} catch (e2: Exception) {
Log.e("FullscreenFilterManager", "回调执行也失败: ${e2.message}", e2)
}
}
}
}
} catch (e: Exception) {
Log.e("FullscreenFilterManager", "processRealTimeData异常: ${e.message}", e)
try {
callback(data)
} catch (e2: Exception) {
Log.e("FullscreenFilterManager", "最终回调也失败: ${e2.message}", e2)
}
}
}
/**
* 处理实时数据兼容旧接口
*/
fun processRealTimeData(dataType: String, data: List<Float>) {
processRealTimeData(dataType, data) { filteredData ->
updateChartWithData(filteredData)
}
}
/**
* 更新图表数据
*/
private fun updateChartWithData(data: List<Float>) {
try {
val charts = chartManager?.getCharts()
if (charts != null && charts.isNotEmpty()) {
val chart = charts[0]
chart.updateData(data)
}
} catch (e: Exception) {
Log.e("FullscreenFilterManager", "更新图表数据失败: ${e.message}")
}
}
/**
* 对数据进行叠加滤波处理
*/
private fun applyOverlayFiltersToData(data: List<Float>): List<Float> {
if (data.isEmpty()) return data
try {
var result = data
// 按顺序应用启用的滤波器
// 1. 高通滤波(去除基线漂移)
if (highpassFilter.enabled && highpassFilter.frequency > 0) {
try {
result = applyHighPassFilter(result, highpassFilter.frequency)
Log.d("FullscreenFilterManager", "高通滤波完成: ${result.size}个数据点")
} catch (e: Exception) {
Log.e("FullscreenFilterManager", "高通滤波失败: ${e.message}", e)
// 高通滤波失败时跳过,继续处理其他滤波器
}
}
// 2. 低通滤波(去除高频噪声)
if (lowpassFilter.enabled && lowpassFilter.frequency > 0) {
try {
result = applyLowPassFilter(result, lowpassFilter.frequency)
Log.d("FullscreenFilterManager", "低通滤波完成: ${result.size}个数据点")
} catch (e: Exception) {
Log.e("FullscreenFilterManager", "低通滤波失败: ${e.message}", e)
// 低通滤波失败时跳过,继续处理其他滤波器
}
}
// 3. 陷波滤波(去除工频干扰)
if (notchFilter.enabled && notchFilter.frequency > 0) {
try {
result = applyNotchFilter(result, notchFilter.frequency)
Log.d("FullscreenFilterManager", "陷波滤波完成: ${result.size}个数据点")
} catch (e: Exception) {
Log.e("FullscreenFilterManager", "陷波滤波失败: ${e.message}", e)
// 陷波滤波失败时跳过
}
}
// 最终检查:确保结果数据有效
if (result.isEmpty()) {
Log.w("FullscreenFilterManager", "滤波后数据为空,返回原始数据")
return data
}
// 检查是否有异常值
val hasInvalidValues = result.any { it.isNaN() || it.isInfinite() }
if (hasInvalidValues) {
Log.w("FullscreenFilterManager", "滤波后数据包含异常值,返回原始数据")
return data
}
return result
} catch (e: Exception) {
Log.e("FullscreenFilterManager", "滤波处理异常: ${e.message}", e)
return data
}
}
/**
* 低通滤波简单移动平均 - 更稳定的算法
*/
private fun applyLowPassFilter(data: List<Float>, cutoffFreq: Double): List<Float> {
if (data.isEmpty()) return data
val result = mutableListOf<Float>()
val sampleRate = getSampleRate()
// 计算窗口大小,限制在合理范围内
val windowSize = (sampleRate / cutoffFreq).toInt().coerceIn(3, 15)
// 使用简单的移动平均,避免复杂的边界处理
for (i in data.indices) {
val start = maxOf(0, i - windowSize / 2)
val end = minOf(data.size, i + windowSize / 2 + 1)
var sum = 0f
var count = 0
for (j in start until end) {
val value = data[j]
if (!value.isNaN() && !value.isInfinite()) {
sum += value
count++
}
}
val average = if (count > 0) sum / count else data[i]
result.add(average.coerceIn(-1000f, 1000f))
}
return result
}
/**
* 高通滤波简单差分滤波 - 更稳定的算法
*/
private fun applyHighPassFilter(data: List<Float>, cutoffFreq: Double): List<Float> {
if (data.isEmpty()) return data
val result = mutableListOf<Float>()
// 使用简单的差分滤波,去除基线漂移
// 计算移动平均作为基线
val sampleRate = getSampleRate()
val baselineWindow = (sampleRate / cutoffFreq).toInt().coerceIn(5, 25)
for (i in data.indices) {
val start = maxOf(0, i - baselineWindow / 2)
val end = minOf(data.size, i + baselineWindow / 2 + 1)
var sum = 0f
var count = 0
for (j in start until end) {
val value = data[j]
if (!value.isNaN() && !value.isInfinite()) {
sum += value
count++
}
}
val baseline = if (count > 0) sum / count else 0f
val highPassValue = data[i] - baseline
result.add(highPassValue.coerceIn(-1000f, 1000f))
}
return result
}
/**
* 陷波滤波简单陷波滤波 - 更稳定的算法
*/
private fun applyNotchFilter(data: List<Float>, notchFreq: Double): List<Float> {
if (data.isEmpty()) return data
val result = mutableListOf<Float>()
val sampleRate = getSampleRate()
// 使用简单的陷波滤波:检测特定频率的周期性信号并去除
val period = (sampleRate / notchFreq).toInt().coerceIn(2, 20)
for (i in data.indices) {
val currentValue = data[i]
// 计算周围几个周期的平均值
var sum = 0f
var count = 0
for (offset in -period..period) {
val index = i + offset
if (index >= 0 && index < data.size) {
val value = data[index]
if (!value.isNaN() && !value.isInfinite()) {
sum += value
count++
}
}
}
val average = if (count > 0) sum / count else currentValue
// 简单的陷波:如果当前值与平均值差异很大,则使用平均值
val diff = kotlin.math.abs(currentValue - average)
val threshold = kotlin.math.abs(average) * 0.1f // 10%的阈值
val filteredValue = if (diff > threshold) {
average * 0.8f + currentValue * 0.2f // 混合处理
} else {
currentValue
}
result.add(filteredValue.coerceIn(-1000f, 1000f))
}
return result
}
/**
* 获取当前滤波器状态信息
*/
fun getFilterStatus(): String {
return if (isAnyFilterEnabled) {
val enabledFilters = mutableListOf<String>()
if (lowpassFilter.enabled) enabledFilters.add("低通(${lowpassFilter.frequency.toInt()}Hz)")
if (highpassFilter.enabled) enabledFilters.add("高通(${highpassFilter.frequency.toInt()}Hz)")
if (notchFilter.enabled) enabledFilters.add("陷波(${notchFilter.frequency.toInt()}Hz)")
enabledFilters.joinToString(", ")
} else {
"无滤波"
}
}
/**
* 清理资源
*/
fun cleanup() {
try {
Log.d("FullscreenFilterManager", "=== cleanup 开始 ===")
// 清理对话框
try {
Log.d("FullscreenFilterManager", "开始清理对话框")
if (currentDialog != null) {
currentDialog?.dismiss()
currentDialog = null
Log.d("FullscreenFilterManager", "对话框清理完成")
} else {
Log.d("FullscreenFilterManager", "对话框为空,无需清理")
}
} catch (e: Exception) {
Log.e("FullscreenFilterManager", "关闭对话框失败: ${e.message}", e)
Log.e("FullscreenFilterManager", "对话框异常堆栈: ${e.stackTraceToString()}")
}
// 清理UI组件引用
Log.d("FullscreenFilterManager", "清理UI组件引用")
filterButton = null
// 清理数据缓存
Log.d("FullscreenFilterManager", "清理数据缓存")
try {
originalDataCache.clear()
Log.d("FullscreenFilterManager", "原始数据缓存清理完成")
} catch (e: Exception) {
Log.e("FullscreenFilterManager", "清理原始数据缓存失败: ${e.message}", e)
}
try {
filteredDataCache.clear()
Log.d("FullscreenFilterManager", "滤波数据缓存清理完成")
} catch (e: Exception) {
Log.e("FullscreenFilterManager", "清理滤波数据缓存失败: ${e.message}", e)
}
try {
dataBuffer.clear()
Log.d("FullscreenFilterManager", "数据缓冲区清理完成")
} catch (e: Exception) {
Log.e("FullscreenFilterManager", "清理数据缓冲区失败: ${e.message}", e)
}
// 注意scope是lifecycleScope会自动管理协程生命周期
Log.d("FullscreenFilterManager", "=== cleanup 完成 ===")
} catch (e: Exception) {
Log.e("FullscreenFilterManager", "清理资源时发生异常: ${e.message}", e)
Log.e("FullscreenFilterManager", "异常堆栈: ${e.stackTraceToString()}")
}
}
/**
* 添加原始数据到缓存
*/
private fun addToOriginalCache(key: String, data: List<Float>) {
try {
// 限制数据大小
val limitedData = if (data.size > maxCacheDataSize) {
data.takeLast(maxCacheDataSize).toMutableList()
} else {
data.toMutableList()
}
originalDataCache[key] = CacheEntry(limitedData)
cleanupCacheIfNeeded(originalDataCache, "原始数据")
} catch (e: Exception) {
Log.e("FullscreenFilterManager", "添加原始数据缓存失败: ${e.message}")
}
}
/**
* 添加滤波数据到缓存
*/
private fun addToFilteredCache(key: String, data: List<Float>) {
try {
// 限制数据大小
val limitedData = if (data.size > maxCacheDataSize) {
data.takeLast(maxCacheDataSize).toMutableList()
} else {
data.toMutableList()
}
filteredDataCache[key] = CacheEntry(limitedData)
cleanupCacheIfNeeded(filteredDataCache, "滤波数据")
} catch (e: Exception) {
Log.e("FullscreenFilterManager", "添加滤波数据缓存失败: ${e.message}")
}
}
/**
* 从原始数据缓存获取数据
*/
private fun getFromOriginalCache(key: String): List<Float>? {
val entry = originalDataCache[key]
return entry?.let {
it.accessCount++
it.data
}
}
/**
* 从滤波数据缓存获取数据
*/
private fun getFromFilteredCache(key: String): List<Float>? {
val entry = filteredDataCache[key]
return entry?.let {
it.accessCount++
it.data
}
}
/**
* 清理缓存如果需要
*/
private fun cleanupCacheIfNeeded(cache: MutableMap<String, CacheEntry>, cacheName: String) {
try {
// 检查条目数量
if (cache.size > maxCacheEntries) {
// 按访问频率和时间排序,保留最常用的
val sortedEntries = cache.entries.sortedWith(compareBy(
{ -it.value.accessCount }, // 访问次数多的优先保留
{ -it.value.timestamp } // 最近访问的优先保留
))
// 保留前80%的条目
val keepCount = (maxCacheEntries * 0.8).toInt()
val entriesToRemove = sortedEntries.drop(keepCount)
entriesToRemove.forEach { (key, _) ->
cache.remove(key)
}
Log.d("FullscreenFilterManager", "${cacheName}缓存清理: 保留 $keepCount 条,移除 ${entriesToRemove.size}")
}
// 清理过期条目
val currentTime = System.currentTimeMillis()
val expiredKeys = cache.filter { (_, entry) ->
currentTime - entry.timestamp > cacheExpirationTime
}.keys.toList()
expiredKeys.forEach { key ->
cache.remove(key)
}
if (expiredKeys.isNotEmpty()) {
Log.d("FullscreenFilterManager", "${cacheName}缓存清理: 移除 ${expiredKeys.size} 个过期条目")
}
} catch (e: Exception) {
Log.e("FullscreenFilterManager", "${cacheName}缓存清理失败: ${e.message}")
}
}
/**
* 定期清理内存防止长时间运行导致内存泄漏
*/
internal fun cleanupMemory() {
try {
// 清理数据缓冲区
if (dataBuffer.size > maxBufferSize) {
val excess = dataBuffer.size - maxBufferSize
repeat(excess) {
if (dataBuffer.isNotEmpty()) {
dataBuffer.removeAt(0)
}
}
Log.d("FullscreenFilterManager", "清理了 $excess 个数据点")
}
// 智能清理缓存
cleanupCacheIfNeeded(originalDataCache, "原始数据")
cleanupCacheIfNeeded(filteredDataCache, "滤波数据")
// 记录缓存状态
Log.d("FullscreenFilterManager", "缓存状态: 原始数据${originalDataCache.size}条,滤波数据${filteredDataCache.size}")
} catch (e: Exception) {
Log.e("FullscreenFilterManager", "内存清理失败: ${e.message}", e)
}
}
}

View File

@ -0,0 +1,157 @@
package com.example.cmake_project_test
import java.nio.ByteBuffer
import java.nio.ByteOrder
/**
* PPTM设备命令编码器
* 负责编码PPTM设备的命令数据包包含CRC16校验
*/
class PPTMCommandEncoder {
companion object {
private const val TAG = "PPTMCommandEncoder"
private const val MAX_PACKET_SIZE = 244
// PPTM协议操作码
const val OPCODE_START_SAMPLE = 0x0001
const val OPCODE_STOP_SAMPLE = 0x0001
// PPTM协议常量
const val COLLECTION_ON = 0x01
const val COLLECTION_OFF = 0x00
}
/**
* CRC-16-CCITT-FALSE校验
* @param source 数据源
* @param offset 偏移
* @param length 长度
* @return CRC校验值
*/
fun crc16(source: ByteArray, offset: Int, length: Int): Int {
var wCRCin = 0xFFFF
val wCPoly = 0x1021
for (i in offset until offset + length) {
for (j in 0..7) {
val bit = ((source[i].toInt() shr (7 - j)) and 1) == 1
val c15 = ((wCRCin shr 15) and 1) == 1
wCRCin = wCRCin shl 1
if (c15 xor bit) {
wCRCin = wCRCin xor wCPoly
}
}
}
return wCRCin and 0xFFFF
}
/**
* 编码命令数据
* @param opcode 操作码
* @param payload 负载数据
* @param payloadLen 负载长度
* @return 编码后的完整数据包
*/
fun encode(opcode: Short, payload: ByteArray, payloadLen: Short): ByteArray {
val buf = ByteBuffer.allocate(MAX_PACKET_SIZE)
buf.order(ByteOrder.LITTLE_ENDIAN)
// 写入操作码
buf.putShort(opcode)
// 写入负载长度
buf.putShort(payloadLen)
// 写入负载数据
buf.put(payload)
// 计算CRC16校验
val calcCrcDataLen = buf.position()
val crc = crc16(buf.array(), 0, calcCrcDataLen)
buf.putShort(crc.toShort())
// 获取完整数据包
val allDataLen = buf.position()
val dst = ByteArray(allDataLen)
buf.flip()
buf.get(dst, 0, allDataLen)
return dst
}
/**
* 创建PPTM开始采样命令
* @return 编码后的命令
*/
fun createPptmStartSampleCmd(): ByteArray {
val payload = byteArrayOf(
COLLECTION_ON.toByte(), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
)
return encode(OPCODE_START_SAMPLE.toShort(), payload, payload.size.toShort())
}
/**
* 创建PPTM停止采样命令
* @return 编码后的命令
*/
fun createPptmStopSampleCmd(): ByteArray {
val payload = byteArrayOf(
COLLECTION_OFF.toByte(), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
)
return encode(OPCODE_STOP_SAMPLE.toShort(), payload, payload.size.toShort())
}
/**
* 创建自定义PPTM命令
* @param opcode 操作码
* @param payload 负载数据
* @return 编码后的命令
*/
fun createCustomPptmCmd(opcode: Short, payload: ByteArray): ByteArray {
return encode(opcode, payload, payload.size.toShort())
}
/**
* 验证数据包CRC16校验
* @param packet 完整数据包
* @return 校验是否通过
*/
fun verifyCrc16(packet: ByteArray): Boolean {
if (packet.size < 6) return false // 至少需要操作码(2) + 长度(2) + CRC(2)
val dataLen = packet.size - 2 // 减去CRC16的2字节
val calculatedCrc = crc16(packet, 0, dataLen)
val packetCrc = ((packet[packet.size - 1].toInt() and 0xFF) shl 8) or
(packet[packet.size - 2].toInt() and 0xFF)
return calculatedCrc == packetCrc
}
/**
* 解析PPTM数据包
* @param packet 完整数据包
* @return 解析结果 (操作码, 负载数据) null
*/
fun parsePacket(packet: ByteArray): Pair<Short, ByteArray>? {
if (!verifyCrc16(packet)) {
return null
}
if (packet.size < 6) return null
val buf = ByteBuffer.wrap(packet)
buf.order(ByteOrder.LITTLE_ENDIAN)
val opcode = buf.short
val payloadLen = buf.short
if (packet.size < 4 + payloadLen + 2) return null // 操作码(2) + 长度(2) + 负载 + CRC(2)
val payload = ByteArray(payloadLen.toInt())
buf.get(payload)
return Pair(opcode, payload)
}
}

View File

@ -89,7 +89,7 @@ class StreamingSignalProcessor {
notchQ: Double notchQ: Double
): List<Float> { ): List<Float> {
if (processorId == -1L) { if (processorId == -1L) {
Log.w("StreamingSignalProcessor", "信号处理器未初始化") Log.w("StreamingSignalProcessor", "信号处理器未初始化,返回原始数据")
return newData return newData
} }
@ -105,33 +105,18 @@ class StreamingSignalProcessor {
// 当缓冲区有足够数据时进行窗口处理 // 当缓冲区有足够数据时进行窗口处理
while (dataBuffer.size >= windowSize) { while (dataBuffer.size >= windowSize) {
// 提取当前窗口数据过滤掉null值 val windowData = dataBuffer.take(windowSize).toFloatArray()
val windowDataList = dataBuffer.take(windowSize).filterNotNull()
if (windowDataList.size < windowSize) {
Log.w("StreamingSignalProcessor", "窗口数据包含null值跳过处理")
// 移除无效数据
repeat(stepSize) {
if (dataBuffer.isNotEmpty()) {
dataBuffer.removeAt(0)
}
}
continue
}
val windowData = windowDataList.toFloatArray()
Log.d("StreamingSignalProcessor", "提取窗口数据,长度: ${windowData.size}")
// 应用信号处理
val processedWindow = processWindowWithParameters(windowData, sampleRate, lowpassCutoff, notchFreq, notchQ) val processedWindow = processWindowWithParameters(windowData, sampleRate, lowpassCutoff, notchFreq, notchQ)
Log.d("StreamingSignalProcessor", "窗口处理完成,结果长度: ${processedWindow.size}")
// 只保留非重叠部分的结果 if (processedWindow.isNotEmpty()) {
val nonOverlapSize = stepSize processedSamples.addAll(processedWindow)
val nonOverlapData = processedWindow.take(nonOverlapSize) Log.d("StreamingSignalProcessor", "处理窗口成功,输出数据长度: ${processedWindow.size}")
processedSamples.addAll(nonOverlapData) } else {
Log.d("StreamingSignalProcessor", "添加非重叠数据,长度: ${nonOverlapData.size}") Log.w("StreamingSignalProcessor", "窗口处理失败,使用原始数据")
processedSamples.addAll(windowData.toList())
}
// 移除已处理的数据(保留重叠部分) // 移除已处理的数据
repeat(stepSize) { repeat(stepSize) {
if (dataBuffer.isNotEmpty()) { if (dataBuffer.isNotEmpty()) {
dataBuffer.removeAt(0) dataBuffer.removeAt(0)
@ -139,10 +124,7 @@ class StreamingSignalProcessor {
} }
} }
Log.d("StreamingSignalProcessor", "数据流处理完成,输出长度: ${processedSamples.size}") Log.d("StreamingSignalProcessor", "数据流处理完成,总共处理了 ${processedSamples.size} 个样本")
if (processedSamples.isNotEmpty()) {
Log.d("StreamingSignalProcessor", "输出数据前3个值: ${processedSamples.take(3).joinToString(", ")}")
}
return processedSamples return processedSamples
} }
@ -191,257 +173,133 @@ class StreamingSignalProcessor {
filtered = lowpassResult filtered = lowpassResult
Log.d("StreamingSignalProcessor", "低通滤波成功(${lowpassCutoff}Hz结果前3个值: ${filtered.take(3).joinToString(", ")}") Log.d("StreamingSignalProcessor", "低通滤波成功(${lowpassCutoff}Hz结果前3个值: ${filtered.take(3).joinToString(", ")}")
} }
} else {
Log.d("StreamingSignalProcessor", "跳过低通滤波低通截止频率为0")
} }
// 3. 陷波滤波(去除工频干扰)- 默认关闭,用户可手动开启 // 3. 陷波滤波(去除工频干扰)- 可选
if (notchFreq > 0) { if (notchFreq > 0) {
val notchFiltered = signalProcessor.notchFilter(filtered, sampleRate, notchFreq, notchQ) val notchResult = signalProcessor.notchFilter(filtered, sampleRate, notchFreq, notchQ)
if (notchFiltered == null) { if (notchResult == null) {
Log.w("StreamingSignalProcessor", "陷波滤波失败,使用低通滤波结果") Log.w("StreamingSignalProcessor", "陷波滤波失败,使用低通滤波结果")
Log.d("StreamingSignalProcessor", "返回低通滤波结果前3个值: ${filtered.take(3).joinToString(", ")}")
return filtered.toList()
} else { } else {
Log.d("StreamingSignalProcessor", "陷波滤波成功(${notchFreq}Hz, Q=${notchQ}结果前3个值: ${notchFiltered.take(3).joinToString(", ")}") filtered = notchResult
return notchFiltered.toList() Log.d("StreamingSignalProcessor", "陷波滤波成功(${notchFreq}Hz结果前3个值: ${filtered.take(3).joinToString(", ")}")
} }
} else { }
Log.d("StreamingSignalProcessor", "跳过陷波滤波(默认关闭),返回低通滤波结果")
Log.d("StreamingSignalProcessor", "窗口数据处理完成,最终结果长度: ${filtered.size}")
return filtered.toList() return filtered.toList()
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e("StreamingSignalProcessor", "窗口处理异常: ${e.message}") Log.e("StreamingSignalProcessor", "处理窗口数据异常: ${e.message}", e)
return windowData.toList() return windowData.toList() // 异常时返回原始数据
} }
} }
/**
* 根据数据类型获取合适的采样率
*/
private fun getSampleRateForDataType(dataType: type.SensorData.DataType): Double {
return when (dataType) {
type.SensorData.DataType.EEG -> 250.0
type.SensorData.DataType.ECG_2LEAD, type.SensorData.DataType.ECG_12LEAD, type.SensorData.DataType.PW_ECG_SL -> 250.0
type.SensorData.DataType.PPG -> 50.0
type.SensorData.DataType.STETHOSCOPE -> 8000.0
type.SensorData.DataType.SNORE -> 8000.0
type.SensorData.DataType.RESPIRATION -> 50.0
else -> 50.0 // 默认采样率
}
}
/**
* 根据数据类型获取合适的滤波器参数
* 返回格式: Triple<低通截止频率, 陷波频率, 陷波品质因数>
*/
private fun getFilterParametersForDataType(dataType: type.SensorData.DataType): Triple<Double, Double, Double> {
return when (dataType) {
type.SensorData.DataType.EEG -> {
// EEG: 低通40Hz, 陷波50Hz
Triple(40.0, 50.0, 30.0)
}
type.SensorData.DataType.ECG_2LEAD, type.SensorData.DataType.ECG_12LEAD, type.SensorData.DataType.PW_ECG_SL -> {
// ECG: 优化滤波参数,提高实时性
// 低通80Hz (减少计算量,保持信号质量)
// 陷波50Hz (中国工频)
// 品质因数5 (减少计算复杂度)
Triple(80.0, 50.0, 5.0)
}
type.SensorData.DataType.PPG -> {
// PPG: 低通10Hz, 陷波50Hz
Triple(10.0, 50.0, 30.0)
}
type.SensorData.DataType.MIT_BIH -> {
// MIT-BIH: 低通40Hz, 陷波50Hz
Triple(40.0, 50.0, 30.0)
}
type.SensorData.DataType.STETHOSCOPE -> {
// 听诊器: 低通4000Hz, 陷波50Hz
Triple(4000.0, 50.0, 30.0)
}
type.SensorData.DataType.SNORE -> {
// 鼾声: 低通4000Hz, 陷波50Hz
Triple(4000.0, 50.0, 30.0)
}
type.SensorData.DataType.RESPIRATION -> {
// 呼吸: 低通1Hz, 不陷波
Triple(1.0, 0.0, 0.0)
}
else -> {
// 默认参数
Triple(40.0, 50.0, 30.0)
}
}
}
/**
* 处理传感器数据包
*/
fun processSensorData(sensorData: SensorData): SensorData {
val channelData = sensorData.getChannelData()
if (channelData == null || channelData.isEmpty()) {
return sensorData
}
try {
// 根据数据类型设置采样率和滤波器参数
val dataType = sensorData.getDataType()
val sampleRate = getSampleRateForDataType(dataType)
val (lowpassCutoff, notchFreq, notchQ) = getFilterParametersForDataType(dataType)
Log.d("StreamingSignalProcessor", "处理数据类型: $dataType, 采样率: ${sampleRate}Hz, 低通: ${lowpassCutoff}Hz, 陷波: ${notchFreq}Hz")
// 处理每个通道
val processedChannels = channelData.map { channel ->
val channelData = channel.toFloatArray()
val processedData = processStreamingDataWithParameters(channelData.toList(), sampleRate, lowpassCutoff, notchFreq, notchQ)
processedData.toFloatArray()
}
// 创建处理后的传感器数据
val processedSensorData = SensorData()
processedSensorData.setDataType(sensorData.getDataType())
processedSensorData.setTimestamp(sensorData.getTimestamp())
processedSensorData.setPacketSn(sensorData.getPacketSn())
processedSensorData.setChannelData(processedChannels.map { it.toList() })
return processedSensorData
} catch (e: Exception) {
Log.e("StreamingSignalProcessor", "处理传感器数据异常: ${e.message}")
return sensorData
}
}
/**
* 批量处理传感器数据包
*/
fun processSensorDataList(sensorDataList: List<SensorData>): List<SensorData> {
return sensorDataList.map { processSensorData(it) }
}
/** /**
* 获取缓冲区状态 * 获取缓冲区状态
*/ */
fun getBufferStatus(): Map<String, Int> { fun getBufferStatus(): Map<String, Int> {
return mapOf( return mapOf(
"buffer_size" to dataBuffer.size, "dataBufferSize" to dataBuffer.size,
"window_size" to windowSize, "processedDataSize" to processedData.size,
"overlap_size" to overlapSize, "windowSize" to windowSize,
"step_size" to stepSize "overlapSize" to overlapSize,
"stepSize" to stepSize
) )
} }
/** /**
* 清空缓冲区 * 计算信号质量
*/ */
fun clearBuffer() {
dataBuffer.clear()
processedData.clear()
lastProcessedIndex = 0
Log.d("StreamingSignalProcessor", "缓冲区已清空")
}
/**
* 设置窗口参数
*/
fun setWindowParameters(windowSize: Int, overlapSize: Int) {
this.windowSize = windowSize
this.overlapSize = overlapSize
this.stepSize = windowSize - overlapSize
Log.d("StreamingSignalProcessor", "窗口参数已更新: 窗口大小=$windowSize, 重叠大小=$overlapSize")
}
/**
* 设置滤波器参数
*/
fun setFilterParameters(lowpassCutoff: Double, notchFreq: Double, notchQ: Double) {
this.lowpassCutoff = lowpassCutoff
this.notchFreq = notchFreq
this.notchQ = notchQ
Log.d("StreamingSignalProcessor", "滤波器参数已更新")
}
/**
* 清理资源
*/
fun cleanup() {
try {
if (processorId != -1L) {
signalProcessor.destroyProcessor()
processorId = -1L
}
clearBuffer()
Log.d("StreamingSignalProcessor", "流式信号处理器资源已清理")
} catch (e: Exception) {
Log.e("StreamingSignalProcessor", "清理资源时发生错误: ${e.message}")
}
}
// 委托给SignalProcessorJNI的方法
fun normalizeAmplitude(signal: FloatArray): FloatArray? {
if (signalProcessorInitialized) {
signalProcessor.normalizeAmplitude(signal)
return signal // normalizeAmplitude修改原数组返回原数组
} else {
Log.w("StreamingSignalProcessor", "信号处理器未初始化,返回原始信号")
return signal
}
}
fun bandpassFilter(signal: FloatArray, sampleRate: Float, lowFreq: Float, highFreq: Float): FloatArray? {
return if (signalProcessorInitialized) {
signalProcessor.bandpassFilter(signal, sampleRate.toDouble(), lowFreq.toDouble(), highFreq.toDouble())
} else {
Log.w("StreamingSignalProcessor", "信号处理器未初始化,返回原始信号")
signal
}
}
fun highpassFilter(signal: FloatArray, sampleRate: Float, cutoffFreq: Float): FloatArray? {
return if (signalProcessorInitialized) {
signalProcessor.highpassFilter(signal, sampleRate.toDouble(), cutoffFreq.toDouble())
} else {
Log.w("StreamingSignalProcessor", "信号处理器未初始化,返回原始信号")
signal
}
}
fun lowpassFilter(signal: FloatArray, sampleRate: Float, cutoffFreq: Float): FloatArray? {
return if (signalProcessorInitialized) {
signalProcessor.lowpassFilter(signal, sampleRate.toDouble(), cutoffFreq.toDouble())
} else {
Log.w("StreamingSignalProcessor", "信号处理器未初始化,返回原始信号")
signal
}
}
fun notchFilter(signal: FloatArray, sampleRate: Float, notchFreq: Float): FloatArray? {
return if (signalProcessorInitialized) {
signalProcessor.notchFilter(signal, sampleRate.toDouble(), notchFreq.toDouble())
} else {
Log.w("StreamingSignalProcessor", "信号处理器未初始化,返回原始信号")
signal
}
}
fun processRealtimeChunk(signal: FloatArray, sampleRate: Float): FloatArray? {
return if (signalProcessorInitialized) {
signalProcessor.processRealtimeChunk(signal, sampleRate.toDouble())
} else {
Log.w("StreamingSignalProcessor", "信号处理器未初始化,返回原始信号")
signal
}
}
fun calculateSignalQuality(signal: FloatArray): Float { fun calculateSignalQuality(signal: FloatArray): Float {
return if (signalProcessorInitialized) { if (signal.isEmpty()) return 0.0f
signalProcessor.calculateSignalQuality(signal)
} else { try {
Log.w("StreamingSignalProcessor", "信号处理器未初始化,返回默认质量值") // 简单的信号质量评估:基于信号幅度和噪声水平
0.0f val mean = signal.average().toFloat()
val variance = signal.map { (it - mean) * (it - mean) }.average().toFloat()
val stdDev = kotlin.math.sqrt(variance)
// 信号质量评分0-1
val quality = kotlin.math.min(1.0f, stdDev / 100.0f)
return quality
} catch (e: Exception) {
Log.e("StreamingSignalProcessor", "计算信号质量失败: ${e.message}")
return 0.0f
}
}
/**
* 带通滤波
*/
fun bandpassFilter(signal: FloatArray, sampleRate: Float, lowCutoff: Float, highCutoff: Float): FloatArray? {
return try {
signalProcessor.bandpassFilter(signal, sampleRate.toDouble(), lowCutoff.toDouble(), highCutoff.toDouble())
} catch (e: Exception) {
Log.e("StreamingSignalProcessor", "带通滤波失败: ${e.message}")
null
}
}
/**
* 高通滤波
*/
fun highpassFilter(signal: FloatArray, sampleRate: Float, cutoff: Float): FloatArray? {
return try {
signalProcessor.highpassFilter(signal, sampleRate.toDouble(), cutoff.toDouble())
} catch (e: Exception) {
Log.e("StreamingSignalProcessor", "高通滤波失败: ${e.message}")
null
}
}
/**
* 低通滤波
*/
fun lowpassFilter(signal: FloatArray, sampleRate: Float, cutoff: Float): FloatArray? {
return try {
signalProcessor.lowpassFilter(signal, sampleRate.toDouble(), cutoff.toDouble())
} catch (e: Exception) {
Log.e("StreamingSignalProcessor", "低通滤波失败: ${e.message}")
null
}
}
/**
* 陷波滤波
*/
fun notchFilter(signal: FloatArray, sampleRate: Float, frequency: Float, q: Float): FloatArray? {
return try {
signalProcessor.notchFilter(signal, sampleRate.toDouble(), frequency.toDouble(), q.toDouble())
} catch (e: Exception) {
Log.e("StreamingSignalProcessor", "陷波滤波失败: ${e.message}")
null
}
}
/**
* 幅度归一化
*/
fun normalizeAmplitude(signal: FloatArray): FloatArray? {
return try {
val result = signal.copyOf()
signalProcessor.normalizeAmplitude(result)
result
} catch (e: Exception) {
Log.e("StreamingSignalProcessor", "幅度归一化失败: ${e.message}")
null
}
}
/**
* 实时处理数据块
*/
fun processRealtimeChunk(signal: FloatArray, sampleRate: Float): FloatArray? {
return try {
signalProcessor.processRealtimeChunk(signal, sampleRate.toDouble())
} catch (e: Exception) {
Log.e("StreamingSignalProcessor", "实时处理失败: ${e.message}")
null
} }
} }
} }

View File

@ -0,0 +1,146 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#FFFFFF">
<!-- 全屏图表容器 - 直接占满整个屏幕 -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFF">
<!-- 生命体征显示区域 - 浮动在左上角 -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|start"
android:layout_margin="16dp"
android:orientation="vertical"
android:background="#80000000"
android:padding="12dp"
android:elevation="4dp">
<!-- 心率显示 -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="4dp">
<TextView
android:id="@+id/heart_rate_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="心率 (bpm)"
android:textColor="#FFFFFF"
android:textSize="12sp"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/heart_rate_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="--"
android:textColor="#00FF00"
android:textSize="14sp"
android:textStyle="bold" />
</LinearLayout>
<!-- 血氧显示 -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="4dp">
<TextView
android:id="@+id/spo2_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="血氧 (%)"
android:textColor="#FFFFFF"
android:textSize="12sp"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/spo2_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="--"
android:textColor="#0080FF"
android:textSize="14sp"
android:textStyle="bold" />
</LinearLayout>
<!-- 体温显示 -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/temperature_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="体温 (°C)"
android:textColor="#FFFFFF"
android:textSize="12sp"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/temperature_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="--"
android:textColor="#FF4040"
android:textSize="14sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
<!-- 滤波器按键 - 浮动在右上角 -->
<Button
android:id="@+id/filter_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="滤波器"
android:textSize="12sp"
android:background="@drawable/button_background"
android:textColor="#FFFFFF"
android:padding="8dp"
android:layout_gravity="top|end"
android:layout_margin="16dp" />
<!-- 退出全屏按钮 - 浮动在右上角,位于滤波器按键下方 -->
<Button
android:id="@+id/exit_fullscreen_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="退出全屏"
android:textSize="12sp"
android:background="@drawable/button_background"
android:textColor="#FFFFFF"
android:padding="8dp"
android:layout_gravity="top|end"
android:layout_marginTop="60dp"
android:layout_marginEnd="16dp" />
<!-- 可滚动的全屏图表容器 -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:id="@+id/fullscreen_charts_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="0dp">
<!-- 全屏图表将在这里动态添加 -->
</LinearLayout>
</ScrollView>
</FrameLayout>
</LinearLayout>

View File

@ -111,11 +111,43 @@
</LinearLayout> </LinearLayout>
<!-- 第四行按钮 - 图表控制和调试 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="4dp">
<Button
android:id="@+id/reset_charts_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:text="重置图表"
android:textSize="14sp"
android:background="@drawable/button_background"
android:textColor="#FFFFFF"
android:enabled="false" />
<Button
android:id="@+id/debug_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:text="调试"
android:textSize="14sp"
android:background="@drawable/button_background"
android:textColor="#FFFFFF" />
</LinearLayout> </LinearLayout>
<!-- ECG图表区域 --> </LinearLayout>
<!-- 动态图表区域 -->
<LinearLayout <LinearLayout
android:id="@+id/ecg_chart_container" android:id="@+id/dynamic_chart_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="0.7" android:layout_weight="0.7"
@ -132,38 +164,37 @@
android:background="#E0E0E0"> android:background="#E0E0E0">
<TextView <TextView
android:id="@+id/chart_title"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:text="ECG实时监测" android:text="生理信号实时监测"
android:textSize="16sp" android:textSize="16sp"
android:textStyle="bold" android:textStyle="bold"
android:textColor="#333333" android:textColor="#333333"
android:gravity="center" /> android:gravity="center" />
</LinearLayout> </LinearLayout>
<!-- ECG节律视图 --> <!-- 可滚动的图表容器 -->
<com.example.cmake_project_test.ECGRhythmView <ScrollView
android:id="@+id/ecg_rhythm_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="match_parent"
android:layout_weight="0.5" android:fillViewport="true">
android:background="#F8F8F8" />
<!-- 分隔线 --> <LinearLayout
<View android:id="@+id/charts_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1dp" android:layout_height="wrap_content"
android:background="#CCCCCC" /> android:orientation="vertical"
android:padding="4dp">
<!-- ECG波形视图 --> <!-- 图表将在这里动态添加 -->
<com.example.cmake_project_test.ECGWaveformView
android:id="@+id/ecg_waveform_view" </LinearLayout>
android:layout_width="match_parent"
android:layout_height="0dp" </ScrollView>
android:layout_weight="0.5"
android:background="#F0F0F0" />
</LinearLayout> </LinearLayout>

View File

@ -0,0 +1,295 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxHeight="600dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp"
android:background="#FFFFFF">
<!-- 对话框标题 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="滤波器设置"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#333333"
android:gravity="center"
android:layout_marginBottom="20dp" />
<!-- 滤波器说明 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="可以选择多个滤波器同时应用,滤波器会按顺序叠加处理"
android:textSize="12sp"
android:textColor="#666666"
android:gravity="center"
android:layout_marginBottom="20dp" />
<!-- 低通滤波器设置 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="15dp"
android:background="#F5F5F5"
android:padding="15dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="10dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="低通滤波"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="#333333" />
<Switch
android:id="@+id/lowpass_enable_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="去除高频噪声,保留低频信号"
android:textSize="12sp"
android:textColor="#666666"
android:layout_marginBottom="10dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="截止频率:"
android:textSize="12sp"
android:textColor="#666666"
android:layout_marginEnd="10dp" />
<EditText
android:id="@+id/lowpass_frequency_input"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="40"
android:textSize="14sp"
android:inputType="numberDecimal"
android:background="@android:drawable/edit_text"
android:padding="8dp"
android:layout_marginEnd="5dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hz"
android:textSize="12sp"
android:textColor="#666666" />
</LinearLayout>
</LinearLayout>
<!-- 高通滤波器设置 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="15dp"
android:background="#F5F5F5"
android:padding="15dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="10dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="高通滤波"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="#333333" />
<Switch
android:id="@+id/highpass_enable_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="去除基线漂移,保留高频信号"
android:textSize="12sp"
android:textColor="#666666"
android:layout_marginBottom="10dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="截止频率:"
android:textSize="12sp"
android:textColor="#666666"
android:layout_marginEnd="10dp" />
<EditText
android:id="@+id/highpass_frequency_input"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="1"
android:textSize="14sp"
android:inputType="numberDecimal"
android:background="@android:drawable/edit_text"
android:padding="8dp"
android:layout_marginEnd="5dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hz"
android:textSize="12sp"
android:textColor="#666666" />
</LinearLayout>
</LinearLayout>
<!-- 50Hz陷波滤波器设置 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="20dp"
android:background="#F5F5F5"
android:padding="15dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="10dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="50Hz陷波滤波"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="#333333" />
<Switch
android:id="@+id/notch_enable_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="去除工频干扰50Hz及其谐波"
android:textSize="12sp"
android:textColor="#666666"
android:layout_marginBottom="10dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="陷波频率:"
android:textSize="12sp"
android:textColor="#666666"
android:layout_marginEnd="10dp" />
<EditText
android:id="@+id/notch_frequency_input"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="50"
android:textSize="14sp"
android:inputType="numberDecimal"
android:background="@android:drawable/edit_text"
android:padding="8dp"
android:layout_marginEnd="5dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hz"
android:textSize="12sp"
android:textColor="#666666" />
</LinearLayout>
</LinearLayout>
<!-- 按钮区域 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<Button
android:id="@+id/btn_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="取消"
android:textSize="14sp"
android:background="@drawable/button_background"
android:textColor="#666666"
android:padding="12dp"
android:layout_marginEnd="10dp" />
<Button
android:id="@+id/btn_apply"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="应用"
android:textSize="14sp"
android:background="@drawable/button_background"
android:textColor="#FFFFFF"
android:padding="12dp" />
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@ -1,3 +1,4 @@
<resources> <resources>
<string name="app_name">Cmake_project_test</string> <string name="app_name">生理信号监测</string>
<string name="app_description">多设备生理信号实时监测与分析应用</string>
</resources> </resources>

View File

@ -13,4 +13,21 @@
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item> <item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. --> <!-- Customize your theme here. -->
</style> </style>
<!-- 全屏主题 -->
<style name="Theme.Cmake_project_test.NoActionBar" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">@android:color/black</item>
<!-- 全屏模式 -->
<item name="android:windowFullscreen">true</item>
<item name="android:windowNoTitle">true</item>
</style>
</resources> </resources>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path name="my_files" path="." />
<cache-path name="my_cache" path="." />
</paths>

View File

@ -1,141 +0,0 @@
# 应用运行状态总结
## ✅ 编译和安装状态
### 编译成功
- **编译命令**: `./gradlew assembleDebug`
- **状态**: ✅ 成功
- **错误**: 无
### 安装成功
- **安装命令**: `./gradlew installDebug`
- **状态**: ✅ 成功
- **错误**: 无
### 代码质量检查
- **Lint检查**: `./gradlew lintDebug`
- **状态**: ✅ 通过
- **警告**: 无
## 🔧 修复的问题
### 1. 缺少Build类导入
**问题**: MainActivity中使用`Build.VERSION.SDK_INT`但未导入Build类
**修复**: 添加了`import android.os.Build`
**状态**: ✅ 已修复
### 2. 权限配置优化
**问题**: 权限配置不够完善特别是Android 12+的支持
**修复**:
- 更新了AndroidManifest.xml权限声明
- 添加了动态权限检查
- 增强了权限状态显示
**状态**: ✅ 已修复
### 3. 蓝牙扫描功能增强
**问题**: 扫描功能不够详细,缺少错误处理
**修复**:
- 延长扫描时间到15秒
- 添加详细的设备信息显示
- 增强错误处理和状态提示
**状态**: ✅ 已修复
## 📱 应用功能状态
### 核心功能
- [x] **蓝牙权限管理** - 完整的权限检查和请求
- [x] **蓝牙设备扫描** - 支持BLE和传统蓝牙扫描
- [x] **设备连接管理** - 连接、断开、状态监控
- [x] **数据收发功能** - 支持各种指令发送
- [x] **UI状态显示** - 详细的状态信息和提示
### 权限支持
- [x] **Android 11及以下** - 需要位置权限
- [x] **Android 12+** - 使用新蓝牙权限模型
- [x] **动态权限检查** - 根据Android版本自动适配
- [x] **权限状态显示** - 详细的权限状态反馈
### 蓝牙功能
- [x] **BLE扫描** - 低功耗蓝牙设备扫描
- [x] **传统蓝牙扫描** - 经典蓝牙设备扫描
- [x] **设备连接** - 支持直接MAC地址连接
- [x] **服务发现** - 自动发现蓝牙服务和特征
- [x] **数据通道** - 建立数据收发通道
## 🎯 目标设备支持
### 目标设备: `A4:C3:37:86:9F:73`
- [x] **MAC地址验证** - 支持多种格式验证
- [x] **直接连接** - 长按蓝牙按钮直接连接
- [x] **设备识别** - 扫描时自动识别目标设备
- [x] **连接重试** - 超时后自动重试连接
## 📊 测试建议
### 1. 权限测试
1. **启动应用**
2. **观察权限状态显示**
3. **授予必要权限**
4. **确认权限状态更新**
### 2. 蓝牙扫描测试
1. **点击"连接蓝牙"按钮**
2. **观察扫描过程状态**
3. **检查设备发现信息**
4. **验证目标设备识别**
### 3. 连接测试
1. **长按"连接蓝牙"按钮**
2. **输入目标MAC地址**
3. **观察连接过程**
4. **验证连接状态**
### 4. 数据收发测试
1. **建立连接后**
2. **点击"发送指令"按钮**
3. **测试各种指令发送**
4. **验证数据接收**
## 🚀 下一步操作
### 立即测试
1. **在目标设备上启动应用**
2. **检查权限授予情况**
3. **测试蓝牙扫描功能**
4. **尝试连接目标设备**
### 如果遇到问题
1. **查看应用状态信息**
2. **检查Android Studio Logcat**
3. **参考故障排除指南**
4. **提供详细错误信息**
## 📋 技术规格
### 编译环境
- **Gradle版本**: 8.0+
- **Android Gradle Plugin**: 8.0+
- **目标SDK**: 34
- **最低SDK**: 21
### 权限要求
- **BLUETOOTH_SCAN** - 蓝牙扫描权限
- **BLUETOOTH_CONNECT** - 蓝牙连接权限
- **ACCESS_FINE_LOCATION** - 精确位置权限Android 11及以下
- **ACCESS_COARSE_LOCATION** - 粗略位置权限Android 11及以下
### 蓝牙支持
- **BLE (Bluetooth Low Energy)** - 低功耗蓝牙
- **传统蓝牙** - 经典蓝牙协议
- **双模支持** - 自动检测和切换
## 🎉 总结
应用已成功编译、安装并修复了所有已知问题。主要功能包括:
1. **完整的权限管理** - 支持所有Android版本的权限模型
2. **强大的蓝牙功能** - 扫描、连接、数据收发
3. **友好的用户界面** - 详细的状态信息和提示
4. **目标设备支持** - 专门针对目标设备的优化
应用现在可以正常运行,建议立即在目标设备上进行测试!

View File

@ -1,116 +0,0 @@
# 快速故障排除指南
## 当前问题客户端找不到SerialTest服务器
### 立即检查清单
#### ✅ 手机端检查
- [ ] 蓝牙已开启
- [ ] 蓝牙可见性设置为"始终可见"
- [ ] 已清除蓝牙缓存
- [ ] 重启过蓝牙服务
#### ✅ 电脑端检查
- [ ] SerialTest正在运行
- [ ] 选择了正确的COM端口
- [ ] 启用了蓝牙服务器模式
- [ ] 电脑蓝牙已开启
- [ ] 电脑蓝牙设置为"可发现"
#### ✅ 环境检查
- [ ] 设备距离在10米内
- [ ] 远离其他蓝牙设备干扰
- [ ] 没有金属屏蔽物
### 快速测试步骤
#### 步骤1基础扫描测试
1. **启动Android应用**
2. **点击"连接蓝牙"按钮**(普通点击)
3. **观察扫描结果**
- 是否显示"🔍 开始扫描蓝牙设备..."
- 是否显示"📡 使用BLE扫描模式"或"📡 使用传统蓝牙扫描模式"
- 是否发现任何设备
#### 步骤2直接连接测试
1. **长按"连接蓝牙"按钮**
2. **输入MAC地址**`60:E9:AA:30:8B:0A`
3. **点击连接**
4. **观察连接状态**
#### 步骤3系统蓝牙测试
1. **打开手机蓝牙设置**
2. **扫描设备**
3. **查看是否发现您的电脑设备**
4. **如果发现,尝试配对**
### 常见问题及解决方案
#### 问题1扫描不到任何设备
**可能原因**
- 目标设备蓝牙未开启
- 设备不在可见范围内
- 蓝牙服务异常
**解决方案**
1. 重启手机蓝牙
2. 重启电脑蓝牙
3. 检查设备距离
4. 使用系统蓝牙设置测试
#### 问题2扫描到设备但连接失败
**可能原因**
- 设备未配对
- 协议不兼容
- SerialTest配置错误
**解决方案**
1. 在系统蓝牙设置中手动配对
2. 检查SerialTest配置
3. 确认波特率设置
#### 问题3连接成功但无法通信
**可能原因**
- 数据格式不匹配
- 波特率设置错误
- 协议不兼容
**解决方案**
1. 检查SerialTest数据格式设置
2. 尝试不同的波特率
3. 发送测试数据验证
### 调试信息查看
#### Android Studio Logcat
过滤标签:`BluetoothManager`
查看以下信息:
- 扫描开始和结束
- 设备发现详情
- 连接状态变化
- 错误信息
#### 应用状态显示
应用会显示:
- 🔍 扫描状态
- 📱 发现的设备信息
- ✅ 连接成功提示
- ❌ 错误信息
### 下一步操作
1. **按照检查清单逐一确认**
2. **执行快速测试步骤**
3. **记录每个步骤的结果**
4. **查看Logcat日志**
5. **如果问题持续,提供详细错误信息**
### 需要提供的信息
如果问题仍然存在,请提供:
1. **手机型号和Android版本**
2. **电脑操作系统版本**
3. **SerialTest版本和配置截图**
4. **应用扫描过程的截图**
5. **Logcat中的错误日志**
6. **系统蓝牙设置中的设备列表截图**

View File

@ -1,105 +0,0 @@
# 快速测试指南
## 您的代码数据流分析
### 数据流路径:
```
蓝牙设备 → BluetoothManager → DataManager → 信号处理 → 图表显示
```
### 详细流程:
1. **BluetoothManager** 负责:
- 扫描和连接蓝牙设备
- 接收蓝牙数据
- 发送指令到设备
2. **DataManager** 负责:
- 解析接收到的数据包
- 应用信号处理算法(滤波、陷波等)
- 计算心率等指标
- 通过回调发送数据到UI
3. **实时显示**
- ECG节律视图和波形视图
- 实时更新图表数据
- 显示处理进度和状态
## 测试您的电脑连接
### 方法1直接连接推荐
1. 启动应用
2. **长按"连接蓝牙"按钮**
3. 输入您的电脑MAC地址`60:E9:AA:30:8B:0A`(使用冒号分隔符)
4. 点击"连接"
5. 观察连接状态
### 方法2扫描连接
1. 点击"连接蓝牙"按钮
2. 等待扫描完成
3. 在设备列表中选择您的电脑
4. 点击连接
## 数据收发测试
### 发送测试数据:
1. 连接成功后,**长按"发送指令"按钮**
2. 选择测试选项:
- **发送ECG测试数据**模拟ECG数据包
- **发送心跳包**:简单连接测试
- **发送设备信息查询**:查询设备状态
- **发送自定义测试数据**:自定义十六进制数据
### 接收测试:
1. 点击"启动程序"按钮
2. 观察图表是否显示测试数据
3. 查看状态信息区域的数据接收情况
## 图表显示测试
### 立即测试:
- **点击"启动程序"** → 生成简单测试数据
- **长按"启动程序"** → 生成复杂ECG波形
- **点击"清空数据"** → 清空图表数据
### 实时显示:
- 连接成功后,图表会自动显示接收到的数据
- 支持实时滤波和信号处理
- 双视图显示:节律视图 + 波形视图
## 调试技巧
### 查看日志:
在Android Studio中打开Logcat过滤标签
- `MainActivity`:主要操作日志
- `BluetoothManager`:蓝牙连接日志
- `DataManager`:数据处理日志
### 常见问题:
1. **连接失败**检查权限和MAC地址
2. **数据不显示**:点击"启动程序"生成测试数据
3. **图表空白**:检查图表容器可见性
## 测试建议
1. **先测试连接**:确保能连接到您的电脑
2. **再测试发送**:发送各种测试数据
3. **最后测试接收**:验证数据接收和显示
4. **查看日志**通过Logcat监控详细过程
## 数据格式说明
### ECG测试数据包
```
AA 55 01 [长度] [ECG数据...] [校验和]
```
### 自定义数据:
支持十六进制格式,如:`01 02 03 04 05`
## 下一步
1. 编译并运行应用
2. 按照上述步骤测试连接
3. 验证数据收发功能
4. 检查图表显示效果
5. 查看日志确认各环节正常

View File

@ -1,120 +0,0 @@
# 手机连接测试指南
## 目标设备信息
- **设备类型**: 手机
- **MAC地址**: `A4:C3:37:86:9F:73`
- **连接方式**: 蓝牙
## 连接步骤
### 第一步:准备目标手机
1. **确保目标手机蓝牙已开启**
2. **设置蓝牙可见性**
- 打开设置 → 蓝牙
- 确保蓝牙已开启
- 设置为"可发现"或"始终可见"
3. **检查配对状态**
- 如果之前配对过,建议先取消配对
- 重新开始配对过程
### 第二步:使用应用连接
1. **启动Android应用**
2. **长按"连接蓝牙"按钮**
3. **输入MAC地址**`A4:C3:37:86:9F:73`
4. **点击"连接"按钮**
### 第三步:观察连接状态
应用会显示以下状态信息:
- 🔍 **开始扫描蓝牙设备...**
- ✅ **权限检查通过,开始扫描...**
- 📡 **使用BLE扫描模式**或**使用传统蓝牙扫描模式**
- 📱 **发现设备: [设备名称]**
- 🎯 **找到目标设备!**(如果发现目标手机)
- ✅ **设备已连接: [设备名称]**
- 🔍 **服务发现成功**
- 📡 **数据通道已建立,可以发送指令开始接收数据**
## 预期结果
### 连接成功时:
- 按钮变为"断开蓝牙"(红色)
- "发送指令"按钮启用(蓝色)
- 显示详细的设备信息
- 服务发现成功
- 数据通道建立
### 连接失败时:
- 显示错误信息
- 按钮保持"连接蓝牙"状态
- 提供可能的解决方案
## 测试数据发送
连接成功后,可以测试数据通信:
### 1. 发送测试数据
1. **长按"发送指令"按钮**
2. **选择测试数据类型**
- 发送ECG测试数据
- 发送心跳包
- 发送设备信息查询
- 发送自定义测试数据
### 2. 观察数据接收
- 在目标手机上查看是否收到数据
- 检查数据格式是否正确
- 验证通信是否双向
## 故障排除
### 如果扫描不到设备:
1. **检查目标手机蓝牙设置**
2. **确保设备在10米范围内**
3. **重启两台设备的蓝牙**
4. **使用系统蓝牙设置测试**
### 如果连接失败:
1. **检查MAC地址是否正确**
2. **确认目标手机蓝牙已开启**
3. **尝试在系统蓝牙设置中手动配对**
4. **查看Logcat日志获取详细错误信息**
### 如果连接成功但无法通信:
1. **检查目标手机是否支持相应的蓝牙服务**
2. **确认数据格式是否兼容**
3. **尝试发送不同类型的测试数据**
## 调试信息
### Android Studio Logcat
过滤标签:`BluetoothManager`
查看以下关键信息:
- 扫描开始和结束
- 设备发现详情
- 连接状态变化
- 服务发现过程
- 数据收发情况
### 应用状态显示
应用会实时显示:
- 扫描进度
- 发现的设备信息
- 连接状态
- 错误信息
## 下一步操作
1. **按照上述步骤连接目标手机**
2. **观察连接过程的状态信息**
3. **测试数据收发功能**
4. **记录任何错误或异常情况**
5. **如果遇到问题查看Logcat日志**
## 需要提供的信息
如果连接失败,请提供:
1. **目标手机型号和Android版本**
2. **应用显示的状态信息**
3. **Logcat中的错误日志**
4. **目标手机蓝牙设置截图**
5. **连接过程的详细描述**

View File

@ -1,143 +0,0 @@
# 蓝牙连接测试总结
## 您的代码数据流分析
### 完整数据流:
```
蓝牙设备 (60-E9-AA-30-8B-0A)
BluetoothManager (连接管理)
DataManager (数据解析和处理)
信号处理 (滤波、陷波等)
实时图表显示 (ECG双视图)
```
### 各组件功能:
1. **BluetoothManager**
- 扫描和连接蓝牙设备
- 接收蓝牙数据流
- 发送指令到设备
- 管理连接状态
2. **DataManager**
- 解析接收到的数据包
- 应用信号处理算法
- 计算心率等指标
- 通过回调发送数据到UI
3. **实时显示系统**
- ECG节律视图
- ECG波形视图
- 实时数据更新
- 状态信息显示
## 已添加的测试功能
### 1. 直接连接功能
- **长按"连接蓝牙"按钮** → 弹出直接连接对话框
- 预设您的电脑MAC地址`60:E9:AA:30:8B:0A`(使用冒号分隔符)
- 支持自定义MAC地址输入
### 2. 测试数据发送功能
- **长按"发送指令"按钮** → 弹出测试数据对话框
- 支持多种测试数据类型:
- ECG测试数据包
- 心跳包
- 设备信息查询
- 自定义十六进制数据
### 3. 图表显示测试
- **点击"启动程序"** → 立即生成测试数据
- **长按"启动程序"** → 生成复杂ECG波形
- **点击"清空数据"** → 清空图表数据
## 测试步骤
### 第一步:连接测试
1. 启动应用
2. **长按"连接蓝牙"按钮**
3. 输入MAC地址`60:E9:AA:30:8B:0A`(使用冒号分隔符)
4. 点击"连接"
5. 观察连接状态
### 第二步:发送测试
1. 连接成功后,**长按"发送指令"按钮**
2. 选择测试数据类型
3. 发送测试数据
4. 观察发送状态
### 第三步:接收测试
1. 点击"启动程序"按钮
2. 观察图表显示
3. 查看数据接收情况
### 第四步:图表测试
1. 长按"启动程序"按钮
2. 观察ECG波形显示
3. 测试陷波滤波器功能
## 调试信息
### 日志标签:
- `MainActivity`:主要操作日志
- `BluetoothManager`:蓝牙连接日志
- `DataManager`:数据处理日志
### 状态显示:
- 连接状态实时更新
- 数据接收进度显示
- 错误信息详细提示
## 数据格式
### ECG测试数据包
```
AA 55 01 [长度低字节] [长度高字节] [ECG数据...] [校验和]
```
### 自定义数据:
支持十六进制格式,如:`01 02 03 04 05`
## 测试建议
1. **先测试连接**:确保能连接到您的电脑
2. **再测试发送**:发送各种测试数据验证连接
3. **最后测试接收**:验证数据接收和图表显示
4. **查看日志**通过Logcat监控详细过程
## 常见问题解决
### 连接失败:
- 检查蓝牙权限
- 确认MAC地址正确
- 确保设备在范围内
### 数据不显示:
- 点击"启动程序"生成测试数据
- 检查连接状态
- 查看日志错误信息
### 图表空白:
- 检查图表容器可见性
- 确认数据回调正常
- 尝试重新生成测试数据
## 下一步操作
1. 编译并运行应用
2. 按照测试步骤进行连接测试
3. 验证数据收发功能
4. 检查图表显示效果
5. 查看日志确认各环节正常
## 技术支持
如果遇到问题,请:
1. 查看Android Studio的Logcat日志
2. 检查权限设置
3. 确认设备MAC地址
4. 验证蓝牙功能正常

View File

@ -1,91 +0,0 @@
# 编译错误修复总结
## 问题描述
在之前的代码中出现了多个编译错误,导致应用无法正常构建。
## 修复的错误
### 1. 变量名冲突
**问题**: `bluetoothManager` 变量名与类名冲突
**修复**: 将变量名改为 `bluetoothManagerService`
```kotlin
// 修复前
private var bluetoothManager: BluetoothManager? = null
// 修复后
private var bluetoothManagerService: android.bluetooth.BluetoothManager? = null
```
### 2. 未定义的引用
**问题**: 多个回调和方法未定义
**修复**: 添加了完整的回调定义
- `bleScanCallback` - BLE扫描回调
- `registerClassicScanReceiver()` - 注册传统蓝牙扫描接收器
- `unregisterClassicScanReceiver()` - 注销传统蓝牙扫描接收器
### 3. 语法错误
**问题**: `override` 修饰符使用错误
**修复**: 正确使用 `override` 修饰符在回调对象中
### 4. 缺少闭合大括号
**问题**: 文件结构不完整
**修复**: 补充完整的类结构
## 当前状态
### ✅ 编译成功
- 所有编译错误已修复
- 应用可以正常构建
- 只有一些关于已弃用API的警告不影响功能
### ⚠️ 警告信息
以下API已被弃用但功能正常
- `characteristic.value` - 特征值设置
- `gatt.writeCharacteristic()` - 写入特征
- `intent.getParcelableExtra()` - 获取Parcelable数据
## 功能验证
### 已实现的功能
1. **蓝牙初始化** - 自动检测蓝牙适配器
2. **设备扫描** - 支持BLE和传统蓝牙扫描
3. **设备连接** - 支持直接MAC地址连接
4. **服务发现** - 自动发现蓝牙服务和特征
5. **数据收发** - 支持发送各种类型的指令
6. **状态监控** - 详细的连接状态显示
### 测试准备
现在可以测试连接目标手机:
- **MAC地址**: `A4:C3:37:86:9F:73`
- **连接方式**: 长按"连接蓝牙"按钮
- **状态显示**: 详细的状态信息和调试信息
## 下一步操作
1. **运行应用** - 在Android Studio中运行应用
2. **测试连接** - 尝试连接目标手机
3. **观察日志** - 查看Logcat中的详细信息
4. **功能验证** - 测试数据收发功能
## 技术细节
### 蓝牙协议支持
- **BLE (Bluetooth Low Energy)** - 低功耗蓝牙
- **传统蓝牙** - 经典蓝牙协议
- **自动检测** - 根据设备能力自动选择协议
### 设备兼容性
- **多种UUID支持** - 支持多种ECG设备UUID
- **智能检测** - 自动检测设备服务和特征
- **错误处理** - 完善的错误处理和用户提示
### 调试功能
- **详细日志** - 完整的操作日志记录
- **状态显示** - 实时的状态信息更新
- **错误提示** - 友好的错误信息显示
## 总结
所有编译错误已成功修复,应用现在可以正常构建和运行。代码结构完整,功能齐全,可以开始进行蓝牙连接测试。

View File

@ -1,217 +0,0 @@
# 蓝牙扫描故障排除指南
## 问题描述
应用无法扫描到目标设备,需要诊断和解决扫描问题。
## 🔍 常见原因分析
### 1. 权限问题
**症状**: 扫描无结果,日志显示权限错误
**检查项目**:
- [ ] `BLUETOOTH_SCAN` 权限已授予
- [ ] `BLUETOOTH_CONNECT` 权限已授予
- [ ] `ACCESS_FINE_LOCATION` 权限已授予
- [ ] Android 12+ 需要额外权限
**解决方案**:
1. 进入应用设置 → 权限
2. 确保所有蓝牙相关权限已开启
3. 重启应用
### 2. 目标设备设置问题
**症状**: 扫描到其他设备但找不到目标设备
**检查项目**:
- [ ] 目标设备蓝牙已开启
- [ ] 设置为"可发现"或"始终可见"
- [ ] 未与其他设备连接
- [ ] 设备在扫描范围内10米内
**解决方案**:
1. 打开目标设备设置
2. 进入蓝牙设置
3. 确保蓝牙开启且可发现
4. 断开其他连接
### 3. 扫描范围问题
**症状**: 有时能找到设备,有时找不到
**检查项目**:
- [ ] 设备距离在10米内
- [ ] 无金属屏蔽物
- [ ] 远离其他蓝牙设备
- [ ] 无WiFi干扰
**解决方案**:
1. 将设备靠近1-2米内
2. 移除金属物品
3. 关闭其他蓝牙设备
4. 尝试不同位置
### 4. 系统蓝牙服务问题
**症状**: 扫描完全无结果
**检查项目**:
- [ ] 系统蓝牙服务正常
- [ ] 蓝牙适配器工作正常
- [ ] 无其他应用占用蓝牙
**解决方案**:
1. 重启蓝牙服务
2. 重启设备
3. 检查系统蓝牙设置
## 🛠️ 增强的扫描功能
### 新增功能特性:
1. **详细状态提示** - 显示扫描进度和注意事项
2. **延长扫描时间** - 从10秒增加到15秒
3. **智能错误处理** - BLE失败自动切换到传统蓝牙
4. **设备信息显示** - 显示设备名称、地址、信号强度
5. **目标设备识别** - 自动识别目标设备并提供连接建议
### 扫描流程:
```
开始扫描 → 权限检查 → BLE扫描(15秒) → 失败重试 → 传统蓝牙(15秒) → 结果汇总
```
## 📋 测试步骤
### 第一步:基础检查
1. **确认应用权限**
- 进入应用设置 → 权限
- 确保蓝牙和位置权限已开启
2. **检查系统蓝牙**
- 进入系统设置 → 蓝牙
- 确保蓝牙已开启
3. **检查目标设备**
- 确保目标设备蓝牙开启
- 设置为可发现模式
### 第二步:系统测试
1. **使用系统蓝牙扫描**
- 在系统蓝牙设置中扫描
- 查看是否能发现目标设备
- 记录扫描结果
2. **测试设备可见性**
- 尝试手动配对
- 确认设备可以被发现
### 第三步:应用测试
1. **启动应用扫描**
- 点击"连接蓝牙"按钮
- 观察扫描过程
- 查看状态信息
2. **观察扫描结果**
- 查看发现的设备列表
- 检查设备信息显示
- 记录扫描时间
### 第四步:日志分析
1. **查看应用日志**
- 打开Android Studio Logcat
- 过滤蓝牙相关日志
- 分析扫描过程
2. **检查错误信息**
- 查看权限错误
- 查看扫描失败原因
- 记录错误代码
## 🔧 调试技巧
### 1. 使用系统蓝牙验证
```bash
# 在系统蓝牙设置中手动扫描
# 确认目标设备是否可见
# 测试手动配对是否成功
```
### 2. 检查设备信息
- **目标设备型号和Android版本**
- **蓝牙芯片类型**
- **支持的蓝牙协议**
- **设备MAC地址**
### 3. 环境测试
- **尝试不同位置**
- **检查环境干扰**
- **测试不同距离**
- **移除金属物品**
### 4. 应用调试
- **查看详细日志**
- **观察状态变化**
- **记录错误信息**
- **分析扫描模式**
## 📊 常见错误码及解决方案
| 错误码 | 含义 | 解决方案 |
|--------|------|----------|
| SCAN_FAILED_ALREADY_STARTED | 扫描已在进行中 | 等待当前扫描完成 |
| SCAN_FAILED_APPLICATION_REGISTRATION_FAILED | 应用注册失败 | 重启应用或设备 |
| SCAN_FAILED_FEATURE_UNSUPPORTED | 设备不支持BLE | 使用传统蓝牙扫描 |
| SCAN_FAILED_INTERNAL_ERROR | 内部错误 | 重启蓝牙服务 |
| SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES | 硬件资源不足 | 关闭其他蓝牙应用 |
## 🎯 针对目标设备的特殊检查
### 目标设备: `A4:C3:37:86:9F:73`
1. **设备信息确认**
- 确认MAC地址正确
- 检查设备型号
- 确认Android版本
2. **蓝牙设置检查**
- 确保蓝牙开启
- 设置为可发现模式
- 断开其他连接
3. **连接测试**
- 使用系统蓝牙测试连接
- 确认设备可以被发现
- 测试手动配对
## 📝 故障排除清单
### 扫描前检查:
- [ ] 应用权限已授予
- [ ] 系统蓝牙已开启
- [ ] 目标设备蓝牙已开启
- [ ] 目标设备设置为可发现
- [ ] 设备距离在10米内
- [ ] 无其他蓝牙连接
### 扫描中观察:
- [ ] 扫描状态信息显示
- [ ] 发现设备列表更新
- [ ] 目标设备是否出现
- [ ] 扫描时间是否足够
- [ ] 错误信息提示
### 扫描后分析:
- [ ] 扫描结果汇总
- [ ] 发现的设备数量
- [ ] 目标设备是否找到
- [ ] 设备信息是否完整
- [ ] 连接建议是否提供
## 🚀 下一步操作
1. **按照上述步骤逐一检查**
2. **使用系统蓝牙设置测试**
3. **记录所有测试结果**
4. **如果问题持续,提供详细设备信息**
## 📞 需要提供的信息
如果问题仍然存在,请提供:
1. **目标设备型号和Android版本**
2. **系统蓝牙设置测试结果**
3. **应用扫描日志**
4. **设备环境信息**
5. **详细的测试步骤和结果**
6. **错误信息和状态码**

View File

@ -1,177 +0,0 @@
# 蓝牙权限配置完整指南
## 📋 权限配置概述
本应用已配置完整的蓝牙权限支持,包括:
- **Android 11及以下**:需要位置权限进行蓝牙扫描
- **Android 12+**:使用新的蓝牙权限模型,无需位置权限
## 🔧 AndroidManifest.xml 配置
### 基础蓝牙权限
```xml
<!-- 基础蓝牙权限 (Android 11及以下) -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- Android 12+ 新蓝牙权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
```
### 位置权限仅Android 11及以下需要
```xml
<!-- 位置权限 (Android 11及以下需要) -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"
android:maxSdkVersion="30" />
```
### Android 12+ 特殊配置
```xml
<!-- Android 12+ 蓝牙扫描权限 (不用于定位) -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
```
## 📱 运行时权限请求
### 动态权限检查
应用会根据Android版本自动确定所需权限
```kotlin
// Android 12+ 使用新的蓝牙权限
private val REQUIRED_PERMISSIONS: Array<String> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT
)
} else {
// Android 11及以下需要位置权限
arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
}
```
### 权限状态显示
应用会显示详细的权限状态:
- ✅ 已授予权限
- ❌ 缺少权限
- 💡 权限请求提示
## 🔍 权限检查流程
### 1. 应用启动时检查
- 显示当前权限状态
- 提示用户授予必要权限
### 2. 蓝牙操作前检查
- 自动检查所需权限
- 如果缺少权限,自动请求
- 显示权限请求结果
### 3. 权限结果处理
- 显示已授予和被拒绝的权限
- 提供手动设置指导
- 根据权限状态决定是否继续操作
## 🚨 常见权限问题
### 1. 权限被拒绝
**症状**: 扫描无结果,显示"权限被拒绝"
**解决方案**:
1. 进入应用设置 → 权限
2. 手动开启蓝牙和位置权限
3. 重启应用
### 2. Android 12+ 权限问题
**症状**: 新设备上扫描失败
**解决方案**:
1. 确保使用新的蓝牙权限
2. 检查`neverForLocation`标志
3. 确认权限已正确授予
### 3. 位置权限问题
**症状**: Android 11及以下设备扫描失败
**解决方案**:
1. 确保位置权限已授予
2. 检查GPS是否开启某些厂商系统需要
3. 重启蓝牙服务
## 📊 权限状态检查
### 应用内权限状态显示
```
权限状态:
BLUETOOTH_SCAN: ✓
BLUETOOTH_CONNECT: ✓
ACCESS_FINE_LOCATION: ✓
ACCESS_COARSE_LOCATION: ✓
```
### 系统设置检查
1. **设置 → 应用 → 本应用 → 权限**
2. **设置 → 隐私 → 位置信息**
3. **设置 → 蓝牙**
## 🛠️ 调试权限问题
### 1. 检查权限声明
```bash
# 确认AndroidManifest.xml中的权限声明正确
# 检查maxSdkVersion和usesPermissionFlags
```
### 2. 检查运行时权限
```kotlin
// 在代码中检查权限状态
ContextCompat.checkSelfPermission(context, permission)
```
### 3. 查看权限请求结果
```kotlin
// 在onRequestPermissionsResult中处理结果
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray)
```
## 📝 权限配置清单
### AndroidManifest.xml 检查项
- [ ] 基础蓝牙权限已声明
- [ ] Android 12+ 新权限已声明
- [ ] 位置权限限制到Android 11及以下
- [ ] `neverForLocation`标志已设置
- [ ] 蓝牙功能特性已声明
### 代码权限检查项
- [ ] 动态权限检查已实现
- [ ] 权限请求已处理
- [ ] 权限结果已处理
- [ ] 权限状态显示已实现
### 测试检查项
- [ ] Android 11及以下设备测试通过
- [ ] Android 12+ 设备测试通过
- [ ] 权限拒绝场景测试通过
- [ ] 权限授予后功能正常
## 🎯 针对目标设备的权限检查
### 目标设备: `A4:C3:37:86:9F:73`
1. **确认设备Android版本**
2. **检查对应权限是否已授予**
3. **验证权限配置是否正确**
4. **测试扫描功能是否正常**
## 🚀 下一步操作
1. **重新编译应用**
2. **在目标设备上测试权限**
3. **确认权限授予后扫描正常**
4. **如果仍有问题,检查设备特定设置**

View File

@ -1,183 +0,0 @@
# 蓝牙调试指南 - SerialTest连接问题
## 问题描述
- 电脑端SerialTest作为蓝牙服务器运行
- 手机端Android应用作为客户端
- 问题:客户端找不到服务器设备
## 第一步:检查手机蓝牙设置
### 1.1 确保蓝牙可见性
1. **打开手机设置** → **蓝牙**
2. **确保蓝牙已开启**
3. **设置可见性**
- 点击"更多设置"或"高级设置"
- 找到"可见性"或"可发现性"选项
- 设置为"始终可见"或"可发现"
### 1.2 检查配对状态
1. **查看已配对设备列表**
2. **如果看到您的电脑设备**
- 点击设备名称
- 选择"取消配对"
- 重新配对
### 1.3 重置蓝牙设置
1. **清除蓝牙缓存**
- 设置 → 应用管理 → 蓝牙
- 清除数据和缓存
2. **重启蓝牙**
- 关闭蓝牙
- 等待10秒
- 重新开启蓝牙
## 第二步检查电脑端SerialTest设置
### 2.1 SerialTest配置
1. **确保SerialTest正确配置**
- 选择正确的COM端口
- 设置波特率通常9600或115200
- 启用蓝牙服务器模式
### 2.2 电脑蓝牙设置
1. **检查电脑蓝牙状态**
- 确保蓝牙已开启
- 设置为"可发现"模式
2. **查看蓝牙设备**
- 控制面板 → 设备和打印机
- 确认蓝牙适配器正常工作
## 第三步:使用应用进行调试
### 3.1 扫描测试
1. **启动Android应用**
2. **点击"连接蓝牙"按钮**(普通点击,不是长按)
3. **观察扫描结果**
- 查看是否发现您的电脑设备
- 记录设备名称和MAC地址
### 3.2 直接连接测试
1. **长按"连接蓝牙"按钮**
2. **输入电脑MAC地址**`60:E9:AA:30:8B:0A`
3. **点击连接**
4. **观察连接状态**
### 3.3 查看详细日志
在Android Studio的Logcat中查看
- 过滤标签:`BluetoothManager`
- 查找扫描和连接相关的日志
## 第四步:常见解决方案
### 4.1 如果扫描不到设备
1. **重启蓝牙服务**
- 电脑:重启蓝牙适配器
- 手机:重启蓝牙
2. **检查距离**确保设备在10米范围内
3. **检查干扰**:远离其他蓝牙设备
### 4.2 如果连接失败
1. **重新配对设备**
- 在手机蓝牙设置中删除电脑设备
- 在电脑蓝牙设置中删除手机设备
- 重新配对
2. **检查SerialTest状态**
- 确保SerialTest正在运行
- 检查是否有错误信息
### 4.3 如果连接成功但无法通信
1. **检查SerialTest配置**
- 确认波特率设置
- 确认数据格式设置
2. **测试数据发送**
- 使用应用发送测试数据
- 在SerialTest中查看是否收到数据
## 第五步:高级调试
### 5.1 使用系统蓝牙设置
1. **在手机蓝牙设置中手动连接**
- 扫描设备
- 选择您的电脑设备
- 输入配对码通常是0000或1234
### 5.2 检查蓝牙协议
1. **确认SerialTest使用的协议**
- SPP串口协议
- RFCOMM
- 其他协议
2. **确认应用支持的协议**
### 5.3 使用其他蓝牙工具测试
1. **下载蓝牙调试工具**
- nRF Connect推荐
- Bluetooth Scanner
2. **测试连接**
- 扫描设备
- 尝试连接
- 查看服务列表
## 第六步:应用调试功能
### 6.1 增强扫描功能
应用现在支持:
- **BLE扫描**:低功耗蓝牙扫描
- **传统蓝牙扫描**:经典蓝牙扫描
- **智能检测**:自动检测设备类型
### 6.2 连接状态监控
应用会显示:
- ✅ **连接成功**:设备已连接
- ❌ **连接失败**:详细的错误信息
- 🔍 **服务发现**:发现的服务和特征
- 📡 **数据通道**:通信通道状态
## 第七步:测试步骤
### 7.1 基础测试
1. **启动SerialTest** → 配置服务器
2. **启动Android应用** → 扫描设备
3. **连接设备** → 观察状态
4. **发送测试数据** → 验证通信
### 7.2 数据通信测试
1. **发送ECG测试数据**
2. **发送心跳包**
3. **发送设备信息查询**
4. **观察SerialTest接收情况**
## 常见错误及解决方案
### 错误1扫描不到设备
**解决方案**
- 检查设备可见性
- 重启蓝牙服务
- 检查距离和干扰
### 错误2连接超时
**解决方案**
- 重新配对设备
- 检查SerialTest状态
- 确认MAC地址正确
### 错误3服务发现失败
**解决方案**
- 检查设备协议兼容性
- 使用系统蓝牙设置连接
- 查看详细错误日志
## 下一步操作
1. **按照上述步骤逐一检查**
2. **记录每个步骤的结果**
3. **查看Android Studio的Logcat日志**
4. **如果仍有问题,提供详细的错误信息**
## 技术支持
如果问题仍然存在,请提供:
1. **手机型号和Android版本**
2. **电脑操作系统版本**
3. **SerialTest版本和配置**
4. **详细的错误日志**
5. **扫描和连接过程的截图**

View File

@ -1,96 +0,0 @@
# 蓝牙连接测试说明
## 数据流概述
您的应用数据流如下:
1. **蓝牙连接****数据接收****数据解析****信号处理** → **图表显示**
### 详细流程:
- **蓝牙管理器** (BluetoothManager) 负责设备连接和数据接收
- **数据管理器** (DataManager) 负责数据解析和信号处理
- **实时数据回调** 将处理后的数据发送到图表显示
- **ECG图表视图** 实时显示ECG波形
## 测试步骤
### 1. 直接连接测试
1. 启动应用
2. **长按"连接蓝牙"按钮** → 弹出直接连接对话框
3. 输入您的电脑MAC地址`60:E9:AA:30:8B:0A`(使用冒号分隔符)
4. 点击"连接"按钮
5. 观察连接状态
### 2. 数据收发测试
1. 连接成功后,**长按"发送指令"按钮** → 弹出测试数据对话框
2. 选择测试选项:
- **发送ECG测试数据**发送模拟ECG数据包
- **发送心跳包**:发送简单的心跳数据
- **发送设备信息查询**:查询设备信息
- **发送自定义测试数据**:发送自定义十六进制数据
### 3. 图表显示测试
1. 点击"启动程序"按钮 → 立即生成测试数据并显示图表
2. 长按"启动程序"按钮 → 生成更复杂的ECG波形测试
3. 观察ECG双视图的实时更新
## 测试功能说明
### 蓝牙连接功能
- **扫描设备**:自动扫描附近的蓝牙设备
- **直接连接**通过MAC地址直接连接指定设备
- **连接状态**:实时显示连接状态和错误信息
### 数据发送功能
- **ECG测试数据**生成符合ECG格式的模拟数据包
- **心跳包**:简单的连接测试数据
- **自定义数据**:支持十六进制格式的自定义数据
### 数据接收功能
- **实时接收**:自动接收蓝牙数据
- **数据解析**:使用原生解析器解析数据包
- **信号处理**:应用滤波和信号处理算法
- **实时显示**:立即更新图表显示
## 调试信息
应用会在日志中输出详细的调试信息:
- 蓝牙连接状态
- 数据接收情况
- 数据解析结果
- 信号处理进度
- 图表更新状态
## 常见问题
### 连接失败
1. 检查蓝牙权限是否已授予
2. 确认设备MAC地址正确使用冒号分隔符60:E9:AA:30:8B:0A
3. 确保目标设备在范围内且可发现
### 数据接收问题
1. 检查连接状态
2. 查看日志中的错误信息
3. 尝试发送测试数据验证连接
### 图表不显示
1. 点击"启动程序"按钮生成测试数据
2. 检查图表容器是否可见
3. 查看数据回调是否正常
## 测试建议
1. **先测试连接**:确保能成功连接到您的电脑
2. **再测试发送**:发送各种测试数据验证连接稳定性
3. **最后测试接收**:验证数据接收和图表显示功能
4. **查看日志**通过Android Studio的Logcat查看详细调试信息
## 数据格式
### ECG测试数据包格式
```
AA 55 01 [长度低字节] [长度高字节] [ECG数据...] [校验和]
```
### 自定义数据格式
支持十六进制格式,如:`01 02 03 04 05`

View File

@ -1,98 +0,0 @@
# 连接状态显示增强
## 连接成功提示
现在当蓝牙设备连接成功时,应用会显示详细的连接状态信息:
### 主要提示信息:
- ✅ **设备已连接**: [设备名称/地址]
- 🎉 **连接成功!设备信息:**
- 设备名称: [设备名称]
- 设备地址: [MAC地址]
- 设备类型: [设备类型]
- 📡 **数据通道已建立,可以开始收发数据**
- 📊 **ECG图表已准备就绪请点击'发送指令'按钮开始接收数据**
### 服务发现信息:
- 🔍 **服务发现成功**
- 📋 **发现 X 个服务**
- 📡 **数据通道已建立,可以发送指令开始接收数据**
- 💡 **提示: 长按'发送指令'按钮可以发送测试数据**
## 连接状态变化
### 连接成功时:
1. **按钮状态变化**
- "连接蓝牙" → "断开蓝牙"
- 按钮颜色变为红色 (#F44336)
- "发送指令"按钮启用并变为蓝色
2. **图表显示**
- ECG图表容器自动显示
- 准备接收数据
3. **状态信息**
- 显示详细的设备信息
- 提供下一步操作提示
### 连接断开时:
1. **按钮状态变化**
- "断开蓝牙" → "连接蓝牙"
- 按钮颜色变为绿色 (#4CAF50)
- "发送指令"按钮禁用并变为灰色
2. **状态信息**
- 显示断开连接提示
## 测试步骤
### 1. 连接测试
1. 启动应用
2. **长按"连接蓝牙"按钮**
3. 输入MAC地址`60:E9:AA:30:8B:0A`
4. 点击"连接"
5. **观察连接成功提示**
- ✅ 设备已连接
- 🎉 连接成功!设备信息
- 📡 数据通道已建立
### 2. 验证连接状态
1. 检查按钮状态变化
2. 查看状态信息区域的详细提示
3. 确认ECG图表容器已显示
### 3. 测试数据收发
1. **长按"发送指令"按钮**发送测试数据
2. 观察数据接收状态
3. 查看图表显示效果
## 调试信息
### 日志标签:
- `MainActivity`: 主要操作日志
- `BluetoothManager`: 蓝牙连接日志
### 状态显示:
- 连接状态实时更新
- 设备信息详细显示
- 操作提示清晰明确
## 常见问题
### 连接成功但没有显示提示:
1. 检查状态信息区域是否可见
2. 查看日志确认回调是否正常
3. 确认UI线程更新是否成功
### 设备信息不完整:
1. 某些设备可能不提供完整信息
2. 应用会显示可用的信息
3. 不影响连接功能
## 下一步操作
连接成功后,您可以:
1. **发送测试数据**:长按"发送指令"按钮
2. **查看图表显示**:点击"启动程序"按钮
3. **测试数据接收**:观察实时数据流
4. **应用信号处理**:测试陷波滤波器等功能

View File

@ -1,164 +0,0 @@
# 连接超时故障排除指南
## 问题描述
应用尝试连接到目标设备 `A4:C3:37:86:9F:73` 时出现连接超时错误。
## 日志分析
### 关键日志信息
```
尝试直接连接到设备: A4:C3:37:86:9F:73
on_create_connection_timeout, address: a4:c3:37:86:9f:73
Connection failed le remote:a4:c3:37:86:9f:73
status=147 clientIf=16 connected=false
```
### 问题分析
- **连接启动成功** - 应用成功发起连接请求
- **30秒超时** - 连接在30秒后超时
- **状态码147** - 表示连接失败
- **LE连接失败** - 低功耗蓝牙连接失败
## 可能原因及解决方案
### 1. 目标设备蓝牙设置问题
#### 检查项目:
- [ ] 蓝牙已开启
- [ ] 设置为"可发现"模式
- [ ] 未与其他设备连接
- [ ] 蓝牙服务正常运行
#### 解决步骤:
1. **打开目标手机设置**
2. **进入蓝牙设置**
3. **确保蓝牙已开启**
4. **设置为"始终可见"或"可发现"**
5. **断开其他蓝牙连接**
### 2. 设备距离和干扰
#### 检查项目:
- [ ] 设备距离在10米内
- [ ] 无金属屏蔽物
- [ ] 远离其他蓝牙设备
- [ ] 无WiFi干扰
#### 解决步骤:
1. **将两台设备靠近**1-2米内
2. **移除金属物品**
3. **关闭其他蓝牙设备**
4. **尝试不同位置**
### 3. 系统蓝牙服务问题
#### 检查项目:
- [ ] 系统蓝牙服务正常
- [ ] 蓝牙权限已授予
- [ ] 无其他应用占用蓝牙
#### 解决步骤:
1. **重启两台设备的蓝牙**
2. **检查应用权限**
3. **关闭其他蓝牙应用**
4. **重启设备**
### 4. 设备兼容性问题
#### 检查项目:
- [ ] 目标设备支持BLE
- [ ] 设备型号和Android版本
- [ ] 蓝牙协议兼容性
#### 解决步骤:
1. **确认设备支持BLE**
2. **更新系统版本**
3. **尝试传统蓝牙连接**
4. **使用系统蓝牙设置测试**
## 增强的连接功能
### 新增功能:
1. **详细状态提示** - 显示连接进度和注意事项
2. **自动重试机制** - 超时后自动尝试传统蓝牙连接
3. **错误状态码解析** - 根据状态码提供具体建议
4. **连接超时设置** - 30秒BLE + 15秒传统蓝牙
### 连接流程:
```
开始连接 → BLE连接(30秒) → 超时重试 → 传统蓝牙(15秒) → 最终结果
```
## 测试步骤
### 第一步:基础检查
1. **确认目标设备蓝牙开启**
2. **设置为可发现模式**
3. **断开其他连接**
4. **将设备靠近**
### 第二步:系统测试
1. **在系统蓝牙设置中扫描**
2. **查看是否发现目标设备**
3. **尝试手动配对**
4. **记录配对结果**
### 第三步:应用测试
1. **启动应用**
2. **长按"连接蓝牙"按钮**
3. **输入MAC地址A4:C3:37:86:9F:73**
4. **观察连接过程**
### 第四步:日志分析
1. **查看应用状态信息**
2. **检查Logcat日志**
3. **记录错误信息**
4. **分析失败原因**
## 常见错误码及含义
| 状态码 | 含义 | 解决方案 |
|--------|------|----------|
| 0 | 正常断开 | 检查设备状态 |
| 8 | 连接超时 | 检查设备可见性和距离 |
| 19 | 连接被拒绝 | 检查设备是否忙 |
| 22 | 连接失败 | 检查协议兼容性 |
| 147 | 连接失败 | 检查设备状态和设置 |
## 调试建议
### 1. 使用系统蓝牙测试
- 在系统蓝牙设置中手动连接
- 验证设备是否可见和可连接
- 确认配对是否成功
### 2. 检查设备信息
- 目标设备型号和Android版本
- 蓝牙芯片类型
- 支持的蓝牙协议
### 3. 环境测试
- 尝试不同位置
- 检查环境干扰
- 测试不同距离
### 4. 应用调试
- 查看详细日志
- 观察状态变化
- 记录错误信息
## 下一步操作
1. **按照上述步骤逐一检查**
2. **使用系统蓝牙设置测试连接**
3. **记录所有测试结果**
4. **如果问题持续,提供详细设备信息**
## 需要提供的信息
如果问题仍然存在,请提供:
1. **目标设备型号和Android版本**
2. **系统蓝牙设置测试结果**
3. **详细的错误日志**
4. **设备环境信息**
5. **测试步骤和结果**