This commit is contained in:
parent
d6c870b243
commit
1ce6cbffb3
|
|
@ -1,74 +1,15 @@
|
||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include "../include/cpp/add.h"
|
|
||||||
#include "../include/cpp/data_praser.h"
|
#include "../include/cpp/data_praser.h"
|
||||||
#include "../include/cpp/signal_processor.h"
|
#include "../include/cpp/signal_processor.h"
|
||||||
#include "../include/cpp/data_mapper.h"
|
#include "../include/cpp/data_mapper.h"
|
||||||
#include "../include/cpp/indicator_cal.h"
|
#include "../include/cpp/indicator_cal.h"
|
||||||
|
|
||||||
using core::math::add;
|
|
||||||
|
|
||||||
extern "C" JNIEXPORT jstring JNICALL
|
|
||||||
Java_com_example_cmake_1project_1test_MainActivity_stringFromJNI(
|
|
||||||
JNIEnv* env,
|
|
||||||
jobject /* this */) {
|
|
||||||
std::string hello = "Hello from C++ (multi-file)";
|
|
||||||
return env->NewStringUTF(hello.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" JNIEXPORT jint JNICALL
|
|
||||||
Java_com_example_cmake_1project_1test_MainActivity_addFromJNI(
|
|
||||||
JNIEnv* env,
|
|
||||||
jobject /* this */,
|
|
||||||
jint a,
|
|
||||||
jint b) {
|
|
||||||
(void)env;
|
|
||||||
return static_cast<jint>(add(static_cast<int>(a), static_cast<int>(b)));
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" JNIEXPORT jobject JNICALL
|
|
||||||
Java_com_example_cmake_1project_1test_MainActivity_parseDeviceDataFromJNI(
|
|
||||||
JNIEnv* env,
|
|
||||||
jobject /* this */,
|
|
||||||
jbyteArray fileData) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 将Java的byte数组转换为C++的vector
|
|
||||||
std::vector<uint8_t> cppFileData = convertJavaByteArrayToVector(env, fileData);
|
|
||||||
|
|
||||||
if (cppFileData.empty()) {
|
|
||||||
__android_log_print(ANDROID_LOG_WARN, "DataParser", "Empty file data received");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用C++的解析函数
|
|
||||||
std::vector<SensorData> result = parse_device_data(cppFileData);
|
|
||||||
|
|
||||||
// 将结果转换为Java对象
|
|
||||||
jobject javaResult = convertSensorDataVectorToJavaList(env, result);
|
|
||||||
|
|
||||||
if (javaResult == nullptr) {
|
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "DataParser", "Failed to convert result to Java object");
|
|
||||||
} else {
|
|
||||||
__android_log_print(ANDROID_LOG_INFO, "DataParser", "Successfully parsed %zu data packets", result.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
return javaResult;
|
|
||||||
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "DataParser", "Exception during parsing: %s", e.what());
|
|
||||||
return nullptr;
|
|
||||||
} catch (...) {
|
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "DataParser", "Unknown exception during parsing");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ================= 流式解析 JNI 接口 =================
|
// ================= 流式解析 JNI 接口 =================
|
||||||
|
|
||||||
|
|
@ -436,28 +377,6 @@ Java_com_example_cmake_1project_1test_SignalProcessorJNI_processRealtimeChunk(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 便捷方法:从整块数据按固定块大小模拟流式解析,返回解析到的所有包
|
|
||||||
extern "C" JNIEXPORT jobject JNICALL
|
|
||||||
Java_com_example_cmake_1project_1test_MainActivity_parseStreamFromBytes(
|
|
||||||
JNIEnv* env,
|
|
||||||
jobject /* this */,
|
|
||||||
jbyteArray data,
|
|
||||||
jint chunkSize) {
|
|
||||||
if (data == nullptr || chunkSize <= 0) return nullptr;
|
|
||||||
|
|
||||||
std::vector<uint8_t> src = convertJavaByteArrayToVector(env, data);
|
|
||||||
auto* parser = new StreamParser();
|
|
||||||
size_t offset = 0;
|
|
||||||
while (offset < src.size()) {
|
|
||||||
size_t n = std::min(static_cast<size_t>(chunkSize), src.size() - offset);
|
|
||||||
parser->appendData(src.data() + offset, n);
|
|
||||||
offset += n;
|
|
||||||
}
|
|
||||||
std::vector<SensorData> packets = parser->getAllPackets();
|
|
||||||
delete parser;
|
|
||||||
return convertSensorDataVectorToJavaList(env, packets);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// DataMapper JNI函数实现
|
// DataMapper JNI函数实现
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -512,7 +512,7 @@ float MetricsCalculator::calculate_heart_rate_ppg(const std::vector<float>& ppg_
|
||||||
// 检测脉搏波峰
|
// 检测脉搏波峰
|
||||||
auto pulse_peaks = detect_pulse_peaks(ppg_signal, sample_rate);
|
auto pulse_peaks = detect_pulse_peaks(ppg_signal, sample_rate);
|
||||||
if (pulse_peaks.size() < 2) {
|
if (pulse_peaks.size() < 2) {
|
||||||
std::cerr << "警告: 检测到的脉搏波峰数量不足: " << pulse_peaks.size() << ",至少需要2个" << std::endl;
|
std::cerr << "警告: 检测到的脉搏波峰数量不足: " << pulse_peaks.size() << ",至少需要2个" << std::endl;
|
||||||
return 0.0f;
|
return 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -551,13 +551,13 @@ float MetricsCalculator::calculate_heart_rate_ppg(const std::vector<float>& ppg_
|
||||||
float MetricsCalculator::calculate_spo2(const SensorData& ppg_data) {
|
float MetricsCalculator::calculate_spo2(const SensorData& ppg_data) {
|
||||||
// 输入验证
|
// 输入验证
|
||||||
if (!std::holds_alternative<std::vector<std::vector<float>>>(ppg_data.channel_data)) {
|
if (!std::holds_alternative<std::vector<std::vector<float>>>(ppg_data.channel_data)) {
|
||||||
std::cerr << "警告: PPG数据格式不正确,需要多通道数据" << std::endl;
|
std::cerr << "警告: PPG数据格式不正确,需要多通道数据" << std::endl;
|
||||||
return 0.0f;
|
return 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& channels = std::get<std::vector<std::vector<float>>>(ppg_data.channel_data);
|
const auto& channels = std::get<std::vector<std::vector<float>>>(ppg_data.channel_data);
|
||||||
if (channels.size() < 2) {
|
if (channels.size() < 2) {
|
||||||
std::cerr << "警告: PPG数据通道数不足,需要至少2个通道,当前: " << channels.size() << std::endl;
|
std::cerr << "警告: PPG数据通道数不足,需要至少2个通道,当前: " << channels.size() << ",需要至少2个通道" << std::endl;
|
||||||
return 0.0f;
|
return 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -755,90 +755,7 @@ class BluetoothManager(private val context: Context) {
|
||||||
return packet
|
return packet
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析轻迅协议数据包
|
|
||||||
*/
|
|
||||||
private fun parseProtocolPacket(packet: ByteArray): Triple<Int, Int, ByteArray>? {
|
|
||||||
if (packet.size < Protocol.MIN_PACKET_SIZE) {
|
|
||||||
Log.e(TAG, "数据包长度不足: ${packet.size} < ${Protocol.MIN_PACKET_SIZE}")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 功能码 (小端格式)
|
|
||||||
val functionCode = (packet[1].toInt() and 0xFF shl 8) or (packet[0].toInt() and 0xFF)
|
|
||||||
|
|
||||||
// 数据长度 (小端格式)
|
|
||||||
val dataLength = (packet[3].toInt() and 0xFF shl 8) or (packet[2].toInt() and 0xFF)
|
|
||||||
|
|
||||||
// 验证数据包长度
|
|
||||||
val expectedLength = Protocol.PACKET_HEADER_SIZE + dataLength + Protocol.CRC_SIZE
|
|
||||||
if (packet.size != expectedLength) {
|
|
||||||
Log.e(TAG, "数据包长度不匹配: 实际=${packet.size}, 期望=${expectedLength}")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提取数据内容
|
|
||||||
val data = if (dataLength > 0) {
|
|
||||||
ByteArray(dataLength)
|
|
||||||
} else {
|
|
||||||
ByteArray(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dataLength > 0) {
|
|
||||||
System.arraycopy(packet, Protocol.PACKET_HEADER_SIZE, data, 0, dataLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证CRC16
|
|
||||||
val calculatedCrc = calculateCRC16(packet, 0, Protocol.PACKET_HEADER_SIZE + dataLength)
|
|
||||||
val receivedCrc = (packet[packet.size - 1].toInt() and 0xFF shl 8) or (packet[packet.size - 2].toInt() and 0xFF)
|
|
||||||
|
|
||||||
if (calculatedCrc != receivedCrc) {
|
|
||||||
Log.e(TAG, "CRC校验失败: 计算=${calculatedCrc}, 接收=${receivedCrc}")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return Triple(functionCode, dataLength, data)
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "解析数据包异常: ${e.message}")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理接收到的协议数据
|
|
||||||
*/
|
|
||||||
private fun handleProtocolData(packet: ByteArray) {
|
|
||||||
// 首先尝试解析为轻迅协议数据包
|
|
||||||
val parsed = parseProtocolPacket(packet)
|
|
||||||
if (parsed != null) {
|
|
||||||
val (functionCode, dataLength, data) = parsed
|
|
||||||
|
|
||||||
totalPacketsReceived++
|
|
||||||
totalBytesReceived += packet.size
|
|
||||||
|
|
||||||
Log.d(TAG, "收到协议数据: 功能码=0x${functionCode.toString(16).uppercase()}, 数据长度=$dataLength")
|
|
||||||
|
|
||||||
// 根据功能码处理数据
|
|
||||||
when (functionCode) {
|
|
||||||
Protocol.FUNC_QUERY_DEVICE_INFO -> handleDeviceInfoResponse(data)
|
|
||||||
Protocol.FUNC_QUERY_BATTERY -> handleBatteryResponse(data)
|
|
||||||
Protocol.FUNC_START_COLLECTION -> handleCollectionResponse(data)
|
|
||||||
Protocol.FUNC_DATA_STREAM -> handleDataStream(data)
|
|
||||||
Protocol.FUNC_ALARM_DATA -> handleAlarmData(data)
|
|
||||||
Protocol.FUNC_STATUS_REPORT -> handleStatusReport(data)
|
|
||||||
else -> {
|
|
||||||
Log.w(TAG, "未知功能码: 0x${functionCode.toString(16).uppercase()}")
|
|
||||||
callback?.onProtocolDataReceived(functionCode, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 如果协议解析失败,尝试其他解析方式
|
|
||||||
Log.w(TAG, "轻迅协议解析失败,尝试其他解析方式")
|
|
||||||
handleRawData(packet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理原始数据(非标准协议格式)
|
* 处理原始数据(非标准协议格式)
|
||||||
|
|
@ -1714,7 +1631,7 @@ class BluetoothManager(private val context: Context) {
|
||||||
Log.d(TAG, "解析功能码: 0x${functionCode.toString(16).uppercase()}")
|
Log.d(TAG, "解析功能码: 0x${functionCode.toString(16).uppercase()}")
|
||||||
Log.d(TAG, "解析数据长度: $dataLength")
|
Log.d(TAG, "解析数据长度: $dataLength")
|
||||||
|
|
||||||
if (functionCode == Protocol.FUNC_DATA_STREAM && dataLength == 238) {
|
if (functionCode == Protocol.FUNC_DATA_STREAM && dataLength == 238) {
|
||||||
Log.d(TAG, "✅ 确认是轻迅协议数据流包")
|
Log.d(TAG, "✅ 确认是轻迅协议数据流包")
|
||||||
Log.d(TAG, "协议包结构: 功能码(2) + 数据长度(2) + 数据内容(238) + CRC16(2) = 244字节")
|
Log.d(TAG, "协议包结构: 功能码(2) + 数据长度(2) + 数据内容(238) + CRC16(2) = 244字节")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -568,38 +568,7 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取原始通道数据
|
|
||||||
*/
|
|
||||||
private fun getRawChannelData(channelIndex: Int): List<Float> {
|
|
||||||
val data = mutableListOf<Float>()
|
|
||||||
try {
|
|
||||||
// 从原始数据包中提取指定通道的数据
|
|
||||||
val packets = dataManager.getPacketBuffer()
|
|
||||||
for (packet in packets) {
|
|
||||||
val channelData = packet.getChannelData()
|
|
||||||
if (channelData != null && channelIndex < channelData.size) {
|
|
||||||
data.addAll(channelData[channelIndex])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("MainActivity", "获取原始通道数据失败: ${e.message}", e)
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取处理后的通道数据(滤波后的数据)
|
|
||||||
*/
|
|
||||||
private fun getProcessedChannelData(channelIndex: Int): List<Float> {
|
|
||||||
try {
|
|
||||||
// 直接从DataManager获取滤波后的数据
|
|
||||||
return dataManager.getProcessedChannelData(channelIndex)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("MainActivity", "获取处理后通道数据失败: ${e.message}", e)
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 实时数据回调实现
|
// 实时数据回调实现
|
||||||
override fun onProcessedDataAvailable(channelIndex: Int, processedData: List<Float>) {
|
override fun onProcessedDataAvailable(channelIndex: Int, processedData: List<Float>) {
|
||||||
|
|
@ -1236,8 +1205,6 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
|
||||||
private fun showStimSwitchDialog() {
|
private fun showStimSwitchDialog() {
|
||||||
val dialogOptions = arrayOf(
|
val dialogOptions = arrayOf(
|
||||||
"高级电刺激控制",
|
"高级电刺激控制",
|
||||||
"预设模式选择",
|
|
||||||
"简单开关控制",
|
|
||||||
"关闭电刺激"
|
"关闭电刺激"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1246,9 +1213,7 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
|
||||||
.setItems(dialogOptions) { _, which ->
|
.setItems(dialogOptions) { _, which ->
|
||||||
when (which) {
|
when (which) {
|
||||||
0 -> showAdvancedStimDialog() // 高级电刺激控制
|
0 -> showAdvancedStimDialog() // 高级电刺激控制
|
||||||
1 -> showPresetStimDialog() // 预设模式选择
|
1 -> {
|
||||||
2 -> showSimpleStimDialog() // 简单开关控制
|
|
||||||
3 -> {
|
|
||||||
bluetoothManager.stimSwitch(enable = false)
|
bluetoothManager.stimSwitch(enable = false)
|
||||||
updateStatus("发送电刺激关闭指令")
|
updateStatus("发送电刺激关闭指令")
|
||||||
}
|
}
|
||||||
|
|
@ -1406,153 +1371,7 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
|
||||||
|
|
||||||
return layout
|
return layout
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 预设模式选择对话框
|
|
||||||
*/
|
|
||||||
private fun showPresetStimDialog() {
|
|
||||||
val presetOptions = arrayOf(
|
|
||||||
"高强度短期刺激 (30s)",
|
|
||||||
"低强度长期刺激 (15分钟)",
|
|
||||||
"康复训练模式 (5分钟)",
|
|
||||||
"自定义预设参数"
|
|
||||||
)
|
|
||||||
|
|
||||||
androidx.appcompat.app.AlertDialog.Builder(this)
|
|
||||||
.setTitle("预设刺激模式")
|
|
||||||
.setItems(presetOptions) { _, which ->
|
|
||||||
when (which) {
|
|
||||||
0 -> {
|
|
||||||
bluetoothManager.stimHighIntensityShort(intensity = 80)
|
|
||||||
updateStatus("启动高强度短期刺激模式")
|
|
||||||
}
|
|
||||||
1 -> {
|
|
||||||
bluetoothManager.stimLowIntensityLong(intensity = 30)
|
|
||||||
updateStatus("启动低强度长期刺激模式")
|
|
||||||
}
|
|
||||||
2 -> {
|
|
||||||
bluetoothManager.stimRehabilitationMode(intensity = 60)
|
|
||||||
updateStatus("启动康复训练模式")
|
|
||||||
}
|
|
||||||
3 -> showPresetParamDialog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setNegativeButton("取消", null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 预设参数调整对话框 - 简化版本
|
|
||||||
*/
|
|
||||||
private fun showPresetParamDialog() {
|
|
||||||
val builder = androidx.appcompat.app.AlertDialog.Builder(this)
|
|
||||||
builder.setTitle("调整预设参数")
|
|
||||||
|
|
||||||
val layout = LinearLayout(this).apply {
|
|
||||||
orientation = LinearLayout.VERTICAL
|
|
||||||
setPadding(50, 30, 50, 30)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 预设类型选择
|
|
||||||
val presetTypeView = Spinner(this).apply {
|
|
||||||
adapter = ArrayAdapter(this@MainActivity, android.R.layout.simple_spinner_item, arrayOf("高强度短期", "低强度长期", "康复训练")).also { adapter ->
|
|
||||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
|
||||||
}
|
|
||||||
setSelection(0)
|
|
||||||
}
|
|
||||||
val presetLabel = TextView(this).apply {
|
|
||||||
text = "预设类型:"
|
|
||||||
}
|
|
||||||
layout.addView(presetLabel)
|
|
||||||
layout.addView(presetTypeView)
|
|
||||||
|
|
||||||
// 强度输入
|
|
||||||
val intensityInput = createSimpleInputField("强度 (%):", "60")
|
|
||||||
layout.addView(intensityInput)
|
|
||||||
|
|
||||||
// 持续时间输入
|
|
||||||
val durationInput = createSimpleInputField("持续时间:", "30")
|
|
||||||
layout.addView(durationInput)
|
|
||||||
|
|
||||||
builder.setView(layout)
|
|
||||||
builder.setPositiveButton("应用") { dialog, _ ->
|
|
||||||
try {
|
|
||||||
val presetType = presetTypeView.selectedItemPosition
|
|
||||||
val intensityChild = intensityInput.getChildAt(1) as EditText
|
|
||||||
val durationChild = durationInput.getChildAt(1) as EditText
|
|
||||||
val intensity = intensityChild.text.toString().toInt().coerceIn(1, 100)
|
|
||||||
val duration = durationChild.text.toString().toInt().coerceIn(1, 120)
|
|
||||||
|
|
||||||
when (presetType) {
|
|
||||||
0 -> {
|
|
||||||
bluetoothManager.stimHighIntensityShort(durationSeconds = duration, intensity = intensity)
|
|
||||||
updateStatus("应用高强度短期模式: ${duration}s, 强度${intensity}%")
|
|
||||||
}
|
|
||||||
1 -> {
|
|
||||||
bluetoothManager.stimLowIntensityLong(durationMinutes = duration, intensity = intensity)
|
|
||||||
updateStatus("应用低强度长期模式: ${duration}分钟, 强度${intensity}%")
|
|
||||||
}
|
|
||||||
2 -> {
|
|
||||||
bluetoothManager.stimRehabilitationMode(intensity = intensity)
|
|
||||||
updateStatus("应用康复训练模式: 强度${intensity}%")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: NumberFormatException) {
|
|
||||||
updateStatus("❌ 参数输入错误")
|
|
||||||
}
|
|
||||||
dialog.dismiss()
|
|
||||||
}
|
|
||||||
builder.setNegativeButton("取消", null)
|
|
||||||
builder.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 简单开关控制对话框
|
|
||||||
*/
|
|
||||||
private fun showSimpleStimDialog() {
|
|
||||||
val simpleOptions = arrayOf(
|
|
||||||
"开启刺激A (默认参数)",
|
|
||||||
"开启刺激B (默认参数)",
|
|
||||||
"关闭电刺激"
|
|
||||||
)
|
|
||||||
|
|
||||||
androidx.appcompat.app.AlertDialog.Builder(this)
|
|
||||||
.setTitle("简单电刺激控制")
|
|
||||||
.setItems(simpleOptions) { _, which ->
|
|
||||||
when (which) {
|
|
||||||
0 -> {
|
|
||||||
bluetoothManager.stimSwitchSimple(0, true)
|
|
||||||
updateStatus("发送电刺激开关指令: 刺激A 开启 (默认参数)")
|
|
||||||
}
|
|
||||||
1 -> {
|
|
||||||
bluetoothManager.stimSwitchSimple(1, true)
|
|
||||||
updateStatus("发送电刺激开关指令: 刺激B 开启 (默认参数)")
|
|
||||||
}
|
|
||||||
2 -> {
|
|
||||||
bluetoothManager.stimSwitch(enable = false)
|
|
||||||
updateStatus("发送电刺激关闭指令")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setNegativeButton("取消", null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示调试状态对话框
|
|
||||||
*/
|
|
||||||
private fun showDebugStatus() {
|
|
||||||
val status = bluetoothManager.debugConnectionStatus()
|
|
||||||
|
|
||||||
androidx.appcompat.app.AlertDialog.Builder(this)
|
|
||||||
.setTitle("协议统计信息")
|
|
||||||
.setMessage(status)
|
|
||||||
.setPositiveButton("确定", null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun clearAllData() {
|
private fun clearAllData() {
|
||||||
// 清空动态图表数据
|
// 清空动态图表数据
|
||||||
dynamicChartManager.clearAllCharts()
|
dynamicChartManager.clearAllCharts()
|
||||||
|
|
@ -1563,13 +1382,6 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
|
||||||
updateStatus("已清空所有数据")
|
updateStatus("已清空所有数据")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startDataMonitoring() {
|
|
||||||
if (!isDataMonitoringEnabled) {
|
|
||||||
isDataMonitoringEnabled = true
|
|
||||||
updateStatus("📊 数据监控已启动")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDeviceInfoReceived(deviceInfo: com.example.cmake_project_test.BluetoothManager.DeviceInfo) {
|
override fun onDeviceInfoReceived(deviceInfo: com.example.cmake_project_test.BluetoothManager.DeviceInfo) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
updateStatus("📋 设备信息: ${deviceInfo.manufacturer} ${deviceInfo.model}")
|
updateStatus("📋 设备信息: ${deviceInfo.manufacturer} ${deviceInfo.model}")
|
||||||
|
|
@ -1699,174 +1511,17 @@ class MainActivity : AppCompatActivity(), NativeMethodCallback, DataManager.Real
|
||||||
}.start()
|
}.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exportWaveformData(data: List<Float>, stats: Map<String, Any>) {
|
|
||||||
Thread {
|
|
||||||
try {
|
|
||||||
if (data.isEmpty()) {
|
|
||||||
runOnUiThread {
|
|
||||||
updateStatus("❌ 波形视图没有数据可以导出")
|
|
||||||
}
|
|
||||||
return@Thread
|
|
||||||
}
|
|
||||||
|
|
||||||
val timestamp = java.text.SimpleDateFormat("yyyyMMdd_HHmmss", java.util.Locale.getDefault()).format(java.util.Date())
|
|
||||||
val fileName = "ECG_WaveformData_$timestamp.csv"
|
|
||||||
|
|
||||||
val csvContent = buildString {
|
|
||||||
appendLine("ECG波形视图数据 (2.5秒显示窗口)")
|
|
||||||
appendLine("导出时间: ${java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault()).format(java.util.Date())}")
|
|
||||||
appendLine("数据统计:")
|
|
||||||
appendLine("- 数据点数量: ${stats["dataPoints"]}")
|
|
||||||
appendLine("- 最小值: ${stats["minValue"]}")
|
|
||||||
appendLine("- 最大值: ${stats["maxValue"]}")
|
|
||||||
appendLine("- 数据范围: ${stats["range"]}")
|
|
||||||
appendLine("- 采样率: 250Hz")
|
|
||||||
appendLine("- 时间窗口: 2.5秒")
|
|
||||||
appendLine()
|
|
||||||
appendLine("时间(ms),数据点,ECG值")
|
|
||||||
|
|
||||||
val sampleInterval = 4L // 4ms per sample (250Hz)
|
|
||||||
data.forEachIndexed { index, value ->
|
|
||||||
val timeMs = index * sampleInterval
|
|
||||||
appendLine("$timeMs,$index,$value")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val file = saveFileToExternalStorage(fileName, csvContent)
|
|
||||||
if (file != null) {
|
|
||||||
runOnUiThread {
|
|
||||||
updateStatus("✅ 波形视图数据已导出: ${file.name}")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
runOnUiThread {
|
|
||||||
updateStatus("❌ 导出波形视图数据失败")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("MainActivity", "导出波形视图数据失败: ${e.message}", e)
|
|
||||||
runOnUiThread {
|
|
||||||
updateStatus("❌ 导出波形视图数据失败: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出所有图表数据
|
* 导出所有图表数据
|
||||||
*/
|
*/
|
||||||
private fun exportAllChartData(
|
|
||||||
waveformData: List<Float>,
|
|
||||||
waveformStats: Map<String, Any>
|
|
||||||
) {
|
|
||||||
Thread {
|
|
||||||
try {
|
|
||||||
if (waveformData.isEmpty()) {
|
|
||||||
runOnUiThread {
|
|
||||||
updateStatus("❌ 没有图表数据可以导出")
|
|
||||||
}
|
|
||||||
return@Thread
|
|
||||||
}
|
|
||||||
|
|
||||||
val timestamp = java.text.SimpleDateFormat("yyyyMMdd_HHmmss", java.util.Locale.getDefault()).format(java.util.Date())
|
|
||||||
val fileName = "ECG_AllChartData_$timestamp.csv"
|
|
||||||
|
|
||||||
val csvContent = buildString {
|
|
||||||
appendLine("ECG图表数据完整导出")
|
|
||||||
appendLine("导出时间: ${java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault()).format(java.util.Date())}")
|
|
||||||
appendLine("采样率: 250Hz")
|
|
||||||
appendLine()
|
|
||||||
|
|
||||||
// 波形视图数据
|
|
||||||
appendLine("=== 波形视图数据 (2.5秒窗口) ===")
|
|
||||||
appendLine("数据点数量: ${waveformStats["dataPoints"]}")
|
|
||||||
appendLine("最小值: ${waveformStats["minValue"]}")
|
|
||||||
appendLine("最大值: ${waveformStats["maxValue"]}")
|
|
||||||
appendLine("数据范围: ${waveformStats["range"]}")
|
|
||||||
appendLine()
|
|
||||||
appendLine("时间(ms),数据点,波形ECG值")
|
|
||||||
|
|
||||||
val sampleInterval = 4L // 4ms per sample (250Hz)
|
|
||||||
waveformData.forEachIndexed { index, value ->
|
|
||||||
val timeMs = index * sampleInterval
|
|
||||||
appendLine("$timeMs,$index,$value")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val file = saveFileToExternalStorage(fileName, csvContent)
|
|
||||||
if (file != null) {
|
|
||||||
runOnUiThread {
|
|
||||||
updateStatus("✅ 所有图表数据已导出: ${file.name}")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
runOnUiThread {
|
|
||||||
updateStatus("❌ 导出所有图表数据失败")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("MainActivity", "导出所有图表数据失败: ${e.message}", e)
|
|
||||||
runOnUiThread {
|
|
||||||
updateStatus("❌ 导出所有图表数据失败: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出图表统计信息
|
* 导出图表统计信息
|
||||||
*/
|
*/
|
||||||
private fun exportChartStats(waveformStats: Map<String, Any>) {
|
|
||||||
Thread {
|
|
||||||
try {
|
|
||||||
val timestamp = java.text.SimpleDateFormat("yyyyMMdd_HHmmss", java.util.Locale.getDefault()).format(java.util.Date())
|
|
||||||
val fileName = "ECG_ChartStats_$timestamp.txt"
|
|
||||||
|
|
||||||
val statsContent = buildString {
|
|
||||||
appendLine("ECG图表统计信息")
|
|
||||||
appendLine("导出时间: ${java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault()).format(java.util.Date())}")
|
|
||||||
appendLine("=".repeat(50))
|
|
||||||
appendLine()
|
|
||||||
|
|
||||||
appendLine("波形视图统计 (2.5秒窗口):")
|
|
||||||
appendLine("- 数据点数量: ${waveformStats["dataPoints"]}")
|
|
||||||
appendLine("- 最小值: ${waveformStats["minValue"]}")
|
|
||||||
appendLine("- 最大值: ${waveformStats["maxValue"]}")
|
|
||||||
appendLine("- 数据范围: ${waveformStats["range"]}")
|
|
||||||
appendLine("- 是否有数据: ${waveformStats["isDataAvailable"]}")
|
|
||||||
appendLine("- 采样率: 250Hz")
|
|
||||||
appendLine("- 时间窗口: 2.5秒")
|
|
||||||
appendLine()
|
|
||||||
|
|
||||||
appendLine("数据质量评估:")
|
|
||||||
val waveformRange = waveformStats["range"] as Float
|
|
||||||
|
|
||||||
when {
|
|
||||||
waveformRange > 100f -> appendLine("- 波形视图: 信号强度良好")
|
|
||||||
waveformRange > 50f -> appendLine("- 波形视图: 信号强度中等")
|
|
||||||
else -> appendLine("- 波形视图: 信号强度较弱")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val file = saveFileToExternalStorage(fileName, statsContent)
|
|
||||||
if (file != null) {
|
|
||||||
runOnUiThread {
|
|
||||||
updateStatus("✅ 图表统计信息已导出: ${file.name}")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
runOnUiThread {
|
|
||||||
updateStatus("❌ 导出图表统计信息失败")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("MainActivity", "导出图表统计信息失败: ${e.message}", e)
|
|
||||||
runOnUiThread {
|
|
||||||
updateStatus("❌ 导出图表统计信息失败: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 显示文件分享对话框
|
* 显示文件分享对话框
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,554 @@
|
||||||
|
# 蓝牙电刺激控制应用 - 工作交接文档
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
### 项目名称
|
||||||
|
蓝牙电刺激控制应用 (SDK_APP)
|
||||||
|
|
||||||
|
### 项目功能
|
||||||
|
基于Android的蓝牙电刺激设备控制应用,支持多种设备类型(ESP32S3 SPP、轻迅设备、新设备),提供完整的电刺激参数控制和数据采集功能。
|
||||||
|
|
||||||
|
### 技术栈
|
||||||
|
- **开发语言**: Kotlin
|
||||||
|
- **构建工具**: Gradle
|
||||||
|
- **最低支持**: Android 5.1 (API 21)
|
||||||
|
- **目标支持**: Android 15 (API 35)
|
||||||
|
- **蓝牙协议**: BLE + Classic Bluetooth
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心功能模块
|
||||||
|
|
||||||
|
### 1. 蓝牙连接管理 (`BluetoothManager.kt`)
|
||||||
|
|
||||||
|
#### 1.1 设备兼容性
|
||||||
|
```kotlin
|
||||||
|
// 支持的设备类型
|
||||||
|
enum class DeviceType {
|
||||||
|
ESP32S3_DEVICE, // ESP32S3 SPP设备
|
||||||
|
QINGXUN, // 轻迅设备
|
||||||
|
NEW_DEVICE // 新设备
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.2 扫描策略
|
||||||
|
- **Android 8/8.1**: 混合扫描模式(BLE + 传统蓝牙)
|
||||||
|
- **Android 6-11**: 优先BLE,失败时回退到传统蓝牙
|
||||||
|
- **Android 12+**: 完整BLE扫描支持
|
||||||
|
|
||||||
|
#### 1.3 连接流程
|
||||||
|
1. 权限检查
|
||||||
|
2. 蓝牙适配器初始化
|
||||||
|
3. 设备扫描
|
||||||
|
4. 服务发现
|
||||||
|
5. 特征值订阅
|
||||||
|
6. 数据通信
|
||||||
|
|
||||||
|
### 2. 电刺激控制协议
|
||||||
|
|
||||||
|
#### 2.1 协议格式(19字节)
|
||||||
|
```
|
||||||
|
[0-1] 功能码 (0x0003)
|
||||||
|
[2-3] 数据长度 (0x000D = 13字节)
|
||||||
|
[4] 开关状态及电刺激类型
|
||||||
|
[5] 强度值
|
||||||
|
[6-7] 频率值 (小端格式)
|
||||||
|
[8-9] 总持续时间 (小端格式)
|
||||||
|
[10-11] 休息时间 (小端格式)
|
||||||
|
[12-13] 静默时间 (小端格式)
|
||||||
|
[14] 缓进时间
|
||||||
|
[15] 保持时间
|
||||||
|
[16] 缓出时间
|
||||||
|
[17-18] CRC16校验
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2 参数控制
|
||||||
|
| 参数 | 范围 | 默认值 | 单位 | 说明 |
|
||||||
|
|------|------|--------|------|------|
|
||||||
|
| 刺激类型 | 0-15 | 1 | - | 电刺激类型选择 |
|
||||||
|
| 强度 | 1-100 | 50 | % | 刺激强度百分比 |
|
||||||
|
| 频率 | 1-200 | 100 | Hz | 刺激频率 |
|
||||||
|
| 总时长 | 1-3600 | 10 | 秒 | 总刺激持续时间 |
|
||||||
|
| 休息时间 | 100-10000 | 1000 | 毫秒 | 刺激间隔休息时间 |
|
||||||
|
| 静默时间 | 100-5000 | 500 | 毫秒 | 刺激间隔静默时间 |
|
||||||
|
| 缓进时间 | 1-30 | 2 | 秒 | 刺激强度逐渐增加时间 |
|
||||||
|
| 保持时间 | 1-300 | 5 | 秒 | 刺激强度保持时间 |
|
||||||
|
| 缓出时间 | 1-30 | 2 | 秒 | 刺激强度逐渐减少时间 |
|
||||||
|
|
||||||
|
### 3. 数据存储功能
|
||||||
|
|
||||||
|
#### 3.1 数据导出功能
|
||||||
|
- **CSV格式导出**: 支持多通道生理信号数据导出
|
||||||
|
- **文件存储**: 保存到外部存储目录
|
||||||
|
- **数据格式**: 包含时间戳、通道数据、统计信息
|
||||||
|
|
||||||
|
#### 3.2 导出类型
|
||||||
|
| 导出类型 | 文件名格式 | 内容说明 |
|
||||||
|
|----------|------------|----------|
|
||||||
|
| **单通道数据** | `ECG_ChannelData_YYYYMMDD_HHMMSS.csv` | 单个通道的完整数据 |
|
||||||
|
| **多通道数据** | `ECG_MultiChannelData_YYYYMMDD_HHMMSS.csv` | 所有通道的同步数据 |
|
||||||
|
| **波形视图数据** | `ECG_WaveformData_YYYYMMDD_HHMMSS.csv` | 2.5秒显示窗口数据 |
|
||||||
|
| **完整图表数据** | `ECG_AllChartData_YYYYMMDD_HHMMSS.csv` | 所有图表数据汇总 |
|
||||||
|
|
||||||
|
#### 3.3 数据管理 (`DataManager.kt`)
|
||||||
|
- **数据解析**: 使用原生方法解析蓝牙数据流
|
||||||
|
- **缓冲管理**: 实时数据缓冲和回调管理
|
||||||
|
- **多回调支持**: 支持多个数据接收器
|
||||||
|
|
||||||
|
### 4. 图表显示功能
|
||||||
|
|
||||||
|
#### 4.1 动态图表系统 (`DynamicChartView.kt`)
|
||||||
|
- **实时绘制**: 60fps实时数据绘制
|
||||||
|
- **多通道支持**: 支持单通道和多通道数据显示
|
||||||
|
- **生产者-消费者模式**: 使用队列缓冲数据,分离数据接收和显示
|
||||||
|
|
||||||
|
#### 4.2 图表管理器 (`DynamicChartManager.kt`)
|
||||||
|
- **动态创建**: 根据设备类型自动创建图表
|
||||||
|
- **设备配置**: 不同设备类型的图表配置
|
||||||
|
- **全屏支持**: 支持图表全屏显示
|
||||||
|
|
||||||
|
#### 4.3 全屏图表 (`FullscreenChartActivity.kt`)
|
||||||
|
- **全屏模式**: 隐藏状态栏和导航栏
|
||||||
|
- **生命体征显示**: 实时显示心率和血氧
|
||||||
|
- **滤波器控制**: 集成滤波器管理器
|
||||||
|
- **Y轴范围设置**: 支持自定义Y轴范围
|
||||||
|
|
||||||
|
#### 4.4 图表特性
|
||||||
|
| 特性 | 说明 | 技术实现 |
|
||||||
|
|------|------|----------|
|
||||||
|
| **实时更新** | 60fps刷新率 | ScheduledThreadPoolExecutor |
|
||||||
|
| **数据缓冲** | 队列容量10000点 | LinkedBlockingQueue |
|
||||||
|
| **内存管理** | 自动清理旧数据 | 定期内存检查 |
|
||||||
|
| **多通道** | 支持PPG等多通道设备 | 多通道数据队列 |
|
||||||
|
| **交互控制** | 缩放、平移 | 手势识别 |
|
||||||
|
|
||||||
|
### 5. 滤波器系统
|
||||||
|
|
||||||
|
#### 5.1 滤波器管理器 (`FullscreenFilterManager.kt`)
|
||||||
|
- **实时滤波**: 支持多种滤波器类型
|
||||||
|
- **参数调节**: 可调节滤波器参数
|
||||||
|
- **性能优化**: 高效的数据处理算法
|
||||||
|
|
||||||
|
#### 5.2 支持的滤波器
|
||||||
|
- **低通滤波器**: 去除高频噪声
|
||||||
|
- **高通滤波器**: 去除低频漂移
|
||||||
|
- **带通滤波器**: 保留特定频率范围
|
||||||
|
- **陷波滤波器**: 去除工频干扰
|
||||||
|
|
||||||
|
### 6. 用户界面 (`MainActivity.kt`)
|
||||||
|
|
||||||
|
#### 6.1 权限管理
|
||||||
|
```kotlin
|
||||||
|
// 动态权限请求
|
||||||
|
private val REQUIRED_PERMISSIONS: Array<String> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
// Android 12+ 使用新的蓝牙权限
|
||||||
|
arrayOf(
|
||||||
|
Manifest.permission.BLUETOOTH_SCAN,
|
||||||
|
Manifest.permission.BLUETOOTH_CONNECT,
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||||
|
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||||
|
)
|
||||||
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
// Android 6-11 只需要位置权限
|
||||||
|
arrayOf(
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||||
|
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Android 5.1及以下不需要动态权限
|
||||||
|
emptyArray()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.2 电刺激控制界面
|
||||||
|
- **高级电刺激控制**: 完整参数自定义
|
||||||
|
- **预设模式选择**: 快速选择预设方案
|
||||||
|
- **简单开关控制**: 使用默认参数
|
||||||
|
- **滑动对话框**: 支持滚动查看所有参数
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 代码运行机制
|
||||||
|
|
||||||
|
### 1. 应用启动流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[应用启动] --> B[MainActivity.onCreate]
|
||||||
|
B --> C[权限检查]
|
||||||
|
C --> D[蓝牙初始化]
|
||||||
|
D --> E[UI初始化]
|
||||||
|
E --> F[等待用户操作]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 蓝牙连接流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[用户点击连接] --> B[权限检查]
|
||||||
|
B --> C{权限是否通过?}
|
||||||
|
C -->|否| D[请求权限]
|
||||||
|
C -->|是| E[开始扫描]
|
||||||
|
D --> E
|
||||||
|
E --> F[设备发现]
|
||||||
|
F --> G[选择设备]
|
||||||
|
G --> H[建立连接]
|
||||||
|
H --> I[服务发现]
|
||||||
|
I --> J[特征值订阅]
|
||||||
|
J --> K[数据通信就绪]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 电刺激控制流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[用户选择电刺激控制] --> B[选择控制方式]
|
||||||
|
B --> C{控制方式}
|
||||||
|
C -->|高级控制| D[参数输入对话框]
|
||||||
|
C -->|预设模式| E[预设选择]
|
||||||
|
C -->|简单控制| F[快速开关]
|
||||||
|
D --> G[参数验证]
|
||||||
|
E --> G
|
||||||
|
F --> G
|
||||||
|
G --> H[构建数据包]
|
||||||
|
H --> I[发送指令]
|
||||||
|
I --> J[状态反馈]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 数据包构建流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[接收参数] --> B[创建19字节数组]
|
||||||
|
B --> C[写入功能码]
|
||||||
|
C --> D[写入数据长度]
|
||||||
|
D --> E[写入开关状态]
|
||||||
|
E --> F[写入强度]
|
||||||
|
F --> G[写入频率]
|
||||||
|
G --> H[写入持续时间]
|
||||||
|
H --> I[写入休息时间]
|
||||||
|
I --> J[写入静默时间]
|
||||||
|
J --> K[写入缓进时间]
|
||||||
|
K --> L[写入保持时间]
|
||||||
|
L --> M[写入缓出时间]
|
||||||
|
M --> N[计算CRC16]
|
||||||
|
N --> O[发送数据包]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 关键代码文件说明
|
||||||
|
|
||||||
|
### 1. `BluetoothManager.kt` - 蓝牙管理核心
|
||||||
|
|
||||||
|
#### 主要类和方法:
|
||||||
|
- `BluetoothManager`: 蓝牙连接管理主类
|
||||||
|
- `startScan()`: 启动设备扫描
|
||||||
|
- `connectToDevice()`: 连接指定设备
|
||||||
|
- `stimSwitch()`: 发送电刺激控制指令
|
||||||
|
- `handleProtocolData()`: 处理接收到的数据
|
||||||
|
|
||||||
|
#### 关键特性:
|
||||||
|
- 多设备类型支持
|
||||||
|
- Android版本兼容性处理
|
||||||
|
- 混合扫描策略
|
||||||
|
- 协议数据包构建
|
||||||
|
|
||||||
|
### 2. `MainActivity.kt` - 用户界面控制
|
||||||
|
|
||||||
|
#### 主要类和方法:
|
||||||
|
- `MainActivity`: 主界面Activity
|
||||||
|
- `showAdvancedStimDialog()`: 高级电刺激控制对话框
|
||||||
|
- `showPresetStimDialog()`: 预设模式选择
|
||||||
|
- `createSimpleInputField()`: 创建输入字段
|
||||||
|
- `checkAndRequestPermissions()`: 权限检查
|
||||||
|
|
||||||
|
#### 关键特性:
|
||||||
|
- 动态权限管理
|
||||||
|
- 滑动对话框界面
|
||||||
|
- 参数输入验证
|
||||||
|
- 状态实时反馈
|
||||||
|
|
||||||
|
### 3. `DynamicChartView.kt` - 动态图表显示
|
||||||
|
|
||||||
|
#### 主要类和方法:
|
||||||
|
- `DynamicChartView`: 自定义图表视图
|
||||||
|
- `updateData()`: 更新单通道数据
|
||||||
|
- `updateMultiChannelData()`: 更新多通道数据
|
||||||
|
- `startConsumer()`: 启动消费者线程
|
||||||
|
- `stopConsumer()`: 停止消费者线程
|
||||||
|
|
||||||
|
#### 关键特性:
|
||||||
|
- 生产者-消费者模式
|
||||||
|
- 60fps实时绘制
|
||||||
|
- 多通道数据支持
|
||||||
|
- 内存管理优化
|
||||||
|
|
||||||
|
### 4. `DynamicChartManager.kt` - 图表管理器
|
||||||
|
|
||||||
|
#### 主要类和方法:
|
||||||
|
- `DynamicChartManager`: 图表管理主类
|
||||||
|
- `createChartsForDevice()`: 根据设备类型创建图表
|
||||||
|
- `updateChartsData()`: 更新所有图表数据
|
||||||
|
- `getDeviceConfig()`: 获取设备配置
|
||||||
|
|
||||||
|
#### 关键特性:
|
||||||
|
- 动态图表创建
|
||||||
|
- 设备类型适配
|
||||||
|
- 全屏模式支持
|
||||||
|
|
||||||
|
### 5. `FullscreenChartActivity.kt` - 全屏图表
|
||||||
|
|
||||||
|
#### 主要类和方法:
|
||||||
|
- `FullscreenChartActivity`: 全屏图表Activity
|
||||||
|
- `setupFullscreen()`: 设置全屏模式
|
||||||
|
- `setupFullscreenCharts()`: 设置全屏图表
|
||||||
|
- `setupVitalSignsDisplay()`: 设置生命体征显示
|
||||||
|
|
||||||
|
#### 关键特性:
|
||||||
|
- 全屏显示模式
|
||||||
|
- 生命体征实时显示
|
||||||
|
- 滤波器集成
|
||||||
|
- Y轴范围自定义
|
||||||
|
|
||||||
|
### 6. `DataManager.kt` - 数据管理
|
||||||
|
|
||||||
|
#### 主要类和方法:
|
||||||
|
- `DataManager`: 数据管理主类
|
||||||
|
- `processData()`: 处理原始数据
|
||||||
|
- `setRealTimeCallback()`: 设置实时数据回调
|
||||||
|
- `addRealTimeCallback()`: 添加数据回调
|
||||||
|
|
||||||
|
#### 关键特性:
|
||||||
|
- 原生方法调用
|
||||||
|
- 多回调支持
|
||||||
|
- 数据缓冲管理
|
||||||
|
|
||||||
|
### 7. `AndroidManifest.xml` - 权限配置
|
||||||
|
|
||||||
|
#### 权限声明:
|
||||||
|
```xml
|
||||||
|
<!-- 基础蓝牙权限 -->
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||||
|
|
||||||
|
<!-- Android 12+ 新蓝牙权限 -->
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
|
||||||
|
<!-- 位置权限 -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 设备支持
|
||||||
|
|
||||||
|
### 1. ESP32S3 SPP设备
|
||||||
|
- **服务UUID**: 0xABF0
|
||||||
|
- **特征值**:
|
||||||
|
- 数据发送: 0xABF1, 0xABF3
|
||||||
|
- 数据接收: 0xABF2, 0xABF4
|
||||||
|
- **设备名称**: ESP_SPP_SERVER
|
||||||
|
|
||||||
|
### 2. 轻迅设备
|
||||||
|
- **服务UUID**: Nordic UART Service
|
||||||
|
- **特征值**: TX/RX特征值
|
||||||
|
- **协议**: 轻迅自定义协议
|
||||||
|
|
||||||
|
### 3. 新设备
|
||||||
|
- **服务UUID**: 自定义UUID
|
||||||
|
- **特征值**: 自定义特征值
|
||||||
|
- **协议**: 扩展协议支持
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 操作指南
|
||||||
|
|
||||||
|
### 1. 应用启动
|
||||||
|
1. 安装APK到Android设备
|
||||||
|
2. 启动应用
|
||||||
|
3. 检查权限状态
|
||||||
|
4. 点击"连接蓝牙"开始扫描
|
||||||
|
|
||||||
|
### 2. 设备连接
|
||||||
|
1. 确保目标设备处于可发现模式
|
||||||
|
2. 点击"连接蓝牙"按钮
|
||||||
|
3. 等待设备扫描完成
|
||||||
|
4. 选择目标设备进行连接
|
||||||
|
|
||||||
|
### 3. 电刺激控制
|
||||||
|
1. 连接成功后,点击"电刺激开关"
|
||||||
|
2. 选择控制方式:
|
||||||
|
- **高级控制**: 自定义所有参数
|
||||||
|
- **预设模式**: 选择预设方案
|
||||||
|
- **简单控制**: 使用默认参数
|
||||||
|
3. 设置参数并确认
|
||||||
|
4. 查看状态反馈
|
||||||
|
|
||||||
|
### 4. 图表操作
|
||||||
|
1. **查看实时数据**: 连接设备后自动显示图表
|
||||||
|
2. **全屏模式**: 点击图表右上角全屏按钮
|
||||||
|
3. **数据导出**:
|
||||||
|
- 点击"导出数据"按钮
|
||||||
|
- 选择导出类型(单通道/多通道/完整数据)
|
||||||
|
- 文件保存到外部存储
|
||||||
|
4. **滤波器设置**: 在全屏模式下使用滤波器控制
|
||||||
|
5. **Y轴范围**: 在全屏模式下可自定义Y轴范围
|
||||||
|
|
||||||
|
### 5. 数据导出
|
||||||
|
1. **单通道数据导出**:
|
||||||
|
- 点击"导出数据" → "单通道数据"
|
||||||
|
- 选择要导出的通道
|
||||||
|
- 文件格式:CSV
|
||||||
|
2. **多通道数据导出**:
|
||||||
|
- 点击"导出数据" → "多通道数据"
|
||||||
|
- 导出所有通道的同步数据
|
||||||
|
3. **完整数据导出**:
|
||||||
|
- 点击"导出数据" → "完整数据"
|
||||||
|
- 包含所有图表和统计信息
|
||||||
|
4. **文件分享**: 导出完成后可选择分享或保存
|
||||||
|
|
||||||
|
### 6. 参数设置示例
|
||||||
|
```
|
||||||
|
刺激类型: 1
|
||||||
|
强度: 70%
|
||||||
|
频率: 120Hz
|
||||||
|
总时长: 30秒
|
||||||
|
休息时间: 1000ms
|
||||||
|
静默时间: 500ms
|
||||||
|
缓进时间: 2秒
|
||||||
|
保持时间: 26秒
|
||||||
|
缓出时间: 2秒
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 1. 权限问题
|
||||||
|
- **现象**: 显示"权限被拒绝"
|
||||||
|
- **解决**: 检查应用权限设置,确保蓝牙和位置权限已授予
|
||||||
|
|
||||||
|
### 2. 设备扫描失败
|
||||||
|
- **现象**: 找不到目标设备
|
||||||
|
- **解决**:
|
||||||
|
- 确保设备处于可发现模式
|
||||||
|
- 检查蓝牙是否开启
|
||||||
|
- 尝试重启蓝牙服务
|
||||||
|
|
||||||
|
### 3. 连接失败
|
||||||
|
- **现象**: 连接超时或失败
|
||||||
|
- **解决**:
|
||||||
|
- 检查设备距离
|
||||||
|
- 确认设备未被其他应用占用
|
||||||
|
- 尝试重新扫描
|
||||||
|
|
||||||
|
### 4. 数据发送失败
|
||||||
|
- **现象**: 电刺激指令发送失败
|
||||||
|
- **解决**:
|
||||||
|
- 检查连接状态
|
||||||
|
- 验证参数范围
|
||||||
|
- 查看日志输出
|
||||||
|
|
||||||
|
### 5. 图表显示问题
|
||||||
|
- **现象**: 图表不显示数据或显示异常
|
||||||
|
- **解决**:
|
||||||
|
- 检查数据连接状态
|
||||||
|
- 重启图表显示
|
||||||
|
- 检查内存使用情况
|
||||||
|
- 查看队列大小日志
|
||||||
|
|
||||||
|
### 6. 数据导出失败
|
||||||
|
- **现象**: 无法导出数据或文件保存失败
|
||||||
|
- **解决**:
|
||||||
|
- 检查存储权限
|
||||||
|
- 确保有足够存储空间
|
||||||
|
- 检查文件路径权限
|
||||||
|
- 重启应用后重试
|
||||||
|
|
||||||
|
### 7. 全屏模式问题
|
||||||
|
- **现象**: 全屏模式无法正常显示
|
||||||
|
- **解决**:
|
||||||
|
- 检查设备屏幕方向
|
||||||
|
- 重启全屏Activity
|
||||||
|
- 检查系统UI设置
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 开发环境配置
|
||||||
|
|
||||||
|
### 1. 开发工具
|
||||||
|
- Android Studio
|
||||||
|
- Gradle 7.0+
|
||||||
|
- Android SDK API 21-35
|
||||||
|
|
||||||
|
### 2. 构建命令
|
||||||
|
```bash
|
||||||
|
# 编译Kotlin代码
|
||||||
|
.\gradlew.bat compileDebugKotlin
|
||||||
|
|
||||||
|
# 构建APK
|
||||||
|
.\gradlew.bat assembleDebug
|
||||||
|
|
||||||
|
# 清理项目
|
||||||
|
.\gradlew.bat clean
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 调试方法
|
||||||
|
- **日志监控**: 使用Logcat查看详细日志输出
|
||||||
|
- **蓝牙状态**: 监控蓝牙连接和扫描状态
|
||||||
|
- **权限检查**: 查看权限请求和授权日志
|
||||||
|
- **数据包**: 监控电刺激指令发送日志
|
||||||
|
- **图表数据**: 查看队列大小和数据流日志
|
||||||
|
- **内存使用**: 监控内存占用和清理日志
|
||||||
|
- **数据导出**: 查看文件保存和分享日志
|
||||||
|
- **全屏模式**: 监控全屏切换和UI状态
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本历史
|
||||||
|
|
||||||
|
### 当前版本特性
|
||||||
|
- ✅ 支持Android 5.1-15
|
||||||
|
- ✅ ESP32S3 SPP设备支持
|
||||||
|
- ✅ 完整电刺激参数控制
|
||||||
|
- ✅ 滑动对话框界面
|
||||||
|
- ✅ 多设备类型兼容
|
||||||
|
- ✅ 混合扫描策略
|
||||||
|
- ✅ 权限动态管理
|
||||||
|
- ✅ 实时图表显示系统
|
||||||
|
- ✅ 多通道数据支持
|
||||||
|
- ✅ 数据导出功能
|
||||||
|
- ✅ 全屏图表模式
|
||||||
|
- ✅ 滤波器集成
|
||||||
|
- ✅ 生产者-消费者数据模式
|
||||||
|
- ✅ 内存管理优化
|
||||||
|
|
||||||
|
### 已知问题
|
||||||
|
|
||||||
|
#### 7.1 动态图标显示Bug
|
||||||
|
- **问题描述**: 动态图标显示的是队列大小而不是实际数据值
|
||||||
|
- **影响范围**: 图表状态显示
|
||||||
|
- **技术原因**: 状态更新逻辑中错误地使用了队列大小作为显示值
|
||||||
|
- **代码位置**: `DynamicChartView.kt` 中的日志输出和状态更新
|
||||||
|
- **临时解决方案**: 当前通过日志输出队列大小,需要修正为显示实际数据值
|
||||||
|
|
||||||
|
#### 7.2 其他已知问题
|
||||||
|
- **Android 8.1扫描兼容性**: 部分设备扫描兼容性问题
|
||||||
|
- **设备连接稳定性**: 部分设备连接稳定性待优化
|
||||||
|
- **内存使用**: 长时间运行可能导致内存占用过高
|
||||||
|
- **数据同步**: 多通道数据同步可能存在延迟
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 联系方式
|
||||||
|
|
||||||
|
如有技术问题或需要支持,请联系开发团队。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文档版本**: 1.0
|
||||||
|
**最后更新**: 2025年1月
|
||||||
|
**维护人员**: 开发团队
|
||||||
Loading…
Reference in New Issue