STIMULATE COMMIT

This commit is contained in:
ZhangJinLong 2025-09-30 14:10:23 +08:00
parent bd8fd53597
commit d6c870b243
4 changed files with 1037 additions and 151 deletions

View File

@ -1,3 +1,4 @@
{
"java.configuration.updateBuildConfiguration": "interactive"
"java.configuration.updateBuildConfiguration": "interactive",
"idf.pythonInstallPath": "C:\\Espressif\\tools\\idf-python\\3.11.2\\python.exe"
}

View File

@ -6,15 +6,22 @@
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- Android 12+ 新蓝牙权限 -->
<!-- Android 12-14 新蓝牙权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- Android 15+ 可能的额外权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<!-- 位置权限 (所有Android版本都需要用于蓝牙扫描) -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Android 8.1传统蓝牙扫描额外权限 -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.RECEIVE_BLUETOOTH_SCAN_RESULTS" />
<!-- 文件导出权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

View File

@ -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<String>()
// 启动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>(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("💡 建议:长按'连接蓝牙'按钮直接连接")
}

View File

@ -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<String> = 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<String> = 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<String>()
val grantedPermissions = mutableListOf<String>()
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<String>()
val deniedPermissions = mutableListOf<String>()
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()
}
/**
* 显示调试状态对话框
*/