BLE DAN
This commit is contained in:
parent
3d4d7feae1
commit
0258157633
|
|
@ -0,0 +1,173 @@
|
|||
# 蓝牙数据流处理指南
|
||||
|
||||
## 🎯 数据流处理架构
|
||||
|
||||
### 数据流向
|
||||
```
|
||||
蓝牙设备 → 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数据了!🚀
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
# 蓝牙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!🎯
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
# 按钮和对话框修复测试指南
|
||||
|
||||
## 🎯 修复内容
|
||||
|
||||
### 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. 设备选择
|
||||
- 第一个设备:点击"选择第一个设备"
|
||||
- 第二个设备:点击"选择第二个设备"
|
||||
- 更多设备:可以修改代码添加更多按钮
|
||||
|
||||
## 🎉 预期效果
|
||||
|
||||
### 启动时:
|
||||
- ✅ 按钮位置合理,完全可见
|
||||
- ✅ 界面布局清晰
|
||||
- ✅ 状态信息正常显示
|
||||
|
||||
### 扫描时:
|
||||
- ✅ 扫描过程流畅
|
||||
- ✅ 设备发现信息及时更新
|
||||
|
||||
### 对话框:
|
||||
- ✅ 快速弹出,无卡顿
|
||||
- ✅ 设备列表清晰显示
|
||||
- ✅ 选择按钮响应及时
|
||||
|
||||
### 连接后:
|
||||
- ✅ 连接状态正确显示
|
||||
- ✅ 按钮状态正确更新
|
||||
- ✅ 可以开始数据处理
|
||||
|
||||
现在可以测试修复后的按钮位置和对话框性能了!🎉
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
# 设备选择功能改进测试指南
|
||||
|
||||
## 🎯 改进内容
|
||||
|
||||
### 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提高性能
|
||||
- 固定对话框高度避免过大
|
||||
- 扫描完成后立即弹出
|
||||
|
||||
## 🎉 预期效果
|
||||
|
||||
### 扫描过程:
|
||||
- ✅ 显示扫描进度
|
||||
- ✅ 实时显示发现的设备
|
||||
- ✅ 扫描完成后立即提示
|
||||
|
||||
### 设备选择:
|
||||
- ✅ 显示所有扫描到的设备
|
||||
- ✅ 每个设备都可以点击
|
||||
- ✅ 对话框响应流畅
|
||||
|
||||
### 连接过程:
|
||||
- ✅ 点击设备后立即开始连接
|
||||
- ✅ 连接状态正确显示
|
||||
- ✅ 支持连接任意设备
|
||||
|
||||
现在你可以扫描所有设备并点击任意设备进行连接了!🎉
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
# 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数据了!🎯
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
# 蓝牙原始数据显示测试指南
|
||||
|
||||
## 🎯 功能说明
|
||||
|
||||
现在应用已经支持**立即显示蓝牙接收到的原始数据**,无需等待信号处理完成。
|
||||
|
||||
### 数据流处理顺序
|
||||
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
|
||||
```
|
||||
|
||||
### 状态检查
|
||||
- 蓝牙连接状态
|
||||
- 数据接收频率
|
||||
- 图表更新状态
|
||||
- 通道数据完整性
|
||||
|
||||
现在你的应用可以立即显示蓝牙接收到的原始数据了!连接设备后就能看到实时波形。🚀
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
# 智能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
|
||||
|
||||
现在请重新连接你的设备,观察新的调试信息!🎯
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
# 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了!🎉
|
||||
|
|
@ -17,7 +17,8 @@ enum class DataType {
|
|||
RESPIRATION, // 呼吸/姿态
|
||||
SNORE, // 鼾声
|
||||
STETHOSCOPE, // 数字听诊
|
||||
MIT_BIH // 添加MIT-BIH心律失常数据集类型
|
||||
MIT_BIH, // MIT-BIH心律失常数据集类型
|
||||
PW_ECG_SL // 单通道心电数据 (0x4401)
|
||||
};
|
||||
|
||||
// 导联脱落状态
|
||||
|
|
@ -72,6 +73,9 @@ SensorData parse_ppg(const uint8_t* data);
|
|||
// 12导联心电解析 (0x4402)
|
||||
SensorData parse_12lead_ecg(const uint8_t* data);
|
||||
|
||||
// 单通道心电解析 (0x4401) - PW_ECG-SL
|
||||
SensorData parse_pw_ecg_sl(const uint8_t* data);
|
||||
|
||||
// MIT-BIH 212格式解析器
|
||||
SensorData parse_mit_bih_212(const uint8_t* data, size_t size);
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ SensorData Mapper::DataMapper(SensorData& data)
|
|||
case DataType::SNORE: __android_log_print(ANDROID_LOG_INFO, "DataMapper", "SNORE (5)"); break;
|
||||
case DataType::STETHOSCOPE: __android_log_print(ANDROID_LOG_INFO, "DataMapper", "STETHOSCOPE (6)"); break;
|
||||
case DataType::MIT_BIH: __android_log_print(ANDROID_LOG_INFO, "DataMapper", "MIT_BIH (7)"); break;
|
||||
case DataType::PW_ECG_SL: __android_log_print(ANDROID_LOG_INFO, "DataMapper", "PW_ECG_SL (8)"); break;
|
||||
default: __android_log_print(ANDROID_LOG_INFO, "DataMapper", "未知类型"); break;
|
||||
}
|
||||
__android_log_print(ANDROID_LOG_INFO, "DataMapper", "包序号: %d", data_mapped.packet_sn);
|
||||
|
|
|
|||
|
|
@ -226,6 +226,39 @@ SensorData parse_12lead_ecg(const uint8_t* data) {
|
|||
return result;
|
||||
}
|
||||
|
||||
// 单通道心电解析 (0x4401) - PW_ECG-SL
|
||||
SensorData parse_pw_ecg_sl(const uint8_t* data) {
|
||||
SensorData result;
|
||||
result.data_type = DataType::PW_ECG_SL;
|
||||
result.packet_sn = read_le<uint16_t>(data);
|
||||
|
||||
// 跳过 DataType(2) 和 data_len(2)
|
||||
const uint8_t* payload = data + 6;
|
||||
|
||||
// 导联脱落状态 (1字节,高4位+中4位)
|
||||
result.lead_status.status[0] = *payload;
|
||||
result.lead_status.status[1] = 0; // 第二个字节未使用
|
||||
payload += 1;
|
||||
|
||||
// 解析单通道ECG数据 (115个采样点)
|
||||
auto& channel = result.channel_data.emplace<std::vector<float>>();
|
||||
channel.reserve(115);
|
||||
|
||||
for (int i = 0; i < 115; ++i) {
|
||||
int16_t adc_value = read_le<int16_t>(payload);
|
||||
payload += 2;
|
||||
channel.push_back(adc_value * 0.318f); // 转换为μV,与12导联心电保持一致
|
||||
}
|
||||
|
||||
// 跳过预留1字节
|
||||
payload += 1;
|
||||
|
||||
// 赋值原始二进制数据
|
||||
result.raw_data.assign(data, data + 6 + 1 + 115 * 2 + 1);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// 数字听诊解析 (0x1102)
|
||||
SensorData parse_stethoscope(const uint8_t* data) {
|
||||
|
|
@ -398,6 +431,9 @@ std::vector<SensorData> parse_device_data(const std::vector<uint8_t>& file_data)
|
|||
case 0x4213:
|
||||
grouped_data[DataType::RESPIRATION].push_back(parse_respiration(ptr));
|
||||
break;
|
||||
case 0x4401:
|
||||
grouped_data[DataType::PW_ECG_SL].push_back(parse_pw_ecg_sl(ptr));
|
||||
break;
|
||||
case 0x4402:
|
||||
grouped_data[DataType::ECG_12LEAD].push_back(parse_12lead_ecg(ptr));
|
||||
break;
|
||||
|
|
@ -439,6 +475,9 @@ std::vector<SensorData> parse_device_data(const std::vector<uint8_t>& file_data)
|
|||
case 0x4213:
|
||||
grouped_data[DataType::RESPIRATION].push_back(parse_respiration(ptr));
|
||||
break;
|
||||
case 0x4401:
|
||||
grouped_data[DataType::PW_ECG_SL].push_back(parse_pw_ecg_sl(ptr));
|
||||
break;
|
||||
case 0x4402:
|
||||
grouped_data[DataType::ECG_12LEAD].push_back(parse_12lead_ecg(ptr));
|
||||
break;
|
||||
|
|
@ -660,6 +699,9 @@ void StreamParser::parseBuffer() {
|
|||
case 0x4213:
|
||||
packet = parse_respiration(buffer_.data());
|
||||
break;
|
||||
case 0x4401:
|
||||
packet = parse_pw_ecg_sl(buffer_.data());
|
||||
break;
|
||||
case 0x4402:
|
||||
packet = parse_12lead_ecg(buffer_.data());
|
||||
break;
|
||||
|
|
@ -713,6 +755,7 @@ bool StreamParser::isValidPacket(const uint8_t* data) {
|
|||
case 0x4211: // ECG_2LEAD
|
||||
case 0x4212: // SNORE
|
||||
case 0x4213: // RESPIRATION
|
||||
case 0x4401: // PW_ECG_SL
|
||||
case 0x4402: // ECG_12LEAD
|
||||
case 0x4302: // PPG
|
||||
case 0x1102: // STETHOSCOPE
|
||||
|
|
@ -810,6 +853,7 @@ jobject convertSensorDataToJava(JNIEnv* env, const SensorData& sensorData) {
|
|||
case DataType::PPG: enumName = "PPG"; break;
|
||||
case DataType::ECG_12LEAD: enumName = "ECG_12LEAD"; break;
|
||||
case DataType::MIT_BIH: enumName = "MIT_BIH"; break;
|
||||
case DataType::PW_ECG_SL: enumName = "PW_ECG_SL"; break;
|
||||
case DataType::STETHOSCOPE: enumName = "STETHOSCOPE"; break;
|
||||
case DataType::SNORE: enumName = "SNORE"; break;
|
||||
case DataType::RESPIRATION: enumName = "RESPIRATION"; break;
|
||||
|
|
|
|||
|
|
@ -7,12 +7,11 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Button
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
/**
|
||||
* 蓝牙设备选择对话框
|
||||
|
|
@ -22,6 +21,34 @@ class BluetoothDeviceDialog : DialogFragment() {
|
|||
private var devices: List<BluetoothDevice> = emptyList()
|
||||
private var onDeviceSelectedListener: ((BluetoothDevice) -> Unit)? = null
|
||||
|
||||
// 设备适配器
|
||||
private inner class DeviceAdapter : RecyclerView.Adapter<DeviceAdapter.DeviceViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeviceViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(android.R.layout.simple_list_item_1, parent, false)
|
||||
return DeviceViewHolder(view)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
inner class DeviceViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val textView: TextView = itemView.findViewById(android.R.id.text1)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(devices: List<BluetoothDevice>): BluetoothDeviceDialog {
|
||||
return BluetoothDeviceDialog().apply {
|
||||
|
|
@ -36,39 +63,30 @@ class BluetoothDeviceDialog : DialogFragment() {
|
|||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val builder = AlertDialog.Builder(requireContext())
|
||||
builder.setTitle("选择蓝牙设备")
|
||||
builder.setTitle("选择蓝牙设备 (找到 ${devices.size} 个设备)")
|
||||
|
||||
if (devices.isEmpty()) {
|
||||
builder.setMessage("未找到蓝牙设备")
|
||||
builder.setPositiveButton("重新扫描") { _, _ ->
|
||||
// 重新扫描
|
||||
dismiss()
|
||||
}
|
||||
builder.setNegativeButton("取消") { _, _ ->
|
||||
dismiss()
|
||||
}
|
||||
} else {
|
||||
// 创建设备列表
|
||||
val deviceNames = devices.map { device ->
|
||||
"${device.name ?: "未知设备"} (${device.address})"
|
||||
// 创建RecyclerView显示所有设备
|
||||
val recyclerView = RecyclerView(requireContext()).apply {
|
||||
layoutManager = LinearLayoutManager(requireContext())
|
||||
adapter = DeviceAdapter()
|
||||
|
||||
// 设置固定高度,避免对话框过大
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
600 // 固定高度600dp
|
||||
)
|
||||
}
|
||||
|
||||
val adapter = ArrayAdapter<String>(
|
||||
requireContext(),
|
||||
android.R.layout.simple_list_item_1,
|
||||
deviceNames
|
||||
)
|
||||
|
||||
val listView = ListView(requireContext()).apply {
|
||||
this.adapter = adapter
|
||||
setOnItemClickListener { _, _, position, _ ->
|
||||
val selectedDevice = devices[position]
|
||||
onDeviceSelectedListener?.invoke(selectedDevice)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
builder.setView(listView)
|
||||
builder.setView(recyclerView)
|
||||
builder.setNegativeButton("取消") { _, _ ->
|
||||
dismiss()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,11 +29,44 @@ class BluetoothManager(private val context: Context) {
|
|||
companion object {
|
||||
private const val TAG = "BluetoothManager"
|
||||
|
||||
// 服务UUID (根据你的设备协议调整)
|
||||
private val SERVICE_UUID = UUID.fromString("0000fff0-0000-1000-8000-00805f9b34fb")
|
||||
// 常见的ECG设备UUID列表
|
||||
private val SERVICE_UUIDS = listOf(
|
||||
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
|
||||
)
|
||||
|
||||
// 特征UUID (根据你的设备协议调整)
|
||||
private val CHARACTERISTIC_UUID = UUID.fromString("0000fff1-0000-1000-8000-00805f9b34fb")
|
||||
private val CHARACTERISTIC_UUIDS = listOf(
|
||||
UUID.fromString("6e400002-b5a3-f393-e0a9-68716563686f"), // Nordic UART TX Characteristic - 你的设备
|
||||
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
|
||||
)
|
||||
|
||||
// 设备名称前缀 (根据你的设备调整)
|
||||
private const val DEVICE_NAME_PREFIX = "ECG"
|
||||
|
|
@ -228,6 +261,10 @@ class BluetoothManager(private val context: Context) {
|
|||
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}")
|
||||
}
|
||||
|
|
@ -325,32 +362,124 @@ class BluetoothManager(private val context: Context) {
|
|||
Log.d(TAG, "服务发现成功")
|
||||
callback?.onStatusChanged("服务发现成功")
|
||||
|
||||
// 查找目标服务
|
||||
val service = gatt.getService(SERVICE_UUID)
|
||||
if (service != null) {
|
||||
// 查找目标特征
|
||||
val characteristic = service.getCharacteristic(CHARACTERISTIC_UUID)
|
||||
if (characteristic != null) {
|
||||
// 启用通知
|
||||
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {
|
||||
gatt.setCharacteristicNotification(characteristic, true)
|
||||
// 打印所有可用服务
|
||||
val services = gatt.services
|
||||
Log.d(TAG, "设备提供的服务数量: ${services.size}")
|
||||
|
||||
for (service in services) {
|
||||
Log.d(TAG, "发现服务: ${service.uuid}")
|
||||
callback?.onStatusChanged("发现服务: ${service.uuid}")
|
||||
|
||||
// 打印服务的所有特征
|
||||
val characteristics = service.characteristics
|
||||
Log.d(TAG, "服务 ${service.uuid} 的特征数量: ${characteristics.size}")
|
||||
|
||||
for (characteristic in characteristics) {
|
||||
Log.d(TAG, "发现特征: ${characteristic.uuid}")
|
||||
callback?.onStatusChanged("发现特征: ${characteristic.uuid}")
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试查找匹配的服务和特征
|
||||
var foundService: BluetoothGattService? = null
|
||||
var foundCharacteristic: BluetoothGattCharacteristic? = null
|
||||
|
||||
// 首先尝试预设的UUID列表
|
||||
for (serviceUuid in SERVICE_UUIDS) {
|
||||
val service = gatt.getService(serviceUuid)
|
||||
if (service != null) {
|
||||
Log.d(TAG, "找到匹配的服务: $serviceUuid")
|
||||
foundService = service
|
||||
|
||||
// 查找匹配的特征
|
||||
for (characteristicUuid in CHARACTERISTIC_UUIDS) {
|
||||
val characteristic = service.getCharacteristic(characteristicUuid)
|
||||
if (characteristic != null) {
|
||||
Log.d(TAG, "找到匹配的特征: $characteristicUuid")
|
||||
foundCharacteristic = characteristic
|
||||
break
|
||||
}
|
||||
}
|
||||
callback?.onStatusChanged("数据通道已建立")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 如果预设UUID没有找到,尝试智能检测
|
||||
if (foundService == null || foundCharacteristic == null) {
|
||||
Log.d(TAG, "预设UUID未找到匹配,尝试智能检测")
|
||||
|
||||
// 查找所有非标准蓝牙服务的UUID
|
||||
for (service in services) {
|
||||
val serviceUuid = service.uuid.toString()
|
||||
|
||||
// 跳过标准蓝牙服务(如2a00, 2a01等)
|
||||
if (!serviceUuid.contains("00002a") && !serviceUuid.contains("000018")) {
|
||||
Log.d(TAG, "发现自定义服务: $serviceUuid")
|
||||
|
||||
// 查找该服务下的所有特征
|
||||
for (characteristic in service.characteristics) {
|
||||
val characteristicUuid = characteristic.uuid.toString()
|
||||
|
||||
// 跳过标准特征
|
||||
if (!characteristicUuid.contains("00002a")) {
|
||||
Log.d(TAG, "发现自定义特征: $characteristicUuid")
|
||||
|
||||
// 检查特征是否支持通知
|
||||
if (characteristic.properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0 ||
|
||||
characteristic.properties and BluetoothGattCharacteristic.PROPERTY_INDICATE != 0) {
|
||||
|
||||
Log.d(TAG, "找到支持通知的特征: $characteristicUuid")
|
||||
foundService = service
|
||||
foundCharacteristic = characteristic
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (foundService != null) break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundService != null && foundCharacteristic != null) {
|
||||
// 启用通知
|
||||
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {
|
||||
Log.d(TAG, "=== 启用特征通知 ===")
|
||||
Log.d(TAG, "服务UUID: ${foundService.uuid}")
|
||||
Log.d(TAG, "特征UUID: ${foundCharacteristic.uuid}")
|
||||
Log.d(TAG, "特征属性: ${foundCharacteristic.properties}")
|
||||
|
||||
val success = gatt.setCharacteristicNotification(foundCharacteristic, true)
|
||||
Log.d(TAG, "启用特征通知结果: $success")
|
||||
|
||||
callback?.onStatusChanged("数据通道已建立,开始接收数据")
|
||||
} else {
|
||||
callback?.onError("未找到目标特征")
|
||||
Log.e(TAG, "缺少BLUETOOTH_CONNECT权限")
|
||||
callback?.onError("缺少BLUETOOTH_CONNECT权限")
|
||||
}
|
||||
} else {
|
||||
callback?.onError("未找到目标服务")
|
||||
Log.e(TAG, "未找到匹配的服务或特征")
|
||||
callback?.onError("未找到匹配的服务或特征,请检查设备协议")
|
||||
|
||||
// 提供所有可用服务的UUID供参考
|
||||
val availableServices = services.joinToString(", ") { it.uuid.toString() }
|
||||
Log.d(TAG, "可用服务UUID: $availableServices")
|
||||
callback?.onStatusChanged("可用服务: $availableServices")
|
||||
}
|
||||
} else {
|
||||
callback?.onError("服务发现失败")
|
||||
Log.e(TAG, "服务发现失败,状态: $status")
|
||||
callback?.onError("服务发现失败,状态: $status")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
|
||||
// 接收数据
|
||||
val data = characteristic.value
|
||||
Log.d(TAG, "=== 蓝牙特征值变化 ===")
|
||||
Log.d(TAG, "特征UUID: ${characteristic.uuid}")
|
||||
Log.d(TAG, "接收到数据: ${data.size} 字节")
|
||||
if (data.isNotEmpty()) {
|
||||
Log.d(TAG, "数据前10字节: ${data.take(10).joinToString(", ") { "0x%02X".format(it) }}")
|
||||
}
|
||||
callback?.onDataReceived(data)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
|
|||
interface RealTimeDataCallback {
|
||||
fun onProcessedDataAvailable(channelIndex: Int, processedData: List<Float>)
|
||||
fun onStreamingProgress(progress: Int, totalSamples: Int, processedSamples: Int)
|
||||
fun onRawDataAvailable(channelIndex: Int, rawData: List<Float>) // 新增:原始数据回调
|
||||
}
|
||||
|
||||
private var realTimeCallback: RealTimeDataCallback? = null
|
||||
|
|
@ -72,7 +73,13 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
|
|||
* 处理蓝牙通知数据块
|
||||
*/
|
||||
fun onBleNotify(chunk: ByteArray) {
|
||||
if (chunk.isEmpty()) return
|
||||
if (chunk.isEmpty()) {
|
||||
Log.w("DataManager", "接收到空的蓝牙数据块")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d("DataManager", "接收到蓝牙数据: ${chunk.size} 字节")
|
||||
Log.d("DataManager", "数据前10字节: ${chunk.take(10).joinToString(", ") { "0x%02X".format(it) }}")
|
||||
|
||||
ensureParser()
|
||||
rawStream.write(chunk)
|
||||
|
|
@ -80,12 +87,21 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
|
|||
|
||||
// 拉取解析出的数据包
|
||||
val packets = nativeCallback.streamParserDrainPackets(parserHandle)
|
||||
Log.d("DataManager", "解析结果: ${if (packets != null) packets.size else 0} 个数据包")
|
||||
|
||||
if (!packets.isNullOrEmpty()) {
|
||||
totalPacketsParsed += packets.size
|
||||
packetBuffer.addAll(packets)
|
||||
|
||||
Log.d("DataManager", "解析出 ${packets.size} 个数据包")
|
||||
|
||||
// 立即发送原始数据到图表显示
|
||||
sendRawDataToCharts(packets)
|
||||
|
||||
// 应用流式数据处理
|
||||
processStreamingData(packets)
|
||||
} else {
|
||||
Log.w("DataManager", "没有解析出有效数据包")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -161,6 +177,45 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即发送原始数据到图表显示
|
||||
*/
|
||||
private fun sendRawDataToCharts(packets: List<SensorData>) {
|
||||
if (packets.isEmpty()) {
|
||||
Log.w("DataManager", "sendRawDataToCharts: 没有数据包")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d("DataManager", "立即发送原始数据到图表,处理 ${packets.size} 个数据包")
|
||||
|
||||
// 直接处理原始数据包,不进行通道映射
|
||||
for ((packetIndex, packet) in packets.withIndex()) {
|
||||
Log.d("DataManager", "处理数据包 $packetIndex: 数据类型=${packet.getDataType()}")
|
||||
|
||||
val channelData = packet.getChannelData()
|
||||
if (channelData != null) {
|
||||
Log.d("DataManager", "数据包 $packetIndex 有 ${channelData.size} 个通道")
|
||||
for ((channelIndex, channel) in channelData.withIndex()) {
|
||||
Log.d("DataManager", "通道 $channelIndex 数据长度: ${channel.size}")
|
||||
if (channel.isNotEmpty()) {
|
||||
Log.d("DataManager", "通道 $channelIndex 前3个值: ${channel.take(3).joinToString(", ")}")
|
||||
// 立即发送原始数据到图表
|
||||
if (realTimeCallback != null) {
|
||||
realTimeCallback!!.onRawDataAvailable(channelIndex, channel)
|
||||
Log.d("DataManager", "已发送原始数据到通道 $channelIndex,数据长度: ${channel.size}")
|
||||
} else {
|
||||
Log.e("DataManager", "realTimeCallback 为空,无法发送数据")
|
||||
}
|
||||
} else {
|
||||
Log.w("DataManager", "通道 $channelIndex 数据为空")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.w("DataManager", "数据包 $packetIndex 没有通道数据")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 流式数据处理 - 将数据包按通道合并并处理
|
||||
*/
|
||||
|
|
@ -267,7 +322,7 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
|
|||
// 获取采样率
|
||||
val sampleRate = when (currentDataType) {
|
||||
type.SensorData.DataType.EEG -> 250.0
|
||||
type.SensorData.DataType.ECG_2LEAD, type.SensorData.DataType.ECG_12LEAD -> 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
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import android.Manifest
|
|||
import android.content.pm.PackageManager
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import android.view.View
|
||||
import com.example.cmake_project_test.databinding.ActivityMainBinding
|
||||
import type.SensorData
|
||||
|
||||
|
|
@ -508,7 +509,7 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
|
|||
// 触发UI更新
|
||||
uiManager.scheduleUiUpdate(dataManager) {
|
||||
val text = uiManager.buildDisplayContent(dataManager)
|
||||
binding.sampleText.text = text
|
||||
binding.sampleText.text = text
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -560,6 +561,9 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
|
|||
if (channelIndex == 0) {
|
||||
// 直接在主线程更新,避免频繁的线程切换
|
||||
try {
|
||||
// 显示ECG图表
|
||||
binding.ecgChartContainer.visibility = View.VISIBLE
|
||||
|
||||
binding.ecgRhythmView.updateData(processedData)
|
||||
binding.ecgWaveformView.updateData(processedData)
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -568,6 +572,26 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
|
|||
}
|
||||
}
|
||||
|
||||
override fun onRawDataAvailable(channelIndex: Int, rawData: List<Float>) {
|
||||
// 立即显示原始数据到图表
|
||||
try {
|
||||
Log.d("MainActivity", "收到原始数据回调,通道: $channelIndex,数据长度: ${rawData.size}")
|
||||
if (rawData.isNotEmpty()) {
|
||||
Log.d("MainActivity", "原始数据前3个值: ${rawData.take(3).joinToString(", ")}")
|
||||
}
|
||||
|
||||
// 显示ECG图表
|
||||
binding.ecgChartContainer.visibility = View.VISIBLE
|
||||
|
||||
binding.ecgRhythmView.updateData(rawData)
|
||||
binding.ecgWaveformView.updateData(rawData)
|
||||
|
||||
Log.d("MainActivity", "已更新图表,通道: $channelIndex,数据长度: ${rawData.size}")
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "显示原始数据失败: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStreamingProgress(progress: Int, totalSamples: Int, processedSamples: Int) {
|
||||
runOnUiThread {
|
||||
try {
|
||||
|
|
@ -651,11 +675,35 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
|
|||
}
|
||||
|
||||
override fun onDataReceived(data: ByteArray) {
|
||||
runOnUiThread {
|
||||
updateStatus("接收到蓝牙数据: ${data.size} 字节")
|
||||
// 将蓝牙数据传递给DataManager处理
|
||||
dataManager.onBleNotify(data)
|
||||
Log.d("MainActivity", "=== 蓝牙数据接收开始 ===")
|
||||
Log.d("MainActivity", "接收到蓝牙数据: ${data.size} 字节")
|
||||
if (data.isNotEmpty()) {
|
||||
Log.d("MainActivity", "数据前10字节: ${data.take(10).joinToString(", ") { "0x%02X".format(it) }}")
|
||||
}
|
||||
|
||||
// 在后台线程处理数据,避免阻塞UI
|
||||
Thread {
|
||||
try {
|
||||
Log.d("MainActivity", "开始处理蓝牙数据: ${data.size} 字节")
|
||||
|
||||
// 将蓝牙数据传递给DataManager处理
|
||||
dataManager.onBleNotify(data)
|
||||
|
||||
// 在UI线程更新状态和显示图表
|
||||
runOnUiThread {
|
||||
updateStatus("接收到蓝牙数据: ${data.size} 字节")
|
||||
|
||||
// 显示ECG图表
|
||||
binding.ecgChartContainer.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "处理蓝牙数据失败: ${e.message}", e)
|
||||
runOnUiThread {
|
||||
updateStatus("处理蓝牙数据失败: ${e.message}")
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
override fun onError(message: String) {
|
||||
|
|
@ -674,8 +722,10 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
|
|||
|
||||
override fun onScanComplete(devices: List<BluetoothDevice>) {
|
||||
runOnUiThread {
|
||||
updateStatus("扫描完成,找到 ${devices.size} 个设备")
|
||||
|
||||
if (devices.isNotEmpty()) {
|
||||
// 显示设备选择对话框
|
||||
// 立即显示设备选择对话框
|
||||
val dialog = BluetoothDeviceDialog.newInstance(devices)
|
||||
dialog.setOnDeviceSelectedListener { device ->
|
||||
// 连接选中的设备
|
||||
|
|
@ -686,7 +736,7 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
|
|||
}
|
||||
dialog.show(supportFragmentManager, "BluetoothDeviceDialog")
|
||||
} else {
|
||||
updateStatus("未找到蓝牙设备")
|
||||
updateStatus("未找到蓝牙设备,请重试")
|
||||
binding.bluetoothButton.text = "连接蓝牙"
|
||||
binding.bluetoothButton.setBackgroundColor(Color.parseColor("#4CAF50"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ class StreamingSignalProcessor {
|
|||
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 -> 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
|
||||
|
|
@ -230,7 +230,7 @@ class StreamingSignalProcessor {
|
|||
// EEG: 低通40Hz, 陷波50Hz
|
||||
Triple(40.0, 50.0, 30.0)
|
||||
}
|
||||
type.SensorData.DataType.ECG_2LEAD, type.SensorData.DataType.ECG_12LEAD -> {
|
||||
type.SensorData.DataType.ECG_2LEAD, type.SensorData.DataType.ECG_12LEAD, type.SensorData.DataType.PW_ECG_SL -> {
|
||||
// ECG: 专业ECG滤波参数
|
||||
// 低通100Hz (平衡信号清晰度和噪声抑制)
|
||||
// 陷波50Hz (中国工频)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import java.util.List;
|
|||
|
||||
public class SensorData {
|
||||
public enum DataType {
|
||||
EEG, ECG_2LEAD, PPG, ECG_12LEAD, MIT_BIH, STETHOSCOPE, SNORE, RESPIRATION
|
||||
EEG, ECG_2LEAD, PPG, ECG_12LEAD, MIT_BIH, STETHOSCOPE, SNORE, RESPIRATION, PW_ECG_SL
|
||||
}
|
||||
|
||||
public DataType dataType;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp"
|
||||
android:background="#E8E8E8">
|
||||
android:background="#E8E8E8"
|
||||
android:layout_marginTop="50dp">
|
||||
|
||||
<!-- 第一行按钮 -->
|
||||
<LinearLayout
|
||||
|
|
@ -82,11 +83,13 @@
|
|||
|
||||
<!-- 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:background="#F8F8F8"
|
||||
android:visibility="gone">
|
||||
|
||||
<!-- 图表标题 -->
|
||||
<LinearLayout
|
||||
|
|
|
|||
Loading…
Reference in New Issue