This commit is contained in:
ZhangJinLong 2025-09-03 13:12:28 +08:00
parent cb28adca69
commit f87f090542
4 changed files with 74 additions and 60 deletions

View File

@ -50,11 +50,11 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
// 流式数据处理相关
private val channelBuffers = mutableMapOf<Int, MutableList<Float>>() // 通道号 -> 数据缓冲区
private val processedChannelBuffers = mutableMapOf<Int, MutableList<Float>>() // 处理后的通道数据
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<SensorData>) {
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()) {

View File

@ -98,28 +98,30 @@ class ECGChartView @JvmOverloads constructor(
private val lockButtonRect = RectF()
fun updateData(newData: List<Float>) {
// 累积数据而不是替换
// 快速累积数据
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() {

View File

@ -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 {
@ -71,6 +82,11 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
private var lastChartStatusTime = 0L
private var lastProgressUpdateTime = 0L
// 导出功能相关
private var exportedDataCount = 0
private val dataExportBuffer = mutableListOf<Float>()
private val exportTimeStamps = mutableListOf<Long>()
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<Float>) {
// 限制图表更新频率,避免过多更新导致卡顿
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

View File

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