APP COMMIT
This commit is contained in:
parent
42250dff0e
commit
a3be0bf6ca
|
|
@ -4,6 +4,14 @@
|
|||
<selectionStates>
|
||||
<SelectionState runConfigName="Unnamed">
|
||||
<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>
|
||||
</selectionStates>
|
||||
</component>
|
||||
|
|
|
|||
|
|
@ -2,5 +2,6 @@
|
|||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/../../../SDK_APP(1)/SDK_APP" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -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" // 默认数据文件名
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<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等设备)
|
||||
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 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,10 +166,39 @@ 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
|
||||
|
|
@ -201,47 +281,287 @@ class DynamicChartView @JvmOverloads constructor(
|
|||
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<Float>) {
|
||||
if (newData.isEmpty()) return
|
||||
|
||||
isDataAvailable = true
|
||||
// 生产者:将单个数据点放入队列
|
||||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新累积数据点计数器
|
||||
totalDataPointsReceived += newData.size
|
||||
/**
|
||||
* 生产者:接收多通道数据
|
||||
*/
|
||||
fun updateMultiChannelData(channelData: List<List<Float>>) {
|
||||
if (channelData.isEmpty()) return
|
||||
|
||||
// 直接添加新数据,来多少显示多少
|
||||
dataPoints.addAll(newData)
|
||||
// 生产者:将多通道数据点放入队列
|
||||
// 首先检查所有通道是否有相同数量的数据点
|
||||
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", "多通道数据点数量不一致,无法按时间点处理")
|
||||
}
|
||||
}
|
||||
|
||||
// 限制数据点数量,保持滚动显示
|
||||
if (dataPoints.size > maxDataPoints) {
|
||||
val excessData = dataPoints.size - maxDataPoints
|
||||
dataPoints = dataPoints.takeLast(maxDataPoints).toMutableList()
|
||||
Log.d("DynamicChartView", "清理了 $excessData 个数据点,当前数据点: ${dataPoints.size}")
|
||||
/**
|
||||
* 消费者:定时消费数据并更新显示
|
||||
*/
|
||||
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()
|
||||
|
||||
// 更新数据范围
|
||||
updateDataRange(newData)
|
||||
|
||||
// 立即重绘
|
||||
invalidate()
|
||||
lastMemoryCheckTime = currentTime
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("DynamicChartView", "消费者处理数据时发生异常: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -249,8 +569,6 @@ 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()
|
||||
|
|
@ -263,9 +581,8 @@ class DynamicChartView @JvmOverloads constructor(
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -347,53 +664,6 @@ class DynamicChartView @JvmOverloads constructor(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新多通道数据(用于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>) {
|
||||
// 如果设置了自定义Y轴范围,使用自定义范围
|
||||
if (customMinValue != null && customMaxValue != null) {
|
||||
|
|
@ -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<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) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
|
||||
|
|
@ -921,7 +1208,7 @@ class DynamicChartView @JvmOverloads constructor(
|
|||
* 获取当前图表数据
|
||||
*/
|
||||
fun getCurrentData(): List<Float> {
|
||||
return dataPoints.toList()
|
||||
return dataBuffer.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -929,15 +1216,15 @@ class DynamicChartView @JvmOverloads constructor(
|
|||
*/
|
||||
fun getDataStats(): Map<String, Any> {
|
||||
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),
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<Float>()
|
||||
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秒数据
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -744,19 +744,43 @@ 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()
|
||||
}
|
||||
|
||||
// 立即显示动态图表容器
|
||||
|
||||
// UI更新频率限制:防止过高频率导致UI阻塞
|
||||
val shouldUpdateUI = currentTime - lastDataStatusTime > 100 // 最多每100ms更新一次UI
|
||||
|
||||
if (shouldUpdateUI) {
|
||||
runOnUiThread {
|
||||
binding.dynamicChartContainer.visibility = View.VISIBLE
|
||||
|
||||
|
|
@ -767,12 +791,13 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
|
|||
}
|
||||
|
||||
// 减少数据接收状态更新频率
|
||||
if (currentTime - lastDataStatusTime > 5000) { // 改为5秒内只更新一次状态
|
||||
if (currentTime - lastDataStatusTime > 2000) { // 改为2秒内只更新一次状态
|
||||
updateStatus("接收到蓝牙数据: ${data.size} 字节,图表已显示")
|
||||
updateDataStatistics()
|
||||
lastDataStatusTime = currentTime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 在后台线程处理数据,避免阻塞UI
|
||||
Thread {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
// 窗口参数 - 针对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<Float>()
|
||||
|
|
|
|||
Loading…
Reference in New Issue