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()