From f87f0905422e673809ec1f5ec971e1540cdec8f9 Mon Sep 17 00:00:00 2001 From: ZhangJinLong <19357383190@163.com> Date: Wed, 3 Sep 2025 13:12:28 +0800 Subject: [PATCH] chart --- .../example/cmake_project_test/DataManager.kt | 54 ++++++++++--------- .../cmake_project_test/ECGChartView.kt | 21 ++++---- .../cmake_project_test/MainActivity.kt | 41 ++++++++------ .../StreamingSignalProcessor.kt | 18 +++---- 4 files changed, 74 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/com/example/cmake_project_test/DataManager.kt b/app/src/main/java/com/example/cmake_project_test/DataManager.kt index b74c129..90b8724 100644 --- a/app/src/main/java/com/example/cmake_project_test/DataManager.kt +++ b/app/src/main/java/com/example/cmake_project_test/DataManager.kt @@ -50,11 +50,11 @@ class DataManager(private val nativeCallback: NativeMethodCallback) { // 流式数据处理相关 private val channelBuffers = mutableMapOf>() // 通道号 -> 数据缓冲区 private val processedChannelBuffers = mutableMapOf>() // 处理后的通道数据 - private val processingWindowSize = 1000 // 优化:增大处理窗口大小,确保心率计算 - private val minSamplesForMetrics = 250 // 优化:250个样本(1秒@250Hz),适应十二导联心电 + private val processingWindowSize = 100 // 优化:0.4秒窗口(250Hz采样率),进一步提高实时性 + private val minSamplesForMetrics = 25 // 优化:25个样本(0.1秒@250Hz),快速响应 private var currentDataType: type.SensorData.DataType? = null // 当前数据类型 private var lastProcessTime = 0L // 上次处理时间 - private val processingInterval = 200L // 优化:200ms处理间隔,提高实时性 + private val processingInterval = 25L // 优化:25ms处理间隔,进一步提高实时性 private var totalProcessedSamples = 0L // 总处理样本数 // 陷波滤波器状态 @@ -95,11 +95,11 @@ class DataManager(private val nativeCallback: NativeMethodCallback) { Log.d("DataManager", "解析出 ${packets.size} 个数据包") - // 立即发送原始数据到图表显示 + // 立即发送原始数据到图表显示(优先显示原始数据) sendRawDataToCharts(packets) - // 应用流式数据处理 - processStreamingData(packets) + // 可选:后台进行流式处理(不影响显示) + // processStreamingData(packets) } else { Log.w("DataManager", "没有解析出有效数据包,尝试生成测试数据") @@ -220,32 +220,23 @@ class DataManager(private val nativeCallback: NativeMethodCallback) { */ private fun sendRawDataToCharts(packets: List) { if (packets.isEmpty()) { - Log.w("DataManager", "sendRawDataToCharts: 没有数据包") return } - Log.d("DataManager", "立即发送单导联蓝牙数据到图表,处理 ${packets.size} 个数据包") + // 快速处理:减少日志,提高速度 + var totalDataPoints = 0 // 专门处理单导联数据包 - for ((packetIndex, packet) in packets.withIndex()) { - Log.d("DataManager", "处理单导联数据包 $packetIndex: 数据类型=${packet.getDataType()}") - + for (packet in packets) { val channelData = packet.getChannelData() if (channelData != null && channelData.isNotEmpty()) { - Log.d("DataManager", "单导联数据包 $packetIndex 有 ${channelData.size} 个通道") - // 对于单导联数据,我们主要关注第一个通道(通常是导联I或II) val primaryChannel = channelData[0] if (primaryChannel.isNotEmpty()) { - Log.d("DataManager", "单导联主通道数据长度: ${primaryChannel.size}") - Log.d("DataManager", "单导联主通道前3个值: ${primaryChannel.take(3).joinToString(", ")}") - // 立即发送单导联数据到图表 if (realTimeCallback != null) { realTimeCallback!!.onRawDataAvailable(0, primaryChannel) // 使用通道0表示主导联 - Log.d("DataManager", "已发送单导联数据到图表,数据长度: ${primaryChannel.size}") - } else { - Log.e("DataManager", "realTimeCallback 为空,无法发送单导联数据") + totalDataPoints += primaryChannel.size } // 如果有其他通道(如参考通道),也发送 @@ -253,21 +244,18 @@ class DataManager(private val nativeCallback: NativeMethodCallback) { for (i in 1 until channelData.size) { val additionalChannel = channelData[i] if (additionalChannel.isNotEmpty()) { - Log.d("DataManager", "发送附加通道 $i 数据,长度: ${additionalChannel.size}") realTimeCallback?.onRawDataAvailable(i, additionalChannel) } } } - } else { - Log.w("DataManager", "单导联主通道数据为空") } - } else { - Log.w("DataManager", "单导联数据包 $packetIndex 没有通道数据") } } - // 添加调试信息 - Log.d("DataManager", "单导联蓝牙数据发送完成,总共处理了 ${packets.size} 个数据包") + // 只在需要时记录日志 + if (totalDataPoints > 0) { + Log.d("DataManager", "快速发送原始数据: ${totalDataPoints} 个数据点") + } } /** @@ -399,6 +387,20 @@ class DataManager(private val nativeCallback: NativeMethodCallback) { Log.d("DataManager", "所有通道处理完成,总共处理了 $localProcessedSamples 个样本") + // 立即发送处理后的数据到图表 + if (processedChannels.isNotEmpty() && processedChannels[0].isNotEmpty()) { + val processedData = processedChannels[0] + Log.d("DataManager", "发送处理后的数据到图表,长度: ${processedData.size}") + Log.d("DataManager", "处理后数据前3个值: ${processedData.take(3).joinToString(", ")}") + + if (realTimeCallback != null) { + realTimeCallback!!.onRawDataAvailable(0, processedData) + Log.d("DataManager", "已发送处理后数据到图表") + } else { + Log.e("DataManager", "realTimeCallback 为空,无法发送处理后数据") + } + } + // 计算指标(使用第一个处理后的通道) Log.d("DataManager", "准备计算指标,处理后通道数量: ${processedChannels.size}") if (processedChannels.isNotEmpty()) { diff --git a/app/src/main/java/com/example/cmake_project_test/ECGChartView.kt b/app/src/main/java/com/example/cmake_project_test/ECGChartView.kt index b6ce5ef..7ffe613 100644 --- a/app/src/main/java/com/example/cmake_project_test/ECGChartView.kt +++ b/app/src/main/java/com/example/cmake_project_test/ECGChartView.kt @@ -98,28 +98,30 @@ class ECGChartView @JvmOverloads constructor( private val lockButtonRect = RectF() fun updateData(newData: List) { - // 累积数据而不是替换 + // 快速累积数据 dataPoints.addAll(newData) - // 限制数据点数量 + // 限制数据点数量(避免内存溢出) if (dataPoints.size > maxDataPoints) { dataPoints = dataPoints.takeLast(maxDataPoints).toMutableList() } - // 计算数据范围 + // 快速计算数据范围(减少计算量) if (dataPoints.isNotEmpty()) { - minValue = dataPoints.minOrNull() ?: 0f - maxValue = dataPoints.maxOrNull() ?: 0f + // 只计算最近的数据范围,提高性能 + val recentData = dataPoints.takeLast(minOf(1000, dataPoints.size)) + minValue = recentData.minOrNull() ?: 0f + maxValue = recentData.maxOrNull() ?: 0f - // 确保有足够的显示范围,并添加上下边距 + // 确保有足够的显示范围 val range = maxValue - minValue if (range < 0.1f) { val center = (maxValue + minValue) / 2 minValue = center - 0.05f maxValue = center + 0.05f } else { - // 添加20%的上下边距,让曲线显示在中间 - val margin = range * 0.2f + // 添加10%的上下边距(减少边距,提高响应速度) + val margin = range * 0.1f minValue -= margin maxValue += margin } @@ -131,7 +133,8 @@ class ECGChartView @JvmOverloads constructor( offsetY = 0f } - invalidate() // 重绘 + // 立即重绘 + invalidate() } fun clearData() { diff --git a/app/src/main/java/com/example/cmake_project_test/MainActivity.kt b/app/src/main/java/com/example/cmake_project_test/MainActivity.kt index 93a72c3..825f72c 100644 --- a/app/src/main/java/com/example/cmake_project_test/MainActivity.kt +++ b/app/src/main/java/com/example/cmake_project_test/MainActivity.kt @@ -13,6 +13,17 @@ import androidx.core.content.ContextCompat import android.view.View import com.example.cmake_project_test.databinding.ActivityMainBinding import type.SensorData +import android.graphics.Bitmap +import android.graphics.Canvas +import android.os.Environment +import java.io.File +import java.io.FileWriter +import java.text.SimpleDateFormat +import java.util.* +import android.content.Intent +import androidx.core.content.FileProvider +import android.net.Uri +import android.widget.Toast class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.RealTimeDataCallback, BluetoothManager.BluetoothCallback { @@ -70,6 +81,11 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real private var lastChartUpdateTime = 0L private var lastChartStatusTime = 0L private var lastProgressUpdateTime = 0L + + // 导出功能相关 + private var exportedDataCount = 0 + private val dataExportBuffer = mutableListOf() + private val exportTimeStamps = mutableListOf() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -141,6 +157,12 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real showDebugStatusDialog() true } + + // 添加导出按钮(长按清空数据按钮) + binding.clearDataButton.setOnLongClickListener { + showExportDialog() + true + } } private var notchFilterEnabled = false @@ -402,21 +424,9 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real } override fun onRawDataAvailable(channelIndex: Int, rawData: List) { - // 限制图表更新频率,避免过多更新导致卡顿 - val currentTime = System.currentTimeMillis() - if (currentTime - lastChartUpdateTime < 100) { // 100ms内不重复更新图表 - return - } - lastChartUpdateTime = currentTime - - // 立即显示原始数据到图表 + // 立即显示原始数据到图表,无延迟限制 runOnUiThread { try { - Log.d("MainActivity", "收到原始数据回调,通道: $channelIndex,数据长度: ${rawData.size}") - if (rawData.isNotEmpty()) { - Log.d("MainActivity", "原始数据前3个值: ${rawData.take(3).joinToString(", ")}") - } - // 确保图表容器可见 binding.ecgChartContainer.visibility = View.VISIBLE @@ -424,9 +434,8 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real binding.ecgRhythmView.updateData(rawData) binding.ecgWaveformView.updateData(rawData) - Log.d("MainActivity", "已立即更新原始数据图表,通道: $channelIndex,数据长度: ${rawData.size}") - - // 减少状态更新频率 + // 减少状态更新频率,但不影响图表更新 + val currentTime = System.currentTimeMillis() if (currentTime - lastChartStatusTime > 2000) { // 2秒内只更新一次状态 updateStatus("实时图表已更新,通道: $channelIndex,数据点: ${rawData.size}") lastChartStatusTime = currentTime diff --git a/app/src/main/java/com/example/cmake_project_test/StreamingSignalProcessor.kt b/app/src/main/java/com/example/cmake_project_test/StreamingSignalProcessor.kt index 7bc4c72..6d4093d 100644 --- a/app/src/main/java/com/example/cmake_project_test/StreamingSignalProcessor.kt +++ b/app/src/main/java/com/example/cmake_project_test/StreamingSignalProcessor.kt @@ -14,10 +14,10 @@ class StreamingSignalProcessor { private var processorId: Long = -1L private var signalProcessorInitialized = false - // 窗口参数 - 针对ECG信号优化,确保心率计算 - private var windowSize = 500 // 优化:2秒窗口(250Hz采样率),确保心率计算 - private var overlapSize = 100 // 优化:20%重叠,减少边界效应 - private var stepSize = windowSize - overlapSize // 步长:400个样本 + // 窗口参数 - 针对ECG信号优化,进一步提高实时性 + private var windowSize = 50 // 优化:0.2秒窗口(250Hz采样率),进一步提高实时性 + private var overlapSize = 10 // 优化:20%重叠,减少边界效应 + private var stepSize = windowSize - overlapSize // 步长:40个样本 // 数据缓冲区 private val dataBuffer = mutableListOf() @@ -182,7 +182,7 @@ class StreamingSignalProcessor { Log.d("StreamingSignalProcessor", "高通滤波成功(${highpassCutoff}Hz),结果前3个值: ${filtered.take(3).joinToString(", ")}") } - // 2. 低通滤波(去除肌电等高频噪声)- 使用100Hz,平衡信号清晰度和噪声抑制 + // 2. 低通滤波(去除肌电等高频噪声)- 使用80Hz,减少计算量 if (lowpassCutoff > 0) { val lowpassResult = signalProcessor.lowpassFilter(filtered, sampleRate, lowpassCutoff) if (lowpassResult == null) { @@ -243,11 +243,11 @@ class StreamingSignalProcessor { Triple(40.0, 50.0, 30.0) } type.SensorData.DataType.ECG_2LEAD, type.SensorData.DataType.ECG_12LEAD, type.SensorData.DataType.PW_ECG_SL -> { - // ECG: 专业ECG滤波参数 - // 低通100Hz (平衡信号清晰度和噪声抑制) + // ECG: 优化滤波参数,提高实时性 + // 低通80Hz (减少计算量,保持信号质量) // 陷波50Hz (中国工频) - // 品质因数10 (避免过度滤波) - Triple(100.0, 50.0, 10.0) + // 品质因数5 (减少计算复杂度) + Triple(80.0, 50.0, 5.0) } type.SensorData.DataType.PPG -> { // PPG: 低通10Hz, 陷波50Hz