APP COMMIT

This commit is contained in:
ZhangJinLong 2025-09-22 09:17:09 +08:00
parent 42250dff0e
commit a3be0bf6ca
11 changed files with 1111 additions and 185 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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" // 默认数据文件名

View File

@ -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}")
}
}
/**

View File

@ -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

View File

@ -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),

View File

@ -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) {

View File

@ -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秒数据
}
}

View File

@ -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 {

View File

@ -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"
}
}

View File

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