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