This commit is contained in:
ZhangJinLong 2025-09-02 18:30:21 +08:00
parent 3d4d7feae1
commit 0258157633
18 changed files with 1693 additions and 56 deletions

View File

@ -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数据了🚀

143
BLUETOOTH_UUID_DEBUG.md Normal file
View File

@ -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🎯

198
BUTTON_AND_DIALOG_FIX.md Normal file
View File

@ -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. 设备选择
- 第一个设备:点击"选择第一个设备"
- 第二个设备:点击"选择第二个设备"
- 更多设备:可以修改代码添加更多按钮
## 🎉 预期效果
### 启动时:
- ✅ 按钮位置合理,完全可见
- ✅ 界面布局清晰
- ✅ 状态信息正常显示
### 扫描时:
- ✅ 扫描过程流畅
- ✅ 设备发现信息及时更新
### 对话框:
- ✅ 快速弹出,无卡顿
- ✅ 设备列表清晰显示
- ✅ 选择按钮响应及时
### 连接后:
- ✅ 连接状态正确显示
- ✅ 按钮状态正确更新
- ✅ 可以开始数据处理
现在可以测试修复后的按钮位置和对话框性能了!🎉

View File

@ -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提高性能
- 固定对话框高度避免过大
- 扫描完成后立即弹出
## 🎉 预期效果
### 扫描过程:
- ✅ 显示扫描进度
- ✅ 实时显示发现的设备
- ✅ 扫描完成后立即提示
### 设备选择:
- ✅ 显示所有扫描到的设备
- ✅ 每个设备都可以点击
- ✅ 对话框响应流畅
### 连接过程:
- ✅ 点击设备后立即开始连接
- ✅ 连接状态正确显示
- ✅ 支持连接任意设备
现在你可以扫描所有设备并点击任意设备进行连接了!🎉

136
NORDIC_UART_ADAPTATION.md Normal file
View File

@ -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数据了🎯

132
RAW_DATA_DISPLAY_GUIDE.md Normal file
View File

@ -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
```
### 状态检查
- 蓝牙连接状态
- 数据接收频率
- 图表更新状态
- 通道数据完整性
现在你的应用可以立即显示蓝牙接收到的原始数据了!连接设备后就能看到实时波形。🚀

151
SMART_UUID_DETECTION.md Normal file
View File

@ -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
现在请重新连接你的设备,观察新的调试信息!🎯

182
UI_FIX_TEST_GUIDE.md Normal file
View File

@ -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了🎉

View File

@ -17,7 +17,8 @@ enum class DataType {
RESPIRATION, // 呼吸/姿态 RESPIRATION, // 呼吸/姿态
SNORE, // 鼾声 SNORE, // 鼾声
STETHOSCOPE, // 数字听诊 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) // 12导联心电解析 (0x4402)
SensorData parse_12lead_ecg(const uint8_t* data); SensorData parse_12lead_ecg(const uint8_t* data);
// 单通道心电解析 (0x4401) - PW_ECG-SL
SensorData parse_pw_ecg_sl(const uint8_t* data);
// MIT-BIH 212格式解析器 // MIT-BIH 212格式解析器
SensorData parse_mit_bih_212(const uint8_t* data, size_t size); SensorData parse_mit_bih_212(const uint8_t* data, size_t size);

View File

@ -38,6 +38,7 @@ SensorData Mapper::DataMapper(SensorData& data)
case DataType::SNORE: __android_log_print(ANDROID_LOG_INFO, "DataMapper", "SNORE (5)"); break; 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::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::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; default: __android_log_print(ANDROID_LOG_INFO, "DataMapper", "未知类型"); break;
} }
__android_log_print(ANDROID_LOG_INFO, "DataMapper", "包序号: %d", data_mapped.packet_sn); __android_log_print(ANDROID_LOG_INFO, "DataMapper", "包序号: %d", data_mapped.packet_sn);

View File

@ -226,6 +226,39 @@ SensorData parse_12lead_ecg(const uint8_t* data) {
return result; 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) // 数字听诊解析 (0x1102)
SensorData parse_stethoscope(const uint8_t* data) { 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: case 0x4213:
grouped_data[DataType::RESPIRATION].push_back(parse_respiration(ptr)); grouped_data[DataType::RESPIRATION].push_back(parse_respiration(ptr));
break; break;
case 0x4401:
grouped_data[DataType::PW_ECG_SL].push_back(parse_pw_ecg_sl(ptr));
break;
case 0x4402: case 0x4402:
grouped_data[DataType::ECG_12LEAD].push_back(parse_12lead_ecg(ptr)); grouped_data[DataType::ECG_12LEAD].push_back(parse_12lead_ecg(ptr));
break; break;
@ -439,6 +475,9 @@ std::vector<SensorData> parse_device_data(const std::vector<uint8_t>& file_data)
case 0x4213: case 0x4213:
grouped_data[DataType::RESPIRATION].push_back(parse_respiration(ptr)); grouped_data[DataType::RESPIRATION].push_back(parse_respiration(ptr));
break; break;
case 0x4401:
grouped_data[DataType::PW_ECG_SL].push_back(parse_pw_ecg_sl(ptr));
break;
case 0x4402: case 0x4402:
grouped_data[DataType::ECG_12LEAD].push_back(parse_12lead_ecg(ptr)); grouped_data[DataType::ECG_12LEAD].push_back(parse_12lead_ecg(ptr));
break; break;
@ -660,6 +699,9 @@ void StreamParser::parseBuffer() {
case 0x4213: case 0x4213:
packet = parse_respiration(buffer_.data()); packet = parse_respiration(buffer_.data());
break; break;
case 0x4401:
packet = parse_pw_ecg_sl(buffer_.data());
break;
case 0x4402: case 0x4402:
packet = parse_12lead_ecg(buffer_.data()); packet = parse_12lead_ecg(buffer_.data());
break; break;
@ -713,6 +755,7 @@ bool StreamParser::isValidPacket(const uint8_t* data) {
case 0x4211: // ECG_2LEAD case 0x4211: // ECG_2LEAD
case 0x4212: // SNORE case 0x4212: // SNORE
case 0x4213: // RESPIRATION case 0x4213: // RESPIRATION
case 0x4401: // PW_ECG_SL
case 0x4402: // ECG_12LEAD case 0x4402: // ECG_12LEAD
case 0x4302: // PPG case 0x4302: // PPG
case 0x1102: // STETHOSCOPE case 0x1102: // STETHOSCOPE
@ -810,6 +853,7 @@ jobject convertSensorDataToJava(JNIEnv* env, const SensorData& sensorData) {
case DataType::PPG: enumName = "PPG"; break; case DataType::PPG: enumName = "PPG"; break;
case DataType::ECG_12LEAD: enumName = "ECG_12LEAD"; break; case DataType::ECG_12LEAD: enumName = "ECG_12LEAD"; break;
case DataType::MIT_BIH: enumName = "MIT_BIH"; 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::STETHOSCOPE: enumName = "STETHOSCOPE"; break;
case DataType::SNORE: enumName = "SNORE"; break; case DataType::SNORE: enumName = "SNORE"; break;
case DataType::RESPIRATION: enumName = "RESPIRATION"; break; case DataType::RESPIRATION: enumName = "RESPIRATION"; break;

View File

@ -7,12 +7,11 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.ListView
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment 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 devices: List<BluetoothDevice> = emptyList()
private var onDeviceSelectedListener: ((BluetoothDevice) -> Unit)? = null 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 { companion object {
fun newInstance(devices: List<BluetoothDevice>): BluetoothDeviceDialog { fun newInstance(devices: List<BluetoothDevice>): BluetoothDeviceDialog {
return BluetoothDeviceDialog().apply { return BluetoothDeviceDialog().apply {
@ -36,39 +63,30 @@ class BluetoothDeviceDialog : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(requireContext()) val builder = AlertDialog.Builder(requireContext())
builder.setTitle("选择蓝牙设备") builder.setTitle("选择蓝牙设备 (找到 ${devices.size} 个设备)")
if (devices.isEmpty()) { if (devices.isEmpty()) {
builder.setMessage("未找到蓝牙设备") builder.setMessage("未找到蓝牙设备")
builder.setPositiveButton("重新扫描") { _, _ -> builder.setPositiveButton("重新扫描") { _, _ ->
// 重新扫描
dismiss() dismiss()
} }
builder.setNegativeButton("取消") { _, _ -> builder.setNegativeButton("取消") { _, _ ->
dismiss() dismiss()
} }
} else { } else {
// 创建设备列表 // 创建RecyclerView显示所有设备
val deviceNames = devices.map { device -> val recyclerView = RecyclerView(requireContext()).apply {
"${device.name ?: "未知设备"} (${device.address})" layoutManager = LinearLayoutManager(requireContext())
} adapter = DeviceAdapter()
val adapter = ArrayAdapter<String>( // 设置固定高度,避免对话框过大
requireContext(), layoutParams = ViewGroup.LayoutParams(
android.R.layout.simple_list_item_1, ViewGroup.LayoutParams.MATCH_PARENT,
deviceNames 600 // 固定高度600dp
) )
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("取消") { _, _ -> builder.setNegativeButton("取消") { _, _ ->
dismiss() dismiss()
} }

View File

@ -29,11 +29,44 @@ class BluetoothManager(private val context: Context) {
companion object { companion object {
private const val TAG = "BluetoothManager" private const val TAG = "BluetoothManager"
// 服务UUID (根据你的设备协议调整) // 常见的ECG设备UUID列表
private val SERVICE_UUID = UUID.fromString("0000fff0-0000-1000-8000-00805f9b34fb") 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_UUIDS = listOf(
private val CHARACTERISTIC_UUID = UUID.fromString("0000fff1-0000-1000-8000-00805f9b34fb") 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" private const val DEVICE_NAME_PREFIX = "ECG"
@ -228,6 +261,10 @@ class BluetoothManager(private val context: Context) {
try { try {
bluetoothLeScanner?.stopScan(bleScanCallback) bluetoothLeScanner?.stopScan(bleScanCallback)
Log.d(TAG, "BLE扫描已停止") Log.d(TAG, "BLE扫描已停止")
// 扫描完成后立即触发回调
callback?.onStatusChanged("扫描完成,找到 ${discoveredDevices.size} 个设备")
callback?.onScanComplete(discoveredDevices.toList())
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "停止BLE扫描失败: ${e.message}") Log.e(TAG, "停止BLE扫描失败: ${e.message}")
} }
@ -325,32 +362,124 @@ class BluetoothManager(private val context: Context) {
Log.d(TAG, "服务发现成功") Log.d(TAG, "服务发现成功")
callback?.onStatusChanged("服务发现成功") callback?.onStatusChanged("服务发现成功")
// 查找目标服务 // 打印所有可用服务
val service = gatt.getService(SERVICE_UUID) 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) { if (service != null) {
// 查找目标特征 Log.d(TAG, "找到匹配的服务: $serviceUuid")
val characteristic = service.getCharacteristic(CHARACTERISTIC_UUID) foundService = service
// 查找匹配的特征
for (characteristicUuid in CHARACTERISTIC_UUIDS) {
val characteristic = service.getCharacteristic(characteristicUuid)
if (characteristic != null) { if (characteristic != null) {
Log.d(TAG, "找到匹配的特征: $characteristicUuid")
foundCharacteristic = characteristic
break
}
}
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) { if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {
gatt.setCharacteristicNotification(characteristic, true) Log.d(TAG, "=== 启用特征通知 ===")
} Log.d(TAG, "服务UUID: ${foundService.uuid}")
callback?.onStatusChanged("数据通道已建立") Log.d(TAG, "特征UUID: ${foundCharacteristic.uuid}")
Log.d(TAG, "特征属性: ${foundCharacteristic.properties}")
val success = gatt.setCharacteristicNotification(foundCharacteristic, true)
Log.d(TAG, "启用特征通知结果: $success")
callback?.onStatusChanged("数据通道已建立,开始接收数据")
} else { } else {
callback?.onError("未找到目标特征") Log.e(TAG, "缺少BLUETOOTH_CONNECT权限")
callback?.onError("缺少BLUETOOTH_CONNECT权限")
} }
} else { } 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 { } else {
callback?.onError("服务发现失败") Log.e(TAG, "服务发现失败,状态: $status")
callback?.onError("服务发现失败,状态: $status")
} }
} }
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
// 接收数据 // 接收数据
val data = characteristic.value val data = characteristic.value
Log.d(TAG, "=== 蓝牙特征值变化 ===")
Log.d(TAG, "特征UUID: ${characteristic.uuid}")
Log.d(TAG, "接收到数据: ${data.size} 字节") Log.d(TAG, "接收到数据: ${data.size} 字节")
if (data.isNotEmpty()) {
Log.d(TAG, "数据前10字节: ${data.take(10).joinToString(", ") { "0x%02X".format(it) }}")
}
callback?.onDataReceived(data) callback?.onDataReceived(data)
} }
} }

View File

@ -17,6 +17,7 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
interface RealTimeDataCallback { interface RealTimeDataCallback {
fun onProcessedDataAvailable(channelIndex: Int, processedData: List<Float>) fun onProcessedDataAvailable(channelIndex: Int, processedData: List<Float>)
fun onStreamingProgress(progress: Int, totalSamples: Int, processedSamples: Int) fun onStreamingProgress(progress: Int, totalSamples: Int, processedSamples: Int)
fun onRawDataAvailable(channelIndex: Int, rawData: List<Float>) // 新增:原始数据回调
} }
private var realTimeCallback: RealTimeDataCallback? = null private var realTimeCallback: RealTimeDataCallback? = null
@ -72,7 +73,13 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
* 处理蓝牙通知数据块 * 处理蓝牙通知数据块
*/ */
fun onBleNotify(chunk: ByteArray) { 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() ensureParser()
rawStream.write(chunk) rawStream.write(chunk)
@ -80,12 +87,21 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
// 拉取解析出的数据包 // 拉取解析出的数据包
val packets = nativeCallback.streamParserDrainPackets(parserHandle) val packets = nativeCallback.streamParserDrainPackets(parserHandle)
Log.d("DataManager", "解析结果: ${if (packets != null) packets.size else 0} 个数据包")
if (!packets.isNullOrEmpty()) { if (!packets.isNullOrEmpty()) {
totalPacketsParsed += packets.size totalPacketsParsed += packets.size
packetBuffer.addAll(packets) packetBuffer.addAll(packets)
Log.d("DataManager", "解析出 ${packets.size} 个数据包")
// 立即发送原始数据到图表显示
sendRawDataToCharts(packets)
// 应用流式数据处理 // 应用流式数据处理
processStreamingData(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) { val sampleRate = when (currentDataType) {
type.SensorData.DataType.EEG -> 250.0 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.PPG -> 50.0
type.SensorData.DataType.STETHOSCOPE -> 8000.0 type.SensorData.DataType.STETHOSCOPE -> 8000.0
type.SensorData.DataType.SNORE -> 8000.0 type.SensorData.DataType.SNORE -> 8000.0

View File

@ -9,6 +9,7 @@ import android.Manifest
import android.content.pm.PackageManager import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import android.view.View
import com.example.cmake_project_test.databinding.ActivityMainBinding import com.example.cmake_project_test.databinding.ActivityMainBinding
import type.SensorData import type.SensorData
@ -560,6 +561,9 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
if (channelIndex == 0) { if (channelIndex == 0) {
// 直接在主线程更新,避免频繁的线程切换 // 直接在主线程更新,避免频繁的线程切换
try { try {
// 显示ECG图表
binding.ecgChartContainer.visibility = View.VISIBLE
binding.ecgRhythmView.updateData(processedData) binding.ecgRhythmView.updateData(processedData)
binding.ecgWaveformView.updateData(processedData) binding.ecgWaveformView.updateData(processedData)
} catch (e: Exception) { } 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) { override fun onStreamingProgress(progress: Int, totalSamples: Int, processedSamples: Int) {
runOnUiThread { runOnUiThread {
try { try {
@ -651,11 +675,35 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
} }
override fun onDataReceived(data: ByteArray) { override fun onDataReceived(data: ByteArray) {
runOnUiThread { Log.d("MainActivity", "=== 蓝牙数据接收开始 ===")
updateStatus("接收到蓝牙数据: ${data.size} 字节") 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处理
dataManager.onBleNotify(data) 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) { override fun onError(message: String) {
@ -674,8 +722,10 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
override fun onScanComplete(devices: List<BluetoothDevice>) { override fun onScanComplete(devices: List<BluetoothDevice>) {
runOnUiThread { runOnUiThread {
updateStatus("扫描完成,找到 ${devices.size} 个设备")
if (devices.isNotEmpty()) { if (devices.isNotEmpty()) {
// 显示设备选择对话框 // 立即显示设备选择对话框
val dialog = BluetoothDeviceDialog.newInstance(devices) val dialog = BluetoothDeviceDialog.newInstance(devices)
dialog.setOnDeviceSelectedListener { device -> dialog.setOnDeviceSelectedListener { device ->
// 连接选中的设备 // 连接选中的设备
@ -686,7 +736,7 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
} }
dialog.show(supportFragmentManager, "BluetoothDeviceDialog") dialog.show(supportFragmentManager, "BluetoothDeviceDialog")
} else { } else {
updateStatus("未找到蓝牙设备") updateStatus("未找到蓝牙设备,请重试")
binding.bluetoothButton.text = "连接蓝牙" binding.bluetoothButton.text = "连接蓝牙"
binding.bluetoothButton.setBackgroundColor(Color.parseColor("#4CAF50")) binding.bluetoothButton.setBackgroundColor(Color.parseColor("#4CAF50"))
} }

View File

@ -211,7 +211,7 @@ class StreamingSignalProcessor {
private fun getSampleRateForDataType(dataType: type.SensorData.DataType): Double { private fun getSampleRateForDataType(dataType: type.SensorData.DataType): Double {
return when (dataType) { return when (dataType) {
type.SensorData.DataType.EEG -> 250.0 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.PPG -> 50.0
type.SensorData.DataType.STETHOSCOPE -> 8000.0 type.SensorData.DataType.STETHOSCOPE -> 8000.0
type.SensorData.DataType.SNORE -> 8000.0 type.SensorData.DataType.SNORE -> 8000.0
@ -230,7 +230,7 @@ class StreamingSignalProcessor {
// EEG: 低通40Hz, 陷波50Hz // EEG: 低通40Hz, 陷波50Hz
Triple(40.0, 50.0, 30.0) 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滤波参数 // ECG: 专业ECG滤波参数
// 低通100Hz (平衡信号清晰度和噪声抑制) // 低通100Hz (平衡信号清晰度和噪声抑制)
// 陷波50Hz (中国工频) // 陷波50Hz (中国工频)

View File

@ -3,7 +3,7 @@ import java.util.List;
public class SensorData { public class SensorData {
public enum DataType { 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; public DataType dataType;

View File

@ -12,7 +12,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:padding="8dp" android:padding="8dp"
android:background="#E8E8E8"> android:background="#E8E8E8"
android:layout_marginTop="50dp">
<!-- 第一行按钮 --> <!-- 第一行按钮 -->
<LinearLayout <LinearLayout
@ -82,11 +83,13 @@
<!-- ECG图表区域 --> <!-- ECG图表区域 -->
<LinearLayout <LinearLayout
android:id="@+id/ecg_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"
android:orientation="vertical" android:orientation="vertical"
android:background="#F8F8F8"> android:background="#F8F8F8"
android:visibility="gone">
<!-- 图表标题 --> <!-- 图表标题 -->
<LinearLayout <LinearLayout