This commit is contained in:
ZhangJinLong 2025-09-25 10:29:07 +08:00
parent a3be0bf6ca
commit bd8fd53597
19 changed files with 190 additions and 3443 deletions

View File

@ -2,6 +2,5 @@
<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

@ -565,22 +565,23 @@ std::vector<SensorData> parse_device_data(const std::vector<uint8_t>& file_data)
return final_results;
}
// CRC16校验函数 (Modbus CRC16)
// CRC16校验函数 (CCITT CRC16)
uint16_t calculate_crc16(const uint8_t* data, size_t length) {
uint16_t crc = 0xFFFF;
uint16_t wCRCin = 0xFFFF;
const uint16_t wCPoly = 0x1021;
for (size_t i = 0; i < length; i++) {
crc ^= data[i];
for (int j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0xA001;
} else {
crc = crc >> 1;
bool bit = ((data[i] >> (7 - j)) & 1) == 1;
bool c15 = ((wCRCin >> 15) & 1) == 1;
wCRCin = wCRCin << 1;
if (c15 ^ bit) {
wCRCin = wCRCin ^ wCPoly;
}
}
}
return crc;
return wCRCin & 0xFFFF;
}
// 工具函数:将数值转换为十六进制字符串

View File

@ -26,9 +26,6 @@ import java.util.UUID
*/
class BluetoothManager(private val context: Context) {
// PPTM命令编码器
private val pptmEncoder = PPTMCommandEncoder()
companion object {
private const val TAG = "BluetoothManager"
@ -44,22 +41,14 @@ class BluetoothManager(private val context: Context) {
// 功能码定义
const val FUNC_QUERY_DEVICE_INFO = 0x0000 // 查询设备信息
const val FUNC_START_COLLECTION = 0x0001 // 开启采集
const val FUNC_QUERY_BATTERY = 0x0002 // 查询电量
const val FUNC_SET_SAMPLE_RATE = 0x0003 // 设置采样率
const val FUNC_SET_GAIN = 0x0004 // 设置增益
const val FUNC_SET_FILTER = 0x0005 // 设置滤波器
const val FUNC_SET_LEAD = 0x0006 // 设置导联
const val FUNC_SET_ALARM = 0x0007 // 设置报警
const val FUNC_SET_TIME = 0x0008 // 设置时间
const val FUNC_SET_DATE = 0x0009 // 设置日期
const val FUNC_POWER_LINE_FILTER = 0x000A // 工频滤波
const val FUNC_QUERY_STATUS = 0x000B // 查询状态
const val FUNC_RESET_DEVICE = 0x000C // 设备复位
const val FUNC_UPDATE_FIRMWARE = 0x000D // 固件升级
const val FUNC_DATA_STREAM = 0x8000 // 数据流
const val FUNC_ALARM_DATA = 0x8001 // 报警数据
const val FUNC_STATUS_REPORT = 0x8002 // 状态报告
const val FUNC_START_COLLECTION = 0x0001 // 采集使能开关(带时间戳)
const val FUNC_QUERY_BATTERY = 0x0002 // 电量查询
const val FUNC_STIM_SWITCH = 0x0003 // 电刺激开关
const val FUNC_POWER_LINE_FILTER = 0x000A // 工频滤波开关
const val FUNC_SYNC_TIMESTAMP = 0x0080 // 同步时间戳64bit毫秒
const val FUNC_DATA_STREAM = 0x8000 // 数据上传
const val FUNC_ALARM_DATA = 0x8001 // 设备状态上报
const val FUNC_STATUS_REPORT = 0x8002 // 电量上报
// 数据包类型
const val PACKET_TYPE_COMMAND = 0x00 // 命令包
@ -75,31 +64,7 @@ class BluetoothManager(private val context: Context) {
const val POWER_LINE_FILTER_OFF = 0x00 // 关闭工频滤波
const val POWER_LINE_FILTER_ON = 0x01 // 开启工频滤波
// 采样率
const val SAMPLE_RATE_125 = 125 // 125Hz
const val SAMPLE_RATE_250 = 250 // 250Hz
const val SAMPLE_RATE_500 = 500 // 500Hz
const val SAMPLE_RATE_1000 = 1000 // 1000Hz
// 增益设置
const val GAIN_1 = 1 // 1倍增益
const val GAIN_2 = 2 // 2倍增益
const val GAIN_4 = 4 // 4倍增益
const val GAIN_8 = 8 // 8倍增益
// 导联设置
const val LEAD_I = 0x01 // I导联
const val LEAD_II = 0x02 // II导联
const val LEAD_III = 0x03 // III导联
const val LEAD_AVR = 0x04 // aVR导联
const val LEAD_AVL = 0x05 // aVL导联
const val LEAD_AVF = 0x06 // aVF导联
const val LEAD_V1 = 0x07 // V1导联
const val LEAD_V2 = 0x08 // V2导联
const val LEAD_V3 = 0x09 // V3导联
const val LEAD_V4 = 0x0A // V4导联
const val LEAD_V5 = 0x0B // V5导联
const val LEAD_V6 = 0x0C // V6导联
// 已移除采样率、增益、导联设置常量按PDF协议未定义
// 响应状态码
const val RESPONSE_SUCCESS = 0x00 // 成功
@ -156,9 +121,7 @@ class BluetoothManager(private val context: Context) {
// 协议状态
private var deviceInfo: DeviceInfo? = null
private var isCollecting = false
private var currentSampleRate = Protocol.SAMPLE_RATE_500
private var currentGain = Protocol.GAIN_1
private var currentLead = Protocol.LEAD_II
// 已移除采样率、增益、导联状态变量按PDF协议未定义
private var powerLineFilterEnabled = false
// 数据统计
@ -908,32 +871,47 @@ class BluetoothManager(private val context: Context) {
}
/**
* 查询设备信息
* 功能码: 0x0000
* 电刺激开关
* 功能码: 0x0003
* 数据格式: [开关及类型(1字节)]
* 数据长度字段表示整个包的长度(7字节)
*/
fun queryDeviceInfo() {
fun stimSwitch(stimType: Int, enable: Boolean) {
try {
val packet = buildProtocolPacket(Protocol.FUNC_QUERY_DEVICE_INFO)
Log.d(TAG, "发送查询设备信息指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}")
val data = ByteArray(1)
// 开关及类型控制
// 高4位开关控制0000=关闭0001=开启)
// 低4位刺激类型0000~1111即0-15
val switchBits = if (enable) 0x10 else 0x00 // 高4位0001或0000
val typeBits = stimType and 0x0F // 低4位确保在0-15范围内
data[0] = (switchBits or typeBits).toByte()
val packet = buildProtocolPacket(Protocol.FUNC_STIM_SWITCH, data)
Log.d(TAG, "发送电刺激开关指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}")
sendCommand(packet)
} catch (e: Exception) {
Log.e(TAG, "构建查询设备信息指令失败: ${e.message}")
callback?.onCommandSent(false, "构建查询设备信息指令失败: ${e.message}")
Log.e(TAG, "构建电刺激开关指令失败: ${e.message}")
callback?.onCommandSent(false, "构建电刺激开关指令失败: ${e.message}")
}
}
/**
* 查询电量
* 功能码: 0x0002
* 同步时间戳
* 功能码: 0x0080
* 数据格式: [时间戳(8字节, 小端, 毫秒)]
*/
fun queryBattery() {
fun syncTimestamp(timestampMs: Long) {
try {
val packet = buildProtocolPacket(Protocol.FUNC_QUERY_BATTERY)
Log.d(TAG, "发送查询电量指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}")
val data = ByteArray(8)
for (i in 0..7) {
data[i] = ((timestampMs shr (i * 8)) and 0xFF).toByte()
}
val packet = buildProtocolPacket(Protocol.FUNC_SYNC_TIMESTAMP, data)
Log.d(TAG, "发送同步时间戳指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}")
sendCommand(packet)
} catch (e: Exception) {
Log.e(TAG, "构建查询电量指令失败: ${e.message}")
callback?.onCommandSent(false, "构建查询电量指令失败: ${e.message}")
Log.e(TAG, "构建同步时间戳指令失败: ${e.message}")
callback?.onCommandSent(false, "构建同步时间戳指令失败: ${e.message}")
}
}
@ -942,67 +920,21 @@ class BluetoothManager(private val context: Context) {
* 功能码: 0x0003
* 数据格式: [采样率(2字节)]
*/
fun setSampleRate(sampleRate: Int) {
try {
val data = ByteArray(2)
data[0] = (sampleRate and 0xFF).toByte()
data[1] = ((sampleRate shr 8) and 0xFF).toByte()
val packet = buildProtocolPacket(Protocol.FUNC_SET_SAMPLE_RATE, data)
Log.d(TAG, "发送设置采样率指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}")
sendCommand(packet)
currentSampleRate = sampleRate
} catch (e: Exception) {
Log.e(TAG, "构建设置采样率指令失败: ${e.message}")
callback?.onCommandSent(false, "构建设置采样率指令失败: ${e.message}")
}
}
// 已移除设置采样率按PDF未列出
/**
* 设置增益
* 功能码: 0x0004
* 数据格式: [增益(1字节)]
*/
fun setGain(gain: Int) {
try {
val data = ByteArray(1)
data[0] = gain.toByte()
val packet = buildProtocolPacket(Protocol.FUNC_SET_GAIN, data)
Log.d(TAG, "发送设置增益指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}")
sendCommand(packet)
currentGain = gain
} catch (e: Exception) {
Log.e(TAG, "构建设置增益指令失败: ${e.message}")
callback?.onCommandSent(false, "构建设置增益指令失败: ${e.message}")
}
}
// 已移除设置增益按PDF未列出
/**
* 设置导联
* 功能码: 0x0006
* 数据格式: [导联(1字节)]
*/
fun setLead(lead: Int) {
try {
val data = ByteArray(1)
data[0] = lead.toByte()
val packet = buildProtocolPacket(Protocol.FUNC_SET_LEAD, data)
Log.d(TAG, "发送设置导联指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}")
sendCommand(packet)
currentLead = lead
} catch (e: Exception) {
Log.e(TAG, "构建设置导联指令失败: ${e.message}")
callback?.onCommandSent(false, "构建设置导联指令失败: ${e.message}")
}
}
// 已移除设置导联按PDF未列出
/**
* 工频滤波开关
@ -1030,31 +962,13 @@ class BluetoothManager(private val context: Context) {
* 查询状态
* 功能码: 0x000B
*/
fun queryStatus() {
try {
val packet = buildProtocolPacket(Protocol.FUNC_QUERY_STATUS)
Log.d(TAG, "发送查询状态指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}")
sendCommand(packet)
} catch (e: Exception) {
Log.e(TAG, "构建查询状态指令失败: ${e.message}")
callback?.onCommandSent(false, "构建查询状态指令失败: ${e.message}")
}
}
// 已移除查询状态按PDF未列出
/**
* 设备复位
* 功能码: 0x000C
*/
fun resetDevice() {
try {
val packet = buildProtocolPacket(Protocol.FUNC_RESET_DEVICE)
Log.d(TAG, "发送设备复位指令: ${packet.joinToString(", ") { "0x%02X".format(it) }}")
sendCommand(packet)
} catch (e: Exception) {
Log.e(TAG, "构建设备复位指令失败: ${e.message}")
callback?.onCommandSent(false, "构建设备复位指令失败: ${e.message}")
}
}
// 已移除设备复位按PDF未列出
/**
* 获取协议统计信息
@ -1066,9 +980,7 @@ class BluetoothManager(private val context: Context) {
"totalPacketsReceived" to totalPacketsReceived,
"totalBytesReceived" to totalBytesReceived,
"isCollecting" to isCollecting,
"currentSampleRate" to currentSampleRate,
"currentGain" to currentGain,
"currentLead" to currentLead,
// 已移除采样率、增益、导联状态按PDF协议未定义
"powerLineFilterEnabled" to powerLineFilterEnabled,
"deviceInfo" to deviceInfo
)
@ -1164,21 +1076,21 @@ class BluetoothManager(private val context: Context) {
* 计算CRC16-CCITT-FALSE校验
*/
private fun calculateCRC16(data: ByteArray, offset: Int, length: Int): Int {
var crc = 0xFFFF
var wCRCin = 0xFFFF
val wCPoly = 0x1021
for (i in offset until offset + length) {
crc = crc xor (data[i].toInt() and 0xFF)
for (j in 0..7) {
if ((crc and 0x0001) != 0) {
crc = crc shr 1
crc = crc xor 0x8408
} else {
crc = crc shr 1
val bit = ((data[i].toInt() shr (7 - j)) and 1) == 1
val c15 = ((wCRCin shr 15) and 1) == 1
wCRCin = wCRCin shl 1
if (c15 xor bit) {
wCRCin = wCRCin xor wCPoly
}
}
}
return crc
return wCRCin and 0xFFFF
}
/**
@ -1691,80 +1603,4 @@ class BluetoothManager(private val context: Context) {
isCleaningUp = true
}
}
// ==================== PPTM设备命令方法 ====================
/**
* 发送PPTM开始采样命令
* 使用PPTM协议编码器生成带CRC16校验的命令
*/
fun sendPptmStartSampleCmd() {
try {
val command = pptmEncoder.createPptmStartSampleCmd()
Log.d(TAG, "发送PPTM开始采样命令: ${command.joinToString(", ") { "0x%02X".format(it) }}")
sendCommand(command)
callback?.onStatusChanged("📤 已发送PPTM开始采样命令")
} catch (e: Exception) {
Log.e(TAG, "发送PPTM开始采样命令失败: ${e.message}")
callback?.onCommandSent(false, "发送PPTM开始采样命令失败: ${e.message}")
}
}
/**
* 发送PPTM停止采样命令
* 使用PPTM协议编码器生成带CRC16校验的命令
*/
fun sendPptmStopSampleCmd() {
try {
val command = pptmEncoder.createPptmStopSampleCmd()
Log.d(TAG, "发送PPTM停止采样命令: ${command.joinToString(", ") { "0x%02X".format(it) }}")
sendCommand(command)
callback?.onStatusChanged("📤 已发送PPTM停止采样命令")
} catch (e: Exception) {
Log.e(TAG, "发送PPTM停止采样命令失败: ${e.message}")
callback?.onCommandSent(false, "发送PPTM停止采样命令失败: ${e.message}")
}
}
/**
* 发送自定义PPTM命令
* @param opcode 操作码
* @param payload 负载数据
*/
fun sendCustomPptmCmd(opcode: Short, payload: ByteArray) {
try {
val command = pptmEncoder.createCustomPptmCmd(opcode, payload)
Log.d(TAG, "发送自定义PPTM命令: 操作码=0x%04X, 负载=${payload.joinToString(", ") { "0x%02X".format(it) }}".format(opcode))
sendCommand(command)
callback?.onStatusChanged("📤 已发送自定义PPTM命令: 操作码=0x%04X".format(opcode))
} catch (e: Exception) {
Log.e(TAG, "发送自定义PPTM命令失败: ${e.message}")
callback?.onCommandSent(false, "发送自定义PPTM命令失败: ${e.message}")
}
}
/**
* 验证PPTM数据包CRC16校验
* @param packet 完整数据包
* @return 校验是否通过
*/
fun verifyPptmPacket(packet: ByteArray): Boolean {
return pptmEncoder.verifyCrc16(packet)
}
/**
* 解析PPTM数据包
* @param packet 完整数据包
* @return 解析结果 (操作码, 负载数据) null
*/
fun parsePptmPacket(packet: ByteArray): Pair<Short, ByteArray>? {
return pptmEncoder.parsePacket(packet)
}
/**
* 获取PPTM编码器实例用于调试
*/
fun getPptmEncoder(): PPTMCommandEncoder {
return pptmEncoder
}
}

View File

@ -1,183 +0,0 @@
package com.example.cmake_project_test
import android.util.Log
import type.SensorData
/**
* 完整数据处理管道示例
* 演示从原始数据到最终指标的完整流程
*/
class CompletePipelineExample {
private val dataMapper = DataMapper()
private val indicatorCalculator = IndicatorCalculator()
private val signalProcessor = SignalProcessorJNI()
private var dataMapperInitialized = false
private var indicatorCalculatorInitialized = false
private var signalProcessorInitialized = false
/**
* 初始化完整管道
*/
fun initialize(): Boolean {
Log.d("CompletePipeline", "初始化完整数据处理管道...")
// 初始化数据映射器
dataMapperInitialized = dataMapper.initialize()
if (!dataMapperInitialized) {
Log.e("CompletePipeline", "数据映射器初始化失败")
return false
}
// 初始化指标计算器
indicatorCalculatorInitialized = indicatorCalculator.initialize()
if (!indicatorCalculatorInitialized) {
Log.e("CompletePipeline", "指标计算器初始化失败")
cleanup()
return false
}
// 初始化信号处理器
try {
signalProcessorInitialized = signalProcessor.createProcessor()
if (!signalProcessorInitialized) {
Log.e("CompletePipeline", "信号处理器初始化失败")
cleanup()
return false
}
} catch (e: Exception) {
Log.e("CompletePipeline", "信号处理器初始化异常: ${e.message}")
cleanup()
return false
}
Log.d("CompletePipeline", "完整数据处理管道初始化成功")
return true
}
/**
* 处理单个数据包的完整流程
* 原始数据 解析数据 通道映射 信号处理 指标计算
*/
fun processCompletePipeline(rawData: SensorData, sampleRate: Float = 250.0f): Map<String, Float>? {
if (!isInitialized()) {
Log.w("CompletePipeline", "管道未初始化")
return null
}
Log.d("CompletePipeline", "开始处理完整数据管道...")
Log.d("CompletePipeline", "输入数据类型: ${rawData.dataType}, 采样率: $sampleRate Hz")
try {
// 步骤1: 通道映射
Log.d("CompletePipeline", "步骤1: 执行通道映射...")
val mappedData = dataMapper.mapSensorData(rawData)
if (mappedData == null) {
Log.e("CompletePipeline", "通道映射失败")
return null
}
Log.d("CompletePipeline", "通道映射完成")
// 步骤2: 信号处理 (这里可以添加信号处理逻辑)
Log.d("CompletePipeline", "步骤2: 执行信号处理...")
// 暂时跳过信号处理,直接使用映射后的数据
val processedData = mappedData
Log.d("CompletePipeline", "信号处理完成")
// 步骤3: 指标计算
Log.d("CompletePipeline", "步骤3: 执行指标计算...")
val metrics = indicatorCalculator.processCompletePipeline(processedData, sampleRate)
if (metrics == null) {
Log.e("CompletePipeline", "指标计算失败")
return null
}
Log.d("CompletePipeline", "指标计算完成,计算了 ${metrics.size} 个指标")
Log.d("CompletePipeline", "完整数据处理管道执行成功")
// 打印主要指标
metrics["heart_rate"]?.let {
Log.d("CompletePipeline", "心率: ${String.format("%.1f", it)} bpm")
}
metrics["signal_quality"]?.let {
Log.d("CompletePipeline", "信号质量: ${String.format("%.2f", it)}")
}
metrics["spo2"]?.let {
Log.d("CompletePipeline", "血氧饱和度: ${String.format("%.1f", it)}%")
}
return metrics
} catch (e: Exception) {
Log.e("CompletePipeline", "完整数据处理管道异常: ${e.message}")
return null
}
}
/**
* 批量处理数据包
*/
fun processBatchPipeline(rawDataList: List<SensorData>, sampleRate: Float = 250.0f): List<Map<String, Float>> {
if (!isInitialized()) {
Log.w("CompletePipeline", "管道未初始化")
return emptyList()
}
Log.d("CompletePipeline", "开始批量处理 ${rawDataList.size} 个数据包...")
val results = mutableListOf<Map<String, Float>>()
var successCount = 0
for ((index, rawData) in rawDataList.withIndex()) {
val metrics = processCompletePipeline(rawData, sampleRate)
if (metrics != null) {
results.add(metrics)
successCount++
} else {
results.add(emptyMap())
}
// 每处理100个数据包打印一次进度
if ((index + 1) % 100 == 0) {
Log.d("CompletePipeline", "已处理 ${index + 1}/${rawDataList.size} 个数据包")
}
}
Log.d("CompletePipeline", "批量处理完成: 成功 $successCount/${rawDataList.size} 个数据包")
return results
}
/**
* 检查管道是否已初始化
*/
fun isInitialized(): Boolean {
return dataMapperInitialized && indicatorCalculatorInitialized && signalProcessorInitialized
}
/**
* 清理资源
*/
fun cleanup() {
try {
if (dataMapperInitialized) {
// 使用公共方法或直接设置为false
dataMapperInitialized = false
}
if (indicatorCalculatorInitialized) {
// 使用公共方法或直接设置为false
indicatorCalculatorInitialized = false
}
if (signalProcessorInitialized) {
signalProcessor.destroyProcessor()
signalProcessorInitialized = false
}
Log.d("CompletePipelineExample", "资源清理完成")
} catch (e: Exception) {
Log.e("CompletePipelineExample", "资源清理异常: ${e.message}")
}
}
}

View File

@ -1,26 +0,0 @@
package com.example.cmake_project_test
object Constants {
// UI更新相关
const val UPDATE_INTERVAL = 200L // 优化每200毫秒更新一次UI减慢更新速度以便观察心电波形
// 数据分块相关
const val CHUNK_SIZE = 64 // 数据分块大小
// 缓冲区管理相关
const val BUFFER_CLEANUP_THRESHOLD = 50 // 缓冲区清理阈值
const val BUFFER_KEEP_COUNT = 30 // 缓冲区保留数量
// 显示限制相关
const val MAX_DISPLAY_PACKETS = 5 // 最大显示数据包数量
const val MAX_DETAIL_PACKETS = 3 // 最大详情数据包数量
const val MAX_DISPLAY_CHANNELS = 4 // 最大显示通道数量
const val MAX_12LEAD_CHANNELS = 6 // 12导联心电最大通道数量
const val MAX_DISPLAY_SAMPLES = 750 // 最大显示采样点数量调整为显示约3秒数据基于250Hz采样率
// 文件相关
const val DEFAULT_DATA_FILE = "ecg_data_raw.dat" // 默认数据文件名
// 延迟相关
const val SIMULATION_DELAY = 1L // 模拟传输延迟(毫秒)- 减少延迟
}

View File

@ -7,6 +7,17 @@ import java.io.IOException
import java.nio.ByteBuffer
import java.nio.ByteOrder
/**
* 原生方法回调接口
* 用于DataManager调用MainActivity中的原生方法
*/
interface NativeMethodCallback {
fun createStreamParser(): Long
fun destroyStreamParser(handle: Long)
fun streamParserAppend(handle: Long, chunk: ByteArray)
fun streamParserDrainPackets(handle: Long): List<SensorData>?
}
/**
* 数据管理类
* 负责数据解析缓冲管理和原生方法调用
@ -165,45 +176,7 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
// 注释掉后台处理,只显示原始数据
// processStreamingData(packets)
} else {
Log.w("DataManager", "没有解析出有效数据包,尝试生成测试数据")
// 如果原生解析器没有解析出数据包,生成一些测试数据来验证图表显示
generateTestDataForChart()
}
}
/**
* 生成测试数据用于验证图表显示
*/
private fun generateTestDataForChart() {
try {
Log.d("DataManager", "生成测试数据用于验证图表显示")
// 生成模拟的单导联ECG数据
val testData = mutableListOf<Float>()
val sampleCount = 100 // 生成100个样本点
for (i in 0 until sampleCount) {
val t = i.toFloat() / 10f // 时间参数
val value = (Math.sin(t * 2 * Math.PI) * 100 +
Math.sin(t * 4 * Math.PI) * 50 +
Math.sin(t * 8 * Math.PI) * 25).toFloat()
testData.add(value)
}
Log.d("DataManager", "生成测试数据: ${testData.size} 个样本点")
Log.d("DataManager", "测试数据前5个值: ${testData.take(5).joinToString(", ")}")
// 发送测试数据到图表
if (realTimeCallback != null) {
realTimeCallback!!.onRawDataAvailable(0, testData)
Log.d("DataManager", "已发送测试数据到图表")
} else {
Log.e("DataManager", "realTimeCallback 为空,无法发送测试数据")
}
} catch (e: Exception) {
Log.e("DataManager", "生成测试数据失败: ${e.message}", e)
Log.w("DataManager", "没有解析出有效数据包")
}
}
@ -649,48 +622,17 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
/**
* 处理文件数据
*/
fun processFileData(fileData: ByteArray, progressCallback: ((Int) -> Unit)? = null) {
Log.d("DataManager", "开始处理文件数据,数据大小: ${fileData.size} 字节")
try {
val chunkSize = 1024
var processedBytes = 0
for (i in fileData.indices step chunkSize) {
val endIndex = minOf(i + chunkSize, fileData.size)
val chunk = fileData.copyOfRange(i, endIndex)
onBleNotify(chunk)
processedBytes += chunk.size
// 性能优化:减少进度更新频率
if (processedBytes % (chunkSize * 10) == 0) {
val progress = (processedBytes * 100 / fileData.size).coerceAtMost(100)
progressCallback?.invoke(progress)
}
}
Log.d("DataManager", "文件数据处理完成,总共处理了 $processedBytes 字节")
} catch (e: Exception) {
Log.e("DataManager", "处理文件数据时发生错误: ${e.message}", e)
}
}
// Getter方法
fun getPacketBufferSize(): Int = packetBuffer.size
fun getProcessedPacketsSize(): Int = processedPackets.size
fun getCalculatedMetricsSize(): Int = calculatedMetrics.size
fun getRawStreamSize(): Int = rawStream.size()
fun getTotalPacketsParsed(): Long = totalPacketsParsed
fun getPacketBuffer(): List<SensorData> = packetBuffer
fun getProcessedPackets(): List<SensorData> = processedPackets
fun getCalculatedMetrics(): List<Map<String, Float>> = calculatedMetrics
fun getLatestMetrics(): Map<String, Float> = latestMetrics
fun getChannelBuffersStatus(): Map<Int, Int> = channelBuffers.mapValues { it.value.size }
// 流式处理相关getter方法
fun getProcessedChannelBuffersStatus(): Map<Int, Int> = processedChannelBuffers.mapValues { it.value.size }
fun getProcessedChannelData(channelIndex: Int): List<Float> = processedChannelBuffers[channelIndex] ?: emptyList()
fun getTotalProcessedSamples(): Long = totalProcessedSamples
fun getCurrentDataType(): type.SensorData.DataType? = currentDataType
@ -716,168 +658,9 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
}
}
/**
* 计算信号质量
*/
fun calculateSignalQuality(packet: SensorData): Float {
if (!streamingSignalProcessorInitialized || streamingSignalProcessor == null) return 0.0f
try {
// 获取第一个通道的数据进行质量评估
val channelData = packet.getChannelData()
if (channelData != null && channelData.isNotEmpty()) {
val firstChannel = channelData[0]
if (firstChannel != null) {
val signal = firstChannel.toFloatArray()
val quality = streamingSignalProcessor!!.calculateSignalQuality(signal)
// 添加调试信息
Log.d("DataManager", "信号质量计算: 通道0, 数据长度=${signal.size}, 质量=$quality")
return quality
}
}
} catch (e: Exception) {
Log.e("DataManager", "计算信号质量时发生错误: ${e.message}")
}
return 0.0f
}
/**
* 应用信号处理
*/
fun applySignalProcessing(packets: List<SensorData>): List<SensorData> {
Log.d("DataManager", "开始应用信号处理,处理 ${packets.size} 个数据包")
ensureStreamingSignalProcessor()
if (!streamingSignalProcessorInitialized || streamingSignalProcessor == null) {
Log.w("DataManager", "流式信号处理器未初始化,跳过信号处理")
return packets
}
val processedPackets = mutableListOf<SensorData>()
for (packet in packets) {
try {
val processedPacket = processSinglePacket(packet)
processedPackets.add(processedPacket)
} catch (e: Exception) {
Log.e("DataManager", "处理数据包时发生错误: ${e.message}")
processedPackets.add(packet) // 处理失败时返回原始数据包
}
}
Log.d("DataManager", "信号处理完成,处理了 ${processedPackets.size} 个数据包")
return processedPackets
}
/**
* 处理单个数据包
*/
private fun processSinglePacket(packet: SensorData): SensorData {
val channelData = packet.getChannelData()
if (channelData == null || channelData.isEmpty()) {
return packet
}
val processedChannels = mutableListOf<List<Float>>()
for (channel in channelData) {
val processedChannel = when (packet.getDataType()) {
type.SensorData.DataType.EEG -> applyEEGFilters(channel)
type.SensorData.DataType.ECG_2LEAD, type.SensorData.DataType.ECG_12LEAD -> applyECGFilters(channel)
type.SensorData.DataType.PPG -> channel // PPG不进行滤波处理直接返回原始数据
else -> channel // 其他类型暂时不处理
}
processedChannels.add(processedChannel)
}
// 创建处理后的数据包
val processedPacket = SensorData()
processedPacket.setDataType(packet.getDataType())
processedPacket.setTimestamp(packet.getTimestamp())
processedPacket.setPacketSn(packet.getPacketSn())
processedPacket.setChannelData(processedChannels)
return processedPacket
}
/**
* 应用EEG滤波器
*/
private fun applyEEGFilters(channel: List<Float>): List<Float> {
val signal = channel.toFloatArray()
try {
if (streamingSignalProcessor != null) {
// 1. 带通滤波 (1-40HzEEG主要频率范围)
val bandpassFiltered = streamingSignalProcessor!!.bandpassFilter(
signal,
filterSettings.sampleRate.toFloat(),
1.0f,
40.0f
)
if (bandpassFiltered != null) {
// 2. 幅度归一化
val normalized = streamingSignalProcessor!!.normalizeAmplitude(bandpassFiltered)
if (normalized != null) {
return normalized.toList()
}
}
}
} catch (e: Exception) {
Log.e("DataManager", "EEG滤波处理失败: ${e.message}")
}
return channel // 处理失败时返回原始数据
}
/**
* 应用ECG滤波器
*/
private fun applyECGFilters(channel: List<Float>): List<Float> {
val signal = channel.toFloatArray()
try {
if (streamingSignalProcessor != null) {
// 1. 高通滤波 (0.5Hz,去除基线漂移)
val highpassFiltered = streamingSignalProcessor!!.highpassFilter(
signal,
filterSettings.sampleRate.toFloat(),
0.5f
)
if (highpassFiltered != null) {
// 2. 低通滤波 (40Hz去除高频噪声)
val lowpassFiltered = streamingSignalProcessor!!.lowpassFilter(
highpassFiltered,
filterSettings.sampleRate.toFloat(),
40.0f
)
if (lowpassFiltered != null) {
// 3. 陷波滤波 (50Hz去除工频干扰)
val notchFiltered = streamingSignalProcessor!!.notchFilter(
lowpassFiltered,
filterSettings.sampleRate.toFloat(),
50.0f,
10.0f
)
if (notchFiltered != null) {
return notchFiltered.toList()
}
}
}
}
} catch (e: Exception) {
Log.e("DataManager", "ECG滤波处理失败: ${e.message}")
}
return channel // 处理失败时返回原始数据
}
/**
* 清理缓冲区
@ -982,39 +765,6 @@ class DataManager(private val nativeCallback: NativeMethodCallback) {
/**
* 应用PPG滤波器
*/
private fun applyPPGFilters(channel: List<Float>): List<Float> {
val signal = channel.toFloatArray()
try {
if (streamingSignalProcessor != null) {
// 1. 低通滤波 (8HzPPG主要频率范围)
val filtered = streamingSignalProcessor!!.lowpassFilter(
signal,
filterSettings.sampleRate.toFloat(),
8.0f
)
if (filtered != null) {
// 2. 去运动伪影
val motionArtifactRemoved = streamingSignalProcessor!!.processRealtimeChunk(
filtered,
filterSettings.sampleRate.toFloat()
)
if (motionArtifactRemoved != null) {
return motionArtifactRemoved.toList()
}
}
}
} catch (e: Exception) {
Log.e("DataManager", "PPG滤波处理失败: ${e.message}")
}
return channel // 处理失败时返回原始数据
}
/**
* 设置滤波器参数

View File

@ -1,79 +0,0 @@
package com.example.cmake_project_test
import type.SensorData
/**
* 设备类型帮助类
* 负责设备类型名称映射和通道数据详情构建
*/
object DeviceTypeHelper {
/**
* 根据数据类型获取设备名称
*/
fun getDeviceName(dataType: SensorData.DataType): String {
return when (dataType) {
SensorData.DataType.EEG -> "脑电设备"
SensorData.DataType.ECG_2LEAD -> "胸腹设备"
SensorData.DataType.PPG -> "血氧设备"
SensorData.DataType.ECG_12LEAD -> "12导联心电"
SensorData.DataType.STETHOSCOPE -> "数字听诊"
SensorData.DataType.SNORE -> "鼾声设备"
SensorData.DataType.RESPIRATION -> "呼吸/姿态"
SensorData.DataType.MIT_BIH -> "MIT-BIH"
else -> "未知设备"
}
}
/**
* 构建通道数据详情字符串
*/
fun buildChannelDetails(
data: List<SensorData>,
maxPackets: Int = Constants.MAX_DETAIL_PACKETS,
maxChannels: Int = Constants.MAX_DISPLAY_CHANNELS,
maxSamples: Int = Constants.MAX_DISPLAY_SAMPLES
): String {
if (data.isEmpty()) {
return "无通道数据"
}
val details = mutableListOf<String>()
data.take(maxPackets).forEachIndexed { packetIndex, sensorData ->
if (sensorData.channelData.isNullOrEmpty()) {
details.add("数据包 ${packetIndex + 1}: 无通道数据")
return@forEachIndexed
}
details.add("数据包 ${packetIndex + 1} (${getDeviceName(sensorData.dataType ?: SensorData.DataType.EEG)}):")
sensorData.channelData.take(maxChannels).forEachIndexed { channelIndex, channel ->
if (channel.isNullOrEmpty()) {
details.add(" 通道 ${channelIndex + 1}: 空数据")
return@forEachIndexed
}
// 只显示前几个采样点
val sampleCount = minOf(maxSamples, channel.size)
val channelDataStr = channel.take(sampleCount).joinToString(", ") { "%.1f".format(it) }
details.add(" 通道 ${channelIndex + 1}: ${sampleCount}/${channel.size} 采样点")
details.add(" 示例: $channelDataStr${if (channel.size > sampleCount) "..." else ""}")
}
if (sensorData.channelData.size > maxChannels) {
details.add(" ... 还有 ${sensorData.channelData.size - maxChannels} 个通道")
}
// 添加分隔线
details.add("")
}
if (data.size > maxPackets) {
details.add("... 还有 ${data.size - maxPackets} 个数据包")
}
return details.joinToString("\n")
}
}

View File

@ -1,414 +0,0 @@
package com.example.cmake_project_test
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import kotlin.math.max
import kotlin.math.min
class ECGChartView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paint = Paint().apply {
color = Color.BLUE
strokeWidth = 2f
style = Paint.Style.STROKE
isAntiAlias = true
}
private val gridPaint = Paint().apply {
color = Color.LTGRAY
strokeWidth = 1f
style = Paint.Style.STROKE
alpha = 100
}
private val textPaint = Paint().apply {
color = Color.BLACK
textSize = 30f
isAntiAlias = true
}
private val buttonPaint = Paint().apply {
color = Color.GRAY
style = Paint.Style.FILL
alpha = 150
}
private val buttonTextPaint = Paint().apply {
color = Color.WHITE
textSize = 20f
isAntiAlias = true
textAlign = Paint.Align.CENTER
}
private val path = Path()
private var dataPoints = mutableListOf<Float>()
private var maxDataPoints = 1000 // 优化:显示更多数据点,提供更好的可视化效果
private var minValue = Float.MAX_VALUE
private var maxValue = Float.MIN_VALUE
// 缩放控制参数
private var scaleX = 1.0f // X轴缩放因子
private var scaleY = 1.0f // Y轴缩放因子
private var offsetX = 0f // X轴偏移
private var offsetY = 0f // Y轴偏移
// 自动居中控制
private var autoCenter = true // 自动居中模式
private var lockToCenter = true // 锁定到屏幕中央
// 手势检测器
private val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent): Boolean {
// 双击重置缩放并锁定到中央
resetZoom()
lockToCenter = true
invalidate()
return true
}
override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
// 只有在非锁定模式下才允许平移
if (!lockToCenter) {
offsetX -= distanceX
offsetY -= distanceY
invalidate()
}
return true
}
})
// 按钮区域
private val buttonSize = 60f
private val buttonMargin = 20f
// 缩放按钮区域
private val zoomInXButtonRect = RectF()
private val zoomOutXButtonRect = RectF()
private val zoomInYButtonRect = RectF()
private val zoomOutYButtonRect = RectF()
private val resetButtonRect = RectF()
private val lockButtonRect = RectF()
fun updateData(newData: List<Float>) {
// 快速累积数据
dataPoints.addAll(newData)
// 限制数据点数量(避免内存溢出)
if (dataPoints.size > maxDataPoints) {
dataPoints = dataPoints.takeLast(maxDataPoints).toMutableList()
}
// 快速计算数据范围(减少计算量)
if (dataPoints.isNotEmpty()) {
// 只计算最近的数据范围,提高性能
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 {
// 添加10%的上下边距(减少边距,提高响应速度)
val margin = range * 0.1f
minValue -= margin
maxValue += margin
}
}
// 如果启用自动居中,重置偏移
if (autoCenter) {
offsetX = 0f
offsetY = 0f
}
// 立即重绘
invalidate()
}
fun clearData() {
dataPoints.clear()
invalidate()
}
// 缩放控制方法
fun zoomInX() {
scaleX = min(scaleX * 1.2f, 5.0f)
invalidate()
}
fun zoomOutX() {
scaleX = max(scaleX / 1.2f, 0.2f)
invalidate()
}
fun zoomInY() {
scaleY = min(scaleY * 1.2f, 5.0f)
invalidate()
}
fun zoomOutY() {
scaleY = max(scaleY / 1.2f, 0.2f)
invalidate()
}
fun resetZoom() {
scaleX = 1.0f
scaleY = 1.0f
offsetX = 0f
offsetY = 0f
lockToCenter = true
invalidate()
}
// 切换锁定模式
fun toggleLockMode() {
lockToCenter = !lockToCenter
if (lockToCenter) {
offsetX = 0f
offsetY = 0f
}
invalidate()
}
// 获取锁定状态
fun isLockedToCenter(): Boolean = lockToCenter
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// 检查是否点击了按钮
val x = event.x
val y = event.y
if (zoomInXButtonRect.contains(x, y)) {
zoomInX()
return true
} else if (zoomOutXButtonRect.contains(x, y)) {
zoomOutX()
return true
} else if (zoomInYButtonRect.contains(x, y)) {
zoomInY()
return true
} else if (zoomOutYButtonRect.contains(x, y)) {
zoomOutY()
return true
} else if (resetButtonRect.contains(x, y)) {
resetZoom()
return true
} else if (lockButtonRect.contains(x, y)) {
toggleLockMode()
return true
}
}
}
// 处理手势
return gestureDetector.onTouchEvent(event) || super.onTouchEvent(event)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val width = width.toFloat()
val height = height.toFloat()
// 更新按钮位置
updateButtonPositions(width, height)
// 绘制网格
drawGrid(canvas, width, height)
// 绘制标题
canvas.drawText("ECG Channel 1 (滤波后)", 20f, 40f, textPaint)
// 绘制缩放信息
canvas.drawText("X缩放: ${String.format("%.1f", scaleX)}x", 20f, 70f, textPaint)
canvas.drawText("Y缩放: ${String.format("%.1f", scaleY)}x", 20f, 100f, textPaint)
// 绘制锁定状态
val lockStatus = if (lockToCenter) "🔒 已锁定" else "🔓 已解锁"
canvas.drawText(lockStatus, 20f, 130f, textPaint)
// 绘制数据点数量
canvas.drawText("数据点: ${dataPoints.size}", 20f, height - 20f, textPaint)
// 绘制数据范围
if (dataPoints.isNotEmpty()) {
canvas.drawText("范围: ${String.format("%.3f", minValue)} - ${String.format("%.3f", maxValue)}",
20f, height - 50f, textPaint)
}
// 绘制控制按钮
drawControlButtons(canvas)
// 绘制曲线
if (dataPoints.size > 1) {
drawCurve(canvas, width, height)
}
}
private fun updateButtonPositions(width: Float, height: Float) {
val buttonY = height - buttonSize - buttonMargin
// X轴缩放按钮
zoomInXButtonRect.set(
width - buttonSize * 6 - buttonMargin * 6,
buttonY,
width - buttonSize * 5 - buttonMargin * 5,
buttonY + buttonSize
)
zoomOutXButtonRect.set(
width - buttonSize * 5 - buttonMargin * 5,
buttonY,
width - buttonSize * 4 - buttonMargin * 4,
buttonY + buttonSize
)
// Y轴缩放按钮
zoomInYButtonRect.set(
width - buttonSize * 4 - buttonMargin * 4,
buttonY,
width - buttonSize * 3 - buttonMargin * 3,
buttonY + buttonSize
)
zoomOutYButtonRect.set(
width - buttonSize * 3 - buttonMargin * 3,
buttonY,
width - buttonSize * 2 - buttonMargin * 2,
buttonY + buttonSize
)
// 重置按钮
resetButtonRect.set(
width - buttonSize * 2 - buttonMargin * 2,
buttonY,
width - buttonSize - buttonMargin,
buttonY + buttonSize
)
// 锁定按钮
lockButtonRect.set(
width - buttonSize - buttonMargin,
buttonY,
width - buttonMargin,
buttonY + buttonSize
)
}
private fun drawControlButtons(canvas: Canvas) {
// 绘制X轴缩放按钮
canvas.drawRoundRect(zoomInXButtonRect, 10f, 10f, buttonPaint)
canvas.drawText("X+", zoomInXButtonRect.centerX(), zoomInXButtonRect.centerY() + 7f, buttonTextPaint)
canvas.drawRoundRect(zoomOutXButtonRect, 10f, 10f, buttonTextPaint)
canvas.drawText("X-", zoomOutXButtonRect.centerX(), zoomOutXButtonRect.centerY() + 7f, buttonTextPaint)
// 绘制Y轴缩放按钮
canvas.drawRoundRect(zoomInYButtonRect, 10f, 10f, buttonPaint)
canvas.drawText("Y+", zoomInYButtonRect.centerX(), zoomInYButtonRect.centerY() + 7f, buttonTextPaint)
canvas.drawRoundRect(zoomOutYButtonRect, 10f, 10f, buttonPaint)
canvas.drawText("Y-", zoomOutYButtonRect.centerX(), zoomOutYButtonRect.centerY() + 7f, buttonTextPaint)
// 绘制重置按钮
canvas.drawRoundRect(resetButtonRect, 10f, 10f, buttonPaint)
canvas.drawText("重置", resetButtonRect.centerX(), resetButtonRect.centerY() + 7f, buttonTextPaint)
// 绘制锁定按钮
val lockButtonColor = if (lockToCenter) Color.GREEN else Color.RED
buttonPaint.color = lockButtonColor
canvas.drawRoundRect(lockButtonRect, 10f, 10f, buttonPaint)
canvas.drawText(if (lockToCenter) "🔒" else "🔓", lockButtonRect.centerX(), lockButtonRect.centerY() + 7f, buttonTextPaint)
buttonPaint.color = Color.GRAY // 恢复默认颜色
}
private fun drawGrid(canvas: Canvas, width: Float, height: Float) {
// 绘制垂直线
val verticalSpacing = width / 10
for (i in 0..10) {
val x = i * verticalSpacing
canvas.drawLine(x, 0f, x, height, gridPaint)
}
// 绘制水平线
val horizontalSpacing = height / 8
for (i in 0..8) {
val y = i * horizontalSpacing
canvas.drawLine(0f, y, width, y, gridPaint)
}
}
private fun drawCurve(canvas: Canvas, width: Float, height: Float) {
path.reset()
val padding = 60f
val drawWidth = width - 2 * padding
val drawHeight = height - 2 * padding
// 应用缩放和偏移
val scaledWidth = drawWidth * scaleX
val scaledHeight = drawHeight * scaleY
// 如果锁定到中央强制偏移为0
val effectiveOffsetX = if (lockToCenter) 0f else offsetX
val effectiveOffsetY = if (lockToCenter) 0f else offsetY
val xStep = scaledWidth / (dataPoints.size - 1)
for (i in dataPoints.indices) {
val x = padding + i * xStep + effectiveOffsetX
val normalizedValue = (dataPoints[i] - minValue) / (maxValue - minValue)
// 使用0.1到0.9的范围确保曲线在中间80%的区域显示
val y = padding + (0.1f + normalizedValue * 0.8f) * scaledHeight + effectiveOffsetY
// 确保点在可见区域内
if (x >= padding && x <= width - padding && y >= padding && y <= height - padding) {
if (i == 0) {
path.moveTo(x, y)
} else {
path.lineTo(x, y)
}
}
}
canvas.drawPath(path, paint)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val desiredWidth = 800
val desiredHeight = 400
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val width = when (widthMode) {
MeasureSpec.EXACTLY -> widthSize
MeasureSpec.AT_MOST -> min(desiredWidth, widthSize)
else -> desiredWidth
}
val height = when (heightMode) {
MeasureSpec.EXACTLY -> heightSize
MeasureSpec.AT_MOST -> min(desiredHeight, heightSize)
else -> desiredHeight
}
setMeasuredDimension(width, height)
}
}

View File

@ -1,367 +0,0 @@
package com.example.cmake_project_test
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import kotlin.math.max
import kotlin.math.min
/**
* ECG节律视图
* 显示10秒2500的连续信号用于评估心率节律
*/
class ECGRhythmView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paint = Paint().apply {
color = Color.BLUE
strokeWidth = 1.5f
style = Paint.Style.STROKE
isAntiAlias = false // 关闭抗锯齿,提高绘制性能
}
private val gridPaint = Paint().apply {
color = Color.LTGRAY
strokeWidth = 0.5f
style = Paint.Style.STROKE
alpha = 80
}
private val textPaint = Paint().apply {
color = Color.BLACK
textSize = 24f
isAntiAlias = true
}
private val path = Path()
private var dataPoints = mutableListOf<Float>()
private var maxDataPoints = 2500 // 10秒数据250Hz采样率
private var minValue = Float.MAX_VALUE
private var maxValue = Float.MIN_VALUE
private var isDataAvailable = false // 标记是否有数据
// 简化的数据管理:直接显示新数据
// 缩放控制参数
private var scaleX = 1.0f
private var scaleY = 1.0f
private var offsetX = 0f
private var offsetY = 0f
// 手势检测器
private val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent): Boolean {
resetZoom()
return true
}
override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
offsetX -= distanceX
offsetY -= distanceY
invalidate()
return true
}
})
// 缩放手势检测器
private val scaleGestureDetector = android.view.ScaleGestureDetector(context,
object : android.view.ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: android.view.ScaleGestureDetector): Boolean {
scaleX *= detector.scaleFactor
scaleY *= detector.scaleFactor
// 限制缩放范围
scaleX = scaleX.coerceIn(0.1f, 5.0f)
scaleY = scaleY.coerceIn(0.1f, 5.0f)
invalidate()
return true
}
})
// 触摸事件处理
private var lastTouchX = 0f
private var lastTouchY = 0f
private var isDragging = false
fun updateData(newData: List<Float>) {
if (newData.isEmpty()) return
isDataAvailable = true
// 直接添加新数据,来多少显示多少
dataPoints.addAll(newData)
// 限制数据点数量,保持滚动显示
if (dataPoints.size > maxDataPoints) {
dataPoints = dataPoints.takeLast(maxDataPoints).toMutableList()
}
// 更新数据范围
updateDataRange(newData)
// 立即重绘
invalidate()
}
private fun updateDataRange(newData: List<Float>) {
// 使用全部数据点计算范围,提供更准确的范围
if (dataPoints.isEmpty()) return
// 计算全部数据点的范围
minValue = dataPoints.minOrNull() ?: 0f
maxValue = dataPoints.maxOrNull() ?: 0f
// 确保有足够的显示范围
val range = maxValue - minValue
if (range < 0.1f) {
val center = (maxValue + minValue) / 2
minValue = center - 0.05f
maxValue = center + 0.05f
} else {
// 添加10%的上下边距
val margin = range * 0.1f
minValue -= margin
maxValue += margin
}
}
fun clearData() {
dataPoints.clear()
invalidate()
}
/**
* 获取当前图表数据
*/
fun getCurrentData(): List<Float> {
return dataPoints.toList()
}
/**
* 获取数据统计信息
*/
fun getDataStats(): Map<String, Any> {
return mapOf(
"dataPoints" to dataPoints.size,
"minValue" to minValue,
"maxValue" to maxValue,
"range" to (maxValue - minValue),
"isDataAvailable" to isDataAvailable
)
}
fun resetZoom() {
scaleX = 1.0f
scaleY = 1.0f
offsetX = 0f
offsetY = 0f
invalidate()
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
lastTouchX = event.x
lastTouchY = event.y
isDragging = false
}
MotionEvent.ACTION_MOVE -> {
if (event.pointerCount == 1) {
// 单指拖拽
val deltaX = event.x - lastTouchX
val deltaY = event.y - lastTouchY
offsetX += deltaX
offsetY += deltaY
lastTouchX = event.x
lastTouchY = event.y
isDragging = true
invalidate()
}
}
MotionEvent.ACTION_UP -> {
if (!isDragging) {
// 如果没有拖拽,可能是点击事件
return gestureDetector.onTouchEvent(event)
}
}
}
// 处理缩放手势
scaleGestureDetector.onTouchEvent(event)
return true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val width = width.toFloat()
val height = height.toFloat()
// 绘制网格
drawGrid(canvas, width, height)
// 绘制标题
canvas.drawText("ECG节律视图 (10秒)", 20f, 30f, textPaint)
// 绘制时间轴标签
canvas.drawText("0s", 20f, height - 10f, textPaint)
canvas.drawText("5s", width / 2 - 15f, height - 10f, textPaint)
canvas.drawText("10s", width - 50f, height - 10f, textPaint)
// 绘制数据点数量
canvas.drawText("数据点: ${dataPoints.size}", 20f, height - 35f, textPaint)
// 绘制数据范围
if (dataPoints.isNotEmpty()) {
canvas.drawText("范围: ${String.format("%.3f", minValue)} - ${String.format("%.3f", maxValue)}",
20f, height - 60f, textPaint)
}
// 绘制缩放信息
canvas.drawText("X缩放: ${String.format("%.1f", scaleX)}x", width - 150f, 30f, textPaint)
canvas.drawText("Y缩放: ${String.format("%.1f", scaleY)}x", width - 150f, 55f, textPaint)
// 如果没有数据,显示默认内容
if (!isDataAvailable) {
drawDefaultContent(canvas, width, height)
} else {
// 绘制曲线
if (dataPoints.size > 1) {
drawCurve(canvas, width, height)
}
}
}
private fun drawGrid(canvas: Canvas, width: Float, height: Float) {
// 绘制垂直线(时间轴)
val verticalSpacing = width / 20 // 20条垂直线每0.5秒一条
for (i in 0..20) {
val x = i * verticalSpacing
canvas.drawLine(x, 0f, x, height, gridPaint)
}
// 绘制水平线(幅度轴)
val horizontalSpacing = height / 10
for (i in 0..10) {
val y = i * horizontalSpacing
canvas.drawLine(0f, y, width, y, gridPaint)
}
}
private fun drawDefaultContent(canvas: Canvas, width: Float, height: Float) {
// 绘制默认的提示信息
val centerX = width / 2
val centerY = height / 2
// 绘制提示文字
val hintPaint = Paint().apply {
color = Color.GRAY
textSize = 32f
isAntiAlias = true
textAlign = Paint.Align.CENTER
}
canvas.drawText("等待数据...", centerX, centerY - 20f, hintPaint)
canvas.drawText("请先连接蓝牙设备", centerX, centerY + 20f, hintPaint)
canvas.drawText("然后点击'启动程序'显示原始数据", centerX, centerY + 60f, hintPaint)
// 绘制一个简单的示例波形
drawSampleWaveform(canvas, width, height)
}
private fun drawSampleWaveform(canvas: Canvas, width: Float, height: Float) {
val samplePaint = Paint().apply {
color = Color.LTGRAY
strokeWidth = 2f
style = Paint.Style.STROKE
isAntiAlias = true
alpha = 100
}
val path = Path()
val padding = 80f
val drawWidth = width - 2 * padding
val drawHeight = height - 2 * padding
val centerY = height / 2
path.moveTo(padding, centerY)
// 绘制一个简单的正弦波示例
for (i in 0..100) {
val x = padding + (i * drawWidth / 100)
val y = centerY + (Math.sin(i * 0.3) * drawHeight / 4).toFloat()
path.lineTo(x, y)
}
canvas.drawPath(path, samplePaint)
}
private fun drawCurve(canvas: Canvas, width: Float, height: Float) {
path.reset()
val padding = 80f
val drawWidth = width - 2 * padding
val drawHeight = height - 2 * padding
// 应用缩放和偏移
val scaledWidth = drawWidth * scaleX
val scaledHeight = drawHeight * scaleY
// 性能优化:减少数据点绘制,提高流畅度
val stepSize = maxOf(1, dataPoints.size / 1000) // 最多绘制1000个点
val xStep = scaledWidth / (dataPoints.size - 1)
var firstPoint = true
for (i in dataPoints.indices step stepSize) {
val x = padding + i * xStep + offsetX
val normalizedValue = (dataPoints[i] - minValue) / (maxValue - minValue)
// 使用0.1到0.9的范围确保曲线在中间80%的区域显示
val y = padding + (0.1f + normalizedValue * 0.8f) * scaledHeight + offsetY
// 确保点在可见区域内
if (x >= padding && x <= width - padding && y >= padding && y <= height - padding) {
if (firstPoint) {
path.moveTo(x, y)
firstPoint = false
} else {
path.lineTo(x, y)
}
}
}
canvas.drawPath(path, paint)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val desiredWidth = 1000
val desiredHeight = 300
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val width = when (widthMode) {
MeasureSpec.EXACTLY -> widthSize
MeasureSpec.AT_MOST -> min(desiredWidth, widthSize)
else -> desiredWidth
}
val height = when (heightMode) {
MeasureSpec.EXACTLY -> heightSize
MeasureSpec.AT_MOST -> min(desiredHeight, heightSize)
else -> desiredHeight
}
setMeasuredDimension(width, height)
}
}

View File

@ -1,363 +0,0 @@
package com.example.cmake_project_test
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import kotlin.math.max
import kotlin.math.min
/**
* ECG波形视图
* 显示2.5625的放大信号用于精确测量波形形态和间期
*/
class ECGWaveformView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paint = Paint().apply {
color = Color.RED
strokeWidth = 2f
style = Paint.Style.STROKE
isAntiAlias = true
}
private val gridPaint = Paint().apply {
color = Color.LTGRAY
strokeWidth = 1f
style = Paint.Style.STROKE
alpha = 100
}
private val textPaint = Paint().apply {
color = Color.BLACK
textSize = 28f
isAntiAlias = true
}
private val path = Path()
private var dataPoints = mutableListOf<Float>()
private var maxDataPoints = 625 // 2.5秒数据250Hz采样率
private var minValue = Float.MAX_VALUE
private var maxValue = Float.MIN_VALUE
private var isDataAvailable = false // 标记是否有数据
// 简化的数据管理:直接显示新数据
// 缩放控制参数
private var scaleX = 1.0f
private var scaleY = 1.0f
private var offsetX = 0f
private var offsetY = 0f
// 手势检测器
private val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent): Boolean {
resetZoom()
return true
}
override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
offsetX -= distanceX
offsetY -= distanceY
invalidate()
return true
}
})
// 缩放手势检测器
private val scaleGestureDetector = android.view.ScaleGestureDetector(context,
object : android.view.ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: android.view.ScaleGestureDetector): Boolean {
scaleX *= detector.scaleFactor
scaleY *= detector.scaleFactor
// 限制缩放范围
scaleX = scaleX.coerceIn(0.1f, 5.0f)
scaleY = scaleY.coerceIn(0.1f, 5.0f)
invalidate()
return true
}
})
// 触摸事件处理
private var lastTouchX = 0f
private var lastTouchY = 0f
private var isDragging = false
fun updateData(newData: List<Float>) {
if (newData.isEmpty()) return
isDataAvailable = true
// 直接添加新数据,来多少显示多少
dataPoints.addAll(newData)
// 限制数据点数量,保持滚动显示
if (dataPoints.size > maxDataPoints) {
dataPoints = dataPoints.takeLast(maxDataPoints).toMutableList()
}
// 更新数据范围
updateDataRange(newData)
// 立即重绘
invalidate()
}
private fun updateDataRange(newData: List<Float>) {
// 使用全部数据点计算范围,提供更准确的范围
if (dataPoints.isEmpty()) return
// 计算全部数据点的范围
minValue = dataPoints.minOrNull() ?: 0f
maxValue = dataPoints.maxOrNull() ?: 0f
// 确保有足够的显示范围
val range = maxValue - minValue
if (range < 0.1f) {
val center = (maxValue + minValue) / 2
minValue = center - 0.05f
maxValue = center + 0.05f
} else {
// 添加15%的上下边距
val margin = range * 0.15f
minValue -= margin
maxValue += margin
}
}
fun clearData() {
dataPoints.clear()
invalidate()
}
/**
* 获取当前图表数据
*/
fun getCurrentData(): List<Float> {
return dataPoints.toList()
}
/**
* 获取数据统计信息
*/
fun getDataStats(): Map<String, Any> {
return mapOf(
"dataPoints" to dataPoints.size,
"minValue" to minValue,
"maxValue" to maxValue,
"range" to (maxValue - minValue),
"isDataAvailable" to isDataAvailable
)
}
fun resetZoom() {
scaleX = 1.0f
scaleY = 1.0f
offsetX = 0f
offsetY = 0f
invalidate()
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
lastTouchX = event.x
lastTouchY = event.y
isDragging = false
}
MotionEvent.ACTION_MOVE -> {
if (event.pointerCount == 1) {
// 单指拖拽
val deltaX = event.x - lastTouchX
val deltaY = event.y - lastTouchY
offsetX += deltaX
offsetY += deltaY
lastTouchX = event.x
lastTouchY = event.y
isDragging = true
invalidate()
}
}
MotionEvent.ACTION_UP -> {
if (!isDragging) {
// 如果没有拖拽,可能是点击事件
return gestureDetector.onTouchEvent(event)
}
}
}
// 处理缩放手势
scaleGestureDetector.onTouchEvent(event)
return true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val width = width.toFloat()
val height = height.toFloat()
// 绘制网格
drawGrid(canvas, width, height)
// 绘制标题
canvas.drawText("ECG波形视图 (2.5秒)", 20f, 35f, textPaint)
// 绘制时间轴标签
canvas.drawText("0s", 20f, height - 10f, textPaint)
canvas.drawText("1.25s", width / 2 - 25f, height - 10f, textPaint)
canvas.drawText("2.5s", width - 60f, height - 10f, textPaint)
// 绘制数据点数量
canvas.drawText("数据点: ${dataPoints.size}", 20f, height - 40f, textPaint)
// 绘制数据范围
if (dataPoints.isNotEmpty()) {
canvas.drawText("范围: ${String.format("%.3f", minValue)} - ${String.format("%.3f", maxValue)}",
20f, height - 70f, textPaint)
}
// 绘制缩放信息
canvas.drawText("X缩放: ${String.format("%.1f", scaleX)}x", width - 150f, 35f, textPaint)
canvas.drawText("Y缩放: ${String.format("%.1f", scaleY)}x", width - 150f, 65f, textPaint)
// 如果没有数据,显示默认内容
if (!isDataAvailable) {
drawDefaultContent(canvas, width, height)
} else {
// 绘制曲线
if (dataPoints.size > 1) {
drawCurve(canvas, width, height)
}
}
}
private fun drawGrid(canvas: Canvas, width: Float, height: Float) {
// 绘制垂直线(时间轴)- 更密集的网格用于精确测量
val verticalSpacing = width / 25 // 25条垂直线每0.1秒一条
for (i in 0..25) {
val x = i * verticalSpacing
canvas.drawLine(x, 0f, x, height, gridPaint)
}
// 绘制水平线(幅度轴)
val horizontalSpacing = height / 12 // 12条水平线更密集
for (i in 0..12) {
val y = i * horizontalSpacing
canvas.drawLine(0f, y, width, y, gridPaint)
}
}
private fun drawDefaultContent(canvas: Canvas, width: Float, height: Float) {
// 绘制默认的提示信息
val centerX = width / 2
val centerY = height / 2
// 绘制提示文字
val hintPaint = Paint().apply {
color = Color.GRAY
textSize = 28f
isAntiAlias = true
textAlign = Paint.Align.CENTER
}
canvas.drawText("等待数据...", centerX, centerY - 20f, hintPaint)
canvas.drawText("请先连接蓝牙设备", centerX, centerY + 20f, hintPaint)
canvas.drawText("然后点击'启动程序'显示原始数据", centerX, centerY + 60f, hintPaint)
// 绘制一个简单的示例波形
drawSampleWaveform(canvas, width, height)
}
private fun drawSampleWaveform(canvas: Canvas, width: Float, height: Float) {
val samplePaint = Paint().apply {
color = Color.LTGRAY
strokeWidth = 1.5f
style = Paint.Style.STROKE
isAntiAlias = true
alpha = 80
}
val path = Path()
val padding = 100f
val drawWidth = width - 2 * padding
val drawHeight = height - 2 * padding
val centerY = height / 2
path.moveTo(padding, centerY)
// 绘制一个简单的正弦波示例
for (i in 0..100) {
val x = padding + (i * drawWidth / 100)
val y = centerY + (Math.sin(i * 0.5) * drawHeight / 6).toFloat()
path.lineTo(x, y)
}
canvas.drawPath(path, samplePaint)
}
private fun drawCurve(canvas: Canvas, width: Float, height: Float) {
path.reset()
val padding = 100f
val drawWidth = width - 2 * padding
val drawHeight = height - 2 * padding
// 应用缩放和偏移
val scaledWidth = drawWidth * scaleX
val scaledHeight = drawHeight * scaleY
val xStep = scaledWidth / (dataPoints.size - 1)
for (i in dataPoints.indices) {
val x = padding + i * xStep + offsetX
val normalizedValue = (dataPoints[i] - minValue) / (maxValue - minValue)
// 使用0.1到0.9的范围确保曲线在中间80%的区域显示
val y = padding + (0.1f + normalizedValue * 0.8f) * scaledHeight + offsetY
// 确保点在可见区域内
if (x >= padding && x <= width - padding && y >= padding && y <= height - padding) {
if (i == 0) {
path.moveTo(x, y)
} else {
path.lineTo(x, y)
}
}
}
canvas.drawPath(path, paint)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val desiredWidth = 1000
val desiredHeight = 400
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val width = when (widthMode) {
MeasureSpec.EXACTLY -> widthSize
MeasureSpec.AT_MOST -> min(desiredWidth, widthSize)
else -> desiredWidth
}
val height = when (heightMode) {
MeasureSpec.EXACTLY -> heightSize
MeasureSpec.AT_MOST -> min(desiredHeight, heightSize)
else -> desiredHeight
}
setMeasuredDimension(width, height)
}
}

View File

@ -1,40 +0,0 @@
package com.example.cmake_project_test
import android.content.Context
import android.util.Log
import java.io.IOException
/**
* 文件帮助类
* 负责文件读取操作
*/
object FileHelper {
/**
* assets 文件夹读取文件到字节数组
*/
fun readAssetFile(context: Context, fileName: String): ByteArray? {
return try {
context.assets.open(fileName).use { inputStream ->
// 使用更可靠的文件读取方式
val bytes = mutableListOf<Byte>()
val buffer = ByteArray(1024)
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
for (i in 0 until bytesRead) {
bytes.add(buffer[i])
}
}
bytes.toByteArray()
}
} catch (e: IOException) {
Log.e("FileHelper", "Error reading asset file: $fileName", e)
null
} catch (e: Exception) {
Log.e("FileHelper", "Unexpected error reading asset file: $fileName", e)
null
}
}
}

View File

@ -972,8 +972,7 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
*/
private fun showCommandDialog() {
val commands = arrayOf(
"轻迅协议指令",
"PPTM协议指令"
"轻迅协议指令"
)
androidx.appcompat.app.AlertDialog.Builder(this)
@ -981,7 +980,6 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
.setItems(commands) { _, which ->
when (which) {
0 -> showQingXunProtocolDialog()
1 -> showPptmProtocolDialog()
}
}
.setNegativeButton("取消", null)
@ -995,8 +993,7 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
val commands = arrayOf(
"开启采集",
"停止采集",
"查询设备信息",
"查询电量",
"电刺激开关",
"工频滤波开关"
)
@ -1006,87 +1003,8 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
when (which) {
0 -> sendQingXunStartCollection()
1 -> sendQingXunStopCollection()
2 -> bluetoothManager.queryDeviceInfo()
3 -> bluetoothManager.queryBattery()
4 -> showPowerLineFilterDialog()
}
}
.setNegativeButton("取消", null)
.show()
}
/**
* 显示PPTM协议指令对话框
*/
private fun showPptmProtocolDialog() {
val commands = arrayOf(
"开始采样",
"停止采样",
"自定义PPTM指令"
)
androidx.appcompat.app.AlertDialog.Builder(this)
.setTitle("PPTM协议指令")
.setItems(commands) { _, which ->
when (which) {
0 -> {
bluetoothManager.sendPptmStartSampleCmd()
updateStatus("发送PPTM开始采样指令")
}
1 -> {
bluetoothManager.sendPptmStopSampleCmd()
updateStatus("发送PPTM停止采样指令")
}
2 -> showCustomPptmCommandDialog()
}
}
.setNegativeButton("取消", null)
.show()
}
/**
* 显示自定义PPTM指令对话框
*/
private fun showCustomPptmCommandDialog() {
val input = android.widget.EditText(this)
input.hint = "输入操作码0x0001"
val payloadInput = android.widget.EditText(this)
payloadInput.hint = "输入负载数据十六进制01 00 00 00"
val layout = android.widget.LinearLayout(this).apply {
orientation = android.widget.LinearLayout.VERTICAL
setPadding(50, 20, 50, 20)
addView(input)
addView(payloadInput)
}
androidx.appcompat.app.AlertDialog.Builder(this)
.setTitle("自定义PPTM指令")
.setView(layout)
.setPositiveButton("发送") { _, _ ->
val opcodeStr = input.text.toString().trim()
val payloadStr = payloadInput.text.toString().trim()
if (opcodeStr.isNotEmpty()) {
try {
val opcode = if (opcodeStr.startsWith("0x")) {
opcodeStr.substring(2).toShort(16)
} else {
opcodeStr.toShort()
}
val payload = if (payloadStr.isNotEmpty()) {
payloadStr.split(" ").map { it.toInt(16).toByte() }.toByteArray()
} else {
ByteArray(0)
}
bluetoothManager.sendCustomPptmCmd(opcode, payload)
updateStatus("发送自定义PPTM指令: 操作码=0x%04X".format(opcode))
} catch (e: Exception) {
updateStatus("PPTM指令格式错误: ${e.message}")
}
2 -> showStimSwitchDialog()
3 -> showPowerLineFilterDialog()
}
}
.setNegativeButton("取消", null)
@ -1102,9 +1020,7 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
*/
private fun sendQingXunStartCollection() {
val options = arrayOf(
"立即开启",
"延迟开启5秒后",
"指定时间戳开启"
"立即开启"
)
androidx.appcompat.app.AlertDialog.Builder(this)
@ -1115,17 +1031,6 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
bluetoothManager.startCollection(0L) // 立即开启
updateStatus("发送轻迅协议开启采集指令(立即),准备接收数据...")
}
1 -> {
val timestamp = System.currentTimeMillis() + 5000 // 5秒后
bluetoothManager.startCollection(timestamp)
updateStatus("发送轻迅协议开启采集指令5秒后准备接收数据...")
}
2 -> {
showTimestampInputDialog { timestamp ->
bluetoothManager.startCollection(timestamp)
updateStatus("发送轻迅协议开启采集指令(指定时间戳: $timestamp),准备接收数据...")
}
}
}
}
.setNegativeButton("取消", null)
@ -1137,9 +1042,7 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
*/
private fun sendQingXunStopCollection() {
val options = arrayOf(
"立即停止",
"延迟停止5秒后",
"指定时间戳停止"
"立即停止"
)
androidx.appcompat.app.AlertDialog.Builder(this)
@ -1150,17 +1053,6 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
bluetoothManager.stopCollection(0L) // 立即停止
updateStatus("发送轻迅协议停止采集指令(立即)")
}
1 -> {
val timestamp = System.currentTimeMillis() + 5000 // 5秒后
bluetoothManager.stopCollection(timestamp)
updateStatus("发送轻迅协议停止采集指令5秒后")
}
2 -> {
showTimestampInputDialog { timestamp ->
bluetoothManager.stopCollection(timestamp)
updateStatus("发送轻迅协议停止采集指令(指定时间戳: $timestamp")
}
}
}
}
.setNegativeButton("取消", null)
@ -1195,71 +1087,30 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
}
/**
* 显示时间戳输入对话框
* 显示电刺激开关对话框
*/
private fun showTimestampInputDialog(onConfirm: (Long) -> Unit) {
val input = android.widget.EditText(this)
input.hint = "输入时间戳毫秒0表示立即执行"
input.setText("0")
androidx.appcompat.app.AlertDialog.Builder(this)
.setTitle("输入时间戳")
.setView(input)
.setPositiveButton("确定") { _, _ ->
try {
val timestamp = input.text.toString().toLong()
onConfirm(timestamp)
} catch (e: NumberFormatException) {
updateStatus("时间戳格式错误,请输入数字")
}
}
.setNegativeButton("取消", null)
.show()
}
/**
* 显示轻迅协议命令菜单
*/
private fun showQingXunProtocolMenu() {
val options = arrayOf(
"查询设备信息",
"开启采集",
"停止采集",
"查询电量",
"设置采样率",
"设置增益",
"设置导联",
"工频滤波开关",
"查询状态",
"设备复位",
"查看协议统计"
private fun showStimSwitchDialog() {
val stimOptions = arrayOf(
"开启刺激A",
"开启刺激B",
"关闭电刺激"
)
androidx.appcompat.app.AlertDialog.Builder(this)
.setTitle("轻迅协议V1.0.1命令")
.setItems(options) { _, which ->
.setTitle("电刺激开关")
.setItems(stimOptions) { _, which ->
when (which) {
0 -> {
bluetoothManager.queryDeviceInfo()
updateStatus("发送查询设备信息指令")
bluetoothManager.stimSwitch(0, true)
updateStatus("发送电刺激开关指令: 刺激A 开启")
}
1 -> sendQingXunStartCollection()
2 -> sendQingXunStopCollection()
3 -> {
bluetoothManager.queryBattery()
updateStatus("发送查询电量指令")
1 -> {
bluetoothManager.stimSwitch(1, true)
updateStatus("发送电刺激开关指令: 刺激B 开启")
}
4 -> showSampleRateDialog()
5 -> showGainDialog()
6 -> showLeadDialog()
7 -> showPowerLineFilterDialog()
8 -> {
bluetoothManager.queryStatus()
updateStatus("发送查询状态指令")
}
9 -> {
bluetoothManager.resetDevice()
updateStatus("发送设备复位指令")
2 -> {
bluetoothManager.stimSwitch(0, false)
updateStatus("发送电刺激开关指令: 关闭电刺激")
}
}
}
@ -1268,150 +1119,15 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
}
/**
* 显示采样率设置对话框
* 显示调试状态对话框
*/
private fun showSampleRateDialog() {
val options = arrayOf(
"125 Hz",
"250 Hz",
"500 Hz",
"1000 Hz",
"2000 Hz",
"自定义"
)
private fun showDebugStatus() {
val status = bluetoothManager.debugConnectionStatus()
androidx.appcompat.app.AlertDialog.Builder(this)
.setTitle("设置采样率")
.setItems(options) { _, which ->
when (which) {
0 -> bluetoothManager.setSampleRate(125)
1 -> bluetoothManager.setSampleRate(250)
2 -> bluetoothManager.setSampleRate(500)
3 -> bluetoothManager.setSampleRate(1000)
4 -> bluetoothManager.setSampleRate(2000)
5 -> showCustomSampleRateDialog()
}
updateStatus("发送设置采样率指令")
}
.setNegativeButton("取消", null)
.show()
}
/**
* 显示自定义采样率对话框
*/
private fun showCustomSampleRateDialog() {
val input = android.widget.EditText(this)
input.hint = "输入采样率 (Hz)"
input.setText("500")
androidx.appcompat.app.AlertDialog.Builder(this)
.setTitle("自定义采样率")
.setView(input)
.setPositiveButton("确定") { _, _ ->
try {
val sampleRate = input.text.toString().toInt()
if (sampleRate > 0 && sampleRate <= 10000) {
bluetoothManager.setSampleRate(sampleRate)
updateStatus("发送设置采样率指令: ${sampleRate} Hz")
} else {
updateStatus("采样率范围错误请输入1-10000之间的数字")
}
} catch (e: NumberFormatException) {
updateStatus("采样率格式错误,请输入数字")
}
}
.setNegativeButton("取消", null)
.show()
}
/**
* 显示增益设置对话框
*/
private fun showGainDialog() {
val options = arrayOf(
"1x",
"2x",
"4x",
"8x",
"16x",
"32x",
"自定义"
)
androidx.appcompat.app.AlertDialog.Builder(this)
.setTitle("设置增益")
.setItems(options) { _, which ->
when (which) {
0 -> bluetoothManager.setGain(1)
1 -> bluetoothManager.setGain(2)
2 -> bluetoothManager.setGain(4)
3 -> bluetoothManager.setGain(8)
4 -> bluetoothManager.setGain(16)
5 -> bluetoothManager.setGain(32)
6 -> showCustomGainDialog()
}
updateStatus("发送设置增益指令")
}
.setNegativeButton("取消", null)
.show()
}
/**
* 显示自定义增益对话框
*/
private fun showCustomGainDialog() {
val input = android.widget.EditText(this)
input.hint = "输入增益倍数"
input.setText("1")
androidx.appcompat.app.AlertDialog.Builder(this)
.setTitle("自定义增益")
.setView(input)
.setPositiveButton("确定") { _, _ ->
try {
val gain = input.text.toString().toInt()
if (gain > 0 && gain <= 100) {
bluetoothManager.setGain(gain)
updateStatus("发送设置增益指令: ${gain}x")
} else {
updateStatus("增益范围错误请输入1-100之间的数字")
}
} catch (e: NumberFormatException) {
updateStatus("增益格式错误,请输入数字")
}
}
.setNegativeButton("取消", null)
.show()
}
/**
* 显示导联设置对话框
*/
private fun showLeadDialog() {
val options = arrayOf(
"I导联",
"II导联",
"III导联",
"aVR导联",
"aVL导联",
"aVF导联",
"V1导联",
"V2导联",
"V3导联",
"V4导联",
"V5导联",
"V6导联"
)
androidx.appcompat.app.AlertDialog.Builder(this)
.setTitle("设置导联")
.setItems(options) { _, which ->
val lead = which + 1 // 导联编号从1开始
bluetoothManager.setLead(lead)
updateStatus("发送设置导联指令: ${options[which]}")
}
.setNegativeButton("取消", null)
.setTitle("协议统计信息")
.setMessage(status)
.setPositiveButton("确定", null)
.show()
}
@ -1426,16 +1142,6 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
updateStatus("已清空所有数据")
}
/**
* 测试图表显示功能
*/
/**
* 显示调试状态对话框
*/
private fun startDataMonitoring() {
if (!isDataMonitoringEnabled) {
isDataMonitoringEnabled = true
@ -1572,18 +1278,6 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
}.start()
}
/**
* 导出设备信息
*/
/**
* 导出节律视图数据
*/
/**
* 导出波形视图数据
*/
private fun exportWaveformData(data: List<Float>, stats: Map<String, Any>) {
Thread {
try {

View File

@ -1,14 +0,0 @@
package com.example.cmake_project_test
import type.SensorData
/**
* 原生方法回调接口
* 用于DataManager调用MainActivity中的原生方法
*/
interface NativeMethodCallback {
fun createStreamParser(): Long
fun destroyStreamParser(handle: Long)
fun streamParserAppend(handle: Long, chunk: ByteArray)
fun streamParserDrainPackets(handle: Long): List<SensorData>?
}

View File

@ -1,157 +0,0 @@
package com.example.cmake_project_test
import java.nio.ByteBuffer
import java.nio.ByteOrder
/**
* PPTM设备命令编码器
* 负责编码PPTM设备的命令数据包包含CRC16校验
*/
class PPTMCommandEncoder {
companion object {
private const val TAG = "PPTMCommandEncoder"
private const val MAX_PACKET_SIZE = 244
// PPTM协议操作码
const val OPCODE_START_SAMPLE = 0x0001
const val OPCODE_STOP_SAMPLE = 0x0001
// PPTM协议常量
const val COLLECTION_ON = 0x01
const val COLLECTION_OFF = 0x00
}
/**
* CRC-16-CCITT-FALSE校验
* @param source 数据源
* @param offset 偏移
* @param length 长度
* @return CRC校验值
*/
fun crc16(source: ByteArray, offset: Int, length: Int): Int {
var wCRCin = 0xFFFF
val wCPoly = 0x1021
for (i in offset until offset + length) {
for (j in 0..7) {
val bit = ((source[i].toInt() shr (7 - j)) and 1) == 1
val c15 = ((wCRCin shr 15) and 1) == 1
wCRCin = wCRCin shl 1
if (c15 xor bit) {
wCRCin = wCRCin xor wCPoly
}
}
}
return wCRCin and 0xFFFF
}
/**
* 编码命令数据
* @param opcode 操作码
* @param payload 负载数据
* @param payloadLen 负载长度
* @return 编码后的完整数据包
*/
fun encode(opcode: Short, payload: ByteArray, payloadLen: Short): ByteArray {
val buf = ByteBuffer.allocate(MAX_PACKET_SIZE)
buf.order(ByteOrder.LITTLE_ENDIAN)
// 写入操作码
buf.putShort(opcode)
// 写入负载长度
buf.putShort(payloadLen)
// 写入负载数据
buf.put(payload)
// 计算CRC16校验
val calcCrcDataLen = buf.position()
val crc = crc16(buf.array(), 0, calcCrcDataLen)
buf.putShort(crc.toShort())
// 获取完整数据包
val allDataLen = buf.position()
val dst = ByteArray(allDataLen)
buf.flip()
buf.get(dst, 0, allDataLen)
return dst
}
/**
* 创建PPTM开始采样命令
* @return 编码后的命令
*/
fun createPptmStartSampleCmd(): ByteArray {
val payload = byteArrayOf(
COLLECTION_ON.toByte(), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
)
return encode(OPCODE_START_SAMPLE.toShort(), payload, payload.size.toShort())
}
/**
* 创建PPTM停止采样命令
* @return 编码后的命令
*/
fun createPptmStopSampleCmd(): ByteArray {
val payload = byteArrayOf(
COLLECTION_OFF.toByte(), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
)
return encode(OPCODE_STOP_SAMPLE.toShort(), payload, payload.size.toShort())
}
/**
* 创建自定义PPTM命令
* @param opcode 操作码
* @param payload 负载数据
* @return 编码后的命令
*/
fun createCustomPptmCmd(opcode: Short, payload: ByteArray): ByteArray {
return encode(opcode, payload, payload.size.toShort())
}
/**
* 验证数据包CRC16校验
* @param packet 完整数据包
* @return 校验是否通过
*/
fun verifyCrc16(packet: ByteArray): Boolean {
if (packet.size < 6) return false // 至少需要操作码(2) + 长度(2) + CRC(2)
val dataLen = packet.size - 2 // 减去CRC16的2字节
val calculatedCrc = crc16(packet, 0, dataLen)
val packetCrc = ((packet[packet.size - 1].toInt() and 0xFF) shl 8) or
(packet[packet.size - 2].toInt() and 0xFF)
return calculatedCrc == packetCrc
}
/**
* 解析PPTM数据包
* @param packet 完整数据包
* @return 解析结果 (操作码, 负载数据) null
*/
fun parsePacket(packet: ByteArray): Pair<Short, ByteArray>? {
if (!verifyCrc16(packet)) {
return null
}
if (packet.size < 6) return null
val buf = ByteBuffer.wrap(packet)
buf.order(ByteOrder.LITTLE_ENDIAN)
val opcode = buf.short
val payloadLen = buf.short
if (packet.size < 4 + payloadLen + 2) return null // 操作码(2) + 长度(2) + 负载 + CRC(2)
val payload = ByteArray(payloadLen.toInt())
buf.get(payload)
return Pair(opcode, payload)
}
}

View File

@ -1,484 +0,0 @@
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

@ -1,206 +0,0 @@
package com.example.cmake_project_test
import android.util.Log
import type.SensorData
/**
* 信号处理使用示例
* 展示如何在现有流式数据读取代码结构中使用信号处理功能
*/
class SignalProcessingExample(private val dataManager: DataManager) {
companion object {
private const val TAG = "SignalProcessingExample"
}
/**
* 示例1实时信号质量监控
* 在流式数据处理过程中实时监控信号质量
*/
fun demonstrateRealTimeQualityMonitoring() {
Log.d(TAG, "=== 开始实时信号质量监控演示 ===")
val packets = dataManager.getPacketBuffer()
if (packets.isEmpty()) {
Log.w(TAG, "没有数据包可供监控")
return
}
// 监控前10个数据包的质量
val packetsToMonitor = packets.take(10)
var totalQuality = 0.0f
var validPackets = 0
for ((index, packet) in packetsToMonitor.withIndex()) {
val quality = dataManager.calculateSignalQuality(packet)
if (quality > 0) {
totalQuality += quality
validPackets++
Log.d(TAG, "数据包 $index (${packet.dataType}): 质量 = $quality")
}
}
if (validPackets > 0) {
val averageQuality = totalQuality / validPackets
Log.d(TAG, "平均信号质量: $averageQuality")
// 根据质量决定是否需要调整滤波器参数
if (averageQuality < 0.5f) {
Log.w(TAG, "信号质量较低,建议检查传感器连接或调整滤波器参数")
} else if (averageQuality > 0.8f) {
Log.i(TAG, "信号质量良好")
}
}
}
/**
* 示例2按设备类型应用不同的信号处理策略
*/
fun demonstrateDeviceSpecificProcessing() {
Log.d(TAG, "=== 开始设备特定信号处理演示 ===")
val packets = dataManager.getPacketBuffer()
if (packets.isEmpty()) {
Log.w(TAG, "没有数据包可供处理")
return
}
// 按设备类型分组
val packetsByType = packets.groupBy { it.dataType }
for ((dataType, typePackets) in packetsByType) {
Log.d(TAG, "处理 ${dataType.name} 类型数据,共 ${typePackets.size} 个数据包")
when (dataType) {
SensorData.DataType.EEG -> {
Log.d(TAG, "应用EEG专用滤波器带通滤波(1-40Hz) + 幅度归一化")
}
SensorData.DataType.ECG_2LEAD, SensorData.DataType.ECG_12LEAD -> {
Log.d(TAG, "应用ECG专用滤波器高通(0.5Hz) + 低通(40Hz) + 陷波(50Hz)")
}
SensorData.DataType.PPG -> {
Log.d(TAG, "应用PPG专用滤波器低通(8Hz) + 运动伪影去除")
}
else -> {
Log.d(TAG, "数据类型 ${dataType.name} 暂不支持专用处理")
}
}
}
}
/**
* 示例3批量信号处理
* 对大量数据进行批量信号处理
*/
fun demonstrateBatchProcessing() {
Log.d(TAG, "=== 开始批量信号处理演示 ===")
val packets = dataManager.getPacketBuffer()
if (packets.isEmpty()) {
Log.w(TAG, "没有数据包可供批量处理")
return
}
Log.d(TAG, "开始批量处理 ${packets.size} 个数据包...")
// 记录处理开始时间
val startTime = System.currentTimeMillis()
// 应用信号处理
val processedPackets = dataManager.applySignalProcessing(packets)
// 记录处理结束时间
val endTime = System.currentTimeMillis()
val processingTime = endTime - startTime
Log.d(TAG, "批量处理完成!")
Log.d(TAG, "处理时间: ${processingTime}ms")
Log.d(TAG, "处理数据包: ${processedPackets.size}")
Log.d(TAG, "平均处理速度: ${packets.size * 1000.0 / processingTime} 包/秒")
// 显示处理后的信号质量统计
if (processedPackets.isNotEmpty()) {
val qualityScores = mutableListOf<Float>()
for (packet in processedPackets.take(20)) { // 只检查前20个包
val quality = dataManager.calculateSignalQuality(packet)
if (quality > 0) {
qualityScores.add(quality)
}
}
if (qualityScores.isNotEmpty()) {
val avgQuality = qualityScores.average()
val maxQuality = qualityScores.maxOrNull() ?: 0f
val minQuality = qualityScores.minOrNull() ?: 0f
Log.d(TAG, "处理后信号质量统计:")
Log.d(TAG, " 平均质量: ${String.format("%.3f", avgQuality)}")
Log.d(TAG, " 最高质量: ${String.format("%.3f", maxQuality)}")
Log.d(TAG, " 最低质量: ${String.format("%.3f", minQuality)}")
}
}
}
/**
* 示例4自适应滤波器参数调整
* 根据信号质量自动调整滤波器参数
*/
fun demonstrateAdaptiveFilterAdjustment() {
Log.d(TAG, "=== 开始自适应滤波器参数调整演示 ===")
val packets = dataManager.getPacketBuffer()
if (packets.isEmpty()) {
Log.w(TAG, "没有数据包可供自适应处理")
return
}
// 获取第一个数据包进行质量评估
val firstPacket = packets.first()
val initialQuality = dataManager.calculateSignalQuality(firstPacket)
Log.d(TAG, "初始信号质量: $initialQuality")
if (initialQuality < 0.3f) {
Log.w(TAG, "信号质量过低,建议检查硬件连接")
} else if (initialQuality < 0.6f) {
Log.i(TAG, "信号质量一般,可以尝试调整滤波器参数")
// 这里可以添加自适应参数调整逻辑
// 例如:根据噪声水平调整截止频率
when (firstPacket.dataType) {
SensorData.DataType.EEG -> {
Log.d(TAG, "建议调整EEG滤波器降低高通截止频率到0.5Hz提高低通截止频率到50Hz")
}
SensorData.DataType.ECG_2LEAD, SensorData.DataType.ECG_12LEAD -> {
Log.d(TAG, "建议调整ECG滤波器提高高通截止频率到1Hz降低低通截止频率到30Hz")
}
SensorData.DataType.PPG -> {
Log.d(TAG, "建议调整PPG滤波器降低低通截止频率到6Hz")
}
else -> {
Log.d(TAG, "该数据类型暂不支持自适应调整")
}
}
} else {
Log.i(TAG, "信号质量良好,当前滤波器参数合适")
}
}
/**
* 运行所有演示
*/
fun runAllDemonstrations() {
Log.d(TAG, "开始运行所有信号处理演示...")
try {
demonstrateRealTimeQualityMonitoring()
demonstrateDeviceSpecificProcessing()
demonstrateBatchProcessing()
demonstrateAdaptiveFilterAdjustment()
Log.d(TAG, "所有演示完成!")
} catch (e: Exception) {
Log.e(TAG, "演示过程中发生错误", e)
}
}
}

View File

@ -1,271 +0,0 @@
package com.example.cmake_project_test
import android.util.Log
import kotlin.math.PI
import kotlin.math.sin
/**
* 信号处理使用示例类
* 展示如何使用JNI信号处理功能
*/
class SignalProcessorExample {
private val signalProcessor = SignalProcessorJNI()
private val sampleRate = 1000.0 // 1kHz采样率
init {
// 初始化信号处理器
if (!signalProcessor.createProcessor()) {
Log.e("SignalProcessorExample", "Failed to create signal processor")
}
}
/**
* 生成测试信号正弦波 + 噪声
*/
fun generateTestSignal(frequency: Double, duration: Double, noiseLevel: Double = 0.1): FloatArray {
val numSamples = (sampleRate * duration).toInt()
val signal = FloatArray(numSamples)
for (i in 0 until numSamples) {
val time = i / sampleRate
val sineWave = sin(2 * PI * frequency * time).toFloat()
val noise = (Math.random() * 2 - 1).toFloat() * noiseLevel
signal[i] = (sineWave + noise).toFloat()
}
return signal
}
/**
* 演示带通滤波
*/
fun demonstrateBandpassFilter() {
Log.d("SignalProcessorExample", "=== 带通滤波演示 ===")
// 生成包含多个频率的测试信号
val signal = generateTestSignal(50.0, 1.0, 0.2) // 50Hz + 噪声
// 应用带通滤波 (40-60Hz)
val filteredSignal = signalProcessor.bandpassFilter(signal, sampleRate, 40.0, 60.0)
if (filteredSignal != null) {
Log.d("SignalProcessorExample", "滤波成功!原始信号长度: ${signal.size}, 滤波后长度: ${filteredSignal.size}")
// 计算信号质量
val originalQuality = signalProcessor.calculateSignalQuality(signal)
val filteredQuality = signalProcessor.calculateSignalQuality(filteredSignal)
Log.d("SignalProcessorExample", "原始信号质量: $originalQuality")
Log.d("SignalProcessorExample", "滤波后信号质量: $filteredQuality")
} else {
Log.e("SignalProcessorExample", "带通滤波失败")
}
}
/**
* 演示低通滤波
*/
fun demonstrateLowpassFilter() {
Log.d("SignalProcessorExample", "=== 低通滤波演示 ===")
// 生成高频信号
val signal = generateTestSignal(200.0, 1.0, 0.1)
// 应用低通滤波 (100Hz截止)
val filteredSignal = signalProcessor.lowpassFilter(signal, sampleRate, 100.0)
if (filteredSignal != null) {
Log.d("SignalProcessorExample", "低通滤波成功!")
// 计算信号质量
val originalQuality = signalProcessor.calculateSignalQuality(signal)
val filteredQuality = signalProcessor.calculateSignalQuality(filteredSignal)
Log.d("SignalProcessorExample", "原始信号质量: $originalQuality")
Log.d("SignalProcessorExample", "滤波后信号质量: $filteredQuality")
} else {
Log.e("SignalProcessorExample", "低通滤波失败")
}
}
/**
* 演示高通滤波
*/
fun demonstrateHighpassFilter() {
Log.d("SignalProcessorExample", "=== 高通滤波演示 ===")
// 生成包含低频和高频的信号
val signal = generateTestSignal(10.0, 1.0, 0.1) // 10Hz低频信号
// 应用高通滤波 (50Hz截止)
val filteredSignal = signalProcessor.highpassFilter(signal, sampleRate, 50.0)
if (filteredSignal != null) {
Log.d("SignalProcessorExample", "高通滤波成功!")
// 计算信号质量
val originalQuality = signalProcessor.calculateSignalQuality(signal)
val filteredQuality = signalProcessor.calculateSignalQuality(filteredSignal)
Log.d("SignalProcessorExample", "原始信号质量: $originalQuality")
Log.d("SignalProcessorExample", "滤波后信号质量: $filteredQuality")
} else {
Log.e("SignalProcessorExample", "高通滤波失败")
}
}
/**
* 演示陷波滤波去除工频干扰
*/
fun demonstrateNotchFilter() {
Log.d("SignalProcessorExample", "=== 陷波滤波演示 ===")
// 生成包含工频干扰的信号
val signal = generateTestSignal(50.0, 1.0, 0.3) // 50Hz工频 + 噪声
// 应用陷波滤波 (50Hz陷波)
val filteredSignal = signalProcessor.notchFilter(signal, sampleRate, 50.0, 30.0)
if (filteredSignal != null) {
Log.d("SignalProcessorExample", "陷波滤波成功!")
// 计算信号质量
val originalQuality = signalProcessor.calculateSignalQuality(signal)
val filteredQuality = signalProcessor.calculateSignalQuality(filteredSignal)
Log.d("SignalProcessorExample", "原始信号质量: $originalQuality")
Log.d("SignalProcessorExample", "滤波后信号质量: $filteredQuality")
} else {
Log.e("SignalProcessorExample", "陷波滤波失败")
}
}
/**
* 演示ECG信号质量评估
*/
fun demonstrateECGSQI() {
Log.d("SignalProcessorExample", "=== ECG信号质量评估演示 ===")
// 生成模拟ECG信号
val ecgSignal = generateTestSignal(1.0, 2.0, 0.05) // 1Hz心跳 + 低噪声
// 计算ECG信号质量指数
val sqi = signalProcessor.calculateECGSQI(ecgSignal, sampleRate)
Log.d("SignalProcessorExample", "ECG信号质量指数: $sqi")
if (sqi > 0.7f) {
Log.d("SignalProcessorExample", "ECG信号质量良好")
} else if (sqi > 0.4f) {
Log.d("SignalProcessorExample", "ECG信号质量一般")
} else {
Log.d("SignalProcessorExample", "ECG信号质量较差")
}
}
/**
* 演示信号特征提取
*/
fun demonstrateFeatureExtraction() {
Log.d("SignalProcessorExample", "=== 信号特征提取演示 ===")
// 生成测试信号
val signal = generateTestSignal(100.0, 0.5, 0.15)
// 提取特征
val features = signalProcessor.extractFeatures(signal, sampleRate)
if (features != null) {
Log.d("SignalProcessorExample", "特征提取成功!特征数量: ${features.size}")
// 显示前几个特征值
for (i in 0 until minOf(5, features.size)) {
Log.d("SignalProcessorExample", "特征 $i: ${features[i]}")
}
} else {
Log.e("SignalProcessorExample", "特征提取失败")
}
}
/**
* 演示信号归一化
*/
fun demonstrateNormalization() {
Log.d("SignalProcessorExample", "=== 信号归一化演示 ===")
// 生成幅度较大的信号
val signal = generateTestSignal(75.0, 0.5, 0.2)
// 找到最大绝对值
val maxAbs = signal.maxOfOrNull { kotlin.math.abs(it) } ?: 0f
Log.d("SignalProcessorExample", "归一化前最大绝对值: $maxAbs")
// 归一化
signalProcessor.normalizeAmplitude(signal)
// 检查归一化结果
val normalizedMaxAbs = signal.maxOfOrNull { kotlin.math.abs(it) } ?: 0f
Log.d("SignalProcessorExample", "归一化后最大绝对值: $normalizedMaxAbs")
if (kotlin.math.abs(normalizedMaxAbs - 1.0f) < 0.01f) {
Log.d("SignalProcessorExample", "归一化成功!")
} else {
Log.e("SignalProcessorExample", "归一化失败")
}
}
/**
* 演示相关性计算
*/
fun demonstrateCorrelation() {
Log.d("SignalProcessorExample", "=== 相关性计算演示 ===")
// 生成两个相关信号
val signal1 = generateTestSignal(50.0, 1.0, 0.1)
val signal2 = generateTestSignal(50.0, 1.0, 0.1) // 相同频率
// 计算相关性
val correlation = signalProcessor.calculateCorrelation(signal1, signal2)
Log.d("SignalProcessorExample", "信号相关性: $correlation")
if (correlation > 0.8f) {
Log.d("SignalProcessorExample", "信号高度相关")
} else if (correlation > 0.5f) {
Log.d("SignalProcessorExample", "信号中等相关")
} else {
Log.d("SignalProcessorExample", "信号相关性较低")
}
}
/**
* 运行所有演示
*/
fun runAllDemonstrations() {
Log.d("SignalProcessorExample", "开始运行所有信号处理演示...")
try {
demonstrateBandpassFilter()
demonstrateLowpassFilter()
demonstrateHighpassFilter()
demonstrateNotchFilter()
demonstrateECGSQI()
demonstrateFeatureExtraction()
demonstrateNormalization()
demonstrateCorrelation()
Log.d("SignalProcessorExample", "所有演示完成!")
} catch (e: Exception) {
Log.e("SignalProcessorExample", "演示过程中发生错误", e)
}
}
/**
* 清理资源
*/
fun cleanup() {
signalProcessor.destroyProcessor()
Log.d("SignalProcessorExample", "信号处理器已销毁")
}
}

View File

@ -10,6 +10,77 @@ import java.util.concurrent.atomic.AtomicBoolean
*/
class UiManager {
companion object {
// UI更新相关
const val UPDATE_INTERVAL = 200L // 优化每200毫秒更新一次UI减慢更新速度以便观察心电波形
// 显示限制相关
const val MAX_DISPLAY_PACKETS = 5 // 最大显示数据包数量
const val MAX_DETAIL_PACKETS = 3 // 最大详情数据包数量
const val MAX_DISPLAY_CHANNELS = 4 // 最大显示通道数量
const val MAX_12LEAD_CHANNELS = 6 // 12导联心电最大通道数量
const val MAX_DISPLAY_SAMPLES = 750 // 最大显示采样点数量调整为显示约3秒数据基于250Hz采样率
// 设备类型名称映射
fun getDeviceName(dataType: SensorData.DataType): String {
return when (dataType) {
SensorData.DataType.EEG -> "脑电设备"
SensorData.DataType.ECG_2LEAD -> "胸腹设备"
SensorData.DataType.PPG -> "血氧设备"
SensorData.DataType.ECG_12LEAD -> "12导联心电"
SensorData.DataType.STETHOSCOPE -> "数字听诊"
SensorData.DataType.SNORE -> "鼾声设备"
SensorData.DataType.RESPIRATION -> "呼吸/姿态"
SensorData.DataType.MIT_BIH -> "MIT-BIH"
else -> "未知设备"
}
}
// 构建通道数据详情
fun buildChannelDetails(
data: List<SensorData>,
maxPackets: Int = MAX_DETAIL_PACKETS,
maxChannels: Int = MAX_DISPLAY_CHANNELS,
maxSamples: Int = MAX_DISPLAY_SAMPLES
): String {
if (data.isEmpty()) {
return "无通道数据"
}
val details = mutableListOf<String>()
data.take(maxPackets).forEachIndexed { packetIndex, sensorData ->
if (sensorData.channelData.isNullOrEmpty()) {
details.add("数据包 ${packetIndex + 1}: 无通道数据")
return@forEachIndexed
}
details.add("数据包 ${packetIndex + 1} (${getDeviceName(sensorData.dataType ?: SensorData.DataType.EEG)}):")
sensorData.channelData.take(maxChannels).forEachIndexed { channelIndex, channel ->
if (channel.isNullOrEmpty()) {
details.add(" 通道 ${channelIndex + 1}: 空数据")
return@forEachIndexed
}
val sampleCount = minOf(maxSamples, channel.size)
val channelDataStr = channel.take(sampleCount).joinToString(", ") { "%.1f".format(it) }
details.add(" 通道 ${channelIndex + 1}: ${sampleCount}/${channel.size} 采样点")
details.add(" 示例: $channelDataStr${if (channel.size > sampleCount) "..." else ""}")
}
if (sensorData.channelData.size > maxChannels) {
details.add(" ... 还有 ${sensorData.channelData.size - maxChannels} 个通道")
}
details.add("")
}
if (data.size > maxPackets) {
details.add("... 还有 ${data.size - maxPackets} 个数据包")
}
return details.joinToString("\n")
}
}
private val uiUpdatePending = AtomicBoolean(false)
private var lastUpdateTime = 0L
@ -21,7 +92,7 @@ class UiManager {
updateCallback: () -> Unit
) {
val currentTime = System.currentTimeMillis()
if (currentTime - lastUpdateTime >= Constants.UPDATE_INTERVAL &&
if (currentTime - lastUpdateTime >= UPDATE_INTERVAL &&
uiUpdatePending.compareAndSet(false, true)) {
lastUpdateTime = currentTime
updateCallback()
@ -45,7 +116,7 @@ class UiManager {
if (packetBuffer.isNotEmpty()) {
// 获取设备类型信息
val deviceTypes = packetBuffer.mapNotNull { it.dataType }.distinct()
val deviceInfo = deviceTypes.joinToString(", ") { DeviceTypeHelper.getDeviceName(it) }
val deviceInfo = deviceTypes.joinToString(", ") { getDeviceName(it) }
append("设备类型: $deviceInfo\n")
} else {
append("设备类型: 无\n")
@ -82,22 +153,22 @@ class UiManager {
val metricsInfo = buildMetricsInfo(calculatedMetrics)
// 只显示最新的一些数据包详情
val recentPackets = packetBuffer.takeLast(Constants.MAX_DISPLAY_PACKETS)
val recentPackets = packetBuffer.takeLast(MAX_DISPLAY_PACKETS)
val has12Lead = recentPackets.any { it.dataType == SensorData.DataType.ECG_12LEAD }
val channelDetails = if (has12Lead) {
DeviceTypeHelper.buildChannelDetails(
buildChannelDetails(
recentPackets,
maxPackets = Constants.MAX_DETAIL_PACKETS,
maxChannels = Constants.MAX_12LEAD_CHANNELS,
maxSamples = Constants.MAX_DISPLAY_SAMPLES
maxPackets = MAX_DETAIL_PACKETS,
maxChannels = MAX_12LEAD_CHANNELS,
maxSamples = MAX_DISPLAY_SAMPLES
)
} else {
DeviceTypeHelper.buildChannelDetails(
buildChannelDetails(
recentPackets,
maxPackets = Constants.MAX_DETAIL_PACKETS,
maxChannels = Constants.MAX_DISPLAY_CHANNELS,
maxSamples = Constants.MAX_DISPLAY_SAMPLES
maxPackets = MAX_DETAIL_PACKETS,
maxChannels = MAX_DISPLAY_CHANNELS,
maxSamples = MAX_DISPLAY_SAMPLES
)
}
@ -133,7 +204,7 @@ class UiManager {
val latestPacket = processedPackets.lastOrNull()
if (latestPacket != null) {
append("\n最新处理数据包:\n")
append("- 数据类型: ${DeviceTypeHelper.getDeviceName(latestPacket.getDataType())}\n")
append("- 数据类型: ${getDeviceName(latestPacket.getDataType())}\n")
append("- 时间戳: ${latestPacket.getTimestamp()}\n")
append("- 包序号: ${latestPacket.getPacketSn()}\n")

View File

@ -0,0 +1,418 @@
轻迅蓝牙通信协议
V1.0.0
目录
1. 范围...................................................................................................................................................3
2. 蓝牙接口定义 ................................................................................................................................ 3
2.1 服务接口 .......................................................................................................................................3
2.2 广播接口 .......................................................................................................................................4
3. 帧格式说明.....................................................................................................................................5
3.1 帧数据包结构 ............................................................................................................................ 5
3.2 功能码定义................................................................................................................................. 6
3.3 功能码描述................................................................................................................................. 6
3.4 数据标识定义 .......................................................................................................................... 10
4. 通信逻辑说明 ..............................................................................................................................13
4.1 丢包处理 ................................................................................................................................... 13
4.2 压缩............................................................................................................................................ 13
4.2 空中升级 ................................................................................................................................... 13
1. 范围
本协议文档用于蓝牙主设备终端与轻迅采集设备之间蓝牙数据通信。
2. 蓝牙接口定义
2.1 服务接口
2.1.1 自定义服务
2.1.1.1 数据服务
采用自定义 128bit UUID
UUID: 6e400001-b5a3-f393-e0a9-68716563686f
包含 Write Characteristic 和 Notify Characteristic 两个子接口
(1) RXWrite Characteristic(提供 BLE 主设备 向 BLE 从设备发送消息或取相应消息)
UUID6e400002-b5a3-f393-e0a9-68716563686f
属性Write/WriteNoRSP
数据类型:字节
数据长度244每包最大可传输数据
(2) TXNotify Characteristic(提供给 BLE 从设备向 BLE 主设备 发送通知信息)
UUID 6e400003-b5a3-f393-e0a9-68716563686f
属性: Notify
数据类型:字节
数据长度244每包最大可传输数据
2.1.1.1 空中升级服务
采用 Nordic DFU 功能实现
Service UUID: 0xFE59
2.1.2 标准服务
2.1.2.1 设备信息服务
采用 Bluetooth SIG 标准规定 Device Information Service
Service UUID0x180A
包含设备编号,软件和硬件版本等信息的特征值
具体对应特征值 UUID 和属性可查看标准文档
2.1.2.2 电量服务
采用 Bluetooth SIG 标准规定 Battery Service
Service UUID0x180F
对应特征值 UUID 和属性可查看标准文档
2.2 广播接口
2.2.1 广播数据数据格式介绍
2.2.2 广播包结构
广播数据段描述 类型 说明
设备 LE 物理连接标识 0x01 长度0x02
类型0x01
数据0x06
设备名称 0x09 长度0x07
类型0x09
数据48 51 5F 42 45 45
设备名称视具体设备而定
广播内容: 02 01 06 07 09 48 51 5F 42 45 45
2.2.3 扫描响应包结构
广播数据段描述 类型 说明
厂商自定义数据 0xFF
长度0x1A
类型0xFF
数据:
Company ID2 字节
Protocol Version1 字节
Device Code2 字节
MAC Address6 字节
广播内容0C FF 58 51 01 02 01 76 D8 57 F7 68 C2
Company ID 公司 ID 固定值为 0x5158
Protocol Version 用于指示当前设备使用的协议版本 0x01
Device Code 包含产品类型Device Type高 8 位与子类型Sub Device Type低 8
位)
Device Type 产品类型与 Sub Device Type 产品子类型定义如下表:
产品类型 类型 ID 产品子类型 子类型 ID
多导睡眠设备 0x42 胸腹模块 0x10
腕部模块 0x20
额头模块 0x30
腿部模块 0x40
MAC Address 声明当前设备 mac 地址,用于兼容 IOS 设备无法获取 BLE mac 地址问题
3. 帧格式说明
3.1 帧数据包结构
序号 长度 字段 说明
1 2 功能码
2 2 数据长度 len
3~(2+len) len 数据
3+len 2 MIC 校验
4+len
功能码:操作命令码。
数据长度:本次传送数据的长度。
MIC 校验:采用 CRC-16-CCITT-FALSE 校验,校验内容包括功能码、数据长度、数据 3
个部分
说明:除特殊说明外,协议默认为小端格式。
3.2 功能码定义
APP->BLE
序号 功能码 功能 备注
1 0x0000 查询设备信息 获取当前设备设置参数
2 0x0001 采集使能开关 0 或 1指定时间点
3 0x0002 电量查询 查询设备电量
4 0x0003 电刺激开关 开启关闭不同类型的电刺激
5 0x000A 工频滤波开关 开启关闭工频滤波算法
6 0x0080 同步时间戳 64bit 毫秒时间戳
BLE->APP
序号 功能码 功能 备注
1 0x8000 数据上传 上报采集数据
2 0x8001 设备状态上报 异常状态上报
3 0x8002 电量上报 设备主动上报电量
3.3 功能码描述
3.3.1 设备信息
功能码 0x0000
主->从:
长度 字段 说明
2 0x0000 功能码
2 0x0000 数据长度
2 CRC16 检验
从->主:
长度 字段 说明
2 0x0000 功能码
2 0x0001 数据长度
1 见下表格式
Data
2 检验
CRC16
Data 格式:
Byte Bit 状态位
采集使能
0 0
1~7 保留
3.3.2 采集使能
功能码 0x0001
主->从:
长度 字段 说明
2 0x0001 功能码
2 0x0009 数据长度
1 0或1 采集开关
8 Timestamp 指定时间戳
2 CRC16 检验
从->主:
长度 字段 说明
2 0x0001 功能码
2 0x0001 数据长度
采集开启状
1 0或1
2 CRC16 检验
Timestamp 为开启或关闭采集操作的指定时间戳(毫秒级),如果为 0 则代表立即执行操作。
可用于多设备同步开启采集。
3.3.3 电量查询
功能码 0x0002
主->从: 长度 字段 说明
从->主: 2 0x0002 功能码
2 0x0000 数据长度
2 CRC16 检验
长度 字段 说明
2 0x0002 功能码
2 0x0001 数据长度
1 percent 电量百分比
2 CRC16 检验
3.3.4 电刺激开关
功能码0x0003
主->从: 长度 字段 说明
从->主: 2 0x0003 功能码
2 0x0001 数据长度
开关状态
1 0x00 or 0x10~0x1F 以及电刺
激类型
2 CRC16 检验
长度 字段 说明
2 0x0003 功能码
2 0x0001 数据长度
开关状态
1 0x00 or 0x10~0x1F 以及电刺
激类型
2 CRC16 检验
说明:
0x00 高四位如果为 0000 表示电刺激关闭。
0x10~0x1F 高四位如果为 0001 表示电刺激开启,低四位 0000~1111 表示开启 16 种不
同类型的电刺激。
从机接收到指令后,根据指令通过 SPI 开启或关闭电刺激后,回传数据给主机。
3.3.5 工频滤波开关
功能码 0x000A
主->从:
长度 字段 说明
2 0x000A 功能码
2 0x0001 数据长度
1 0或1 开关状态
2 CRC16 检验
从->主:
长度 字段 说明
2 0x000A 功能码
2 0x0000 数据长度
2 CRC16 检验
用于开启关闭工频滤波。设备默认开启工频滤波,在需要时可以进行临时关闭和开启。设
备重启后,开关状态不保存。
3.3.6 同步时间戳
功能码 0x0080
主->从:
长度 字段 说明
2 0x0080 功能码
2 0x0008 数据长度
8 Timestamp 毫秒时间戳
2 CRC16 检验
从->主:
长度 字段 说明
2 0x0080 功能码
2 0x0000 数据长度
2 CRC16 检验
Timestamp 为毫秒级时间戳。若主设备接入网络,则会提供 unix 时间戳,从设备可以用来
获取当前时间。若没有接入网络,主设备提供的时间戳为开机后的时间,仅能用于操作指
令同步,不能当作实际时间。
3.3.7 数据上报
功能码 0x8000
从->主:
长度 字段 说明
功能码
2 0x8000 数据长度
2 len 上报数据
len Data 检验
2
CRC16
Data 格式
长度 字段 说明
2 SN 包序号
N 数据 1 TLD 格式
M 数据 2 TLD 格式
采集数据采用 TLD 格式,数据按照[Type][Length][Data]的顺序排列组织。每包数据可以包含
多个 TLD 组。
具体 TLD 数据定义参考 3.4 数据标识定义。
3.3.8 状态上报
当前协议暂不处理
3.4 数据标识定义
3.4.1 多导睡眠设备-胸腹模块数据定义
胸腹电信号数据 Length(2 Byte) Data
Type(2 Byte)
0x4211 232 uint8_t loff_state[2];
int16_t ecg1[25] ;
int16_t ecg2[25] ;
int16_t emg1[25] ;
int16_t emg2[25] ;
int16_t br_temperature[5] ;
int16_t br_impedance1[5] ;
int16_t br_impedance2[5] ;
采样率:
口鼻呼吸气流温度100Hz
胸腹呼吸阻抗100Hz
心电500Hz
下颌肌电500Hz
脱落状态: 每个 1294 对应一个字节
胸腹鼾声数据 Length(2 Byte) Data
Type(2 Byte) 232 int8_t snore[232] ;
0x4212
Length(2 Byte) 采样率:
胸腹呼吸气压数据 232 鼾声500Hz
Type(2 Byte)
0x4213 Data
int16_t br_nose_pressure[114] ;
uint16_t movement ;
uint8_t posture ;
uint8_t ambient;
采样率:
鼻呼吸气流气压100Hz
体位体动1Hz
环境光1Hz
3.4.2 多导睡眠设备-腕部模块数据定义
Type(2 Byte) Length(2 Byte) Data
0x4220 232 int16_t ppg_hr[58] ;
int16_t ppg_spo2[58] ;
3.4.3 多导睡眠设备-额头模块数据定义 采样率25Hz
Type(2 Byte) Length(2 Byte) Data
0x4230 232 uint8_t loff_state[2] ;
int16_t eeg[6][14] ;
3.4.4 多导睡眠设备-腿部模块数据定义 int16_t eog[2][14] ;
uint8_t reserve[6] ;
Type(2 Byte) Length(2 Byte)
0x4240 232 采样率500Hz
Data
uint8_t loff_state[2];
int16_t emg[115] ;
采样率500Hz
4. 通信逻辑说明
4.1 丢包处理
当前协议暂不启用丢包处理
4.2 压缩
当前协议暂不启用压缩
4.2 空中升级
空中升级采用 Nordic DFU 方案,具体做法是 App 发送特定数据使设备进入 DFU 状态,之后
App 重连特定 DFU 设备进行升级。具体可以参考 Nordic DFU 方案文档。