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">