diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 24ccfcd..94a25f7 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,6 +2,5 @@ - \ No newline at end of file diff --git a/app/src/main/cpp/src/data_praser.cpp b/app/src/main/cpp/src/data_praser.cpp index 2b0f574..4aa4baf 100644 --- a/app/src/main/cpp/src/data_praser.cpp +++ b/app/src/main/cpp/src/data_praser.cpp @@ -565,22 +565,23 @@ std::vector parse_device_data(const std::vector& file_data) return final_results; } -// CRC16校验函数 (Modbus CRC16) +// CRC16校验函数 (CCITT CRC16) uint16_t calculate_crc16(const uint8_t* data, size_t length) { - uint16_t crc = 0xFFFF; + uint16_t wCRCin = 0xFFFF; + const uint16_t wCPoly = 0x1021; for (size_t i = 0; i < length; i++) { - crc ^= data[i]; for (int j = 0; j < 8; j++) { - if (crc & 0x0001) { - crc = (crc >> 1) ^ 0xA001; - } else { - crc = crc >> 1; + bool bit = ((data[i] >> (7 - j)) & 1) == 1; + bool c15 = ((wCRCin >> 15) & 1) == 1; + wCRCin = wCRCin << 1; + if (c15 ^ bit) { + wCRCin = wCRCin ^ wCPoly; } } } - return crc; + return wCRCin & 0xFFFF; } // 工具函数:将数值转换为十六进制字符串 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 9fee58b..4a6ee41 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 @@ -26,9 +26,6 @@ import java.util.UUID */ class BluetoothManager(private val context: Context) { - // PPTM命令编码器 - private val pptmEncoder = PPTMCommandEncoder() - companion object { private const val TAG = "BluetoothManager" @@ -44,22 +41,14 @@ class BluetoothManager(private val context: Context) { // 功能码定义 const val FUNC_QUERY_DEVICE_INFO = 0x0000 // 查询设备信息 - const val FUNC_START_COLLECTION = 0x0001 // 开启采集 - const val FUNC_QUERY_BATTERY = 0x0002 // 查询电量 - const val FUNC_SET_SAMPLE_RATE = 0x0003 // 设置采样率 - const val FUNC_SET_GAIN = 0x0004 // 设置增益 - const val FUNC_SET_FILTER = 0x0005 // 设置滤波器 - const val FUNC_SET_LEAD = 0x0006 // 设置导联 - const val FUNC_SET_ALARM = 0x0007 // 设置报警 - const val FUNC_SET_TIME = 0x0008 // 设置时间 - const val FUNC_SET_DATE = 0x0009 // 设置日期 - const val FUNC_POWER_LINE_FILTER = 0x000A // 工频滤波 - const val FUNC_QUERY_STATUS = 0x000B // 查询状态 - const val FUNC_RESET_DEVICE = 0x000C // 设备复位 - const val FUNC_UPDATE_FIRMWARE = 0x000D // 固件升级 - const val FUNC_DATA_STREAM = 0x8000 // 数据流 - const val FUNC_ALARM_DATA = 0x8001 // 报警数据 - const val FUNC_STATUS_REPORT = 0x8002 // 状态报告 + const val FUNC_START_COLLECTION = 0x0001 // 采集使能开关(带时间戳) + const val FUNC_QUERY_BATTERY = 0x0002 // 电量查询 + const val FUNC_STIM_SWITCH = 0x0003 // 电刺激开关 + const val FUNC_POWER_LINE_FILTER = 0x000A // 工频滤波开关 + const val FUNC_SYNC_TIMESTAMP = 0x0080 // 同步时间戳(64bit毫秒) + const val FUNC_DATA_STREAM = 0x8000 // 数据上传 + const val FUNC_ALARM_DATA = 0x8001 // 设备状态上报 + const val FUNC_STATUS_REPORT = 0x8002 // 电量上报 // 数据包类型 const val PACKET_TYPE_COMMAND = 0x00 // 命令包 @@ -75,31 +64,7 @@ class BluetoothManager(private val context: Context) { const val POWER_LINE_FILTER_OFF = 0x00 // 关闭工频滤波 const val POWER_LINE_FILTER_ON = 0x01 // 开启工频滤波 - // 采样率 - const val SAMPLE_RATE_125 = 125 // 125Hz - const val SAMPLE_RATE_250 = 250 // 250Hz - const val SAMPLE_RATE_500 = 500 // 500Hz - const val SAMPLE_RATE_1000 = 1000 // 1000Hz - - // 增益设置 - const val GAIN_1 = 1 // 1倍增益 - const val GAIN_2 = 2 // 2倍增益 - const val GAIN_4 = 4 // 4倍增益 - const val GAIN_8 = 8 // 8倍增益 - - // 导联设置 - const val LEAD_I = 0x01 // I导联 - const val LEAD_II = 0x02 // II导联 - const val LEAD_III = 0x03 // III导联 - const val LEAD_AVR = 0x04 // aVR导联 - const val LEAD_AVL = 0x05 // aVL导联 - const val LEAD_AVF = 0x06 // aVF导联 - const val LEAD_V1 = 0x07 // V1导联 - const val LEAD_V2 = 0x08 // V2导联 - const val LEAD_V3 = 0x09 // V3导联 - const val LEAD_V4 = 0x0A // V4导联 - const val LEAD_V5 = 0x0B // V5导联 - const val LEAD_V6 = 0x0C // V6导联 + // 已移除:采样率、增益、导联设置常量(按PDF协议未定义) // 响应状态码 const val RESPONSE_SUCCESS = 0x00 // 成功 @@ -156,9 +121,7 @@ class BluetoothManager(private val context: Context) { // 协议状态 private var deviceInfo: DeviceInfo? = null private var isCollecting = false - private var currentSampleRate = Protocol.SAMPLE_RATE_500 - private var currentGain = Protocol.GAIN_1 - private var currentLead = Protocol.LEAD_II + // 已移除:采样率、增益、导联状态变量(按PDF协议未定义) private var powerLineFilterEnabled = false // 数据统计 @@ -908,101 +871,70 @@ class BluetoothManager(private val context: Context) { } /** - * 查询设备信息 - * 功能码: 0x0000 + * 电刺激开关 + * 功能码: 0x0003 + * 数据格式: [开关及类型(1字节)] + * 数据长度字段表示整个包的长度(7字节) */ - fun queryDeviceInfo() { + fun stimSwitch(stimType: Int, enable: Boolean) { try { - val packet = buildProtocolPacket(Protocol.FUNC_QUERY_DEVICE_INFO) - Log.d(TAG, "发送查询设备信息指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}") + 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() + + 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}") } } - + /** - * 查询电量 - * 功能码: 0x0002 + * 同步时间戳 + * 功能码: 0x0080 + * 数据格式: [时间戳(8字节, 小端, 毫秒)] */ - fun queryBattery() { + fun syncTimestamp(timestampMs: Long) { try { - val packet = buildProtocolPacket(Protocol.FUNC_QUERY_BATTERY) - Log.d(TAG, "发送查询电量指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}") + val data = ByteArray(8) + for (i in 0..7) { + data[i] = ((timestampMs shr (i * 8)) and 0xFF).toByte() + } + val packet = buildProtocolPacket(Protocol.FUNC_SYNC_TIMESTAMP, 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}") } } - + /** * 设置采样率 * 功能码: 0x0003 * 数据格式: [采样率(2字节)] */ - fun setSampleRate(sampleRate: Int) { - try { - val data = ByteArray(2) - data[0] = (sampleRate and 0xFF).toByte() - data[1] = ((sampleRate shr 8) and 0xFF).toByte() - - val packet = buildProtocolPacket(Protocol.FUNC_SET_SAMPLE_RATE, data) - Log.d(TAG, "发送设置采样率指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}") - sendCommand(packet) - - currentSampleRate = sampleRate - - } catch (e: Exception) { - Log.e(TAG, "构建设置采样率指令失败: ${e.message}") - callback?.onCommandSent(false, "构建设置采样率指令失败: ${e.message}") - } - } + // 已移除:设置采样率(按PDF未列出) /** * 设置增益 * 功能码: 0x0004 * 数据格式: [增益(1字节)] */ - fun setGain(gain: Int) { - try { - val data = ByteArray(1) - data[0] = gain.toByte() - - val packet = buildProtocolPacket(Protocol.FUNC_SET_GAIN, data) - Log.d(TAG, "发送设置增益指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}") - sendCommand(packet) - - currentGain = gain - - } catch (e: Exception) { - Log.e(TAG, "构建设置增益指令失败: ${e.message}") - callback?.onCommandSent(false, "构建设置增益指令失败: ${e.message}") - } - } + // 已移除:设置增益(按PDF未列出) /** * 设置导联 * 功能码: 0x0006 * 数据格式: [导联(1字节)] */ - fun setLead(lead: Int) { - try { - val data = ByteArray(1) - data[0] = lead.toByte() - - val packet = buildProtocolPacket(Protocol.FUNC_SET_LEAD, data) - Log.d(TAG, "发送设置导联指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}") - sendCommand(packet) - - currentLead = lead - - } catch (e: Exception) { - Log.e(TAG, "构建设置导联指令失败: ${e.message}") - callback?.onCommandSent(false, "构建设置导联指令失败: ${e.message}") - } - } + // 已移除:设置导联(按PDF未列出) /** * 工频滤波开关 @@ -1030,31 +962,13 @@ class BluetoothManager(private val context: Context) { * 查询状态 * 功能码: 0x000B */ - fun queryStatus() { - try { - val packet = buildProtocolPacket(Protocol.FUNC_QUERY_STATUS) - Log.d(TAG, "发送查询状态指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}") - sendCommand(packet) - } catch (e: Exception) { - Log.e(TAG, "构建查询状态指令失败: ${e.message}") - callback?.onCommandSent(false, "构建查询状态指令失败: ${e.message}") - } - } + // 已移除:查询状态(按PDF未列出) /** * 设备复位 * 功能码: 0x000C */ - fun resetDevice() { - try { - val packet = buildProtocolPacket(Protocol.FUNC_RESET_DEVICE) - Log.d(TAG, "发送设备复位指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}") - sendCommand(packet) - } catch (e: Exception) { - Log.e(TAG, "构建设备复位指令失败: ${e.message}") - callback?.onCommandSent(false, "构建设备复位指令失败: ${e.message}") - } - } + // 已移除:设备复位(按PDF未列出) /** * 获取协议统计信息 @@ -1066,9 +980,7 @@ class BluetoothManager(private val context: Context) { "totalPacketsReceived" to totalPacketsReceived, "totalBytesReceived" to totalBytesReceived, "isCollecting" to isCollecting, - "currentSampleRate" to currentSampleRate, - "currentGain" to currentGain, - "currentLead" to currentLead, + // 已移除:采样率、增益、导联状态(按PDF协议未定义) "powerLineFilterEnabled" to powerLineFilterEnabled, "deviceInfo" to deviceInfo ) @@ -1164,21 +1076,21 @@ class BluetoothManager(private val context: Context) { * 计算CRC16-CCITT-FALSE校验 */ private fun calculateCRC16(data: ByteArray, offset: Int, length: Int): Int { - var crc = 0xFFFF + var wCRCin = 0xFFFF + val wCPoly = 0x1021 for (i in offset until offset + length) { - crc = crc xor (data[i].toInt() and 0xFF) for (j in 0..7) { - if ((crc and 0x0001) != 0) { - crc = crc shr 1 - crc = crc xor 0x8408 - } else { - crc = crc shr 1 + val bit = ((data[i].toInt() shr (7 - j)) and 1) == 1 + val c15 = ((wCRCin shr 15) and 1) == 1 + wCRCin = wCRCin shl 1 + if (c15 xor bit) { + wCRCin = wCRCin xor wCPoly } } } - return crc + return wCRCin and 0xFFFF } /** @@ -1691,80 +1603,4 @@ class BluetoothManager(private val context: Context) { isCleaningUp = true } } - - // ==================== PPTM设备命令方法 ==================== - - /** - * 发送PPTM开始采样命令 - * 使用PPTM协议编码器生成带CRC16校验的命令 - */ - fun sendPptmStartSampleCmd() { - try { - val command = pptmEncoder.createPptmStartSampleCmd() - Log.d(TAG, "发送PPTM开始采样命令: ${command.joinToString(", ") { "0x%02X".format(it) }}") - sendCommand(command) - callback?.onStatusChanged("📤 已发送PPTM开始采样命令") - } catch (e: Exception) { - Log.e(TAG, "发送PPTM开始采样命令失败: ${e.message}") - callback?.onCommandSent(false, "发送PPTM开始采样命令失败: ${e.message}") - } - } - - /** - * 发送PPTM停止采样命令 - * 使用PPTM协议编码器生成带CRC16校验的命令 - */ - fun sendPptmStopSampleCmd() { - try { - val command = pptmEncoder.createPptmStopSampleCmd() - Log.d(TAG, "发送PPTM停止采样命令: ${command.joinToString(", ") { "0x%02X".format(it) }}") - sendCommand(command) - callback?.onStatusChanged("📤 已发送PPTM停止采样命令") - } catch (e: Exception) { - Log.e(TAG, "发送PPTM停止采样命令失败: ${e.message}") - callback?.onCommandSent(false, "发送PPTM停止采样命令失败: ${e.message}") - } - } - - /** - * 发送自定义PPTM命令 - * @param opcode 操作码 - * @param payload 负载数据 - */ - fun sendCustomPptmCmd(opcode: Short, payload: ByteArray) { - try { - val command = pptmEncoder.createCustomPptmCmd(opcode, payload) - Log.d(TAG, "发送自定义PPTM命令: 操作码=0x%04X, 负载=${payload.joinToString(", ") { "0x%02X".format(it) }}".format(opcode)) - sendCommand(command) - callback?.onStatusChanged("📤 已发送自定义PPTM命令: 操作码=0x%04X".format(opcode)) - } catch (e: Exception) { - Log.e(TAG, "发送自定义PPTM命令失败: ${e.message}") - callback?.onCommandSent(false, "发送自定义PPTM命令失败: ${e.message}") - } - } - - /** - * 验证PPTM数据包CRC16校验 - * @param packet 完整数据包 - * @return 校验是否通过 - */ - fun verifyPptmPacket(packet: ByteArray): Boolean { - return pptmEncoder.verifyCrc16(packet) - } - - /** - * 解析PPTM数据包 - * @param packet 完整数据包 - * @return 解析结果 (操作码, 负载数据) 或 null - */ - fun parsePptmPacket(packet: ByteArray): Pair? { - return pptmEncoder.parsePacket(packet) - } - - /** - * 获取PPTM编码器实例(用于调试) - */ - fun getPptmEncoder(): PPTMCommandEncoder { - return pptmEncoder - } } diff --git a/app/src/main/java/com/example/cmake_project_test/CompletePipelineExample.kt b/app/src/main/java/com/example/cmake_project_test/CompletePipelineExample.kt deleted file mode 100644 index 992985c..0000000 --- a/app/src/main/java/com/example/cmake_project_test/CompletePipelineExample.kt +++ /dev/null @@ -1,183 +0,0 @@ -package com.example.cmake_project_test - -import android.util.Log -import type.SensorData - -/** - * 完整数据处理管道示例 - * 演示从原始数据到最终指标的完整流程 - */ -class CompletePipelineExample { - - private val dataMapper = DataMapper() - private val indicatorCalculator = IndicatorCalculator() - private val signalProcessor = SignalProcessorJNI() - - private var dataMapperInitialized = false - private var indicatorCalculatorInitialized = false - private var signalProcessorInitialized = false - - /** - * 初始化完整管道 - */ - fun initialize(): Boolean { - Log.d("CompletePipeline", "初始化完整数据处理管道...") - - // 初始化数据映射器 - dataMapperInitialized = dataMapper.initialize() - if (!dataMapperInitialized) { - Log.e("CompletePipeline", "数据映射器初始化失败") - return false - } - - // 初始化指标计算器 - indicatorCalculatorInitialized = indicatorCalculator.initialize() - if (!indicatorCalculatorInitialized) { - Log.e("CompletePipeline", "指标计算器初始化失败") - cleanup() - return false - } - - // 初始化信号处理器 - try { - signalProcessorInitialized = signalProcessor.createProcessor() - if (!signalProcessorInitialized) { - Log.e("CompletePipeline", "信号处理器初始化失败") - cleanup() - return false - } - } catch (e: Exception) { - Log.e("CompletePipeline", "信号处理器初始化异常: ${e.message}") - cleanup() - return false - } - - Log.d("CompletePipeline", "完整数据处理管道初始化成功") - return true - } - - /** - * 处理单个数据包的完整流程 - * 原始数据 → 解析数据 → 通道映射 → 信号处理 → 指标计算 - */ - fun processCompletePipeline(rawData: SensorData, sampleRate: Float = 250.0f): Map? { - if (!isInitialized()) { - Log.w("CompletePipeline", "管道未初始化") - return null - } - - Log.d("CompletePipeline", "开始处理完整数据管道...") - Log.d("CompletePipeline", "输入数据类型: ${rawData.dataType}, 采样率: $sampleRate Hz") - - try { - // 步骤1: 通道映射 - Log.d("CompletePipeline", "步骤1: 执行通道映射...") - val mappedData = dataMapper.mapSensorData(rawData) - if (mappedData == null) { - Log.e("CompletePipeline", "通道映射失败") - return null - } - Log.d("CompletePipeline", "通道映射完成") - - // 步骤2: 信号处理 (这里可以添加信号处理逻辑) - Log.d("CompletePipeline", "步骤2: 执行信号处理...") - // 暂时跳过信号处理,直接使用映射后的数据 - val processedData = mappedData - Log.d("CompletePipeline", "信号处理完成") - - // 步骤3: 指标计算 - Log.d("CompletePipeline", "步骤3: 执行指标计算...") - val metrics = indicatorCalculator.processCompletePipeline(processedData, sampleRate) - if (metrics == null) { - Log.e("CompletePipeline", "指标计算失败") - return null - } - - Log.d("CompletePipeline", "指标计算完成,计算了 ${metrics.size} 个指标") - Log.d("CompletePipeline", "完整数据处理管道执行成功") - - // 打印主要指标 - metrics["heart_rate"]?.let { - Log.d("CompletePipeline", "心率: ${String.format("%.1f", it)} bpm") - } - metrics["signal_quality"]?.let { - Log.d("CompletePipeline", "信号质量: ${String.format("%.2f", it)}") - } - metrics["spo2"]?.let { - Log.d("CompletePipeline", "血氧饱和度: ${String.format("%.1f", it)}%") - } - - return metrics - - } catch (e: Exception) { - Log.e("CompletePipeline", "完整数据处理管道异常: ${e.message}") - return null - } - } - - /** - * 批量处理数据包 - */ - fun processBatchPipeline(rawDataList: List, sampleRate: Float = 250.0f): List> { - if (!isInitialized()) { - Log.w("CompletePipeline", "管道未初始化") - return emptyList() - } - - Log.d("CompletePipeline", "开始批量处理 ${rawDataList.size} 个数据包...") - - val results = mutableListOf>() - var successCount = 0 - - for ((index, rawData) in rawDataList.withIndex()) { - val metrics = processCompletePipeline(rawData, sampleRate) - if (metrics != null) { - results.add(metrics) - successCount++ - } else { - results.add(emptyMap()) - } - - // 每处理100个数据包打印一次进度 - if ((index + 1) % 100 == 0) { - Log.d("CompletePipeline", "已处理 ${index + 1}/${rawDataList.size} 个数据包") - } - } - - Log.d("CompletePipeline", "批量处理完成: 成功 $successCount/${rawDataList.size} 个数据包") - return results - } - - /** - * 检查管道是否已初始化 - */ - fun isInitialized(): Boolean { - return dataMapperInitialized && indicatorCalculatorInitialized && signalProcessorInitialized - } - - /** - * 清理资源 - */ - fun cleanup() { - try { - if (dataMapperInitialized) { - // 使用公共方法或直接设置为false - dataMapperInitialized = false - } - - if (indicatorCalculatorInitialized) { - // 使用公共方法或直接设置为false - indicatorCalculatorInitialized = false - } - - if (signalProcessorInitialized) { - signalProcessor.destroyProcessor() - signalProcessorInitialized = false - } - - Log.d("CompletePipelineExample", "资源清理完成") - } catch (e: Exception) { - Log.e("CompletePipelineExample", "资源清理异常: ${e.message}") - } - } -} diff --git a/app/src/main/java/com/example/cmake_project_test/Constants.kt b/app/src/main/java/com/example/cmake_project_test/Constants.kt deleted file mode 100644 index 28938cf..0000000 --- a/app/src/main/java/com/example/cmake_project_test/Constants.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.cmake_project_test - -object Constants { - // UI更新相关 - const val UPDATE_INTERVAL = 200L // 优化:每200毫秒更新一次UI,减慢更新速度以便观察心电波形 - - // 数据分块相关 - const val CHUNK_SIZE = 64 // 数据分块大小 - - // 缓冲区管理相关 - const val BUFFER_CLEANUP_THRESHOLD = 50 // 缓冲区清理阈值 - const val BUFFER_KEEP_COUNT = 30 // 缓冲区保留数量 - - // 显示限制相关 - const val MAX_DISPLAY_PACKETS = 5 // 最大显示数据包数量 - const val MAX_DETAIL_PACKETS = 3 // 最大详情数据包数量 - const val MAX_DISPLAY_CHANNELS = 4 // 最大显示通道数量 - const val MAX_12LEAD_CHANNELS = 6 // 12导联心电最大通道数量 - const val MAX_DISPLAY_SAMPLES = 750 // 最大显示采样点数量,调整为显示约3秒数据(基于250Hz采样率) - - // 文件相关 - const val DEFAULT_DATA_FILE = "ecg_data_raw.dat" // 默认数据文件名 - - // 延迟相关 - const val SIMULATION_DELAY = 1L // 模拟传输延迟(毫秒)- 减少延迟 -} diff --git a/app/src/main/java/com/example/cmake_project_test/DataManager.kt b/app/src/main/java/com/example/cmake_project_test/DataManager.kt index acc5483..bfa8e8b 100644 --- a/app/src/main/java/com/example/cmake_project_test/DataManager.kt +++ b/app/src/main/java/com/example/cmake_project_test/DataManager.kt @@ -7,6 +7,17 @@ import java.io.IOException import java.nio.ByteBuffer import java.nio.ByteOrder +/** + * 原生方法回调接口 + * 用于DataManager调用MainActivity中的原生方法 + */ +interface NativeMethodCallback { + fun createStreamParser(): Long + fun destroyStreamParser(handle: Long) + fun streamParserAppend(handle: Long, chunk: ByteArray) + fun streamParserDrainPackets(handle: Long): List? +} + /** * 数据管理类 * 负责数据解析、缓冲管理和原生方法调用 @@ -165,45 +176,7 @@ class DataManager(private val nativeCallback: NativeMethodCallback) { // 注释掉后台处理,只显示原始数据 // processStreamingData(packets) } else { - Log.w("DataManager", "没有解析出有效数据包,尝试生成测试数据") - - // 如果原生解析器没有解析出数据包,生成一些测试数据来验证图表显示 - generateTestDataForChart() - } - } - - /** - * 生成测试数据用于验证图表显示 - */ - private fun generateTestDataForChart() { - try { - Log.d("DataManager", "生成测试数据用于验证图表显示") - - // 生成模拟的单导联ECG数据 - val testData = mutableListOf() - val sampleCount = 100 // 生成100个样本点 - - for (i in 0 until sampleCount) { - val t = i.toFloat() / 10f // 时间参数 - val value = (Math.sin(t * 2 * Math.PI) * 100 + - Math.sin(t * 4 * Math.PI) * 50 + - Math.sin(t * 8 * Math.PI) * 25).toFloat() - testData.add(value) - } - - Log.d("DataManager", "生成测试数据: ${testData.size} 个样本点") - Log.d("DataManager", "测试数据前5个值: ${testData.take(5).joinToString(", ")}") - - // 发送测试数据到图表 - if (realTimeCallback != null) { - realTimeCallback!!.onRawDataAvailable(0, testData) - Log.d("DataManager", "已发送测试数据到图表") - } else { - Log.e("DataManager", "realTimeCallback 为空,无法发送测试数据") - } - - } catch (e: Exception) { - Log.e("DataManager", "生成测试数据失败: ${e.message}", e) + Log.w("DataManager", "没有解析出有效数据包") } } @@ -649,48 +622,17 @@ class DataManager(private val nativeCallback: NativeMethodCallback) { /** * 处理文件数据 */ - fun processFileData(fileData: ByteArray, progressCallback: ((Int) -> Unit)? = null) { - Log.d("DataManager", "开始处理文件数据,数据大小: ${fileData.size} 字节") - - try { - val chunkSize = 1024 - var processedBytes = 0 - - for (i in fileData.indices step chunkSize) { - val endIndex = minOf(i + chunkSize, fileData.size) - val chunk = fileData.copyOfRange(i, endIndex) - - onBleNotify(chunk) - processedBytes += chunk.size - - // 性能优化:减少进度更新频率 - if (processedBytes % (chunkSize * 10) == 0) { - val progress = (processedBytes * 100 / fileData.size).coerceAtMost(100) - progressCallback?.invoke(progress) - } - } - - Log.d("DataManager", "文件数据处理完成,总共处理了 $processedBytes 字节") - - } catch (e: Exception) { - Log.e("DataManager", "处理文件数据时发生错误: ${e.message}", e) - } - } + // Getter方法 fun getPacketBufferSize(): Int = packetBuffer.size - fun getProcessedPacketsSize(): Int = processedPackets.size - fun getCalculatedMetricsSize(): Int = calculatedMetrics.size fun getRawStreamSize(): Int = rawStream.size() fun getTotalPacketsParsed(): Long = totalPacketsParsed fun getPacketBuffer(): List = packetBuffer fun getProcessedPackets(): List = processedPackets fun getCalculatedMetrics(): List> = calculatedMetrics - fun getLatestMetrics(): Map = latestMetrics - fun getChannelBuffersStatus(): Map = channelBuffers.mapValues { it.value.size } // 流式处理相关getter方法 - fun getProcessedChannelBuffersStatus(): Map = processedChannelBuffers.mapValues { it.value.size } fun getProcessedChannelData(channelIndex: Int): List = processedChannelBuffers[channelIndex] ?: emptyList() fun getTotalProcessedSamples(): Long = totalProcessedSamples fun getCurrentDataType(): type.SensorData.DataType? = currentDataType @@ -716,168 +658,9 @@ class DataManager(private val nativeCallback: NativeMethodCallback) { } } - /** - * 计算信号质量 - */ - fun calculateSignalQuality(packet: SensorData): Float { - if (!streamingSignalProcessorInitialized || streamingSignalProcessor == null) return 0.0f - - try { - // 获取第一个通道的数据进行质量评估 - val channelData = packet.getChannelData() - if (channelData != null && channelData.isNotEmpty()) { - val firstChannel = channelData[0] - if (firstChannel != null) { - val signal = firstChannel.toFloatArray() - val quality = streamingSignalProcessor!!.calculateSignalQuality(signal) - - // 添加调试信息 - Log.d("DataManager", "信号质量计算: 通道0, 数据长度=${signal.size}, 质量=$quality") - return quality - } - } - } catch (e: Exception) { - Log.e("DataManager", "计算信号质量时发生错误: ${e.message}") - } - - return 0.0f - } - /** - * 应用信号处理 - */ - fun applySignalProcessing(packets: List): List { - Log.d("DataManager", "开始应用信号处理,处理 ${packets.size} 个数据包") - - ensureStreamingSignalProcessor() - if (!streamingSignalProcessorInitialized || streamingSignalProcessor == null) { - Log.w("DataManager", "流式信号处理器未初始化,跳过信号处理") - return packets - } - - val processedPackets = mutableListOf() - - for (packet in packets) { - try { - val processedPacket = processSinglePacket(packet) - processedPackets.add(processedPacket) - } catch (e: Exception) { - Log.e("DataManager", "处理数据包时发生错误: ${e.message}") - processedPackets.add(packet) // 处理失败时返回原始数据包 - } - } - - Log.d("DataManager", "信号处理完成,处理了 ${processedPackets.size} 个数据包") - return processedPackets - } - - /** - * 处理单个数据包 - */ - private fun processSinglePacket(packet: SensorData): SensorData { - val channelData = packet.getChannelData() - if (channelData == null || channelData.isEmpty()) { - return packet - } - - val processedChannels = mutableListOf>() - - for (channel in channelData) { - val processedChannel = when (packet.getDataType()) { - type.SensorData.DataType.EEG -> applyEEGFilters(channel) - type.SensorData.DataType.ECG_2LEAD, type.SensorData.DataType.ECG_12LEAD -> applyECGFilters(channel) - type.SensorData.DataType.PPG -> channel // PPG不进行滤波处理,直接返回原始数据 - else -> channel // 其他类型暂时不处理 - } - processedChannels.add(processedChannel) - } - - // 创建处理后的数据包 - val processedPacket = SensorData() - processedPacket.setDataType(packet.getDataType()) - processedPacket.setTimestamp(packet.getTimestamp()) - processedPacket.setPacketSn(packet.getPacketSn()) - processedPacket.setChannelData(processedChannels) - - return processedPacket - } - - /** - * 应用EEG滤波器 - */ - private fun applyEEGFilters(channel: List): List { - val signal = channel.toFloatArray() - - try { - if (streamingSignalProcessor != null) { - // 1. 带通滤波 (1-40Hz,EEG主要频率范围) - val bandpassFiltered = streamingSignalProcessor!!.bandpassFilter( - signal, - filterSettings.sampleRate.toFloat(), - 1.0f, - 40.0f - ) - - if (bandpassFiltered != null) { - // 2. 幅度归一化 - val normalized = streamingSignalProcessor!!.normalizeAmplitude(bandpassFiltered) - - if (normalized != null) { - return normalized.toList() - } - } - } - } catch (e: Exception) { - Log.e("DataManager", "EEG滤波处理失败: ${e.message}") - } - - return channel // 处理失败时返回原始数据 - } - - /** - * 应用ECG滤波器 - */ - private fun applyECGFilters(channel: List): List { - val signal = channel.toFloatArray() - - try { - if (streamingSignalProcessor != null) { - // 1. 高通滤波 (0.5Hz,去除基线漂移) - val highpassFiltered = streamingSignalProcessor!!.highpassFilter( - signal, - filterSettings.sampleRate.toFloat(), - 0.5f - ) - - if (highpassFiltered != null) { - // 2. 低通滤波 (40Hz,去除高频噪声) - val lowpassFiltered = streamingSignalProcessor!!.lowpassFilter( - highpassFiltered, - filterSettings.sampleRate.toFloat(), - 40.0f - ) - - if (lowpassFiltered != null) { - // 3. 陷波滤波 (50Hz,去除工频干扰) - val notchFiltered = streamingSignalProcessor!!.notchFilter( - lowpassFiltered, - filterSettings.sampleRate.toFloat(), - 50.0f, - 10.0f - ) - - if (notchFiltered != null) { - return notchFiltered.toList() - } - } - } - } - } catch (e: Exception) { - Log.e("DataManager", "ECG滤波处理失败: ${e.message}") - } - - return channel // 处理失败时返回原始数据 - } + + /** * 清理缓冲区 @@ -982,39 +765,6 @@ class DataManager(private val nativeCallback: NativeMethodCallback) { - /** - * 应用PPG滤波器 - */ - private fun applyPPGFilters(channel: List): List { - val signal = channel.toFloatArray() - - try { - if (streamingSignalProcessor != null) { - // 1. 低通滤波 (8Hz,PPG主要频率范围) - val filtered = streamingSignalProcessor!!.lowpassFilter( - signal, - filterSettings.sampleRate.toFloat(), - 8.0f - ) - - if (filtered != null) { - // 2. 去运动伪影 - val motionArtifactRemoved = streamingSignalProcessor!!.processRealtimeChunk( - filtered, - filterSettings.sampleRate.toFloat() - ) - - if (motionArtifactRemoved != null) { - return motionArtifactRemoved.toList() - } - } - } - } catch (e: Exception) { - Log.e("DataManager", "PPG滤波处理失败: ${e.message}") - } - - return channel // 处理失败时返回原始数据 - } /** * 设置滤波器参数 diff --git a/app/src/main/java/com/example/cmake_project_test/DeviceTypeHelper.kt b/app/src/main/java/com/example/cmake_project_test/DeviceTypeHelper.kt deleted file mode 100644 index a210b68..0000000 --- a/app/src/main/java/com/example/cmake_project_test/DeviceTypeHelper.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.example.cmake_project_test - -import type.SensorData - -/** - * 设备类型帮助类 - * 负责设备类型名称映射和通道数据详情构建 - */ -object DeviceTypeHelper { - - /** - * 根据数据类型获取设备名称 - */ - fun getDeviceName(dataType: SensorData.DataType): String { - return when (dataType) { - SensorData.DataType.EEG -> "脑电设备" - SensorData.DataType.ECG_2LEAD -> "胸腹设备" - SensorData.DataType.PPG -> "血氧设备" - SensorData.DataType.ECG_12LEAD -> "12导联心电" - SensorData.DataType.STETHOSCOPE -> "数字听诊" - SensorData.DataType.SNORE -> "鼾声设备" - SensorData.DataType.RESPIRATION -> "呼吸/姿态" - SensorData.DataType.MIT_BIH -> "MIT-BIH" - else -> "未知设备" - } - } - - /** - * 构建通道数据详情字符串 - */ - fun buildChannelDetails( - data: List, - maxPackets: Int = Constants.MAX_DETAIL_PACKETS, - maxChannels: Int = Constants.MAX_DISPLAY_CHANNELS, - maxSamples: Int = Constants.MAX_DISPLAY_SAMPLES - ): String { - if (data.isEmpty()) { - return "无通道数据" - } - - val details = mutableListOf() - - data.take(maxPackets).forEachIndexed { packetIndex, sensorData -> - if (sensorData.channelData.isNullOrEmpty()) { - details.add("数据包 ${packetIndex + 1}: 无通道数据") - return@forEachIndexed - } - - details.add("数据包 ${packetIndex + 1} (${getDeviceName(sensorData.dataType ?: SensorData.DataType.EEG)}):") - - sensorData.channelData.take(maxChannels).forEachIndexed { channelIndex, channel -> - if (channel.isNullOrEmpty()) { - details.add(" 通道 ${channelIndex + 1}: 空数据") - return@forEachIndexed - } - - // 只显示前几个采样点 - val sampleCount = minOf(maxSamples, channel.size) - val channelDataStr = channel.take(sampleCount).joinToString(", ") { "%.1f".format(it) } - - details.add(" 通道 ${channelIndex + 1}: ${sampleCount}/${channel.size} 采样点") - details.add(" 示例: $channelDataStr${if (channel.size > sampleCount) "..." else ""}") - } - - if (sensorData.channelData.size > maxChannels) { - details.add(" ... 还有 ${sensorData.channelData.size - maxChannels} 个通道") - } - - // 添加分隔线 - details.add("") - } - - if (data.size > maxPackets) { - details.add("... 还有 ${data.size - maxPackets} 个数据包") - } - - return details.joinToString("\n") - } -} diff --git a/app/src/main/java/com/example/cmake_project_test/ECGChartView.kt b/app/src/main/java/com/example/cmake_project_test/ECGChartView.kt deleted file mode 100644 index 7ffe613..0000000 --- a/app/src/main/java/com/example/cmake_project_test/ECGChartView.kt +++ /dev/null @@ -1,414 +0,0 @@ -package com.example.cmake_project_test - -import android.content.Context -import android.graphics.* -import android.util.AttributeSet -import android.view.GestureDetector -import android.view.MotionEvent -import android.view.View -import kotlin.math.max -import kotlin.math.min - -class ECGChartView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : View(context, attrs, defStyleAttr) { - - private val paint = Paint().apply { - color = Color.BLUE - strokeWidth = 2f - style = Paint.Style.STROKE - isAntiAlias = true - } - - private val gridPaint = Paint().apply { - color = Color.LTGRAY - strokeWidth = 1f - style = Paint.Style.STROKE - alpha = 100 - } - - private val textPaint = Paint().apply { - color = Color.BLACK - textSize = 30f - isAntiAlias = true - } - - private val buttonPaint = Paint().apply { - color = Color.GRAY - style = Paint.Style.FILL - alpha = 150 - } - - private val buttonTextPaint = Paint().apply { - color = Color.WHITE - textSize = 20f - isAntiAlias = true - textAlign = Paint.Align.CENTER - } - - private val path = Path() - private var dataPoints = mutableListOf() - private var maxDataPoints = 1000 // 优化:显示更多数据点,提供更好的可视化效果 - private var minValue = Float.MAX_VALUE - private var maxValue = Float.MIN_VALUE - - // 缩放控制参数 - private var scaleX = 1.0f // X轴缩放因子 - private var scaleY = 1.0f // Y轴缩放因子 - private var offsetX = 0f // X轴偏移 - private var offsetY = 0f // Y轴偏移 - - // 自动居中控制 - private var autoCenter = true // 自动居中模式 - private var lockToCenter = true // 锁定到屏幕中央 - - // 手势检测器 - private val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { - override fun onDoubleTap(e: MotionEvent): Boolean { - // 双击重置缩放并锁定到中央 - resetZoom() - lockToCenter = true - invalidate() - return true - } - - override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean { - // 只有在非锁定模式下才允许平移 - if (!lockToCenter) { - offsetX -= distanceX - offsetY -= distanceY - invalidate() - } - return true - } - }) - - // 按钮区域 - private val buttonSize = 60f - private val buttonMargin = 20f - - // 缩放按钮区域 - private val zoomInXButtonRect = RectF() - private val zoomOutXButtonRect = RectF() - private val zoomInYButtonRect = RectF() - private val zoomOutYButtonRect = RectF() - private val resetButtonRect = RectF() - private val lockButtonRect = RectF() - - fun updateData(newData: List) { - // 快速累积数据 - dataPoints.addAll(newData) - - // 限制数据点数量(避免内存溢出) - if (dataPoints.size > maxDataPoints) { - dataPoints = dataPoints.takeLast(maxDataPoints).toMutableList() - } - - // 快速计算数据范围(减少计算量) - if (dataPoints.isNotEmpty()) { - // 只计算最近的数据范围,提高性能 - val recentData = dataPoints.takeLast(minOf(1000, dataPoints.size)) - minValue = recentData.minOrNull() ?: 0f - maxValue = recentData.maxOrNull() ?: 0f - - // 确保有足够的显示范围 - val range = maxValue - minValue - if (range < 0.1f) { - val center = (maxValue + minValue) / 2 - minValue = center - 0.05f - maxValue = center + 0.05f - } else { - // 添加10%的上下边距(减少边距,提高响应速度) - val margin = range * 0.1f - minValue -= margin - maxValue += margin - } - } - - // 如果启用自动居中,重置偏移 - if (autoCenter) { - offsetX = 0f - offsetY = 0f - } - - // 立即重绘 - invalidate() - } - - fun clearData() { - dataPoints.clear() - invalidate() - } - - // 缩放控制方法 - fun zoomInX() { - scaleX = min(scaleX * 1.2f, 5.0f) - invalidate() - } - - fun zoomOutX() { - scaleX = max(scaleX / 1.2f, 0.2f) - invalidate() - } - - fun zoomInY() { - scaleY = min(scaleY * 1.2f, 5.0f) - invalidate() - } - - fun zoomOutY() { - scaleY = max(scaleY / 1.2f, 0.2f) - invalidate() - } - - fun resetZoom() { - scaleX = 1.0f - scaleY = 1.0f - offsetX = 0f - offsetY = 0f - lockToCenter = true - invalidate() - } - - // 切换锁定模式 - fun toggleLockMode() { - lockToCenter = !lockToCenter - if (lockToCenter) { - offsetX = 0f - offsetY = 0f - } - invalidate() - } - - // 获取锁定状态 - fun isLockedToCenter(): Boolean = lockToCenter - - override fun onTouchEvent(event: MotionEvent): Boolean { - when (event.action) { - MotionEvent.ACTION_DOWN -> { - // 检查是否点击了按钮 - val x = event.x - val y = event.y - - if (zoomInXButtonRect.contains(x, y)) { - zoomInX() - return true - } else if (zoomOutXButtonRect.contains(x, y)) { - zoomOutX() - return true - } else if (zoomInYButtonRect.contains(x, y)) { - zoomInY() - return true - } else if (zoomOutYButtonRect.contains(x, y)) { - zoomOutY() - return true - } else if (resetButtonRect.contains(x, y)) { - resetZoom() - return true - } else if (lockButtonRect.contains(x, y)) { - toggleLockMode() - return true - } - } - } - - // 处理手势 - return gestureDetector.onTouchEvent(event) || super.onTouchEvent(event) - } - - override fun onDraw(canvas: Canvas) { - super.onDraw(canvas) - - val width = width.toFloat() - val height = height.toFloat() - - // 更新按钮位置 - updateButtonPositions(width, height) - - // 绘制网格 - drawGrid(canvas, width, height) - - // 绘制标题 - canvas.drawText("ECG Channel 1 (滤波后)", 20f, 40f, textPaint) - - // 绘制缩放信息 - canvas.drawText("X缩放: ${String.format("%.1f", scaleX)}x", 20f, 70f, textPaint) - canvas.drawText("Y缩放: ${String.format("%.1f", scaleY)}x", 20f, 100f, textPaint) - - // 绘制锁定状态 - val lockStatus = if (lockToCenter) "🔒 已锁定" else "🔓 已解锁" - canvas.drawText(lockStatus, 20f, 130f, textPaint) - - // 绘制数据点数量 - canvas.drawText("数据点: ${dataPoints.size}", 20f, height - 20f, textPaint) - - // 绘制数据范围 - if (dataPoints.isNotEmpty()) { - canvas.drawText("范围: ${String.format("%.3f", minValue)} - ${String.format("%.3f", maxValue)}", - 20f, height - 50f, textPaint) - } - - // 绘制控制按钮 - drawControlButtons(canvas) - - // 绘制曲线 - if (dataPoints.size > 1) { - drawCurve(canvas, width, height) - } - } - - private fun updateButtonPositions(width: Float, height: Float) { - val buttonY = height - buttonSize - buttonMargin - - // X轴缩放按钮 - zoomInXButtonRect.set( - width - buttonSize * 6 - buttonMargin * 6, - buttonY, - width - buttonSize * 5 - buttonMargin * 5, - buttonY + buttonSize - ) - - zoomOutXButtonRect.set( - width - buttonSize * 5 - buttonMargin * 5, - buttonY, - width - buttonSize * 4 - buttonMargin * 4, - buttonY + buttonSize - ) - - // Y轴缩放按钮 - zoomInYButtonRect.set( - width - buttonSize * 4 - buttonMargin * 4, - buttonY, - width - buttonSize * 3 - buttonMargin * 3, - buttonY + buttonSize - ) - - zoomOutYButtonRect.set( - width - buttonSize * 3 - buttonMargin * 3, - buttonY, - width - buttonSize * 2 - buttonMargin * 2, - buttonY + buttonSize - ) - - // 重置按钮 - resetButtonRect.set( - width - buttonSize * 2 - buttonMargin * 2, - buttonY, - width - buttonSize - buttonMargin, - buttonY + buttonSize - ) - - // 锁定按钮 - lockButtonRect.set( - width - buttonSize - buttonMargin, - buttonY, - width - buttonMargin, - buttonY + buttonSize - ) - } - - private fun drawControlButtons(canvas: Canvas) { - // 绘制X轴缩放按钮 - canvas.drawRoundRect(zoomInXButtonRect, 10f, 10f, buttonPaint) - canvas.drawText("X+", zoomInXButtonRect.centerX(), zoomInXButtonRect.centerY() + 7f, buttonTextPaint) - - canvas.drawRoundRect(zoomOutXButtonRect, 10f, 10f, buttonTextPaint) - canvas.drawText("X-", zoomOutXButtonRect.centerX(), zoomOutXButtonRect.centerY() + 7f, buttonTextPaint) - - // 绘制Y轴缩放按钮 - canvas.drawRoundRect(zoomInYButtonRect, 10f, 10f, buttonPaint) - canvas.drawText("Y+", zoomInYButtonRect.centerX(), zoomInYButtonRect.centerY() + 7f, buttonTextPaint) - - canvas.drawRoundRect(zoomOutYButtonRect, 10f, 10f, buttonPaint) - canvas.drawText("Y-", zoomOutYButtonRect.centerX(), zoomOutYButtonRect.centerY() + 7f, buttonTextPaint) - - // 绘制重置按钮 - canvas.drawRoundRect(resetButtonRect, 10f, 10f, buttonPaint) - canvas.drawText("重置", resetButtonRect.centerX(), resetButtonRect.centerY() + 7f, buttonTextPaint) - - // 绘制锁定按钮 - val lockButtonColor = if (lockToCenter) Color.GREEN else Color.RED - buttonPaint.color = lockButtonColor - canvas.drawRoundRect(lockButtonRect, 10f, 10f, buttonPaint) - canvas.drawText(if (lockToCenter) "🔒" else "🔓", lockButtonRect.centerX(), lockButtonRect.centerY() + 7f, buttonTextPaint) - buttonPaint.color = Color.GRAY // 恢复默认颜色 - } - - private fun drawGrid(canvas: Canvas, width: Float, height: Float) { - // 绘制垂直线 - val verticalSpacing = width / 10 - for (i in 0..10) { - val x = i * verticalSpacing - canvas.drawLine(x, 0f, x, height, gridPaint) - } - - // 绘制水平线 - val horizontalSpacing = height / 8 - for (i in 0..8) { - val y = i * horizontalSpacing - canvas.drawLine(0f, y, width, y, gridPaint) - } - } - - private fun drawCurve(canvas: Canvas, width: Float, height: Float) { - path.reset() - - val padding = 60f - val drawWidth = width - 2 * padding - val drawHeight = height - 2 * padding - - // 应用缩放和偏移 - val scaledWidth = drawWidth * scaleX - val scaledHeight = drawHeight * scaleY - - // 如果锁定到中央,强制偏移为0 - val effectiveOffsetX = if (lockToCenter) 0f else offsetX - val effectiveOffsetY = if (lockToCenter) 0f else offsetY - - val xStep = scaledWidth / (dataPoints.size - 1) - - for (i in dataPoints.indices) { - val x = padding + i * xStep + effectiveOffsetX - val normalizedValue = (dataPoints[i] - minValue) / (maxValue - minValue) - // 使用0.1到0.9的范围,确保曲线在中间80%的区域显示 - val y = padding + (0.1f + normalizedValue * 0.8f) * scaledHeight + effectiveOffsetY - - // 确保点在可见区域内 - if (x >= padding && x <= width - padding && y >= padding && y <= height - padding) { - if (i == 0) { - path.moveTo(x, y) - } else { - path.lineTo(x, y) - } - } - } - - canvas.drawPath(path, paint) - } - - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - val desiredWidth = 800 - val desiredHeight = 400 - - val widthMode = MeasureSpec.getMode(widthMeasureSpec) - val widthSize = MeasureSpec.getSize(widthMeasureSpec) - val heightMode = MeasureSpec.getMode(heightMeasureSpec) - val heightSize = MeasureSpec.getSize(heightMeasureSpec) - - val width = when (widthMode) { - MeasureSpec.EXACTLY -> widthSize - MeasureSpec.AT_MOST -> min(desiredWidth, widthSize) - else -> desiredWidth - } - - val height = when (heightMode) { - MeasureSpec.EXACTLY -> heightSize - MeasureSpec.AT_MOST -> min(desiredHeight, heightSize) - else -> desiredHeight - } - - setMeasuredDimension(width, height) - } -} diff --git a/app/src/main/java/com/example/cmake_project_test/ECGRhythmView.kt b/app/src/main/java/com/example/cmake_project_test/ECGRhythmView.kt deleted file mode 100644 index 6924947..0000000 --- a/app/src/main/java/com/example/cmake_project_test/ECGRhythmView.kt +++ /dev/null @@ -1,367 +0,0 @@ -package com.example.cmake_project_test - -import android.content.Context -import android.graphics.* -import android.util.AttributeSet -import android.util.Log -import android.view.GestureDetector -import android.view.MotionEvent -import android.view.View -import kotlin.math.max -import kotlin.math.min - -/** - * ECG节律视图 - * 显示10秒(2500点)的连续信号,用于评估心率节律 - */ -class ECGRhythmView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : View(context, attrs, defStyleAttr) { - - private val paint = Paint().apply { - color = Color.BLUE - strokeWidth = 1.5f - style = Paint.Style.STROKE - isAntiAlias = false // 关闭抗锯齿,提高绘制性能 - } - - private val gridPaint = Paint().apply { - color = Color.LTGRAY - strokeWidth = 0.5f - style = Paint.Style.STROKE - alpha = 80 - } - - private val textPaint = Paint().apply { - color = Color.BLACK - textSize = 24f - isAntiAlias = true - } - - private val path = Path() - private var dataPoints = mutableListOf() - private var maxDataPoints = 2500 // 10秒数据(250Hz采样率) - private var minValue = Float.MAX_VALUE - private var maxValue = Float.MIN_VALUE - private var isDataAvailable = false // 标记是否有数据 - - // 简化的数据管理:直接显示新数据 - - // 缩放控制参数 - private var scaleX = 1.0f - private var scaleY = 1.0f - private var offsetX = 0f - private var offsetY = 0f - - // 手势检测器 - private val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { - override fun onDoubleTap(e: MotionEvent): Boolean { - resetZoom() - return true - } - - override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean { - offsetX -= distanceX - offsetY -= distanceY - invalidate() - return true - } - }) - - // 缩放手势检测器 - private val scaleGestureDetector = android.view.ScaleGestureDetector(context, - object : android.view.ScaleGestureDetector.SimpleOnScaleGestureListener() { - override fun onScale(detector: android.view.ScaleGestureDetector): Boolean { - scaleX *= detector.scaleFactor - scaleY *= detector.scaleFactor - - // 限制缩放范围 - scaleX = scaleX.coerceIn(0.1f, 5.0f) - scaleY = scaleY.coerceIn(0.1f, 5.0f) - - invalidate() - return true - } - }) - - // 触摸事件处理 - private var lastTouchX = 0f - private var lastTouchY = 0f - private var isDragging = false - - fun updateData(newData: List) { - if (newData.isEmpty()) return - - isDataAvailable = true - - // 直接添加新数据,来多少显示多少 - dataPoints.addAll(newData) - - // 限制数据点数量,保持滚动显示 - if (dataPoints.size > maxDataPoints) { - dataPoints = dataPoints.takeLast(maxDataPoints).toMutableList() - } - - // 更新数据范围 - updateDataRange(newData) - - // 立即重绘 - invalidate() - } - - private fun updateDataRange(newData: List) { - // 使用全部数据点计算范围,提供更准确的范围 - if (dataPoints.isEmpty()) return - - // 计算全部数据点的范围 - minValue = dataPoints.minOrNull() ?: 0f - maxValue = dataPoints.maxOrNull() ?: 0f - - // 确保有足够的显示范围 - val range = maxValue - minValue - if (range < 0.1f) { - val center = (maxValue + minValue) / 2 - minValue = center - 0.05f - maxValue = center + 0.05f - } else { - // 添加10%的上下边距 - val margin = range * 0.1f - minValue -= margin - maxValue += margin - } - } - - fun clearData() { - dataPoints.clear() - invalidate() - } - - /** - * 获取当前图表数据 - */ - fun getCurrentData(): List { - return dataPoints.toList() - } - - /** - * 获取数据统计信息 - */ - fun getDataStats(): Map { - return mapOf( - "dataPoints" to dataPoints.size, - "minValue" to minValue, - "maxValue" to maxValue, - "range" to (maxValue - minValue), - "isDataAvailable" to isDataAvailable - ) - } - - fun resetZoom() { - scaleX = 1.0f - scaleY = 1.0f - offsetX = 0f - offsetY = 0f - invalidate() - } - - override fun onTouchEvent(event: MotionEvent): Boolean { - when (event.action) { - MotionEvent.ACTION_DOWN -> { - lastTouchX = event.x - lastTouchY = event.y - isDragging = false - } - MotionEvent.ACTION_MOVE -> { - if (event.pointerCount == 1) { - // 单指拖拽 - val deltaX = event.x - lastTouchX - val deltaY = event.y - lastTouchY - offsetX += deltaX - offsetY += deltaY - lastTouchX = event.x - lastTouchY = event.y - isDragging = true - invalidate() - } - } - MotionEvent.ACTION_UP -> { - if (!isDragging) { - // 如果没有拖拽,可能是点击事件 - return gestureDetector.onTouchEvent(event) - } - } - } - - // 处理缩放手势 - scaleGestureDetector.onTouchEvent(event) - - return true - } - - override fun onDraw(canvas: Canvas) { - super.onDraw(canvas) - - val width = width.toFloat() - val height = height.toFloat() - - // 绘制网格 - drawGrid(canvas, width, height) - - // 绘制标题 - canvas.drawText("ECG节律视图 (10秒)", 20f, 30f, textPaint) - - // 绘制时间轴标签 - canvas.drawText("0s", 20f, height - 10f, textPaint) - canvas.drawText("5s", width / 2 - 15f, height - 10f, textPaint) - canvas.drawText("10s", width - 50f, height - 10f, textPaint) - - // 绘制数据点数量 - canvas.drawText("数据点: ${dataPoints.size}", 20f, height - 35f, textPaint) - - // 绘制数据范围 - if (dataPoints.isNotEmpty()) { - canvas.drawText("范围: ${String.format("%.3f", minValue)} - ${String.format("%.3f", maxValue)}", - 20f, height - 60f, textPaint) - } - - // 绘制缩放信息 - canvas.drawText("X缩放: ${String.format("%.1f", scaleX)}x", width - 150f, 30f, textPaint) - canvas.drawText("Y缩放: ${String.format("%.1f", scaleY)}x", width - 150f, 55f, textPaint) - - // 如果没有数据,显示默认内容 - if (!isDataAvailable) { - drawDefaultContent(canvas, width, height) - } else { - // 绘制曲线 - if (dataPoints.size > 1) { - drawCurve(canvas, width, height) - } - } - } - - private fun drawGrid(canvas: Canvas, width: Float, height: Float) { - // 绘制垂直线(时间轴) - val verticalSpacing = width / 20 // 20条垂直线,每0.5秒一条 - for (i in 0..20) { - val x = i * verticalSpacing - canvas.drawLine(x, 0f, x, height, gridPaint) - } - - // 绘制水平线(幅度轴) - val horizontalSpacing = height / 10 - for (i in 0..10) { - val y = i * horizontalSpacing - canvas.drawLine(0f, y, width, y, gridPaint) - } - } - - private fun drawDefaultContent(canvas: Canvas, width: Float, height: Float) { - // 绘制默认的提示信息 - val centerX = width / 2 - val centerY = height / 2 - - // 绘制提示文字 - val hintPaint = Paint().apply { - color = Color.GRAY - textSize = 32f - isAntiAlias = true - textAlign = Paint.Align.CENTER - } - - canvas.drawText("等待数据...", centerX, centerY - 20f, hintPaint) - canvas.drawText("请先连接蓝牙设备", centerX, centerY + 20f, hintPaint) - canvas.drawText("然后点击'启动程序'显示原始数据", centerX, centerY + 60f, hintPaint) - - // 绘制一个简单的示例波形 - drawSampleWaveform(canvas, width, height) - } - - private fun drawSampleWaveform(canvas: Canvas, width: Float, height: Float) { - val samplePaint = Paint().apply { - color = Color.LTGRAY - strokeWidth = 2f - style = Paint.Style.STROKE - isAntiAlias = true - alpha = 100 - } - - val path = Path() - val padding = 80f - val drawWidth = width - 2 * padding - val drawHeight = height - 2 * padding - val centerY = height / 2 - - path.moveTo(padding, centerY) - - // 绘制一个简单的正弦波示例 - for (i in 0..100) { - val x = padding + (i * drawWidth / 100) - val y = centerY + (Math.sin(i * 0.3) * drawHeight / 4).toFloat() - path.lineTo(x, y) - } - - canvas.drawPath(path, samplePaint) - } - - private fun drawCurve(canvas: Canvas, width: Float, height: Float) { - path.reset() - - val padding = 80f - val drawWidth = width - 2 * padding - val drawHeight = height - 2 * padding - - // 应用缩放和偏移 - val scaledWidth = drawWidth * scaleX - val scaledHeight = drawHeight * scaleY - - // 性能优化:减少数据点绘制,提高流畅度 - val stepSize = maxOf(1, dataPoints.size / 1000) // 最多绘制1000个点 - val xStep = scaledWidth / (dataPoints.size - 1) - - var firstPoint = true - for (i in dataPoints.indices step stepSize) { - val x = padding + i * xStep + offsetX - val normalizedValue = (dataPoints[i] - minValue) / (maxValue - minValue) - // 使用0.1到0.9的范围,确保曲线在中间80%的区域显示 - val y = padding + (0.1f + normalizedValue * 0.8f) * scaledHeight + offsetY - - // 确保点在可见区域内 - if (x >= padding && x <= width - padding && y >= padding && y <= height - padding) { - if (firstPoint) { - path.moveTo(x, y) - firstPoint = false - } else { - path.lineTo(x, y) - } - } - } - - canvas.drawPath(path, paint) - } - - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - val desiredWidth = 1000 - val desiredHeight = 300 - - val widthMode = MeasureSpec.getMode(widthMeasureSpec) - val widthSize = MeasureSpec.getSize(widthMeasureSpec) - val heightMode = MeasureSpec.getMode(heightMeasureSpec) - val heightSize = MeasureSpec.getSize(heightMeasureSpec) - - val width = when (widthMode) { - MeasureSpec.EXACTLY -> widthSize - MeasureSpec.AT_MOST -> min(desiredWidth, widthSize) - else -> desiredWidth - } - - val height = when (heightMode) { - MeasureSpec.EXACTLY -> heightSize - MeasureSpec.AT_MOST -> min(desiredHeight, heightSize) - else -> desiredHeight - } - - setMeasuredDimension(width, height) - } -} diff --git a/app/src/main/java/com/example/cmake_project_test/ECGWaveformView.kt b/app/src/main/java/com/example/cmake_project_test/ECGWaveformView.kt deleted file mode 100644 index 5a27680..0000000 --- a/app/src/main/java/com/example/cmake_project_test/ECGWaveformView.kt +++ /dev/null @@ -1,363 +0,0 @@ -package com.example.cmake_project_test - -import android.content.Context -import android.graphics.* -import android.util.AttributeSet -import android.util.Log -import android.view.GestureDetector -import android.view.MotionEvent -import android.view.View -import kotlin.math.max -import kotlin.math.min - -/** - * ECG波形视图 - * 显示2.5秒(625点)的放大信号,用于精确测量波形形态和间期 - */ -class ECGWaveformView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : View(context, attrs, defStyleAttr) { - - private val paint = Paint().apply { - color = Color.RED - strokeWidth = 2f - style = Paint.Style.STROKE - isAntiAlias = true - } - - private val gridPaint = Paint().apply { - color = Color.LTGRAY - strokeWidth = 1f - style = Paint.Style.STROKE - alpha = 100 - } - - private val textPaint = Paint().apply { - color = Color.BLACK - textSize = 28f - isAntiAlias = true - } - - private val path = Path() - private var dataPoints = mutableListOf() - private var maxDataPoints = 625 // 2.5秒数据(250Hz采样率) - private var minValue = Float.MAX_VALUE - private var maxValue = Float.MIN_VALUE - private var isDataAvailable = false // 标记是否有数据 - - // 简化的数据管理:直接显示新数据 - - // 缩放控制参数 - private var scaleX = 1.0f - private var scaleY = 1.0f - private var offsetX = 0f - private var offsetY = 0f - - // 手势检测器 - private val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { - override fun onDoubleTap(e: MotionEvent): Boolean { - resetZoom() - return true - } - - override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean { - offsetX -= distanceX - offsetY -= distanceY - invalidate() - return true - } - }) - - // 缩放手势检测器 - private val scaleGestureDetector = android.view.ScaleGestureDetector(context, - object : android.view.ScaleGestureDetector.SimpleOnScaleGestureListener() { - override fun onScale(detector: android.view.ScaleGestureDetector): Boolean { - scaleX *= detector.scaleFactor - scaleY *= detector.scaleFactor - - // 限制缩放范围 - scaleX = scaleX.coerceIn(0.1f, 5.0f) - scaleY = scaleY.coerceIn(0.1f, 5.0f) - - invalidate() - return true - } - }) - - // 触摸事件处理 - private var lastTouchX = 0f - private var lastTouchY = 0f - private var isDragging = false - - fun updateData(newData: List) { - if (newData.isEmpty()) return - - isDataAvailable = true - - // 直接添加新数据,来多少显示多少 - dataPoints.addAll(newData) - - // 限制数据点数量,保持滚动显示 - if (dataPoints.size > maxDataPoints) { - dataPoints = dataPoints.takeLast(maxDataPoints).toMutableList() - } - - // 更新数据范围 - updateDataRange(newData) - - // 立即重绘 - invalidate() - } - - private fun updateDataRange(newData: List) { - // 使用全部数据点计算范围,提供更准确的范围 - if (dataPoints.isEmpty()) return - - // 计算全部数据点的范围 - minValue = dataPoints.minOrNull() ?: 0f - maxValue = dataPoints.maxOrNull() ?: 0f - - // 确保有足够的显示范围 - val range = maxValue - minValue - if (range < 0.1f) { - val center = (maxValue + minValue) / 2 - minValue = center - 0.05f - maxValue = center + 0.05f - } else { - // 添加15%的上下边距 - val margin = range * 0.15f - minValue -= margin - maxValue += margin - } - } - - fun clearData() { - dataPoints.clear() - invalidate() - } - - /** - * 获取当前图表数据 - */ - fun getCurrentData(): List { - return dataPoints.toList() - } - - /** - * 获取数据统计信息 - */ - fun getDataStats(): Map { - return mapOf( - "dataPoints" to dataPoints.size, - "minValue" to minValue, - "maxValue" to maxValue, - "range" to (maxValue - minValue), - "isDataAvailable" to isDataAvailable - ) - } - - fun resetZoom() { - scaleX = 1.0f - scaleY = 1.0f - offsetX = 0f - offsetY = 0f - invalidate() - } - - override fun onTouchEvent(event: MotionEvent): Boolean { - when (event.action) { - MotionEvent.ACTION_DOWN -> { - lastTouchX = event.x - lastTouchY = event.y - isDragging = false - } - MotionEvent.ACTION_MOVE -> { - if (event.pointerCount == 1) { - // 单指拖拽 - val deltaX = event.x - lastTouchX - val deltaY = event.y - lastTouchY - offsetX += deltaX - offsetY += deltaY - lastTouchX = event.x - lastTouchY = event.y - isDragging = true - invalidate() - } - } - MotionEvent.ACTION_UP -> { - if (!isDragging) { - // 如果没有拖拽,可能是点击事件 - return gestureDetector.onTouchEvent(event) - } - } - } - - // 处理缩放手势 - scaleGestureDetector.onTouchEvent(event) - - return true - } - - override fun onDraw(canvas: Canvas) { - super.onDraw(canvas) - - val width = width.toFloat() - val height = height.toFloat() - - // 绘制网格 - drawGrid(canvas, width, height) - - // 绘制标题 - canvas.drawText("ECG波形视图 (2.5秒)", 20f, 35f, textPaint) - - // 绘制时间轴标签 - canvas.drawText("0s", 20f, height - 10f, textPaint) - canvas.drawText("1.25s", width / 2 - 25f, height - 10f, textPaint) - canvas.drawText("2.5s", width - 60f, height - 10f, textPaint) - - // 绘制数据点数量 - canvas.drawText("数据点: ${dataPoints.size}", 20f, height - 40f, textPaint) - - // 绘制数据范围 - if (dataPoints.isNotEmpty()) { - canvas.drawText("范围: ${String.format("%.3f", minValue)} - ${String.format("%.3f", maxValue)}", - 20f, height - 70f, textPaint) - } - - // 绘制缩放信息 - canvas.drawText("X缩放: ${String.format("%.1f", scaleX)}x", width - 150f, 35f, textPaint) - canvas.drawText("Y缩放: ${String.format("%.1f", scaleY)}x", width - 150f, 65f, textPaint) - - // 如果没有数据,显示默认内容 - if (!isDataAvailable) { - drawDefaultContent(canvas, width, height) - } else { - // 绘制曲线 - if (dataPoints.size > 1) { - drawCurve(canvas, width, height) - } - } - } - - private fun drawGrid(canvas: Canvas, width: Float, height: Float) { - // 绘制垂直线(时间轴)- 更密集的网格用于精确测量 - val verticalSpacing = width / 25 // 25条垂直线,每0.1秒一条 - for (i in 0..25) { - val x = i * verticalSpacing - canvas.drawLine(x, 0f, x, height, gridPaint) - } - - // 绘制水平线(幅度轴) - val horizontalSpacing = height / 12 // 12条水平线,更密集 - for (i in 0..12) { - val y = i * horizontalSpacing - canvas.drawLine(0f, y, width, y, gridPaint) - } - } - - private fun drawDefaultContent(canvas: Canvas, width: Float, height: Float) { - // 绘制默认的提示信息 - val centerX = width / 2 - val centerY = height / 2 - - // 绘制提示文字 - val hintPaint = Paint().apply { - color = Color.GRAY - textSize = 28f - isAntiAlias = true - textAlign = Paint.Align.CENTER - } - - canvas.drawText("等待数据...", centerX, centerY - 20f, hintPaint) - canvas.drawText("请先连接蓝牙设备", centerX, centerY + 20f, hintPaint) - canvas.drawText("然后点击'启动程序'显示原始数据", centerX, centerY + 60f, hintPaint) - - // 绘制一个简单的示例波形 - drawSampleWaveform(canvas, width, height) - } - - private fun drawSampleWaveform(canvas: Canvas, width: Float, height: Float) { - val samplePaint = Paint().apply { - color = Color.LTGRAY - strokeWidth = 1.5f - style = Paint.Style.STROKE - isAntiAlias = true - alpha = 80 - } - - val path = Path() - val padding = 100f - val drawWidth = width - 2 * padding - val drawHeight = height - 2 * padding - val centerY = height / 2 - - path.moveTo(padding, centerY) - - // 绘制一个简单的正弦波示例 - for (i in 0..100) { - val x = padding + (i * drawWidth / 100) - val y = centerY + (Math.sin(i * 0.5) * drawHeight / 6).toFloat() - path.lineTo(x, y) - } - - canvas.drawPath(path, samplePaint) - } - - private fun drawCurve(canvas: Canvas, width: Float, height: Float) { - path.reset() - - val padding = 100f - val drawWidth = width - 2 * padding - val drawHeight = height - 2 * padding - - // 应用缩放和偏移 - val scaledWidth = drawWidth * scaleX - val scaledHeight = drawHeight * scaleY - - val xStep = scaledWidth / (dataPoints.size - 1) - - for (i in dataPoints.indices) { - val x = padding + i * xStep + offsetX - val normalizedValue = (dataPoints[i] - minValue) / (maxValue - minValue) - // 使用0.1到0.9的范围,确保曲线在中间80%的区域显示 - val y = padding + (0.1f + normalizedValue * 0.8f) * scaledHeight + offsetY - - // 确保点在可见区域内 - if (x >= padding && x <= width - padding && y >= padding && y <= height - padding) { - if (i == 0) { - path.moveTo(x, y) - } else { - path.lineTo(x, y) - } - } - } - - canvas.drawPath(path, paint) - } - - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - val desiredWidth = 1000 - val desiredHeight = 400 - - val widthMode = MeasureSpec.getMode(widthMeasureSpec) - val widthSize = MeasureSpec.getSize(widthMeasureSpec) - val heightMode = MeasureSpec.getMode(heightMeasureSpec) - val heightSize = MeasureSpec.getSize(heightMeasureSpec) - - val width = when (widthMode) { - MeasureSpec.EXACTLY -> widthSize - MeasureSpec.AT_MOST -> min(desiredWidth, widthSize) - else -> desiredWidth - } - - val height = when (heightMode) { - MeasureSpec.EXACTLY -> heightSize - MeasureSpec.AT_MOST -> min(desiredHeight, heightSize) - else -> desiredHeight - } - - setMeasuredDimension(width, height) - } -} diff --git a/app/src/main/java/com/example/cmake_project_test/FileHelper.kt b/app/src/main/java/com/example/cmake_project_test/FileHelper.kt deleted file mode 100644 index 7978707..0000000 --- a/app/src/main/java/com/example/cmake_project_test/FileHelper.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.cmake_project_test - -import android.content.Context -import android.util.Log -import java.io.IOException - -/** - * 文件帮助类 - * 负责文件读取操作 - */ -object FileHelper { - - /** - * 从 assets 文件夹读取文件到字节数组 - */ - fun readAssetFile(context: Context, fileName: String): ByteArray? { - return try { - context.assets.open(fileName).use { inputStream -> - // 使用更可靠的文件读取方式 - val bytes = mutableListOf() - val buffer = ByteArray(1024) - var bytesRead: Int - - while (inputStream.read(buffer).also { bytesRead = it } != -1) { - for (i in 0 until bytesRead) { - bytes.add(buffer[i]) - } - } - - bytes.toByteArray() - } - } catch (e: IOException) { - Log.e("FileHelper", "Error reading asset file: $fileName", e) - null - } catch (e: Exception) { - Log.e("FileHelper", "Unexpected error reading asset file: $fileName", e) - null - } - } -} 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 59ca30c..507f212 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 @@ -972,8 +972,7 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real */ private fun showCommandDialog() { val commands = arrayOf( - "轻迅协议指令", - "PPTM协议指令" + "轻迅协议指令" ) androidx.appcompat.app.AlertDialog.Builder(this) @@ -981,7 +980,6 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real .setItems(commands) { _, which -> when (which) { 0 -> showQingXunProtocolDialog() - 1 -> showPptmProtocolDialog() } } .setNegativeButton("取消", null) @@ -995,8 +993,7 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real val commands = arrayOf( "开启采集", "停止采集", - "查询设备信息", - "查询电量", + "电刺激开关", "工频滤波开关" ) @@ -1006,87 +1003,8 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real when (which) { 0 -> sendQingXunStartCollection() 1 -> sendQingXunStopCollection() - 2 -> bluetoothManager.queryDeviceInfo() - 3 -> bluetoothManager.queryBattery() - 4 -> showPowerLineFilterDialog() - } - } - .setNegativeButton("取消", null) - .show() - } - - /** - * 显示PPTM协议指令对话框 - */ - private fun showPptmProtocolDialog() { - val commands = arrayOf( - "开始采样", - "停止采样", - "自定义PPTM指令" - ) - - androidx.appcompat.app.AlertDialog.Builder(this) - .setTitle("PPTM协议指令") - .setItems(commands) { _, which -> - when (which) { - 0 -> { - bluetoothManager.sendPptmStartSampleCmd() - updateStatus("发送PPTM开始采样指令") - } - 1 -> { - bluetoothManager.sendPptmStopSampleCmd() - updateStatus("发送PPTM停止采样指令") - } - 2 -> showCustomPptmCommandDialog() - } - } - .setNegativeButton("取消", null) - .show() - } - - /** - * 显示自定义PPTM指令对话框 - */ - private fun showCustomPptmCommandDialog() { - val input = android.widget.EditText(this) - input.hint = "输入操作码(如:0x0001)" - - val payloadInput = android.widget.EditText(this) - payloadInput.hint = "输入负载数据(十六进制,如:01 00 00 00)" - - val layout = android.widget.LinearLayout(this).apply { - orientation = android.widget.LinearLayout.VERTICAL - setPadding(50, 20, 50, 20) - addView(input) - addView(payloadInput) - } - - androidx.appcompat.app.AlertDialog.Builder(this) - .setTitle("自定义PPTM指令") - .setView(layout) - .setPositiveButton("发送") { _, _ -> - val opcodeStr = input.text.toString().trim() - val payloadStr = payloadInput.text.toString().trim() - - if (opcodeStr.isNotEmpty()) { - try { - val opcode = if (opcodeStr.startsWith("0x")) { - opcodeStr.substring(2).toShort(16) - } else { - opcodeStr.toShort() - } - - val payload = if (payloadStr.isNotEmpty()) { - payloadStr.split(" ").map { it.toInt(16).toByte() }.toByteArray() - } else { - ByteArray(0) - } - - bluetoothManager.sendCustomPptmCmd(opcode, payload) - updateStatus("发送自定义PPTM指令: 操作码=0x%04X".format(opcode)) - } catch (e: Exception) { - updateStatus("PPTM指令格式错误: ${e.message}") - } + 2 -> showStimSwitchDialog() + 3 -> showPowerLineFilterDialog() } } .setNegativeButton("取消", null) @@ -1102,9 +1020,7 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real */ private fun sendQingXunStartCollection() { val options = arrayOf( - "立即开启", - "延迟开启(5秒后)", - "指定时间戳开启" + "立即开启" ) androidx.appcompat.app.AlertDialog.Builder(this) @@ -1115,17 +1031,6 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real bluetoothManager.startCollection(0L) // 立即开启 updateStatus("发送轻迅协议开启采集指令(立即),准备接收数据...") } - 1 -> { - val timestamp = System.currentTimeMillis() + 5000 // 5秒后 - bluetoothManager.startCollection(timestamp) - updateStatus("发送轻迅协议开启采集指令(5秒后),准备接收数据...") - } - 2 -> { - showTimestampInputDialog { timestamp -> - bluetoothManager.startCollection(timestamp) - updateStatus("发送轻迅协议开启采集指令(指定时间戳: $timestamp),准备接收数据...") - } - } } } .setNegativeButton("取消", null) @@ -1137,9 +1042,7 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real */ private fun sendQingXunStopCollection() { val options = arrayOf( - "立即停止", - "延迟停止(5秒后)", - "指定时间戳停止" + "立即停止" ) androidx.appcompat.app.AlertDialog.Builder(this) @@ -1150,17 +1053,6 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real bluetoothManager.stopCollection(0L) // 立即停止 updateStatus("发送轻迅协议停止采集指令(立即)") } - 1 -> { - val timestamp = System.currentTimeMillis() + 5000 // 5秒后 - bluetoothManager.stopCollection(timestamp) - updateStatus("发送轻迅协议停止采集指令(5秒后)") - } - 2 -> { - showTimestampInputDialog { timestamp -> - bluetoothManager.stopCollection(timestamp) - updateStatus("发送轻迅协议停止采集指令(指定时间戳: $timestamp)") - } - } } } .setNegativeButton("取消", null) @@ -1195,71 +1087,30 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real } /** - * 显示时间戳输入对话框 + * 显示电刺激开关对话框 */ - private fun showTimestampInputDialog(onConfirm: (Long) -> Unit) { - val input = android.widget.EditText(this) - input.hint = "输入时间戳(毫秒,0表示立即执行)" - input.setText("0") - - androidx.appcompat.app.AlertDialog.Builder(this) - .setTitle("输入时间戳") - .setView(input) - .setPositiveButton("确定") { _, _ -> - try { - val timestamp = input.text.toString().toLong() - onConfirm(timestamp) - } catch (e: NumberFormatException) { - updateStatus("时间戳格式错误,请输入数字") - } - } - .setNegativeButton("取消", null) - .show() - } - - /** - * 显示轻迅协议命令菜单 - */ - private fun showQingXunProtocolMenu() { - val options = arrayOf( - "查询设备信息", - "开启采集", - "停止采集", - "查询电量", - "设置采样率", - "设置增益", - "设置导联", - "工频滤波开关", - "查询状态", - "设备复位", - "查看协议统计" + private fun showStimSwitchDialog() { + val stimOptions = arrayOf( + "开启刺激A", + "开启刺激B", + "关闭电刺激" ) androidx.appcompat.app.AlertDialog.Builder(this) - .setTitle("轻迅协议V1.0.1命令") - .setItems(options) { _, which -> + .setTitle("电刺激开关") + .setItems(stimOptions) { _, which -> when (which) { 0 -> { - bluetoothManager.queryDeviceInfo() - updateStatus("发送查询设备信息指令") + bluetoothManager.stimSwitch(0, true) + updateStatus("发送电刺激开关指令: 刺激A 开启") } - 1 -> sendQingXunStartCollection() - 2 -> sendQingXunStopCollection() - 3 -> { - bluetoothManager.queryBattery() - updateStatus("发送查询电量指令") + 1 -> { + bluetoothManager.stimSwitch(1, true) + updateStatus("发送电刺激开关指令: 刺激B 开启") } - 4 -> showSampleRateDialog() - 5 -> showGainDialog() - 6 -> showLeadDialog() - 7 -> showPowerLineFilterDialog() - 8 -> { - bluetoothManager.queryStatus() - updateStatus("发送查询状态指令") - } - 9 -> { - bluetoothManager.resetDevice() - updateStatus("发送设备复位指令") + 2 -> { + bluetoothManager.stimSwitch(0, false) + updateStatus("发送电刺激开关指令: 关闭电刺激") } } } @@ -1268,154 +1119,19 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real } /** - * 显示采样率设置对话框 + * 显示调试状态对话框 */ - private fun showSampleRateDialog() { - val options = arrayOf( - "125 Hz", - "250 Hz", - "500 Hz", - "1000 Hz", - "2000 Hz", - "自定义" - ) + private fun showDebugStatus() { + val status = bluetoothManager.debugConnectionStatus() androidx.appcompat.app.AlertDialog.Builder(this) - .setTitle("设置采样率") - .setItems(options) { _, which -> - when (which) { - 0 -> bluetoothManager.setSampleRate(125) - 1 -> bluetoothManager.setSampleRate(250) - 2 -> bluetoothManager.setSampleRate(500) - 3 -> bluetoothManager.setSampleRate(1000) - 4 -> bluetoothManager.setSampleRate(2000) - 5 -> showCustomSampleRateDialog() - } - updateStatus("发送设置采样率指令") - } - .setNegativeButton("取消", null) + .setTitle("协议统计信息") + .setMessage(status) + .setPositiveButton("确定", null) .show() } - /** - * 显示自定义采样率对话框 - */ - private fun showCustomSampleRateDialog() { - val input = android.widget.EditText(this) - input.hint = "输入采样率 (Hz)" - input.setText("500") - - androidx.appcompat.app.AlertDialog.Builder(this) - .setTitle("自定义采样率") - .setView(input) - .setPositiveButton("确定") { _, _ -> - try { - val sampleRate = input.text.toString().toInt() - if (sampleRate > 0 && sampleRate <= 10000) { - bluetoothManager.setSampleRate(sampleRate) - updateStatus("发送设置采样率指令: ${sampleRate} Hz") - } else { - updateStatus("采样率范围错误,请输入1-10000之间的数字") - } - } catch (e: NumberFormatException) { - updateStatus("采样率格式错误,请输入数字") - } - } - .setNegativeButton("取消", null) - .show() - } - /** - * 显示增益设置对话框 - */ - private fun showGainDialog() { - val options = arrayOf( - "1x", - "2x", - "4x", - "8x", - "16x", - "32x", - "自定义" - ) - - androidx.appcompat.app.AlertDialog.Builder(this) - .setTitle("设置增益") - .setItems(options) { _, which -> - when (which) { - 0 -> bluetoothManager.setGain(1) - 1 -> bluetoothManager.setGain(2) - 2 -> bluetoothManager.setGain(4) - 3 -> bluetoothManager.setGain(8) - 4 -> bluetoothManager.setGain(16) - 5 -> bluetoothManager.setGain(32) - 6 -> showCustomGainDialog() - } - updateStatus("发送设置增益指令") - } - .setNegativeButton("取消", null) - .show() - } - - /** - * 显示自定义增益对话框 - */ - private fun showCustomGainDialog() { - val input = android.widget.EditText(this) - input.hint = "输入增益倍数" - input.setText("1") - - androidx.appcompat.app.AlertDialog.Builder(this) - .setTitle("自定义增益") - .setView(input) - .setPositiveButton("确定") { _, _ -> - try { - val gain = input.text.toString().toInt() - if (gain > 0 && gain <= 100) { - bluetoothManager.setGain(gain) - updateStatus("发送设置增益指令: ${gain}x") - } else { - updateStatus("增益范围错误,请输入1-100之间的数字") - } - } catch (e: NumberFormatException) { - updateStatus("增益格式错误,请输入数字") - } - } - .setNegativeButton("取消", null) - .show() - } - - /** - * 显示导联设置对话框 - */ - private fun showLeadDialog() { - val options = arrayOf( - "I导联", - "II导联", - "III导联", - "aVR导联", - "aVL导联", - "aVF导联", - "V1导联", - "V2导联", - "V3导联", - "V4导联", - "V5导联", - "V6导联" - ) - - androidx.appcompat.app.AlertDialog.Builder(this) - .setTitle("设置导联") - .setItems(options) { _, which -> - val lead = which + 1 // 导联编号从1开始 - bluetoothManager.setLead(lead) - updateStatus("发送设置导联指令: ${options[which]}") - } - .setNegativeButton("取消", null) - .show() - } - - private fun clearAllData() { // 清空动态图表数据 dynamicChartManager.clearAllCharts() @@ -1426,16 +1142,6 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real updateStatus("已清空所有数据") } - /** - * 测试图表显示功能 - */ - - /** - * 显示调试状态对话框 - */ - - - private fun startDataMonitoring() { if (!isDataMonitoringEnabled) { isDataMonitoringEnabled = true @@ -1572,18 +1278,6 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real }.start() } - - /** - * 导出设备信息 - */ - - /** - * 导出节律视图数据 - */ - - /** - * 导出波形视图数据 - */ private fun exportWaveformData(data: List, stats: Map) { Thread { try { diff --git a/app/src/main/java/com/example/cmake_project_test/NativeMethodCallback.kt b/app/src/main/java/com/example/cmake_project_test/NativeMethodCallback.kt deleted file mode 100644 index 9c3c5fc..0000000 --- a/app/src/main/java/com/example/cmake_project_test/NativeMethodCallback.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.cmake_project_test - -import type.SensorData - -/** - * 原生方法回调接口 - * 用于DataManager调用MainActivity中的原生方法 - */ -interface NativeMethodCallback { - fun createStreamParser(): Long - fun destroyStreamParser(handle: Long) - fun streamParserAppend(handle: Long, chunk: ByteArray) - fun streamParserDrainPackets(handle: Long): List? -} diff --git a/app/src/main/java/com/example/cmake_project_test/PPTMCommandEncoder.kt b/app/src/main/java/com/example/cmake_project_test/PPTMCommandEncoder.kt deleted file mode 100644 index f8bb0ab..0000000 --- a/app/src/main/java/com/example/cmake_project_test/PPTMCommandEncoder.kt +++ /dev/null @@ -1,157 +0,0 @@ -package com.example.cmake_project_test - -import java.nio.ByteBuffer -import java.nio.ByteOrder - -/** - * PPTM设备命令编码器 - * 负责编码PPTM设备的命令数据包,包含CRC16校验 - */ -class PPTMCommandEncoder { - - companion object { - private const val TAG = "PPTMCommandEncoder" - private const val MAX_PACKET_SIZE = 244 - - // PPTM协议操作码 - const val OPCODE_START_SAMPLE = 0x0001 - const val OPCODE_STOP_SAMPLE = 0x0001 - - // PPTM协议常量 - const val COLLECTION_ON = 0x01 - const val COLLECTION_OFF = 0x00 - } - - /** - * CRC-16-CCITT-FALSE校验 - * @param source 数据源 - * @param offset 偏移 - * @param length 长度 - * @return CRC校验值 - */ - fun crc16(source: ByteArray, offset: Int, length: Int): Int { - var wCRCin = 0xFFFF - val wCPoly = 0x1021 - - for (i in offset until offset + length) { - for (j in 0..7) { - val bit = ((source[i].toInt() shr (7 - j)) and 1) == 1 - val c15 = ((wCRCin shr 15) and 1) == 1 - wCRCin = wCRCin shl 1 - if (c15 xor bit) { - wCRCin = wCRCin xor wCPoly - } - } - } - return wCRCin and 0xFFFF - } - - /** - * 编码命令数据 - * @param opcode 操作码 - * @param payload 负载数据 - * @param payloadLen 负载长度 - * @return 编码后的完整数据包 - */ - fun encode(opcode: Short, payload: ByteArray, payloadLen: Short): ByteArray { - val buf = ByteBuffer.allocate(MAX_PACKET_SIZE) - buf.order(ByteOrder.LITTLE_ENDIAN) - - // 写入操作码 - buf.putShort(opcode) - - // 写入负载长度 - buf.putShort(payloadLen) - - // 写入负载数据 - buf.put(payload) - - // 计算CRC16校验 - val calcCrcDataLen = buf.position() - val crc = crc16(buf.array(), 0, calcCrcDataLen) - buf.putShort(crc.toShort()) - - // 获取完整数据包 - val allDataLen = buf.position() - val dst = ByteArray(allDataLen) - - buf.flip() - buf.get(dst, 0, allDataLen) - - return dst - } - - /** - * 创建PPTM开始采样命令 - * @return 编码后的命令 - */ - fun createPptmStartSampleCmd(): ByteArray { - val payload = byteArrayOf( - COLLECTION_ON.toByte(), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - ) - return encode(OPCODE_START_SAMPLE.toShort(), payload, payload.size.toShort()) - } - - /** - * 创建PPTM停止采样命令 - * @return 编码后的命令 - */ - fun createPptmStopSampleCmd(): ByteArray { - val payload = byteArrayOf( - COLLECTION_OFF.toByte(), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - ) - return encode(OPCODE_STOP_SAMPLE.toShort(), payload, payload.size.toShort()) - } - - /** - * 创建自定义PPTM命令 - * @param opcode 操作码 - * @param payload 负载数据 - * @return 编码后的命令 - */ - fun createCustomPptmCmd(opcode: Short, payload: ByteArray): ByteArray { - return encode(opcode, payload, payload.size.toShort()) - } - - /** - * 验证数据包CRC16校验 - * @param packet 完整数据包 - * @return 校验是否通过 - */ - fun verifyCrc16(packet: ByteArray): Boolean { - if (packet.size < 6) return false // 至少需要操作码(2) + 长度(2) + CRC(2) - - val dataLen = packet.size - 2 // 减去CRC16的2字节 - val calculatedCrc = crc16(packet, 0, dataLen) - val packetCrc = ((packet[packet.size - 1].toInt() and 0xFF) shl 8) or - (packet[packet.size - 2].toInt() and 0xFF) - - return calculatedCrc == packetCrc - } - - /** - * 解析PPTM数据包 - * @param packet 完整数据包 - * @return 解析结果 (操作码, 负载数据) 或 null - */ - fun parsePacket(packet: ByteArray): Pair? { - if (!verifyCrc16(packet)) { - return null - } - - if (packet.size < 6) return null - - val buf = ByteBuffer.wrap(packet) - buf.order(ByteOrder.LITTLE_ENDIAN) - - val opcode = buf.short - val payloadLen = buf.short - - if (packet.size < 4 + payloadLen + 2) return null // 操作码(2) + 长度(2) + 负载 + CRC(2) - - val payload = ByteArray(payloadLen.toInt()) - buf.get(payload) - - return Pair(opcode, payload) - } -} diff --git a/app/src/main/java/com/example/cmake_project_test/ProducerConsumerChartView.kt b/app/src/main/java/com/example/cmake_project_test/ProducerConsumerChartView.kt deleted file mode 100644 index 2630a6b..0000000 --- a/app/src/main/java/com/example/cmake_project_test/ProducerConsumerChartView.kt +++ /dev/null @@ -1,484 +0,0 @@ -package com.example.cmake_project_test - -import android.content.Context -import android.graphics.* -import android.util.AttributeSet -import android.util.Log -import android.view.MotionEvent -import android.view.ScaleGestureDetector -import android.view.View -import java.util.concurrent.* -import java.util.concurrent.atomic.AtomicBoolean -import kotlin.math.* - -/** - * 真正的生产者-消费者模式图表视图 - * - * 关键设计特点: - * 1. 生产者-消费者模式:蓝牙回调生产数据,定时器消费数据 - * 2. 批量处理:减少频繁的UI更新,提高性能 - * 3. 自动滚动:数据超出屏幕宽度时自动滚动显示最新数据 - * 4. 多通道支持:每个通道独立的数据列表和绘制路径 - * 5. 内存控制:自动清理旧数据,防止内存溢出 - */ -class ProducerConsumerChartView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : View(context, attrs, defStyleAttr) { - - // ==================== 生产者-消费者模式核心 ==================== - - // 生产者:蓝牙数据回调线程 - private val dataQueue = LinkedBlockingQueue>() - private val multiChannelDataQueue = LinkedBlockingQueue>>() - - // 消费者:定时器线程 - private val displayExecutor = ScheduledThreadPoolExecutor(1) { r -> - Thread(r, "ChartDisplayThread").apply { isDaemon = true } - } - private var displayTask: ScheduledFuture<*>? = null - - // 消费者控制 - private val isConsuming = AtomicBoolean(false) - // 修复 'val' cannot be reassigned 错误 - // 将 val 改为 var - private var displayIntervalMs = 16L // 60fps = 16ms间隔 - - // ==================== 数据缓冲区 ==================== - - // 单通道数据缓冲区 - private val dataBuffer = mutableListOf() - private val maxBufferSize = 2000 - - // 多通道数据缓冲区 - private val multiChannelBuffer = mutableListOf>() - private var isMultiChannelMode = false - private val channelColors = listOf(Color.RED, Color.BLUE, Color.GREEN, Color.YELLOW) - - // ==================== 显示控制 ==================== - - private var lastDisplayTime = 0L - private var pendingDataCount = 0 - private var totalDataProduced = 0L - private var totalDataConsumed = 0L - - // ==================== 图表配置 ==================== - - private var chartTitle = "通道" - private var channelIndex = 0 - private var deviceType = "未知设备" - private var sampleRate = 250f - private var timeWindow = 4f - private var maxDataPoints = 1000 - - // ==================== 绘制相关 ==================== - - private val paint = Paint().apply { - isAntiAlias = true - strokeWidth = 2f - style = Paint.Style.STROKE - } - - private val textPaint = Paint().apply { - isAntiAlias = true - textSize = 32f - color = Color.WHITE - } - - private val backgroundPaint = Paint().apply { - color = Color.BLACK - style = Paint.Style.FILL - } - - private val gridPaint = Paint().apply { - color = Color.GRAY - strokeWidth = 1f - alpha = 100 - } - - // 数据范围 - private var minValue = -1000f - private var maxValue = 1000f - private var dataRange = maxValue - minValue - - // ==================== 交互控制 ==================== - - private var scaleFactor = 1.0f - private var translateX = 0f - private var translateY = 0f - private var viewportStart = 0f - private var viewportEnd = 1.0f - private var isDragging = false - private var lastTouchX = 0f - private var lastTouchY = 0f - - private val scaleGestureDetector = ScaleGestureDetector(context, ScaleListener()) - - // ==================== 初始化 ==================== - - init { - Log.d("ProducerConsumerChart", "初始化生产者-消费者图表视图") - startConsumer() - } - - // ==================== 生产者-消费者模式实现 ==================== - - /** - * 启动消费者线程(定时器) - */ - private fun startConsumer() { - if (isConsuming.get()) { - Log.w("ProducerConsumerChart", "消费者已在运行") - return - } - - isConsuming.set(true) - displayTask = displayExecutor.scheduleAtFixedRate({ - try { - consumeData() - } catch (e: Exception) { - Log.e("ProducerConsumerChart", "消费者异常: ${e.message}", e) - } - }, 0, displayIntervalMs, TimeUnit.MILLISECONDS) - - Log.d("ProducerConsumerChart", "消费者线程已启动,刷新间隔: ${displayIntervalMs}ms") - } - - /** - * 停止消费者线程 - */ - private fun stopConsumer() { - isConsuming.set(false) - displayTask?.cancel(false) - displayTask = null - Log.d("ProducerConsumerChart", "消费者线程已停止") - } - - /** - * 生产者:接收蓝牙数据 - */ - fun produceData(newData: List) { - if (newData.isEmpty()) return - - // 生产者:将数据放入队列 - val success = dataQueue.offer(newData) - if (success) { - totalDataProduced += newData.size - Log.d("ProducerConsumerChart", "生产者:数据已入队 ${newData.size}个点,队列大小: ${dataQueue.size}") - } else { - Log.w("ProducerConsumerChart", "生产者:队列已满,丢弃数据 ${newData.size}个点") - } - } - - /** - * 生产者:接收多通道数据 - */ - fun produceMultiChannelData(channelData: List>) { - if (channelData.isEmpty()) return - - // 生产者:将数据放入队列 - val success = multiChannelDataQueue.offer(channelData) - if (success) { - totalDataProduced += channelData.sumOf { it.size } - Log.d("ProducerConsumerChart", "生产者:多通道数据已入队 ${channelData.size}个通道,队列大小: ${multiChannelDataQueue.size}") - } else { - Log.w("ProducerConsumerChart", "生产者:多通道队列已满,丢弃数据") - } - } - - /** - * 消费者:定时消费数据并更新显示 - */ - private fun consumeData() { - val currentTime = System.currentTimeMillis() - var hasNewData = false - - // 批量消费单通道数据 - val singleChannelBatch = mutableListOf>() - dataQueue.drainTo(singleChannelBatch, 10) // 每次最多消费10个数据包 - - if (singleChannelBatch.isNotEmpty()) { - isMultiChannelMode = false - for (data in singleChannelBatch) { - dataBuffer.addAll(data) - totalDataConsumed += data.size - hasNewData = true - } - - // 控制缓冲区大小 - if (dataBuffer.size > maxBufferSize) { - val excess = dataBuffer.size - maxBufferSize - repeat(excess) { - if (dataBuffer.isNotEmpty()) { - dataBuffer.removeAt(0) - } - } - Log.d("ProducerConsumerChart", "消费者:清理了 $excess 个旧数据点") - } - } - - // 批量消费多通道数据 - val multiChannelBatch = mutableListOf>>() - multiChannelDataQueue.drainTo(multiChannelBatch, 5) // 每次最多消费5个多通道数据包 - - if (multiChannelBatch.isNotEmpty()) { - isMultiChannelMode = true - for (channelData in multiChannelBatch) { - // 确保多通道缓冲区有足够的通道 - while (multiChannelBuffer.size < channelData.size) { - multiChannelBuffer.add(mutableListOf()) - } - - // 添加数据到各通道 - channelData.forEachIndexed { index, data -> - if (index < multiChannelBuffer.size) { - multiChannelBuffer[index].addAll(data) - totalDataConsumed += data.size - hasNewData = true - } - } - } - - // 控制各通道缓冲区大小 - multiChannelBuffer.forEach { buffer -> - if (buffer.size > maxBufferSize) { - val excess = buffer.size - maxBufferSize - repeat(excess) { - if (buffer.isNotEmpty()) { - buffer.removeAt(0) - } - } - } - } - } - - // 如果有新数据,触发UI更新 - if (hasNewData) { - postInvalidate() // 在主线程更新UI - Log.d("ProducerConsumerChart", "消费者:批量处理完成,单通道队列: ${dataQueue.size}, 多通道队列: ${multiChannelDataQueue.size}") - } - - // 定期打印统计信息 - if (currentTime - lastDisplayTime > 5000) { - Log.d("ProducerConsumerChart", "统计 - 生产: $totalDataProduced, 消费: $totalDataConsumed, 队列: ${dataQueue.size + multiChannelDataQueue.size}") - lastDisplayTime = currentTime - } - } - - // ==================== 图表配置 ==================== - - fun setChartConfig( - title: String, - channelIdx: Int, - device: String, - maxPoints: Int, - sampleRateHz: Float, - timeWindowSec: Float - ) { - chartTitle = title - channelIndex = channelIdx - deviceType = device - maxDataPoints = maxPoints - sampleRate = sampleRateHz - timeWindow = timeWindowSec - - Log.d("ProducerConsumerChart", "图表配置: $title, 设备: $device, 采样率: ${sampleRateHz}Hz") - } - - // ==================== 绘制方法 ==================== - - override fun onDraw(canvas: Canvas) { - super.onDraw(canvas) - - val width = width.toFloat() - val height = height.toFloat() - - // 绘制背景 - canvas.drawRect(0f, 0f, width, height, backgroundPaint) - - // 绘制网格 - drawGrid(canvas, width, height) - - // 绘制数据 - if (isMultiChannelMode) { - drawMultiChannelData(canvas, width, height) - } else { - drawSingleChannelData(canvas, width, height) - } - - // 绘制标题和统计信息 - drawInfo(canvas, width, height) - } - - private fun drawGrid(canvas: Canvas, width: Float, height: Float) { - val gridSpacing = 50f - - // 垂直线 - var x = 0f - while (x <= width) { - canvas.drawLine(x, 0f, x, height, gridPaint) - x += gridSpacing - } - - // 水平线 - var y = 0f - while (y <= height) { - canvas.drawLine(0f, y, width, y, gridPaint) - y += gridSpacing - } - } - - private fun drawSingleChannelData(canvas: Canvas, width: Float, height: Float) { - if (dataBuffer.isEmpty()) return - - val path = Path() - val dataCount = dataBuffer.size - val stepX = width / maxOf(1, dataCount - 1) - - // 计算Y轴缩放 - val centerY = height / 2 - val scaleY = height / dataRange - - // 绘制数据线 - paint.color = Color.CYAN - paint.strokeWidth = 2f - - var isFirstPoint = true - dataBuffer.forEachIndexed { index, value -> - val x = index * stepX - val y = centerY - (value - (minValue + maxValue) / 2) * scaleY - - if (isFirstPoint) { - path.moveTo(x, y) - isFirstPoint = false - } else { - path.lineTo(x, y) - } - } - - canvas.drawPath(path, paint) - } - - private fun drawMultiChannelData(canvas: Canvas, width: Float, height: Float) { - if (multiChannelBuffer.isEmpty()) return - - val channelHeight = height / multiChannelBuffer.size - - multiChannelBuffer.forEachIndexed { channelIndex, channelData -> - if (channelData.isEmpty()) return@forEachIndexed - - val path = Path() - val dataCount = channelData.size - val stepX = width / maxOf(1, dataCount - 1) - - // 计算Y轴缩放 - val centerY = channelHeight * (channelIndex + 0.5f) - val scaleY = channelHeight / dataRange - - // 绘制数据线 - paint.color = channelColors[channelIndex % channelColors.size] - paint.strokeWidth = 2f - - var isFirstPoint = true - channelData.forEachIndexed { index, value -> - val x = index * stepX - val y = centerY - (value - (minValue + maxValue) / 2) * scaleY - - if (isFirstPoint) { - path.moveTo(x, y) - isFirstPoint = false - } else { - path.lineTo(x, y) - } - } - - canvas.drawPath(path, paint) - } - } - - private fun drawInfo(canvas: Canvas, width: Float, height: Float) { - val info = buildString { - appendLine("$chartTitle (通道$channelIndex)") - appendLine("设备: $deviceType") - appendLine("采样率: ${sampleRate}Hz") - appendLine("数据点: ${if (isMultiChannelMode) multiChannelBuffer.sumOf { it.size } else dataBuffer.size}") - appendLine("队列: ${dataQueue.size + multiChannelDataQueue.size}") - appendLine("生产: $totalDataProduced, 消费: $totalDataConsumed") - } - - val lines = info.trim().split("\n") - var y = 40f - lines.forEach { line -> - canvas.drawText(line, 20f, y, textPaint) - y += 35f - } - } - - // ==================== 交互处理 ==================== - - override fun onTouchEvent(event: MotionEvent): Boolean { - scaleGestureDetector.onTouchEvent(event) - - when (event.action) { - MotionEvent.ACTION_DOWN -> { - isDragging = true - lastTouchX = event.x - lastTouchY = event.y - } - MotionEvent.ACTION_MOVE -> { - if (isDragging) { - val deltaX = event.x - lastTouchX - val deltaY = event.y - lastTouchY - - translateX += deltaX - translateY += deltaY - - lastTouchX = event.x - lastTouchY = event.y - - invalidate() - } - } - MotionEvent.ACTION_UP -> { - isDragging = false - } - } - - return true - } - - private inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() { - override fun onScale(detector: ScaleGestureDetector): Boolean { - scaleFactor *= detector.scaleFactor - scaleFactor = scaleFactor.coerceIn(0.1f, 10.0f) - invalidate() - return true - } - } - - // ==================== 生命周期管理 ==================== - - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - stopConsumer() - displayExecutor.shutdown() - Log.d("ProducerConsumerChart", "视图已销毁,消费者线程已停止") - } - - // ==================== 公共接口 ==================== - - fun setDisplayRefreshRate(frequencyHz: Int) { - val newInterval = 1000L / frequencyHz - if (newInterval != displayIntervalMs) { - stopConsumer() - displayIntervalMs = newInterval - startConsumer() - Log.d("ProducerConsumerChart", "显示刷新频率已调整为: ${frequencyHz}Hz (${displayIntervalMs}ms间隔)") - } - } - - fun getQueueStatus(): String { - return "单通道队列: ${dataQueue.size}, 多通道队列: ${multiChannelDataQueue.size}, 生产: $totalDataProduced, 消费: $totalDataConsumed" - } -} diff --git a/app/src/main/java/com/example/cmake_project_test/SignalProcessingExample.kt b/app/src/main/java/com/example/cmake_project_test/SignalProcessingExample.kt deleted file mode 100644 index 7552ce5..0000000 --- a/app/src/main/java/com/example/cmake_project_test/SignalProcessingExample.kt +++ /dev/null @@ -1,206 +0,0 @@ -package com.example.cmake_project_test - -import android.util.Log -import type.SensorData - -/** - * 信号处理使用示例 - * 展示如何在现有流式数据读取代码结构中使用信号处理功能 - */ -class SignalProcessingExample(private val dataManager: DataManager) { - - companion object { - private const val TAG = "SignalProcessingExample" - } - - /** - * 示例1:实时信号质量监控 - * 在流式数据处理过程中实时监控信号质量 - */ - fun demonstrateRealTimeQualityMonitoring() { - Log.d(TAG, "=== 开始实时信号质量监控演示 ===") - - val packets = dataManager.getPacketBuffer() - if (packets.isEmpty()) { - Log.w(TAG, "没有数据包可供监控") - return - } - - // 监控前10个数据包的质量 - val packetsToMonitor = packets.take(10) - var totalQuality = 0.0f - var validPackets = 0 - - for ((index, packet) in packetsToMonitor.withIndex()) { - val quality = dataManager.calculateSignalQuality(packet) - if (quality > 0) { - totalQuality += quality - validPackets++ - Log.d(TAG, "数据包 $index (${packet.dataType}): 质量 = $quality") - } - } - - if (validPackets > 0) { - val averageQuality = totalQuality / validPackets - Log.d(TAG, "平均信号质量: $averageQuality") - - // 根据质量决定是否需要调整滤波器参数 - if (averageQuality < 0.5f) { - Log.w(TAG, "信号质量较低,建议检查传感器连接或调整滤波器参数") - } else if (averageQuality > 0.8f) { - Log.i(TAG, "信号质量良好") - } - } - } - - /** - * 示例2:按设备类型应用不同的信号处理策略 - */ - fun demonstrateDeviceSpecificProcessing() { - Log.d(TAG, "=== 开始设备特定信号处理演示 ===") - - val packets = dataManager.getPacketBuffer() - if (packets.isEmpty()) { - Log.w(TAG, "没有数据包可供处理") - return - } - - // 按设备类型分组 - val packetsByType = packets.groupBy { it.dataType } - - for ((dataType, typePackets) in packetsByType) { - Log.d(TAG, "处理 ${dataType.name} 类型数据,共 ${typePackets.size} 个数据包") - - when (dataType) { - SensorData.DataType.EEG -> { - Log.d(TAG, "应用EEG专用滤波器:带通滤波(1-40Hz) + 幅度归一化") - } - SensorData.DataType.ECG_2LEAD, SensorData.DataType.ECG_12LEAD -> { - Log.d(TAG, "应用ECG专用滤波器:高通(0.5Hz) + 低通(40Hz) + 陷波(50Hz)") - } - SensorData.DataType.PPG -> { - Log.d(TAG, "应用PPG专用滤波器:低通(8Hz) + 运动伪影去除") - } - else -> { - Log.d(TAG, "数据类型 ${dataType.name} 暂不支持专用处理") - } - } - } - } - - /** - * 示例3:批量信号处理 - * 对大量数据进行批量信号处理 - */ - fun demonstrateBatchProcessing() { - Log.d(TAG, "=== 开始批量信号处理演示 ===") - - val packets = dataManager.getPacketBuffer() - if (packets.isEmpty()) { - Log.w(TAG, "没有数据包可供批量处理") - return - } - - Log.d(TAG, "开始批量处理 ${packets.size} 个数据包...") - - // 记录处理开始时间 - val startTime = System.currentTimeMillis() - - // 应用信号处理 - val processedPackets = dataManager.applySignalProcessing(packets) - - // 记录处理结束时间 - val endTime = System.currentTimeMillis() - val processingTime = endTime - startTime - - Log.d(TAG, "批量处理完成!") - Log.d(TAG, "处理时间: ${processingTime}ms") - Log.d(TAG, "处理数据包: ${processedPackets.size} 个") - Log.d(TAG, "平均处理速度: ${packets.size * 1000.0 / processingTime} 包/秒") - - // 显示处理后的信号质量统计 - if (processedPackets.isNotEmpty()) { - val qualityScores = mutableListOf() - for (packet in processedPackets.take(20)) { // 只检查前20个包 - val quality = dataManager.calculateSignalQuality(packet) - if (quality > 0) { - qualityScores.add(quality) - } - } - - if (qualityScores.isNotEmpty()) { - val avgQuality = qualityScores.average() - val maxQuality = qualityScores.maxOrNull() ?: 0f - val minQuality = qualityScores.minOrNull() ?: 0f - - Log.d(TAG, "处理后信号质量统计:") - Log.d(TAG, " 平均质量: ${String.format("%.3f", avgQuality)}") - Log.d(TAG, " 最高质量: ${String.format("%.3f", maxQuality)}") - Log.d(TAG, " 最低质量: ${String.format("%.3f", minQuality)}") - } - } - } - - /** - * 示例4:自适应滤波器参数调整 - * 根据信号质量自动调整滤波器参数 - */ - fun demonstrateAdaptiveFilterAdjustment() { - Log.d(TAG, "=== 开始自适应滤波器参数调整演示 ===") - - val packets = dataManager.getPacketBuffer() - if (packets.isEmpty()) { - Log.w(TAG, "没有数据包可供自适应处理") - return - } - - // 获取第一个数据包进行质量评估 - val firstPacket = packets.first() - val initialQuality = dataManager.calculateSignalQuality(firstPacket) - - Log.d(TAG, "初始信号质量: $initialQuality") - - if (initialQuality < 0.3f) { - Log.w(TAG, "信号质量过低,建议检查硬件连接") - } else if (initialQuality < 0.6f) { - Log.i(TAG, "信号质量一般,可以尝试调整滤波器参数") - - // 这里可以添加自适应参数调整逻辑 - // 例如:根据噪声水平调整截止频率 - when (firstPacket.dataType) { - SensorData.DataType.EEG -> { - Log.d(TAG, "建议调整EEG滤波器:降低高通截止频率到0.5Hz,提高低通截止频率到50Hz") - } - SensorData.DataType.ECG_2LEAD, SensorData.DataType.ECG_12LEAD -> { - Log.d(TAG, "建议调整ECG滤波器:提高高通截止频率到1Hz,降低低通截止频率到30Hz") - } - SensorData.DataType.PPG -> { - Log.d(TAG, "建议调整PPG滤波器:降低低通截止频率到6Hz") - } - else -> { - Log.d(TAG, "该数据类型暂不支持自适应调整") - } - } - } else { - Log.i(TAG, "信号质量良好,当前滤波器参数合适") - } - } - - /** - * 运行所有演示 - */ - fun runAllDemonstrations() { - Log.d(TAG, "开始运行所有信号处理演示...") - - try { - demonstrateRealTimeQualityMonitoring() - demonstrateDeviceSpecificProcessing() - demonstrateBatchProcessing() - demonstrateAdaptiveFilterAdjustment() - - Log.d(TAG, "所有演示完成!") - } catch (e: Exception) { - Log.e(TAG, "演示过程中发生错误", e) - } - } -} diff --git a/app/src/main/java/com/example/cmake_project_test/SignalProcessorExample.kt b/app/src/main/java/com/example/cmake_project_test/SignalProcessorExample.kt deleted file mode 100644 index bd13998..0000000 --- a/app/src/main/java/com/example/cmake_project_test/SignalProcessorExample.kt +++ /dev/null @@ -1,271 +0,0 @@ -package com.example.cmake_project_test - -import android.util.Log -import kotlin.math.PI -import kotlin.math.sin - -/** - * 信号处理使用示例类 - * 展示如何使用JNI信号处理功能 - */ -class SignalProcessorExample { - - private val signalProcessor = SignalProcessorJNI() - private val sampleRate = 1000.0 // 1kHz采样率 - - init { - // 初始化信号处理器 - if (!signalProcessor.createProcessor()) { - Log.e("SignalProcessorExample", "Failed to create signal processor") - } - } - - /** - * 生成测试信号:正弦波 + 噪声 - */ - fun generateTestSignal(frequency: Double, duration: Double, noiseLevel: Double = 0.1): FloatArray { - val numSamples = (sampleRate * duration).toInt() - val signal = FloatArray(numSamples) - - for (i in 0 until numSamples) { - val time = i / sampleRate - val sineWave = sin(2 * PI * frequency * time).toFloat() - val noise = (Math.random() * 2 - 1).toFloat() * noiseLevel - signal[i] = (sineWave + noise).toFloat() - } - - return signal - } - - /** - * 演示带通滤波 - */ - fun demonstrateBandpassFilter() { - Log.d("SignalProcessorExample", "=== 带通滤波演示 ===") - - // 生成包含多个频率的测试信号 - val signal = generateTestSignal(50.0, 1.0, 0.2) // 50Hz + 噪声 - - // 应用带通滤波 (40-60Hz) - val filteredSignal = signalProcessor.bandpassFilter(signal, sampleRate, 40.0, 60.0) - - if (filteredSignal != null) { - Log.d("SignalProcessorExample", "滤波成功!原始信号长度: ${signal.size}, 滤波后长度: ${filteredSignal.size}") - - // 计算信号质量 - val originalQuality = signalProcessor.calculateSignalQuality(signal) - val filteredQuality = signalProcessor.calculateSignalQuality(filteredSignal) - - Log.d("SignalProcessorExample", "原始信号质量: $originalQuality") - Log.d("SignalProcessorExample", "滤波后信号质量: $filteredQuality") - } else { - Log.e("SignalProcessorExample", "带通滤波失败") - } - } - - /** - * 演示低通滤波 - */ - fun demonstrateLowpassFilter() { - Log.d("SignalProcessorExample", "=== 低通滤波演示 ===") - - // 生成高频信号 - val signal = generateTestSignal(200.0, 1.0, 0.1) - - // 应用低通滤波 (100Hz截止) - val filteredSignal = signalProcessor.lowpassFilter(signal, sampleRate, 100.0) - - if (filteredSignal != null) { - Log.d("SignalProcessorExample", "低通滤波成功!") - - // 计算信号质量 - val originalQuality = signalProcessor.calculateSignalQuality(signal) - val filteredQuality = signalProcessor.calculateSignalQuality(filteredSignal) - - Log.d("SignalProcessorExample", "原始信号质量: $originalQuality") - Log.d("SignalProcessorExample", "滤波后信号质量: $filteredQuality") - } else { - Log.e("SignalProcessorExample", "低通滤波失败") - } - } - - /** - * 演示高通滤波 - */ - fun demonstrateHighpassFilter() { - Log.d("SignalProcessorExample", "=== 高通滤波演示 ===") - - // 生成包含低频和高频的信号 - val signal = generateTestSignal(10.0, 1.0, 0.1) // 10Hz低频信号 - - // 应用高通滤波 (50Hz截止) - val filteredSignal = signalProcessor.highpassFilter(signal, sampleRate, 50.0) - - if (filteredSignal != null) { - Log.d("SignalProcessorExample", "高通滤波成功!") - - // 计算信号质量 - val originalQuality = signalProcessor.calculateSignalQuality(signal) - val filteredQuality = signalProcessor.calculateSignalQuality(filteredSignal) - - Log.d("SignalProcessorExample", "原始信号质量: $originalQuality") - Log.d("SignalProcessorExample", "滤波后信号质量: $filteredQuality") - } else { - Log.e("SignalProcessorExample", "高通滤波失败") - } - } - - /** - * 演示陷波滤波(去除工频干扰) - */ - fun demonstrateNotchFilter() { - Log.d("SignalProcessorExample", "=== 陷波滤波演示 ===") - - // 生成包含工频干扰的信号 - val signal = generateTestSignal(50.0, 1.0, 0.3) // 50Hz工频 + 噪声 - - // 应用陷波滤波 (50Hz陷波) - val filteredSignal = signalProcessor.notchFilter(signal, sampleRate, 50.0, 30.0) - - if (filteredSignal != null) { - Log.d("SignalProcessorExample", "陷波滤波成功!") - - // 计算信号质量 - val originalQuality = signalProcessor.calculateSignalQuality(signal) - val filteredQuality = signalProcessor.calculateSignalQuality(filteredSignal) - - Log.d("SignalProcessorExample", "原始信号质量: $originalQuality") - Log.d("SignalProcessorExample", "滤波后信号质量: $filteredQuality") - } else { - Log.e("SignalProcessorExample", "陷波滤波失败") - } - } - - /** - * 演示ECG信号质量评估 - */ - fun demonstrateECGSQI() { - Log.d("SignalProcessorExample", "=== ECG信号质量评估演示 ===") - - // 生成模拟ECG信号 - val ecgSignal = generateTestSignal(1.0, 2.0, 0.05) // 1Hz心跳 + 低噪声 - - // 计算ECG信号质量指数 - val sqi = signalProcessor.calculateECGSQI(ecgSignal, sampleRate) - - Log.d("SignalProcessorExample", "ECG信号质量指数: $sqi") - - if (sqi > 0.7f) { - Log.d("SignalProcessorExample", "ECG信号质量良好") - } else if (sqi > 0.4f) { - Log.d("SignalProcessorExample", "ECG信号质量一般") - } else { - Log.d("SignalProcessorExample", "ECG信号质量较差") - } - } - - /** - * 演示信号特征提取 - */ - fun demonstrateFeatureExtraction() { - Log.d("SignalProcessorExample", "=== 信号特征提取演示 ===") - - // 生成测试信号 - val signal = generateTestSignal(100.0, 0.5, 0.15) - - // 提取特征 - val features = signalProcessor.extractFeatures(signal, sampleRate) - - if (features != null) { - Log.d("SignalProcessorExample", "特征提取成功!特征数量: ${features.size}") - - // 显示前几个特征值 - for (i in 0 until minOf(5, features.size)) { - Log.d("SignalProcessorExample", "特征 $i: ${features[i]}") - } - } else { - Log.e("SignalProcessorExample", "特征提取失败") - } - } - - /** - * 演示信号归一化 - */ - fun demonstrateNormalization() { - Log.d("SignalProcessorExample", "=== 信号归一化演示 ===") - - // 生成幅度较大的信号 - val signal = generateTestSignal(75.0, 0.5, 0.2) - - // 找到最大绝对值 - val maxAbs = signal.maxOfOrNull { kotlin.math.abs(it) } ?: 0f - Log.d("SignalProcessorExample", "归一化前最大绝对值: $maxAbs") - - // 归一化 - signalProcessor.normalizeAmplitude(signal) - - // 检查归一化结果 - val normalizedMaxAbs = signal.maxOfOrNull { kotlin.math.abs(it) } ?: 0f - Log.d("SignalProcessorExample", "归一化后最大绝对值: $normalizedMaxAbs") - - if (kotlin.math.abs(normalizedMaxAbs - 1.0f) < 0.01f) { - Log.d("SignalProcessorExample", "归一化成功!") - } else { - Log.e("SignalProcessorExample", "归一化失败") - } - } - - /** - * 演示相关性计算 - */ - fun demonstrateCorrelation() { - Log.d("SignalProcessorExample", "=== 相关性计算演示 ===") - - // 生成两个相关信号 - val signal1 = generateTestSignal(50.0, 1.0, 0.1) - val signal2 = generateTestSignal(50.0, 1.0, 0.1) // 相同频率 - - // 计算相关性 - val correlation = signalProcessor.calculateCorrelation(signal1, signal2) - - Log.d("SignalProcessorExample", "信号相关性: $correlation") - - if (correlation > 0.8f) { - Log.d("SignalProcessorExample", "信号高度相关") - } else if (correlation > 0.5f) { - Log.d("SignalProcessorExample", "信号中等相关") - } else { - Log.d("SignalProcessorExample", "信号相关性较低") - } - } - - /** - * 运行所有演示 - */ - fun runAllDemonstrations() { - Log.d("SignalProcessorExample", "开始运行所有信号处理演示...") - - try { - demonstrateBandpassFilter() - demonstrateLowpassFilter() - demonstrateHighpassFilter() - demonstrateNotchFilter() - demonstrateECGSQI() - demonstrateFeatureExtraction() - demonstrateNormalization() - demonstrateCorrelation() - - Log.d("SignalProcessorExample", "所有演示完成!") - } catch (e: Exception) { - Log.e("SignalProcessorExample", "演示过程中发生错误", e) - } - } - - /** - * 清理资源 - */ - fun cleanup() { - signalProcessor.destroyProcessor() - Log.d("SignalProcessorExample", "信号处理器已销毁") - } -} diff --git a/app/src/main/java/com/example/cmake_project_test/UiManager.kt b/app/src/main/java/com/example/cmake_project_test/UiManager.kt index 8baebd9..3f2667d 100644 --- a/app/src/main/java/com/example/cmake_project_test/UiManager.kt +++ b/app/src/main/java/com/example/cmake_project_test/UiManager.kt @@ -10,6 +10,77 @@ import java.util.concurrent.atomic.AtomicBoolean */ class UiManager { + companion object { + // UI更新相关 + const val UPDATE_INTERVAL = 200L // 优化:每200毫秒更新一次UI,减慢更新速度以便观察心电波形 + + // 显示限制相关 + const val MAX_DISPLAY_PACKETS = 5 // 最大显示数据包数量 + const val MAX_DETAIL_PACKETS = 3 // 最大详情数据包数量 + const val MAX_DISPLAY_CHANNELS = 4 // 最大显示通道数量 + const val MAX_12LEAD_CHANNELS = 6 // 12导联心电最大通道数量 + const val MAX_DISPLAY_SAMPLES = 750 // 最大显示采样点数量,调整为显示约3秒数据(基于250Hz采样率) + + // 设备类型名称映射 + fun getDeviceName(dataType: SensorData.DataType): String { + return when (dataType) { + SensorData.DataType.EEG -> "脑电设备" + SensorData.DataType.ECG_2LEAD -> "胸腹设备" + SensorData.DataType.PPG -> "血氧设备" + SensorData.DataType.ECG_12LEAD -> "12导联心电" + SensorData.DataType.STETHOSCOPE -> "数字听诊" + SensorData.DataType.SNORE -> "鼾声设备" + SensorData.DataType.RESPIRATION -> "呼吸/姿态" + SensorData.DataType.MIT_BIH -> "MIT-BIH" + else -> "未知设备" + } + } + + // 构建通道数据详情 + fun buildChannelDetails( + data: List, + maxPackets: Int = MAX_DETAIL_PACKETS, + maxChannels: Int = MAX_DISPLAY_CHANNELS, + maxSamples: Int = MAX_DISPLAY_SAMPLES + ): String { + if (data.isEmpty()) { + return "无通道数据" + } + + val details = mutableListOf() + + data.take(maxPackets).forEachIndexed { packetIndex, sensorData -> + if (sensorData.channelData.isNullOrEmpty()) { + details.add("数据包 ${packetIndex + 1}: 无通道数据") + return@forEachIndexed + } + + details.add("数据包 ${packetIndex + 1} (${getDeviceName(sensorData.dataType ?: SensorData.DataType.EEG)}):") + + sensorData.channelData.take(maxChannels).forEachIndexed { channelIndex, channel -> + if (channel.isNullOrEmpty()) { + details.add(" 通道 ${channelIndex + 1}: 空数据") + return@forEachIndexed + } + val sampleCount = minOf(maxSamples, channel.size) + val channelDataStr = channel.take(sampleCount).joinToString(", ") { "%.1f".format(it) } + details.add(" 通道 ${channelIndex + 1}: ${sampleCount}/${channel.size} 采样点") + details.add(" 示例: $channelDataStr${if (channel.size > sampleCount) "..." else ""}") + } + + if (sensorData.channelData.size > maxChannels) { + details.add(" ... 还有 ${sensorData.channelData.size - maxChannels} 个通道") + } + details.add("") + } + + if (data.size > maxPackets) { + details.add("... 还有 ${data.size - maxPackets} 个数据包") + } + return details.joinToString("\n") + } + } + private val uiUpdatePending = AtomicBoolean(false) private var lastUpdateTime = 0L @@ -21,7 +92,7 @@ class UiManager { updateCallback: () -> Unit ) { val currentTime = System.currentTimeMillis() - if (currentTime - lastUpdateTime >= Constants.UPDATE_INTERVAL && + if (currentTime - lastUpdateTime >= UPDATE_INTERVAL && uiUpdatePending.compareAndSet(false, true)) { lastUpdateTime = currentTime updateCallback() @@ -45,7 +116,7 @@ class UiManager { if (packetBuffer.isNotEmpty()) { // 获取设备类型信息 val deviceTypes = packetBuffer.mapNotNull { it.dataType }.distinct() - val deviceInfo = deviceTypes.joinToString(", ") { DeviceTypeHelper.getDeviceName(it) } + val deviceInfo = deviceTypes.joinToString(", ") { getDeviceName(it) } append("设备类型: $deviceInfo\n") } else { append("设备类型: 无\n") @@ -82,22 +153,22 @@ class UiManager { val metricsInfo = buildMetricsInfo(calculatedMetrics) // 只显示最新的一些数据包详情 - val recentPackets = packetBuffer.takeLast(Constants.MAX_DISPLAY_PACKETS) + val recentPackets = packetBuffer.takeLast(MAX_DISPLAY_PACKETS) val has12Lead = recentPackets.any { it.dataType == SensorData.DataType.ECG_12LEAD } val channelDetails = if (has12Lead) { - DeviceTypeHelper.buildChannelDetails( + buildChannelDetails( recentPackets, - maxPackets = Constants.MAX_DETAIL_PACKETS, - maxChannels = Constants.MAX_12LEAD_CHANNELS, - maxSamples = Constants.MAX_DISPLAY_SAMPLES + maxPackets = MAX_DETAIL_PACKETS, + maxChannels = MAX_12LEAD_CHANNELS, + maxSamples = MAX_DISPLAY_SAMPLES ) } else { - DeviceTypeHelper.buildChannelDetails( + buildChannelDetails( recentPackets, - maxPackets = Constants.MAX_DETAIL_PACKETS, - maxChannels = Constants.MAX_DISPLAY_CHANNELS, - maxSamples = Constants.MAX_DISPLAY_SAMPLES + maxPackets = MAX_DETAIL_PACKETS, + maxChannels = MAX_DISPLAY_CHANNELS, + maxSamples = MAX_DISPLAY_SAMPLES ) } @@ -133,7 +204,7 @@ class UiManager { val latestPacket = processedPackets.lastOrNull() if (latestPacket != null) { append("\n最新处理数据包:\n") - append("- 数据类型: ${DeviceTypeHelper.getDeviceName(latestPacket.getDataType())}\n") + append("- 数据类型: ${getDeviceName(latestPacket.getDataType())}\n") append("- 时间戳: ${latestPacket.getTimestamp()}\n") append("- 包序号: ${latestPacket.getPacketSn()}\n") diff --git a/app/轻迅蓝牙通信协议V1.0.1_ql.pdf b/app/轻迅蓝牙通信协议V1.0.1_ql.pdf new file mode 100644 index 0000000..ee967f1 --- /dev/null +++ b/app/轻迅蓝牙通信协议V1.0.1_ql.pdf @@ -0,0 +1,418 @@ +轻迅蓝牙通信协议 + V1.0.0 + 目录 + + 1. 范围...................................................................................................................................................3 + 2. 蓝牙接口定义 ................................................................................................................................ 3 + + 2.1 服务接口 .......................................................................................................................................3 + 2.2 广播接口 .......................................................................................................................................4 + 3. 帧格式说明.....................................................................................................................................5 + 3.1 帧数据包结构 ............................................................................................................................ 5 + 3.2 功能码定义................................................................................................................................. 6 + 3.3 功能码描述................................................................................................................................. 6 + 3.4 数据标识定义 .......................................................................................................................... 10 + 4. 通信逻辑说明 ..............................................................................................................................13 + 4.1 丢包处理 ................................................................................................................................... 13 + 4.2 压缩............................................................................................................................................ 13 + 4.2 空中升级 ................................................................................................................................... 13 + 1. 范围 + +本协议文档用于蓝牙主设备终端与轻迅采集设备之间蓝牙数据通信。 + +2. 蓝牙接口定义 + +2.1 服务接口 + +2.1.1 自定义服务 + +2.1.1.1 数据服务 + +采用自定义 128bit UUID +UUID: 6e400001-b5a3-f393-e0a9-68716563686f +包含 Write Characteristic 和 Notify Characteristic 两个子接口 +(1) RX:Write Characteristic(提供 BLE 主设备 向 BLE 从设备发送消息或取相应消息) +UUID:6e400002-b5a3-f393-e0a9-68716563686f +属性:Write/WriteNoRSP +数据类型:字节 +数据长度:244(每包最大可传输数据) +(2) TX:Notify Characteristic(提供给 BLE 从设备向 BLE 主设备 发送通知信息) +UUID: 6e400003-b5a3-f393-e0a9-68716563686f +属性: Notify +数据类型:字节 +数据长度:244(每包最大可传输数据) + +2.1.1.1 空中升级服务 + +采用 Nordic DFU 功能实现 +Service UUID: 0xFE59 + 2.1.2 标准服务 + +2.1.2.1 设备信息服务 + +采用 Bluetooth SIG 标准规定 Device Information Service +Service UUID:0x180A +包含设备编号,软件和硬件版本等信息的特征值 +具体对应特征值 UUID 和属性可查看标准文档 + +2.1.2.2 电量服务 + +采用 Bluetooth SIG 标准规定 Battery Service +Service UUID:0x180F +对应特征值 UUID 和属性可查看标准文档 + +2.2 广播接口 + +2.2.1 广播数据数据格式介绍 + +2.2.2 广播包结构 + +广播数据段描述 类型 说明 + +设备 LE 物理连接标识 0x01 长度:0x02 + 类型:0x01 + 数据:0x06 + 设备名称 0x09 长度:0x07 + 类型:0x09 + 数据:48 51 5F 42 45 45 + + 设备名称视具体设备而定 +广播内容: 02 01 06 07 09 48 51 5F 42 45 45 + +2.2.3 扫描响应包结构 + +广播数据段描述 类型 说明 +厂商自定义数据 0xFF + 长度:0x1A + 类型:0xFF + 数据: + + Company ID:2 字节 + Protocol Version:1 字节 + Device Code:2 字节 + MAC Address:6 字节 + +广播内容:0C FF 58 51 01 02 01 76 D8 57 F7 68 C2 + +Company ID 公司 ID 固定值为 0x5158 +Protocol Version 用于指示当前设备使用的协议版本 0x01 + + Device Code 包含产品类型(Device Type,高 8 位)与子类型(Sub Device Type,低 8 +位) + + Device Type 产品类型与 Sub Device Type 产品子类型定义如下表: + +产品类型 类型 ID 产品子类型 子类型 ID +多导睡眠设备 0x42 胸腹模块 0x10 + 腕部模块 0x20 + 额头模块 0x30 + 腿部模块 0x40 + +MAC Address 声明当前设备 mac 地址,用于兼容 IOS 设备无法获取 BLE mac 地址问题 + +3. 帧格式说明 + +3.1 帧数据包结构 + + 序号 长度 字段 说明 + 1 2 功能码 + 2 2 数据长度 len + + 3~(2+len) len 数据 + + 3+len 2 MIC 校验 + 4+len + + 功能码:操作命令码。 + 数据长度:本次传送数据的长度。 + MIC 校验:采用 CRC-16-CCITT-FALSE 校验,校验内容包括功能码、数据长度、数据 3 +个部分 + 说明:除特殊说明外,协议默认为小端格式。 + +3.2 功能码定义 + + APP->BLE + +序号 功能码 功能 备注 + 1 0x0000 查询设备信息 获取当前设备设置参数 + 2 0x0001 采集使能开关 0 或 1,指定时间点 + 3 0x0002 电量查询 查询设备电量 + 4 0x0003 电刺激开关 开启关闭不同类型的电刺激 + 5 0x000A 工频滤波开关 开启关闭工频滤波算法 + 6 0x0080 同步时间戳 64bit 毫秒时间戳 + + BLE->APP + +序号 功能码 功能 备注 + 1 0x8000 数据上传 上报采集数据 + 2 0x8001 设备状态上报 异常状态上报 + 3 0x8002 电量上报 设备主动上报电量 + +3.3 功能码描述 + +3.3.1 设备信息 + +功能码 0x0000 +主->从: + + 长度 字段 说明 + 2 0x0000 功能码 + 2 0x0000 数据长度 + + 2 CRC16 检验 + +从->主: + + 长度 字段 说明 + 2 0x0000 功能码 + 2 0x0001 数据长度 + 1 见下表格式 + Data + 2 检验 + CRC16 + +Data 格式: + + Byte Bit 状态位 + 采集使能 + 0 0 + 1~7 保留 + +3.3.2 采集使能 + +功能码 0x0001 +主->从: + + 长度 字段 说明 + 2 0x0001 功能码 + 2 0x0009 数据长度 + 1 0或1 采集开关 + 8 Timestamp 指定时间戳 + + 2 CRC16 检验 + +从->主: + + 长度 字段 说明 + 2 0x0001 功能码 + 2 0x0001 数据长度 + 采集开启状 + 1 0或1 + 态 + + 2 CRC16 检验 + +Timestamp 为开启或关闭采集操作的指定时间戳(毫秒级),如果为 0 则代表立即执行操作。 +可用于多设备同步开启采集。 + +3.3.3 电量查询 + +功能码 0x0002 + 主->从: 长度 字段 说明 +从->主: 2 0x0002 功能码 + 2 0x0000 数据长度 + + 2 CRC16 检验 + + 长度 字段 说明 + 2 0x0002 功能码 + 2 0x0001 数据长度 + 1 percent 电量百分比 + + 2 CRC16 检验 + +3.3.4 电刺激开关 + +功能码:0x0003 + +主->从: 长度 字段 说明 +从->主: 2 0x0003 功能码 + 2 0x0001 数据长度 + 开关状态 + 1 0x00 or 0x10~0x1F 以及电刺 + 激类型 + 2 CRC16 检验 + + 长度 字段 说明 + + 2 0x0003 功能码 + + 2 0x0001 数据长度 + + 开关状态 + + 1 0x00 or 0x10~0x1F 以及电刺 + + 激类型 + + 2 CRC16 检验 + +说明: + +0x00 高四位如果为 0000 表示电刺激关闭。 + +0x10~0x1F 高四位如果为 0001 表示电刺激开启,低四位 0000~1111 表示开启 16 种不 + +同类型的电刺激。 + +从机接收到指令后,根据指令通过 SPI 开启或关闭电刺激后,回传数据给主机。 + 3.3.5 工频滤波开关 + +功能码 0x000A +主->从: + + 长度 字段 说明 + 2 0x000A 功能码 + 2 0x0001 数据长度 + 1 0或1 开关状态 + + 2 CRC16 检验 + +从->主: + + 长度 字段 说明 + 2 0x000A 功能码 + 2 0x0000 数据长度 + + 2 CRC16 检验 + +用于开启关闭工频滤波。设备默认开启工频滤波,在需要时可以进行临时关闭和开启。设 +备重启后,开关状态不保存。 + +3.3.6 同步时间戳 + +功能码 0x0080 +主->从: + + 长度 字段 说明 + 2 0x0080 功能码 + 2 0x0008 数据长度 + 8 Timestamp 毫秒时间戳 + + 2 CRC16 检验 + +从->主: + + 长度 字段 说明 + 2 0x0080 功能码 + 2 0x0000 数据长度 + + 2 CRC16 检验 + +Timestamp 为毫秒级时间戳。若主设备接入网络,则会提供 unix 时间戳,从设备可以用来 +获取当前时间。若没有接入网络,主设备提供的时间戳为开机后的时间,仅能用于操作指 +令同步,不能当作实际时间。 + 3.3.7 数据上报 + +功能码 0x8000 + +从->主: + + 长度 字段 说明 + 功能码 + 2 0x8000 数据长度 + 2 len 上报数据 + len Data 检验 + 2 + CRC16 + +Data 格式 + + 长度 字段 说明 + + 2 SN 包序号 + + N 数据 1 TLD 格式 + M 数据 2 TLD 格式 + … + … + +采集数据采用 TLD 格式,数据按照[Type][Length][Data]的顺序排列组织。每包数据可以包含 +多个 TLD 组。 + +具体 TLD 数据定义参考 3.4 数据标识定义。 + +3.3.8 状态上报 + +当前协议暂不处理 + +3.4 数据标识定义 + +3.4.1 多导睡眠设备-胸腹模块数据定义 + +胸腹电信号数据 Length(2 Byte) Data +Type(2 Byte) + 0x4211 232 uint8_t loff_state[2]; + + int16_t ecg1[25] ; + + int16_t ecg2[25] ; + + int16_t emg1[25] ; + + int16_t emg2[25] ; + + int16_t br_temperature[5] ; + + int16_t br_impedance1[5] ; + + int16_t br_impedance2[5] ; + + 采样率: + 口鼻呼吸气流温度:100Hz + 胸腹呼吸阻抗:100Hz + 心电:500Hz + 下颌肌电:500Hz + + 脱落状态: 每个 1294 对应一个字节 + 胸腹鼾声数据 Length(2 Byte) Data +Type(2 Byte) 232 int8_t snore[232] ; +0x4212 + Length(2 Byte) 采样率: +胸腹呼吸气压数据 232 鼾声:500Hz +Type(2 Byte) +0x4213 Data + int16_t br_nose_pressure[114] ; + uint16_t movement ; + uint8_t posture ; + uint8_t ambient; + + 采样率: + 鼻呼吸气流气压:100Hz + 体位,体动:1Hz + 环境光:1Hz + +3.4.2 多导睡眠设备-腕部模块数据定义 + +Type(2 Byte) Length(2 Byte) Data +0x4220 232 int16_t ppg_hr[58] ; + int16_t ppg_spo2[58] ; +3.4.3 多导睡眠设备-额头模块数据定义 采样率:25Hz + +Type(2 Byte) Length(2 Byte) Data +0x4230 232 uint8_t loff_state[2] ; + int16_t eeg[6][14] ; +3.4.4 多导睡眠设备-腿部模块数据定义 int16_t eog[2][14] ; + uint8_t reserve[6] ; +Type(2 Byte) Length(2 Byte) +0x4240 232 采样率:500Hz + + Data + uint8_t loff_state[2]; + int16_t emg[115] ; + 采样率:500Hz + +4. 通信逻辑说明 +4.1 丢包处理 + +当前协议暂不启用丢包处理 + +4.2 压缩 + +当前协议暂不启用压缩 + +4.2 空中升级 + +空中升级采用 Nordic DFU 方案,具体做法是 App 发送特定数据使设备进入 DFU 状态,之后 +App 重连特定 DFU 设备进行升级。具体可以参考 Nordic DFU 方案文档。 +