APP COMMIT
This commit is contained in:
parent
42250dff0e
commit
a3be0bf6ca
|
|
@ -4,6 +4,14 @@
|
||||||
<selectionStates>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="Unnamed">
|
<SelectionState runConfigName="Unnamed">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
<DropdownSelection timestamp="2025-09-18T02:13:50.928134500Z">
|
||||||
|
<Target type="DEFAULT_BOOT">
|
||||||
|
<handle>
|
||||||
|
<DeviceId pluginId="PhysicalDevice" identifier="serial=ba2e16dd" />
|
||||||
|
</handle>
|
||||||
|
</Target>
|
||||||
|
</DropdownSelection>
|
||||||
|
<DialogSelection />
|
||||||
</SelectionState>
|
</SelectionState>
|
||||||
</selectionStates>
|
</selectionStates>
|
||||||
</component>
|
</component>
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,6 @@
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
<mapping directory="$PROJECT_DIR$/../../../SDK_APP(1)/SDK_APP" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
|
|
@ -2,7 +2,7 @@ package com.example.cmake_project_test
|
||||||
|
|
||||||
object Constants {
|
object Constants {
|
||||||
// UI更新相关
|
// UI更新相关
|
||||||
const val UPDATE_INTERVAL = 100L // 优化:每100毫秒更新一次UI,提高响应性
|
const val UPDATE_INTERVAL = 200L // 优化:每200毫秒更新一次UI,减慢更新速度以便观察心电波形
|
||||||
|
|
||||||
// 数据分块相关
|
// 数据分块相关
|
||||||
const val CHUNK_SIZE = 64 // 数据分块大小
|
const val CHUNK_SIZE = 64 // 数据分块大小
|
||||||
|
|
@ -16,7 +16,7 @@ object Constants {
|
||||||
const val MAX_DETAIL_PACKETS = 3 // 最大详情数据包数量
|
const val MAX_DETAIL_PACKETS = 3 // 最大详情数据包数量
|
||||||
const val MAX_DISPLAY_CHANNELS = 4 // 最大显示通道数量
|
const val MAX_DISPLAY_CHANNELS = 4 // 最大显示通道数量
|
||||||
const val MAX_12LEAD_CHANNELS = 6 // 12导联心电最大通道数量
|
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" // 默认数据文件名
|
const val DEFAULT_DATA_FILE = "ecg_data_raw.dat" // 默认数据文件名
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,35 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
|
||||||
return
|
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", "接收到蓝牙数据: ${chunk.size} 字节")
|
||||||
Log.d("DataManager", "数据前10字节: ${chunk.take(10).joinToString(", ") { "0x%02X".format(it) }}")
|
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 个最新数据包")
|
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个数据包
|
// 清理处理后数据包缓冲区,保留最新的50个数据包
|
||||||
val maxKeepProcessedPackets = 50
|
val maxKeepProcessedPackets = 50
|
||||||
if (processedPackets.size > maxKeepProcessedPackets) {
|
if (processedPackets.size > maxKeepProcessedPackets) {
|
||||||
|
|
@ -879,11 +919,67 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
|
||||||
Log.d("DataManager", "清理了 $toRemove 个旧处理后数据包,保留 $maxKeepProcessedPackets 个最新数据包")
|
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) {
|
} catch (e: Exception) {
|
||||||
Log.e("DataManager", "清理缓冲区时发生错误: ${e.message}")
|
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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -197,12 +197,12 @@ class DynamicChartManager(
|
||||||
for (i in 0 until config.channelCount) {
|
for (i in 0 until config.channelCount) {
|
||||||
val chartView = DynamicChartView(context)
|
val chartView = DynamicChartView(context)
|
||||||
|
|
||||||
// 设置图表配置
|
// 设置图表配置,传递设备类型信息
|
||||||
val channelName = if (i < config.channelNames.size) config.channelNames[i] else "通道"
|
val channelName = if (i < config.channelNames.size) config.channelNames[i] else "通道"
|
||||||
chartView.setChartConfig(
|
chartView.setChartConfig(
|
||||||
title = channelName,
|
title = channelName,
|
||||||
channelIdx = i + 1,
|
channelIdx = i + 1,
|
||||||
device = config.deviceName,
|
device = "${config.deviceName}_${dataType.name}", // 包含设备类型信息
|
||||||
maxPoints = config.maxDataPoints,
|
maxPoints = config.maxDataPoints,
|
||||||
sampleRateHz = config.sampleRate,
|
sampleRateHz = config.sampleRate,
|
||||||
timeWindowSec = config.timeWindow
|
timeWindowSec = config.timeWindow
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,11 @@ import android.view.View
|
||||||
import android.view.ScaleGestureDetector
|
import android.view.ScaleGestureDetector
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
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 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轴范围
|
// 全屏模式和自定义Y轴范围
|
||||||
private var isFullscreenMode = false
|
private var isFullscreenMode = false
|
||||||
private var customMinValue: Float? = null
|
private var customMinValue: Float? = null
|
||||||
|
|
@ -74,12 +91,37 @@ class DynamicChartView @JvmOverloads constructor(
|
||||||
private var sampleRate = 250f // 默认采样率
|
private var sampleRate = 250f // 默认采样率
|
||||||
private var timeWindow = 4f // 默认时间窗口(秒)
|
private var timeWindow = 4f // 默认时间窗口(秒)
|
||||||
|
|
||||||
// 简化的数据管理:直接显示新数据
|
// ==================== 生产者-消费者模式核心 ====================
|
||||||
private val dataBuffer = mutableListOf<Float>()
|
|
||||||
|
// 生产者:蓝牙数据回调线程
|
||||||
|
private val dataQueue = LinkedBlockingQueue<Float>(10000) // 增加队列容量,存储单个数据点
|
||||||
|
private val multiChannelDataQueue = LinkedBlockingQueue<List<Float>>(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<Float>()
|
||||||
|
private var maxBufferSize = 5000 // 调整最大缓冲区大小
|
||||||
|
private var maxDisplayPoints = 2500 // 默认最大显示点数,增加以显示更多数据
|
||||||
|
private var maxDisplaySamples = 2500 // 最大显示采样点数量,从FullscreenFilterManager获取
|
||||||
|
|
||||||
|
// 多通道数据缓冲区
|
||||||
|
private val multiChannelBuffer = mutableListOf<MutableList<Float>>()
|
||||||
|
|
||||||
// 多通道数据支持(用于PPG等设备)
|
// 多通道数据支持(用于PPG等设备)
|
||||||
private val multiChannelDataPoints = mutableListOf<MutableList<Float>>()
|
// 移除冗余的数据结构
|
||||||
private val multiChannelDataBuffers = mutableListOf<MutableList<Float>>()
|
// private val multiChannelDataPoints = mutableListOf<MutableList<Float>>()
|
||||||
|
// private val multiChannelDataBuffers = mutableListOf<MutableList<Float>>()
|
||||||
private var isMultiChannelMode = false
|
private var isMultiChannelMode = false
|
||||||
private val channelColors = listOf(Color.RED, Color.BLUE) // 通道颜色
|
private val channelColors = listOf(Color.RED, Color.BLUE) // 通道颜色
|
||||||
|
|
||||||
|
|
@ -99,6 +141,14 @@ class DynamicChartView @JvmOverloads constructor(
|
||||||
// 控制按钮区域
|
// 控制按钮区域
|
||||||
private val controlButtonRect = RectF()
|
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)
|
maxDataPoints = optimizeMaxDataPoints(maxPoints, sampleRateHz)
|
||||||
sampleRate = sampleRateHz
|
sampleRate = sampleRateHz
|
||||||
timeWindow = timeWindowSec
|
timeWindow = timeWindowSec
|
||||||
|
|
||||||
|
// 根据设备类型和采样率设置刷新间隔
|
||||||
|
deviceRefreshInterval = calculateRefreshInterval(device, sampleRateHz, maxPoints)
|
||||||
|
|
||||||
resetViewport()
|
resetViewport()
|
||||||
invalidate()
|
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 {
|
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 >= 8000f -> (baseMaxPoints * 0.3).toInt() // 音频设备减少到30%
|
||||||
sampleRate >= 1000f -> (baseMaxPoints * 0.5).toInt() // 高速设备减少到50%
|
sampleRate >= 1000f -> (baseMaxPoints * 0.5).toInt() // 高速设备减少到50%
|
||||||
sampleRate >= 250f -> baseMaxPoints // ECG等保持不变
|
sampleRate >= 250f -> baseMaxPoints // 其他设备保持不变
|
||||||
else -> (baseMaxPoints * 1.2).toInt() // 低速设备可以稍微增加
|
else -> (baseMaxPoints * 1.2).toInt() // 低速设备可以稍微增加
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,8 +228,6 @@ class DynamicChartView @JvmOverloads constructor(
|
||||||
else -> optimizedPoints
|
else -> optimizedPoints
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保在合理范围内
|
|
||||||
optimizedPoints = optimizedPoints.coerceIn(500, 10000)
|
|
||||||
|
|
||||||
Log.d("DynamicChartView", "数据点限制优化: 原始=$baseMaxPoints, 优化后=$optimizedPoints, 采样率=${sampleRate}Hz, 内存=${maxMemoryMB}MB")
|
Log.d("DynamicChartView", "数据点限制优化: 原始=$baseMaxPoints, 优化后=$optimizedPoints, 采样率=${sampleRate}Hz, 内存=${maxMemoryMB}MB")
|
||||||
return optimizedPoints
|
return optimizedPoints
|
||||||
|
|
@ -200,48 +280,288 @@ class DynamicChartView @JvmOverloads constructor(
|
||||||
showFullscreenButton = show
|
showFullscreenButton = show
|
||||||
invalidate()
|
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等设备)
|
* 设置多通道模式(用于PPG等设备)
|
||||||
*/
|
*/
|
||||||
fun setMultiChannelMode(channelCount: Int) {
|
fun setMultiChannelMode(channelCount: Int) {
|
||||||
isMultiChannelMode = true
|
isMultiChannelMode = true
|
||||||
multiChannelDataPoints.clear()
|
multiChannelBuffer.clear()
|
||||||
multiChannelDataBuffers.clear()
|
|
||||||
|
|
||||||
// 初始化每个通道的数据结构
|
// 初始化每个通道的数据结构
|
||||||
for (i in 0 until channelCount) {
|
for (i in 0 until channelCount) {
|
||||||
multiChannelDataPoints.add(mutableListOf())
|
multiChannelBuffer.add(mutableListOf())
|
||||||
multiChannelDataBuffers.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<Float>) {
|
fun updateData(newData: List<Float>) {
|
||||||
if (newData.isEmpty()) return
|
if (newData.isEmpty()) return
|
||||||
|
|
||||||
isDataAvailable = true
|
|
||||||
|
|
||||||
// 更新累积数据点计数器
|
// 生产者:将单个数据点放入队列
|
||||||
totalDataPointsReceived += newData.size
|
for (dataPoint in newData) {
|
||||||
|
var success = dataQueue.offer(dataPoint)
|
||||||
// 直接添加新数据,来多少显示多少
|
if (!success) {
|
||||||
dataPoints.addAll(newData)
|
Log.w("DynamicChartView", "生产者:队列已满,尝试清理旧数据")
|
||||||
|
// 队列满时,移除旧数据并再次尝试添加新数据
|
||||||
// 限制数据点数量,保持滚动显示
|
val removedPoint = dataQueue.poll()
|
||||||
if (dataPoints.size > maxDataPoints) {
|
if (removedPoint != null) {
|
||||||
val excessData = dataPoints.size - maxDataPoints
|
Log.d("DynamicChartView", "生产者:移除了 1 个旧数据点")
|
||||||
dataPoints = dataPoints.takeLast(maxDataPoints).toMutableList()
|
success = dataQueue.offer(dataPoint)
|
||||||
Log.d("DynamicChartView", "清理了 $excessData 个数据点,当前数据点: ${dataPoints.size}")
|
}
|
||||||
|
if (!success) {
|
||||||
|
Log.w("DynamicChartView", "生产者:再次尝试失败,丢弃数据点")
|
||||||
|
} else {
|
||||||
|
totalDataPointsReceived++
|
||||||
|
Log.d("DynamicChartView", "生产者:数据点已入队,队列大小: ${dataQueue.size}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
totalDataPointsReceived++
|
||||||
|
Log.d("DynamicChartView", "生产者:数据点已入队,队列大小: ${dataQueue.size}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生产者:接收多通道数据
|
||||||
|
*/
|
||||||
|
fun updateMultiChannelData(channelData: List<List<Float>>) {
|
||||||
|
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<Float>()
|
||||||
|
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<List<Float>>()
|
||||||
|
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() {
|
private fun checkMemoryUsage() {
|
||||||
try {
|
try {
|
||||||
// 每1000个数据点检查一次内存使用情况
|
val runtime = Runtime.getRuntime()
|
||||||
if (totalDataPointsReceived % 1000 == 0L) {
|
val usedMemory = runtime.totalMemory() - runtime.freeMemory()
|
||||||
val runtime = Runtime.getRuntime()
|
val maxMemory = runtime.maxMemory()
|
||||||
val usedMemory = runtime.totalMemory() - runtime.freeMemory()
|
val memoryUsagePercent = (usedMemory.toDouble() / maxMemory.toDouble()) * 100
|
||||||
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%,强制清理
|
// 如果内存使用超过80%,强制清理
|
||||||
if (memoryUsagePercent > 80.0) {
|
if (memoryUsagePercent > 80.0) {
|
||||||
Log.w("DynamicChartView", "内存使用过高 (${String.format("%.1f", memoryUsagePercent)}%),执行紧急清理")
|
Log.w("DynamicChartView", "内存使用过高 (${String.format("%.1f", memoryUsagePercent)}%),执行紧急清理")
|
||||||
emergencyCleanup()
|
emergencyCleanup()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} 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", "=== 执行紧急内存清理 ===")
|
Log.d("DynamicChartView", "=== 执行紧急内存清理 ===")
|
||||||
|
|
||||||
// 强制清理数据点,只保留最新的50%数据
|
// 强制清理数据点,只保留最新的50%数据
|
||||||
val keepSize = (dataPoints.size * 0.5).toInt()
|
val keepSize = (dataBuffer.size * 0.5).toInt()
|
||||||
if (keepSize > 0 && dataPoints.size > keepSize) {
|
if (keepSize > 0 && dataBuffer.size > keepSize) {
|
||||||
dataPoints = dataPoints.takeLast(keepSize).toMutableList()
|
dataBuffer = dataBuffer.takeLast(keepSize).toMutableList()
|
||||||
Log.d("DynamicChartView", "紧急清理数据点,保留 $keepSize 个数据点")
|
Log.d("DynamicChartView", "紧急清理数据点,保留 $keepSize 个数据点")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理多通道数据
|
// 清理多通道数据
|
||||||
multiChannelDataPoints.forEachIndexed { index, channelData ->
|
multiChannelBuffer.forEachIndexed { index, channelData ->
|
||||||
val keepChannelSize = (channelData.size * 0.5).toInt()
|
val keepChannelSize = (channelData.size * 0.5).toInt()
|
||||||
if (keepChannelSize > 0 && channelData.size > keepChannelSize) {
|
if (keepChannelSize > 0 && channelData.size > keepChannelSize) {
|
||||||
multiChannelDataPoints[index] = channelData.takeLast(keepChannelSize).toMutableList()
|
multiChannelBuffer[index] = channelData.takeLast(keepChannelSize).toMutableList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新计算数据范围
|
// 重新计算数据范围
|
||||||
updateDataRange(dataPoints)
|
updateDataRange(dataBuffer)
|
||||||
|
|
||||||
// 强制垃圾回收
|
// 强制垃圾回收
|
||||||
System.gc()
|
System.gc()
|
||||||
|
|
@ -309,9 +626,9 @@ class DynamicChartView @JvmOverloads constructor(
|
||||||
*/
|
*/
|
||||||
fun getDataPointCount(): Int {
|
fun getDataPointCount(): Int {
|
||||||
return if (isMultiChannelMode) {
|
return if (isMultiChannelMode) {
|
||||||
multiChannelDataPoints.sumOf { it.size }
|
multiChannelBuffer.sumOf { it.size }
|
||||||
} else {
|
} else {
|
||||||
dataPoints.size
|
dataBuffer.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -322,19 +639,19 @@ class DynamicChartView @JvmOverloads constructor(
|
||||||
try {
|
try {
|
||||||
if (isMultiChannelMode) {
|
if (isMultiChannelMode) {
|
||||||
// 多通道模式
|
// 多通道模式
|
||||||
multiChannelDataPoints.forEachIndexed { index, channelData ->
|
multiChannelBuffer.forEachIndexed { index, channelData ->
|
||||||
if (channelData.size > maxSize) {
|
if (channelData.size > maxSize) {
|
||||||
multiChannelDataPoints[index] = channelData.takeLast(maxSize).toMutableList()
|
multiChannelBuffer[index] = channelData.takeLast(maxSize).toMutableList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 重新计算数据范围
|
// 重新计算数据范围
|
||||||
updateMultiChannelDataRange()
|
updateMultiChannelDataRange()
|
||||||
} else {
|
} else {
|
||||||
// 单通道模式
|
// 单通道模式
|
||||||
if (dataPoints.size > maxSize) {
|
if (dataBuffer.size > maxSize) {
|
||||||
dataPoints = dataPoints.takeLast(maxSize).toMutableList()
|
dataBuffer = dataBuffer.takeLast(maxSize).toMutableList()
|
||||||
// 重新计算数据范围
|
// 重新计算数据范围
|
||||||
updateDataRange(dataPoints)
|
updateDataRange(dataBuffer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -346,53 +663,6 @@ class DynamicChartView @JvmOverloads constructor(
|
||||||
Log.e("DynamicChartView", "裁剪数据失败: ${e.message}")
|
Log.e("DynamicChartView", "裁剪数据失败: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新多通道数据(用于PPG等设备)
|
|
||||||
*/
|
|
||||||
fun updateMultiChannelData(channelData: List<List<Float>>) {
|
|
||||||
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<Float>) {
|
private fun updateDataRange(newData: List<Float>) {
|
||||||
// 如果设置了自定义Y轴范围,使用自定义范围
|
// 如果设置了自定义Y轴范围,使用自定义范围
|
||||||
|
|
@ -403,23 +673,24 @@ class DynamicChartView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用全部数据点计算范围,提供更准确的范围
|
// 使用全部数据点计算范围,提供更准确的范围
|
||||||
if (dataPoints.isEmpty()) return
|
if (dataBuffer.isEmpty()) return
|
||||||
|
|
||||||
// 计算全部数据点的范围
|
// 计算全部数据点的范围
|
||||||
minValue = dataPoints.minOrNull() ?: 0f
|
minValue = dataBuffer.minOrNull() ?: 0f
|
||||||
maxValue = dataPoints.maxOrNull() ?: 0f
|
maxValue = dataBuffer.maxOrNull() ?: 0f
|
||||||
|
|
||||||
// 确保有足够的显示范围
|
// 确保有足够的显示范围
|
||||||
val range = maxValue - minValue
|
val range = maxValue - minValue
|
||||||
if (range < 0.1f) {
|
if (range < 1.0f) { // 增加最小范围阈值,适应更多数据类型
|
||||||
val center = (maxValue + minValue) / 2
|
val center = (maxValue + minValue) / 2
|
||||||
minValue = center - 0.05f
|
minValue = center - 0.5f
|
||||||
maxValue = center + 0.05f
|
maxValue = center + 0.5f
|
||||||
} else {
|
} else {
|
||||||
val margin = range * 0.1f
|
val margin = range * 0.2f // 增加边距比例,显示更宽松
|
||||||
minValue -= margin
|
minValue -= margin
|
||||||
maxValue += margin
|
maxValue += margin
|
||||||
}
|
}
|
||||||
|
Log.d("DynamicChartView", "单通道显示范围调整: ${minValue}-${maxValue}")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -434,7 +705,7 @@ class DynamicChartView @JvmOverloads constructor(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (multiChannelDataPoints.isEmpty()) {
|
if (multiChannelBuffer.isEmpty()) {
|
||||||
Log.w("DynamicChartView", "多通道数据点为空,无法计算范围")
|
Log.w("DynamicChartView", "多通道数据点为空,无法计算范围")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -444,8 +715,8 @@ class DynamicChartView @JvmOverloads constructor(
|
||||||
var globalMax = Float.MIN_VALUE
|
var globalMax = Float.MIN_VALUE
|
||||||
var hasValidData = false
|
var hasValidData = false
|
||||||
|
|
||||||
for (channelIndex in multiChannelDataPoints.indices) {
|
for (channelIndex in multiChannelBuffer.indices) {
|
||||||
val channelData = multiChannelDataPoints[channelIndex]
|
val channelData = multiChannelBuffer[channelIndex]
|
||||||
if (channelData.isNotEmpty()) {
|
if (channelData.isNotEmpty()) {
|
||||||
val channelMin = channelData.minOrNull() ?: 0f
|
val channelMin = channelData.minOrNull() ?: 0f
|
||||||
val channelMax = channelData.maxOrNull() ?: 0f
|
val channelMax = channelData.maxOrNull() ?: 0f
|
||||||
|
|
@ -468,12 +739,12 @@ class DynamicChartView @JvmOverloads constructor(
|
||||||
|
|
||||||
// 确保有足够的显示范围
|
// 确保有足够的显示范围
|
||||||
val range = maxValue - minValue
|
val range = maxValue - minValue
|
||||||
if (range < 0.1f) {
|
if (range < 1.0f) { // 增加最小范围阈值
|
||||||
val center = (maxValue + minValue) / 2
|
val center = (maxValue + minValue) / 2
|
||||||
minValue = center - 0.05f
|
minValue = center - 0.5f
|
||||||
maxValue = center + 0.05f
|
maxValue = center + 0.5f
|
||||||
} else {
|
} else {
|
||||||
val margin = range * 0.1f
|
val margin = range * 0.2f // 增加边距比例
|
||||||
minValue -= margin
|
minValue -= margin
|
||||||
maxValue += margin
|
maxValue += margin
|
||||||
}
|
}
|
||||||
|
|
@ -486,25 +757,17 @@ class DynamicChartView @JvmOverloads constructor(
|
||||||
Log.d("DynamicChartView", "=== 开始清理图表数据 ===")
|
Log.d("DynamicChartView", "=== 开始清理图表数据 ===")
|
||||||
|
|
||||||
// 清理单通道数据
|
// 清理单通道数据
|
||||||
dataPoints.clear()
|
|
||||||
dataBuffer.clear()
|
dataBuffer.clear()
|
||||||
Log.d("DynamicChartView", "单通道数据已清理")
|
Log.d("DynamicChartView", "单通道数据已清理")
|
||||||
|
|
||||||
// 清除多通道数据
|
// 清除多通道数据
|
||||||
multiChannelDataPoints.forEachIndexed { index, channelData ->
|
multiChannelBuffer.forEachIndexed { index, channelData ->
|
||||||
try {
|
try {
|
||||||
channelData.clear()
|
channelData.clear()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("DynamicChartView", "清理多通道数据 $index 失败: ${e.message}", e)
|
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", "多通道数据已清理")
|
Log.d("DynamicChartView", "多通道数据已清理")
|
||||||
|
|
||||||
// 重置状态
|
// 重置状态
|
||||||
|
|
@ -553,6 +816,10 @@ class DynamicChartView @JvmOverloads constructor(
|
||||||
* 当视图从窗口分离时调用
|
* 当视图从窗口分离时调用
|
||||||
*/
|
*/
|
||||||
override fun onDetachedFromWindow() {
|
override fun onDetachedFromWindow() {
|
||||||
|
super.onDetachedFromWindow()
|
||||||
|
stopConsumer()
|
||||||
|
displayExecutor.shutdown()
|
||||||
|
Log.d("DynamicChartView", "视图已销毁,消费者线程已停止")
|
||||||
try {
|
try {
|
||||||
Log.d("DynamicChartView", "=== onDetachedFromWindow 开始 ===")
|
Log.d("DynamicChartView", "=== onDetachedFromWindow 开始 ===")
|
||||||
|
|
||||||
|
|
@ -675,9 +942,9 @@ class DynamicChartView @JvmOverloads constructor(
|
||||||
} else {
|
} else {
|
||||||
// 绘制曲线 - 检查单通道或多通道数据
|
// 绘制曲线 - 检查单通道或多通道数据
|
||||||
val hasDataToDraw = if (isMultiChannelMode) {
|
val hasDataToDraw = if (isMultiChannelMode) {
|
||||||
multiChannelDataPoints.any { it.size > 1 }
|
multiChannelBuffer.any { it.size > 1 }
|
||||||
} else {
|
} else {
|
||||||
dataPoints.size > 1
|
dataBuffer.size > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasDataToDraw) {
|
if (hasDataToDraw) {
|
||||||
|
|
@ -756,13 +1023,13 @@ class DynamicChartView @JvmOverloads constructor(
|
||||||
|
|
||||||
private fun drawStats(canvas: Canvas, width: Float, height: Float) {
|
private fun drawStats(canvas: Canvas, width: Float, height: Float) {
|
||||||
val totalDataPoints = if (isMultiChannelMode) {
|
val totalDataPoints = if (isMultiChannelMode) {
|
||||||
multiChannelDataPoints.sumOf { it.size }
|
multiChannelBuffer.sumOf { it.size }
|
||||||
} else {
|
} else {
|
||||||
dataPoints.size
|
dataBuffer.size
|
||||||
}
|
}
|
||||||
|
|
||||||
val statsText = if (isMultiChannelMode) {
|
val statsText = if (isMultiChannelMode) {
|
||||||
"显示数据点: ${totalDataPoints} (${multiChannelDataPoints.size}通道) | 累积: ${totalDataPointsReceived}"
|
"显示数据点: ${totalDataPoints} (${multiChannelBuffer.size}通道) | 累积: ${totalDataPointsReceived}"
|
||||||
} else {
|
} else {
|
||||||
"显示数据点: ${totalDataPoints} | 累积: ${totalDataPointsReceived}"
|
"显示数据点: ${totalDataPoints} | 累积: ${totalDataPointsReceived}"
|
||||||
}
|
}
|
||||||
|
|
@ -852,11 +1119,11 @@ class DynamicChartView @JvmOverloads constructor(
|
||||||
val drawWidth = width - 2 * padding
|
val drawWidth = width - 2 * padding
|
||||||
val drawHeight = height - 2 * padding
|
val drawHeight = height - 2 * padding
|
||||||
|
|
||||||
if (isMultiChannelMode && multiChannelDataPoints.isNotEmpty()) {
|
if (isMultiChannelMode && multiChannelBuffer.isNotEmpty()) {
|
||||||
Log.d("DynamicChartView", "多通道绘制: 通道数=${multiChannelDataPoints.size}, 数据点=${multiChannelDataPoints.map { it.size }}")
|
Log.d("DynamicChartView", "多通道绘制: 通道数=${multiChannelBuffer.size}, 数据点=${multiChannelBuffer.map { it.size }}")
|
||||||
// 多通道绘制模式
|
// 多通道绘制模式
|
||||||
for (channelIndex in multiChannelDataPoints.indices) {
|
for (channelIndex in multiChannelBuffer.indices) {
|
||||||
val channelData = multiChannelDataPoints[channelIndex]
|
val channelData = multiChannelBuffer[channelIndex]
|
||||||
if (channelData.isEmpty()) {
|
if (channelData.isEmpty()) {
|
||||||
Log.d("DynamicChartView", "通道$channelIndex 数据为空,跳过绘制")
|
Log.d("DynamicChartView", "通道$channelIndex 数据为空,跳过绘制")
|
||||||
continue
|
continue
|
||||||
|
|
@ -870,11 +1137,13 @@ class DynamicChartView @JvmOverloads constructor(
|
||||||
strokeWidth = 2f
|
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 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
|
val y = padding + (0.1f + normalizedValue * 0.8f) * drawHeight
|
||||||
|
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
|
|
@ -888,14 +1157,17 @@ class DynamicChartView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 单通道绘制模式
|
// 单通道绘制模式
|
||||||
if (dataPoints.isEmpty()) return
|
if (dataBuffer.isEmpty()) return
|
||||||
|
|
||||||
path.reset()
|
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 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
|
val y = padding + (0.1f + normalizedValue * 0.8f) * drawHeight
|
||||||
|
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
|
|
@ -909,6 +1181,21 @@ class DynamicChartView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对数据进行采样以减少绘制点数,提高性能
|
||||||
|
*/
|
||||||
|
private fun sampleData(data: List<Float>, maxPoints: Int): List<Float> {
|
||||||
|
if (data.size <= maxPoints) return data
|
||||||
|
|
||||||
|
val step = data.size.toDouble() / maxPoints.toDouble()
|
||||||
|
val sampled = mutableListOf<Float>()
|
||||||
|
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) {
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||||
|
|
||||||
|
|
@ -921,7 +1208,7 @@ class DynamicChartView @JvmOverloads constructor(
|
||||||
* 获取当前图表数据
|
* 获取当前图表数据
|
||||||
*/
|
*/
|
||||||
fun getCurrentData(): List<Float> {
|
fun getCurrentData(): List<Float> {
|
||||||
return dataPoints.toList()
|
return dataBuffer.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -929,15 +1216,15 @@ class DynamicChartView @JvmOverloads constructor(
|
||||||
*/
|
*/
|
||||||
fun getDataStats(): Map<String, Any> {
|
fun getDataStats(): Map<String, Any> {
|
||||||
val totalDataPoints = if (isMultiChannelMode) {
|
val totalDataPoints = if (isMultiChannelMode) {
|
||||||
multiChannelDataPoints.sumOf { it.size }
|
multiChannelBuffer.sumOf { it.size }
|
||||||
} else {
|
} else {
|
||||||
dataPoints.size
|
dataBuffer.size
|
||||||
}
|
}
|
||||||
|
|
||||||
return mapOf(
|
return mapOf(
|
||||||
"dataPoints" to totalDataPoints,
|
"dataPoints" to totalDataPoints,
|
||||||
"multiChannelMode" to isMultiChannelMode,
|
"multiChannelMode" to isMultiChannelMode,
|
||||||
"channelCount" to if (isMultiChannelMode) multiChannelDataPoints.size else 1,
|
"channelCount" to if (isMultiChannelMode) multiChannelBuffer.size else 1,
|
||||||
"minValue" to minValue,
|
"minValue" to minValue,
|
||||||
"maxValue" to maxValue,
|
"maxValue" to maxValue,
|
||||||
"range" to (maxValue - minValue),
|
"range" to (maxValue - minValue),
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,18 @@ class FullscreenChartActivity : AppCompatActivity(), DataManager.RealTimeDataCal
|
||||||
// 初始化滤波器管理器
|
// 初始化滤波器管理器
|
||||||
setupFilterManager()
|
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()) {
|
if (charts.isNotEmpty()) {
|
||||||
val chart = charts.getOrNull(0)
|
val chart = charts.getOrNull(0)
|
||||||
if (chart == null) {
|
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()) {
|
if (charts.isNotEmpty()) {
|
||||||
val chart = charts.getOrNull(channelIndex.coerceAtMost(charts.size - 1))
|
val chart = charts.getOrNull(channelIndex.coerceAtMost(charts.size - 1))
|
||||||
if (chart == null) {
|
if (chart == null) {
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,9 @@ class FullscreenFilterManager(
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据设备类型获取采样率
|
* 获取当前采样率
|
||||||
*/
|
*/
|
||||||
private fun getSampleRate(): Double {
|
fun getSampleRate(): Double {
|
||||||
return when (deviceType) {
|
return when (deviceType) {
|
||||||
"ECG_2LEAD", "ECG_12LEAD", "PW_ECG_SL", "EEG" -> 250.0
|
"ECG_2LEAD", "ECG_12LEAD", "PW_ECG_SL", "EEG" -> 250.0
|
||||||
"PPG" -> 50.0
|
"PPG" -> 50.0
|
||||||
|
|
@ -55,6 +55,7 @@ class FullscreenFilterManager(
|
||||||
private var maxDataPointsPerBatch = 100 // 每次最多处理100个数据点
|
private var maxDataPointsPerBatch = 100 // 每次最多处理100个数据点
|
||||||
private val dataBuffer = mutableListOf<Float>()
|
private val dataBuffer = mutableListOf<Float>()
|
||||||
private var maxBufferSize = 500 // 缓冲区最大大小(动态调整)
|
private var maxBufferSize = 500 // 缓冲区最大大小(动态调整)
|
||||||
|
private var maxDisplaySamples = 0 // 最大显示采样点数量
|
||||||
|
|
||||||
// UI组件引用
|
// UI组件引用
|
||||||
private var filterButton: Button? = null
|
private var filterButton: Button? = null
|
||||||
|
|
@ -101,20 +102,24 @@ class FullscreenFilterManager(
|
||||||
val sampleRate = getSampleRate()
|
val sampleRate = getSampleRate()
|
||||||
when {
|
when {
|
||||||
sampleRate >= 8000.0 -> { // 音频设备
|
sampleRate >= 8000.0 -> { // 音频设备
|
||||||
maxDataPointsPerBatch = 50
|
|
||||||
maxBufferSize = 200
|
|
||||||
}
|
|
||||||
sampleRate >= 1000.0 -> { // 高速设备
|
|
||||||
maxDataPointsPerBatch = 100
|
maxDataPointsPerBatch = 100
|
||||||
maxBufferSize = 400
|
maxBufferSize = 400
|
||||||
|
maxDisplaySamples = (sampleRate * 10).toInt() // 显示10秒数据
|
||||||
}
|
}
|
||||||
sampleRate >= 250.0 -> { // 中速设备(ECG等)
|
sampleRate >= 1000.0 -> { // 高速设备
|
||||||
maxDataPointsPerBatch = 150
|
|
||||||
maxBufferSize = 600
|
|
||||||
}
|
|
||||||
else -> { // 低速设备
|
|
||||||
maxDataPointsPerBatch = 200
|
maxDataPointsPerBatch = 200
|
||||||
maxBufferSize = 800
|
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秒数据
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -744,33 +744,58 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
|
||||||
Log.d("MainActivity", "数据前10字节: ${data.take(10).joinToString(", ") { "0x%02X".format(it) }}")
|
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
|
receivedDataCount += data.size
|
||||||
val currentTime = System.currentTimeMillis()
|
|
||||||
if (lastDataTime > 0) {
|
if (lastDataTime > 0) {
|
||||||
val timeDiff = currentTime - lastDataTime
|
val timeDiff = currentTime - lastDataTime
|
||||||
if (timeDiff > 0) {
|
if (timeDiff > 0) {
|
||||||
dataRate = (data.size * 1000f) / timeDiff // 字节/秒
|
dataRate = (data.size * 1000f) / timeDiff // 字节/秒
|
||||||
|
|
||||||
|
// 如果传输速率超过阈值,执行紧急处理
|
||||||
|
if (dataRate > 15000) { // 超过15KB/s
|
||||||
|
Log.w("MainActivity", "传输速率过高(${String.format("%.1f", dataRate)}字节/秒),执行紧急清理")
|
||||||
|
dataManager.checkMemoryUsage()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastDataTime = currentTime
|
lastDataTime = currentTime
|
||||||
|
|
||||||
|
// 定期检查内存使用情况,防止闪退
|
||||||
|
if (currentTime - lastDataTime > 5000) { // 每5秒检查一次
|
||||||
|
dataManager.checkMemoryUsage()
|
||||||
|
}
|
||||||
|
|
||||||
// 立即显示动态图表容器
|
|
||||||
runOnUiThread {
|
// UI更新频率限制:防止过高频率导致UI阻塞
|
||||||
binding.dynamicChartContainer.visibility = View.VISIBLE
|
val shouldUpdateUI = currentTime - lastDataStatusTime > 100 // 最多每100ms更新一次UI
|
||||||
|
|
||||||
// 如果数据处理还没启动,自动启动
|
if (shouldUpdateUI) {
|
||||||
if (!dataProcessingStarted) {
|
runOnUiThread {
|
||||||
startDataProcessing()
|
binding.dynamicChartContainer.visibility = View.VISIBLE
|
||||||
updateStatus("检测到数据,自动启动数据处理")
|
|
||||||
}
|
// 如果数据处理还没启动,自动启动
|
||||||
|
if (!dataProcessingStarted) {
|
||||||
// 减少数据接收状态更新频率
|
startDataProcessing()
|
||||||
if (currentTime - lastDataStatusTime > 5000) { // 改为5秒内只更新一次状态
|
updateStatus("检测到数据,自动启动数据处理")
|
||||||
updateStatus("接收到蓝牙数据: ${data.size} 字节,图表已显示")
|
}
|
||||||
updateDataStatistics()
|
|
||||||
lastDataStatusTime = currentTime
|
// 减少数据接收状态更新频率
|
||||||
|
if (currentTime - lastDataStatusTime > 2000) { // 改为2秒内只更新一次状态
|
||||||
|
updateStatus("接收到蓝牙数据: ${data.size} 字节,图表已显示")
|
||||||
|
updateDataStatistics()
|
||||||
|
lastDataStatusTime = currentTime
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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<List<Float>>()
|
||||||
|
private val multiChannelDataQueue = LinkedBlockingQueue<List<List<Float>>>()
|
||||||
|
|
||||||
|
// 消费者:定时器线程
|
||||||
|
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<Float>()
|
||||||
|
private val maxBufferSize = 2000
|
||||||
|
|
||||||
|
// 多通道数据缓冲区
|
||||||
|
private val multiChannelBuffer = mutableListOf<MutableList<Float>>()
|
||||||
|
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<Float>) {
|
||||||
|
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<List<Float>>) {
|
||||||
|
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<List<Float>>()
|
||||||
|
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<List<List<Float>>>()
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,9 +15,9 @@ class StreamingSignalProcessor {
|
||||||
private var signalProcessorInitialized = false
|
private var signalProcessorInitialized = false
|
||||||
|
|
||||||
// 窗口参数 - 针对ECG信号优化,进一步提高实时性
|
// 窗口参数 - 针对ECG信号优化,进一步提高实时性
|
||||||
private var windowSize = 50 // 优化:0.2秒窗口(250Hz采样率),进一步提高实时性
|
private var windowSize = 30 // 优化:0.12秒窗口(250Hz采样率),进一步提高实时性
|
||||||
private var overlapSize = 10 // 优化:20%重叠,减少边界效应
|
private var overlapSize = 6 // 优化:20%重叠,减少边界效应
|
||||||
private var stepSize = windowSize - overlapSize // 步长:40个样本
|
private var stepSize = windowSize - overlapSize // 步长:24个样本
|
||||||
|
|
||||||
// 数据缓冲区
|
// 数据缓冲区
|
||||||
private val dataBuffer = mutableListOf<Float>()
|
private val dataBuffer = mutableListOf<Float>()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue