From a3be0bf6ca09460965987ce5ccd606dede48b049 Mon Sep 17 00:00:00 2001 From: ZhangJinLong <19357383190@163.com> Date: Mon, 22 Sep 2025 09:17:09 +0800 Subject: [PATCH] APP COMMIT --- .idea/deploymentTargetSelector.xml | 8 + .idea/vcs.xml | 1 + .../example/cmake_project_test/Constants.kt | 4 +- .../example/cmake_project_test/DataManager.kt | 96 +++ .../cmake_project_test/DynamicChartManager.kt | 4 +- .../cmake_project_test/DynamicChartView.kt | 585 +++++++++++++----- .../FullscreenChartActivity.kt | 24 +- .../FullscreenFilterManager.kt | 27 +- .../cmake_project_test/MainActivity.kt | 57 +- .../ProducerConsumerChartView.kt | 484 +++++++++++++++ .../StreamingSignalProcessor.kt | 6 +- 11 files changed, 1111 insertions(+), 185 deletions(-) create mode 100644 app/src/main/java/com/example/cmake_project_test/ProducerConsumerChartView.kt diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index 849be02..0ba8431 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -4,6 +4,14 @@ diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 94a25f7..24ccfcd 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,5 +2,6 @@ + \ No newline at end of file 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 index ed6bfc4..28938cf 100644 --- a/app/src/main/java/com/example/cmake_project_test/Constants.kt +++ b/app/src/main/java/com/example/cmake_project_test/Constants.kt @@ -2,7 +2,7 @@ package com.example.cmake_project_test object Constants { // UI更新相关 - const val UPDATE_INTERVAL = 100L // 优化:每100毫秒更新一次UI,提高响应性 + const val UPDATE_INTERVAL = 200L // 优化:每200毫秒更新一次UI,减慢更新速度以便观察心电波形 // 数据分块相关 const val CHUNK_SIZE = 64 // 数据分块大小 @@ -16,7 +16,7 @@ object Constants { const val MAX_DETAIL_PACKETS = 3 // 最大详情数据包数量 const val MAX_DISPLAY_CHANNELS = 4 // 最大显示通道数量 const val MAX_12LEAD_CHANNELS = 6 // 12导联心电最大通道数量 - const val MAX_DISPLAY_SAMPLES = 10 // 最大显示采样点数量 + const val MAX_DISPLAY_SAMPLES = 750 // 最大显示采样点数量,调整为显示约3秒数据(基于250Hz采样率) // 文件相关 const val DEFAULT_DATA_FILE = "ecg_data_raw.dat" // 默认数据文件名 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 02b4efa..acc5483 100644 --- a/app/src/main/java/com/example/cmake_project_test/DataManager.kt +++ b/app/src/main/java/com/example/cmake_project_test/DataManager.kt @@ -101,6 +101,35 @@ class DataManager(private val nativeCallback: NativeMethodCallback) { return } + val currentTime = System.currentTimeMillis() + + // 数据处理频率限制:防止过高频率导致系统过载 + if (lastProcessTime > 0 && (currentTime - lastProcessTime) < 10) { // 小于10ms间隔,跳过处理 + Log.w("DataManager", "数据处理频率过高(${currentTime - lastProcessTime}ms),跳过本次处理") + return + } + lastProcessTime = currentTime + + // 内存保护:检查缓冲区大小,防止内存溢出 + if (packetBuffer.size > 500) { // 降低阈值,更早清理 + Log.w("DataManager", "数据包缓冲区过大(${packetBuffer.size}),执行紧急清理") + cleanupBuffer() + } + + if (channelBuffers.values.sumOf { it.size } > 5000) { // 降低阈值 + Log.w("DataManager", "通道缓冲区过大,执行紧急清理") + channelBuffers.forEach { (_, buffer) -> + if (buffer.size > 500) { // 降低单个缓冲区阈值 + val toRemove = buffer.size - 250 + repeat(toRemove) { + if (buffer.isNotEmpty()) { + buffer.removeAt(0) + } + } + } + } + } + Log.d("DataManager", "接收到蓝牙数据: ${chunk.size} 字节") Log.d("DataManager", "数据前10字节: ${chunk.take(10).joinToString(", ") { "0x%02X".format(it) }}") @@ -867,6 +896,17 @@ class DataManager(private val nativeCallback: NativeMethodCallback) { Log.d("DataManager", "清理了 $toRemove 个旧数据包,保留 $maxKeepPackets 个最新数据包") } + // 清理映射后的数据包缓冲区 + if (mappedPacketBuffer.size > maxKeepPackets) { + val toRemove = mappedPacketBuffer.size - maxKeepPackets + repeat(toRemove) { + if (mappedPacketBuffer.isNotEmpty()) { + mappedPacketBuffer.removeAt(0) + } + } + Log.d("DataManager", "清理了 $toRemove 个旧映射数据包,保留 $maxKeepPackets 个最新数据包") + } + // 清理处理后数据包缓冲区,保留最新的50个数据包 val maxKeepProcessedPackets = 50 if (processedPackets.size > maxKeepProcessedPackets) { @@ -879,11 +919,67 @@ class DataManager(private val nativeCallback: NativeMethodCallback) { Log.d("DataManager", "清理了 $toRemove 个旧处理后数据包,保留 $maxKeepProcessedPackets 个最新数据包") } + // 清理通道缓冲区,防止内存溢出 + channelBuffers.forEach { (channel, buffer) -> + if (buffer.size > 2000) { + val toRemove = buffer.size - 1000 + repeat(toRemove) { + if (buffer.isNotEmpty()) { + buffer.removeAt(0) + } + } + Log.d("DataManager", "清理通道 $channel 缓冲区,移除 $toRemove 个数据点") + } + } + + // 清理处理后的通道缓冲区 + processedChannelBuffers.forEach { (channel, buffer) -> + if (buffer.size > 2000) { + val toRemove = buffer.size - 1000 + repeat(toRemove) { + if (buffer.isNotEmpty()) { + buffer.removeAt(0) + } + } + Log.d("DataManager", "清理处理后通道 $channel 缓冲区,移除 $toRemove 个数据点") + } + } + } catch (e: Exception) { Log.e("DataManager", "清理缓冲区时发生错误: ${e.message}") } } + /** + * 检查内存使用情况并执行预防性清理 + */ + fun checkMemoryUsage() { + try { + val runtime = Runtime.getRuntime() + val usedMemoryMB = (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024 + val maxMemoryMB = runtime.maxMemory() / 1024 / 1024 + val memoryUsagePercent = (usedMemoryMB * 100) / maxMemoryMB + + Log.d("DataManager", "内存使用情况: ${usedMemoryMB}MB / ${maxMemoryMB}MB (${memoryUsagePercent}%)") + + // 如果内存使用超过80%,执行紧急清理 + if (memoryUsagePercent > 80) { + Log.w("DataManager", "内存使用率过高(${memoryUsagePercent}%),执行紧急清理") + cleanupBuffer() + + // 强制垃圾回收 + System.gc() + + val newUsedMemoryMB = (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024 + val newMemoryUsagePercent = (newUsedMemoryMB * 100) / maxMemoryMB + Log.d("DataManager", "清理后内存使用: ${newUsedMemoryMB}MB / ${maxMemoryMB}MB (${newMemoryUsagePercent}%)") + } + + } catch (e: Exception) { + Log.e("DataManager", "检查内存使用情况失败: ${e.message}") + } + } + /** diff --git a/app/src/main/java/com/example/cmake_project_test/DynamicChartManager.kt b/app/src/main/java/com/example/cmake_project_test/DynamicChartManager.kt index 38bc595..f8aff6f 100644 --- a/app/src/main/java/com/example/cmake_project_test/DynamicChartManager.kt +++ b/app/src/main/java/com/example/cmake_project_test/DynamicChartManager.kt @@ -197,12 +197,12 @@ class DynamicChartManager( for (i in 0 until config.channelCount) { val chartView = DynamicChartView(context) - // 设置图表配置 + // 设置图表配置,传递设备类型信息 val channelName = if (i < config.channelNames.size) config.channelNames[i] else "通道" chartView.setChartConfig( title = channelName, channelIdx = i + 1, - device = config.deviceName, + device = "${config.deviceName}_${dataType.name}", // 包含设备类型信息 maxPoints = config.maxDataPoints, sampleRateHz = config.sampleRate, timeWindowSec = config.timeWindow diff --git a/app/src/main/java/com/example/cmake_project_test/DynamicChartView.kt b/app/src/main/java/com/example/cmake_project_test/DynamicChartView.kt index 714a013..dd2f3ad 100644 --- a/app/src/main/java/com/example/cmake_project_test/DynamicChartView.kt +++ b/app/src/main/java/com/example/cmake_project_test/DynamicChartView.kt @@ -9,6 +9,11 @@ import android.view.View import android.view.ScaleGestureDetector import kotlin.math.max import kotlin.math.min +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.ScheduledThreadPoolExecutor +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean /** * 可调节的通用生理信号图表视图 @@ -57,6 +62,18 @@ class DynamicChartView @JvmOverloads constructor( // 累积数据点计数器 private var totalDataPointsReceived = 0L + // 更新频率控制 + private var lastUpdateTime = 0L + private var deviceRefreshInterval = 50L // 默认刷新间隔(ms) + private var isManualRefreshMode = false // 手动刷新模式 + private var pendingDataUpdate = false // 是否有待更新的数据 + + // 数据缓冲和显示分离 + private var lastDisplayTime = 0L // 上次显示时间 + private var lastMemoryCheckTime = 0L // 上次内存检查时间 + private var displayRefreshRate = 16L // 显示刷新频率:60Hz (16ms间隔) + private var dataBufferDirty = false // 数据缓冲区是否有新数据 + // 全屏模式和自定义Y轴范围 private var isFullscreenMode = false private var customMinValue: Float? = null @@ -74,12 +91,37 @@ class DynamicChartView @JvmOverloads constructor( private var sampleRate = 250f // 默认采样率 private var timeWindow = 4f // 默认时间窗口(秒) - // 简化的数据管理:直接显示新数据 - private val dataBuffer = mutableListOf() + // ==================== 生产者-消费者模式核心 ==================== + + // 生产者:蓝牙数据回调线程 + private val dataQueue = LinkedBlockingQueue(10000) // 增加队列容量,存储单个数据点 + private val multiChannelDataQueue = LinkedBlockingQueue>(5000) // 增加队列容量,存储多通道数据点 + + // 消费者:定时器线程 + private val displayExecutor = ScheduledThreadPoolExecutor(1) { r -> + Thread(r, "ChartDisplayThread").apply { isDaemon = true } + } + private var displayTask: ScheduledFuture<*>? = null + + // 消费者控制 + private val isConsuming = AtomicBoolean(false) + private var displayIntervalMs = 16L // 60fps = 16ms间隔 + + // ==================== 数据缓冲区 ==================== + + // 单通道数据缓冲区 + private var dataBuffer = mutableListOf() + private var maxBufferSize = 5000 // 调整最大缓冲区大小 + private var maxDisplayPoints = 2500 // 默认最大显示点数,增加以显示更多数据 + private var maxDisplaySamples = 2500 // 最大显示采样点数量,从FullscreenFilterManager获取 + + // 多通道数据缓冲区 + private val multiChannelBuffer = mutableListOf>() // 多通道数据支持(用于PPG等设备) - private val multiChannelDataPoints = mutableListOf>() - private val multiChannelDataBuffers = mutableListOf>() + // 移除冗余的数据结构 + // private val multiChannelDataPoints = mutableListOf>() + // private val multiChannelDataBuffers = mutableListOf>() private var isMultiChannelMode = false private val channelColors = listOf(Color.RED, Color.BLUE) // 通道颜色 @@ -99,6 +141,14 @@ class DynamicChartView @JvmOverloads constructor( // 控制按钮区域 private val controlButtonRect = RectF() + // 在类属性中添加 totalDataConsumed + private var totalDataConsumed = 0L + + init { + Log.d("DynamicChartView", "初始化生产者-消费者图表视图") + startConsumer() + } + /** * 设置图表配置 */ @@ -116,9 +166,38 @@ class DynamicChartView @JvmOverloads constructor( maxDataPoints = optimizeMaxDataPoints(maxPoints, sampleRateHz) sampleRate = sampleRateHz timeWindow = timeWindowSec + + // 根据设备类型和采样率设置刷新间隔 + deviceRefreshInterval = calculateRefreshInterval(device, sampleRateHz, maxPoints) + resetViewport() invalidate() } + + /** + * 根据设备类型和采样率计算合适的刷新间隔 + * 数据接收不限制,但显示频率根据设备类型和采样率控制 + */ + private fun calculateRefreshInterval(device: String, sampleRate: Float, maxPoints: Int): Long { + // 设置显示刷新频率:根据设备类型设置不同的刷新间隔 + displayRefreshRate = when { + device.contains("音频", ignoreCase = true) -> 50L // 音频设备:50ms + device.contains("EMG8ch", ignoreCase = true) -> 97L // EMG8ch设备:97ms + device.contains("PPG", ignoreCase = true) -> 96L // PPG设备:96ms + device.contains("ECG", ignoreCase = true) -> 97L // ECG设备:97ms + else -> { + // 根据采样率动态计算刷新间隔,确保处理速度大于接收速度 + if (sampleRate > 0) { + val targetIntervalMs = (1000.0 / sampleRate * 10).toLong().coerceIn(10L, 100L) // 每次处理10个点的最小间隔 + targetIntervalMs + } else { + 16L // 默认:60Hz (16ms间隔) + } + } + } + // 数据接收不限制,返回0表示不跳过任何数据 + return 0L + } /** * 根据设备类型和内存情况优化最大数据点数量 @@ -132,9 +211,12 @@ class DynamicChartView @JvmOverloads constructor( // 根据采样率调整 optimizedPoints = when { + deviceType.contains("EOG", ignoreCase = true) -> 2500 // EOG设备固定显示2500个数据点 + deviceType.contains("PPTM", ignoreCase = true) -> 500 // PPTM设备固定显示500个数据点 + deviceType.contains("ECG", ignoreCase = true) -> 500 // ECG设备固定显示500个数据点 sampleRate >= 8000f -> (baseMaxPoints * 0.3).toInt() // 音频设备减少到30% sampleRate >= 1000f -> (baseMaxPoints * 0.5).toInt() // 高速设备减少到50% - sampleRate >= 250f -> baseMaxPoints // ECG等保持不变 + sampleRate >= 250f -> baseMaxPoints // 其他设备保持不变 else -> (baseMaxPoints * 1.2).toInt() // 低速设备可以稍微增加 } @@ -146,8 +228,6 @@ class DynamicChartView @JvmOverloads constructor( else -> optimizedPoints } - // 确保在合理范围内 - optimizedPoints = optimizedPoints.coerceIn(500, 10000) Log.d("DynamicChartView", "数据点限制优化: 原始=$baseMaxPoints, 优化后=$optimizedPoints, 采样率=${sampleRate}Hz, 内存=${maxMemoryMB}MB") return optimizedPoints @@ -200,48 +280,288 @@ class DynamicChartView @JvmOverloads constructor( showFullscreenButton = show invalidate() } + + /** + * 设置手动刷新模式 + */ + fun setManualRefreshMode(enabled: Boolean) { + isManualRefreshMode = enabled + if (enabled) { + Log.d("DynamicChartView", "启用手动刷新模式") + } else { + Log.d("DynamicChartView", "禁用手动刷新模式") + // 如果有待更新的数据,立即刷新 + if (pendingDataUpdate) { + invalidate() + pendingDataUpdate = false + } + } + } + + /** + * 手动触发刷新 + */ + fun manualRefresh() { + if (isManualRefreshMode && pendingDataUpdate) { + Log.d("DynamicChartView", "手动刷新图表") + invalidate() + pendingDataUpdate = false + } + } + + /** + * 设置显示刷新频率 + * @param frequencyHz 刷新频率(Hz),如10Hz或20Hz + */ + fun setDisplayRefreshRate(frequencyHz: Int) { + val newInterval = 1000L / frequencyHz + if (newInterval != displayIntervalMs) { + stopConsumer() + displayIntervalMs = newInterval + startConsumer() + Log.d("DynamicChartView", "显示刷新频率已调整为: ${frequencyHz}Hz (${displayIntervalMs}ms间隔)") + } + } /** * 设置多通道模式(用于PPG等设备) */ fun setMultiChannelMode(channelCount: Int) { isMultiChannelMode = true - multiChannelDataPoints.clear() - multiChannelDataBuffers.clear() + multiChannelBuffer.clear() // 初始化每个通道的数据结构 for (i in 0 until channelCount) { - multiChannelDataPoints.add(mutableListOf()) - multiChannelDataBuffers.add(mutableListOf()) + multiChannelBuffer.add(mutableListOf()) } } + /** + * 设置最大显示采样点数量 + */ + fun setMaxDisplaySamples(samples: Int) { + maxDisplaySamples = samples + maxDataPoints = optimizeMaxDataPoints(maxDisplaySamples, sampleRate.toFloat()) + maxDisplayPoints = maxDataPoints + maxBufferSize = maxDataPoints * 2 // 确保缓冲区大小足够大 + invalidate() + } + + // ==================== 生产者-消费者模式实现 ==================== + + /** + * 启动消费者线程(定时器) + */ + private fun startConsumer() { + if (isConsuming.get()) { + Log.w("DynamicChartView", "消费者已在运行") + return + } + + isConsuming.set(true) + displayTask = displayExecutor.scheduleAtFixedRate({ + try { + consumeData() + } catch (e: Exception) { + Log.e("DynamicChartView", "消费者异常: ${e.message}", e) + } + }, 0, displayIntervalMs, TimeUnit.MILLISECONDS) + + Log.d("DynamicChartView", "消费者线程已启动,刷新间隔: ${displayIntervalMs}ms") + } + + /** + * 停止消费者线程 + */ + private fun stopConsumer() { + isConsuming.set(false) + displayTask?.cancel(false) + displayTask = null + Log.d("DynamicChartView", "消费者线程已停止") + } + + /** + * 生产者:接收蓝牙数据 + */ fun updateData(newData: List) { if (newData.isEmpty()) return - - isDataAvailable = true - // 更新累积数据点计数器 - totalDataPointsReceived += newData.size - - // 直接添加新数据,来多少显示多少 - dataPoints.addAll(newData) - - // 限制数据点数量,保持滚动显示 - if (dataPoints.size > maxDataPoints) { - val excessData = dataPoints.size - maxDataPoints - dataPoints = dataPoints.takeLast(maxDataPoints).toMutableList() - Log.d("DynamicChartView", "清理了 $excessData 个数据点,当前数据点: ${dataPoints.size}") + // 生产者:将单个数据点放入队列 + for (dataPoint in newData) { + var success = dataQueue.offer(dataPoint) + if (!success) { + Log.w("DynamicChartView", "生产者:队列已满,尝试清理旧数据") + // 队列满时,移除旧数据并再次尝试添加新数据 + val removedPoint = dataQueue.poll() + if (removedPoint != null) { + Log.d("DynamicChartView", "生产者:移除了 1 个旧数据点") + success = dataQueue.offer(dataPoint) + } + if (!success) { + Log.w("DynamicChartView", "生产者:再次尝试失败,丢弃数据点") + } else { + totalDataPointsReceived++ + Log.d("DynamicChartView", "生产者:数据点已入队,队列大小: ${dataQueue.size}") + } + } else { + totalDataPointsReceived++ + Log.d("DynamicChartView", "生产者:数据点已入队,队列大小: ${dataQueue.size}") + } + } + } + + /** + * 生产者:接收多通道数据 + */ + fun updateMultiChannelData(channelData: List>) { + if (channelData.isEmpty()) return + + // 生产者:将多通道数据点放入队列 + // 首先检查所有通道是否有相同数量的数据点 + val dataPointCount = channelData[0].size + if (channelData.all { it.size == dataPointCount }) { + // 按时间点组织数据,每个时间点包含所有通道的数据 + for (i in 0 until dataPointCount) { + val timePointData = channelData.map { it[i] } + var success = multiChannelDataQueue.offer(timePointData) + if (!success) { + Log.w("DynamicChartView", "生产者:多通道队列已满,尝试清理旧数据") + // 队列满时,移除旧数据并再次尝试添加新数据 + val removedPoint = multiChannelDataQueue.poll() + if (removedPoint != null) { + Log.d("DynamicChartView", "生产者:移除了 1 个旧多通道数据点") + success = multiChannelDataQueue.offer(timePointData) + } + if (!success) { + Log.w("DynamicChartView", "生产者:多通道再次尝试失败,丢弃数据点") + } else { + totalDataPointsReceived += timePointData.size + Log.d("DynamicChartView", "生产者:多通道数据点已入队 ${timePointData.size}个通道,队列大小: ${multiChannelDataQueue.size}") + } + } else { + totalDataPointsReceived += timePointData.size + Log.d("DynamicChartView", "生产者:多通道数据点已入队 ${timePointData.size}个通道,队列大小: ${multiChannelDataQueue.size}") + } + } + } else { + Log.w("DynamicChartView", "多通道数据点数量不一致,无法按时间点处理") + } + } + + /** + * 消费者:定时消费数据并更新显示 + */ + private fun consumeData() { + val currentTime = System.currentTimeMillis() + var hasNewData = false + + try { + // 消费单通道数据 - 每次处理少量数据点以实现平滑更新 + val singleChannelPoints = mutableListOf() + val pointsToProcess = when { + deviceType.contains("音频", ignoreCase = true) -> 10 // 音频设备:每次处理10个点 + deviceType.contains("EMG8ch", ignoreCase = true) -> 25 // EMG8ch设备:每次处理25个点 + deviceType.contains("PPG", ignoreCase = true) -> 5 // PPG设备:每次处理5个点 + deviceType.contains("ECG", ignoreCase = true) -> 25 // ECG设备:每次处理25个点 + else -> if (sampleRate > 0) { + // 根据采样率和刷新间隔计算应该处理的数据点数量,确保处理速度大于接收速度 + val intervalSeconds = displayIntervalMs / 1000.0 + val calculatedPoints = (sampleRate * intervalSeconds * 1.5).toInt().coerceAtLeast(1) // 增加50%的处理量,确保处理速度大于接收速度 + calculatedPoints + } else { + 10 // 默认值 + } + } + + dataQueue.drainTo(singleChannelPoints, pointsToProcess) + + if (singleChannelPoints.isNotEmpty()) { + isMultiChannelMode = false + dataBuffer.addAll(singleChannelPoints) + totalDataConsumed += singleChannelPoints.size + hasNewData = true + isDataAvailable = true // 设置数据可用标志 + + // 控制缓冲区大小 + if (dataBuffer.size > maxBufferSize) { + val excess = dataBuffer.size - maxBufferSize + repeat(excess) { + if (dataBuffer.isNotEmpty()) { + dataBuffer.removeAt(0) + } + } + Log.d("DynamicChartView", "消费者:清理了 $excess 个旧数据点") + } + // 更新单通道数据范围 + updateDataRange(dataBuffer) + } + + // 消费多通道数据 - 每次处理少量数据点 + val multiChannelPoints = mutableListOf>() + val multiPointsToProcess = when { + deviceType.contains("音频", ignoreCase = true) -> 10 // 音频设备:每次处理10个点 + deviceType.contains("EMG8ch", ignoreCase = true) -> 25 // EMG8ch设备:每次处理25个点 + deviceType.contains("PPG", ignoreCase = true) -> 5 // PPG设备:每次处理5个点 + deviceType.contains("ECG", ignoreCase = true) -> 25 // ECG设备:每次处理25个点 + else -> pointsToProcess // 使用相同的处理数量 + } + + multiChannelDataQueue.drainTo(multiChannelPoints, multiPointsToProcess) + + if (multiChannelPoints.isNotEmpty()) { + isMultiChannelMode = true + for (timePointData in multiChannelPoints) { + // 确保多通道缓冲区有足够的通道 + while (multiChannelBuffer.size < timePointData.size) { + multiChannelBuffer.add(mutableListOf()) + } + + // 添加数据到各通道 + timePointData.forEachIndexed { index, dataPoint -> + if (index < multiChannelBuffer.size) { + multiChannelBuffer[index].add(dataPoint) + totalDataConsumed++ + hasNewData = true + isDataAvailable = true // 设置数据可用标志 + } + } + } + + // 控制各通道缓冲区大小 + multiChannelBuffer.forEach { buffer -> + if (buffer.size > maxBufferSize) { + val excess = buffer.size - maxBufferSize + repeat(excess) { + if (buffer.isNotEmpty()) { + buffer.removeAt(0) + } + } + } + } + // 更新多通道数据范围 + updateMultiChannelDataRange() + } + + // 如果有新数据,触发UI更新 + if (hasNewData) { + postInvalidate() // 在主线程更新UI + Log.d("DynamicChartView", "消费者:定时处理完成,单通道队列: ${dataQueue.size}, 多通道队列: ${multiChannelDataQueue.size}") + } + + // 定期打印统计信息 + if (currentTime - lastDisplayTime > 5000) { + Log.d("DynamicChartView", "统计 - 接收: $totalDataPointsReceived, 消费: $totalDataConsumed, 队列: ${dataQueue.size + multiChannelDataQueue.size}") + lastDisplayTime = currentTime + } + + // 定期检查内存使用情况 + if (currentTime - lastMemoryCheckTime > 1000) { // 每秒检查一次 + checkMemoryUsage() + lastMemoryCheckTime = currentTime + } + } catch (e: Exception) { + Log.e("DynamicChartView", "消费者处理数据时发生异常: ${e.message}", e) } - - // 定期检查内存使用情况 - checkMemoryUsage() - - // 更新数据范围 - updateDataRange(newData) - - // 立即重绘 - invalidate() } /** @@ -249,23 +569,20 @@ class DynamicChartView @JvmOverloads constructor( */ private fun checkMemoryUsage() { try { - // 每1000个数据点检查一次内存使用情况 - if (totalDataPointsReceived % 1000 == 0L) { - val runtime = Runtime.getRuntime() - val usedMemory = runtime.totalMemory() - runtime.freeMemory() - val maxMemory = runtime.maxMemory() - val memoryUsagePercent = (usedMemory.toDouble() / maxMemory.toDouble()) * 100 + val runtime = Runtime.getRuntime() + val usedMemory = runtime.totalMemory() - runtime.freeMemory() + val maxMemory = runtime.maxMemory() + val memoryUsagePercent = (usedMemory.toDouble() / maxMemory.toDouble()) * 100 - Log.d("DynamicChartView", "内存使用情况: ${usedMemory / 1024 / 1024}MB/${maxMemory / 1024 / 1024}MB (${String.format("%.1f", memoryUsagePercent)}%)") + Log.d("DynamicChartView", "内存使用情况: ${usedMemory / 1024 / 1024}MB/${maxMemory / 1024 / 1024}MB (${String.format("%.1f", memoryUsagePercent)}%)") - // 如果内存使用超过80%,强制清理 - if (memoryUsagePercent > 80.0) { - Log.w("DynamicChartView", "内存使用过高 (${String.format("%.1f", memoryUsagePercent)}%),执行紧急清理") - emergencyCleanup() - } + // 如果内存使用超过80%,强制清理 + if (memoryUsagePercent > 80.0) { + Log.w("DynamicChartView", "内存使用过高 (${String.format("%.1f", memoryUsagePercent)}%),执行紧急清理") + emergencyCleanup() } } catch (e: Exception) { - Log.e("DynamicChartView", "检查内存使用情况失败: ${e.message}") + Log.e("DynamicChartView", "检查内存使用情况失败: ${e.message}", e) } } @@ -277,22 +594,22 @@ class DynamicChartView @JvmOverloads constructor( Log.d("DynamicChartView", "=== 执行紧急内存清理 ===") // 强制清理数据点,只保留最新的50%数据 - val keepSize = (dataPoints.size * 0.5).toInt() - if (keepSize > 0 && dataPoints.size > keepSize) { - dataPoints = dataPoints.takeLast(keepSize).toMutableList() + val keepSize = (dataBuffer.size * 0.5).toInt() + if (keepSize > 0 && dataBuffer.size > keepSize) { + dataBuffer = dataBuffer.takeLast(keepSize).toMutableList() Log.d("DynamicChartView", "紧急清理数据点,保留 $keepSize 个数据点") } // 清理多通道数据 - multiChannelDataPoints.forEachIndexed { index, channelData -> + multiChannelBuffer.forEachIndexed { index, channelData -> val keepChannelSize = (channelData.size * 0.5).toInt() if (keepChannelSize > 0 && channelData.size > keepChannelSize) { - multiChannelDataPoints[index] = channelData.takeLast(keepChannelSize).toMutableList() + multiChannelBuffer[index] = channelData.takeLast(keepChannelSize).toMutableList() } } // 重新计算数据范围 - updateDataRange(dataPoints) + updateDataRange(dataBuffer) // 强制垃圾回收 System.gc() @@ -309,9 +626,9 @@ class DynamicChartView @JvmOverloads constructor( */ fun getDataPointCount(): Int { return if (isMultiChannelMode) { - multiChannelDataPoints.sumOf { it.size } + multiChannelBuffer.sumOf { it.size } } else { - dataPoints.size + dataBuffer.size } } @@ -322,19 +639,19 @@ class DynamicChartView @JvmOverloads constructor( try { if (isMultiChannelMode) { // 多通道模式 - multiChannelDataPoints.forEachIndexed { index, channelData -> + multiChannelBuffer.forEachIndexed { index, channelData -> if (channelData.size > maxSize) { - multiChannelDataPoints[index] = channelData.takeLast(maxSize).toMutableList() + multiChannelBuffer[index] = channelData.takeLast(maxSize).toMutableList() } } // 重新计算数据范围 updateMultiChannelDataRange() } else { // 单通道模式 - if (dataPoints.size > maxSize) { - dataPoints = dataPoints.takeLast(maxSize).toMutableList() + if (dataBuffer.size > maxSize) { + dataBuffer = dataBuffer.takeLast(maxSize).toMutableList() // 重新计算数据范围 - updateDataRange(dataPoints) + updateDataRange(dataBuffer) } } @@ -346,53 +663,6 @@ class DynamicChartView @JvmOverloads constructor( Log.e("DynamicChartView", "裁剪数据失败: ${e.message}") } } - - /** - * 更新多通道数据(用于PPG等设备) - */ - fun updateMultiChannelData(channelData: List>) { - if (channelData.isEmpty() || !isMultiChannelMode) { - Log.d("DynamicChartView", "跳过多通道更新: channelData.isEmpty=${channelData.isEmpty()}, isMultiChannelMode=$isMultiChannelMode") - return - } - - Log.d("DynamicChartView", "多通道数据更新: 通道数=${channelData.size}, 每个通道数据点=${channelData.map { it.size }}") - isDataAvailable = true - - // 更新累积数据点计数器(使用第一个通道的数据点数量) - if (channelData.isNotEmpty()) { - totalDataPointsReceived += channelData[0].size - } - - // 直接添加新数据,来多少显示多少 - for (channelIndex in channelData.indices) { - if (channelIndex >= multiChannelDataPoints.size) { - Log.w("DynamicChartView", "通道索引超出范围: $channelIndex >= ${multiChannelDataPoints.size}") - continue - } - - val channelDataPoints = multiChannelDataPoints[channelIndex] - channelDataPoints.addAll(channelData[channelIndex]) - - // 限制数据点数量,保持滚动显示 - if (channelDataPoints.size > maxDataPoints) { - multiChannelDataPoints[channelIndex] = channelDataPoints.takeLast(maxDataPoints).toMutableList() - } - - Log.d("DynamicChartView", "通道$channelIndex 添加了${channelData[channelIndex].size}个数据点") - } - - // 更新数据范围 - 计算所有通道的数据范围 - updateMultiChannelDataRange() - - Log.d("DynamicChartView", "多通道数据处理完成: 通道数据点=${multiChannelDataPoints.map { it.size }}, 范围=${minValue}-${maxValue}") - - // 检查内存使用情况 - checkMemoryUsage() - - // 立即重绘 - invalidate() - } private fun updateDataRange(newData: List) { // 如果设置了自定义Y轴范围,使用自定义范围 @@ -403,23 +673,24 @@ class DynamicChartView @JvmOverloads constructor( } // 使用全部数据点计算范围,提供更准确的范围 - if (dataPoints.isEmpty()) return + if (dataBuffer.isEmpty()) return // 计算全部数据点的范围 - minValue = dataPoints.minOrNull() ?: 0f - maxValue = dataPoints.maxOrNull() ?: 0f + minValue = dataBuffer.minOrNull() ?: 0f + maxValue = dataBuffer.maxOrNull() ?: 0f // 确保有足够的显示范围 val range = maxValue - minValue - if (range < 0.1f) { + if (range < 1.0f) { // 增加最小范围阈值,适应更多数据类型 val center = (maxValue + minValue) / 2 - minValue = center - 0.05f - maxValue = center + 0.05f + minValue = center - 0.5f + maxValue = center + 0.5f } else { - val margin = range * 0.1f + val margin = range * 0.2f // 增加边距比例,显示更宽松 minValue -= margin maxValue += margin } + Log.d("DynamicChartView", "单通道显示范围调整: ${minValue}-${maxValue}") } /** @@ -434,7 +705,7 @@ class DynamicChartView @JvmOverloads constructor( return } - if (multiChannelDataPoints.isEmpty()) { + if (multiChannelBuffer.isEmpty()) { Log.w("DynamicChartView", "多通道数据点为空,无法计算范围") return } @@ -444,8 +715,8 @@ class DynamicChartView @JvmOverloads constructor( var globalMax = Float.MIN_VALUE var hasValidData = false - for (channelIndex in multiChannelDataPoints.indices) { - val channelData = multiChannelDataPoints[channelIndex] + for (channelIndex in multiChannelBuffer.indices) { + val channelData = multiChannelBuffer[channelIndex] if (channelData.isNotEmpty()) { val channelMin = channelData.minOrNull() ?: 0f val channelMax = channelData.maxOrNull() ?: 0f @@ -468,12 +739,12 @@ class DynamicChartView @JvmOverloads constructor( // 确保有足够的显示范围 val range = maxValue - minValue - if (range < 0.1f) { + if (range < 1.0f) { // 增加最小范围阈值 val center = (maxValue + minValue) / 2 - minValue = center - 0.05f - maxValue = center + 0.05f + minValue = center - 0.5f + maxValue = center + 0.5f } else { - val margin = range * 0.1f + val margin = range * 0.2f // 增加边距比例 minValue -= margin maxValue += margin } @@ -486,25 +757,17 @@ class DynamicChartView @JvmOverloads constructor( Log.d("DynamicChartView", "=== 开始清理图表数据 ===") // 清理单通道数据 - dataPoints.clear() dataBuffer.clear() Log.d("DynamicChartView", "单通道数据已清理") // 清除多通道数据 - multiChannelDataPoints.forEachIndexed { index, channelData -> + multiChannelBuffer.forEachIndexed { index, channelData -> try { channelData.clear() } catch (e: Exception) { Log.e("DynamicChartView", "清理多通道数据 $index 失败: ${e.message}", e) } } - multiChannelDataBuffers.forEachIndexed { index, channelBuffer -> - try { - channelBuffer.clear() - } catch (e: Exception) { - Log.e("DynamicChartView", "清理多通道缓冲区 $index 失败: ${e.message}", e) - } - } Log.d("DynamicChartView", "多通道数据已清理") // 重置状态 @@ -553,6 +816,10 @@ class DynamicChartView @JvmOverloads constructor( * 当视图从窗口分离时调用 */ override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + stopConsumer() + displayExecutor.shutdown() + Log.d("DynamicChartView", "视图已销毁,消费者线程已停止") try { Log.d("DynamicChartView", "=== onDetachedFromWindow 开始 ===") @@ -675,9 +942,9 @@ class DynamicChartView @JvmOverloads constructor( } else { // 绘制曲线 - 检查单通道或多通道数据 val hasDataToDraw = if (isMultiChannelMode) { - multiChannelDataPoints.any { it.size > 1 } + multiChannelBuffer.any { it.size > 1 } } else { - dataPoints.size > 1 + dataBuffer.size > 1 } if (hasDataToDraw) { @@ -756,13 +1023,13 @@ class DynamicChartView @JvmOverloads constructor( private fun drawStats(canvas: Canvas, width: Float, height: Float) { val totalDataPoints = if (isMultiChannelMode) { - multiChannelDataPoints.sumOf { it.size } + multiChannelBuffer.sumOf { it.size } } else { - dataPoints.size + dataBuffer.size } val statsText = if (isMultiChannelMode) { - "显示数据点: ${totalDataPoints} (${multiChannelDataPoints.size}通道) | 累积: ${totalDataPointsReceived}" + "显示数据点: ${totalDataPoints} (${multiChannelBuffer.size}通道) | 累积: ${totalDataPointsReceived}" } else { "显示数据点: ${totalDataPoints} | 累积: ${totalDataPointsReceived}" } @@ -852,11 +1119,11 @@ class DynamicChartView @JvmOverloads constructor( val drawWidth = width - 2 * padding val drawHeight = height - 2 * padding - if (isMultiChannelMode && multiChannelDataPoints.isNotEmpty()) { - Log.d("DynamicChartView", "多通道绘制: 通道数=${multiChannelDataPoints.size}, 数据点=${multiChannelDataPoints.map { it.size }}") + if (isMultiChannelMode && multiChannelBuffer.isNotEmpty()) { + Log.d("DynamicChartView", "多通道绘制: 通道数=${multiChannelBuffer.size}, 数据点=${multiChannelBuffer.map { it.size }}") // 多通道绘制模式 - for (channelIndex in multiChannelDataPoints.indices) { - val channelData = multiChannelDataPoints[channelIndex] + for (channelIndex in multiChannelBuffer.indices) { + val channelData = multiChannelBuffer[channelIndex] if (channelData.isEmpty()) { Log.d("DynamicChartView", "通道$channelIndex 数据为空,跳过绘制") continue @@ -870,11 +1137,13 @@ class DynamicChartView @JvmOverloads constructor( strokeWidth = 2f } - val xStep = if (channelData.size > 1) drawWidth / (channelData.size - 1) else drawWidth + // 采样数据以提高性能 + val sampledData = sampleData(channelData, maxDisplayPoints) + val xStep = if (sampledData.size > 1) drawWidth / (sampledData.size - 1) else drawWidth - for (i in channelData.indices) { + for (i in sampledData.indices) { val x = padding + i * xStep - val normalizedValue = (channelData[i] - minValue) / (maxValue - minValue) + val normalizedValue = (sampledData[i] - minValue) / (maxValue - minValue) val y = padding + (0.1f + normalizedValue * 0.8f) * drawHeight if (i == 0) { @@ -888,14 +1157,17 @@ class DynamicChartView @JvmOverloads constructor( } } else { // 单通道绘制模式 - if (dataPoints.isEmpty()) return + if (dataBuffer.isEmpty()) return path.reset() - val xStep = if (dataPoints.size > 1) drawWidth / (dataPoints.size - 1) else drawWidth - for (i in dataPoints.indices) { + // 采样数据以提高性能 + val sampledData = sampleData(dataBuffer, maxDisplayPoints) + val xStep = if (sampledData.size > 1) drawWidth / (sampledData.size - 1) else drawWidth + + for (i in sampledData.indices) { val x = padding + i * xStep - val normalizedValue = (dataPoints[i] - minValue) / (maxValue - minValue) + val normalizedValue = (sampledData[i] - minValue) / (maxValue - minValue) val y = padding + (0.1f + normalizedValue * 0.8f) * drawHeight if (i == 0) { @@ -909,6 +1181,21 @@ class DynamicChartView @JvmOverloads constructor( } } + /** + * 对数据进行采样以减少绘制点数,提高性能 + */ + private fun sampleData(data: List, maxPoints: Int): List { + if (data.size <= maxPoints) return data + + val step = data.size.toDouble() / maxPoints.toDouble() + val sampled = mutableListOf() + for (i in 0 until maxPoints) { + val index = (i * step).toInt().coerceIn(0, data.size - 1) + sampled.add(data[index]) + } + return sampled + } + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) @@ -921,7 +1208,7 @@ class DynamicChartView @JvmOverloads constructor( * 获取当前图表数据 */ fun getCurrentData(): List { - return dataPoints.toList() + return dataBuffer.toList() } /** @@ -929,15 +1216,15 @@ class DynamicChartView @JvmOverloads constructor( */ fun getDataStats(): Map { val totalDataPoints = if (isMultiChannelMode) { - multiChannelDataPoints.sumOf { it.size } + multiChannelBuffer.sumOf { it.size } } else { - dataPoints.size + dataBuffer.size } return mapOf( "dataPoints" to totalDataPoints, "multiChannelMode" to isMultiChannelMode, - "channelCount" to if (isMultiChannelMode) multiChannelDataPoints.size else 1, + "channelCount" to if (isMultiChannelMode) multiChannelBuffer.size else 1, "minValue" to minValue, "maxValue" to maxValue, "range" to (maxValue - minValue), diff --git a/app/src/main/java/com/example/cmake_project_test/FullscreenChartActivity.kt b/app/src/main/java/com/example/cmake_project_test/FullscreenChartActivity.kt index 6186ad5..4a887eb 100644 --- a/app/src/main/java/com/example/cmake_project_test/FullscreenChartActivity.kt +++ b/app/src/main/java/com/example/cmake_project_test/FullscreenChartActivity.kt @@ -117,6 +117,18 @@ class FullscreenChartActivity : AppCompatActivity(), DataManager.RealTimeDataCal // 初始化滤波器管理器 setupFilterManager() + // 设置最大显示采样点数量 + val sampleRate = filterManager?.getSampleRate() ?: 250.0 + val deviceType = intent.getStringExtra("deviceType") ?: "未知设备" + val maxDisplaySamples = when { + deviceType.contains("EOG", ignoreCase = true) -> 2500 // EOG设备固定显示2500个数据点 + deviceType.contains("PPTM", ignoreCase = true) -> 500 // PPTM设备固定显示500个数据点 + deviceType.contains("ECG", ignoreCase = true) -> 500 // ECG设备固定显示500个数据点 + else -> (sampleRate * 10.0).toInt().coerceAtMost(10000) // 其他设备限制在10秒数据或10000点以内 + } + fullscreenChartManager?.getCharts()?.forEach { chart -> + chart.setMaxDisplaySamples(maxDisplaySamples) + } } @@ -561,7 +573,11 @@ class FullscreenChartActivity : AppCompatActivity(), DataManager.RealTimeDataCal } // 通过滤波器管理器处理数据 - val charts = chartManager.getCharts() + val charts = if (isSingleChart) { + listOfNotNull(fullscreenChartManager?.getCharts()?.getOrNull(chartIndex)) + } else { + fullscreenChartManager?.getCharts() ?: emptyList() + } if (charts.isNotEmpty()) { val chart = charts.getOrNull(0) if (chart == null) { @@ -696,7 +712,11 @@ class FullscreenChartActivity : AppCompatActivity(), DataManager.RealTimeDataCal } // 通过滤波器管理器处理数据 - val charts = chartManager.getCharts() + val charts = if (isSingleChart) { + listOfNotNull(fullscreenChartManager?.getCharts()?.getOrNull(chartIndex)) + } else { + fullscreenChartManager?.getCharts() ?: emptyList() + } if (charts.isNotEmpty()) { val chart = charts.getOrNull(channelIndex.coerceAtMost(charts.size - 1)) if (chart == null) { diff --git a/app/src/main/java/com/example/cmake_project_test/FullscreenFilterManager.kt b/app/src/main/java/com/example/cmake_project_test/FullscreenFilterManager.kt index 97ccc81..946fd76 100644 --- a/app/src/main/java/com/example/cmake_project_test/FullscreenFilterManager.kt +++ b/app/src/main/java/com/example/cmake_project_test/FullscreenFilterManager.kt @@ -31,9 +31,9 @@ class FullscreenFilterManager( ) /** - * 根据设备类型获取采样率 + * 获取当前采样率 */ - private fun getSampleRate(): Double { + fun getSampleRate(): Double { return when (deviceType) { "ECG_2LEAD", "ECG_12LEAD", "PW_ECG_SL", "EEG" -> 250.0 "PPG" -> 50.0 @@ -55,6 +55,7 @@ class FullscreenFilterManager( private var maxDataPointsPerBatch = 100 // 每次最多处理100个数据点 private val dataBuffer = mutableListOf() private var maxBufferSize = 500 // 缓冲区最大大小(动态调整) + private var maxDisplaySamples = 0 // 最大显示采样点数量 // UI组件引用 private var filterButton: Button? = null @@ -101,20 +102,24 @@ class FullscreenFilterManager( val sampleRate = getSampleRate() when { sampleRate >= 8000.0 -> { // 音频设备 - maxDataPointsPerBatch = 50 - maxBufferSize = 200 - } - sampleRate >= 1000.0 -> { // 高速设备 maxDataPointsPerBatch = 100 maxBufferSize = 400 + maxDisplaySamples = (sampleRate * 10).toInt() // 显示10秒数据 } - sampleRate >= 250.0 -> { // 中速设备(ECG等) - maxDataPointsPerBatch = 150 - maxBufferSize = 600 - } - else -> { // 低速设备 + sampleRate >= 1000.0 -> { // 高速设备 maxDataPointsPerBatch = 200 maxBufferSize = 800 + maxDisplaySamples = (sampleRate * 10).toInt() // 显示10秒数据 + } + sampleRate >= 250.0 -> { // 中速设备(ECG等) + maxDataPointsPerBatch = 300 + maxBufferSize = 1200 + maxDisplaySamples = (sampleRate * 10).toInt() // 显示10秒数据 + } + else -> { // 低速设备 + maxDataPointsPerBatch = 400 + maxBufferSize = 1600 + maxDisplaySamples = (sampleRate * 10).toInt() // 显示10秒数据 } } 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 67aa2de..59ca30c 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 @@ -744,33 +744,58 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real Log.d("MainActivity", "数据前10字节: ${data.take(10).joinToString(", ") { "0x%02X".format(it) }}") } + val currentTime = System.currentTimeMillis() + + // 传输速率限制:防止过高频率导致闪退 + if (lastDataTime > 0) { + val timeDiff = currentTime - lastDataTime + if (timeDiff < 20) { // 小于20ms间隔,跳过处理 + Log.w("MainActivity", "数据传输频率过高(${timeDiff}ms),跳过本次处理") + return + } + } + // 更新数据统计 receivedDataCount += data.size - val currentTime = System.currentTimeMillis() if (lastDataTime > 0) { val timeDiff = currentTime - lastDataTime if (timeDiff > 0) { dataRate = (data.size * 1000f) / timeDiff // 字节/秒 + + // 如果传输速率超过阈值,执行紧急处理 + if (dataRate > 15000) { // 超过15KB/s + Log.w("MainActivity", "传输速率过高(${String.format("%.1f", dataRate)}字节/秒),执行紧急清理") + dataManager.checkMemoryUsage() + } } } lastDataTime = currentTime + // 定期检查内存使用情况,防止闪退 + if (currentTime - lastDataTime > 5000) { // 每5秒检查一次 + dataManager.checkMemoryUsage() + } - // 立即显示动态图表容器 - runOnUiThread { - binding.dynamicChartContainer.visibility = View.VISIBLE - - // 如果数据处理还没启动,自动启动 - if (!dataProcessingStarted) { - startDataProcessing() - updateStatus("检测到数据,自动启动数据处理") - } - - // 减少数据接收状态更新频率 - if (currentTime - lastDataStatusTime > 5000) { // 改为5秒内只更新一次状态 - updateStatus("接收到蓝牙数据: ${data.size} 字节,图表已显示") - updateDataStatistics() - lastDataStatusTime = currentTime + + // UI更新频率限制:防止过高频率导致UI阻塞 + val shouldUpdateUI = currentTime - lastDataStatusTime > 100 // 最多每100ms更新一次UI + + if (shouldUpdateUI) { + runOnUiThread { + binding.dynamicChartContainer.visibility = View.VISIBLE + + // 如果数据处理还没启动,自动启动 + if (!dataProcessingStarted) { + startDataProcessing() + updateStatus("检测到数据,自动启动数据处理") + } + + // 减少数据接收状态更新频率 + if (currentTime - lastDataStatusTime > 2000) { // 改为2秒内只更新一次状态 + updateStatus("接收到蓝牙数据: ${data.size} 字节,图表已显示") + updateDataStatistics() + lastDataStatusTime = currentTime + } } } 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 new file mode 100644 index 0000000..2630a6b --- /dev/null +++ b/app/src/main/java/com/example/cmake_project_test/ProducerConsumerChartView.kt @@ -0,0 +1,484 @@ +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/StreamingSignalProcessor.kt b/app/src/main/java/com/example/cmake_project_test/StreamingSignalProcessor.kt index 077853f..1aff0c7 100644 --- a/app/src/main/java/com/example/cmake_project_test/StreamingSignalProcessor.kt +++ b/app/src/main/java/com/example/cmake_project_test/StreamingSignalProcessor.kt @@ -15,9 +15,9 @@ class StreamingSignalProcessor { private var signalProcessorInitialized = false // 窗口参数 - 针对ECG信号优化,进一步提高实时性 - private var windowSize = 50 // 优化:0.2秒窗口(250Hz采样率),进一步提高实时性 - private var overlapSize = 10 // 优化:20%重叠,减少边界效应 - private var stepSize = windowSize - overlapSize // 步长:40个样本 + private var windowSize = 30 // 优化:0.12秒窗口(250Hz采样率),进一步提高实时性 + private var overlapSize = 6 // 优化:20%重叠,减少边界效应 + private var stepSize = windowSize - overlapSize // 步长:24个样本 // 数据缓冲区 private val dataBuffer = mutableListOf()