🏠 WiFi CSI 非接触式健康监测

Android APK 完整实现方案 | ESP32 + 手机APP + 云端AI

系统架构概览

核心思路:ESP32采集WiFi CSI数据 → 通过BLE/WiFi传输到手机 → 手机APP进行信号处理 → 本地AI推理 → 异常预警

🏠 居家守护

72 BPM
实时心率
16 RPM
呼吸频率
在线 卫生间

⚠️ 紧急预警

🚨 检测到疑似跌倒
🆘

卫生间区域
14:32:15

自动通知: 子女手机、社区医院

项目文件结构

📁 WiFiHealthMonitor/
📁 app/src/main/java/com/wifihealth/monitor/
📄 MainActivity.kt — 主界面
📄 BleManager.kt — ESP32蓝牙连接管理
📄 CsiDataReceiver.kt — CSI数据接收解析
📄 SignalProcessor.kt — 信号处理算法
📄 VitalSignExtractor.kt — 心率/呼吸提取
📄 FallDetector.kt — 跌倒检测AI模型
📄 AlertManager.kt — 预警管理
📄 DataUploader.kt — 云端数据同步
📄 LocalDatabase.kt — Room本地数据库
📁 app/src/main/cpp/
📄 native_processing.cpp — C++信号处理加速
📄 CMakeLists.txt
📁 app/src/main/ml/
📄 fall_detection.tflite — 跌倒检测模型
📄 heart_rate.tflite — 心率估计模型
📁 app/src/main/res/layout/
📄 activity_main.xml
📄 view_vital_card.xml
📄 dialog_alert.xml
📄 build.gradle.kts
📄 AndroidManifest.xml

核心代码实现

kotlin // build.gradle.kts (Module: app) plugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("kotlin-kapt") } android { namespace = "com.wifihealth.monitor" compileSdk = 34 defaultConfig { applicationId = "com.wifihealth.monitor" minSdk = 26 targetSdk = 34 versionCode = 1 versionName = "1.0.0" // TFLite模型配置 aaptOptions { noCompress("tflite", "lite") } } buildFeatures { viewBinding = true buildConfig = true } externalNativeBuild { cmake { path = file("src/main/cpp/CMakeLists.txt") version = "3.22.1" } } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } } dependencies { // Android核心 implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.appcompat:appcompat:1.6.1") implementation("com.google.android.material:material:1.11.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") // 协程 implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") // BLE蓝牙低功耗 implementation("no.nordicsemi.android:ble:2.7.2") // TensorFlow Lite - 本地AI推理 implementation("org.tensorflow:tensorflow-lite:2.14.0") implementation("org.tensorflow:tensorflow-lite-gpu:2.14.0") implementation("org.tensorflow:tensorflow-lite-support:0.4.4") // 信号处理: JTransforms (FFT) implementation("com.github.wendykierp:JTransforms:3.1") // 图表展示 implementation("com.github.PhilJay:MPAndroidChart:v3.1.0") // Room数据库 implementation("androidx.room:room-runtime:2.6.1") kapt("androidx.room:room-compiler:2.6.1") implementation("androidx.room:room-ktx:2.6.1") // Retrofit网络 implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:converter-gson:2.9.0") // 日志 implementation("com.jakewharton.timber:timber:5.0.1") }
kotlin package com.wifihealth.monitor import android.bluetooth.* import android.content.Context import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import no.nordicsemi.android.ble.BleManager import no.nordicsemi.android.ble.data.Data import timber.log.Timber import java.util.* /** * ESP32 CSI数据采集器的BLE管理器 * 服务UUID: 0x180D (Heart Rate) 复用或自定义 */ class Esp32BleManager( context: Context, private val onCsiDataReceived: (CsiFrame) -> Unit ) : BleManager(context) { companion object { // 自定义服务UUID (需与ESP32固件匹配) val CSI_SERVICE_UUID: UUID = UUID.fromString("4fafc201-1fb5-459e-8fcc-c5c9c331914b") val CSI_NOTIFY_CHAR_UUID: UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361b26a8") val CSI_WRITE_CHAR_UUID: UUID = UUID.fromString("1c95d5e3-d8f7-413a-bf3d-7a2e5d3be1a0") // 控制命令 const val CMD_START_CSI = 0x01 const val CMD_STOP_CSI = 0x02 const val CMD_SET_CONFIG = 0x03 } private var csiNotifyCharacteristic: BluetoothGattCharacteristic? = null private var csiWriteCharacteristic: BluetoothGattCharacteristic? = null private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected) val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow() sealed class ConnectionState { object Disconnected : ConnectionState() object Connecting : ConnectionState() object Connected : ConnectionState() data class Error(val message: String) : ConnectionState() } override fun getMinLogPriority() = Log.VERBOSE override fun initialize() { // 设置通知回调 setNotificationCallback(csiNotifyCharacteristic) .with { _, data -> parseCsiData(data)?.let { csiFrame -> onCsiDataReceived(csiFrame) } } // 启用通知 beginAtomicRequestQueue() .add(enableNotifications(csiNotifyCharacteristic) .fail { _, status -> Timber.e("启用CSI通知失败: $status") } ) .done { _connectionState.value = ConnectionState.Connected } .enqueue() } override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean { val service = gatt.getService(CSI_SERVICE_UUID) return service?.let { csiNotifyCharacteristic = it.getCharacteristic(CSI_NOTIFY_CHAR_UUID) csiWriteCharacteristic = it.getCharacteristic(CSI_WRITE_CHAR_UUID) val notifyProps = csiNotifyCharacteristic?.properties ?: 0 val hasNotify = (notifyProps and BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0 hasNotify && csiWriteCharacteristic != null } ?: false } override fun onServicesInvalidated() { csiNotifyCharacteristic = null csiWriteCharacteristic = null } /** 发送控制命令到ESP32 */ fun sendCommand(command: Byte, payload: ByteArray? = null) { val data = payload?.let { byteArrayOf(command, *it) } ?: byteArrayOf(command) csiWriteCharacteristic?.let { char -> writeCharacteristic(char, data, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) .enqueue() } } /** 解析ESP32发送的CSI数据帧 */ private fun parseCsiData(data: Data): CsiFrame? { return try { val bytes = data.value ?: return null val buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN) // 帧格式: [timestamp(8)] [sequence(2)] [subcarrier_count(2)] [csi_data(N)] val timestamp = buffer.long val sequence = buffer.short.toInt() and 0xFFFF val subcarrierCount = buffer.short.toInt() and 0xFFFF val amplitudes = FloatArray(subcarrierCount) val phases = FloatArray(subcarrierCount) for (i in 0 until subcarrierCount) { // 复数格式: [real(4)] [imag(4)] val real = buffer.float val imag = buffer.float amplitudes[i] = kotlin.math.sqrt(real * real + imag * imag) phases[i] = kotlin.math.atan2(imag, real) } CsiFrame(timestamp, sequence, amplitudes, phases) } catch (e: Exception) { Timber.e(e, "CSI数据解析失败") null } } /** 开始CSI采集 */ fun startCsiCollection() = sendCommand(CMD_START_CSI) /** 停止CSI采集 */ fun stopCsiCollection() = sendCommand(CMD_STOP_CSI) } /** CSI数据帧 */ data class CsiFrame( val timestamp: Long, // 微秒级时间戳 val sequenceNumber: Int, // 序列号 val amplitudes: FloatArray, // 子载波幅度 val phases: FloatArray // 子载波相位 )
kotlin package com.wifihealth.monitor import kotlin.math.* /** * CSI数据接收与预处理 * 处理从ESP32接收的原始CSI数据,进行质量检查和初步滤波 */ class CsiDataProcessor { companion object { const val CSI_SAMPLING_RATE = 500 // Hz, ESP32 CSI采样率 const val FFT_SIZE = 1024 const val HISTORY_SIZE = 5000 // 保存10秒历史数据 } private val csiHistory = ArrayDeque<CsiFrame>() private val lock = Any() // 滑动窗口用于实时处理 private val amplitudeBuffer = mutableListOf<FloatArray>() private val phaseBuffer = mutableListOf<FloatArray>() /** * 接收新的CSI帧并进行预处理 */ fun processNewFrame(frame: CsiFrame): ProcessedCsiData? { synchronized(lock) { // 1. 异常值检测 (Hampel滤波) if (!validateFrame(frame)) { return null } // 2. 添加到历史 csiHistory.addLast(frame) if (csiHistory.size > HISTORY_SIZE) { csiHistory.removeFirst() } // 3. 相位净化 val sanitizedPhases = sanitizePhase(frame.phases) // 4. 子载波选择 (选择方差最大的子载波) val selectedSubcarrier = selectBestSubcarrier(frame.amplitudes, sanitizedPhases) // 5. 构建处理后的数据 return ProcessedCsiData( timestamp = frame.timestamp, amplitude = frame.amplitudes[selectedSubcarrier], phase = sanitizedPhases[selectedSubcarrier], selectedSubcarrierIndex = selectedSubcarrier, allAmplitudes = frame.amplitudes.copyOf(), allPhases = sanitizedPhases.copyOf() ) } } /** * Hampel滤波器: 检测并标记异常值 */ private fun validateFrame(frame: CsiFrame): Boolean { // 检查幅度是否全为零或NaN val validAmplitudes = frame.amplitudes.count { it > 0 && !it.isNaN() } val validRatio = validAmplitudes.toFloat() / frame.amplitudes.size if (validRatio < 0.8) return false // 超过20%无效则丢弃 // 检查幅度突变 (Hampel标识符) val median = frame.amplitudes.sorted().let { if (it.size % 2 == 0) (it[it.size / 2 - 1] + it[it.size / 2]) / 2 else it[it.size / 2] } val mad = frame.amplitudes.map { abs(it - median) }.sorted().let { if (it.size % 2 == 0) (it[it.size / 2 - 1] + it[it.size / 2]) / 2 else it[it.size / 2] } // 如果存在极端异常值,标记为无效 val threshold = 3.0 * 1.4826 * mad // 3 * MAD缩放因子 val outlierCount = frame.amplitudes.count { abs(it - median) > threshold } return outlierCount < frame.amplitudes.size * 0.1 } /** * 相位净化: 消除载波频率偏移(CFO)和采样频率偏移(SFO) */ private fun sanitizePhase(rawPhases: FloatArray): FloatArray { val n = rawPhases.size val sanitized = FloatArray(n) // 解缠绕相位 (unwrap) val unwrapped = FloatArray(n) unwrapped[0] = rawPhases[0] var cumulativeOffset = 0f for (i in 1 until n) { var diff = rawPhases[i] - rawPhases[i - 1] + cumulativeOffset while (diff > PI) { diff -= 2 * PI.toFloat() cumulativeOffset -= 2 * PI.toFloat() } while (diff < -PI) { diff += 2 * PI.toFloat() cumulativeOffset += 2 * PI.toFloat() } unwrapped[i] = rawPhases[i] + cumulativeOffset } // 线性拟合去除趋势 (假设子载波索引为 -28 to 28) val subcarrierIndices = FloatArray(n) { (it - n / 2).toFloat() } // 最小二乘法线性回归 var sumX = 0f; var sumY = 0f var sumXY = 0f; var sumX2 = 0f for (i in 0 until n) { sumX += subcarrierIndices[i] sumY += unwrapped[i] sumXY += subcarrierIndices[i] * unwrapped[i] sumX2 += subcarrierIndices[i] * subcarrierIndices[i] } val slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX) val intercept = (sumY - slope * sumX) / n // 去除线性趋势 for (i in 0 until n) { sanitized[i] = unwrapped[i] - (slope * subcarrierIndices[i] + intercept) } return sanitized } /** * 选择最佳子载波: 基于方差和人体敏感度 */ private fun selectBestSubcarrier( amplitudes: FloatArray, phases: FloatArray ): Int { // 计算每个子载波的相位方差 (方差越大说明包含更多人体运动信息) val variances = phases.map { abs(it) } // 简化: 使用相位绝对值作为活跃度指标 // 排除边缘子载波 (通常噪声较大) val startIdx = amplitudes.size / 4 val endIdx = amplitudes.size * 3 / 4 var bestIdx = startIdx var maxVariance = 0f for (i in startIdx until endIdx) { if (variances[i] > maxVariance) { maxVariance = variances[i] bestIdx = i } } return bestIdx } /** * 获取最近N个样本的时序数据 (用于后续信号处理) */ fun getRecentSequence(count: Int): List<ProcessedCsiData> { synchronized(lock) { return csiHistory.takeLast(count).map { frame -> val sanitizedPhases = sanitizePhase(frame.phases) val bestSubcarrier = selectBestSubcarrier(frame.amplitudes, sanitizedPhases) ProcessedCsiData( timestamp = frame.timestamp, amplitude = frame.amplitudes[bestSubcarrier], phase = sanitizedPhases[bestSubcarrier], selectedSubcarrierIndex = bestSubcarrier, allAmplitudes = frame.amplitudes.copyOf(), allPhases = sanitizedPhases.copyOf() ) } } } } data class ProcessedCsiData( val timestamp: Long, val amplitude: Float, val phase: Float, val selectedSubcarrierIndex: Int, val allAmplitudes: FloatArray, val allPhases: FloatArray )
kotlin package com.wifihealth.monitor import org.jtransforms.fft.DoubleFFT_1D import kotlin.math.* /** * 信号处理核心: 滤波、FFT、时频分析 */ class SignalProcessor { companion object { const val SAMPLING_RATE = 500.0 // Hz // 频段定义 const val BREATH_MIN = 0.1 // Hz (6次/分钟) const val BREATH_MAX = 0.5 // Hz (30次/分钟) const val HEART_MIN = 0.8 // Hz (48次/分钟) const val HEART_MAX = 2.5 // Hz (150次/分钟) } /** * 带通滤波器 (基于FFT的零相位滤波) */ fun bandpassFilter( signal: DoubleArray, lowFreq: Double, highFreq: Double ): DoubleArray { val n = signal.size val fft = DoubleFFT_1D(n.toLong()) // 复制信号用于FFT (需要偶数长度) val complex = DoubleArray(n * 2) for (i in signal.indices) { complex[2 * i] = signal[i] complex[2 * i + 1] = 0.0 } // 正向FFT fft.complexForward(complex) // 零频附近的频率分辨率 val freqResolution = SAMPLING_RATE / n // 频域滤波: 只保留目标频段 for (k in 0 until n / 2) { val freq = k * freqResolution val shouldKeep = freq >= lowFreq && freq <= highFreq if (!shouldKeep) { complex[2 * k] = 0.0 complex[2 * k + 1] = 0.0 // 对称部分 if (k > 0 && k < n / 2) { complex[2 * (n - k)] = 0.0 complex[2 * (n - k) + 1] = 0.0 } } } // 逆向FFT fft.complexInverse(complex, true) // 提取实部 return DoubleArray(n) { complex[2 * it] } } /** * 计算信号的功率谱密度 (PSD) */ fun computePSD(signal: DoubleArray): Pair<DoubleArray, DoubleArray> { val n = signal.size val fft = DoubleFFT_1D(n.toLong()) val complex = DoubleArray(n * 2) for (i in signal.indices) { complex[2 * i] = signal[i] } fft.complexForward(complex) // 计算单边功率谱 val psd = DoubleArray(n / 2 + 1) val freqs = DoubleArray(n / 2 + 1) psd[0] = complex[0] * complex[0] / (n * n) // DC分量 freqs[0] = 0.0 for (k in 1 until n / 2) { val real = complex[2 * k] val imag = complex[2 * k + 1] psd[k] = 2 * (real * real + imag * imag) / (n * n) freqs[k] = k * SAMPLING_RATE / n } if (n % 2 == 0) { val k = n / 2 val real = complex[2 * k] psd[k] = real * real / (n * n) freqs[k] = SAMPLING_RATE / 2 } return Pair(freqs, psd) } /** * 计算STFT时频图 (用于跌倒检测的CNN输入) */ fun computeSpectrogram( signal: DoubleArray, windowSize: Int = 256, hopSize: Int = 128 ): Array<DoubleArray> { val numFrames = (signal.size - windowSize) / hopSize + 1 val spectrogram = Array(numFrames) { DoubleArray(windowSize / 2 + 1) } val window = DoubleArray(windowSize) { i -> 0.54 - 0.46 * cos(2 * PI * i / (windowSize - 1)) // Hamming窗 } val fft = DoubleFFT_1D(windowSize.toLong()) for (frameIdx in 0 until numFrames) { val start = frameIdx * hopSize val frame = DoubleArray(windowSize * 2) // 加窗 for (i in 0 until windowSize) { frame[2 * i] = signal[start + i] * window[i] frame[2 * i + 1] = 0.0 } fft.complexForward(frame) // 计算幅度 for (k in 0..windowSize / 2) { val real = frame[2 * k] val imag = frame[2 * k + 1] spectrogram[frameIdx][k] = 10 * log10(real * real + imag * imag + 1e-10) } } return spectrogram } /** * 滑动平均滤波 */ fun movingAverage(signal: DoubleArray, windowSize: Int): DoubleArray { val result = DoubleArray(signal.size) var sum = 0.0 for (i in signal.indices) { sum += signal[i] if (i >= windowSize) sum -= signal[i - windowSize] result[i] = sum / min(i + 1, windowSize) } return result } }
kotlin package com.wifihealth.monitor import kotlin.math.* /** * 生命体征提取: 呼吸频率与心率 * 使用VMD-like分解分离呼吸和心跳信号 */ class VitalSignExtractor( private val signalProcessor: SignalProcessor ) { /** * 从CSI相位序列中提取呼吸和心率 */ fun extractVitalSigns( phaseSequence: DoubleArray, windowSeconds: Int = 30 ): VitalSignsResult { // 1. 带通滤波分离呼吸频段 (0.1-0.5 Hz) val breathSignal = signalProcessor.bandpassFilter( phaseSequence, SignalProcessor.BREATH_MIN, SignalProcessor.BREATH_MAX ) // 2. 从呼吸信号中估计呼吸频率 val breathRate = estimateDominantFrequency( breathSignal, SignalProcessor.BREATH_MIN, SignalProcessor.BREATH_MAX ) // 3. 去除呼吸谐波后提取心跳频段 val residualSignal = removeBreathHarmonics(phaseSequence, breathRate) val heartSignal = signalProcessor.bandpassFilter( residualSignal, SignalProcessor.HEART_MIN, SignalProcessor.HEART_MAX ) // 4. 估计心率 val heartRate = estimateDominantFrequency( heartSignal, SignalProcessor.HEART_MIN, SignalProcessor.HEART_MAX ) // 5. 计算信噪比评估置信度 val breathSNR = computeSNR(breathSignal) val heartSNR = computeSNR(heartSignal) return VitalSignsResult( breathRate = breathRate * 60, // 转换为 次/分钟 heartRate = heartRate * 60, breathConfidence = min(1.0, breathSNR / 10.0), heartConfidence = min(1.0, heartSNR / 10.0), breathWaveform = breathSignal.takeLast(500).toList(), heartWaveform = heartSignal.takeLast(500).toList() ) } /** * 估计主导频率 (峰值检测) */ private fun estimateDominantFrequency( signal: DoubleArray, minFreq: Double, maxFreq: Double ): Double { val (freqs, psd) = signalProcessor.computePSD(signal) var maxPower = 0.0 var dominantFreq = (minFreq + maxFreq) / 2 for (i in freqs.indices) { if (freqs[i] >= minFreq && freqs[i] <= maxFreq) { if (psd[i] > maxPower) { maxPower = psd[i] dominantFreq = freqs[i] } } } return dominantFreq } /** * 去除呼吸谐波对心率的干扰 * 简化实现: 陷波滤波器去除呼吸基频及其谐波 */ private fun removeBreathHarmonics( signal: DoubleArray, breathFreq: Double ): DoubleArray { var result = signal.copyOf() // 去除前3个谐波 for (harmonic in 1..3) { val notchFreq = breathFreq * harmonic if (notchFreq > SignalProcessor.HEART_MAX) break result = applyNotchFilter(result, notchFreq, 0.02) // 2%带宽 } return result } /** * 简单陷波滤波器 (频域实现) */ private fun applyNotchFilter( signal: DoubleArray, notchFreq: Double, bandwidth: Double ): DoubleArray { val n = signal.size val fft = org.jtransforms.fft.DoubleFFT_1D(n.toLong()) val complex = DoubleArray(n * 2) for (i in signal.indices) { complex[2 * i] = signal[i] } fft.complexForward(complex) val freqRes = SignalProcessor.SAMPLING_RATE / n val notchBin = (notchFreq / freqRes).toInt() val widthBins = ((bandwidth * notchFreq) / freqRes).toInt().coerceAtLeast(1) // 零化陷波频段 for (k in max(0, notchBin - widthBins)..min(n / 2, notchBin + widthBins)) { complex[2 * k] = 0.0 complex[2 * k + 1] = 0.0 if (k > 0) { complex[2 * (n - k)] = 0.0 complex[2 * (n - k) + 1] = 0.0 } } fft.complexInverse(complex, true) return DoubleArray(n) { complex[2 * it] } } private fun computeSNR(signal: DoubleArray): Double { val mean = signal.average() val variance = signal.map { (it - mean) * (it - mean) }.average() return 10 * log10(variance + 1e-10) } } data class VitalSignsResult( val breathRate: Double, // 呼吸频率 (次/分钟) val heartRate: Double, // 心率 (次/分钟) val breathConfidence: Double, // 呼吸估计置信度 0-1 val heartConfidence: Double, // 心率估计置信度 0-1 val breathWaveform: List<Double>, val heartWaveform: List<Double> )
kotlin package com.wifihealth.monitor import android.content.Context import org.tensorflow.lite.Interpreter import org.tensorflow.lite.support.common.FileUtil import java.nio.MappedByteBuffer /** * 跌倒检测AI推理引擎 * 使用TFLite模型在手机上本地推理 */ class FallDetector(context: Context) { companion object { // 模型输入尺寸: [1, 64, 64, 1] 时频图 const val INPUT_HEIGHT = 64 const val INPUT_WIDTH = 64 const val INPUT_CHANNELS = 1 // 滑动窗口用于时序判断 const val CONFIRMATION_FRAMES = 3 // 连续3帧确认才报警 const val FALL_THRESHOLD = 0.85 // 跌倒概率阈值 } private val interpreter: Interpreter private val inputBuffer = Array(1) { Array(INPUT_HEIGHT) { Array(INPUT_WIDTH) { FloatArray(INPUT_CHANNELS) } }} // 时序平滑 private val predictionHistory = ArrayDeque<Float>() private var consecutiveFallFrames = 0 init { val modelBuffer: MappedByteBuffer = FileUtil.loadMappedFile( context, "fall_detection.tflite" ) val options = Interpreter.Options().apply { setNumThreads(4) useNNAPI = true // 使用NPU加速 (如果可用) } interpreter = Interpreter(modelBuffer, options) } /** * 处理新的CSI数据,返回跌倒检测结果 */ fun processFrame( spectrogram: Array<DoubleArray>, signalProcessor: SignalProcessor ): FallDetectionResult { // 1. 将时频图归一化并调整尺寸到模型输入 normalizeSpectrogram(spectrogram) // 2. TFLite推理 val output = Array(1) { FloatArray(4) } // [正常, 走动, 坐下, 跌倒] interpreter.run(inputBuffer, output) val probs = output[0] val fallProb = probs[3] // 3. 时序平滑: 防止瞬时误报 predictionHistory.addLast(fallProb) if (predictionHistory.size > 10) { predictionHistory.removeFirst() } // 4. 判断逻辑 val smoothedProb = predictionHistory.average().toFloat() val isFallDetected = when { smoothedProb > FALL_THRESHOLD -> { consecutiveFallFrames++ consecutiveFallFrames >= CONFIRMATION_FRAMES } else -> { consecutiveFallFrames = 0 false } } return FallDetectionResult( isFallDetected = isFallDetected, fallProbability = smoothedProb, activityClass = probs.indices.maxByOrNull { probs[it] } ?: 0, allProbabilities = probs.toList(), confidence = smoothedProb ) } /** * 归一化时频图并填充输入buffer */ private fun normalizeSpectrogram(spec: Array<DoubleArray>) { // 找到最大最小值用于归一化 var minVal = Double.POSITIVE_INFINITY var maxVal = Double.NEGATIVE_INFINITY for (row in spec) { for (v in row) { if (v < minVal) minVal = v if (v > maxVal) maxVal = v } } val range = maxVal - minVal // 双线性插值到64x64 val srcH = spec.size val srcW = if (srcH > 0) spec[0].size else 0 for (y in 0 until INPUT_HEIGHT) { for (x in 0 until INPUT_WIDTH) { // 计算源坐标 val srcY = y * srcH / INPUT_HEIGHT val srcX = x * srcW / INPUT_WIDTH val value = if (srcY < srcH && srcX < srcW && range > 0) { ((spec[srcY][srcX] - minVal) / range).toFloat() } else { 0f } inputBuffer[0][y][x][0] = value } } } /** * 释放资源 */ fun close() { interpreter.close() } } data class FallDetectionResult( val isFallDetected: Boolean, val fallProbability: Float, val activityClass: Int, // 0=正常, 1=走动, 2=坐下, 3=跌倒 val allProbabilities: List<Float>, val confidence: Float )
kotlin package com.wifihealth.monitor import android.app.* import android.content.Context import android.content.Intent import android.media.AudioAttributes import android.net.Uri import android.os.Build import android.os.VibrationEffect import android.os.Vibrator import android.os.VibratorManager import androidx.core.app.NotificationCompat import kotlinx.coroutines.* /** * 预警管理系统 * 处理跌倒报警、心率异常、呼吸暂停等紧急情况 */ class AlertManager(private val context: Context) { companion object { const val CHANNEL_ID_FALL = "fall_alert" const val CHANNEL_ID_VITAL = "vital_sign_alert" const val NOTIFICATION_ID_FALL = 1001 const val NOTIFICATION_ID_HEART = 1002 const val NOTIFICATION_ID_BREATH = 1003 // 心率异常阈值 const val HEART_RATE_MIN = 50 const val HEART_RATE_MAX = 120 // 呼吸异常阈值 const val BREATH_RATE_MIN = 8 const val BREATH_RATE_MAX = 30 } private val notificationManager = context.getSystemService( Context.NOTIFICATION_SERVICE ) as NotificationManager private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) private var alertDialog: AlertDialog? = null init { createNotificationChannels() } private fun createNotificationChannels() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // 跌倒报警通道 (最高优先级) val fallChannel = NotificationChannel( CHANNEL_ID_FALL, "跌倒紧急报警", NotificationManager.IMPORTANCE_HIGH ).apply { description = "检测到疑似跌倒时的紧急通知" setSound( Uri.parse("android.resource://${context.packageName}/raw/emergency_alert"), AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_ALARM) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .build() ) enableVibration(true) vibrationPattern = longArrayOf(0, 500, 200, 500, 200, 500) enableLights(true) lightColor = 0xFFFF0000.toInt() } // 生命体征异常通道 val vitalChannel = NotificationChannel( CHANNEL_ID_VITAL, "生命体征预警", NotificationManager.IMPORTANCE_HIGH ).apply { description = "心率或呼吸频率异常提醒" enableVibration(true) } notificationManager.createNotificationChannels(listOf(fallChannel, vitalChannel)) } } /** * 触发跌倒报警 */ fun triggerFallAlert( location: String = "卫生间", confidence: Float ) { // 1. 强震动 vibrateEmergency() // 2. 全屏报警弹窗 (即使锁屏也显示) showFullScreenAlert(location, confidence) // 3. 发送通知 val intent = Intent(context, EmergencyActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS putExtra("alert_type", "fall") putExtra("location", location) putExtra("confidence", confidence) } val pendingIntent = PendingIntent.getActivity( context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) val notification = NotificationCompat.Builder(context, CHANNEL_ID_FALL) .setSmallIcon(android.R.drawable.ic_dialog_alert) .setContentTitle("🚨 检测到疑似跌倒") .setContentText("位置: $location | 置信度: ${(confidence * 100).toInt()}%") .setPriority(NotificationCompat.PRIORITY_MAX) .setCategory(NotificationCompat.CATEGORY_ALARM) .setFullScreenIntent(pendingIntent, true) .setAutoCancel(false) .setOngoing(true) .addAction( android.R.drawable.ic_delete, "误报解除", createDismissAction("fall") ) .addAction( android.R.drawable.ic_menu_call, "立即求助", createEmergencyCallAction() ) .build() notificationManager.notify(NOTIFICATION_ID_FALL, notification) // 4. 自动拨打电话 (需要权限) scope.launch { delay(30000) // 30秒后如果未解除,自动联系紧急联系人 if (alertDialog?.isShowing == true) { makeEmergencyCall() } } } /** * 心率异常报警 */ fun triggerHeartRateAlert(heartRate: Double, isTooLow: Boolean) { val type = if (isTooLow) "心率过缓" else "心率过快" val notification = NotificationCompat.Builder(context, CHANNEL_ID_VITAL) .setSmallIcon(android.R.drawable.ic_dialog_info) .setContentTitle("⚠️ $type") .setContentText("当前心率: ${heartRate.toInt()} BPM") .setPriority(NotificationCompat.PRIORITY_HIGH) .build() notificationManager.notify(NOTIFICATION_ID_HEART, notification) } /** * 呼吸异常报警 (包括呼吸暂停) */ fun triggerBreathAlert(breathRate: Double, isApnea: Boolean = false) { val title = if (isApnea) "🚨 检测到呼吸暂停" else "⚠️ 呼吸频率异常" val notification = NotificationCompat.Builder(context, CHANNEL_ID_VITAL) .setSmallIcon(android.R.drawable.ic_dialog_info) .setContentTitle(title) .setContentText("当前呼吸: ${breathRate.toInt()} 次/分钟") .setPriority( if (isApnea) NotificationCompat.PRIORITY_MAX else NotificationCompat.PRIORITY_HIGH ) .build() notificationManager.notify(NOTIFICATION_ID_BREATH, notification) } /** * 显示全屏报警弹窗 */ private fun showFullScreenAlert(location: String, confidence: Float) { val dialogIntent = Intent(context, EmergencyActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS putExtra("show_alert", true) putExtra("location", location) putExtra("confidence", confidence) } context.startActivity(dialogIntent) } /** * 紧急震动模式 */ private fun vibrateEmergency() { val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager vibratorManager.defaultVibrator } else { @Suppress("DEPRECATION") context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { vibrator.vibrate( VibrationEffect.createWaveform( longArrayOf(0, 500, 200, 500, 200, 500), -1 ) ) } } private fun createDismissAction(type: String): PendingIntent { val intent = Intent(context, AlertDismissReceiver::class.java).apply { action = "DISMISS_ALERT" putExtra("type", type) } return PendingIntent.getBroadcast( context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } private fun createEmergencyCallAction(): PendingIntent { val intent = Intent(Intent.ACTION_CALL).apply { data = Uri.parse("tel:120") // 或配置的紧急联系人 } return PendingIntent.getActivity( context, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } private fun makeEmergencyCall() { // 实现自动拨打紧急联系人 } /** * 解除所有报警 */ fun dismissAllAlerts() { notificationManager.cancelAll() alertDialog?.dismiss() alertDialog = null } } /** * 紧急报警Activity (全屏显示,锁屏也可弹出) */ class EmergencyActivity : Activity() { // 实现全屏报警UI... }
kotlin package com.wifihealth.monitor import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.* import com.github.mikephil.charting.charts.LineChart import kotlinx.coroutines.* import kotlinx.coroutines.flow.* /** * 主界面: 实时显示心率、呼吸、CSI波形 */ class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding // 核心组件 private lateinit var bleManager: Esp32BleManager private lateinit var csiProcessor: CsiDataProcessor private lateinit var signalProcessor: SignalProcessor private lateinit var vitalExtractor: VitalSignExtractor private lateinit var fallDetector: FallDetector private lateinit var alertManager: AlertManager // 状态流 private val _uiState = MutableStateFlow(UiState()) val uiState: StateFlow<UiState> = _uiState.asStateFlow() private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) initComponents() setupUI() startMonitoring() } private fun initComponents() { csiProcessor = CsiDataProcessor() signalProcessor = SignalProcessor() vitalExtractor = VitalSignExtractor(signalProcessor) fallDetector = FallDetector(this) alertManager = AlertManager(this) bleManager = Esp32BleManager(this) { csiFrame -> // CSI数据回调 processCsiFrame(csiFrame) } } private fun setupUI() { // 绑定图表 setupHeartChart(binding.chartHeart) setupBreathChart(binding.chartBreath) setupCsiChart(binding.chartCsi) // 连接状态 lifecycleScope.launch { bleManager.connectionState.collect { state -> updateConnectionUI(state) } } // UI状态更新 lifecycleScope.launch { uiState.collect { state -> updateVitalDisplay(state) } } // 按钮事件 binding.btnConnect.setOnClickListener { scanAndConnect() } binding.btnStart.setOnClickListener { bleManager.startCsiCollection() } binding.btnStop.setOnClickListener { bleManager.stopCsiCollection() } } /** * 处理每一帧CSI数据 (运行在IO线程) */ private fun processCsiFrame(frame: CsiFrame) { scope.launch(Dispatchers.Default) { // 1. 预处理 val processed = csiProcessor.processNewFrame(frame) ?: return@launch // 2. 获取最近的时间序列 val recentData = csiProcessor.getRecentSequence( SignalProcessor.SAMPLING_RATE.toInt() * 30 // 30秒窗口 ) if (recentData.size < 100) return@launch // 数据不足 val phaseSequence = recentData.map { it.phase.toDouble() }.toDoubleArray() // 3. 提取生命体征 (每5秒执行一次) if (frame.sequenceNumber % (5 * 500) == 0) { val vitals = vitalExtractor.extractVitalSigns(phaseSequence) checkVitalAlerts(vitals) withContext(Dispatchers.Main) { _uiState.update { it.copy( heartRate = vitals.heartRate, breathRate = vitals.breathRate, heartConfidence = vitals.heartConfidence, breathConfidence = vitals.breathConfidence, heartWaveform = vitals.heartWaveform, breathWaveform = vitals.breathWaveform ) } } } // 4. 跌倒检测 (每1秒执行) if (frame.sequenceNumber % 500 == 0) { val amplitudeSequence = recentData.map { it.amplitude.toDouble() }.toDoubleArray() val spectrogram = signalProcessor.computeSpectrogram(amplitudeSequence) val fallResult = fallDetector.processFrame(spectrogram, signalProcessor) if (fallResult.isFallDetected) { withContext(Dispatchers.Main) { alertManager.triggerFallAlert( confidence = fallResult.confidence ) } } } // 5. 更新实时CSI波形 withContext(Dispatchers.Main) { updateCsiWaveform(processed) } } } private fun checkVitalAlerts(vitals: VitalSignsResult) { // 心率异常 when { vitals.heartRate < AlertManager.HEART_RATE_MIN -> { alertManager.triggerHeartRateAlert(vitals.heartRate, isTooLow = true) } vitals.heartRate > AlertManager.HEART_RATE_MAX -> { alertManager.triggerHeartRateAlert(vitals.heartRate, isTooLow = false) } } // 呼吸异常 when { vitals.breathRate < AlertManager.BREATH_RATE_MIN -> { alertManager.triggerBreathAlert(vitals.breathRate) } vitals.breathRate > AlertManager.BREATH_RATE_MAX -> { alertManager.triggerBreathAlert(vitals.breathRate) } } // 呼吸暂停检测 (呼吸频率接近0且持续) if (vitals.breathRate < 3 && vitals.breathConfidence > 0.7) { alertManager.triggerBreathAlert(vitals.breathRate, isApnea = true) } } private fun scanAndConnect() { // 扫描并连接ESP32设备 } override fun onDestroy() { super.onDestroy() fallDetector.close() scope.cancel() } } data class UiState( val isConnected: Boolean = false, val isMonitoring: Boolean = false, val heartRate: Double = 0.0, val breathRate: Double = 0.0, val heartConfidence: Double = 0.0, val breathConfidence: Double = 0.0, val heartWaveform: List<Double> = emptyList(), val breathWaveform: List<Double> = emptyList(), val csiAmplitude: List<Float> = emptyList() )

ESP32固件核心代码

ESP32端需要运行CSI采集固件,通过WiFi CSI API获取信道状态信息:

cpp // ESP32 CSI采集固件 (Arduino框架) #include <WiFi.h> #include <BLEDevice.h> #include <BLEServer.h> #include <BLEUtils.h> // WiFi CSI回调函数 void wifi_csi_cb(void *ctx, wifi_csi_info_t *data) { const wifi_pkt_rx_ctrl_t *rx_ctrl = &data->rx_ctrl; // 提取CSI数据 int8_t *csi_data = data->buf; int csi_len = rx_ctrl->sig_len; // 构建数据包: [timestamp][seq][subcarrier_count][csi_complex...] uint8_t packet[2048]; int offset = 0; // 时间戳 (8 bytes) uint64_t timestamp = micros(); memcpy(packet + offset, ×tamp, sizeof(timestamp)); offset += sizeof(timestamp); // 序列号 (2 bytes) static uint16_t seq = 0; memcpy(packet + offset, &seq, sizeof(seq)); offset += sizeof(seq); seq++; // 子载波数量 (2 bytes) - ESP32通常有64个子载波 (HT20) uint16_t subcarrier_count = 64; memcpy(packet + offset, &subcarrier_count, sizeof(subcarrier_count)); offset += sizeof(subcarrier_count); // CSI复数数据 (每个子载波: real float + imag float) for (int i = 0; i < subcarrier_count; i++) { // 从ESP32 CSI格式转换 float real = (float)csi_data[i * 2]; float imag = (float)csi_data[i * 2 + 1]; memcpy(packet + offset, &real, sizeof(float)); offset += sizeof(float); memcpy(packet + offset, &imag, sizeof(float)); offset += sizeof(float); } // 通过BLE发送给手机 if (bleConnected && pCharacteristic != nullptr) { pCharacteristic->setValue(packet, offset); pCharacteristic->notify(); } } void setup() { Serial.begin(115200); // 初始化WiFi (STA模式,连接到家中路由器) WiFi.mode(WIFI_STA); WiFi.begin("HOME_SSID", "PASSWORD"); // 启用CSI wifi_csi_config_t csi_config = { .lltf_en = true, .htltf_en = true, .stbc_htltf2_en = true, .ltf_merge_en = true, .channel_filter_en = true, .manu_scale = false, .shift = 0 }; esp_wifi_set_csi_config(&csi_config); esp_wifi_set_csi_rx_cb(wifi_csi_cb, NULL); esp_wifi_set_csi(true); // 初始化BLE BLEDevice::init("ESP32-CSI-Sensor"); BLEServer *pServer = BLEDevice::createServer(); BLEService *pService = pServer->createService(CSI_SERVICE_UUID); pCharacteristic = pService->createCharacteristic( CSI_NOTIFY_CHAR_UUID, BLECharacteristic::PROPERTY_NOTIFY ); pService->start(); BLEAdvertising *pAdvertising = BLEDevice::getAdvertising(); pAdvertising->start(); } void loop() { // 主循环: 维持WiFi连接 if (WiFi.status() != WL_CONNECTED) { WiFi.reconnect(); } delay(100); }

部署步骤

  1. 硬件准备
    ESP32-DevKitC开发板 × 2 (一个Tx发送WiFi信号,一个Rx采集CSI)
    或单ESP32作为Rx,利用家中现有路由器作为Tx
  2. 烧录ESP32固件
    使用Arduino IDE或PlatformIO编译上传上述固件
    配置家中WiFi SSID和密码
  3. 构建Android APK
    Android Studio打开项目 → Sync Gradle → Build APK
  4. 配对连接
    手机蓝牙扫描"ESP32-CSI-Sensor" → 配对连接 → 开始监测
  5. 校准与测试
    静止2分钟建立环境基线 → 测试模拟跌倒 → 验证报警功能

关键配置与注意事项

配置项 推荐值 说明
CSI采样率 500-1000 Hz 越高精度越好,但BLE带宽受限
FFT窗口 30秒 心率提取最少需要20秒数据
跌倒确认帧数 3帧 (约1.5秒) 平衡响应速度与误报率
BLE传输间隔 20ms iOS兼容的最小间隔
子载波数量 64 (HT20) ESP32支持的最大值
⚠️ 重要限制:
1. ESP32的CSI API需要连接到AP才能获取,不能作为AP自身采集
2. 当前方案为单目标检测,多人在场会相互干扰
3. 心率精度受呼吸谐波干扰,实际误差约3-8 BPM
4. 需要通过医疗器械认证才能宣传医疗级监测

优化方向

短期优化

  • ✅ 使用ESP32-S3替代ESP32 (支持WiFi 4, CSI更稳定)
  • ✅ 添加环境温湿度补偿
  • ✅ 实现多天线MIMO (ESP32-C6)
  • ✅ 本地模型量化 (INT8) 加速推理

长期演进

  • 🚀 升级到WiFi 6 (802.11ax) 更高分辨率CSI
  • 🚀 UWB超宽带精确定位
  • 🚀 毫米波雷达融合感知
  • 🚀 联邦学习保护隐私的模型更新