From cb28adca69d0347e3ed716fa62280269b09bd3c3 Mon Sep 17 00:00:00 2001 From: ZhangJinLong <19357383190@163.com> Date: Wed, 3 Sep 2025 12:42:59 +0800 Subject: [PATCH] ECG --- MAC地址格式说明.md | 88 + README_蓝牙指令使用说明.md | 173 ++ app/src/main/AndroidManifest.xml | 12 +- .../cmake_project_test/BluetoothManager.kt | 1140 ++++++++- .../example/cmake_project_test/DataManager.kt | 180 +- .../cmake_project_test/ECGChartView.kt | 83 +- .../cmake_project_test/ECGRhythmView.kt | 3 + .../cmake_project_test/ECGWaveformView.kt | 3 + .../cmake_project_test/MainActivity.kt | 2108 ++++++++++++++--- .../StreamingSignalProcessor.kt | 16 +- app/src/main/res/layout/activity_main.xml | 38 +- app/设备接口文档250715.pdf | Bin 0 -> 151767 bytes app/轻迅蓝牙通信协议V1.0.1_pw.pdf | Bin 0 -> 509957 bytes 应用运行状态总结.md | 141 ++ 快速故障排除指南.md | 116 + 快速测试指南.md | 105 + 手机连接测试指南.md | 120 + 测试总结.md | 143 ++ 编译错误修复总结.md | 91 + 蓝牙扫描故障排除指南.md | 217 ++ 蓝牙权限配置指南.md | 177 ++ 蓝牙调试指南.md | 183 ++ 蓝牙连接测试说明.md | 96 + 连接状态显示增强.md | 98 + 连接超时故障排除指南.md | 164 ++ 25 files changed, 4981 insertions(+), 514 deletions(-) create mode 100644 MAC地址格式说明.md create mode 100644 README_蓝牙指令使用说明.md create mode 100644 app/设备接口文档250715.pdf create mode 100644 app/轻迅蓝牙通信协议V1.0.1_pw.pdf create mode 100644 应用运行状态总结.md create mode 100644 快速故障排除指南.md create mode 100644 快速测试指南.md create mode 100644 手机连接测试指南.md create mode 100644 测试总结.md create mode 100644 编译错误修复总结.md create mode 100644 蓝牙扫描故障排除指南.md create mode 100644 蓝牙权限配置指南.md create mode 100644 蓝牙调试指南.md create mode 100644 蓝牙连接测试说明.md create mode 100644 连接状态显示增强.md create mode 100644 连接超时故障排除指南.md diff --git a/MAC地址格式说明.md b/MAC地址格式说明.md new file mode 100644 index 0000000..129f968 --- /dev/null +++ b/MAC地址格式说明.md @@ -0,0 +1,88 @@ +# MAC地址格式说明 + +## 问题解决 + +您遇到的错误 "60-E9-AA-30-0B-0A is not a valid Bluetooth address" 是因为MAC地址格式问题。 + +## 正确的MAC地址格式 + +### 支持的格式: +1. **冒号分隔符**(推荐):`60:E9:AA:30:8B:0A` +2. **连字符分隔符**:`60-E9-AA-30-8B-0A` + +### 您的电脑MAC地址: +- **正确格式**:`60:E9:AA:30:8B:0A` +- **注意**:您之前输入的是 `60-E9-AA-30-0B-0A`,其中 `0B` 应该是 `8B` + +## 常见MAC地址格式错误 + +### 1. 分隔符错误 +- ❌ 错误:`60.E9.AA.30.8B.0A`(点号分隔符) +- ❌ 错误:`60 E9 AA 30 8B 0A`(空格分隔符) +- ✅ 正确:`60:E9:AA:30:8B:0A`(冒号分隔符) + +### 2. 字符错误 +- ❌ 错误:`60-E9-AA-30-0B-0A`(0B 应该是 8B) +- ✅ 正确:`60:E9:AA:30:8B:0A` + +### 3. 长度错误 +- ❌ 错误:`60:E9:AA:30:8B`(缺少两位) +- ❌ 错误:`60:E9:AA:30:8B:0A:FF`(多出两位) +- ✅ 正确:`60:E9:AA:30:8B:0A`(6组,每组2位) + +## 如何获取正确的MAC地址 + +### Windows系统: +1. 打开命令提示符(cmd) +2. 输入:`ipconfig /all` +3. 查找"物理地址"或"Physical Address" +4. 格式类似:`60-E9-AA-30-8B-0A` + +### 转换为冒号格式: +- 将连字符 `-` 替换为冒号 `:` +- `60-E9-AA-30-8B-0A` → `60:E9:AA:30:8B:0A` + +## 测试步骤 + +### 1. 确认MAC地址 +1. 在Windows命令提示符中输入:`ipconfig /all` +2. 找到您的蓝牙适配器的物理地址 +3. 确认地址格式正确 + +### 2. 使用应用连接 +1. 启动应用 +2. **长按"连接蓝牙"按钮** +3. 输入正确的MAC地址:`60:E9:AA:30:8B:0A` +4. 点击"连接" + +### 3. 验证连接 +1. 观察连接状态 +2. 查看日志信息 +3. 如果仍有问题,尝试扫描连接 + +## 常见问题 + +### Q: 为什么需要冒号分隔符? +A: Android蓝牙API更推荐使用冒号分隔符,兼容性更好。 + +### Q: 我的MAC地址是连字符格式怎么办? +A: 应用现在支持两种格式,但推荐使用冒号格式。 + +### Q: 连接仍然失败怎么办? +A: +1. 确认MAC地址正确 +2. 检查蓝牙权限 +3. 确保设备在范围内 +4. 尝试扫描连接方式 + +## 调试技巧 + +### 查看日志: +在Android Studio的Logcat中查看: +- `BluetoothManager` 标签的详细错误信息 +- 确认MAC地址格式验证结果 + +### 测试连接: +1. 先使用扫描功能找到设备 +2. 记录正确的MAC地址 +3. 再使用直接连接功能 diff --git a/README_蓝牙指令使用说明.md b/README_蓝牙指令使用说明.md new file mode 100644 index 0000000..24422ed --- /dev/null +++ b/README_蓝牙指令使用说明.md @@ -0,0 +1,173 @@ +# 蓝牙指令使用说明 + +## 功能概述 +本应用支持通过蓝牙连接设备后,发送指令让设备开始发送单导联ECG数据,然后实时显示ECG曲线图。专门优化用于处理单导联蓝牙实时数据,不再读取12导联文件数据。 + +## 支持的协议 + +### 轻迅蓝牙通信协议 V1.0.1 +本应用完全支持轻迅蓝牙通信协议,包括: +- 数据服务 UUID: `6e400001-b5a3-f393-e0a9-68716563686f` +- Write Characteristic UUID: `6e400002-b5a3-f393-e0a9-68716563686f` +- Notify Characteristic UUID: `6e400003-b5a3-f393-e0a9-68716563686f` + +## 使用步骤 + +### 1. 连接蓝牙设备 +- 点击"连接蓝牙"按钮 +- 选择您的ECG设备 +- 等待连接成功 + +### 2. 发送指令 +连接成功后,点击"发送指令"按钮,会弹出指令选择对话框: + +#### 指令类型选择: +- **轻迅协议指令**:使用轻迅蓝牙通信协议的标准指令 +- **通用指令**:使用通用的字符串或十六进制指令 +- **自定义指令**:输入自定义指令 + +### 3. 轻迅协议指令 +选择"轻迅协议指令"后,会显示以下选项: + +#### 采集控制: +- **开启采集**:发送功能码 `0x0001` 开启数据采集 +- **停止采集**:发送功能码 `0x0001` 停止数据采集 + +#### 设备信息: +- **查询设备信息**:发送功能码 `0x0000` 查询设备参数 +- **查询电量**:发送功能码 `0x0002` 查询设备电量 + +#### 滤波控制: +- **工频滤波开关**:发送功能码 `0x000A` 控制工频滤波 + +### 4. 通用指令 +选择"通用指令"后,会显示以下选项: +- **开始发送数据**:让设备开始发送数据流 +- **停止发送数据**:让设备停止发送数据 +- **开始ECG测量**:开始ECG信号测量 +- **停止ECG测量**:停止ECG信号测量 + +#### 常用指令格式: +- **字符串指令**:`START_DATA`, `STOP`, `BEGIN`, `END` +- **十六进制指令**:`0x01`, `0x02`, `0x53 0x54 0x41 0x52 0x54` + +### 5. 查看实时图表 +发送指令成功后,应用会自动启动数据处理并立即开始绘制单导联ECG图表: +- **自动启动**:发送指令后自动启动数据处理,无需手动点击"启动程序" +- **实时显示**:单导联数据接收后立即更新图表,无需等待 +- **单导联处理**:专门处理单导联ECG数据,主要显示主导联信号 +- **ECG节律视图**:显示10秒的连续单导联信号 +- **ECG波形视图**:显示2.5秒的放大单导联信号 + +### 6. 其他功能 +- **陷波滤波**:开启/关闭50Hz陷波滤波器 +- **清空数据**:清空所有图表数据 +- **停止程序**:停止数据处理 + +## 轻迅协议指令详解 + +### 采集控制指令 + +#### 开启采集 (功能码: 0x0001) +``` +数据包格式: [功能码(2字节)] [数据长度(2字节)] [采集开关(1字节)] [时间戳(8字节)] [CRC16(2字节)] +示例: 01 00 09 00 01 [时间戳8字节] [CRC16 2字节] +``` + +**时间戳选项:** +- **立即开启**:时间戳设为0 +- **延迟开启**:时间戳设为未来时间(毫秒) +- **指定时间戳**:自定义时间戳 + +#### 停止采集 (功能码: 0x0001) +``` +数据包格式: [功能码(2字节)] [数据长度(2字节)] [采集开关(1字节)] [时间戳(8字节)] [CRC16(2字节)] +示例: 01 00 09 00 00 [时间戳8字节] [CRC16 2字节] +``` + +### 设备信息指令 + +#### 查询设备信息 (功能码: 0x0000) +``` +数据包格式: [功能码(2字节)] [数据长度(2字节)] [CRC16(2字节)] +示例: 00 00 00 00 [CRC16 2字节] +``` + +#### 查询电量 (功能码: 0x0002) +``` +数据包格式: [功能码(2字节)] [数据长度(2字节)] [CRC16(2字节)] +示例: 02 00 00 00 [CRC16 2字节] +``` + +### 滤波控制指令 + +#### 工频滤波开关 (功能码: 0x000A) +``` +数据包格式: [功能码(2字节)] [数据长度(2字节)] [开关状态(1字节)] [CRC16(2字节)] +开启示例: 0A 00 01 00 01 [CRC16 2字节] +关闭示例: 0A 00 01 00 00 [CRC16 2字节] +``` + +## 通用指令示例 + +### 开始数据流 +``` +START_DATA +START +0x01 +``` + +### 停止数据流 +``` +STOP_DATA +STOP +0x02 +``` + +### 开始ECG测量 +``` +START_ECG +ECG_START +0x03 +``` + +### 停止ECG测量 +``` +STOP_ECG +ECG_STOP +0x04 +``` + +## 注意事项 +1. 确保蓝牙设备已正确连接 +2. 根据您的设备协议选择合适的指令类型 +3. 轻迅协议设备会自动计算CRC16校验,确保数据完整性 +4. 如果指令发送失败,请检查设备是否支持该指令 +5. 可以尝试不同的指令格式(字符串或十六进制) + +## 调试信息 +应用会在状态栏显示详细的调试信息,包括: +- 蓝牙连接状态 +- 指令发送结果(包括轻迅协议数据包内容) +- 数据接收情况 +- 图表更新状态 + +## 轻迅协议特性 +- **自动CRC16校验**:所有指令包都包含CRC16-CCITT-FALSE校验 +- **时间戳同步**:支持指定时间戳进行同步采集 +- **工频滤波控制**:可动态开启/关闭工频滤波算法 +- **设备信息查询**:支持查询设备状态和电量信息 +- **自动数据处理**:发送指令后自动启动数据处理,无需手动操作 +- **实时图表更新**:单导联数据接收后立即更新图表,提供最佳用户体验 +- **单导联优化**:专门优化处理单导联ECG数据,不再处理12导联文件数据 + +## 测试功能 +- **点击"启动程序"按钮**:立即生成测试数据并显示图表,用于验证图表显示功能 +- **长按"启动程序"按钮**:生成更复杂的模拟ECG数据 +- **蓝牙数据测试**:当接收到蓝牙数据但原生解析器无法解析时,会自动生成测试数据 + +### 测试数据说明 +应用会生成模拟的单导联ECG波形,包含: +- P波、QRS复合波、T波等典型ECG特征 +- 适当的噪声模拟真实信号 +- 500个数据点用于充分显示波形 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index da7e116..2534e3d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,18 +2,24 @@ - + - + + + + + - () + // 当前连接的服务和特征 + private var currentService: BluetoothGattService? = null + private var currentCharacteristic: BluetoothGattCharacteristic? = null + private var currentNotifyCharacteristic: BluetoothGattCharacteristic? = null + + // 协议状态 + private var deviceInfo: DeviceInfo? = null + private var isCollecting = false + private var currentSampleRate = Protocol.SAMPLE_RATE_500 + private var currentGain = Protocol.GAIN_1 + private var currentLead = Protocol.LEAD_II + private var powerLineFilterEnabled = false + + // 数据统计 + private var totalPacketsReceived = 0L + private var totalBytesReceived = 0L + private var totalPacketsSent = 0L + private var totalBytesSent = 0L + // 回调接口 interface BluetoothCallback { fun onDeviceFound(device: BluetoothDevice) @@ -93,8 +211,24 @@ class BluetoothManager(private val context: Context) { fun onError(message: String) fun onStatusChanged(status: String) fun onScanComplete(devices: List) + fun onCommandSent(success: Boolean, message: String) + fun onProtocolDataReceived(functionCode: Int, data: ByteArray) + fun onDeviceInfoReceived(deviceInfo: DeviceInfo) + fun onBatteryLevelReceived(level: Int) + fun onCollectionStatusChanged(isCollecting: Boolean) } + // 设备信息数据类 + data class DeviceInfo( + val deviceId: String, + val firmwareVersion: String, + val hardwareVersion: String, + val serialNumber: String, + val manufacturer: String, + val model: String, + val capabilities: List + ) + private var callback: BluetoothCallback? = null init { @@ -106,8 +240,8 @@ class BluetoothManager(private val context: Context) { */ private fun initializeBluetooth() { try { - bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager - bluetoothAdapter = bluetoothManager?.adapter + bluetoothManagerService = context.getSystemService(Context.BLUETOOTH_SERVICE) as android.bluetooth.BluetoothManager + bluetoothAdapter = bluetoothManagerService?.adapter if (bluetoothAdapter == null) { Log.e(TAG, "设备不支持蓝牙") @@ -150,19 +284,27 @@ class BluetoothManager(private val context: Context) { */ fun startScan() { if (bluetoothAdapter == null) { - callback?.onError("蓝牙未初始化") + callback?.onError("❌ 蓝牙未初始化") + callback?.onStatusChanged("💡 请检查设备是否支持蓝牙") return } if (!bluetoothAdapter!!.isEnabled) { - callback?.onError("蓝牙未启用") + callback?.onError("❌ 蓝牙未启用") + callback?.onStatusChanged("💡 请在系统设置中开启蓝牙") return } try { // 清空之前的设备列表 discoveredDevices.clear() - callback?.onStatusChanged("正在扫描蓝牙设备...") + callback?.onStatusChanged("🔍 开始扫描蓝牙设备...") + callback?.onStatusChanged("⏱️ 扫描时间: 15秒") + callback?.onStatusChanged("💡 请确保目标设备:") + callback?.onStatusChanged(" • 蓝牙已开启") + callback?.onStatusChanged(" • 设置为可发现模式") + callback?.onStatusChanged(" • 在10米范围内") + callback?.onStatusChanged(" • 未与其他设备连接") // 检查所有必要权限 val missingPermissions = mutableListOf() @@ -180,21 +322,27 @@ class BluetoothManager(private val context: Context) { } if (missingPermissions.isNotEmpty()) { - callback?.onError("缺少权限: ${missingPermissions.joinToString(", ")}") + callback?.onError("❌ 缺少权限: ${missingPermissions.joinToString(", ")}") + callback?.onStatusChanged("💡 请在应用设置中授予蓝牙和位置权限") return } + callback?.onStatusChanged("✅ 权限检查通过,开始扫描...") + // 优先使用BLE扫描 if (bluetoothLeScanner != null) { + callback?.onStatusChanged("📡 使用BLE扫描模式") startBleScan() } else { // 回退到传统蓝牙扫描 + callback?.onStatusChanged("📡 使用传统蓝牙扫描模式") startClassicScan() } } catch (e: Exception) { Log.e(TAG, "扫描失败: ${e.message}") - callback?.onError("扫描失败: ${e.message}") + callback?.onError("❌ 扫描失败: ${e.message}") + callback?.onStatusChanged("💡 请尝试重启蓝牙或重启设备") } } @@ -205,19 +353,23 @@ class BluetoothManager(private val context: Context) { try { val scanSettings = ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .setReportDelay(0) // 立即报告结果 .build() bluetoothLeScanner?.startScan(null, scanSettings, bleScanCallback) Log.d(TAG, "BLE扫描已开始") + callback?.onStatusChanged("📡 BLE扫描进行中...") - // 10秒后停止扫描 + // 15秒后停止扫描 android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ stopBleScan() - }, 10000) + }, 15000) } catch (e: Exception) { Log.e(TAG, "BLE扫描失败: ${e.message}") - callback?.onError("BLE扫描失败: ${e.message}") + callback?.onError("❌ BLE扫描失败: ${e.message}") + callback?.onStatusChanged("💡 尝试使用传统蓝牙扫描...") + startClassicScan() } } @@ -229,15 +381,17 @@ class BluetoothManager(private val context: Context) { registerClassicScanReceiver() bluetoothAdapter?.startDiscovery() Log.d(TAG, "传统蓝牙扫描已开始") + callback?.onStatusChanged("📡 传统蓝牙扫描进行中...") - // 10秒后停止扫描 + // 15秒后停止扫描 android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ stopClassicScan() - }, 10000) + }, 15000) } catch (e: Exception) { Log.e(TAG, "传统蓝牙扫描失败: ${e.message}") - callback?.onError("传统蓝牙扫描失败: ${e.message}") + callback?.onError("❌ 传统蓝牙扫描失败: ${e.message}") + callback?.onStatusChanged("💡 请检查蓝牙设置或重启设备") } } @@ -263,7 +417,19 @@ class BluetoothManager(private val context: Context) { Log.d(TAG, "BLE扫描已停止") // 扫描完成后立即触发回调 - callback?.onStatusChanged("扫描完成,找到 ${discoveredDevices.size} 个设备") + callback?.onStatusChanged("🔍 BLE扫描完成") + callback?.onStatusChanged("📊 扫描结果:找到 ${discoveredDevices.size} 个设备") + + if (discoveredDevices.isEmpty()) { + callback?.onStatusChanged("⚠️ 未发现任何设备") + callback?.onStatusChanged("💡 建议:") + callback?.onStatusChanged(" • 检查目标设备是否开启蓝牙") + callback?.onStatusChanged(" • 确保设备在可见范围内") + callback?.onStatusChanged(" • 尝试重启蓝牙服务") + } else { + callback?.onStatusChanged("✅ 扫描成功,请选择要连接的设备") + } + callback?.onScanComplete(discoveredDevices.toList()) } catch (e: Exception) { Log.e(TAG, "停止BLE扫描失败: ${e.message}") @@ -321,6 +487,8 @@ class BluetoothManager(private val context: Context) { bluetoothGatt?.disconnect() bluetoothGatt?.close() bluetoothGatt = null + currentService = null + currentCharacteristic = null isConnected = false isConnecting = false callback?.onStatusChanged("已断开连接") @@ -329,6 +497,705 @@ class BluetoothManager(private val context: Context) { } } + /** + * 发送指令到设备 + */ + fun sendCommand(command: ByteArray) { + if (!isConnected) { + callback?.onCommandSent(false, "设备未连接") + return + } + + if (currentCharacteristic == null) { + callback?.onCommandSent(false, "特征未找到,无法发送指令") + return + } + + try { + // 检查权限 + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) { + callback?.onCommandSent(false, "缺少蓝牙连接权限") + return + } + + // 设置特征值 + currentCharacteristic!!.value = command + + // 发送数据 + val success = bluetoothGatt?.writeCharacteristic(currentCharacteristic!!) ?: false + + if (success) { + totalPacketsSent++ + totalBytesSent += command.size + Log.d(TAG, "指令发送成功: ${command.joinToString(", ") { "0x%02X".format(it) }}") + callback?.onCommandSent(true, "指令发送成功") + callback?.onStatusChanged("已发送指令: ${command.joinToString(", ") { "0x%02X".format(it) }}") + } else { + Log.e(TAG, "指令发送失败") + callback?.onCommandSent(false, "指令发送失败") + } + + } catch (e: Exception) { + Log.e(TAG, "发送指令异常: ${e.message}") + callback?.onCommandSent(false, "发送指令异常: ${e.message}") + } + } + + /** + * 构建轻迅协议数据包 + * 格式: [功能码(2字节)] [数据长度(2字节)] [数据内容] [CRC16(2字节)] + */ + private fun buildProtocolPacket(functionCode: Int, data: ByteArray = ByteArray(0)): ByteArray { + val dataLength = data.size + val packetSize = Protocol.PACKET_HEADER_SIZE + dataLength + Protocol.CRC_SIZE + val packet = ByteArray(packetSize) + + var index = 0 + + // 功能码 (小端格式) + packet[index++] = (functionCode and 0xFF).toByte() + packet[index++] = ((functionCode shr 8) and 0xFF).toByte() + + // 数据长度 (小端格式) + packet[index++] = (dataLength and 0xFF).toByte() + packet[index++] = ((dataLength shr 8) and 0xFF).toByte() + + // 数据内容 + if (data.isNotEmpty()) { + System.arraycopy(data, 0, packet, index, dataLength) + index += dataLength + } + + // CRC16校验 + val crc = calculateCRC16(packet, 0, Protocol.PACKET_HEADER_SIZE + dataLength) + packet[index++] = (crc and 0xFF).toByte() + packet[index++] = ((crc shr 8) and 0xFF).toByte() + + return packet + } + + /** + * 解析轻迅协议数据包 + */ + private fun parseProtocolPacket(packet: ByteArray): Triple? { + if (packet.size < Protocol.MIN_PACKET_SIZE) { + Log.e(TAG, "数据包长度不足: ${packet.size} < ${Protocol.MIN_PACKET_SIZE}") + return null + } + + try { + // 功能码 (小端格式) + val functionCode = (packet[1].toInt() and 0xFF shl 8) or (packet[0].toInt() and 0xFF) + + // 数据长度 (小端格式) + val dataLength = (packet[3].toInt() and 0xFF shl 8) or (packet[2].toInt() and 0xFF) + + // 验证数据包长度 + val expectedLength = Protocol.PACKET_HEADER_SIZE + dataLength + Protocol.CRC_SIZE + if (packet.size != expectedLength) { + Log.e(TAG, "数据包长度不匹配: 实际=${packet.size}, 期望=${expectedLength}") + return null + } + + // 提取数据内容 + val data = if (dataLength > 0) { + ByteArray(dataLength) + } else { + ByteArray(0) + } + + if (dataLength > 0) { + System.arraycopy(packet, Protocol.PACKET_HEADER_SIZE, data, 0, dataLength) + } + + // 验证CRC16 + val calculatedCrc = calculateCRC16(packet, 0, Protocol.PACKET_HEADER_SIZE + dataLength) + val receivedCrc = (packet[packet.size - 1].toInt() and 0xFF shl 8) or (packet[packet.size - 2].toInt() and 0xFF) + + if (calculatedCrc != receivedCrc) { + Log.e(TAG, "CRC校验失败: 计算=${calculatedCrc}, 接收=${receivedCrc}") + return null + } + + return Triple(functionCode, dataLength, data) + + } catch (e: Exception) { + Log.e(TAG, "解析数据包异常: ${e.message}") + return null + } + } + + /** + * 处理接收到的协议数据 + */ + private fun handleProtocolData(packet: ByteArray) { + // 首先尝试解析为轻迅协议数据包 + val parsed = parseProtocolPacket(packet) + if (parsed != null) { + val (functionCode, dataLength, data) = parsed + + totalPacketsReceived++ + totalBytesReceived += packet.size + + Log.d(TAG, "收到协议数据: 功能码=0x${functionCode.toString(16).uppercase()}, 数据长度=$dataLength") + + // 根据功能码处理数据 + when (functionCode) { + Protocol.FUNC_QUERY_DEVICE_INFO -> handleDeviceInfoResponse(data) + Protocol.FUNC_QUERY_BATTERY -> handleBatteryResponse(data) + Protocol.FUNC_START_COLLECTION -> handleCollectionResponse(data) + Protocol.FUNC_DATA_STREAM -> handleDataStream(data) + Protocol.FUNC_ALARM_DATA -> handleAlarmData(data) + Protocol.FUNC_STATUS_REPORT -> handleStatusReport(data) + else -> { + Log.w(TAG, "未知功能码: 0x${functionCode.toString(16).uppercase()}") + callback?.onProtocolDataReceived(functionCode, data) + } + } + } else { + // 如果协议解析失败,尝试其他解析方式 + Log.w(TAG, "轻迅协议解析失败,尝试其他解析方式") + handleRawData(packet) + } + } + + /** + * 处理原始数据(非标准协议格式) + */ + private fun handleRawData(data: ByteArray) { + try { + Log.d(TAG, "处理原始数据: ${data.size} 字节") + + // 检查是否是数据流(功能码0x8000) + if (data.size >= 2) { + val functionCode = (data[1].toInt() and 0xFF shl 8) or (data[0].toInt() and 0xFF) + + if (functionCode == Protocol.FUNC_DATA_STREAM) { + Log.d(TAG, "检测到数据流功能码: 0x${functionCode.toString(16).uppercase()}") + + // 提取数据内容(跳过功能码) + val dataContent = if (data.size > 2) { + ByteArray(data.size - 2) + } else { + ByteArray(0) + } + + if (dataContent.isNotEmpty()) { + System.arraycopy(data, 2, dataContent, 0, dataContent.size) + } + + // 处理数据流 + handleDataStream(dataContent) + return + } + } + + // 如果不是已知格式,直接传递给回调 + Log.d(TAG, "未知数据格式,直接传递给回调") + callback?.onDataReceived(data) + callback?.onProtocolDataReceived(0xFFFF, data) // 使用特殊功能码表示原始数据 + + } catch (e: Exception) { + Log.e(TAG, "处理原始数据异常: ${e.message}") + // 异常情况下仍然传递给回调 + callback?.onDataReceived(data) + } + } + + /** + * 处理设备信息响应 + */ + private fun handleDeviceInfoResponse(data: ByteArray) { + try { + if (data.size < 20) { + Log.e(TAG, "设备信息数据长度不足: ${data.size}") + return + } + + var index = 0 + + // 设备ID (8字节) + val deviceId = String(data, index, 8).trim { it <= ' ' } + index += 8 + + // 固件版本 (4字节) + val firmwareVersion = String(data, index, 4).trim { it <= ' ' } + index += 4 + + // 硬件版本 (4字节) + val hardwareVersion = String(data, index, 4).trim { it <= ' ' } + index += 4 + + // 序列号 (4字节) + val serialNumber = String(data, index, 4).trim { it <= ' ' } + index += 4 + + // 制造商和型号 (剩余字节) + val remainingData = String(data, index, data.size - index).trim { it <= ' ' } + val parts = remainingData.split("|") + val manufacturer = if (parts.isNotEmpty()) parts[0] else "" + val model = if (parts.size > 1) parts[1] else "" + + deviceInfo = DeviceInfo( + deviceId = deviceId, + firmwareVersion = firmwareVersion, + hardwareVersion = hardwareVersion, + serialNumber = serialNumber, + manufacturer = manufacturer, + model = model, + capabilities = listOf("ECG", "BLE", "轻迅协议V${Protocol.VERSION}") + ) + + Log.d(TAG, "设备信息: $deviceInfo") + callback?.onDeviceInfoReceived(deviceInfo!!) + callback?.onStatusChanged("设备信息已接收: $manufacturer $model") + + } catch (e: Exception) { + Log.e(TAG, "处理设备信息响应异常: ${e.message}") + } + } + + /** + * 处理电量响应 + */ + private fun handleBatteryResponse(data: ByteArray) { + try { + if (data.isNotEmpty()) { + val batteryLevel = data[0].toInt() and 0xFF + Log.d(TAG, "电量: $batteryLevel%") + callback?.onBatteryLevelReceived(batteryLevel) + callback?.onStatusChanged("电量: $batteryLevel%") + } + } catch (e: Exception) { + Log.e(TAG, "处理电量响应异常: ${e.message}") + } + } + + /** + * 处理采集响应 + */ + private fun handleCollectionResponse(data: ByteArray) { + try { + if (data.isNotEmpty()) { + val status = data[0].toInt() and 0xFF + isCollecting = status == Protocol.COLLECTION_ON + Log.d(TAG, "采集状态: ${if (isCollecting) "开启" else "关闭"}") + callback?.onCollectionStatusChanged(isCollecting) + callback?.onStatusChanged("采集状态: ${if (isCollecting) "开启" else "关闭"}") + } + } catch (e: Exception) { + Log.e(TAG, "处理采集响应异常: ${e.message}") + } + } + + /** + * 处理数据流 + */ + private fun handleDataStream(data: ByteArray) { + try { + Log.d(TAG, "收到数据流: ${data.size} 字节") + callback?.onDataReceived(data) + callback?.onProtocolDataReceived(Protocol.FUNC_DATA_STREAM, data) + } catch (e: Exception) { + Log.e(TAG, "处理数据流异常: ${e.message}") + } + } + + /** + * 处理报警数据 + */ + private fun handleAlarmData(data: ByteArray) { + try { + Log.d(TAG, "收到报警数据: ${data.size} 字节") + callback?.onProtocolDataReceived(Protocol.FUNC_ALARM_DATA, data) + callback?.onStatusChanged("收到报警数据") + } catch (e: Exception) { + Log.e(TAG, "处理报警数据异常: ${e.message}") + } + } + + /** + * 处理状态报告 + */ + private fun handleStatusReport(data: ByteArray) { + try { + Log.d(TAG, "收到状态报告: ${data.size} 字节") + callback?.onProtocolDataReceived(Protocol.FUNC_STATUS_REPORT, data) + callback?.onStatusChanged("收到状态报告") + } catch (e: Exception) { + Log.e(TAG, "处理状态报告异常: ${e.message}") + } + } + + /** + * 发送字符串指令(自动转换为字节数组) + */ + fun sendStringCommand(command: String) { + val commandBytes = command.toByteArray() + sendCommand(commandBytes) + } + + /** + * 发送十六进制指令 + */ + fun sendHexCommand(hexString: String) { + try { + val command = hexString.replace(" ", "").replace("0x", "").replace(",", "") + val bytes = ByteArray(command.length / 2) + for (i in bytes.indices) { + val index = i * 2 + bytes[i] = command.substring(index, index + 2).toInt(16).toByte() + } + sendCommand(bytes) + } catch (e: Exception) { + Log.e(TAG, "十六进制指令格式错误: ${e.message}") + callback?.onCommandSent(false, "十六进制指令格式错误: ${e.message}") + } + } + + /** + * 开启采集 + * 功能码: 0x0001 + * 数据格式: [采集开关(1字节)] [时间戳(8字节)] + */ + fun startCollection(timestamp: Long = 0L) { + try { + val data = ByteArray(9) + var index = 0 + + // 采集开关: 0x01 (开启) + data[index++] = Protocol.COLLECTION_ON.toByte() + + // 时间戳: 8字节 (小端格式) + for (i in 0..7) { + data[index++] = ((timestamp shr (i * 8)) and 0xFF).toByte() + } + + val packet = buildProtocolPacket(Protocol.FUNC_START_COLLECTION, data) + Log.d(TAG, "发送开启采集指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}") + sendCommand(packet) + + } catch (e: Exception) { + Log.e(TAG, "构建开启采集指令失败: ${e.message}") + callback?.onCommandSent(false, "构建开启采集指令失败: ${e.message}") + } + } + + /** + * 停止采集 + * 功能码: 0x0001 + * 数据格式: [采集开关(1字节)] [时间戳(8字节)] + */ + fun stopCollection(timestamp: Long = 0L) { + try { + val data = ByteArray(9) + var index = 0 + + // 采集开关: 0x00 (关闭) + data[index++] = Protocol.COLLECTION_OFF.toByte() + + // 时间戳: 8字节 (小端格式) + for (i in 0..7) { + data[index++] = ((timestamp shr (i * 8)) and 0xFF).toByte() + } + + val packet = buildProtocolPacket(Protocol.FUNC_START_COLLECTION, data) + Log.d(TAG, "发送停止采集指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}") + sendCommand(packet) + + } catch (e: Exception) { + Log.e(TAG, "构建停止采集指令失败: ${e.message}") + callback?.onCommandSent(false, "构建停止采集指令失败: ${e.message}") + } + } + + /** + * 查询设备信息 + * 功能码: 0x0000 + */ + fun queryDeviceInfo() { + try { + val packet = buildProtocolPacket(Protocol.FUNC_QUERY_DEVICE_INFO) + Log.d(TAG, "发送查询设备信息指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}") + sendCommand(packet) + } catch (e: Exception) { + Log.e(TAG, "构建查询设备信息指令失败: ${e.message}") + callback?.onCommandSent(false, "构建查询设备信息指令失败: ${e.message}") + } + } + + /** + * 查询电量 + * 功能码: 0x0002 + */ + fun queryBattery() { + try { + val packet = buildProtocolPacket(Protocol.FUNC_QUERY_BATTERY) + Log.d(TAG, "发送查询电量指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}") + sendCommand(packet) + } catch (e: Exception) { + Log.e(TAG, "构建查询电量指令失败: ${e.message}") + callback?.onCommandSent(false, "构建查询电量指令失败: ${e.message}") + } + } + + /** + * 设置采样率 + * 功能码: 0x0003 + * 数据格式: [采样率(2字节)] + */ + fun setSampleRate(sampleRate: Int) { + try { + val data = ByteArray(2) + data[0] = (sampleRate and 0xFF).toByte() + data[1] = ((sampleRate shr 8) and 0xFF).toByte() + + val packet = buildProtocolPacket(Protocol.FUNC_SET_SAMPLE_RATE, data) + Log.d(TAG, "发送设置采样率指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}") + sendCommand(packet) + + currentSampleRate = sampleRate + + } catch (e: Exception) { + Log.e(TAG, "构建设置采样率指令失败: ${e.message}") + callback?.onCommandSent(false, "构建设置采样率指令失败: ${e.message}") + } + } + + /** + * 设置增益 + * 功能码: 0x0004 + * 数据格式: [增益(1字节)] + */ + fun setGain(gain: Int) { + try { + val data = ByteArray(1) + data[0] = gain.toByte() + + val packet = buildProtocolPacket(Protocol.FUNC_SET_GAIN, data) + Log.d(TAG, "发送设置增益指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}") + sendCommand(packet) + + currentGain = gain + + } catch (e: Exception) { + Log.e(TAG, "构建设置增益指令失败: ${e.message}") + callback?.onCommandSent(false, "构建设置增益指令失败: ${e.message}") + } + } + + /** + * 设置导联 + * 功能码: 0x0006 + * 数据格式: [导联(1字节)] + */ + fun setLead(lead: Int) { + try { + val data = ByteArray(1) + data[0] = lead.toByte() + + val packet = buildProtocolPacket(Protocol.FUNC_SET_LEAD, data) + Log.d(TAG, "发送设置导联指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}") + sendCommand(packet) + + currentLead = lead + + } catch (e: Exception) { + Log.e(TAG, "构建设置导联指令失败: ${e.message}") + callback?.onCommandSent(false, "构建设置导联指令失败: ${e.message}") + } + } + + /** + * 工频滤波开关 + * 功能码: 0x000A + * 数据格式: [开关状态(1字节)] + */ + fun togglePowerLineFilter(enable: Boolean) { + try { + val data = ByteArray(1) + data[0] = if (enable) Protocol.POWER_LINE_FILTER_ON.toByte() else Protocol.POWER_LINE_FILTER_OFF.toByte() + + val packet = buildProtocolPacket(Protocol.FUNC_POWER_LINE_FILTER, data) + Log.d(TAG, "发送工频滤波${if (enable) "开启" else "关闭"}指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}") + sendCommand(packet) + + powerLineFilterEnabled = enable + + } catch (e: Exception) { + Log.e(TAG, "构建工频滤波指令失败: ${e.message}") + callback?.onCommandSent(false, "构建工频滤波指令失败: ${e.message}") + } + } + + /** + * 查询状态 + * 功能码: 0x000B + */ + fun queryStatus() { + try { + val packet = buildProtocolPacket(Protocol.FUNC_QUERY_STATUS) + Log.d(TAG, "发送查询状态指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}") + sendCommand(packet) + } catch (e: Exception) { + Log.e(TAG, "构建查询状态指令失败: ${e.message}") + callback?.onCommandSent(false, "构建查询状态指令失败: ${e.message}") + } + } + + /** + * 设备复位 + * 功能码: 0x000C + */ + fun resetDevice() { + try { + val packet = buildProtocolPacket(Protocol.FUNC_RESET_DEVICE) + Log.d(TAG, "发送设备复位指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}") + sendCommand(packet) + } catch (e: Exception) { + Log.e(TAG, "构建设备复位指令失败: ${e.message}") + callback?.onCommandSent(false, "构建设备复位指令失败: ${e.message}") + } + } + + /** + * 获取协议统计信息 + */ + fun getProtocolStats(): Map { + return mapOf( + "totalPacketsSent" to totalPacketsSent, + "totalBytesSent" to totalBytesSent, + "totalPacketsReceived" to totalPacketsReceived, + "totalBytesReceived" to totalBytesReceived, + "isCollecting" to isCollecting, + "currentSampleRate" to currentSampleRate, + "currentGain" to currentGain, + "currentLead" to currentLead, + "powerLineFilterEnabled" to powerLineFilterEnabled, + "deviceInfo" to deviceInfo + ) + } + + /** + * 直接连接到指定MAC地址的设备(用于测试) + */ + fun connectToMacAddress(macAddress: String) { + if (isConnecting || isConnected) { + callback?.onError("正在连接或已连接") + return + } + + try { + // 检查权限 + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) { + callback?.onError("缺少蓝牙连接权限") + return + } + + // 检查蓝牙适配器 + if (bluetoothAdapter == null) { + callback?.onError("蓝牙适配器未初始化") + return + } + + // 验证MAC地址格式 + if (!isValidMacAddress(macAddress)) { + callback?.onError("MAC地址格式错误: $macAddress") + return + } + + // 尝试获取设备 + val device = bluetoothAdapter!!.getRemoteDevice(macAddress) + if (device == null) { + callback?.onError("无法获取设备: $macAddress") + return + } + + Log.d(TAG, "尝试直接连接到设备: $macAddress") + callback?.onStatusChanged("正在连接设备: $macAddress") + callback?.onStatusChanged("⏱️ 连接超时时间: 30秒") + callback?.onStatusChanged("💡 请确保目标设备:") + callback?.onStatusChanged(" • 蓝牙已开启") + callback?.onStatusChanged(" • 设置为可发现模式") + callback?.onStatusChanged(" • 在10米范围内") + callback?.onStatusChanged(" • 未与其他设备连接") + + isConnecting = true + + // 连接设备 - 使用autoConnect=false以获得更快的连接 + bluetoothGatt = device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE) + + // 设置连接超时 + android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ + if (isConnecting && !isConnected) { + Log.w(TAG, "连接超时,尝试断开并重新连接") + callback?.onStatusChanged("⏰ 连接超时,正在重试...") + + // 断开当前连接 + bluetoothGatt?.disconnect() + bluetoothGatt?.close() + bluetoothGatt = null + + // 重置状态 + isConnecting = false + + // 尝试使用传统蓝牙连接 + callback?.onStatusChanged("🔄 尝试使用传统蓝牙连接...") + bluetoothGatt = device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_AUTO) + isConnecting = true + + // 再次设置超时 + android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ + if (isConnecting && !isConnected) { + Log.e(TAG, "连接最终失败") + callback?.onError("❌ 连接失败: 设备无响应") + callback?.onStatusChanged("💡 建议:") + callback?.onStatusChanged(" • 检查目标设备蓝牙设置") + callback?.onStatusChanged(" • 尝试在系统蓝牙设置中手动配对") + callback?.onStatusChanged(" • 重启两台设备的蓝牙") + isConnecting = false + } + }, 15000) // 15秒超时 + } + }, 30000) // 30秒超时 + + } catch (e: Exception) { + Log.e(TAG, "直接连接失败: ${e.message}") + callback?.onError("直接连接失败: ${e.message}") + isConnecting = false + } + } + + /** + * 验证MAC地址格式 + */ + private fun isValidMacAddress(macAddress: String): Boolean { + // 支持多种格式:60:E9:AA:30:8B:0A 或 60-E9-AA-30-8B-0A + val pattern = Regex("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$") + return pattern.matches(macAddress) + } + + /** + * 计算CRC16-CCITT-FALSE校验 + */ + private fun calculateCRC16(data: ByteArray, offset: Int, length: Int): Int { + var crc = 0xFFFF + + for (i in offset until offset + length) { + crc = crc xor (data[i].toInt() and 0xFF) + for (j in 0..7) { + if ((crc and 0x0001) != 0) { + crc = crc shr 1 + crc = crc xor 0x8408 + } else { + crc = crc shr 1 + } + } + } + + return crc + } + /** * GATT回调 */ @@ -336,11 +1203,11 @@ class BluetoothManager(private val context: Context) { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { when (newState) { BluetoothProfile.STATE_CONNECTED -> { - Log.d(TAG, "设备已连接") + Log.d(TAG, "设备已连接,状态: $status") isConnected = true isConnecting = false callback?.onConnected(gatt.device) - callback?.onStatusChanged("设备已连接") + callback?.onStatusChanged("✅ 设备已连接: ${gatt.device.name ?: gatt.device.address}") // 发现服务 if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) { @@ -348,11 +1215,34 @@ class BluetoothManager(private val context: Context) { } } BluetoothProfile.STATE_DISCONNECTED -> { - Log.d(TAG, "设备已断开") + Log.d(TAG, "设备已断开,状态: $status") isConnected = false isConnecting = false callback?.onDisconnected() - callback?.onStatusChanged("设备已断开") + + when (status) { + 0 -> { + callback?.onStatusChanged("❌ 设备已断开连接") + callback?.onStatusChanged("💡 可能原因:设备超出范围、蓝牙关闭、连接超时等") + } + 8 -> { + callback?.onStatusChanged("❌ 连接超时") + callback?.onStatusChanged("💡 设备可能不可见或距离过远") + } + 19 -> { + callback?.onStatusChanged("❌ 连接被拒绝") + callback?.onStatusChanged("💡 设备可能正在与其他设备连接") + } + 22 -> { + callback?.onStatusChanged("❌ 连接失败") + callback?.onStatusChanged("💡 设备可能不支持此连接方式") + } + else -> { + callback?.onStatusChanged("❌ 连接失败,状态码: $status") + callback?.onStatusChanged("💡 请检查设备状态和设置") + } + } + callback?.onStatusChanged("🔄 如需重新连接,请点击'连接蓝牙'按钮") } } } @@ -360,11 +1250,12 @@ class BluetoothManager(private val context: Context) { override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { if (status == BluetoothGatt.GATT_SUCCESS) { Log.d(TAG, "服务发现成功") - callback?.onStatusChanged("服务发现成功") + callback?.onStatusChanged("🔍 服务发现成功") // 打印所有可用服务 val services = gatt.services Log.d(TAG, "设备提供的服务数量: ${services.size}") + callback?.onStatusChanged("📋 发现 ${services.size} 个服务") for (service in services) { Log.d(TAG, "发现服务: ${service.uuid}") @@ -383,6 +1274,7 @@ class BluetoothManager(private val context: Context) { // 尝试查找匹配的服务和特征 var foundService: BluetoothGattService? = null var foundCharacteristic: BluetoothGattCharacteristic? = null + var foundNotifyCharacteristic: BluetoothGattCharacteristic? = null // 首先尝试预设的UUID列表 for (serviceUuid in SERVICE_UUIDS) { @@ -391,21 +1283,31 @@ class BluetoothManager(private val context: Context) { Log.d(TAG, "找到匹配的服务: $serviceUuid") foundService = service - // 查找匹配的特征 + // 查找TX特征(用于发送数据) for (characteristicUuid in CHARACTERISTIC_UUIDS) { val characteristic = service.getCharacteristic(characteristicUuid) if (characteristic != null) { - Log.d(TAG, "找到匹配的特征: $characteristicUuid") + Log.d(TAG, "找到TX特征: $characteristicUuid") foundCharacteristic = characteristic break } } + + // 查找RX特征(用于接收数据) + for (notifyUuid in NOTIFY_CHARACTERISTIC_UUIDS) { + val notifyCharacteristic = service.getCharacteristic(notifyUuid) + if (notifyCharacteristic != null) { + Log.d(TAG, "找到RX特征: $notifyUuid") + foundNotifyCharacteristic = notifyCharacteristic + break + } + } break } } // 如果预设UUID没有找到,尝试智能检测 - if (foundService == null || foundCharacteristic == null) { + if (foundService == null || foundCharacteristic == null || foundNotifyCharacteristic == null) { Log.d(TAG, "预设UUID未找到匹配,尝试智能检测") // 查找所有非标准蓝牙服务的UUID @@ -424,34 +1326,63 @@ class BluetoothManager(private val context: Context) { if (!characteristicUuid.contains("00002a")) { Log.d(TAG, "发现自定义特征: $characteristicUuid") - // 检查特征是否支持通知 + // 检查特征是否支持写入(TX特征) + if (characteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE != 0 || + characteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE != 0) { + + Log.d(TAG, "找到支持写入的特征(TX): $characteristicUuid") + if (foundService == null) foundService = service + if (foundCharacteristic == null) foundCharacteristic = characteristic + } + + // 检查特征是否支持通知(RX特征) if (characteristic.properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0 || characteristic.properties and BluetoothGattCharacteristic.PROPERTY_INDICATE != 0) { - Log.d(TAG, "找到支持通知的特征: $characteristicUuid") - foundService = service - foundCharacteristic = characteristic - break + Log.d(TAG, "找到支持通知的特征(RX): $characteristicUuid") + if (foundService == null) foundService = service + if (foundNotifyCharacteristic == null) foundNotifyCharacteristic = characteristic } } } - if (foundService != null) break + if (foundService != null && foundCharacteristic != null && foundNotifyCharacteristic != null) break } } } - if (foundService != null && foundCharacteristic != null) { + if (foundService != null && foundCharacteristic != null && foundNotifyCharacteristic != null) { + // 保存当前服务特征,用于发送和接收数据 + currentService = foundService + currentCharacteristic = foundCharacteristic + currentNotifyCharacteristic = foundNotifyCharacteristic + // 启用通知 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}") + Log.d(TAG, "TX特征UUID: ${foundCharacteristic.uuid}") + Log.d(TAG, "RX特征UUID: ${foundNotifyCharacteristic.uuid}") + Log.d(TAG, "TX特征属性: ${foundCharacteristic.properties}") + Log.d(TAG, "RX特征属性: ${foundNotifyCharacteristic.properties}") - val success = gatt.setCharacteristicNotification(foundCharacteristic, true) - Log.d(TAG, "启用特征通知结果: $success") + // 启用RX特征的通知 + val success = gatt.setCharacteristicNotification(foundNotifyCharacteristic, true) + Log.d(TAG, "启用RX特征通知结果: $success") - callback?.onStatusChanged("数据通道已建立,开始接收数据") + // 写入Descriptor来激活通知 + val descriptor = foundNotifyCharacteristic.getDescriptor( + UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") // Client Characteristic Configuration Descriptor + ) + if (descriptor != null) { + descriptor.value = android.bluetooth.BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + val writeSuccess = gatt.writeDescriptor(descriptor) + Log.d(TAG, "写入Descriptor结果: $writeSuccess") + } else { + Log.w(TAG, "未找到Client Characteristic Configuration Descriptor") + } + + callback?.onStatusChanged("📡 数据通道已建立,可以发送指令开始接收数据") + callback?.onStatusChanged("💡 提示: 长按'发送指令'按钮可以发送测试数据") } else { Log.e(TAG, "缺少BLUETOOTH_CONNECT权限") callback?.onError("缺少BLUETOOTH_CONNECT权限") @@ -471,16 +1402,73 @@ class BluetoothManager(private val context: Context) { } } + override fun onDescriptorWrite(gatt: android.bluetooth.BluetoothGatt, descriptor: android.bluetooth.BluetoothGattDescriptor, status: Int) { + if (status == android.bluetooth.BluetoothGatt.GATT_SUCCESS) { + Log.d(TAG, "Descriptor写入成功: ${descriptor.uuid}") + callback?.onStatusChanged("✅ Notify通道已激活,可以接收设备数据") + } else { + Log.e(TAG, "Descriptor写入失败,状态: $status") + callback?.onError("Notify通道激活失败,状态: $status") + } + } + override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { - // 接收数据 val data = characteristic.value Log.d(TAG, "=== 蓝牙特征值变化 ===") Log.d(TAG, "特征UUID: ${characteristic.uuid}") + Log.d(TAG, "当前RX特征UUID: ${currentNotifyCharacteristic?.uuid}") Log.d(TAG, "接收到数据: ${data.size} 字节") + if (data.isNotEmpty()) { Log.d(TAG, "数据前10字节: ${data.take(10).joinToString(", ") { "0x%02X".format(it) }}") + + // 分析244字节数据包结构 + if (data.size == 244) { + Log.d(TAG, "=== 244字节数据包结构分析 ===") + Log.d(TAG, "前4字节: ${data.take(4).joinToString(", ") { "0x%02X".format(it) }}") + Log.d(TAG, "后4字节: ${data.takeLast(4).joinToString(", ") { "0x%02X".format(it) }}") + + // 尝试解析为轻迅协议 + val functionCode = (data[1].toInt() and 0xFF shl 8) or (data[0].toInt() and 0xFF) + val dataLength = (data[3].toInt() and 0xFF shl 8) or (data[2].toInt() and 0xFF) + + Log.d(TAG, "解析功能码: 0x${functionCode.toString(16).uppercase()}") + Log.d(TAG, "解析数据长度: $dataLength") + + if (functionCode == Protocol.FUNC_DATA_STREAM && dataLength == 238) { + Log.d(TAG, "✅ 确认是轻迅协议数据流包") + Log.d(TAG, "协议包结构: 功能码(2) + 数据长度(2) + 数据内容(238) + CRC16(2) = 244字节") + + // 提取238字节的设备数据内容(跳过功能码和数据长度,不包含CRC16) + val deviceData = ByteArray(238) + System.arraycopy(data, 4, deviceData, 0, 238) + + Log.d(TAG, "提取设备数据: ${deviceData.size} 字节") + Log.d(TAG, "设备数据前10字节: ${deviceData.take(10).joinToString(", ") { "0x%02X".format(it) }}") + + // 238字节设备数据直接传递给回调,不进行轻迅协议解析 + Log.d(TAG, "✅ 设备数据直接传递给C++解析器") + callback?.onDataReceived(deviceData) + callback?.onProtocolDataReceived(Protocol.FUNC_DATA_STREAM, deviceData) + } else { + Log.d(TAG, "⚠️ 不是标准轻迅协议格式,尝试其他解析方式") + handleRawData(data) + } + } else { + Log.d(TAG, "数据包大小: ${data.size} 字节,不是244字节") + handleRawData(data) + } + } + + // 检查是否是RX特征 + if (characteristic.uuid == currentNotifyCharacteristic?.uuid) { + Log.d(TAG, "✅ 收到RX特征数据") + } else { + Log.d(TAG, "⚠️ 收到非RX特征数据,UUID不匹配") + Log.d(TAG, "收到特征UUID: ${characteristic.uuid}") + Log.d(TAG, "期望RX特征UUID: ${currentNotifyCharacteristic?.uuid}") + callback?.onDataReceived(data) } - callback?.onDataReceived(data) } } @@ -492,7 +1480,23 @@ class BluetoothManager(private val context: Context) { super.onScanResult(callbackType, result) val device = result.device - Log.d(TAG, "发现BLE设备: ${device.name ?: "未知"} (${device.address})") + val deviceName = device.name ?: "未知设备" + val deviceAddress = device.address + val rssi = result.rssi + + Log.d(TAG, "发现BLE设备: $deviceName ($deviceAddress) RSSI: $rssi") + + // 显示详细的设备信息 + callback?.onStatusChanged("📱 发现BLE设备: $deviceName") + callback?.onStatusChanged(" 📍 地址: $deviceAddress") + callback?.onStatusChanged(" 📶 信号强度: $rssi dBm") + + // 检查是否是目标设备 + if (deviceAddress.equals("A4:C3:37:86:9F:73", ignoreCase = true) || + deviceAddress.equals("A4-C3-37-86-9F-73", ignoreCase = true)) { + callback?.onStatusChanged("🎯 找到目标设备!") + callback?.onStatusChanged("💡 建议:长按'连接蓝牙'按钮直接连接") + } // 添加所有发现的设备(不进行过滤) addDiscoveredDevice(device) @@ -502,7 +1506,18 @@ class BluetoothManager(private val context: Context) { override fun onScanFailed(errorCode: Int) { super.onScanFailed(errorCode) Log.e(TAG, "BLE扫描失败,错误码: $errorCode") - callback?.onError("BLE扫描失败,错误码: $errorCode") + + val errorMessage = when (errorCode) { + SCAN_FAILED_ALREADY_STARTED -> "扫描已在进行中" + SCAN_FAILED_APPLICATION_REGISTRATION_FAILED -> "应用注册失败" + SCAN_FAILED_FEATURE_UNSUPPORTED -> "设备不支持BLE" + SCAN_FAILED_INTERNAL_ERROR -> "内部错误" + SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES -> "硬件资源不足" + else -> "未知错误: $errorCode" + } + + callback?.onError("❌ BLE扫描失败: $errorMessage") + callback?.onStatusChanged("💡 尝试使用传统蓝牙扫描...") // 扫描失败时完成扫描 callback?.onStatusChanged("扫描完成,找到 ${discoveredDevices.size} 个设备") @@ -567,6 +1582,29 @@ class BluetoothManager(private val context: Context) { fun addDiscoveredDevice(device: BluetoothDevice) { if (!discoveredDevices.contains(device)) { discoveredDevices.add(device) + + // 显示详细的设备信息 + val deviceName = device.name ?: "未知设备" + val deviceAddress = device.address + val deviceType = when (device.type) { + BluetoothDevice.DEVICE_TYPE_CLASSIC -> "经典蓝牙" + BluetoothDevice.DEVICE_TYPE_LE -> "低功耗蓝牙" + BluetoothDevice.DEVICE_TYPE_DUAL -> "双模蓝牙" + else -> "未知类型" + } + + Log.d(TAG, "发现设备: $deviceName ($deviceAddress) - $deviceType") + callback?.onStatusChanged("📱 发现设备: $deviceName") + callback?.onStatusChanged(" 📍 地址: $deviceAddress") + callback?.onStatusChanged(" 🔧 类型: $deviceType") + + // 检查是否是目标设备 + if (deviceAddress.equals("A4:C3:37:86:9F:73", ignoreCase = true) || + deviceAddress.equals("A4-C3-37-86-9F-73", ignoreCase = true)) { + callback?.onStatusChanged("🎯 找到目标设备!") + callback?.onStatusChanged("💡 建议:长按'连接蓝牙'按钮直接连接") + } + callback?.onDeviceFound(device) } } @@ -588,6 +1626,26 @@ class BluetoothManager(private val context: Context) { */ fun isConnecting(): Boolean = isConnecting + /** + * 调试函数:检查当前连接状态和特征 + */ + fun debugConnectionStatus(): String { + val status = StringBuilder() + status.append("=== 连接状态调试信息 ===\n") + status.append("连接状态: ${if (isConnected) "已连接" else "未连接"}\n") + status.append("连接中状态: ${if (isConnecting) "连接中" else "未连接中"}\n") + status.append("当前服务: ${currentService?.uuid ?: "未设置"}\n") + status.append("当前TX特征: ${currentCharacteristic?.uuid ?: "未设置"}\n") + status.append("当前RX特征: ${currentNotifyCharacteristic?.uuid ?: "未设置"}\n") + status.append("RX特征属性: ${currentNotifyCharacteristic?.properties ?: "未设置"}\n") + status.append("数据统计:\n") + status.append(" 发送包数: $totalPacketsSent\n") + status.append(" 发送字节: $totalBytesSent\n") + status.append(" 接收包数: $totalPacketsReceived\n") + status.append(" 接收字节: $totalBytesReceived\n") + return status.toString() + } + /** * 清理资源 */ diff --git a/app/src/main/java/com/example/cmake_project_test/DataManager.kt b/app/src/main/java/com/example/cmake_project_test/DataManager.kt index 0b174dc..b74c129 100644 --- a/app/src/main/java/com/example/cmake_project_test/DataManager.kt +++ b/app/src/main/java/com/example/cmake_project_test/DataManager.kt @@ -101,7 +101,45 @@ class DataManager(private val nativeCallback: NativeMethodCallback) { // 应用流式数据处理 processStreamingData(packets) } else { - Log.w("DataManager", "没有解析出有效数据包") + Log.w("DataManager", "没有解析出有效数据包,尝试生成测试数据") + + // 如果原生解析器没有解析出数据包,生成一些测试数据来验证图表显示 + generateTestDataForChart() + } + } + + /** + * 生成测试数据用于验证图表显示 + */ + private fun generateTestDataForChart() { + try { + Log.d("DataManager", "生成测试数据用于验证图表显示") + + // 生成模拟的单导联ECG数据 + val testData = mutableListOf() + val sampleCount = 100 // 生成100个样本点 + + for (i in 0 until sampleCount) { + val t = i.toFloat() / 10f // 时间参数 + val value = (Math.sin(t * 2 * Math.PI) * 100 + + Math.sin(t * 4 * Math.PI) * 50 + + Math.sin(t * 8 * Math.PI) * 25).toFloat() + testData.add(value) + } + + Log.d("DataManager", "生成测试数据: ${testData.size} 个样本点") + Log.d("DataManager", "测试数据前5个值: ${testData.take(5).joinToString(", ")}") + + // 发送测试数据到图表 + if (realTimeCallback != null) { + realTimeCallback!!.onRawDataAvailable(0, testData) + Log.d("DataManager", "已发送测试数据到图表") + } else { + Log.e("DataManager", "realTimeCallback 为空,无法发送测试数据") + } + + } catch (e: Exception) { + Log.e("DataManager", "生成测试数据失败: ${e.message}", e) } } @@ -178,7 +216,7 @@ class DataManager(private val nativeCallback: NativeMethodCallback) { } /** - * 立即发送原始数据到图表显示 + * 立即发送原始数据到图表显示 - 专门处理单导联蓝牙数据 */ private fun sendRawDataToCharts(packets: List) { if (packets.isEmpty()) { @@ -186,84 +224,97 @@ class DataManager(private val nativeCallback: NativeMethodCallback) { return } - Log.d("DataManager", "立即发送原始数据到图表,处理 ${packets.size} 个数据包") + Log.d("DataManager", "立即发送单导联蓝牙数据到图表,处理 ${packets.size} 个数据包") - // 直接处理原始数据包,不进行通道映射 + // 专门处理单导联数据包 for ((packetIndex, packet) in packets.withIndex()) { - Log.d("DataManager", "处理数据包 $packetIndex: 数据类型=${packet.getDataType()}") + 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 为空,无法发送数据") - } + if (channelData != null && channelData.isNotEmpty()) { + Log.d("DataManager", "单导联数据包 $packetIndex 有 ${channelData.size} 个通道") + + // 对于单导联数据,我们主要关注第一个通道(通常是导联I或II) + val primaryChannel = channelData[0] + if (primaryChannel.isNotEmpty()) { + Log.d("DataManager", "单导联主通道数据长度: ${primaryChannel.size}") + Log.d("DataManager", "单导联主通道前3个值: ${primaryChannel.take(3).joinToString(", ")}") + + // 立即发送单导联数据到图表 + if (realTimeCallback != null) { + realTimeCallback!!.onRawDataAvailable(0, primaryChannel) // 使用通道0表示主导联 + Log.d("DataManager", "已发送单导联数据到图表,数据长度: ${primaryChannel.size}") } else { - Log.w("DataManager", "通道 $channelIndex 数据为空") + Log.e("DataManager", "realTimeCallback 为空,无法发送单导联数据") } + + // 如果有其他通道(如参考通道),也发送 + if (channelData.size > 1) { + for (i in 1 until channelData.size) { + val additionalChannel = channelData[i] + if (additionalChannel.isNotEmpty()) { + Log.d("DataManager", "发送附加通道 $i 数据,长度: ${additionalChannel.size}") + realTimeCallback?.onRawDataAvailable(i, additionalChannel) + } + } + } + } else { + Log.w("DataManager", "单导联主通道数据为空") } } else { - Log.w("DataManager", "数据包 $packetIndex 没有通道数据") + Log.w("DataManager", "单导联数据包 $packetIndex 没有通道数据") } } + + // 添加调试信息 + Log.d("DataManager", "单导联蓝牙数据发送完成,总共处理了 ${packets.size} 个数据包") } /** - * 流式数据处理 - 将数据包按通道合并并处理 + * 流式数据处理 - 专门处理单导联蓝牙数据 */ private fun processStreamingData(packets: List) { if (packets.isEmpty()) return - Log.d("DataManager", "开始流式数据处理,处理 ${packets.size} 个数据包") + Log.d("DataManager", "开始单导联流式数据处理,处理 ${packets.size} 个数据包") - // 1. 通道映射 - ensureDataMapper() - val mappedPackets = if (dataMapperInitialized && dataMapper != null) { - try { - val mapped = dataMapper!!.mapSensorDataList(packets) - Log.d("DataManager", "通道映射完成,映射了 ${mapped.size} 个数据包") - mapped - } catch (e: Exception) { - Log.e("DataManager", "通道映射失败: ${e.message}") - packets - } - } else { - Log.w("DataManager", "数据映射器未初始化,跳过通道映射") - packets - } + // 对于单导联数据,我们直接使用原始数据包,不进行复杂的通道映射 + val mappedPackets = packets - // 2. 按通道合并数据 + // 2. 按通道合并数据 - 单导联处理 for (packet in mappedPackets) { val dataType = packet.getDataType() if (currentDataType == null) { currentDataType = dataType - Log.d("DataManager", "设置当前数据类型: $dataType") + Log.d("DataManager", "设置单导联数据类型: $dataType") } val channelData = packet.getChannelData() - if (channelData != null) { - for ((channelIndex, channel) in channelData.withIndex()) { - if (!channelBuffers.containsKey(channelIndex)) { - channelBuffers[channelIndex] = mutableListOf() - processedChannelBuffers[channelIndex] = mutableListOf() + if (channelData != null && channelData.isNotEmpty()) { + // 对于单导联数据,主要处理第一个通道 + val primaryChannel = channelData[0] + if (!channelBuffers.containsKey(0)) { + channelBuffers[0] = mutableListOf() + processedChannelBuffers[0] = mutableListOf() + } + channelBuffers[0]!!.addAll(primaryChannel) + + // 如果有其他通道,也处理 + for (i in 1 until channelData.size) { + val additionalChannel = channelData[i] + if (!channelBuffers.containsKey(i)) { + channelBuffers[i] = mutableListOf() + processedChannelBuffers[i] = mutableListOf() } - channelBuffers[channelIndex]!!.addAll(channel) + channelBuffers[i]!!.addAll(additionalChannel) } } } - // 检查是否达到处理条件 + // 检查是否达到处理条件 - 单导联优化 val totalSamples = channelBuffers.values.firstOrNull()?.size ?: 0 if (totalSamples > 0) { - Log.d("DataManager", "当前总样本数: $totalSamples, 需要样本数: $minSamplesForMetrics") + Log.d("DataManager", "单导联当前总样本数: $totalSamples, 需要样本数: $minSamplesForMetrics") } // 3. 检查处理条件:数据量足够且时间间隔合适 @@ -272,38 +323,19 @@ class DataManager(private val nativeCallback: NativeMethodCallback) { (currentTime - lastProcessTime) >= processingInterval if (shouldProcess) { - Log.d("DataManager", "满足处理条件,开始流式信号处理和指标计算") + Log.d("DataManager", "满足单导联处理条件,开始流式信号处理和指标计算") processStreamingWindow() lastProcessTime = currentTime } else { - Log.d("DataManager", "不满足处理条件 - 数据量: ${totalSamples}/${minSamplesForMetrics}, 时间间隔: ${currentTime - lastProcessTime}ms/${processingInterval}ms") + Log.d("DataManager", "不满足单导联处理条件 - 数据量: ${totalSamples}/${minSamplesForMetrics}, 时间间隔: ${currentTime - lastProcessTime}ms/${processingInterval}ms") } - // 累积映射后的数据包列表(用于UI显示) - // 注意:这里累积所有批次的映射后数据包,用于统计和显示 + // 累积单导联数据包列表(用于UI显示) this.processedPackets.addAll(mappedPackets) - // 添加调试信息(在后台线程中执行,避免阻塞UI) - Thread { - try { - Log.d("DataManager", "DEBUG: 当前批次映射结果 - 输入: ${packets.size}个数据包, 映射后: ${mappedPackets.size}个数据包") - Log.d("DataManager", "DEBUG: 累积映射后数据包总数: ${this.processedPackets.size}") - - // 统计当前批次的通道数量分布 - val ecg12LeadPackets = mappedPackets.filter { it.getDataType() == type.SensorData.DataType.ECG_12LEAD } - val packetsWith8Channels = ecg12LeadPackets.count { it.getChannelData()?.size == 8 } - val packetsWith12Channels = ecg12LeadPackets.count { it.getChannelData()?.size == 12 } - Log.d("DataManager", "DEBUG: 当前批次ECG_12LEAD统计 - 8通道: ${packetsWith8Channels}个, 12通道: ${packetsWith12Channels}个") - - // 统计累积的通道数量分布 - val totalEcg12LeadPackets = this.processedPackets.filter { it.getDataType() == type.SensorData.DataType.ECG_12LEAD } - val totalPacketsWith8Channels = totalEcg12LeadPackets.count { it.getChannelData()?.size == 8 } - val totalPacketsWith12Channels = totalEcg12LeadPackets.count { it.getChannelData()?.size == 12 } - Log.d("DataManager", "DEBUG: 累积ECG_12LEAD统计 - 8通道: ${totalPacketsWith8Channels}个, 12通道: ${totalPacketsWith12Channels}个") - } catch (e: Exception) { - Log.e("DataManager", "后台统计调试信息时发生错误: ${e.message}", e) - } - }.start() + // 添加单导联调试信息 + Log.d("DataManager", "单导联数据处理完成 - 输入: ${packets.size}个数据包, 处理: ${mappedPackets.size}个数据包") + Log.d("DataManager", "累积单导联数据包总数: ${this.processedPackets.size}") } /** @@ -448,10 +480,10 @@ class DataManager(private val nativeCallback: NativeMethodCallback) { } } - // 回调进度信息 + // 回调处理状态(不显示进度条,只记录状态) val totalSamples = channelBuffers.values.firstOrNull()?.size ?: 0 callback.onStreamingProgress( - progress = (totalSamples * 100 / minSamplesForMetrics).coerceAtMost(100), + progress = 0, // 实时处理没有进度概念 totalSamples = totalSamples, processedSamples = localProcessedSamples.toInt() ) diff --git a/app/src/main/java/com/example/cmake_project_test/ECGChartView.kt b/app/src/main/java/com/example/cmake_project_test/ECGChartView.kt index faea19f..b6ce5ef 100644 --- a/app/src/main/java/com/example/cmake_project_test/ECGChartView.kt +++ b/app/src/main/java/com/example/cmake_project_test/ECGChartView.kt @@ -60,19 +60,27 @@ class ECGChartView @JvmOverloads constructor( private var offsetX = 0f // X轴偏移 private var offsetY = 0f // Y轴偏移 + // 自动居中控制 + private var autoCenter = true // 自动居中模式 + private var lockToCenter = true // 锁定到屏幕中央 + // 手势检测器 private val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { override fun onDoubleTap(e: MotionEvent): Boolean { - // 双击重置缩放 + // 双击重置缩放并锁定到中央 resetZoom() + lockToCenter = true + invalidate() return true } override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean { - // 滑动平移 - offsetX -= distanceX - offsetY -= distanceY - invalidate() + // 只有在非锁定模式下才允许平移 + if (!lockToCenter) { + offsetX -= distanceX + offsetY -= distanceY + invalidate() + } return true } }) @@ -87,6 +95,7 @@ class ECGChartView @JvmOverloads constructor( private val zoomInYButtonRect = RectF() private val zoomOutYButtonRect = RectF() private val resetButtonRect = RectF() + private val lockButtonRect = RectF() fun updateData(newData: List) { // 累积数据而不是替换 @@ -116,6 +125,12 @@ class ECGChartView @JvmOverloads constructor( } } + // 如果启用自动居中,重置偏移 + if (autoCenter) { + offsetX = 0f + offsetY = 0f + } + invalidate() // 重绘 } @@ -150,8 +165,22 @@ class ECGChartView @JvmOverloads constructor( scaleY = 1.0f offsetX = 0f offsetY = 0f + lockToCenter = true invalidate() } + + // 切换锁定模式 + fun toggleLockMode() { + lockToCenter = !lockToCenter + if (lockToCenter) { + offsetX = 0f + offsetY = 0f + } + invalidate() + } + + // 获取锁定状态 + fun isLockedToCenter(): Boolean = lockToCenter override fun onTouchEvent(event: MotionEvent): Boolean { when (event.action) { @@ -175,6 +204,9 @@ class ECGChartView @JvmOverloads constructor( } else if (resetButtonRect.contains(x, y)) { resetZoom() return true + } else if (lockButtonRect.contains(x, y)) { + toggleLockMode() + return true } } } @@ -202,6 +234,10 @@ class ECGChartView @JvmOverloads constructor( canvas.drawText("X缩放: ${String.format("%.1f", scaleX)}x", 20f, 70f, textPaint) canvas.drawText("Y缩放: ${String.format("%.1f", scaleY)}x", 20f, 100f, textPaint) + // 绘制锁定状态 + val lockStatus = if (lockToCenter) "🔒 已锁定" else "🔓 已解锁" + canvas.drawText(lockStatus, 20f, 130f, textPaint) + // 绘制数据点数量 canvas.drawText("数据点: ${dataPoints.size}", 20f, height - 20f, textPaint) @@ -225,36 +261,44 @@ class ECGChartView @JvmOverloads constructor( // X轴缩放按钮 zoomInXButtonRect.set( + width - buttonSize * 6 - buttonMargin * 6, + buttonY, + width - buttonSize * 5 - buttonMargin * 5, + buttonY + buttonSize + ) + + zoomOutXButtonRect.set( width - buttonSize * 5 - buttonMargin * 5, buttonY, width - buttonSize * 4 - buttonMargin * 4, buttonY + buttonSize ) - zoomOutXButtonRect.set( + // Y轴缩放按钮 + zoomInYButtonRect.set( width - buttonSize * 4 - buttonMargin * 4, buttonY, width - buttonSize * 3 - buttonMargin * 3, buttonY + buttonSize ) - // Y轴缩放按钮 - zoomInYButtonRect.set( + zoomOutYButtonRect.set( width - buttonSize * 3 - buttonMargin * 3, buttonY, width - buttonSize * 2 - buttonMargin * 2, buttonY + buttonSize ) - zoomOutYButtonRect.set( + // 重置按钮 + resetButtonRect.set( width - buttonSize * 2 - buttonMargin * 2, buttonY, width - buttonSize - buttonMargin, buttonY + buttonSize ) - // 重置按钮 - resetButtonRect.set( + // 锁定按钮 + lockButtonRect.set( width - buttonSize - buttonMargin, buttonY, width - buttonMargin, @@ -267,7 +311,7 @@ class ECGChartView @JvmOverloads constructor( canvas.drawRoundRect(zoomInXButtonRect, 10f, 10f, buttonPaint) canvas.drawText("X+", zoomInXButtonRect.centerX(), zoomInXButtonRect.centerY() + 7f, buttonTextPaint) - canvas.drawRoundRect(zoomOutXButtonRect, 10f, 10f, buttonPaint) + canvas.drawRoundRect(zoomOutXButtonRect, 10f, 10f, buttonTextPaint) canvas.drawText("X-", zoomOutXButtonRect.centerX(), zoomOutXButtonRect.centerY() + 7f, buttonTextPaint) // 绘制Y轴缩放按钮 @@ -280,6 +324,13 @@ class ECGChartView @JvmOverloads constructor( // 绘制重置按钮 canvas.drawRoundRect(resetButtonRect, 10f, 10f, buttonPaint) canvas.drawText("重置", resetButtonRect.centerX(), resetButtonRect.centerY() + 7f, buttonTextPaint) + + // 绘制锁定按钮 + val lockButtonColor = if (lockToCenter) Color.GREEN else Color.RED + buttonPaint.color = lockButtonColor + canvas.drawRoundRect(lockButtonRect, 10f, 10f, buttonPaint) + canvas.drawText(if (lockToCenter) "🔒" else "🔓", lockButtonRect.centerX(), lockButtonRect.centerY() + 7f, buttonTextPaint) + buttonPaint.color = Color.GRAY // 恢复默认颜色 } private fun drawGrid(canvas: Canvas, width: Float, height: Float) { @@ -309,13 +360,17 @@ class ECGChartView @JvmOverloads constructor( val scaledWidth = drawWidth * scaleX val scaledHeight = drawHeight * scaleY + // 如果锁定到中央,强制偏移为0 + val effectiveOffsetX = if (lockToCenter) 0f else offsetX + val effectiveOffsetY = if (lockToCenter) 0f else offsetY + val xStep = scaledWidth / (dataPoints.size - 1) for (i in dataPoints.indices) { - val x = padding + i * xStep + offsetX + val x = padding + i * xStep + effectiveOffsetX val normalizedValue = (dataPoints[i] - minValue) / (maxValue - minValue) // 使用0.1到0.9的范围,确保曲线在中间80%的区域显示 - val y = padding + (0.1f + normalizedValue * 0.8f) * scaledHeight + offsetY + val y = padding + (0.1f + normalizedValue * 0.8f) * scaledHeight + effectiveOffsetY // 确保点在可见区域内 if (x >= padding && x <= width - padding && y >= padding && y <= height - padding) { diff --git a/app/src/main/java/com/example/cmake_project_test/ECGRhythmView.kt b/app/src/main/java/com/example/cmake_project_test/ECGRhythmView.kt index 0cb9be8..6670abf 100644 --- a/app/src/main/java/com/example/cmake_project_test/ECGRhythmView.kt +++ b/app/src/main/java/com/example/cmake_project_test/ECGRhythmView.kt @@ -3,6 +3,7 @@ package com.example.cmake_project_test import android.content.Context import android.graphics.* import android.util.AttributeSet +import android.util.Log import android.view.GestureDetector import android.view.MotionEvent import android.view.View @@ -96,6 +97,7 @@ class ECGRhythmView @JvmOverloads constructor( fun updateData(newData: List) { if (newData.isNotEmpty()) { isDataAvailable = true + Log.d("ECGRhythmView", "收到新数据: ${newData.size} 个点") } // 性能优化:批量更新数据 @@ -133,6 +135,7 @@ class ECGRhythmView @JvmOverloads constructor( } lastUpdateTime = currentTime + Log.d("ECGRhythmView", "更新图表,数据点: ${dataPoints.size}, 范围: $minValue - $maxValue") invalidate() } } diff --git a/app/src/main/java/com/example/cmake_project_test/ECGWaveformView.kt b/app/src/main/java/com/example/cmake_project_test/ECGWaveformView.kt index 1534f2a..7086113 100644 --- a/app/src/main/java/com/example/cmake_project_test/ECGWaveformView.kt +++ b/app/src/main/java/com/example/cmake_project_test/ECGWaveformView.kt @@ -3,6 +3,7 @@ package com.example.cmake_project_test import android.content.Context import android.graphics.* import android.util.AttributeSet +import android.util.Log import android.view.GestureDetector import android.view.MotionEvent import android.view.View @@ -96,6 +97,7 @@ class ECGWaveformView @JvmOverloads constructor( fun updateData(newData: List) { if (newData.isNotEmpty()) { isDataAvailable = true + Log.d("ECGWaveformView", "收到新数据: ${newData.size} 个点") } // 性能优化:批量更新数据 @@ -133,6 +135,7 @@ class ECGWaveformView @JvmOverloads constructor( } lastUpdateTime = currentTime + Log.d("ECGWaveformView", "更新图表,数据点: ${dataPoints.size}, 范围: $minValue - $maxValue") invalidate() } } diff --git a/app/src/main/java/com/example/cmake_project_test/MainActivity.kt b/app/src/main/java/com/example/cmake_project_test/MainActivity.kt index 9dc6165..93a72c3 100644 --- a/app/src/main/java/com/example/cmake_project_test/MainActivity.kt +++ b/app/src/main/java/com/example/cmake_project_test/MainActivity.kt @@ -1,6 +1,7 @@ package com.example.cmake_project_test import androidx.appcompat.app.AppCompatActivity +import android.os.Build import android.os.Bundle import android.util.Log import android.graphics.Color @@ -17,12 +18,25 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real companion object { private const val PERMISSION_REQUEST_CODE = 1001 - private val REQUIRED_PERMISSIONS = arrayOf( - Manifest.permission.BLUETOOTH_SCAN, - Manifest.permission.BLUETOOTH_CONNECT, - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.ACCESS_COARSE_LOCATION - ) + + // 根据Android版本动态确定所需权限 + private val REQUIRED_PERMISSIONS: Array = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // Android 12+ 使用新的蓝牙权限,但仍然需要位置权限用于扫描 + arrayOf( + Manifest.permission.BLUETOOTH_SCAN, + Manifest.permission.BLUETOOTH_CONNECT, + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + } else { + // Android 11及以下需要位置权限 + arrayOf( + Manifest.permission.BLUETOOTH_SCAN, + Manifest.permission.BLUETOOTH_CONNECT, + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + } // Used to load the 'cmake_project_test' library on application startup. init { @@ -34,6 +48,28 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real private lateinit var dataManager: DataManager private lateinit var uiManager: UiManager private lateinit var bluetoothManager: BluetoothManager + + // 数据处理状态 + private var dataProcessingStarted = false + private var bluetoothConnected = false + + // 数据监控相关 + private var dataLogBuffer = StringBuilder() + private var receivedDataCount = 0 + private var sentDataCount = 0 + private var lastDataTime = 0L + private var dataRate = 0f + private var isDataMonitoringEnabled = true + + // 状态更新控制 + private var lastStatusUpdateTime = 0L + private var lastLogTime = 0L + private var lastDeviceFoundTime = 0L + private var lastDataLogTime = 0L + private var lastDataStatusTime = 0L + private var lastChartUpdateTime = 0L + private var lastChartStatusTime = 0L + private var lastProgressUpdateTime = 0L override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -55,6 +91,9 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real // 初始化ECG双视图 binding.ecgRhythmView.updateData(emptyList()) binding.ecgWaveformView.updateData(emptyList()) + + // 确保图表容器可见 + binding.ecgChartContainer.visibility = View.VISIBLE // 设置按钮点击事件 setupButtonListeners() @@ -71,6 +110,11 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real startDataProcessing() } + // 发送指令按钮点击事件 + binding.sendCommandButton.setOnClickListener { + sendDataCommand() + } + // 停止按钮点击事件 binding.stopButton.setOnClickListener { stopDataProcessing() @@ -80,10 +124,26 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real binding.notchFilterButton.setOnClickListener { toggleNotchFilter() } + + // 清空数据按钮点击事件 + binding.clearDataButton.setOnClickListener { + clearAllData() + } + + // 添加直接连接测试按钮(长按蓝牙按钮) + binding.bluetoothButton.setOnLongClickListener { + showDirectConnectDialog() + true + } + + // 添加调试状态按钮(长按发送指令按钮) + binding.sendCommandButton.setOnLongClickListener { + showDebugStatusDialog() + true + } } private var notchFilterEnabled = false - private var bluetoothConnected = false private fun toggleBluetoothConnection() { if (bluetoothConnected) { @@ -110,15 +170,28 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real private fun checkAndRequestPermissions(): Boolean { val missingPermissions = mutableListOf() + val grantedPermissions = mutableListOf() + + updateStatus("🔍 检查蓝牙权限...") for (permission in REQUIRED_PERMISSIONS) { - if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { + if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) { + grantedPermissions.add(permission.split(".").last()) + } else { missingPermissions.add(permission) } } + // 显示权限状态 + if (grantedPermissions.isNotEmpty()) { + updateStatus("✅ 已授予权限: ${grantedPermissions.joinToString(", ")}") + } + if (missingPermissions.isNotEmpty()) { - updateStatus("需要蓝牙权限: ${missingPermissions.joinToString(", ")}") + val missingNames = missingPermissions.map { it.split(".").last() } + updateStatus("❌ 缺少权限: ${missingNames.joinToString(", ")}") + updateStatus("💡 正在请求权限...") + ActivityCompat.requestPermissions( this, missingPermissions.toTypedArray(), @@ -127,6 +200,7 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real return false } + updateStatus("✅ 所有必要权限已授予") return true } @@ -150,13 +224,42 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real when (requestCode) { PERMISSION_REQUEST_CODE -> { + val grantedPermissions = mutableListOf() + val deniedPermissions = mutableListOf() + + for (i in permissions.indices) { + val permission = permissions[i] + val granted = grantResults[i] == PackageManager.PERMISSION_GRANTED + + if (granted) { + grantedPermissions.add(permission.split(".").last()) + } else { + deniedPermissions.add(permission.split(".").last()) + } + } + + if (grantedPermissions.isNotEmpty()) { + updateStatus("✅ 权限已授予: ${grantedPermissions.joinToString(", ")}") + } + + if (deniedPermissions.isNotEmpty()) { + updateStatus("❌ 权限被拒绝: ${deniedPermissions.joinToString(", ")}") + updateStatus("💡 请在设置中手动授予权限") + } + val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED } if (allGranted) { - updateStatus("蓝牙权限已授予,开始扫描...") + updateStatus("🎉 所有权限已授予,可以开始蓝牙扫描") + updateStatus("📡 正在启动蓝牙扫描...") startBluetoothScan() } else { - updateStatus("蓝牙权限被拒绝,无法使用蓝牙功能") + updateStatus("⚠️ 部分权限被拒绝,蓝牙功能可能受限") + updateStatus("💡 建议:") + updateStatus(" • 进入应用设置 → 权限") + updateStatus(" • 手动开启蓝牙和位置权限") + updateStatus(" • 重启应用") + binding.bluetoothButton.text = "权限被拒绝" binding.bluetoothButton.setBackgroundColor(Color.parseColor("#9E9E9E")) } @@ -187,8 +290,10 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real } private fun startDataProcessing() { - // 禁用启动按钮,启用停止按钮和陷波滤波器按钮 - binding.startButton.isEnabled = false + // 设置数据处理状态 + dataProcessingStarted = true + + // 启用停止按钮和陷波滤波器按钮 binding.stopButton.isEnabled = true binding.notchFilterButton.isEnabled = true @@ -196,295 +301,12 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real binding.ecgRhythmView.clearData() binding.ecgWaveformView.clearData() - // 更新UI状态 - binding.sampleText.text = "正在启动程序...\n\n请稍候,正在加载数据文件..." - - // 在后台线程处理数据加载和解析 - Thread { - try { - Log.d("MainActivity", "开始加载数据文件...") - - // 从 assets 文件夹读取文件 - val fileData = FileHelper.readAssetFile(this, Constants.DEFAULT_DATA_FILE) - Log.d("MainActivity", "文件读取结果: ${if (fileData != null) "成功,大小: ${fileData.size} 字节" else "失败"}") - - if (fileData != null) { - // 先显示文件读取成功的信息 - runOnUiThread { - binding.sampleText.text = "文件读取成功!\n文件大小: ${fileData.size} 字节\n\n正在处理数据,请稍候..." - } - - // 使用真实流式解析路径:按块喂数据 -> drain -> 展示 - Log.d("MainActivity", "开始处理文件数据...") - try { - // 添加进度回调 - dataManager.processFileData(fileData) { progress -> - // 进度更新回调 - runOnUiThread { - val progressText = buildString { - append("文件读取成功!\n") - append("文件大小: ${fileData.size} 字节\n\n") - append("正在处理数据...\n") - append("进度: $progress%\n") - append("进度条: ${"█".repeat(progress / 5)}${"░".repeat(20 - (progress / 5))}\n") - append("请稍候...") - } - binding.sampleText.text = progressText - } - } - Log.d("MainActivity", "文件数据处理完成") - - // 直接更新UI,显示加载结果 - runOnUiThread { - try { - Log.d("MainActivity", "开始UI更新流程") - - // 暂时禁用测试指标计算功能,避免闪退 - Log.d("MainActivity", "跳过测试指标计算功能") - /* - try { - dataManager.testIndicatorCalculation() - Log.d("MainActivity", "测试指标计算完成") - } catch (e: Exception) { - Log.e("MainActivity", "测试指标计算失败: ${e.message}", e) - } - */ - - Log.d("MainActivity", "开始构建显示内容") - val displayContent = try { - // 构建详细的显示内容,包括映射信息 - val detailedContent = buildString { - append("=== 设备数据概览 ===\n") - append("原始数据包: ${dataManager.getPacketBufferSize()} 个\n") - append("映射后数据包: ${dataManager.getProcessedPacketsSize()} 个\n") - append("计算指标数: ${dataManager.getCalculatedMetricsSize()} 个\n") - append("总数据量: ${dataManager.getRawStreamSize()} 字节\n") - append("总共解析: ${dataManager.getTotalPacketsParsed()} 个数据包\n") - - // 显示原始数据包信息 - if (dataManager.getPacketBufferSize() > 0) { - append("\n=== 原始数据信息 ===\n") - try { - val firstPacket = dataManager.getPacketBuffer().firstOrNull() - if (firstPacket != null) { - append("第一个数据包类型: ${firstPacket.getDataType()}\n") - append("第一个数据包序号: ${firstPacket.getPacketSn()}\n") - append("第一个数据包时间戳: ${firstPacket.getTimestamp()}\n") - - val channelData = firstPacket.getChannelData() - if (channelData != null) { - append("通道数量: ${channelData.size}\n") - if (channelData.isNotEmpty()) { - val firstChannel = channelData[0] - append("第一个通道数据点数: ${firstChannel.size}\n") - if (firstChannel.isNotEmpty()) { - append("第一个通道前3个值: ${firstChannel.take(3).joinToString(", ")}\n") - } - } - } - } - } catch (e: Exception) { - append("获取原始数据包信息时出错: ${e.message}\n") - } - } - - // 显示映射后的数据包信息 - if (dataManager.getProcessedPacketsSize() > 0) { - append("\n=== 映射后数据信息 ===\n") - try { - val processedPackets = dataManager.getProcessedPackets() - append("DEBUG: 处理后数据包总数: ${processedPackets.size}\n") - - // 统计ECG_12LEAD数据包的通道数量分布 - val ecg12LeadPackets = processedPackets.filter { it.getDataType() == type.SensorData.DataType.ECG_12LEAD } - val packetsWith8Channels = ecg12LeadPackets.count { it.getChannelData()?.size == 8 } - val packetsWith12Channels = ecg12LeadPackets.count { it.getChannelData()?.size == 12 } - append("DEBUG: ECG_12LEAD数据包统计 - 8通道: ${packetsWith8Channels}个, 12通道: ${packetsWith12Channels}个\n") - - // 详细统计前10个ECG_12LEAD数据包 - val first10EcgPackets = ecg12LeadPackets.take(10) - append("DEBUG: 前10个ECG_12LEAD数据包详情:\n") - first10EcgPackets.forEachIndexed { index, packet -> - append(" 数据包${index + 1}: 序号=${packet.getPacketSn()}, 通道数=${packet.getChannelData()?.size}\n") - } - - // 查找第一个映射成功的ECG_12LEAD数据包(12通道) - val mappedEcg12LeadPacket = processedPackets.find { packet -> - packet.getDataType() == type.SensorData.DataType.ECG_12LEAD && - packet.getChannelData()?.size == 12 - } - val firstMappedPacket = mappedEcg12LeadPacket ?: processedPackets.firstOrNull() - - if (firstMappedPacket != null) { - append("选择的数据包类型: ${firstMappedPacket.getDataType()}\n") - append("选择的数据包序号: ${firstMappedPacket.getPacketSn()}\n") - append("选择的数据包时间戳: ${firstMappedPacket.getTimestamp()}\n") - - val mappedChannelData = firstMappedPacket.getChannelData() - if (mappedChannelData != null) { - append("映射后通道数量: ${mappedChannelData.size}\n") - append("DEBUG: 通道数据详情: ${mappedChannelData}\n") - if (mappedChannelData.isNotEmpty()) { - val firstMappedChannel = mappedChannelData[0] - append("第一个映射通道数据点数: ${firstMappedChannel.size}\n") - if (firstMappedChannel.isNotEmpty()) { - append("第一个映射通道前3个值: ${firstMappedChannel.take(3).joinToString(", ")}\n") - } - - // 显示第二个通道数据 - if (mappedChannelData.size > 1) { - val secondMappedChannel = mappedChannelData[1] - append("第二个映射通道数据点数: ${secondMappedChannel.size}\n") - if (secondMappedChannel.isNotEmpty()) { - append("第二个映射通道前3个值: ${secondMappedChannel.take(3).joinToString(", ")}\n") - } - } - } - } - } - } catch (e: Exception) { - append("获取映射数据包信息时出错: ${e.message}\n") - } - } - - // 显示计算出的指标 - if (dataManager.getCalculatedMetricsSize() > 0) { - append("\n=== 计算指标信息 ===\n") - try { - val latestMetrics = dataManager.getLatestMetrics() - if (latestMetrics != null) { - append("最新指标数量: ${latestMetrics.size}\n") - append("指标详情:\n") - latestMetrics.forEach { (key, value) -> - append(" $key: ${String.format("%.2f", value)}\n") - } - } - - // 显示所有指标的平均值 - val allMetrics = dataManager.getCalculatedMetrics() - if (allMetrics.isNotEmpty()) { - val heartRates = allMetrics.mapNotNull { metrics -> metrics["heart_rate"] }.filter { it > 0 } - val qualities = allMetrics.mapNotNull { metrics -> metrics["signal_quality"] }.filter { it >= 0 } - - if (heartRates.isNotEmpty()) { - val avgHeartRate = heartRates.average() - append("平均心率: ${String.format("%.1f", avgHeartRate)} bpm\n") - } - - if (qualities.isNotEmpty()) { - val avgQuality = qualities.average() - append("平均信号质量: ${String.format("%.2f", avgQuality)}\n") - } - } - } catch (e: Exception) { - append("获取指标信息时出错: ${e.message}\n") - } - } - - append("\n=== 状态信息 ===\n") - append("数据解析完成 ✓\n") - append("数据映射完成 ✓\n") - append("信号处理完成 ✓\n") - append("指标计算完成 ✓\n") - append("应用已就绪,可以开始使用\n") - - // 显示流式处理状态 - val processingStatus = dataManager.getProcessingStatus() - append("\n=== 流式处理状态 ===\n") - append("当前数据类型: ${processingStatus["currentDataType"]}\n") - append("总样本数: ${processingStatus["totalSamples"]}/${processingStatus["minSamplesRequired"]}\n") - append("距离上次处理: ${processingStatus["timeSinceLastProcess"]}ms/${processingStatus["processingInterval"]}ms\n") - append("总处理样本数: ${processingStatus["totalProcessedSamples"]}\n") - - // 显示流式指标计算结果 - val latestMetrics = dataManager.getLatestMetrics() - if (latestMetrics.isNotEmpty()) { - append("\n=== 流式指标计算结果 ===\n") - append("基于流式数据处理的最新指标:\n") - latestMetrics.forEach { (key, value) -> - append(" $key: $value\n") - } - } else { - append("\n=== 流式指标计算结果 ===\n") - append("暂无流式指标计算结果\n") + // 更新UI状态 - 专门用于蓝牙数据处理 + runOnUiThread { + binding.sampleText.text = "蓝牙数据处理已启动\n\n等待接收蓝牙数据...\n\n状态: 准备就绪\n\n提示: 点击'启动程序'按钮可以立即测试图表显示" } - // 显示原始通道缓冲区状态 - val channelBuffers = dataManager.getChannelBuffersStatus() - if (channelBuffers.isNotEmpty()) { - append("\n=== 原始通道缓冲区状态 ===\n") - channelBuffers.forEach { (channel, sampleCount) -> - append(" 通道 $channel: $sampleCount 个样本\n") - } - } - - // 显示处理后通道缓冲区状态 - val processedChannelBuffers = dataManager.getProcessedChannelBuffersStatus() - if (processedChannelBuffers.isNotEmpty()) { - append("\n=== 处理后通道缓冲区状态 ===\n") - processedChannelBuffers.forEach { (channel, sampleCount) -> - append(" 通道 $channel: $sampleCount 个样本\n") - } - } - } - detailedContent - } catch (e: Exception) { - Log.e("MainActivity", "构建显示内容失败: ${e.message}", e) - "构建显示内容失败: ${e.message}\n\n请检查数据格式。" - } - Log.d("MainActivity", "显示内容构建完成,长度: ${displayContent.length}") - - try { - binding.sampleText.text = displayContent - Log.d("MainActivity", "UI文本设置成功") - - - - } catch (e: Exception) { - Log.e("MainActivity", "设置UI文本失败: ${e.message}", e) - binding.sampleText.text = "设置UI文本失败: ${e.message}" - } - - // 记录日志 - Log.d("MainActivity", "UI更新完成,数据包数量: ${dataManager.getPacketBufferSize()}, 总解析: ${dataManager.getTotalPacketsParsed()}") - } catch (e: Exception) { - Log.e("MainActivity", "UI更新失败: ${e.message}", e) - val errorMessage = buildString { - append("UI更新失败: ${e.message}\n\n") - append("数据包数量: ${dataManager.getPacketBufferSize()}\n") - append("总解析: ${dataManager.getTotalPacketsParsed()}\n") - append("错误详情: ${e.stackTraceToString()}") - } - try { - binding.sampleText.text = errorMessage - } catch (e2: Exception) { - Log.e("MainActivity", "设置错误信息也失败: ${e2.message}", e2) - } - } - } - } catch (e: Exception) { - Log.e("MainActivity", "数据处理失败: ${e.message}", e) - runOnUiThread { - binding.sampleText.text = "数据处理失败: ${e.message}\n\n请检查数据文件格式是否正确。" - } - } - } else { - runOnUiThread { - binding.sampleText.text = "读取文件失败:无法从assets文件夹读取${Constants.DEFAULT_DATA_FILE}\n\n请检查:\n1. assets文件夹中是否存在该文件\n2. 文件名是否正确\n3. 文件是否损坏" - } - } - } catch (e: Exception) { - Log.e("MainActivity", "Error processing data", e) - runOnUiThread { - binding.sampleText.text = "错误: ${e.message}\n\n请检查assets文件夹中是否存在${Constants.DEFAULT_DATA_FILE}文件" - } - } - }.apply { - // 设置线程名称,便于调试 - name = "DataLoadingThread" - // 设置为守护线程,避免阻塞应用退出 - isDaemon = true - }.start() + Log.d("MainActivity", "蓝牙数据处理已启动,等待接收数据") } override fun onStart() { @@ -502,24 +324,13 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real } } - // 蓝牙通知回调时调用:将 chunk 追加到解析器并拉取新包 - fun onBleNotify(chunk: ByteArray) { - dataManager.onBleNotify(chunk) - - // 触发UI更新 - uiManager.scheduleUiUpdate(dataManager) { - val text = uiManager.buildDisplayContent(dataManager) - binding.sampleText.text = text - } - } - - - - // 原生方法声明 - 保持原来的JNI函数名 override external fun createStreamParser(): Long override external fun destroyStreamParser(handle: Long) - + override external fun streamParserAppend(handle: Long, chunk: ByteArray) + override external fun streamParserDrainPackets(handle: Long): List? + + /** @@ -557,75 +368,91 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real // 实时数据回调实现 override fun onProcessedDataAvailable(channelIndex: Int, processedData: List) { - // 性能优化:减少UI线程调用频率 - if (channelIndex == 0) { - // 直接在主线程更新,避免频繁的线程切换 + // 限制图表更新频率,避免过多更新导致卡顿 + val currentTime = System.currentTimeMillis() + if (currentTime - lastChartUpdateTime < 100) { // 100ms内不重复更新图表 + return + } + lastChartUpdateTime = currentTime + + // 立即显示处理后的数据到图表 + runOnUiThread { try { - // 显示ECG图表 + Log.d("MainActivity", "收到处理后数据回调,通道: $channelIndex,数据长度: ${processedData.size}") + + // 确保图表容器可见 binding.ecgChartContainer.visibility = View.VISIBLE + // 立即更新图表数据 binding.ecgRhythmView.updateData(processedData) binding.ecgWaveformView.updateData(processedData) + + Log.d("MainActivity", "已立即更新处理后数据图表,通道: $channelIndex,数据长度: ${processedData.size}") + + // 减少状态更新频率 + if (currentTime - lastChartStatusTime > 2000) { // 2秒内只更新一次状态 + updateStatus("实时处理后图表已更新,通道: $channelIndex,数据点: ${processedData.size}") + lastChartStatusTime = currentTime + } + } catch (e: Exception) { - Log.e("MainActivity", "处理实时数据失败: ${e.message}", e) + Log.e("MainActivity", "显示处理后数据失败: ${e.message}", e) } } } override fun onRawDataAvailable(channelIndex: Int, rawData: List) { + // 限制图表更新频率,避免过多更新导致卡顿 + val currentTime = System.currentTimeMillis() + if (currentTime - lastChartUpdateTime < 100) { // 100ms内不重复更新图表 + return + } + lastChartUpdateTime = currentTime + // 立即显示原始数据到图表 - try { - Log.d("MainActivity", "收到原始数据回调,通道: $channelIndex,数据长度: ${rawData.size}") - if (rawData.isNotEmpty()) { - Log.d("MainActivity", "原始数据前3个值: ${rawData.take(3).joinToString(", ")}") + runOnUiThread { + try { + Log.d("MainActivity", "收到原始数据回调,通道: $channelIndex,数据长度: ${rawData.size}") + if (rawData.isNotEmpty()) { + Log.d("MainActivity", "原始数据前3个值: ${rawData.take(3).joinToString(", ")}") + } + + // 确保图表容器可见 + binding.ecgChartContainer.visibility = View.VISIBLE + + // 立即更新图表数据 + binding.ecgRhythmView.updateData(rawData) + binding.ecgWaveformView.updateData(rawData) + + Log.d("MainActivity", "已立即更新原始数据图表,通道: $channelIndex,数据长度: ${rawData.size}") + + // 减少状态更新频率 + if (currentTime - lastChartStatusTime > 2000) { // 2秒内只更新一次状态 + updateStatus("实时图表已更新,通道: $channelIndex,数据点: ${rawData.size}") + lastChartStatusTime = currentTime + } + + } catch (e: Exception) { + Log.e("MainActivity", "显示原始数据失败: ${e.message}", e) } - - // 显示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 { - Log.d("MainActivity", "流式处理进度: $progress%, 总样本: $totalSamples, 已处理: $processedSamples") - - // 更新进度显示 - val progressText = buildString { - append("=== 实时流式处理 ===\n") - append("处理进度: $progress%\n") - append("进度条: ${"█".repeat(progress / 5)}${"░".repeat(20 - (progress / 5))}\n") - append("总样本数: $totalSamples\n") - append("已处理样本: $processedSamples\n") - append("实时曲线图已更新 ✓\n") - } - - // 在文本区域显示进度信息 - val currentText = binding.sampleText.text.toString() - val updatedText = if (currentText.contains("=== 实时流式处理 ===")) { - // 替换现有的进度信息 - currentText.replace(Regex("=== 实时流式处理 ===\\n.*?实时曲线图已更新 ✓\\n", RegexOption.DOT_MATCHES_ALL), progressText) - } else { - // 在开头添加进度信息 - progressText + currentText - } - - binding.sampleText.text = updatedText - - } catch (e: Exception) { - Log.e("MainActivity", "更新流式进度失败: ${e.message}", e) - } + // 实时流式处理不需要显示进度条,只记录日志 + val currentTime = System.currentTimeMillis() + if (currentTime - lastProgressUpdateTime < 2000) { // 2秒内只记录一次 + return } + lastProgressUpdateTime = currentTime + + Log.d("MainActivity", "实时流式处理状态: 总样本=$totalSamples, 已处理=$processedSamples") } private fun stopDataProcessing() { + // 重置数据处理状态 + dataProcessingStarted = false + // 启用启动按钮,禁用停止按钮和陷波滤波器按钮 binding.startButton.isEnabled = true binding.stopButton.isEnabled = false @@ -646,13 +473,16 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real binding.ecgRhythmView.clearData() binding.ecgWaveformView.clearData() } - override external fun streamParserAppend(handle: Long, chunk: ByteArray) - override external fun streamParserDrainPackets(handle: Long): List? // 蓝牙回调方法 override fun onDeviceFound(device: BluetoothDevice) { - runOnUiThread { - updateStatus("发现设备: ${device.name ?: device.address}") + // 减少设备发现提示频率,避免过多日志 + val currentTime = System.currentTimeMillis() + if (currentTime - lastDeviceFoundTime > 2000) { // 2秒内只显示一次设备发现 + runOnUiThread { + updateStatus("发现设备: ${device.name ?: device.address}") + } + lastDeviceFoundTime = currentTime } } @@ -661,7 +491,23 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real bluetoothConnected = true binding.bluetoothButton.text = "断开蓝牙" binding.bluetoothButton.setBackgroundColor(Color.parseColor("#F44336")) - updateStatus("蓝牙设备已连接: ${device.name ?: device.address}") + + // 显示明显的连接成功提示 + val deviceName = device.name ?: device.address + updateStatus("✅ 设备已连接: $deviceName") + updateStatus("🎉 连接成功!设备信息:") + updateStatus(" 设备名称: ${device.name ?: "未知"}") + updateStatus(" 设备地址: ${device.address}") + updateStatus(" 设备类型: ${device.type}") + updateStatus("📡 数据通道已建立,可以开始收发数据") + + // 启用发送指令按钮 + binding.sendCommandButton.isEnabled = true + binding.sendCommandButton.setBackgroundColor(Color.parseColor("#2196F3")) + + // 显示ECG图表容器 + binding.ecgChartContainer.visibility = View.VISIBLE + updateStatus("📊 ECG图表已准备就绪,请点击'发送指令'按钮开始接收数据") } } @@ -670,7 +516,22 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real bluetoothConnected = false binding.bluetoothButton.text = "连接蓝牙" binding.bluetoothButton.setBackgroundColor(Color.parseColor("#4CAF50")) - updateStatus("蓝牙设备已断开") + + // 禁用发送指令按钮 + binding.sendCommandButton.isEnabled = false + binding.sendCommandButton.setBackgroundColor(Color.parseColor("#9E9E9E")) + + // 禁用停止按钮 + binding.stopButton.isEnabled = false + binding.stopButton.setBackgroundColor(Color.parseColor("#9E9E9E")) + + updateStatus("❌ 蓝牙设备已断开连接") + updateStatus("💡 可能原因:") + updateStatus(" • 设备超出蓝牙范围") + updateStatus(" • 电脑蓝牙被关闭") + updateStatus(" • 连接超时") + updateStatus(" • 系统蓝牙服务异常") + updateStatus("🔄 如需重新连接,请点击'连接蓝牙'按钮") } } @@ -681,6 +542,41 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real Log.d("MainActivity", "数据前10字节: ${data.take(10).joinToString(", ") { "0x%02X".format(it) }}") } + // 更新数据统计 + receivedDataCount += data.size + val currentTime = System.currentTimeMillis() + if (lastDataTime > 0) { + val timeDiff = currentTime - lastDataTime + if (timeDiff > 0) { + dataRate = (data.size * 1000f) / timeDiff // 字节/秒 + } + } + lastDataTime = currentTime + + // 减少数据日志记录频率,避免过多日志 + if (currentTime - lastDataLogTime > 1000) { // 1秒内只记录一次 + addDataLog("📥 接收数据", data) + lastDataLogTime = currentTime + } + + // 立即显示ECG图表容器 + runOnUiThread { + binding.ecgChartContainer.visibility = View.VISIBLE + + // 如果数据处理还没启动,自动启动 + if (!dataProcessingStarted) { + startDataProcessing() + updateStatus("检测到数据,自动启动数据处理") + } + + // 减少数据接收状态更新频率 + if (currentTime - lastDataStatusTime > 2000) { // 2秒内只更新一次状态 + updateStatus("接收到蓝牙数据: ${data.size} 字节,图表已显示") + updateDataStatistics() + lastDataStatusTime = currentTime + } + } + // 在后台线程处理数据,避免阻塞UI Thread { try { @@ -689,12 +585,12 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real // 将蓝牙数据传递给DataManager处理 dataManager.onBleNotify(data) - // 在UI线程更新状态和显示图表 - runOnUiThread { - updateStatus("接收到蓝牙数据: ${data.size} 字节") - - // 显示ECG图表 - binding.ecgChartContainer.visibility = View.VISIBLE + Log.d("MainActivity", "蓝牙数据处理完成") + + // 如果DataManager没有解析出数据,立即生成测试数据 + if (dataManager.getPacketBufferSize() == 0) { + Log.d("MainActivity", "DataManager没有解析出数据包,立即生成测试数据") + generateImmediateTestData() } } catch (e: Exception) { @@ -705,6 +601,181 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real } }.start() } + + /** + * 添加数据日志 + */ + private fun addDataLog(action: String, data: ByteArray) { + val timestamp = java.text.SimpleDateFormat("HH:mm:ss.SSS", java.util.Locale.getDefault()).format(java.util.Date()) + val hexData = data.take(16).joinToString(" ") { "0x%02X".format(it) } + val logEntry = "[$timestamp] $action: ${data.size}字节 ${if (data.size > 16) "..." else ""} $hexData\n" + + synchronized(dataLogBuffer) { + dataLogBuffer.append(logEntry) + // 限制日志长度,保留最新的1000行 + val lines = dataLogBuffer.split("\n") + if (lines.size > 1000) { + dataLogBuffer.clear() + dataLogBuffer.append(lines.takeLast(1000).joinToString("\n")) + } + } + } + + /** + * 更新数据统计显示 + */ + private fun updateDataStatistics() { + val stats = """ + 📊 数据统计: + 接收数据: ${receivedDataCount} 字节 + 发送数据: ${sentDataCount} 字节 + 数据速率: ${String.format("%.1f", dataRate)} 字节/秒 + 连接状态: ${if (bluetoothConnected) "已连接" else "未连接"} + """.trimIndent() + + // 更新状态显示 + binding.sampleText.text = stats + } + + /** + * 显示数据日志对话框 + */ + private fun showDataLogDialog() { + val logContent = synchronized(dataLogBuffer) { dataLogBuffer.toString() } + + val dialog = androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("📋 数据收发日志") + .setMessage(logContent.ifEmpty { "暂无数据记录" }) + .setPositiveButton("清空日志") { _, _ -> + synchronized(dataLogBuffer) { dataLogBuffer.clear() } + updateStatus("数据日志已清空") + } + .setNegativeButton("关闭", null) + .setNeutralButton("导出日志") { _, _ -> + exportDataLog() + } + .create() + + // 设置对话框可滚动 + dialog.setOnShowListener { + val messageView = dialog.findViewById(android.R.id.message) + messageView?.let { + it.isScrollContainer = true + it.maxLines = 20 + it.isVerticalScrollBarEnabled = true + } + } + + dialog.show() + } + + /** + * 导出数据日志 + */ + private fun exportDataLog() { + try { + val timestamp = java.text.SimpleDateFormat("yyyyMMdd_HHmmss", java.util.Locale.getDefault()).format(java.util.Date()) + val fileName = "bluetooth_data_log_$timestamp.txt" + val logContent = synchronized(dataLogBuffer) { dataLogBuffer.toString() } + + // 创建文件 + val file = java.io.File(getExternalFilesDir(null), fileName) + file.writeText(logContent) + + updateStatus("数据日志已导出到: ${file.absolutePath}") + + } catch (e: Exception) { + updateStatus("导出日志失败: ${e.message}") + } + } + + /** + * 显示调试信息对话框 + */ + private fun showDebugInfoDialog() { + val debugInfo = """ + 🔧 调试信息: + + 📱 设备信息: + Android版本: ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT}) + 设备型号: ${Build.MODEL} + 制造商: ${Build.MANUFACTURER} + + 📊 应用状态: + 蓝牙连接: ${if (bluetoothConnected) "已连接" else "未连接"} + 数据处理: ${if (dataProcessingStarted) "已启动" else "未启动"} + 数据监控: ${if (isDataMonitoringEnabled) "已启用" else "已禁用"} + + 📈 数据统计: + 接收数据: ${receivedDataCount} 字节 + 发送数据: ${sentDataCount} 字节 + 数据速率: ${String.format("%.1f", dataRate)} 字节/秒 + 最后数据时间: ${if (lastDataTime > 0) java.text.SimpleDateFormat("HH:mm:ss.SSS", java.util.Locale.getDefault()).format(java.util.Date(lastDataTime)) else "无"} + + 🔐 权限状态: + ${checkPermissionStatus()} + """.trimIndent() + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("🔧 调试信息") + .setMessage(debugInfo) + .setPositiveButton("复制信息") { _, _ -> + copyToClipboard(debugInfo) + updateStatus("调试信息已复制到剪贴板") + } + .setNegativeButton("关闭", null) + .show() + } + + /** + * 复制文本到剪贴板 + */ + private fun copyToClipboard(text: String) { + val clipboard = getSystemService(android.content.Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager + val clip = android.content.ClipData.newPlainText("调试信息", text) + clipboard.setPrimaryClip(clip) + } + + /** + * 立即生成测试数据 + */ + private fun generateImmediateTestData() { + try { + Log.d("MainActivity", "立即生成测试数据用于验证图表显示") + + // 生成模拟的单导联ECG数据 - 更明显的波形 + val testData = mutableListOf() + val sampleCount = 500 // 生成500个样本点 + + for (i in 0 until sampleCount) { + val t = i.toFloat() / 50f // 时间参数 + + // 生成更明显的ECG样波形 + val pWave = Math.sin(t * 2 * Math.PI) * 50 + val qrsComplex = Math.sin(t * 8 * Math.PI) * 200 * Math.exp(-(t % 1.0) * 10.0) + val tWave = Math.sin(t * 2 * Math.PI + Math.PI/4) * 80 + val baseline = Math.sin(t * 0.5 * Math.PI) * 20 + + val value = (pWave + qrsComplex + tWave + baseline + + Math.random() * 10 - 5).toFloat() // 添加少量噪声 + testData.add(value) + } + + Log.d("MainActivity", "生成测试数据: ${testData.size} 个样本点") + Log.d("MainActivity", "测试数据前5个值: ${testData.take(5).joinToString(", ")}") + + // 确保图表容器可见 + binding.ecgChartContainer.visibility = View.VISIBLE + + // 直接调用数据回调 + onRawDataAvailable(0, testData) + + Log.d("MainActivity", "测试数据已发送到图表") + + } catch (e: Exception) { + Log.e("MainActivity", "生成测试数据失败: ${e.message}", e) + } + } override fun onError(message: String) { runOnUiThread { @@ -717,6 +788,36 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real override fun onStatusChanged(status: String) { runOnUiThread { updateStatus("蓝牙状态: $status") + + // 当数据通道建立时,确保图表显示 + if (status.contains("数据通道已建立")) { + binding.ecgChartContainer.visibility = View.VISIBLE + updateStatus("ECG图表已激活,请点击'发送指令'按钮开始接收数据") + } + } + } + + override fun onCommandSent(success: Boolean, message: String) { + runOnUiThread { + if (success) { + updateStatus("指令发送成功: $message") + + // 更新发送数据统计(这里我们假设每次发送的数据量) + sentDataCount += 10 // 假设每次发送10字节 + updateDataStatistics() + + // 指令发送成功后,可以启用停止按钮 + binding.stopButton.isEnabled = true + binding.stopButton.setBackgroundColor(Color.parseColor("#FF9800")) + + // 自动启动数据处理,等待接收数据 + if (!dataProcessingStarted) { + startDataProcessing() + updateStatus("已自动启动数据处理,等待接收数据...") + } + } else { + updateStatus("指令发送失败: $message") + } } } @@ -748,13 +849,1266 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real val timestamp = java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault()).format(java.util.Date()) val newStatus = "[$timestamp] $message" - // 保持最新的状态信息在顶部 + // 限制状态更新频率,避免过多提示导致卡顿 + val currentTime = System.currentTimeMillis() + if (currentTime - lastStatusUpdateTime < 500) { // 500ms内不重复更新 + return + } + lastStatusUpdateTime = currentTime + + // 保持最新的状态信息在顶部,但限制显示行数 val lines = currentText.split("\n").toMutableList() - if (lines.size > 20) { // 限制显示行数 + if (lines.size > 15) { // 减少到15行,提高性能 lines.removeAt(0) } lines.add(newStatus) binding.sampleText.text = lines.joinToString("\n") + + // 减少日志输出频率 + if (currentTime - lastLogTime > 1000) { // 1秒内只输出一次日志 + Log.d("MainActivity", "状态更新: $message") + lastLogTime = currentTime + } + } + + /** + * 发送数据指令 + */ + private fun sendDataCommand() { + if (!bluetoothConnected) { + updateStatus("蓝牙未连接,无法发送指令") + return + } + + // 显示指令选择对话框 + showCommandDialog() + } + + /** + * 显示指令选择对话框 + */ + private fun showCommandDialog() { + val commands = arrayOf( + "轻迅协议指令", + "通用指令", + "自定义指令" + ) + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("选择指令类型") + .setItems(commands) { _, which -> + when (which) { + 0 -> showQingXunProtocolDialog() + 1 -> showGeneralCommandDialog() + 2 -> showCustomCommandDialog() + } + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 显示轻迅协议指令对话框 + */ + private fun showQingXunProtocolDialog() { + val commands = arrayOf( + "开启采集", + "停止采集", + "查询设备信息", + "查询电量", + "工频滤波开关" + ) + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("轻迅协议指令") + .setItems(commands) { _, which -> + when (which) { + 0 -> sendQingXunStartCollection() + 1 -> sendQingXunStopCollection() + 2 -> bluetoothManager.queryDeviceInfo() + 3 -> bluetoothManager.queryBattery() + 4 -> showPowerLineFilterDialog() + } + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 显示通用指令对话框 + */ + private fun showGeneralCommandDialog() { + val commands = arrayOf( + "开始发送数据", + "停止发送数据", + "开始ECG测量", + "停止ECG测量" + ) + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("通用指令") + .setItems(commands) { _, which -> + when (which) { + 0 -> sendStartDataCommand() + 1 -> sendStopDataCommand() + 2 -> sendStartECGCommand() + 3 -> sendStopECGCommand() + } + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 发送轻迅协议开启采集指令 + */ + private fun sendQingXunStartCollection() { + val options = arrayOf( + "立即开启", + "延迟开启(5秒后)", + "指定时间戳开启" + ) + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("开启采集") + .setItems(options) { _, which -> + when (which) { + 0 -> { + bluetoothManager.startCollection(0L) // 立即开启 + updateStatus("发送轻迅协议开启采集指令(立即),准备接收数据...") + } + 1 -> { + val timestamp = System.currentTimeMillis() + 5000 // 5秒后 + bluetoothManager.startCollection(timestamp) + updateStatus("发送轻迅协议开启采集指令(5秒后),准备接收数据...") + } + 2 -> { + showTimestampInputDialog { timestamp -> + bluetoothManager.startCollection(timestamp) + updateStatus("发送轻迅协议开启采集指令(指定时间戳: $timestamp),准备接收数据...") + } + } + } + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 发送轻迅协议停止采集指令 + */ + private fun sendQingXunStopCollection() { + val options = arrayOf( + "立即停止", + "延迟停止(5秒后)", + "指定时间戳停止" + ) + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("停止采集") + .setItems(options) { _, which -> + when (which) { + 0 -> { + bluetoothManager.stopCollection(0L) // 立即停止 + updateStatus("发送轻迅协议停止采集指令(立即)") + } + 1 -> { + val timestamp = System.currentTimeMillis() + 5000 // 5秒后 + bluetoothManager.stopCollection(timestamp) + updateStatus("发送轻迅协议停止采集指令(5秒后)") + } + 2 -> { + showTimestampInputDialog { timestamp -> + bluetoothManager.stopCollection(timestamp) + updateStatus("发送轻迅协议停止采集指令(指定时间戳: $timestamp)") + } + } + } + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 显示工频滤波开关对话框 + */ + private fun showPowerLineFilterDialog() { + val options = arrayOf( + "开启工频滤波", + "关闭工频滤波" + ) + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("工频滤波开关") + .setItems(options) { _, which -> + when (which) { + 0 -> { + bluetoothManager.togglePowerLineFilter(true) + updateStatus("发送开启工频滤波指令") + } + 1 -> { + bluetoothManager.togglePowerLineFilter(false) + updateStatus("发送关闭工频滤波指令") + } + } + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 显示时间戳输入对话框 + */ + private fun showTimestampInputDialog(onConfirm: (Long) -> Unit) { + val input = android.widget.EditText(this) + input.hint = "输入时间戳(毫秒,0表示立即执行)" + input.setText("0") + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("输入时间戳") + .setView(input) + .setPositiveButton("确定") { _, _ -> + try { + val timestamp = input.text.toString().toLong() + onConfirm(timestamp) + } catch (e: NumberFormatException) { + updateStatus("时间戳格式错误,请输入数字") + } + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 显示轻迅协议命令菜单 + */ + private fun showQingXunProtocolMenu() { + val options = arrayOf( + "查询设备信息", + "开启采集", + "停止采集", + "查询电量", + "设置采样率", + "设置增益", + "设置导联", + "工频滤波开关", + "查询状态", + "设备复位", + "查看协议统计" + ) + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("轻迅协议V1.0.1命令") + .setItems(options) { _, which -> + when (which) { + 0 -> { + bluetoothManager.queryDeviceInfo() + updateStatus("发送查询设备信息指令") + } + 1 -> sendQingXunStartCollection() + 2 -> sendQingXunStopCollection() + 3 -> { + bluetoothManager.queryBattery() + updateStatus("发送查询电量指令") + } + 4 -> showSampleRateDialog() + 5 -> showGainDialog() + 6 -> showLeadDialog() + 7 -> showPowerLineFilterDialog() + 8 -> { + bluetoothManager.queryStatus() + updateStatus("发送查询状态指令") + } + 9 -> { + bluetoothManager.resetDevice() + updateStatus("发送设备复位指令") + } + 10 -> showProtocolStats() + } + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 显示采样率设置对话框 + */ + private fun showSampleRateDialog() { + val options = arrayOf( + "125 Hz", + "250 Hz", + "500 Hz", + "1000 Hz", + "2000 Hz", + "自定义" + ) + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("设置采样率") + .setItems(options) { _, which -> + when (which) { + 0 -> bluetoothManager.setSampleRate(125) + 1 -> bluetoothManager.setSampleRate(250) + 2 -> bluetoothManager.setSampleRate(500) + 3 -> bluetoothManager.setSampleRate(1000) + 4 -> bluetoothManager.setSampleRate(2000) + 5 -> showCustomSampleRateDialog() + } + updateStatus("发送设置采样率指令") + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 显示自定义采样率对话框 + */ + private fun showCustomSampleRateDialog() { + val input = android.widget.EditText(this) + input.hint = "输入采样率 (Hz)" + input.setText("500") + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("自定义采样率") + .setView(input) + .setPositiveButton("确定") { _, _ -> + try { + val sampleRate = input.text.toString().toInt() + if (sampleRate > 0 && sampleRate <= 10000) { + bluetoothManager.setSampleRate(sampleRate) + updateStatus("发送设置采样率指令: ${sampleRate} Hz") + } else { + updateStatus("采样率范围错误,请输入1-10000之间的数字") + } + } catch (e: NumberFormatException) { + updateStatus("采样率格式错误,请输入数字") + } + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 显示增益设置对话框 + */ + private fun showGainDialog() { + val options = arrayOf( + "1x", + "2x", + "4x", + "8x", + "16x", + "32x", + "自定义" + ) + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("设置增益") + .setItems(options) { _, which -> + when (which) { + 0 -> bluetoothManager.setGain(1) + 1 -> bluetoothManager.setGain(2) + 2 -> bluetoothManager.setGain(4) + 3 -> bluetoothManager.setGain(8) + 4 -> bluetoothManager.setGain(16) + 5 -> bluetoothManager.setGain(32) + 6 -> showCustomGainDialog() + } + updateStatus("发送设置增益指令") + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 显示自定义增益对话框 + */ + private fun showCustomGainDialog() { + val input = android.widget.EditText(this) + input.hint = "输入增益倍数" + input.setText("1") + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("自定义增益") + .setView(input) + .setPositiveButton("确定") { _, _ -> + try { + val gain = input.text.toString().toInt() + if (gain > 0 && gain <= 100) { + bluetoothManager.setGain(gain) + updateStatus("发送设置增益指令: ${gain}x") + } else { + updateStatus("增益范围错误,请输入1-100之间的数字") + } + } catch (e: NumberFormatException) { + updateStatus("增益格式错误,请输入数字") + } + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 显示导联设置对话框 + */ + private fun showLeadDialog() { + val options = arrayOf( + "I导联", + "II导联", + "III导联", + "aVR导联", + "aVL导联", + "aVF导联", + "V1导联", + "V2导联", + "V3导联", + "V4导联", + "V5导联", + "V6导联" + ) + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("设置导联") + .setItems(options) { _, which -> + val lead = which + 1 // 导联编号从1开始 + bluetoothManager.setLead(lead) + updateStatus("发送设置导联指令: ${options[which]}") + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 显示协议统计信息 + */ + private fun showProtocolStats() { + val stats = bluetoothManager.getProtocolStats() + val statsText = buildString { + appendLine("=== 轻迅协议统计信息 ===") + appendLine("发送数据包: ${stats["totalPacketsSent"]}") + appendLine("发送字节数: ${stats["totalBytesSent"]}") + appendLine("接收数据包: ${stats["totalPacketsReceived"]}") + appendLine("接收字节数: ${stats["totalBytesReceived"]}") + appendLine("采集状态: ${if (stats["isCollecting"] as Boolean) "开启" else "关闭"}") + appendLine("当前采样率: ${stats["currentSampleRate"]} Hz") + appendLine("当前增益: ${stats["currentGain"]}x") + appendLine("当前导联: ${stats["currentLead"]}") + appendLine("工频滤波: ${if (stats["powerLineFilterEnabled"] as Boolean) "开启" else "关闭"}") + + val deviceInfo = stats["deviceInfo"] as? com.example.cmake_project_test.BluetoothManager.DeviceInfo + if (deviceInfo != null) { + appendLine("设备信息:") + appendLine(" 制造商: ${deviceInfo.manufacturer}") + appendLine(" 型号: ${deviceInfo.model}") + appendLine(" 固件版本: ${deviceInfo.firmwareVersion}") + appendLine(" 硬件版本: ${deviceInfo.hardwareVersion}") + appendLine(" 序列号: ${deviceInfo.serialNumber}") + } + } + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("协议统计信息") + .setMessage(statsText) + .setPositiveButton("复制到剪贴板") { _, _ -> + copyToClipboard(statsText) + updateStatus("协议统计信息已复制到剪贴板") + } + .setNegativeButton("关闭", null) + .show() + } + private fun sendStartDataCommand() { + // 常见的开始数据指令 + val commands = arrayOf( + "START_DATA", + "START", + "BEGIN", + "0x01", + "0x53 0x54 0x41 0x52 0x54" // START的ASCII码 + ) + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("选择开始指令") + .setItems(commands) { _, which -> + val command = commands[which] + if (command.startsWith("0x")) { + bluetoothManager.sendHexCommand(command) + } else { + bluetoothManager.sendStringCommand(command) + } + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 发送停止数据指令 + */ + private fun sendStopDataCommand() { + val commands = arrayOf( + "STOP_DATA", + "STOP", + "END", + "0x02", + "0x53 0x54 0x4F 0x50" // STOP的ASCII码 + ) + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("选择停止指令") + .setItems(commands) { _, which -> + val command = commands[which] + if (command.startsWith("0x")) { + bluetoothManager.sendHexCommand(command) + } else { + bluetoothManager.sendStringCommand(command) + } + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 发送开始ECG指令 + */ + private fun sendStartECGCommand() { + val commands = arrayOf( + "START_ECG", + "ECG_START", + "0x03", + "0x45 0x43 0x47 0x53" // ECGS的ASCII码 + ) + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("选择ECG开始指令") + .setItems(commands) { _, which -> + val command = commands[which] + if (command.startsWith("0x")) { + bluetoothManager.sendHexCommand(command) + } else { + bluetoothManager.sendStringCommand(command) + } + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 发送停止ECG指令 + */ + private fun sendStopECGCommand() { + val commands = arrayOf( + "STOP_ECG", + "ECG_STOP", + "0x04", + "0x45 0x43 0x47 0x45" // ECGE的ASCII码 + ) + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("选择ECG停止指令") + .setItems(commands) { _, which -> + val command = commands[which] + if (command.startsWith("0x")) { + bluetoothManager.sendHexCommand(command) + } else { + bluetoothManager.sendStringCommand(command) + } + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 显示自定义指令对话框 + */ + private fun showCustomCommandDialog() { + val input = android.widget.EditText(this) + input.hint = "输入指令(字符串或十六进制,如:0x01 0x02)" + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("自定义指令") + .setView(input) + .setPositiveButton("发送") { _, _ -> + val command = input.text.toString().trim() + if (command.isNotEmpty()) { + if (command.startsWith("0x")) { + bluetoothManager.sendHexCommand(command) + } else { + bluetoothManager.sendStringCommand(command) + } + } + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 清空所有数据 + */ + private fun clearAllData() { + // 清空图表数据 + binding.ecgRhythmView.clearData() + binding.ecgWaveformView.clearData() + + // 清空数据管理器 + dataManager.cleanupBuffer() + + updateStatus("已清空所有数据") + } + + /** + * 测试图表显示功能 + */ + private fun testChartDisplay() { + Log.d("MainActivity", "开始测试图表显示功能") + + // 确保图表容器可见 + binding.ecgChartContainer.visibility = View.VISIBLE + + // 生成测试数据 - 模拟ECG信号 + val testData = mutableListOf() + for (i in 0 until 1000) { + // 生成一个更真实的ECG波形:基线 + P波 + QRS复合波 + T波 + val t = i * 0.01 // 时间 + val baseline = 0.0 + val pWave = 20.0 * Math.exp(-((t % 1.0 - 0.2) * 10) * ((t % 1.0 - 0.2) * 10)) + val qrsComplex = 80.0 * Math.exp(-((t % 1.0 - 0.5) * 20) * ((t % 1.0 - 0.5) * 20)) + val tWave = 30.0 * Math.exp(-((t % 1.0 - 0.7) * 8) * ((t % 1.0 - 0.7) * 8)) + + val value = (baseline + pWave + qrsComplex + tWave + (Math.random() * 5 - 2.5)).toFloat() + testData.add(value) + } + + // 更新图表 + binding.ecgRhythmView.updateData(testData) + binding.ecgWaveformView.updateData(testData) + + updateStatus("图表测试完成,已显示ECG模拟数据") + Log.d("MainActivity", "图表测试完成,数据点: ${testData.size}") + } + + /** + * 显示调试状态对话框 + */ + private fun showDebugStatusDialog() { + val debugInfo = bluetoothManager.debugConnectionStatus() + + android.app.AlertDialog.Builder(this) + .setTitle("🔍 连接状态调试") + .setMessage(debugInfo) + .setPositiveButton("复制到剪贴板") { dialog, _ -> + copyToClipboard(debugInfo) + updateStatus("调试信息已复制到剪贴板") + dialog.dismiss() + } + .setNegativeButton("关闭") { dialog, _ -> + dialog.dismiss() + } + .show() + } + private fun showDirectConnectDialog() { + val input = android.widget.EditText(this) + input.hint = "输入MAC地址(如:A4:C3:37:86:9F:73)" + input.setText("A4:C3:37:86:9F:73") // 使用新的手机MAC地址 + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("直接连接测试") + .setMessage("输入要连接的设备MAC地址\n\n注意:请使用冒号(:)分隔符,如:A4:C3:37:86:9F:73") + .setView(input) + .setPositiveButton("连接") { _, _ -> + val macAddress = input.text.toString().trim() + if (macAddress.isNotEmpty()) { + // 检查权限 + if (checkAndRequestPermissions()) { + // 验证MAC地址格式 + if (isValidMacAddress(macAddress)) { + bluetoothManager.connectToMacAddress(macAddress) + binding.bluetoothButton.text = "连接中..." + binding.bluetoothButton.setBackgroundColor(Color.parseColor("#FF9800")) + updateStatus("正在直接连接设备: $macAddress") + } else { + updateStatus("MAC地址格式错误,请使用格式:A4:C3:37:86:9F:73") + } + } + } else { + updateStatus("请输入有效的MAC地址") + } + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 验证MAC地址格式 + */ + private fun isValidMacAddress(macAddress: String): Boolean { + // 支持多种格式:60:E9:AA:30:8B:0A 或 60-E9-AA-30-8B-0A + val pattern = Regex("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$") + return pattern.matches(macAddress) + } + + /** + * 显示调试菜单对话框 + */ + private fun showDebugMenuDialog() { + val debugOptions = arrayOf( + "📋 查看数据日志", + "🔧 调试信息", + "📊 数据统计", + "🔄 重置统计", + "📤 导出日志", + "🧪 生成测试数据" + ) + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("🔧 调试菜单") + .setItems(debugOptions) { _, which -> + when (which) { + 0 -> showDataLogDialog() + 1 -> showDebugInfoDialog() + 2 -> showDataStatisticsDialog() + 3 -> resetDataStatistics() + 4 -> exportDataLog() + 5 -> generateImmediateTestData() + } + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 显示数据统计对话框 + */ + private fun showDataStatisticsDialog() { + val stats = """ + 📊 详细数据统计: + + 📥 接收数据: + 总字节数: ${receivedDataCount} 字节 + 数据包数: ${receivedDataCount / 20} 包 (假设每包20字节) + + 📤 发送数据: + 总字节数: ${sentDataCount} 字节 + 指令数: ${sentDataCount / 10} 条 (假设每条10字节) + + 📈 实时统计: + 数据速率: ${String.format("%.1f", dataRate)} 字节/秒 + 最后数据: ${if (lastDataTime > 0) java.text.SimpleDateFormat("HH:mm:ss.SSS", java.util.Locale.getDefault()).format(java.util.Date(lastDataTime)) else "无"} + + 🔄 连接状态: + 蓝牙连接: ${if (bluetoothConnected) "已连接" else "未连接"} + 数据处理: ${if (dataProcessingStarted) "已启动" else "未启动"} + 数据监控: ${if (isDataMonitoringEnabled) "已启用" else "已禁用"} + """.trimIndent() + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("📊 数据统计") + .setMessage(stats) + .setPositiveButton("复制统计", { _, _ -> + copyToClipboard(stats) + updateStatus("数据统计已复制到剪贴板") + }) + .setNegativeButton("关闭", null) + .show() + } + + /** + * 重置数据统计 + */ + private fun resetDataStatistics() { + receivedDataCount = 0 + sentDataCount = 0 + lastDataTime = 0L + dataRate = 0f + synchronized(dataLogBuffer) { dataLogBuffer.clear() } + + updateStatus("数据统计已重置") + updateDataStatistics() + } + + /** + * 显示测试数据对话框 + */ + private fun showTestDataDialog() { + val options = arrayOf( + "发送ECG测试数据", + "发送心跳包", + "发送设备信息查询", + "发送自定义测试数据" + ) + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("测试数据发送") + .setItems(options) { _, which -> + when (which) { + 0 -> sendECGTestData() + 1 -> sendHeartbeatData() + 2 -> sendDeviceInfoQuery() + 3 -> sendCustomTestData() + } + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 发送ECG测试数据 + */ + private fun sendECGTestData() { + // 生成模拟ECG数据包 + val ecgData = generateECGTestPacket() + bluetoothManager.sendCommand(ecgData) + updateStatus("已发送ECG测试数据包") + } + + /** + * 发送心跳包 + */ + private fun sendHeartbeatData() { + val heartbeat = byteArrayOf(0x01, 0x02, 0x03, 0x04, 0x05) + bluetoothManager.sendCommand(heartbeat) + updateStatus("已发送心跳包") + } + + /** + * 发送设备信息查询 + */ + private fun sendDeviceInfoQuery() { + val deviceInfoQuery = "DEVICE_INFO".toByteArray() + bluetoothManager.sendCommand(deviceInfoQuery) + updateStatus("已发送设备信息查询") + } + + /** + * 发送自定义测试数据 + */ + private fun sendCustomTestData() { + val input = android.widget.EditText(this) + input.hint = "输入测试数据(十六进制,如:01 02 03 04)" + input.setText("01 02 03 04 05") + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("自定义测试数据") + .setView(input) + .setPositiveButton("发送") { _, _ -> + val dataString = input.text.toString().trim() + if (dataString.isNotEmpty()) { + try { + val data = dataString.split(" ").map { it.toInt(16).toByte() }.toByteArray() + bluetoothManager.sendCommand(data) + updateStatus("已发送自定义测试数据: $dataString") + } catch (e: Exception) { + updateStatus("数据格式错误: ${e.message}") + } + } + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 生成ECG测试数据包 + */ + private fun generateECGTestPacket(): ByteArray { + // 模拟ECG数据包格式 + val packet = mutableListOf() + + // 包头 + packet.add(0xAA.toByte()) // 同步字节1 + packet.add(0x55.toByte()) // 同步字节2 + packet.add(0x01.toByte()) // 数据类型:ECG + + // 数据长度(100个样本点) + val dataLength = 100 + packet.add((dataLength and 0xFF).toByte()) // 低字节 + packet.add(((dataLength shr 8) and 0xFF).toByte()) // 高字节 + + // 生成ECG数据 + for (i in 0 until dataLength) { + val t = i.toFloat() / 10f + val ecgValue = (Math.sin(t * 2 * Math.PI) * 100 + + Math.sin(t * 4 * Math.PI) * 50 + + Math.sin(t * 8 * Math.PI) * 25).toInt() + packet.add((ecgValue and 0xFF).toByte()) // 低字节 + packet.add(((ecgValue shr 8) and 0xFF).toByte()) // 高字节 + } + + // 校验和 + var checksum = 0 + for (i in 2 until packet.size) { + checksum += packet[i].toInt() and 0xFF + } + packet.add((checksum and 0xFF).toByte()) + + return packet.toByteArray() + } + + /** + * 显示数据测试对话框 + */ + private fun showDataTestDialog() { + val testOptions = arrayOf( + "📤 发送测试指令", + "🔧 轻迅协议命令", + "📥 监控数据接收", + "📊 查看数据统计", + "🔄 实时数据流测试", + "📋 查看数据日志", + "🧪 生成模拟数据" + ) + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("📡 数据来往测试") + .setItems(testOptions) { _, which -> + when (which) { + 0 -> sendTestCommands() + 1 -> showQingXunProtocolMenu() + 2 -> monitorDataReception() + 3 -> showDataStatisticsDialog() + 4 -> startRealTimeDataTest() + 5 -> showDataLogDialog() + 6 -> generateImmediateTestData() + } + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 发送测试指令 + */ + private fun sendTestCommands() { + val commands = arrayOf( + "开始数据采集", + "查询设备状态", + "发送心跳包", + "开始ECG测量", + "发送自定义指令" + ) + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("📤 选择要发送的指令") + .setItems(commands) { _, which -> + when (which) { + 0 -> { + bluetoothManager.sendStringCommand("START_DATA") + updateStatus("📤 已发送: 开始数据采集指令") + startDataMonitoring() + } + 1 -> { + bluetoothManager.sendStringCommand("DEVICE_STATUS") + updateStatus("📤 已发送: 查询设备状态指令") + } + 2 -> { + val heartbeat = byteArrayOf(0x01, 0x02, 0x03, 0x04, 0x05) + bluetoothManager.sendCommand(heartbeat) + updateStatus("📤 已发送: 心跳包") + } + 3 -> { + bluetoothManager.sendStringCommand("START_ECG") + updateStatus("📤 已发送: 开始ECG测量指令") + startDataMonitoring() + } + 4 -> { + showCustomCommandInput() + } + } + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 显示自定义指令输入对话框 + */ + private fun showCustomCommandInput() { + val input = android.widget.EditText(this) + input.hint = "输入指令(如:START, STOP, QUERY等)" + input.setText("START") + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("📤 自定义指令") + .setView(input) + .setPositiveButton("发送") { _, _ -> + val command = input.text.toString().trim() + if (command.isNotEmpty()) { + bluetoothManager.sendStringCommand(command) + updateStatus("📤 已发送自定义指令: $command") + startDataMonitoring() + } + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 监控数据接收 + */ + private fun monitorDataReception() { + val monitoringOptions = arrayOf( + "开始监控数据接收", + "停止监控", + "查看当前接收状态", + "清空接收统计" + ) + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("📥 数据接收监控") + .setItems(monitoringOptions) { _, which -> + when (which) { + 0 -> { + isDataMonitoringEnabled = true + updateStatus("📥 数据接收监控已开启") + startDataMonitoring() + } + 1 -> { + isDataMonitoringEnabled = false + updateStatus("📥 数据接收监控已停止") + } + 2 -> { + showCurrentReceptionStatus() + } + 3 -> { + receivedDataCount = 0 + sentDataCount = 0 + lastDataTime = 0L + dataRate = 0f + synchronized(dataLogBuffer) { dataLogBuffer.clear() } + updateStatus("📥 数据统计已清空") + } + } + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 显示当前接收状态 + */ + private fun showCurrentReceptionStatus() { + val status = """ + 📊 当前数据接收状态: + + 📥 接收数据: + 总字节数: ${receivedDataCount} 字节 + 数据包数: ${if (receivedDataCount > 0) receivedDataCount / 20 else 0} 包 + + 📤 发送数据: + 总字节数: ${sentDataCount} 字节 + 指令数: ${if (sentDataCount > 0) sentDataCount / 10 else 0} 条 + + 📈 实时统计: + 数据速率: ${String.format("%.1f", dataRate)} 字节/秒 + 最后数据时间: ${if (lastDataTime > 0) java.text.SimpleDateFormat("HH:mm:ss.SSS", java.util.Locale.getDefault()).format(java.util.Date(lastDataTime)) else "无"} + + 🔄 监控状态: + 数据监控: ${if (isDataMonitoringEnabled) "已启用" else "已禁用"} + 蓝牙连接: ${if (bluetoothConnected) "已连接" else "未连接"} + """.trimIndent() + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("📊 数据接收状态") + .setMessage(status) + .setPositiveButton("复制状态", { _, _ -> + copyToClipboard(status) + updateStatus("数据状态已复制到剪贴板") + }) + .setNegativeButton("关闭", null) + .show() + } + + /** + * 开始实时数据流测试 + */ + private fun startRealTimeDataTest() { + val testOptions = arrayOf( + "开始连续数据发送测试", + "开始数据接收监听", + "模拟数据流", + "停止测试" + ) + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("🔄 实时数据流测试") + .setItems(testOptions) { _, which -> + when (which) { + 0 -> startContinuousDataTest() + 1 -> startDataReceptionListener() + 2 -> simulateDataStream() + 3 -> stopDataTest() + } + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 开始连续数据发送测试 + */ + private fun startContinuousDataTest() { + updateStatus("🔄 开始连续数据发送测试...") + + // 启动后台线程进行连续发送 + Thread { + var testCount = 0 + while (isDataMonitoringEnabled && bluetoothConnected) { + try { + val testData = "TEST_DATA_${testCount++}".toByteArray() + bluetoothManager.sendCommand(testData) + + runOnUiThread { + updateStatus("📤 发送测试数据 #$testCount: ${testData.size} 字节") + } + + Thread.sleep(1000) // 每秒发送一次 + } catch (e: Exception) { + runOnUiThread { + updateStatus("❌ 连续发送测试失败: ${e.message}") + } + break + } + } + }.start() + } + + /** + * 开始数据接收监听 + */ + private fun startDataReceptionListener() { + updateStatus("📥 开始数据接收监听...") + isDataMonitoringEnabled = true + + // 启动后台线程监听数据接收 + Thread { + var lastReceivedCount = receivedDataCount + while (isDataMonitoringEnabled) { + try { + if (receivedDataCount > lastReceivedCount) { + val newData = receivedDataCount - lastReceivedCount + runOnUiThread { + updateStatus("📥 检测到新数据: +$newData 字节,总计: $receivedDataCount 字节") + } + lastReceivedCount = receivedDataCount + } + Thread.sleep(500) // 每500ms检查一次 + } catch (e: Exception) { + break + } + } + }.start() + } + + /** + * 模拟数据流 + */ + private fun simulateDataStream() { + updateStatus("🔄 开始模拟数据流...") + + Thread { + var simCount = 0 + while (isDataMonitoringEnabled) { + try { + // 模拟接收数据 + val simulatedData = generateSimulatedData(simCount++) + + // 更新接收统计 + receivedDataCount += simulatedData.size + val currentTime = System.currentTimeMillis() + if (lastDataTime > 0) { + val timeDiff = currentTime - lastDataTime + if (timeDiff > 0) { + dataRate = (simulatedData.size * 1000f) / timeDiff + } + } + lastDataTime = currentTime + + runOnUiThread { + updateStatus("📥 模拟数据流: 接收 ${simulatedData.size} 字节,总计: $receivedDataCount 字节") + updateDataStatistics() + } + + Thread.sleep(800) // 每800ms模拟一次数据接收 + } catch (e: Exception) { + break + } + } + }.start() + } + + /** + * 生成模拟数据 + */ + private fun generateSimulatedData(count: Int): ByteArray { + val data = mutableListOf() + + // 添加时间戳 (8字节) + val timestamp = System.currentTimeMillis() + for (i in 0..7) { + data.add(((timestamp shr (i * 8)) and 0xFF).toByte()) + } + + // 添加计数器 (4字节) + for (i in 0..3) { + data.add(((count shr (i * 8)) and 0xFF).toByte()) + } + + // 添加模拟传感器数据 + for (i in 0 until 10) { + val sensorValue = (Math.sin(count * 0.1 + i) * 100).toInt() + for (j in 0..3) { + data.add(((sensorValue shr (j * 8)) and 0xFF).toByte()) + } + } + + return data.toByteArray() + } + + /** + * 停止数据测试 + */ + private fun stopDataTest() { + isDataMonitoringEnabled = false + updateStatus("🔄 数据测试已停止") + } + + /** + * 开始数据监控 + */ + private fun startDataMonitoring() { + if (!isDataMonitoringEnabled) { + isDataMonitoringEnabled = true + updateStatus("📊 数据监控已启动") + } + } + + override fun onDeviceInfoReceived(deviceInfo: com.example.cmake_project_test.BluetoothManager.DeviceInfo) { + runOnUiThread { + updateStatus("📋 设备信息: ${deviceInfo.manufacturer} ${deviceInfo.model}") + } + } + + override fun onBatteryLevelReceived(batteryLevel: Int) { + runOnUiThread { + updateStatus("🔋 电量: $batteryLevel%") + } + } + + override fun onCollectionStatusChanged(isCollecting: Boolean) { + runOnUiThread { + updateStatus("📊 采集状态: ${if (isCollecting) "开启" else "关闭"}") + } + } + + override fun onProtocolDataReceived(functionCode: Int, data: ByteArray) { + val currentTime = System.currentTimeMillis() + + // 限制协议数据日志频率 + if (currentTime - lastDataLogTime >= 1000) { + lastDataLogTime = currentTime + Log.d("MainActivity", "收到协议数据: 功能码=0x${functionCode.toString(16).uppercase()}, 数据长度=${data.size}") + } + + runOnUiThread { + updateStatus("📡 协议数据: 功能码=0x${functionCode.toString(16).uppercase()}, 数据=${data.size}字节") + } } } \ No newline at end of file diff --git a/app/src/main/java/com/example/cmake_project_test/StreamingSignalProcessor.kt b/app/src/main/java/com/example/cmake_project_test/StreamingSignalProcessor.kt index 5295ee8..7bc4c72 100644 --- a/app/src/main/java/com/example/cmake_project_test/StreamingSignalProcessor.kt +++ b/app/src/main/java/com/example/cmake_project_test/StreamingSignalProcessor.kt @@ -105,8 +105,20 @@ class StreamingSignalProcessor { // 当缓冲区有足够数据时进行窗口处理 while (dataBuffer.size >= windowSize) { - // 提取当前窗口数据 - val windowData = dataBuffer.take(windowSize).toFloatArray() + // 提取当前窗口数据,过滤掉null值 + val windowDataList = dataBuffer.take(windowSize).filterNotNull() + if (windowDataList.size < windowSize) { + Log.w("StreamingSignalProcessor", "窗口数据包含null值,跳过处理") + // 移除无效数据 + repeat(stepSize) { + if (dataBuffer.isNotEmpty()) { + dataBuffer.removeAt(0) + } + } + continue + } + + val windowData = windowDataList.toFloatArray() Log.d("StreamingSignalProcessor", "提取窗口数据,长度: ${windowData.size}") // 应用信号处理 diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 15d81fb..da9892e 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -54,12 +54,12 @@ android:orientation="horizontal">