From 1ce6cbffb3f9cf9090adce4a55e0627002221d3f Mon Sep 17 00:00:00 2001 From: ZhangJinLong <19357383190@163.com> Date: Thu, 9 Oct 2025 17:39:44 +0800 Subject: [PATCH] f --- ...otlin-compiler-15274677907984621681.salive | 0 app/src/main/cpp/jni/jni_bridge.cpp | 83 +-- app/src/main/cpp/src/indicator_cal.cpp | 6 +- .../cmake_project_test/BluetoothManager.kt | 85 +-- .../cmake_project_test/MainActivity.kt | 355 +---------- 工作交接文档.md | 554 ++++++++++++++++++ 6 files changed, 564 insertions(+), 519 deletions(-) create mode 100644 .kotlin/sessions/kotlin-compiler-15274677907984621681.salive create mode 100644 工作交接文档.md diff --git a/.kotlin/sessions/kotlin-compiler-15274677907984621681.salive b/.kotlin/sessions/kotlin-compiler-15274677907984621681.salive new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/cpp/jni/jni_bridge.cpp b/app/src/main/cpp/jni/jni_bridge.cpp index 6464b3e..caf35a0 100644 --- a/app/src/main/cpp/jni/jni_bridge.cpp +++ b/app/src/main/cpp/jni/jni_bridge.cpp @@ -1,74 +1,15 @@ - #include +#include #include #include #include #include #include #include -#include "../include/cpp/add.h" #include "../include/cpp/data_praser.h" #include "../include/cpp/signal_processor.h" #include "../include/cpp/data_mapper.h" #include "../include/cpp/indicator_cal.h" -using core::math::add; - -extern "C" JNIEXPORT jstring JNICALL -Java_com_example_cmake_1project_1test_MainActivity_stringFromJNI( - JNIEnv* env, - jobject /* this */) { - std::string hello = "Hello from C++ (multi-file)"; - return env->NewStringUTF(hello.c_str()); -} - -extern "C" JNIEXPORT jint JNICALL -Java_com_example_cmake_1project_1test_MainActivity_addFromJNI( - JNIEnv* env, - jobject /* this */, - jint a, - jint b) { - (void)env; - return static_cast(add(static_cast(a), static_cast(b))); -} - -extern "C" JNIEXPORT jobject JNICALL -Java_com_example_cmake_1project_1test_MainActivity_parseDeviceDataFromJNI( - JNIEnv* env, - jobject /* this */, - jbyteArray fileData) { - - try { - // 将Java的byte数组转换为C++的vector - std::vector cppFileData = convertJavaByteArrayToVector(env, fileData); - - if (cppFileData.empty()) { - __android_log_print(ANDROID_LOG_WARN, "DataParser", "Empty file data received"); - return nullptr; - } - - // 调用C++的解析函数 - std::vector result = parse_device_data(cppFileData); - - // 将结果转换为Java对象 - jobject javaResult = convertSensorDataVectorToJavaList(env, result); - - if (javaResult == nullptr) { - __android_log_print(ANDROID_LOG_ERROR, "DataParser", "Failed to convert result to Java object"); - } else { - __android_log_print(ANDROID_LOG_INFO, "DataParser", "Successfully parsed %zu data packets", result.size()); - } - - return javaResult; - - } catch (const std::exception& e) { - __android_log_print(ANDROID_LOG_ERROR, "DataParser", "Exception during parsing: %s", e.what()); - return nullptr; - } catch (...) { - __android_log_print(ANDROID_LOG_ERROR, "DataParser", "Unknown exception during parsing"); - return nullptr; - } -} - // ================= 流式解析 JNI 接口 ================= @@ -436,28 +377,6 @@ Java_com_example_cmake_1project_1test_SignalProcessorJNI_processRealtimeChunk( } } -// 便捷方法:从整块数据按固定块大小模拟流式解析,返回解析到的所有包 -extern "C" JNIEXPORT jobject JNICALL -Java_com_example_cmake_1project_1test_MainActivity_parseStreamFromBytes( - JNIEnv* env, - jobject /* this */, - jbyteArray data, - jint chunkSize) { - if (data == nullptr || chunkSize <= 0) return nullptr; - - std::vector src = convertJavaByteArrayToVector(env, data); - auto* parser = new StreamParser(); - size_t offset = 0; - while (offset < src.size()) { - size_t n = std::min(static_cast(chunkSize), src.size() - offset); - parser->appendData(src.data() + offset, n); - offset += n; - } - std::vector packets = parser->getAllPackets(); - delete parser; - return convertSensorDataVectorToJavaList(env, packets); -} - // ============================================================================ // DataMapper JNI函数实现 // ============================================================================ diff --git a/app/src/main/cpp/src/indicator_cal.cpp b/app/src/main/cpp/src/indicator_cal.cpp index 0d87ed6..a4de769 100644 --- a/app/src/main/cpp/src/indicator_cal.cpp +++ b/app/src/main/cpp/src/indicator_cal.cpp @@ -512,7 +512,7 @@ float MetricsCalculator::calculate_heart_rate_ppg(const std::vector& ppg_ // 检测脉搏波峰 auto pulse_peaks = detect_pulse_peaks(ppg_signal, sample_rate); if (pulse_peaks.size() < 2) { - std::cerr << "警告: 检测到的脉搏波峰数量不足: " << pulse_peaks.size() << ",至少需要2个" << std::endl; + std::cerr << "警告: 检测到的脉搏波峰数量不足: " << pulse_peaks.size() << ",至少需要2个" << std::endl; return 0.0f; } @@ -551,13 +551,13 @@ float MetricsCalculator::calculate_heart_rate_ppg(const std::vector& ppg_ float MetricsCalculator::calculate_spo2(const SensorData& ppg_data) { // 输入验证 if (!std::holds_alternative>>(ppg_data.channel_data)) { - std::cerr << "警告: PPG数据格式不正确,需要多通道数据" << std::endl; + std::cerr << "警告: PPG数据格式不正确,需要多通道数据" << std::endl; return 0.0f; } const auto& channels = std::get>>(ppg_data.channel_data); if (channels.size() < 2) { - std::cerr << "警告: PPG数据通道数不足,需要至少2个通道,当前: " << channels.size() << std::endl; + std::cerr << "警告: PPG数据通道数不足,需要至少2个通道,当前: " << channels.size() << ",需要至少2个通道" << std::endl; return 0.0f; } 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 85f856e..89a3216 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 @@ -755,90 +755,7 @@ class BluetoothManager(private val context: Context) { 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) - } - } /** * 处理原始数据(非标准协议格式) @@ -1714,7 +1631,7 @@ class BluetoothManager(private val context: Context) { Log.d(TAG, "解析功能码: 0x${functionCode.toString(16).uppercase()}") Log.d(TAG, "解析数据长度: $dataLength") - if (functionCode == Protocol.FUNC_DATA_STREAM && dataLength == 238) { + if (functionCode == Protocol.FUNC_DATA_STREAM && dataLength == 238) { Log.d(TAG, "✅ 确认是轻迅协议数据流包") Log.d(TAG, "协议包结构: 功能码(2) + 数据长度(2) + 数据内容(238) + CRC16(2) = 244字节") 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 8171d17..0cd8f23 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 @@ -568,38 +568,7 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real - /** - * 获取原始通道数据 - */ - private fun getRawChannelData(channelIndex: Int): List { - val data = mutableListOf() - try { - // 从原始数据包中提取指定通道的数据 - val packets = dataManager.getPacketBuffer() - for (packet in packets) { - val channelData = packet.getChannelData() - if (channelData != null && channelIndex < channelData.size) { - data.addAll(channelData[channelIndex]) - } - } - } catch (e: Exception) { - Log.e("MainActivity", "获取原始通道数据失败: ${e.message}", e) - } - return data - } - /** - * 获取处理后的通道数据(滤波后的数据) - */ - private fun getProcessedChannelData(channelIndex: Int): List { - try { - // 直接从DataManager获取滤波后的数据 - return dataManager.getProcessedChannelData(channelIndex) - } catch (e: Exception) { - Log.e("MainActivity", "获取处理后通道数据失败: ${e.message}", e) - return emptyList() - } - } // 实时数据回调实现 override fun onProcessedDataAvailable(channelIndex: Int, processedData: List) { @@ -1236,8 +1205,6 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real private fun showStimSwitchDialog() { val dialogOptions = arrayOf( "高级电刺激控制", - "预设模式选择", - "简单开关控制", "关闭电刺激" ) @@ -1246,9 +1213,7 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real .setItems(dialogOptions) { _, which -> when (which) { 0 -> showAdvancedStimDialog() // 高级电刺激控制 - 1 -> showPresetStimDialog() // 预设模式选择 - 2 -> showSimpleStimDialog() // 简单开关控制 - 3 -> { + 1 -> { bluetoothManager.stimSwitch(enable = false) updateStatus("发送电刺激关闭指令") } @@ -1406,153 +1371,7 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real 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() - } - - - /** - * 显示调试状态对话框 - */ - private fun showDebugStatus() { - val status = bluetoothManager.debugConnectionStatus() - - androidx.appcompat.app.AlertDialog.Builder(this) - .setTitle("协议统计信息") - .setMessage(status) - .setPositiveButton("确定", null) - .show() - } - - + private fun clearAllData() { // 清空动态图表数据 dynamicChartManager.clearAllCharts() @@ -1563,13 +1382,6 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real 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}") @@ -1699,174 +1511,17 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real }.start() } - private fun exportWaveformData(data: List, stats: Map) { - Thread { - try { - if (data.isEmpty()) { - runOnUiThread { - updateStatus("❌ 波形视图没有数据可以导出") - } - return@Thread - } - - val timestamp = java.text.SimpleDateFormat("yyyyMMdd_HHmmss", java.util.Locale.getDefault()).format(java.util.Date()) - val fileName = "ECG_WaveformData_$timestamp.csv" - - val csvContent = buildString { - appendLine("ECG波形视图数据 (2.5秒显示窗口)") - appendLine("导出时间: ${java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault()).format(java.util.Date())}") - appendLine("数据统计:") - appendLine("- 数据点数量: ${stats["dataPoints"]}") - appendLine("- 最小值: ${stats["minValue"]}") - appendLine("- 最大值: ${stats["maxValue"]}") - appendLine("- 数据范围: ${stats["range"]}") - appendLine("- 采样率: 250Hz") - appendLine("- 时间窗口: 2.5秒") - appendLine() - appendLine("时间(ms),数据点,ECG值") - - val sampleInterval = 4L // 4ms per sample (250Hz) - data.forEachIndexed { index, value -> - val timeMs = index * sampleInterval - appendLine("$timeMs,$index,$value") - } - } - - val file = saveFileToExternalStorage(fileName, csvContent) - if (file != null) { - runOnUiThread { - updateStatus("✅ 波形视图数据已导出: ${file.name}") - } - } else { - runOnUiThread { - updateStatus("❌ 导出波形视图数据失败") - } - } - - } catch (e: Exception) { - Log.e("MainActivity", "导出波形视图数据失败: ${e.message}", e) - runOnUiThread { - updateStatus("❌ 导出波形视图数据失败: ${e.message}") - } - } - }.start() - } + /** * 导出所有图表数据 */ - private fun exportAllChartData( - waveformData: List, - waveformStats: Map - ) { - Thread { - try { - if (waveformData.isEmpty()) { - runOnUiThread { - updateStatus("❌ 没有图表数据可以导出") - } - return@Thread - } - - val timestamp = java.text.SimpleDateFormat("yyyyMMdd_HHmmss", java.util.Locale.getDefault()).format(java.util.Date()) - val fileName = "ECG_AllChartData_$timestamp.csv" - - val csvContent = buildString { - appendLine("ECG图表数据完整导出") - appendLine("导出时间: ${java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault()).format(java.util.Date())}") - appendLine("采样率: 250Hz") - appendLine() - - // 波形视图数据 - appendLine("=== 波形视图数据 (2.5秒窗口) ===") - appendLine("数据点数量: ${waveformStats["dataPoints"]}") - appendLine("最小值: ${waveformStats["minValue"]}") - appendLine("最大值: ${waveformStats["maxValue"]}") - appendLine("数据范围: ${waveformStats["range"]}") - appendLine() - appendLine("时间(ms),数据点,波形ECG值") - - val sampleInterval = 4L // 4ms per sample (250Hz) - waveformData.forEachIndexed { index, value -> - val timeMs = index * sampleInterval - appendLine("$timeMs,$index,$value") - } - } - - val file = saveFileToExternalStorage(fileName, csvContent) - if (file != null) { - runOnUiThread { - updateStatus("✅ 所有图表数据已导出: ${file.name}") - } - } else { - runOnUiThread { - updateStatus("❌ 导出所有图表数据失败") - } - } - - } catch (e: Exception) { - Log.e("MainActivity", "导出所有图表数据失败: ${e.message}", e) - runOnUiThread { - updateStatus("❌ 导出所有图表数据失败: ${e.message}") - } - } - }.start() - } + /** * 导出图表统计信息 */ - private fun exportChartStats(waveformStats: Map) { - Thread { - try { - val timestamp = java.text.SimpleDateFormat("yyyyMMdd_HHmmss", java.util.Locale.getDefault()).format(java.util.Date()) - val fileName = "ECG_ChartStats_$timestamp.txt" - - val statsContent = buildString { - appendLine("ECG图表统计信息") - appendLine("导出时间: ${java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault()).format(java.util.Date())}") - appendLine("=".repeat(50)) - appendLine() - - appendLine("波形视图统计 (2.5秒窗口):") - appendLine("- 数据点数量: ${waveformStats["dataPoints"]}") - appendLine("- 最小值: ${waveformStats["minValue"]}") - appendLine("- 最大值: ${waveformStats["maxValue"]}") - appendLine("- 数据范围: ${waveformStats["range"]}") - appendLine("- 是否有数据: ${waveformStats["isDataAvailable"]}") - appendLine("- 采样率: 250Hz") - appendLine("- 时间窗口: 2.5秒") - appendLine() - - appendLine("数据质量评估:") - val waveformRange = waveformStats["range"] as Float - - when { - waveformRange > 100f -> appendLine("- 波形视图: 信号强度良好") - waveformRange > 50f -> appendLine("- 波形视图: 信号强度中等") - else -> appendLine("- 波形视图: 信号强度较弱") - } - } - - val file = saveFileToExternalStorage(fileName, statsContent) - if (file != null) { - runOnUiThread { - updateStatus("✅ 图表统计信息已导出: ${file.name}") - } - } else { - runOnUiThread { - updateStatus("❌ 导出图表统计信息失败") - } - } - - } catch (e: Exception) { - Log.e("MainActivity", "导出图表统计信息失败: ${e.message}", e) - runOnUiThread { - updateStatus("❌ 导出图表统计信息失败: ${e.message}") - } - } - }.start() - } + /** * 显示文件分享对话框 diff --git a/工作交接文档.md b/工作交接文档.md new file mode 100644 index 0000000..58a8f6c --- /dev/null +++ b/工作交接文档.md @@ -0,0 +1,554 @@ +# 蓝牙电刺激控制应用 - 工作交接文档 + +## 项目概述 + +### 项目名称 +蓝牙电刺激控制应用 (SDK_APP) + +### 项目功能 +基于Android的蓝牙电刺激设备控制应用,支持多种设备类型(ESP32S3 SPP、轻迅设备、新设备),提供完整的电刺激参数控制和数据采集功能。 + +### 技术栈 +- **开发语言**: Kotlin +- **构建工具**: Gradle +- **最低支持**: Android 5.1 (API 21) +- **目标支持**: Android 15 (API 35) +- **蓝牙协议**: BLE + Classic Bluetooth + +--- + +## 核心功能模块 + +### 1. 蓝牙连接管理 (`BluetoothManager.kt`) + +#### 1.1 设备兼容性 +```kotlin +// 支持的设备类型 +enum class DeviceType { + ESP32S3_DEVICE, // ESP32S3 SPP设备 + QINGXUN, // 轻迅设备 + NEW_DEVICE // 新设备 +} +``` + +#### 1.2 扫描策略 +- **Android 8/8.1**: 混合扫描模式(BLE + 传统蓝牙) +- **Android 6-11**: 优先BLE,失败时回退到传统蓝牙 +- **Android 12+**: 完整BLE扫描支持 + +#### 1.3 连接流程 +1. 权限检查 +2. 蓝牙适配器初始化 +3. 设备扫描 +4. 服务发现 +5. 特征值订阅 +6. 数据通信 + +### 2. 电刺激控制协议 + +#### 2.1 协议格式(19字节) +``` +[0-1] 功能码 (0x0003) +[2-3] 数据长度 (0x000D = 13字节) +[4] 开关状态及电刺激类型 +[5] 强度值 +[6-7] 频率值 (小端格式) +[8-9] 总持续时间 (小端格式) +[10-11] 休息时间 (小端格式) +[12-13] 静默时间 (小端格式) +[14] 缓进时间 +[15] 保持时间 +[16] 缓出时间 +[17-18] CRC16校验 +``` + +#### 2.2 参数控制 +| 参数 | 范围 | 默认值 | 单位 | 说明 | +|------|------|--------|------|------| +| 刺激类型 | 0-15 | 1 | - | 电刺激类型选择 | +| 强度 | 1-100 | 50 | % | 刺激强度百分比 | +| 频率 | 1-200 | 100 | Hz | 刺激频率 | +| 总时长 | 1-3600 | 10 | 秒 | 总刺激持续时间 | +| 休息时间 | 100-10000 | 1000 | 毫秒 | 刺激间隔休息时间 | +| 静默时间 | 100-5000 | 500 | 毫秒 | 刺激间隔静默时间 | +| 缓进时间 | 1-30 | 2 | 秒 | 刺激强度逐渐增加时间 | +| 保持时间 | 1-300 | 5 | 秒 | 刺激强度保持时间 | +| 缓出时间 | 1-30 | 2 | 秒 | 刺激强度逐渐减少时间 | + +### 3. 数据存储功能 + +#### 3.1 数据导出功能 +- **CSV格式导出**: 支持多通道生理信号数据导出 +- **文件存储**: 保存到外部存储目录 +- **数据格式**: 包含时间戳、通道数据、统计信息 + +#### 3.2 导出类型 +| 导出类型 | 文件名格式 | 内容说明 | +|----------|------------|----------| +| **单通道数据** | `ECG_ChannelData_YYYYMMDD_HHMMSS.csv` | 单个通道的完整数据 | +| **多通道数据** | `ECG_MultiChannelData_YYYYMMDD_HHMMSS.csv` | 所有通道的同步数据 | +| **波形视图数据** | `ECG_WaveformData_YYYYMMDD_HHMMSS.csv` | 2.5秒显示窗口数据 | +| **完整图表数据** | `ECG_AllChartData_YYYYMMDD_HHMMSS.csv` | 所有图表数据汇总 | + +#### 3.3 数据管理 (`DataManager.kt`) +- **数据解析**: 使用原生方法解析蓝牙数据流 +- **缓冲管理**: 实时数据缓冲和回调管理 +- **多回调支持**: 支持多个数据接收器 + +### 4. 图表显示功能 + +#### 4.1 动态图表系统 (`DynamicChartView.kt`) +- **实时绘制**: 60fps实时数据绘制 +- **多通道支持**: 支持单通道和多通道数据显示 +- **生产者-消费者模式**: 使用队列缓冲数据,分离数据接收和显示 + +#### 4.2 图表管理器 (`DynamicChartManager.kt`) +- **动态创建**: 根据设备类型自动创建图表 +- **设备配置**: 不同设备类型的图表配置 +- **全屏支持**: 支持图表全屏显示 + +#### 4.3 全屏图表 (`FullscreenChartActivity.kt`) +- **全屏模式**: 隐藏状态栏和导航栏 +- **生命体征显示**: 实时显示心率和血氧 +- **滤波器控制**: 集成滤波器管理器 +- **Y轴范围设置**: 支持自定义Y轴范围 + +#### 4.4 图表特性 +| 特性 | 说明 | 技术实现 | +|------|------|----------| +| **实时更新** | 60fps刷新率 | ScheduledThreadPoolExecutor | +| **数据缓冲** | 队列容量10000点 | LinkedBlockingQueue | +| **内存管理** | 自动清理旧数据 | 定期内存检查 | +| **多通道** | 支持PPG等多通道设备 | 多通道数据队列 | +| **交互控制** | 缩放、平移 | 手势识别 | + +### 5. 滤波器系统 + +#### 5.1 滤波器管理器 (`FullscreenFilterManager.kt`) +- **实时滤波**: 支持多种滤波器类型 +- **参数调节**: 可调节滤波器参数 +- **性能优化**: 高效的数据处理算法 + +#### 5.2 支持的滤波器 +- **低通滤波器**: 去除高频噪声 +- **高通滤波器**: 去除低频漂移 +- **带通滤波器**: 保留特定频率范围 +- **陷波滤波器**: 去除工频干扰 + +### 6. 用户界面 (`MainActivity.kt`) + +#### 6.1 权限管理 +```kotlin +// 动态权限请求 +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 if (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() +} +``` + +#### 6.2 电刺激控制界面 +- **高级电刺激控制**: 完整参数自定义 +- **预设模式选择**: 快速选择预设方案 +- **简单开关控制**: 使用默认参数 +- **滑动对话框**: 支持滚动查看所有参数 + +--- + +## 代码运行机制 + +### 1. 应用启动流程 + +```mermaid +graph TD + A[应用启动] --> B[MainActivity.onCreate] + B --> C[权限检查] + C --> D[蓝牙初始化] + D --> E[UI初始化] + E --> F[等待用户操作] +``` + +### 2. 蓝牙连接流程 + +```mermaid +graph TD + A[用户点击连接] --> B[权限检查] + B --> C{权限是否通过?} + C -->|否| D[请求权限] + C -->|是| E[开始扫描] + D --> E + E --> F[设备发现] + F --> G[选择设备] + G --> H[建立连接] + H --> I[服务发现] + I --> J[特征值订阅] + J --> K[数据通信就绪] +``` + +### 3. 电刺激控制流程 + +```mermaid +graph TD + A[用户选择电刺激控制] --> B[选择控制方式] + B --> C{控制方式} + C -->|高级控制| D[参数输入对话框] + C -->|预设模式| E[预设选择] + C -->|简单控制| F[快速开关] + D --> G[参数验证] + E --> G + F --> G + G --> H[构建数据包] + H --> I[发送指令] + I --> J[状态反馈] +``` + +### 4. 数据包构建流程 + +```mermaid +graph TD + A[接收参数] --> B[创建19字节数组] + B --> C[写入功能码] + C --> D[写入数据长度] + D --> E[写入开关状态] + E --> F[写入强度] + F --> G[写入频率] + G --> H[写入持续时间] + H --> I[写入休息时间] + I --> J[写入静默时间] + J --> K[写入缓进时间] + K --> L[写入保持时间] + L --> M[写入缓出时间] + M --> N[计算CRC16] + N --> O[发送数据包] +``` + +--- + +## 关键代码文件说明 + +### 1. `BluetoothManager.kt` - 蓝牙管理核心 + +#### 主要类和方法: +- `BluetoothManager`: 蓝牙连接管理主类 +- `startScan()`: 启动设备扫描 +- `connectToDevice()`: 连接指定设备 +- `stimSwitch()`: 发送电刺激控制指令 +- `handleProtocolData()`: 处理接收到的数据 + +#### 关键特性: +- 多设备类型支持 +- Android版本兼容性处理 +- 混合扫描策略 +- 协议数据包构建 + +### 2. `MainActivity.kt` - 用户界面控制 + +#### 主要类和方法: +- `MainActivity`: 主界面Activity +- `showAdvancedStimDialog()`: 高级电刺激控制对话框 +- `showPresetStimDialog()`: 预设模式选择 +- `createSimpleInputField()`: 创建输入字段 +- `checkAndRequestPermissions()`: 权限检查 + +#### 关键特性: +- 动态权限管理 +- 滑动对话框界面 +- 参数输入验证 +- 状态实时反馈 + +### 3. `DynamicChartView.kt` - 动态图表显示 + +#### 主要类和方法: +- `DynamicChartView`: 自定义图表视图 +- `updateData()`: 更新单通道数据 +- `updateMultiChannelData()`: 更新多通道数据 +- `startConsumer()`: 启动消费者线程 +- `stopConsumer()`: 停止消费者线程 + +#### 关键特性: +- 生产者-消费者模式 +- 60fps实时绘制 +- 多通道数据支持 +- 内存管理优化 + +### 4. `DynamicChartManager.kt` - 图表管理器 + +#### 主要类和方法: +- `DynamicChartManager`: 图表管理主类 +- `createChartsForDevice()`: 根据设备类型创建图表 +- `updateChartsData()`: 更新所有图表数据 +- `getDeviceConfig()`: 获取设备配置 + +#### 关键特性: +- 动态图表创建 +- 设备类型适配 +- 全屏模式支持 + +### 5. `FullscreenChartActivity.kt` - 全屏图表 + +#### 主要类和方法: +- `FullscreenChartActivity`: 全屏图表Activity +- `setupFullscreen()`: 设置全屏模式 +- `setupFullscreenCharts()`: 设置全屏图表 +- `setupVitalSignsDisplay()`: 设置生命体征显示 + +#### 关键特性: +- 全屏显示模式 +- 生命体征实时显示 +- 滤波器集成 +- Y轴范围自定义 + +### 6. `DataManager.kt` - 数据管理 + +#### 主要类和方法: +- `DataManager`: 数据管理主类 +- `processData()`: 处理原始数据 +- `setRealTimeCallback()`: 设置实时数据回调 +- `addRealTimeCallback()`: 添加数据回调 + +#### 关键特性: +- 原生方法调用 +- 多回调支持 +- 数据缓冲管理 + +### 7. `AndroidManifest.xml` - 权限配置 + +#### 权限声明: +```xml + + + + + + + + + + + +``` + +--- + +## 设备支持 + +### 1. ESP32S3 SPP设备 +- **服务UUID**: 0xABF0 +- **特征值**: + - 数据发送: 0xABF1, 0xABF3 + - 数据接收: 0xABF2, 0xABF4 +- **设备名称**: ESP_SPP_SERVER + +### 2. 轻迅设备 +- **服务UUID**: Nordic UART Service +- **特征值**: TX/RX特征值 +- **协议**: 轻迅自定义协议 + +### 3. 新设备 +- **服务UUID**: 自定义UUID +- **特征值**: 自定义特征值 +- **协议**: 扩展协议支持 + +--- + +## 操作指南 + +### 1. 应用启动 +1. 安装APK到Android设备 +2. 启动应用 +3. 检查权限状态 +4. 点击"连接蓝牙"开始扫描 + +### 2. 设备连接 +1. 确保目标设备处于可发现模式 +2. 点击"连接蓝牙"按钮 +3. 等待设备扫描完成 +4. 选择目标设备进行连接 + +### 3. 电刺激控制 +1. 连接成功后,点击"电刺激开关" +2. 选择控制方式: + - **高级控制**: 自定义所有参数 + - **预设模式**: 选择预设方案 + - **简单控制**: 使用默认参数 +3. 设置参数并确认 +4. 查看状态反馈 + +### 4. 图表操作 +1. **查看实时数据**: 连接设备后自动显示图表 +2. **全屏模式**: 点击图表右上角全屏按钮 +3. **数据导出**: + - 点击"导出数据"按钮 + - 选择导出类型(单通道/多通道/完整数据) + - 文件保存到外部存储 +4. **滤波器设置**: 在全屏模式下使用滤波器控制 +5. **Y轴范围**: 在全屏模式下可自定义Y轴范围 + +### 5. 数据导出 +1. **单通道数据导出**: + - 点击"导出数据" → "单通道数据" + - 选择要导出的通道 + - 文件格式:CSV +2. **多通道数据导出**: + - 点击"导出数据" → "多通道数据" + - 导出所有通道的同步数据 +3. **完整数据导出**: + - 点击"导出数据" → "完整数据" + - 包含所有图表和统计信息 +4. **文件分享**: 导出完成后可选择分享或保存 + +### 6. 参数设置示例 +``` +刺激类型: 1 +强度: 70% +频率: 120Hz +总时长: 30秒 +休息时间: 1000ms +静默时间: 500ms +缓进时间: 2秒 +保持时间: 26秒 +缓出时间: 2秒 +``` + +--- + +## 故障排除 + +### 1. 权限问题 +- **现象**: 显示"权限被拒绝" +- **解决**: 检查应用权限设置,确保蓝牙和位置权限已授予 + +### 2. 设备扫描失败 +- **现象**: 找不到目标设备 +- **解决**: + - 确保设备处于可发现模式 + - 检查蓝牙是否开启 + - 尝试重启蓝牙服务 + +### 3. 连接失败 +- **现象**: 连接超时或失败 +- **解决**: + - 检查设备距离 + - 确认设备未被其他应用占用 + - 尝试重新扫描 + +### 4. 数据发送失败 +- **现象**: 电刺激指令发送失败 +- **解决**: + - 检查连接状态 + - 验证参数范围 + - 查看日志输出 + +### 5. 图表显示问题 +- **现象**: 图表不显示数据或显示异常 +- **解决**: + - 检查数据连接状态 + - 重启图表显示 + - 检查内存使用情况 + - 查看队列大小日志 + +### 6. 数据导出失败 +- **现象**: 无法导出数据或文件保存失败 +- **解决**: + - 检查存储权限 + - 确保有足够存储空间 + - 检查文件路径权限 + - 重启应用后重试 + +### 7. 全屏模式问题 +- **现象**: 全屏模式无法正常显示 +- **解决**: + - 检查设备屏幕方向 + - 重启全屏Activity + - 检查系统UI设置 + +--- + +## 开发环境配置 + +### 1. 开发工具 +- Android Studio +- Gradle 7.0+ +- Android SDK API 21-35 + +### 2. 构建命令 +```bash +# 编译Kotlin代码 +.\gradlew.bat compileDebugKotlin + +# 构建APK +.\gradlew.bat assembleDebug + +# 清理项目 +.\gradlew.bat clean +``` + +### 3. 调试方法 +- **日志监控**: 使用Logcat查看详细日志输出 +- **蓝牙状态**: 监控蓝牙连接和扫描状态 +- **权限检查**: 查看权限请求和授权日志 +- **数据包**: 监控电刺激指令发送日志 +- **图表数据**: 查看队列大小和数据流日志 +- **内存使用**: 监控内存占用和清理日志 +- **数据导出**: 查看文件保存和分享日志 +- **全屏模式**: 监控全屏切换和UI状态 + +--- + +## 版本历史 + +### 当前版本特性 +- ✅ 支持Android 5.1-15 +- ✅ ESP32S3 SPP设备支持 +- ✅ 完整电刺激参数控制 +- ✅ 滑动对话框界面 +- ✅ 多设备类型兼容 +- ✅ 混合扫描策略 +- ✅ 权限动态管理 +- ✅ 实时图表显示系统 +- ✅ 多通道数据支持 +- ✅ 数据导出功能 +- ✅ 全屏图表模式 +- ✅ 滤波器集成 +- ✅ 生产者-消费者数据模式 +- ✅ 内存管理优化 + +### 已知问题 + +#### 7.1 动态图标显示Bug +- **问题描述**: 动态图标显示的是队列大小而不是实际数据值 +- **影响范围**: 图表状态显示 +- **技术原因**: 状态更新逻辑中错误地使用了队列大小作为显示值 +- **代码位置**: `DynamicChartView.kt` 中的日志输出和状态更新 +- **临时解决方案**: 当前通过日志输出队列大小,需要修正为显示实际数据值 + +#### 7.2 其他已知问题 +- **Android 8.1扫描兼容性**: 部分设备扫描兼容性问题 +- **设备连接稳定性**: 部分设备连接稳定性待优化 +- **内存使用**: 长时间运行可能导致内存占用过高 +- **数据同步**: 多通道数据同步可能存在延迟 + +--- + +## 联系方式 + +如有技术问题或需要支持,请联系开发团队。 + +--- + +**文档版本**: 1.0 +**最后更新**: 2025年1月 +**维护人员**: 开发团队