diff --git a/.vscode/settings.json b/.vscode/settings.json index c5f3f6b..4ea45b0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "java.configuration.updateBuildConfiguration": "interactive" + "java.configuration.updateBuildConfiguration": "interactive", + "idf.pythonInstallPath": "C:\\Espressif\\tools\\idf-python\\3.11.2\\python.exe" } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 37e9051..de35cd0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,15 +6,22 @@ - + + + + + + + + diff --git a/app/src/main/java/com/example/cmake_project_test/BluetoothManager.kt b/app/src/main/java/com/example/cmake_project_test/BluetoothManager.kt index 4a6ee41..85f856e 100644 --- a/app/src/main/java/com/example/cmake_project_test/BluetoothManager.kt +++ b/app/src/main/java/com/example/cmake_project_test/BluetoothManager.kt @@ -75,22 +75,37 @@ class BluetoothManager(private val context: Context) { const val RESPONSE_TIMEOUT = 0x05 // 超时 } - // 常见的ECG设备UUID列表 + // 常见的ECG设备UUID列表(已更新包含ESP32S3 SPP服务) private val SERVICE_UUIDS = listOf( - UUID.fromString("6e400001-b5a3-f393-e0a9-68716563686f"), // Nordic UART Service (NUS) - 你的设备 + UUID.fromString("0000abf0-0000-1000-8000-00805f9b34fb"), // // ESP32S3 SPP Service UUID - 0xABF0 + UUID.fromString("6e400001-b5a3-f393-e0a9-68716563686f"), // Nordic UART Service (NUS) - 轻迅设备 + UUID.fromString("1BD790EC-E8B9-7580-0A46-44D30102EDA6"), // 新设备服务UUID ) private val CHARACTERISTIC_UUIDS = listOf( - UUID.fromString("6e400002-b5a3-f393-e0a9-68716563686f"), // Nordic UART TX Characteristic - 发送数据 + UUID.fromString("0000abf1-0000-1000-8000-00805f9b34fb"), // ESP32S3 SPP Data Receive - 0xABF1 (TX) + UUID.fromString("0000abf3-0000-1000-8000-00805f9b34fb"), // ESP32S3 SPP Command Receive - 0xABF3 (TX) + UUID.fromString("6e400002-b5a3-f393-e0a9-68716563686f"), // Nordic UART TX Characteristic - 轻迅设备发送 + UUID.fromString("1BD790EC-E8B9-7580-0A46-44D30202EDA6"), // 新设备写入特征值 ) - // Notify特征UUID列表 + // Notify特征UUID列表(已更新包含ESP32S3 SPP服务) private val NOTIFY_CHARACTERISTIC_UUIDS = listOf( - UUID.fromString("6e400003-b5a3-f393-e0a9-68716563686f"), // Nordic UART RX Characteristic - 你的设备 + UUID.fromString("0000abf2-0000-1000-8000-00805f9b34fb"), // ESP32S3 SPP Data Notify - 0xABF2 (RX) + UUID.fromString("0000abf4-0000-1000-8000-00805f9b34fb"), // ESP32S3 SPP Command Notify - 0xABF4 (RX) + UUID.fromString("6e400003-b5a3-f393-e0a9-68716563686f"), // Nordic UART RX Characteristic - 轻迅设备接收 + UUID.fromString("1BD790EC-E8B9-7580-0A46-44D30302EDA6"), // 新设备读取特征值 ) // 设备名称前缀 (根据你的设备调整) - private const val DEVICE_NAME_PREFIX = "ECG" + private const val DEVICE_NAME_PREFIX = "ESP_SPP_SERVER" + + // 设备类型枚举 + enum class DeviceType { + ESP32S3_DEVICE, // ESP32S3 SPP设备 + QINGXUN, // 轻迅设备 + NEW_DEVICE // 新设备 + } } private var bluetoothManagerService: android.bluetooth.BluetoothManager? = null @@ -118,6 +133,9 @@ class BluetoothManager(private val context: Context) { private var currentCharacteristic: BluetoothGattCharacteristic? = null private var currentNotifyCharacteristic: BluetoothGattCharacteristic? = null + // 当前设备类型 + private var currentDeviceType: DeviceType? = null + // 协议状态 private var deviceInfo: DeviceInfo? = null private var isCollecting = false @@ -207,6 +225,13 @@ class BluetoothManager(private val context: Context) { this.callback = callback } + /** + * 获取当前设备类型 + */ + fun getCurrentDeviceType(): DeviceType? { + return currentDeviceType + } + /** * 扫描蓝牙设备 */ @@ -257,14 +282,45 @@ class BluetoothManager(private val context: Context) { callback?.onStatusChanged("✅ 权限检查通过,开始扫描...") - // 优先使用BLE扫描 - if (bluetoothLeScanner != null) { - callback?.onStatusChanged("📡 使用BLE扫描模式") - startBleScan() - } else { - // 回退到传统蓝牙扫描 - callback?.onStatusChanged("📡 使用传统蓝牙扫描模式") - startClassicScan() + // 改进的兼容性扫描策略 + when { + // Android 8/8.1: 使用混合扫描模式以获得最佳兼容性 + android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.O || + android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.O_MR1 -> { + Log.d(TAG, "📱 Android 8/8.1检测:使用混合扫描模式(BLE + 传统蓝牙)") + callback?.onStatusChanged("📱 Android 8系统优化:混合扫描模式") + callback?.onStatusChanged("🔍 策略:BLE扫描 + 传统蓝牙扫描") + startEnhancedScanning() + } + // Android 6-11: 优先BLE,失败时回退到传统蓝牙 + android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M && + android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.S -> { + if (bluetoothLeScanner != null) { + Log.d(TAG, "📱 Android ${android.os.Build.VERSION.RELEASE}:优先BLE扫描") + callback?.onStatusChanged("📡 使用BLE扫描模式") + callback?.onStatusChanged("🔍 开始BLE扫描...") + startBleScan() + } else { + Log.w(TAG, "⚠️ BLE不可用,使用传统蓝牙扫描") + callback?.onStatusChanged("📡 BLE不可用,使用传统蓝牙扫描") + callback?.onStatusChanged("🔍 开始传统蓝牙扫描...") + startClassicScan() + } + } + // Android 12+: 完整的BLE扫描支持 + else -> { + if (bluetoothLeScanner != null) { + Log.d(TAG, "✅ Android ${android.os.Build.VERSION.RELEASE}:完整的BLE支持") + callback?.onStatusChanged("📡 完整BLE扫描模式") + callback?.onStatusChanged("🔍 开始BLE扫描...") + startBleScan() + } else { + Log.w(TAG, "⚠️ 意外情况:BLE不可用,使用传统蓝牙") + callback?.onStatusChanged("📡 BLE不可用,使用传统蓝牙扫描") + callback?.onStatusChanged("🔍 开始传统蓝牙扫描...") + startClassicScan() + } + } } } catch (e: Exception) { @@ -284,9 +340,19 @@ class BluetoothManager(private val context: Context) { .setReportDelay(0) // 立即报告结果 .build() + // 不使用过滤器,扫描所有BLE设备,确保不会遗漏新设备 + Log.d(TAG, "🚀 开始BLE扫描,无过滤器") bluetoothLeScanner?.startScan(null, scanSettings, bleScanCallback) - Log.d(TAG, "BLE扫描已开始") - callback?.onStatusChanged("📡 BLE扫描进行中...") + Log.d(TAG, "✅ BLE扫描已启动成功") + + callback?.onStatusChanged("📡 BLE扫描已启动") + callback?.onStatusChanged("🔍 扫描模式:所有BLE设备") + callback?.onStatusChanged("⏱️ 扫描时长:15秒") + callback?.onStatusChanged("💡 发现设备实时显示") + callback?.onStatusChanged("🎯 目标服务UUID:") + for (serviceUuid in SERVICE_UUIDS) { + callback?.onStatusChanged(" • $serviceUuid") + } // 取消之前的延迟任务(如果存在) bleScanStopRunnable?.let { mainHandler.removeCallbacks(it) } @@ -314,7 +380,15 @@ class BluetoothManager(private val context: Context) { Log.e(TAG, "BLE扫描失败: ${e.message}") callback?.onError("❌ BLE扫描失败: ${e.message}") callback?.onStatusChanged("💡 尝试使用传统蓝牙扫描...") - startClassicScan() + + // 延迟1秒后启动传统蓝牙扫描,避免同时启动太多扫描 + android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ + try { + startClassicScan() + } catch (fallbackE: Exception) { + callback?.onError("❌ 传统蓝牙扫描也失败: ${fallbackE.message}") + } + }, 1000) } } @@ -323,31 +397,166 @@ class BluetoothManager(private val context: Context) { */ private fun startClassicScan() { try { - registerClassicScanReceiver() - bluetoothAdapter?.startDiscovery() - Log.d(TAG, "传统蓝牙扫描已开始") - callback?.onStatusChanged("📡 传统蓝牙扫描进行中...") + Log.d(TAG, "🚀 开始传统蓝牙扫描") + callback?.onStatusChanged("📡 开始传统蓝牙扫描...") - // 15秒后停止扫描 + // Android 8.1特殊状态检查 + if (android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.O_MR1) { + Log.d(TAG, "📱 Android 8.1状态检查:") + callback?.onStatusChanged("📱 Android 8.1系统检测...") + + // 检查蓝牙适配器状态 + val adapterState = bluetoothAdapter?.state ?: BluetoothAdapter.STATE_OFF + Log.d(TAG, "📱 蓝牙适配器状态: ${adapterState}") + callback?.onStatusChanged("📱 蓝牙适配器状态: ${when(adapterState) { + BluetoothAdapter.STATE_ON -> "已开启" + BluetoothAdapter.STATE_OFF -> "已关闭" + BluetoothAdapter.STATE_TURNING_ON -> "正在开启" + BluetoothAdapter.STATE_TURNING_OFF -> "正在关闭" + else -> "未知($adapterState)" + }}") + + // 检查是否正在探索 + val isDiscovering = bluetoothAdapter?.isDiscovering ?: false + Log.d(TAG, "📱 当前探索状态: $isDiscovering") + callback?.onStatusChanged("📱 当前探索状态: ${if(isDiscovering) "正在探索" else "未在探索"}") + + if (isDiscovering) { + Log.d(TAG, "📱 停止当前探索并重新开始") + bluetoothAdapter?.cancelDiscovery() + Thread.sleep(500) // 等待500ms + } + } + + registerClassicScanReceiver() + val discoveryStarted = bluetoothAdapter?.startDiscovery() ?: false + + if (discoveryStarted) { + Log.d(TAG, "✅ 传统蓝牙扫描启动成功") + callback?.onStatusChanged("✅ 传统蓝牙扫描已启动") + callback?.onStatusChanged("🔍 扫描时长:25秒") + callback?.onStatusChanged("💡 请确保目标设备设置为可发现模式") + callback?.onStatusChanged("🎯 搜索经典蓝牙和音频设备") + + // Android 8.1额外提示 + if (android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.O_MR1) { + callback?.onStatusChanged("📱 Android 8.1提示:扫描传统蓝牙设备") + callback?.onStatusChanged("💡 如果找不到设备,请检查设备的配对模式") + } + } else { + Log.w(TAG, "⚠️ 传统蓝牙扫描启动失败") + callback?.onStatusChanged("❌ 传统蓝牙扫描启动失败") + + if (android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.O_MR1) { + callback?.onStatusChanged("💡 Android 8.1排除步骤:") + callback?.onStatusChanged(" • 检查蓝牙是否已开启") + callback?.onStatusChanged(" • 尝试重启蓝牙") + callback?.onStatusChanged(" • 检查是否有其他应用在扫描") + } + } + + // 25秒后停止扫描(增强扫描模式使用更长的时间) android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ + Log.d(TAG, "⏰ 传统蓝牙扫描超时,自动停止") stopClassicScan() - }, 15000) + }, 25000) } catch (e: Exception) { - Log.e(TAG, "传统蓝牙扫描失败: ${e.message}") + Log.e(TAG, "传统蓝牙扫描异常: ${e.message}", e) callback?.onError("❌ 传统蓝牙扫描失败: ${e.message}") - callback?.onStatusChanged("💡 请检查蓝牙设置或重启设备") + callback?.onStatusChanged("💡 建议:") + callback?.onStatusChanged(" • 检查蓝牙是否开启") + callback?.onStatusChanged(" • 尝试重启蓝牙服务") + callback?.onStatusChanged(" • 重启设备") } } + /** + * 增强扫描模式:同时使用BLE和传统蓝牙扫描(适用于Android 8) + */ + private fun startEnhancedScanning() { + try { + Log.d(TAG, "🚀 开始增强扫描模式") + callback?.onStatusChanged("🚀 启动混合扫描模式...") + callback?.onStatusChanged("💡 同时使用BLE和传统音频蓝牙扫描") + + // 并行启动两种扫描 + var startedCount = 0 + val errors = mutableListOf() + + // 启动BLE扫描 + try { + bluetoothLeScanner?.let { + Log.d(TAG, "📡 启动BLE扫描...") + callback?.onStatusChanged("📡 启动BLE扫描...") + startedCount++ + startBleScan() + } ?: run { + errors.add("BLE扫描器不可用") + } + } catch (e: Exception) { + errors.add("BLE扫描启动失败: ${e.message}") + Log.w(TAG, "BLE扫描启动失败,继续使用传统蓝牙: ${e.message}") + } + + // 启动传统蓝牙扫描 + try { + Log.d(TAG, "🔗 启动传统蓝牙扫描...") + callback?.onStatusChanged("🔗 启动传统蓝牙扫描...") + startedCount++ + startClassicScan() + } catch (e: Exception) { + errors.add("传统蓝牙扫描启动失败: ${e.message}") + Log.w(TAG, "传统蓝牙扫描启动失败: ${e.message}") + } + + // 报告结果 + if (startedCount > 0) { + Log.d(TAG, "✅ 增强扫描模式启动成功:$startedCount 个扫描器") + callback?.onStatusChanged("✅ 混合扫描启动成功") + callback?.onStatusChanged("📊 活跃扫描器:$startedCount 个") + callback?.onStatusChanged("⏱️ 扫描时长:25秒") + callback?.onStatusChanged("💡 正在搜索所有类型的蓝牙设备...") + + if (errors.isNotEmpty()) { + callback?.onStatusChanged("⚠️ 部分扫描器启动失败:") + errors.forEach { error -> + callback?.onStatusChanged(" • $error") + } + } + } else { + callback?.onStatusChanged("❌ 增强扫描模式启动失败") + errors.forEach { error -> + callback?.onStatusChanged(" • $error") + } + throw Exception("所有扫描器启动失败") + } + + } catch (e: Exception) { + Log.e(TAG, "增强扫描模式失败: ${e.message}", e) + callback?.onError("❌ 混合扫描失败: ${e.message}") + callback?.onStatusChanged("💡 尝试单独启动扫描...") + + // 回退到传统蓝牙扫描 + try { + startClassicScan() + callback?.onStatusChanged("✅ 已回退到传统蓝牙扫描") + } catch (fallbackE: Exception) { + callback?.onError("❌ 回退扫描也失败了: ${fallbackE.message}") + } + } + } + /** * 停止扫描 */ fun stopScan() { try { + Log.d(TAG, "🛑 开始停止所有扫描") stopBleScan() stopClassicScan() callback?.onStatusChanged("扫描已停止") + Log.d(TAG, "✅ 所有扫描已停止") } catch (e: Exception) { Log.e(TAG, "停止扫描失败: ${e.message}") } @@ -373,21 +582,32 @@ class BluetoothManager(private val context: Context) { bluetoothLeScanner?.stopScan(bleScanCallback) Log.d(TAG, "BLE扫描已停止") - // 扫描完成后立即触发回调 - callback?.onStatusChanged("🔍 BLE扫描完成") - callback?.onStatusChanged("📊 扫描结果:找到 ${discoveredDevices.size} 个设备") + // 扫描完成后触发回调(仅在非混合模式) + val isMixedScan = android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.O || + android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.O_MR1 - if (discoveredDevices.isEmpty()) { - callback?.onStatusChanged("⚠️ 未发现任何设备") - callback?.onStatusChanged("💡 建议:") - callback?.onStatusChanged(" • 检查目标设备是否开启蓝牙") - callback?.onStatusChanged(" • 确保设备在可见范围内") - callback?.onStatusChanged(" • 尝试重启蓝牙服务") + callback?.onStatusChanged("🔍 BLE扫描完成") + val bleDeviceCount = discoveredDevices.count { it.type == BluetoothDevice.DEVICE_TYPE_LE } + callback?.onStatusChanged("📊 BLE扫描结果:找到 $bleDeviceCount 个BLE设备") + + if (bleDeviceCount == 0) { + callback?.onStatusChanged("⚠️ 未发现BLE设备") + if (!isMixedScan) { + callback?.onStatusChanged("💡 建议:") + callback?.onStatusChanged(" • 检查目标设备是否开启蓝牙") + callback?.onStatusChanged(" • 确保设备在可见范围内") + callback?.onStatusChanged(" • 尝试重启蓝牙服务") + } } else { - callback?.onStatusChanged("✅ 扫描成功,请选择要连接的设备") + callback?.onStatusChanged("✅ BLE扫描成功") } - callback?.onScanComplete(discoveredDevices.toList()) + // 仅在非混合模式下触发完成回调 + if (!isMixedScan) { + callback?.onScanComplete(discoveredDevices.toList()) + } else { + callback?.onStatusChanged("⏳ BLE扫描完成,传统蓝牙扫描继续进行...") + } } catch (e: Exception) { Log.e(TAG, "停止BLE扫描失败: ${e.message}") // 如果是FragmentManager相关错误,记录但不抛出异常 @@ -871,30 +1091,184 @@ class BluetoothManager(private val context: Context) { } /** - * 电刺激开关 + * 高级电刺激控制 * 功能码: 0x0003 - * 数据格式: [开关及类型(1字节)] - * 数据长度字段表示整个包的长度(7字节) + * 数据格式: [开关及类型(1字节)] + [强度(1字节)] + [频率(2字节)] + [总持续时间(2字节)] + [休息时间(2字节)] + [静默时间(2字节)] + [缓进时间(1字节)] + [保持时间(1字节)] + [缓出时间(1字节)] + * 数据长度:15字节 */ - fun stimSwitch(stimType: Int, enable: Boolean) { + fun stimSwitch( + enable: Boolean, // 开关状态:true=开启,false=关闭 + stimType: Int = 0, // 电刺激类型:0x00=关闭,0x10~0x1F=开启+类型 + intensity: Int = 50, // 强度值:1-100 + frequency: Int = 100, // 频率值:Hz + totalDuration: Int = 5000, // 总持续时间:毫秒 + restTime: Int = 1000, // 休息时间:毫秒 + silenceTime: Int = 500, // 静默时间:毫秒 + fadeInTime: Int = 1, // 缓进时间:秒 + holdTime: Int = 3, // 保持时间:秒 + fadeOutTime: Int = 1 // 缓出时间:秒 + ) { try { - val data = ByteArray(1) - // 开关及类型控制 - // 高4位:开关控制(0000=关闭,0001=开启) - // 低4位:刺激类型(0000~1111,即0-15) - val switchBits = if (enable) 0x10 else 0x00 // 高4位:0001或0000 - val typeBits = stimType and 0x0F // 低4位:确保在0-15范围内 - data[0] = (switchBits or typeBits).toByte() + // 根据表格:总包19字节 = 功能码(2) + 数据长度(2) + 数据内容(13) + CRC16(2) + val packet = ByteArray(19) + var index = 0 + + // 1. 功能码 (2字节) - 小端格式 + packet[index++] = (Protocol.FUNC_STIM_SWITCH and 0xFF).toByte() + packet[index++] = ((Protocol.FUNC_STIM_SWITCH shr 8) and 0xFF).toByte() + + // 2. 数据长度 (2字节) - 小端格式,数据内容13字节 + packet[index++] = 13.toByte() // 数据长度低字节 + packet[index++] = 0.toByte() // 数据长度高字节 + + // 3. 开关状态以及电刺激类型 (1字节) + val switchAndType = if (enable) { + val typeBits = stimType and 0x0F // 低4位:类型 + (0x10 or typeBits).toByte() // 0x10表示开启 + } else { + 0x00.toByte() // 0x00表示关闭 + } + packet[index++] = switchAndType + + // 4. 强度 (1字节) - 限制在0-100范围内 + packet[index++] = (intensity.coerceIn(0, 100)).toByte() + + // 5. 频率 (2字节) - 小端格式 + packet[index++] = (frequency and 0xFF).toByte() + packet[index++] = ((frequency shr 8) and 0xFF).toByte() + + // 6. 总持续时间 (2字节) - 小端格式 + packet[index++] = (totalDuration and 0xFF).toByte() + packet[index++] = ((totalDuration shr 8) and 0xFF).toByte() + + // 7. 休息时间 (2字节) - 小端格式 + packet[index++] = (restTime and 0xFF).toByte() + packet[index++] = ((restTime shr 8) and 0xFF).toByte() + + // 8. 静默时间 (2字节) - 小端格式 + packet[index++] = (silenceTime and 0xFF).toByte() + packet[index++] = ((silenceTime shr 8) and 0xFF).toByte() + + // 9. 缓进时间 (1字节) + packet[index++] = (fadeInTime.coerceIn(0, 255)).toByte() + + // 10. 保持时间 (1字节) + packet[index++] = (holdTime.coerceIn(0, 255)).toByte() + + // 11. 缓出时间 (1字节) + packet[index++] = (fadeOutTime.coerceIn(0, 255)).toByte() + + // 12. CRC16校验 (2字节) - 计算前面17字节的CRC + val crc = calculateCRC16(packet, 0, 17) + packet[index++] = (crc and 0xFF).toByte() + packet[index++] = ((crc shr 8) and 0xFF).toByte() + + // 详细日志记录 + Log.d(TAG, "发送高级电刺激控制指令 (19字节):") + Log.d(TAG, " 开关状态: ${if (enable) "开启" else "关闭"}") + Log.d(TAG, " 刺激类型: $stimType") + Log.d(TAG, " 强度: $intensity") + Log.d(TAG, " 频率: ${frequency}Hz") + Log.d(TAG, " 总持续时间: ${totalDuration}ms") + Log.d(TAG, " 休息时间: ${restTime}ms") + Log.d(TAG, " 静默时间: ${silenceTime}ms") + Log.d(TAG, " 缓进时间: ${fadeInTime}s") + Log.d(TAG, " 保持时间: ${holdTime}s") + Log.d(TAG, " 缓出时间: ${fadeOutTime}s") + Log.d(TAG, "数据包结构: 功能码(2) + 数据长度(2) + 数据内容(13) + CRC16(2) = 19字节") + Log.d(TAG, "数据包: ${packet.joinToString(", ") { "0x%02X".format(it) }}") - val packet = buildProtocolPacket(Protocol.FUNC_STIM_SWITCH, 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}") + Log.e(TAG, "构建高级电刺激控制指令失败: ${e.message}") + callback?.onCommandSent(false, "构建高级电刺激控制指令失败: ${e.message}") } } + + /** + * 简化版电刺激开关(为了保持向后兼容) + */ + fun stimSwitchSimple(stimType: Int, enable: Boolean) { + stimSwitch( + enable = enable, + stimType = stimType, + intensity = if (enable) 50 else 0, + frequency = if (enable) 100 else 0, + totalDuration = if (enable) 5000 else 0, + restTime = 1000, + silenceTime = 500, + fadeInTime = 1, + holdTime = 3, + fadeOutTime = 1 + ) + } + /** + * 电刺激预设:高强度短期刺激 + */ + fun stimHighIntensityShort( + stimType: Int = 1, + durationSeconds: Int = 30, + intensity: Int = 80 + ) { + stimSwitch( + enable = true, + stimType = stimType, + intensity = intensity, + frequency = 150, + totalDuration = durationSeconds * 1000, + restTime = 200, + silenceTime = 100, + fadeInTime = 2, + holdTime = durationSeconds - 4, + fadeOutTime = 2 + ) + } + + /** + * 电刺激预设:低强度长期刺激 + */ + fun stimLowIntensityLong( + stimType: Int = 1, + durationMinutes: Int = 15, + intensity: Int = 30 + ) { + stimSwitch( + enable = true, + stimType = stimType, + intensity = intensity, + frequency = 80, + totalDuration = durationMinutes * 60 * 1000, + restTime = 1000, + silenceTime = 500, + fadeInTime = 5, + holdTime = (durationMinutes * 60) - 10, + fadeOutTime = 5 + ) + } + + /** + * 电刺激预设:康复训练模式 + */ + fun stimRehabilitationMode( + stimType: Int = 2, + intensity: Int = 60 + ) { + stimSwitch( + enable = true, + stimType = stimType, + intensity = intensity, + frequency = 120, + totalDuration = 300000, // 5分钟 + restTime = 2000, // 2秒休息 + silenceTime = 1000, // 1秒静默 + fadeInTime = 3, + holdTime = 300, // 5分钟保持 + fadeOutTime = 3 + ) + } + /** * 同步时间戳 * 功能码: 0x0080 @@ -1174,30 +1548,38 @@ class BluetoothManager(private val context: Context) { var foundNotifyCharacteristic: BluetoothGattCharacteristic? = null // 首先尝试预设的UUID列表 - for (serviceUuid in SERVICE_UUIDS) { + for (i in SERVICE_UUIDS.indices) { + val serviceUuid = SERVICE_UUIDS[i] val service = gatt.getService(serviceUuid) if (service != null) { Log.d(TAG, "找到匹配的服务: $serviceUuid") foundService = service - // 查找TX特征(用于发送数据) - for (characteristicUuid in CHARACTERISTIC_UUIDS) { - val characteristic = service.getCharacteristic(characteristicUuid) - if (characteristic != null) { - Log.d(TAG, "找到TX特征: $characteristicUuid") - foundCharacteristic = characteristic - break - } + // 根据服务UUID确定设备类型 + currentDeviceType = when (serviceUuid.toString().uppercase()) { + "0000ABF0-0000-1000-8000-00805F9B34FB" -> DeviceType.ESP32S3_DEVICE + "1BD790EC-E8B9-7580-0A46-44D30102EDA6" -> DeviceType.NEW_DEVICE + "6E400001-B5A3-F393-E0A9-68716563686F" -> DeviceType.QINGXUN + else -> DeviceType.QINGXUN // 默认为轻迅设备 } - // 查找RX特征(用于接收数据) - for (notifyUuid in NOTIFY_CHARACTERISTIC_UUIDS) { - val notifyCharacteristic = service.getCharacteristic(notifyUuid) - if (notifyCharacteristic != null) { - Log.d(TAG, "找到RX特征: $notifyUuid") - foundNotifyCharacteristic = notifyCharacteristic - break - } + Log.d(TAG, "检测到设备类型: $currentDeviceType") + callback?.onStatusChanged("🔍 检测到设备类型: $currentDeviceType") + + // 查找对应的TX特征(用于发送数据) + val characteristicUuid = CHARACTERISTIC_UUIDS[i] + val characteristic = service.getCharacteristic(characteristicUuid) + if (characteristic != null) { + Log.d(TAG, "找到TX特征: $characteristicUuid") + foundCharacteristic = characteristic + } + + // 查找对应的RX特征(用于接收数据) + val notifyUuid = NOTIFY_CHARACTERISTIC_UUIDS[i] + val notifyCharacteristic = service.getCharacteristic(notifyUuid) + if (notifyCharacteristic != null) { + Log.d(TAG, "找到RX特征: $notifyUuid") + foundNotifyCharacteristic = notifyCharacteristic } break } @@ -1381,12 +1763,15 @@ class BluetoothManager(private val context: Context) { val deviceAddress = device.address val rssi = result.rssi - Log.d(TAG, "发现BLE设备: $deviceName ($deviceAddress) RSSI: $rssi") + Log.d(TAG, "🔍 发现BLE设备: $deviceName ($deviceAddress) RSSI: $rssi") + Log.d(TAG, "📊 设备详情: 名称=$deviceName, 地址=$deviceAddress, 信号=$rssi") // 显示详细的设备信息 callback?.onStatusChanged("📱 发现BLE设备: $deviceName") callback?.onStatusChanged(" 📍 地址: $deviceAddress") callback?.onStatusChanged(" 📶 信号强度: $rssi dBm") + callback?.onStatusChanged(" 🔧 设备类型: ${device.type}") + Log.d(TAG, "📱 设备类型: ${device.type}") // 检查是否是目标设备 if (deviceAddress.equals("A4:C3:37:86:9F:73", ignoreCase = true) || @@ -1414,11 +1799,32 @@ class BluetoothManager(private val context: Context) { } callback?.onError("❌ BLE扫描失败: $errorMessage") - callback?.onStatusChanged("💡 尝试使用传统蓝牙扫描...") - // 扫描失败时完成扫描 - callback?.onStatusChanged("扫描完成,找到 ${discoveredDevices.size} 个设备") - callback?.onScanComplete(discoveredDevices.toList()) + // 只在非混合模式下回退到传统蓝牙 + val isMixedScan = android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.O || + android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.O_MR1 + + if (!isMixedScan) { + callback?.onStatusChanged("💡 尝试使用传统蓝牙扫描...") + + // 延迟1秒后启动传统蓝牙扫描,避免同时启动太多扫描 + android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ + try { + startClassicScan() + } catch (fallbackE: Exception) { + callback?.onError("❌ 传统蓝牙扫描也失败: ${fallbackE.message}") + } + }, 1000) + } else { + callback?.onStatusChanged("⚠️ BLE扫描失败,但混合扫描模式将继续使用传统蓝牙") + } + + // 在混合扫描模式下,只记录错误但不结束扫描 + if (!isMixedScan) { + // 扫描失败时完成扫描 + callback?.onStatusChanged("扫描完成,找到 ${discoveredDevices.size} 个设备") + callback?.onScanComplete(discoveredDevices.toList()) + } } } @@ -1431,19 +1837,42 @@ class BluetoothManager(private val context: Context) { BluetoothDevice.ACTION_FOUND -> { val device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) device?.let { - Log.d(TAG, "发现传统蓝牙设备: ${it.name ?: "未知"} (${it.address})") + Log.d(TAG, "🔍 发现传统蓝牙设备: ${it.name ?: "未知"} (${it.address})") + Log.d(TAG, "📱 设备类型: ${it.type}") + Log.d(TAG, "📶 设备状态: ${it.bondState}") + + // Android 8.1特殊处理 + if (android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.O_MR1) { + Log.d(TAG, "📱 Android 8.1检测到传统蓝牙设备") + callback?.onStatusChanged("📱 Android 8.1发现设备: ${it.name ?: "未知设备"}") + } // 添加所有发现的设备(不进行过滤) addDiscoveredDevice(it) callback?.onDeviceFound(it) - } + } ?: Log.w(TAG, "⚠️ ACTION_FOUND广播中未找到设备信息") } BluetoothAdapter.ACTION_DISCOVERY_STARTED -> { Log.d(TAG, "传统蓝牙扫描开始") } BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> { Log.d(TAG, "传统蓝牙扫描完成") - callback?.onStatusChanged("扫描完成,找到 ${discoveredDevices.size} 个设备") + val classicDeviceCount = discoveredDevices.count { it.type != BluetoothDevice.DEVICE_TYPE_LE } + val totalDeviceCount = discoveredDevices.size + + callback?.onStatusChanged("🔍 传统蓝牙扫描完成") + callback?.onStatusChanged("📊 传统蓝牙扫描结果:找到 $classicDeviceCount 个经典蓝牙设备") + callback?.onStatusChanged("📊 总共发现 $totalDeviceCount 个设备") + + // Android 8.1混合扫描模式特殊处理 + val isMixedScan = android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.O || + android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.O_MR1 + + if (isMixedScan) { + callback?.onStatusChanged("✅ Android 8.1混合扫描完成") + callback?.onStatusChanged("📱 BLE + 传统蓝牙双重扫描完成") + } + callback?.onScanComplete(discoveredDevices.toList()) } } @@ -1460,15 +1889,34 @@ class BluetoothManager(private val context: Context) { addAction(BluetoothDevice.ACTION_FOUND) addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED) addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED) + // Android 8.1可能需要额外的广播动作 + addAction(BluetoothAdapter.ACTION_STATE_CHANGED) } - context.registerReceiver(classicScanReceiver, filter) + + // Android 8.1特殊处理:可能需要不同的注册方式 + if (android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.O_MR1) { + Log.d(TAG, "📱 Android 8.1特殊注册广播接收器") + context.registerReceiver(classicScanReceiver, filter) + Log.d(TAG, "✅ Android 8.1广播接收器注册成功") + } else { + context.registerReceiver(classicScanReceiver, filter) + Log.d(TAG, "✅ 传统蓝牙扫描广播接收器已注册") + } + isClassicScanReceiverRegistered = true Log.d(TAG, "传统蓝牙扫描广播接收器已注册") + + // Android 8.1特殊状态 + if (android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.O_MR1) { + Log.d(TAG, "📱 Android 8.1检测:扫描接收器已配置") + callback?.onStatusChanged("📱 Android 8.1检测:广播接收器已配置") + } } else { Log.d(TAG, "传统蓝牙扫描广播接收器已注册,跳过重复注册") } } catch (e: Exception) { Log.e(TAG, "注册广播接收器失败: ${e.message}") + callback?.onStatusChanged("❌ 广播接收器注册失败: ${e.message}") isClassicScanReceiverRegistered = false } } @@ -1514,10 +1962,19 @@ class BluetoothManager(private val context: Context) { 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("🎯 找到目标设备!") + // 检查是否是ESP32S3 SPP服务器或目标设备 + val isESP32S3Device = deviceName.contains("ESP_SPP_SERVER", ignoreCase = true) + val isLegacyTargetDevice = deviceAddress.equals("A4:C3:37:86:9F:73", ignoreCase = true) || + deviceAddress.equals("A4-C3-37-86-9F-73", ignoreCase = true) + + if (isESP32S3Device) { + callback?.onStatusChanged("🔌 ESP32S3 SPP服务器!") + callback?.onStatusChanged(" 📡 SPP服务:0xABF0") + callback?.onStatusChanged(" 📤 数据发送:0xABF1/0xABF3") + callback?.onStatusChanged(" 📥 数据接收:0xABF2/0xABF4") + callback?.onStatusChanged("💡 优先级设备,建议立即连接") + } else if (isLegacyTargetDevice) { + callback?.onStatusChanged("🎯 找到传统目标设备!") callback?.onStatusChanged("💡 建议:长按'连接蓝牙'按钮直接连接") } 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 507f212..8171d17 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 @@ -27,6 +27,13 @@ import android.net.Uri import android.widget.Toast import android.os.Handler import android.os.Looper +import android.widget.TextView +import android.widget.EditText +import android.widget.Spinner +import android.widget.ArrayAdapter +import androidx.appcompat.widget.SwitchCompat +import androidx.core.widget.NestedScrollView +import android.widget.LinearLayout class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.RealTimeDataCallback, BluetoothManager.BluetoothCallback { @@ -34,22 +41,27 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real private const val PERMISSION_REQUEST_CODE = 1001 // 根据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 - ) + private val REQUIRED_PERMISSIONS: Array = when { + 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 + ) + } + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> { + // Android 6-11 只需要位置权限 + arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + } + else -> { + // Android 5.1及以下不需要动态权限 + emptyArray() + } } // Used to load the 'cmake_project_test' library on application startup. @@ -117,7 +129,17 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real // 初始化UI val permissionStatus = checkPermissionStatus() - binding.sampleText.text = "应用已就绪,可以开始使用\n\n权限状态:\n$permissionStatus\n\n点击\"连接蓝牙\"按钮开始蓝牙连接..." + val androidVersion = android.os.Build.VERSION.RELEASE + val apiLevel = android.os.Build.VERSION.SDK_INT + + updateStatus("📱 Android $androidVersion (API $apiLevel) - 就绪") + updateStatus("✅ 蓝牙管理器已初始化") + updateStatus("📋 权限状态:") + for (line in permissionStatus.split("\n")) { + updateStatus(" $line") + } + updateStatus("🎯 准备连接蓝牙设备") + updateStatus("💡 点击'连接蓝牙'按钮开始扫描") // 确保图表容器可见 binding.dynamicChartContainer.visibility = View.VISIBLE @@ -177,39 +199,141 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real private var notchFilterEnabled = false private fun toggleBluetoothConnection() { - if (bluetoothConnected) { - // 断开连接 - bluetoothManager.disconnect() - bluetoothConnected = false - binding.bluetoothButton.text = "连接蓝牙" - binding.bluetoothButton.setBackgroundColor(Color.parseColor("#4CAF50")) - updateStatus("蓝牙已断开") - } else { - // 检查权限后再开始扫描 - if (checkAndRequestPermissions()) { - startBluetoothScan() + // 检查当前按钮状态 + val currentButtonText = binding.bluetoothButton.text.toString() + + when { + bluetoothConnected -> { + // 已连接,断开连接 + bluetoothManager.disconnect() + bluetoothConnected = false + binding.bluetoothButton.text = "连接蓝牙" + binding.bluetoothButton.setBackgroundColor(Color.parseColor("#4CAF50")) + updateStatus("蓝牙已断开") + } + currentButtonText == "扫描中..." -> { + // 正在扫描,停止扫描 + stopBluetoothScan() + } + currentButtonText == "扫描失败" || currentButtonText == "权限检查中..." -> { + // 重试扫描 + retryBluetoothScan() + } + else -> { + // 开始新扫描 + startNewBluetoothScan() } } } + + private fun startNewBluetoothScan() { + updateStatus("🚀 开始蓝牙连接流程...") + updateStatus("📱 检测到Android ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})") + + // Android 15特殊提示 + if (Build.VERSION.SDK_INT >= 35) { + updateStatus("🔔 Android 15检测:确保已授予所有必要权限") + } + + // 检查权限后再开始扫描 + when { + Build.VERSION.SDK_INT < Build.VERSION_CODES.M -> { + // Android 6.0以下不需要动态权限 + updateStatus("✅ Android ${Build.VERSION.RELEASE} 无需动态权限") + startBluetoothScan() + } + checkAndRequestPermissions() -> { + // 权限检查通过,开始扫描 + updateStatus("🎯 权限检查通过,正在启动蓝牙扫描...") + startBluetoothScan() + } + else -> { + // 权限检查未通过或正在请求权限 + updateStatus("⏳ 权限检查中,请稍候...") + updateStatus("💡 Android ${Build.VERSION.RELEASE} 权限指引:") + if (Build.VERSION.SDK_INT >= 35) { + updateStatus(" • 可能的权限:蓝牙扫描、蓝牙连接、位置") + updateStatus(" • 如果权限对话框没有出现,请手动进入应用设置→权限") + } else { + updateStatus(" • 需要的权限:蓝牙扫描、蓝牙连接、位置") + updateStatus(" • 请在弹出的权限对话框中点击 允许") + } + binding.bluetoothButton.text = "权限检查中..." + binding.bluetoothButton.setBackgroundColor(Color.parseColor("#FF9800")) + } + } + } + + private fun stopBluetoothScan() { + try { + bluetoothManager.stopScan() + binding.bluetoothButton.text = "重新扫描" + binding.bluetoothButton.setBackgroundColor(Color.parseColor("#4CAF50")) + updateStatus("⏹️ 蓝牙扫描已停止") + updateStatus("💡 点击'重新扫描'按钮重新开始扫描") + } catch (e: Exception) { + Log.e("MainActivity", "停止蓝牙扫描失败: ${e.message}", e) + updateStatus("❌ 停止扫描失败: ${e.message}") + } + } + + private fun retryBluetoothScan() { + binding.bluetoothButton.text = "重试扫描" + binding.bluetoothButton.setBackgroundColor(Color.parseColor("#FF9800")) + updateStatus("🔄 重试扫描...") + startNewBluetoothScan() + } private fun startBluetoothScan() { - bluetoothManager.startScan() - binding.bluetoothButton.text = "扫描中..." - binding.bluetoothButton.setBackgroundColor(Color.parseColor("#FF9800")) - updateStatus("正在扫描蓝牙设备...") + try { + updateStatus("🚀 准备启动蓝牙扫描...") + bluetoothManager.startScan() + binding.bluetoothButton.text = "扫描中..." + binding.bluetoothButton.setBackgroundColor(Color.parseColor("#FF9800")) + updateStatus("✅ 蓝牙扫描已启动") + updateStatus("⏱️ 扫描时长15秒,请稍候...") + + Log.d("MainActivity", "✅ 蓝牙扫描启动成功") + + // 15秒后如果还在扫描,显示提示 + android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ + if (binding.bluetoothButton.text == "扫描中...") { + updateStatus("💡 扫描时间较长,可能附近没有BLE设备") + updateStatus("🔄 可以尝试停止后重新扫描") + } + }, 15000) + + } catch (e: Exception) { + Log.e("MainActivity", "启动蓝牙扫描失败: ${e.message}", e) + updateStatus("❌ 蓝牙扫描启动失败: ${e.message}") + binding.bluetoothButton.text = "扫描失败" + binding.bluetoothButton.setBackgroundColor(Color.parseColor("#F44336")) + } } private fun checkAndRequestPermissions(): Boolean { val missingPermissions = mutableListOf() val grantedPermissions = mutableListOf() + Log.d("MainActivity", "=== 权限检查开始 ===") + Log.d("MainActivity", "Android版本: ${Build.VERSION.SDK_INT}") + Log.d("MainActivity", "需要检查的权限: ${REQUIRED_PERMISSIONS.contentToString()}") + updateStatus("🔍 检查蓝牙权限...") + updateStatus("📱 Android版本: ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})") for (permission in REQUIRED_PERMISSIONS) { - if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) { - grantedPermissions.add(permission.split(".").last()) + val permissionStatus = ContextCompat.checkSelfPermission(this, permission) + val permissionName = permission.split(".").last() + + Log.d("MainActivity", "权限 $permissionName 状态: $permissionStatus") + + if (permissionStatus == PackageManager.PERMISSION_GRANTED) { + grantedPermissions.add(permissionName) + updateStatus("✅ 已授权: $permissionName") } else { missingPermissions.add(permission) + updateStatus("❌ 缺少权限: $permissionName") } } @@ -219,19 +343,25 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real } if (missingPermissions.isNotEmpty()) { - val missingNames = missingPermissions.map { it.split(".").last() } - updateStatus("❌ 缺少权限: ${missingNames.joinToString(", ")}") updateStatus("💡 正在请求权限...") - ActivityCompat.requestPermissions( - this, - missingPermissions.toTypedArray(), - PERMISSION_REQUEST_CODE - ) - return false + try { + ActivityCompat.requestPermissions( + this, + missingPermissions.toTypedArray(), + PERMISSION_REQUEST_CODE + ) + updateStatus("📋 权限请求对话框已弹出") + return false + } catch (e: Exception) { + Log.e("MainActivity", "请求权限失败: ${e.message}", e) + updateStatus("❌ 权限请求失败: ${e.message}") + return false + } } updateStatus("✅ 所有必要权限已授予") + Log.d("MainActivity", "=== 权限检查完成:通过 ===") return true } @@ -255,26 +385,38 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real when (requestCode) { PERMISSION_REQUEST_CODE -> { + Log.d("MainActivity", "=== 权限请求结果回调 ===") + Log.d("MainActivity", "请求的权限数量: ${permissions.size}") + Log.d("MainActivity", "权限: ${permissions.contentToString()}") + Log.d("MainActivity", "结果: ${grantResults.contentToString()}") + val grantedPermissions = mutableListOf() val deniedPermissions = mutableListOf() + updateStatus("📋 权限请求结果:") + for (i in permissions.indices) { val permission = permissions[i] val granted = grantResults[i] == PackageManager.PERMISSION_GRANTED + val permissionName = permission.split(".").last() + + Log.d("MainActivity", "权限 $permissionName: ${if (granted) "已授予" else "被拒绝"}") if (granted) { - grantedPermissions.add(permission.split(".").last()) + grantedPermissions.add(permissionName) + updateStatus("✅ $permissionName: 已授权") } else { - deniedPermissions.add(permission.split(".").last()) + deniedPermissions.add(permissionName) + updateStatus("❌ $permissionName: 被拒绝") } } if (grantedPermissions.isNotEmpty()) { - updateStatus("✅ 权限已授予: ${grantedPermissions.joinToString(", ")}") + updateStatus("✅ 已授予权限: ${grantedPermissions.joinToString(", ")}") } if (deniedPermissions.isNotEmpty()) { - updateStatus("❌ 权限被拒绝: ${deniedPermissions.joinToString(", ")}") + updateStatus("❌ 被拒绝权限: ${deniedPermissions.joinToString(", ")}") updateStatus("💡 请在设置中手动授予权限") } @@ -283,12 +425,14 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real if (allGranted) { updateStatus("🎉 所有权限已授予,可以开始蓝牙扫描") updateStatus("📡 正在启动蓝牙扫描...") + binding.bluetoothButton.text = "权限通过" + binding.bluetoothButton.setBackgroundColor(Color.parseColor("#4CAF50")) startBluetoothScan() } else { updateStatus("⚠️ 部分权限被拒绝,蓝牙功能可能受限") - updateStatus("💡 建议:") - updateStatus(" • 进入应用设置 → 权限") - updateStatus(" • 手动开启蓝牙和位置权限") + updateStatus("💡 Android ${Build.VERSION.RELEASE} 解决方案:") + updateStatus(" • 进入设置 → 应用 → ${getString(android.R.string.unknownName)} → 权限") + updateStatus(" • 手动开启位置权限") updateStatus(" • 重启应用") binding.bluetoothButton.text = "权限被拒绝" @@ -351,11 +495,11 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real super.onResume() Log.d("MainActivity", "MainActivity恢复") - // 检查蓝牙连接状态 + // 检查蓝牙连接状态(只在真正连接后检查) if (bluetoothConnected) { Log.d("MainActivity", "蓝牙连接状态正常") } else { - Log.w("MainActivity", "⚠️ 蓝牙连接已断开,需要重新连接") + Log.d("MainActivity", "📱 未连接蓝牙设备(正常状态)") } } @@ -1087,30 +1231,26 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real } /** - * 显示电刺激开关对话框 + * 显示电刺激控制对话框 */ private fun showStimSwitchDialog() { - val stimOptions = arrayOf( - "开启刺激A", - "开启刺激B", + val dialogOptions = arrayOf( + "高级电刺激控制", + "预设模式选择", + "简单开关控制", "关闭电刺激" ) androidx.appcompat.app.AlertDialog.Builder(this) - .setTitle("电刺激开关") - .setItems(stimOptions) { _, which -> + .setTitle("电刺激控制") + .setItems(dialogOptions) { _, which -> when (which) { - 0 -> { - bluetoothManager.stimSwitch(0, true) - updateStatus("发送电刺激开关指令: 刺激A 开启") - } - 1 -> { - bluetoothManager.stimSwitch(1, true) - updateStatus("发送电刺激开关指令: 刺激B 开启") - } - 2 -> { - bluetoothManager.stimSwitch(0, false) - updateStatus("发送电刺激开关指令: 关闭电刺激") + 0 -> showAdvancedStimDialog() // 高级电刺激控制 + 1 -> showPresetStimDialog() // 预设模式选择 + 2 -> showSimpleStimDialog() // 简单开关控制 + 3 -> { + bluetoothManager.stimSwitch(enable = false) + updateStatus("发送电刺激关闭指令") } } } @@ -1118,6 +1258,287 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real .show() } + /** + * 高级电刺激控制对话框 - 完整参数版本 + */ + private fun showAdvancedStimDialog() { + val builder = androidx.appcompat.app.AlertDialog.Builder(this) + builder.setTitle("高级电刺激控制") + + // 创建滚动视图 + val scrollView = androidx.core.widget.NestedScrollView(this) + + // 创建输入字段的线性布局 + val layout = android.widget.LinearLayout(this).apply { + orientation = android.widget.LinearLayout.VERTICAL + setPadding(40, 20, 40, 20) + } + + // 启用刺激开关 + val enableSwitch = androidx.appcompat.widget.SwitchCompat(this).apply { + isChecked = true + } + val enableLabel = TextView(this).apply { + text = "启用刺激" + textSize = 16f + setPadding(0, 0, 0, 16) + } + layout.addView(enableLabel) + layout.addView(enableSwitch) + + // 基础参数 + val stimTypeInput = createSimpleInputField("刺激类型 (0-15):", "1") + layout.addView(stimTypeInput) + + val intensityInput = createSimpleInputField("强度 (%):", "50") + layout.addView(intensityInput) + + val frequencyInput = createSimpleInputField("频率 (Hz):", "100") + layout.addView(frequencyInput) + + val durationInput = createSimpleInputField("总时长 (秒):", "10") + layout.addView(durationInput) + + // 时间参数 + val restTimeInput = createSimpleInputField("休息时间 (毫秒):", "1000") + layout.addView(restTimeInput) + + val silenceTimeInput = createSimpleInputField("静默时间 (毫秒):", "500") + layout.addView(silenceTimeInput) + + val fadeInTimeInput = createSimpleInputField("缓进时间 (秒):", "2") + layout.addView(fadeInTimeInput) + + val holdTimeInput = createSimpleInputField("保持时间 (秒):", "5") + layout.addView(holdTimeInput) + + val fadeOutTimeInput = createSimpleInputField("缓出时间 (秒):", "2") + layout.addView(fadeOutTimeInput) + + // 添加提示信息 + val hintText = TextView(this).apply { + text = "💡 提示:保持时间建议 = 总时长 - 缓进时间 - 缓出时间" + textSize = 12f + setTextColor(ContextCompat.getColor(this@MainActivity, android.R.color.darker_gray)) + setPadding(0, 16, 0, 0) + } + layout.addView(hintText) + + scrollView.addView(layout) + builder.setView(scrollView) + + builder.setPositiveButton("开始刺激") { dialog, _ -> + try { + val enable = enableSwitch.isChecked + val stimTypeChild = stimTypeInput.getChildAt(1) as EditText + val intensityChild = intensityInput.getChildAt(1) as EditText + val frequencyChild = frequencyInput.getChildAt(1) as EditText + val durationChild = durationInput.getChildAt(1) as EditText + val restTimeChild = restTimeInput.getChildAt(1) as EditText + val silenceTimeChild = silenceTimeInput.getChildAt(1) as EditText + val fadeInTimeChild = fadeInTimeInput.getChildAt(1) as EditText + val holdTimeChild = holdTimeInput.getChildAt(1) as EditText + val fadeOutTimeChild = fadeOutTimeInput.getChildAt(1) as EditText + + val stimType = stimTypeChild.text.toString().toInt().coerceIn(0, 15) + val intensity = intensityChild.text.toString().toInt().coerceIn(1, 100) + val frequency = frequencyChild.text.toString().toInt().coerceIn(1, 200) + val totalDuration = durationChild.text.toString().toInt().coerceIn(1, 3600) * 1000 + val restTime = restTimeChild.text.toString().toInt().coerceIn(100, 10000) + val silenceTime = silenceTimeChild.text.toString().toInt().coerceIn(100, 5000) + val fadeInTime = fadeInTimeChild.text.toString().toInt().coerceIn(1, 30) + val holdTime = holdTimeChild.text.toString().toInt().coerceIn(1, 300) + val fadeOutTime = fadeOutTimeChild.text.toString().toInt().coerceIn(1, 30) + + bluetoothManager.stimSwitch( + enable = enable, + stimType = stimType, + intensity = intensity, + frequency = frequency, + totalDuration = totalDuration, + restTime = restTime, + silenceTime = silenceTime, + fadeInTime = fadeInTime, + holdTime = holdTime, + fadeOutTime = fadeOutTime + ) + + updateStatus("发送高级电刺激控制指令:") + updateStatus(" 类型: $stimType, 强度: $intensity%, 频率: ${frequency}Hz") + updateStatus(" 总时长: ${totalDuration/1000}s, 保持: ${holdTime}s") + updateStatus(" 休息: ${restTime}ms, 静默: ${silenceTime}ms") + updateStatus(" 缓进/出: ${fadeInTime}s/${fadeOutTime}s") + + } catch (e: NumberFormatException) { + updateStatus("❌ 参数输入错误,请检查数值格式") + } catch (e: Exception) { + updateStatus("❌ 发送失败: ${e.message}") + } + dialog.dismiss() + } + builder.setNegativeButton("取消", null) + builder.show() + } + + /** + * 创建简单的输入字段 + */ + private fun createSimpleInputField(label: String, defaultValue: String): LinearLayout { + val layout = LinearLayout(this).apply { + orientation = LinearLayout.VERTICAL + setPadding(0, 16, 0, 8) + } + + val labelView = TextView(this).apply { + text = label + textSize = 14f + } + + val inputView = EditText(this).apply { + setText(defaultValue) + hint = defaultValue + inputType = android.text.InputType.TYPE_CLASS_NUMBER + textSize = 16f + } + + layout.addView(labelView) + layout.addView(inputView) + + return layout + } + + /** + * 预设模式选择对话框 + */ + private fun showPresetStimDialog() { + val presetOptions = arrayOf( + "高强度短期刺激 (30s)", + "低强度长期刺激 (15分钟)", + "康复训练模式 (5分钟)", + "自定义预设参数" + ) + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("预设刺激模式") + .setItems(presetOptions) { _, which -> + when (which) { + 0 -> { + bluetoothManager.stimHighIntensityShort(intensity = 80) + updateStatus("启动高强度短期刺激模式") + } + 1 -> { + bluetoothManager.stimLowIntensityLong(intensity = 30) + updateStatus("启动低强度长期刺激模式") + } + 2 -> { + bluetoothManager.stimRehabilitationMode(intensity = 60) + updateStatus("启动康复训练模式") + } + 3 -> showPresetParamDialog() + } + } + .setNegativeButton("取消", null) + .show() + } + + /** + * 预设参数调整对话框 - 简化版本 + */ + private fun showPresetParamDialog() { + val builder = androidx.appcompat.app.AlertDialog.Builder(this) + builder.setTitle("调整预设参数") + + val layout = LinearLayout(this).apply { + orientation = LinearLayout.VERTICAL + setPadding(50, 30, 50, 30) + } + + // 预设类型选择 + val presetTypeView = Spinner(this).apply { + adapter = ArrayAdapter(this@MainActivity, android.R.layout.simple_spinner_item, arrayOf("高强度短期", "低强度长期", "康复训练")).also { adapter -> + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + } + setSelection(0) + } + val presetLabel = TextView(this).apply { + text = "预设类型:" + } + layout.addView(presetLabel) + layout.addView(presetTypeView) + + // 强度输入 + val intensityInput = createSimpleInputField("强度 (%):", "60") + layout.addView(intensityInput) + + // 持续时间输入 + val durationInput = createSimpleInputField("持续时间:", "30") + layout.addView(durationInput) + + builder.setView(layout) + builder.setPositiveButton("应用") { dialog, _ -> + try { + val presetType = presetTypeView.selectedItemPosition + val intensityChild = intensityInput.getChildAt(1) as EditText + val durationChild = durationInput.getChildAt(1) as EditText + val intensity = intensityChild.text.toString().toInt().coerceIn(1, 100) + val duration = durationChild.text.toString().toInt().coerceIn(1, 120) + + when (presetType) { + 0 -> { + bluetoothManager.stimHighIntensityShort(durationSeconds = duration, intensity = intensity) + updateStatus("应用高强度短期模式: ${duration}s, 强度${intensity}%") + } + 1 -> { + bluetoothManager.stimLowIntensityLong(durationMinutes = duration, intensity = intensity) + updateStatus("应用低强度长期模式: ${duration}分钟, 强度${intensity}%") + } + 2 -> { + bluetoothManager.stimRehabilitationMode(intensity = intensity) + updateStatus("应用康复训练模式: 强度${intensity}%") + } + } + } catch (e: NumberFormatException) { + updateStatus("❌ 参数输入错误") + } + dialog.dismiss() + } + builder.setNegativeButton("取消", null) + builder.show() + } + + /** + * 简单开关控制对话框 + */ + private fun showSimpleStimDialog() { + val simpleOptions = arrayOf( + "开启刺激A (默认参数)", + "开启刺激B (默认参数)", + "关闭电刺激" + ) + + androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("简单电刺激控制") + .setItems(simpleOptions) { _, which -> + when (which) { + 0 -> { + bluetoothManager.stimSwitchSimple(0, true) + updateStatus("发送电刺激开关指令: 刺激A 开启 (默认参数)") + } + 1 -> { + bluetoothManager.stimSwitchSimple(1, true) + updateStatus("发送电刺激开关指令: 刺激B 开启 (默认参数)") + } + 2 -> { + bluetoothManager.stimSwitch(enable = false) + updateStatus("发送电刺激关闭指令") + } + } + } + .setNegativeButton("取消", null) + .show() + } + + /** * 显示调试状态对话框 */