1256 lines
42 KiB
Python
1256 lines
42 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""
|
|||
|
|
高血压风险评估系统 - 边缘端实时处理模块(RK3588优化版)
|
|||
|
|
Hypertension Risk Assessment - Edge Computing Module (Optimized for RK3588)
|
|||
|
|
|
|||
|
|
Created: 2026-01-09
|
|||
|
|
Author: KKRobot Healthcare AI Team
|
|||
|
|
Version: v1.0_edge_realtime
|
|||
|
|
|
|||
|
|
📌 部署架构:混合部署
|
|||
|
|
- 边缘端(本代码):RK3588设备,实时预警(<1秒响应)
|
|||
|
|
- 云端(配套):深度分析,长期趋势预测
|
|||
|
|
|
|||
|
|
📊 传感器配置:
|
|||
|
|
- 床上:压电陶瓷BCG传感器(250Hz采样)
|
|||
|
|
- 厕所:毫米波雷达(20Hz采样)
|
|||
|
|
|
|||
|
|
🎯 核心功能:
|
|||
|
|
1. 实时BCG心率/HRV提取(30秒滑动窗口)
|
|||
|
|
2. 实时雷达起夜检测
|
|||
|
|
3. 硬规则快速风险评估
|
|||
|
|
4. 即时预警触发(高风险立即推送)
|
|||
|
|
5. 匿名化数据定期上传云端
|
|||
|
|
|
|||
|
|
⚡ RK3588优化:
|
|||
|
|
- 轻量级依赖(numpy/scipy only)
|
|||
|
|
- 流式处理(内存占用<500MB)
|
|||
|
|
- ARM NEON vectorization
|
|||
|
|
- 无深度学习模型
|
|||
|
|
|
|||
|
|
📦 运行要求:
|
|||
|
|
pip install numpy scipy
|
|||
|
|
python edge_hypertension_system.py
|
|||
|
|
|
|||
|
|
💾 存储需求:
|
|||
|
|
- 运行内存:<500MB
|
|||
|
|
- 磁盘缓存:<100MB(7天基线数据)
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import numpy as np
|
|||
|
|
from scipy import signal
|
|||
|
|
from scipy.interpolate import interp1d
|
|||
|
|
import time
|
|||
|
|
import json
|
|||
|
|
import os
|
|||
|
|
from collections import deque
|
|||
|
|
from datetime import datetime, timedelta
|
|||
|
|
import warnings
|
|||
|
|
warnings.filterwarnings("ignore")
|
|||
|
|
|
|||
|
|
# ==================== 0. 边缘端核心配置 ====================
|
|||
|
|
|
|||
|
|
class EdgeConfig:
|
|||
|
|
"""边缘端配置(RK3588优化)"""
|
|||
|
|
|
|||
|
|
def __init__(self):
|
|||
|
|
# 版本标识
|
|||
|
|
self.version = "v1.0_edge_realtime"
|
|||
|
|
self.device_id = "edge_rk3588_001" # 设备唯一标识
|
|||
|
|
|
|||
|
|
# ========== BCG传感器配置 ==========
|
|||
|
|
self.bcg_config = {
|
|||
|
|
'sampling_rate': 250, # 采样率(Hz)
|
|||
|
|
'window_size': 30, # 处理窗口(秒)
|
|||
|
|
'slide_step': 5, # 滑动步长(秒)
|
|||
|
|
'hr_range': (40, 120), # 心率范围(bpm)
|
|||
|
|
'rr_range': (8, 25), # 呼吸率范围(/min)
|
|||
|
|
'quality_threshold': 0.5, # 质量阈值
|
|||
|
|
'离bed_threshold': 0.3 # 离床检测阈值
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# ========== 雷达传感器配置 ==========
|
|||
|
|
self.radar_config = {
|
|||
|
|
'sampling_rate': 20, # 采样率(Hz)
|
|||
|
|
'presence_threshold': 0.6, # 存在检测阈值
|
|||
|
|
'bathroom_zone': { # 厕所区域(3D边界框)
|
|||
|
|
'x_range': (0.5, 2.5), # 米
|
|||
|
|
'y_range': (0.5, 2.0), # 米
|
|||
|
|
'z_range': (0.3, 1.8) # 米
|
|||
|
|
},
|
|||
|
|
'min_visit_duration': 30, # 最短起夜时长(秒)
|
|||
|
|
'event_merge_gap': 60, # 事件合并间隔(秒)
|
|||
|
|
'motion_threshold': 0.3 # 运动检测阈值
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# ========== 实时处理配置 ==========
|
|||
|
|
self.realtime_config = {
|
|||
|
|
'buffer_size': 7500, # BCG缓冲区大小(30秒@250Hz)
|
|||
|
|
'processing_interval': 5, # 处理间隔(秒)
|
|||
|
|
'max_latency': 0.5, # 最大延迟(秒)
|
|||
|
|
'enable_logging': True, # 是否打印日志
|
|||
|
|
'log_interval': 60 # 日志打印间隔(秒)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# ========== 风险评估配置(硬规则) ==========
|
|||
|
|
self.risk_config = {
|
|||
|
|
# RMSSD阈值(HRV核心指标)
|
|||
|
|
'rmssd_high_risk': 20, # <20ms高风险
|
|||
|
|
'rmssd_medium_risk': 30, # 20-30ms中风险
|
|||
|
|
'rmssd_low_risk': 40, # >40ms低风险
|
|||
|
|
|
|||
|
|
# 心率阈值
|
|||
|
|
'hr_high_risk': 85, # >85bpm高风险
|
|||
|
|
'hr_medium_risk': 75, # 75-85bpm中风险
|
|||
|
|
|
|||
|
|
# 起夜频率阈值(每晚)
|
|||
|
|
'bathroom_high_risk': 3, # ≥3次高风险
|
|||
|
|
'bathroom_medium_risk': 2, # 2次中风险
|
|||
|
|
|
|||
|
|
# 起夜心率上升阈值
|
|||
|
|
'hr_surge_high_risk': 20, # >20bpm高风险
|
|||
|
|
'hr_surge_medium_risk': 12, # 12-20bpm中风险
|
|||
|
|
|
|||
|
|
# 权重配置
|
|||
|
|
'weights': {
|
|||
|
|
'rmssd': 0.35,
|
|||
|
|
'sdnn': 0.15,
|
|||
|
|
'hr_level': 0.20,
|
|||
|
|
'hr_surge': 0.10,
|
|||
|
|
'bathroom_freq': 0.20
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# ========== 预警配置 ==========
|
|||
|
|
self.alert_config = {
|
|||
|
|
'high_risk_threshold': 0.65, # 高风险阈值
|
|||
|
|
'medium_risk_threshold': 0.40, # 中风险阈值
|
|||
|
|
'alert_cooldown': 300, # 预警冷却时间(秒)
|
|||
|
|
'enable_sound': True, # 是否声音提醒
|
|||
|
|
'enable_push': True # 是否推送通知
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# ========== 云端通信配置 ==========
|
|||
|
|
self.cloud_config = {
|
|||
|
|
'enable_upload': True, # 是否上传云端
|
|||
|
|
'upload_interval': 300, # 上传间隔(秒,5分钟)
|
|||
|
|
'upload_url': 'https://api.example.com/upload',
|
|||
|
|
'api_key': 'your_api_key_here',
|
|||
|
|
'anonymize': True, # 是否匿名化
|
|||
|
|
'compress': True # 是否压缩
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# ========== 本地缓存配置 ==========
|
|||
|
|
self.cache_config = {
|
|||
|
|
'baseline_file': 'baseline_cache.json',
|
|||
|
|
'event_log_file': 'event_log.jsonl',
|
|||
|
|
'max_baseline_nights': 7, # 基线窗口(天)
|
|||
|
|
'max_event_log_size': 1000, # 最大事件数
|
|||
|
|
'cache_dir': './edge_cache'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 创建缓存目录
|
|||
|
|
os.makedirs(self.cache_config['cache_dir'], exist_ok=True)
|
|||
|
|
|
|||
|
|
# 全局配置实例
|
|||
|
|
config = EdgeConfig()
|
|||
|
|
|
|||
|
|
# ==================== 1. 数据结构层 ====================
|
|||
|
|
|
|||
|
|
class RingBuffer:
|
|||
|
|
"""高效环形缓冲区(节省内存)"""
|
|||
|
|
|
|||
|
|
def __init__(self, size):
|
|||
|
|
self.size = size
|
|||
|
|
self.buffer = np.zeros(size, dtype=np.float32)
|
|||
|
|
self.index = 0
|
|||
|
|
self.is_filled = False
|
|||
|
|
|
|||
|
|
def append(self, value):
|
|||
|
|
"""追加单个值"""
|
|||
|
|
self.buffer[self.index] = value
|
|||
|
|
self.index = (self.index + 1) % self.size
|
|||
|
|
if self.index == 0:
|
|||
|
|
self.is_filled = True
|
|||
|
|
|
|||
|
|
def extend(self, values):
|
|||
|
|
"""批量追加"""
|
|||
|
|
for v in values:
|
|||
|
|
self.append(v)
|
|||
|
|
|
|||
|
|
def get_data(self):
|
|||
|
|
"""获取完整数据(按时间顺序)"""
|
|||
|
|
if not self.is_filled:
|
|||
|
|
return self.buffer[:self.index]
|
|||
|
|
else:
|
|||
|
|
return np.concatenate([
|
|||
|
|
self.buffer[self.index:],
|
|||
|
|
self.buffer[:self.index]
|
|||
|
|
])
|
|||
|
|
|
|||
|
|
def is_full(self):
|
|||
|
|
"""是否已填满"""
|
|||
|
|
return self.is_filled or self.index >= self.size
|
|||
|
|
|
|||
|
|
def clear(self):
|
|||
|
|
"""清空缓冲区"""
|
|||
|
|
self.buffer.fill(0)
|
|||
|
|
self.index = 0
|
|||
|
|
self.is_filled = False
|
|||
|
|
|
|||
|
|
class SensorEvent:
|
|||
|
|
"""传感器事件(轻量级数据结构)"""
|
|||
|
|
|
|||
|
|
def __init__(self, timestamp, event_type, data, quality=1.0):
|
|||
|
|
self.timestamp = timestamp # Unix时间戳(秒)
|
|||
|
|
self.event_type = event_type # 'BCG_HR', 'BCG_HRV', 'RADAR_VISIT', 'ALERT'
|
|||
|
|
self.data = data # 事件数据(dict)
|
|||
|
|
self.quality = quality # 质量分数(0-1)
|
|||
|
|
|
|||
|
|
def to_dict(self):
|
|||
|
|
"""转换为字典(用于序列化)"""
|
|||
|
|
return {
|
|||
|
|
'timestamp': self.timestamp,
|
|||
|
|
'event_type': self.event_type,
|
|||
|
|
'data': self.data,
|
|||
|
|
'quality': self.quality,
|
|||
|
|
'datetime': datetime.fromtimestamp(self.timestamp).strftime('%Y-%m-%d %H:%M:%S')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def anonymize(self):
|
|||
|
|
"""匿名化处理(去除敏感信息)"""
|
|||
|
|
anon_data = self.data.copy()
|
|||
|
|
# 移除可能的设备标识
|
|||
|
|
anon_data.pop('device_id', None)
|
|||
|
|
anon_data.pop('location', None)
|
|||
|
|
return SensorEvent(self.timestamp, self.event_type, anon_data, self.quality)
|
|||
|
|
|
|||
|
|
class BaselineCache:
|
|||
|
|
"""个体基线缓存(7天滚动窗口)"""
|
|||
|
|
|
|||
|
|
def __init__(self, cache_file):
|
|||
|
|
self.cache_file = cache_file
|
|||
|
|
self.baseline = self.load()
|
|||
|
|
|
|||
|
|
def load(self):
|
|||
|
|
"""加载缓存"""
|
|||
|
|
if os.path.exists(self.cache_file):
|
|||
|
|
try:
|
|||
|
|
with open(self.cache_file, 'r') as f:
|
|||
|
|
return json.load(f)
|
|||
|
|
except:
|
|||
|
|
return self._default_baseline()
|
|||
|
|
return self._default_baseline()
|
|||
|
|
|
|||
|
|
def save(self):
|
|||
|
|
"""保存缓存"""
|
|||
|
|
with open(self.cache_file, 'w') as f:
|
|||
|
|
json.dump(self.baseline, f, indent=2)
|
|||
|
|
|
|||
|
|
def update(self, night_features):
|
|||
|
|
"""更新基线(滚动窗口)"""
|
|||
|
|
if 'history' not in self.baseline:
|
|||
|
|
self.baseline['history'] = []
|
|||
|
|
|
|||
|
|
# 添加新数据
|
|||
|
|
self.baseline['history'].append({
|
|||
|
|
'date': datetime.now().strftime('%Y-%m-%d'),
|
|||
|
|
'features': night_features
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
# 保留最近7天
|
|||
|
|
if len(self.baseline['history']) > config.cache_config['max_baseline_nights']:
|
|||
|
|
self.baseline['history'] = self.baseline['history'][-7:]
|
|||
|
|
|
|||
|
|
# 重新计算基线(中位数)
|
|||
|
|
if len(self.baseline['history']) >= 3:
|
|||
|
|
self._recalculate_baseline()
|
|||
|
|
|
|||
|
|
self.save()
|
|||
|
|
|
|||
|
|
def _recalculate_baseline(self):
|
|||
|
|
"""重新计算基线(使用中位数)"""
|
|||
|
|
history = self.baseline['history']
|
|||
|
|
|
|||
|
|
rmssd_values = [h['features'].get('rmssd', 0) for h in history if h['features'].get('rmssd', 0) > 0]
|
|||
|
|
hr_values = [h['features'].get('mean_hr', 0) for h in history if h['features'].get('mean_hr', 0) > 0]
|
|||
|
|
bathroom_values = [h['features'].get('bathroom_freq', 0) for h in history]
|
|||
|
|
|
|||
|
|
if rmssd_values:
|
|||
|
|
self.baseline['rmssd'] = float(np.median(rmssd_values))
|
|||
|
|
if hr_values:
|
|||
|
|
self.baseline['mean_hr'] = float(np.median(hr_values))
|
|||
|
|
if bathroom_values:
|
|||
|
|
self.baseline['bathroom_freq'] = float(np.median(bathroom_values))
|
|||
|
|
|
|||
|
|
def get(self, key, default=None):
|
|||
|
|
"""获取基线值"""
|
|||
|
|
return self.baseline.get(key, default)
|
|||
|
|
|
|||
|
|
def _default_baseline(self):
|
|||
|
|
"""默认基线(群体均值)"""
|
|||
|
|
return {
|
|||
|
|
'rmssd': 35.0, # 健康老年人典型值
|
|||
|
|
'sdnn': 50.0,
|
|||
|
|
'mean_hr': 70.0,
|
|||
|
|
'bathroom_freq': 1.5,
|
|||
|
|
'history': []
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# ==================== 2. BCG信号处理层 ====================
|
|||
|
|
|
|||
|
|
class BCGProcessor:
|
|||
|
|
"""BCG信号实时处理器(ARM优化)"""
|
|||
|
|
|
|||
|
|
def __init__(self, config):
|
|||
|
|
self.config = config
|
|||
|
|
self.fs = config.bcg_config['sampling_rate']
|
|||
|
|
self.hr_range = config.bcg_config['hr_range']
|
|||
|
|
self.rr_range = config.bcg_config['rr_range']
|
|||
|
|
|
|||
|
|
# 预先设计滤波器(避免重复计算)
|
|||
|
|
self.hr_filter = self._design_hr_filter()
|
|||
|
|
self.rr_filter = self._design_rr_filter()
|
|||
|
|
|
|||
|
|
def _design_hr_filter(self):
|
|||
|
|
"""设计心率带通滤波器(0.8-3.0 Hz)"""
|
|||
|
|
nyq = self.fs / 2
|
|||
|
|
low = 0.8 / nyq
|
|||
|
|
high = 3.0 / nyq
|
|||
|
|
b, a = signal.butter(4, [low, high], btype='band')
|
|||
|
|
return b, a
|
|||
|
|
|
|||
|
|
def _design_rr_filter(self):
|
|||
|
|
"""设计呼吸率带通滤波器(0.1-0.6 Hz)"""
|
|||
|
|
nyq = self.fs / 2
|
|||
|
|
low = 0.1 / nyq
|
|||
|
|
high = 0.6 / nyq
|
|||
|
|
b, a = signal.butter(4, [low, high], btype='band')
|
|||
|
|
return b, a
|
|||
|
|
|
|||
|
|
def assess_signal_quality(self, bcg_signal):
|
|||
|
|
"""
|
|||
|
|
快速信号质量评估(<50ms)
|
|||
|
|
|
|||
|
|
返回:质量分数(0-1)
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
# 1. SNR评估(40%权重)
|
|||
|
|
signal_power = np.mean(bcg_signal ** 2)
|
|||
|
|
noise_estimate = np.var(np.diff(bcg_signal))
|
|||
|
|
snr = signal_power / (noise_estimate + 1e-6)
|
|||
|
|
snr_score = np.clip(snr / 10.0, 0, 1)
|
|||
|
|
|
|||
|
|
# 2. 频谱集中度(30%权重)
|
|||
|
|
freqs, psd = signal.welch(bcg_signal, fs=self.fs, nperseg=min(512, len(bcg_signal)))
|
|||
|
|
hr_band_mask = (freqs >= 0.8) & (freqs <= 3.0)
|
|||
|
|
hr_band_power = np.sum(psd[hr_band_mask])
|
|||
|
|
total_power = np.sum(psd)
|
|||
|
|
spectrum_score = hr_band_power / (total_power + 1e-6)
|
|||
|
|
|
|||
|
|
# 3. 周期性评估(30%权重)
|
|||
|
|
autocorr = np.correlate(bcg_signal - np.mean(bcg_signal),
|
|||
|
|
bcg_signal - np.mean(bcg_signal),
|
|||
|
|
mode='same')
|
|||
|
|
autocorr = autocorr[len(autocorr)//2:]
|
|||
|
|
peak_pos = np.argmax(autocorr[int(0.3*self.fs):int(1.5*self.fs)]) + int(0.3*self.fs)
|
|||
|
|
periodicity_score = autocorr[peak_pos] / (autocorr[0] + 1e-6)
|
|||
|
|
periodicity_score = np.clip(periodicity_score, 0, 1)
|
|||
|
|
|
|||
|
|
# 综合评分
|
|||
|
|
quality = (0.40 * snr_score +
|
|||
|
|
0.30 * spectrum_score +
|
|||
|
|
0.30 * periodicity_score)
|
|||
|
|
|
|||
|
|
return float(np.clip(quality, 0, 1))
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
return 0.0
|
|||
|
|
|
|||
|
|
def extract_heart_rate(self, bcg_signal):
|
|||
|
|
"""
|
|||
|
|
实时心率提取(<100ms)
|
|||
|
|
|
|||
|
|
返回:(心率bpm, 置信度, RR间期数组ms)
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
# 1. 带通滤波
|
|||
|
|
filtered = signal.filtfilt(self.hr_filter[0], self.hr_filter[1], bcg_signal)
|
|||
|
|
|
|||
|
|
# 2. 峰值检测(优化参数)
|
|||
|
|
peaks, properties = signal.find_peaks(
|
|||
|
|
filtered,
|
|||
|
|
distance=int(0.4 * self.fs), # 最小峰间距(150bpm对应0.4秒)
|
|||
|
|
prominence=0.3 * np.std(filtered)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if len(peaks) < 5:
|
|||
|
|
return None, 0.0, None
|
|||
|
|
|
|||
|
|
# 3. 计算RR间期
|
|||
|
|
rr_intervals = np.diff(peaks) / self.fs * 1000 # 转换为ms
|
|||
|
|
|
|||
|
|
# 4. 异常值过滤(MAD方法,快速)
|
|||
|
|
median_rr = np.median(rr_intervals)
|
|||
|
|
mad = np.median(np.abs(rr_intervals - median_rr))
|
|||
|
|
valid_mask = np.abs(rr_intervals - median_rr) < 3 * mad
|
|||
|
|
|
|||
|
|
if np.sum(valid_mask) < 5:
|
|||
|
|
return None, 0.0, None
|
|||
|
|
|
|||
|
|
clean_rr = rr_intervals[valid_mask]
|
|||
|
|
|
|||
|
|
# 5. 计算心率
|
|||
|
|
mean_rr_ms = np.mean(clean_rr)
|
|||
|
|
heart_rate = 60000.0 / mean_rr_ms
|
|||
|
|
|
|||
|
|
# 6. 合理性检查
|
|||
|
|
if not (self.hr_range[0] <= heart_rate <= self.hr_range[1]):
|
|||
|
|
return None, 0.0, None
|
|||
|
|
|
|||
|
|
# 7. 置信度评估(基于变异系数)
|
|||
|
|
cv = np.std(clean_rr) / np.mean(clean_rr)
|
|||
|
|
confidence = np.clip(1.0 - cv, 0, 1)
|
|||
|
|
|
|||
|
|
return float(heart_rate), float(confidence), clean_rr
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
return None, 0.0, None
|
|||
|
|
|
|||
|
|
def calculate_hrv_features(self, rr_intervals):
|
|||
|
|
"""
|
|||
|
|
快速HRV特征计算(<20ms)
|
|||
|
|
|
|||
|
|
返回:{'rmssd': float, 'sdnn': float, 'pnn50': float}
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
if len(rr_intervals) < 10:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
# 时域指标
|
|||
|
|
sdnn = np.std(rr_intervals)
|
|||
|
|
|
|||
|
|
diff_rr = np.diff(rr_intervals)
|
|||
|
|
rmssd = np.sqrt(np.mean(diff_rr ** 2))
|
|||
|
|
|
|||
|
|
nn50 = np.sum(np.abs(diff_rr) > 50)
|
|||
|
|
pnn50 = nn50 / len(diff_rr) if len(diff_rr) > 0 else 0
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
'sdnn': float(sdnn),
|
|||
|
|
'rmssd': float(rmssd),
|
|||
|
|
'pnn50': float(pnn50)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def extract_respiratory_rate(self, bcg_signal):
|
|||
|
|
"""
|
|||
|
|
实时呼吸率提取(<80ms)
|
|||
|
|
|
|||
|
|
返回:(呼吸率/min, 置信度)
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
# 1. 带通滤波
|
|||
|
|
filtered = signal.filtfilt(self.rr_filter[0], self.rr_filter[1], bcg_signal)
|
|||
|
|
|
|||
|
|
# 2. Welch功率谱估计
|
|||
|
|
freqs, psd = signal.welch(filtered, fs=self.fs,
|
|||
|
|
nperseg=min(512, len(filtered)),
|
|||
|
|
noverlap=256)
|
|||
|
|
|
|||
|
|
# 3. 限制在呼吸频率范围
|
|||
|
|
rr_band_mask = (freqs >= 0.1) & (freqs <= 0.6)
|
|||
|
|
rr_freqs = freqs[rr_band_mask]
|
|||
|
|
rr_psd = psd[rr_band_mask]
|
|||
|
|
|
|||
|
|
if len(rr_psd) == 0:
|
|||
|
|
return None, 0.0
|
|||
|
|
|
|||
|
|
# 4. 找峰值
|
|||
|
|
peak_idx = np.argmax(rr_psd)
|
|||
|
|
peak_freq = rr_freqs[peak_idx]
|
|||
|
|
respiratory_rate = peak_freq * 60 # 转换为/min
|
|||
|
|
|
|||
|
|
# 5. 合理性检查
|
|||
|
|
if not (self.rr_range[0] <= respiratory_rate <= self.rr_range[1]):
|
|||
|
|
return None, 0.0
|
|||
|
|
|
|||
|
|
# 6. 置信度评估(峰值突出度)
|
|||
|
|
peak_power = rr_psd[peak_idx]
|
|||
|
|
mean_power = np.mean(rr_psd)
|
|||
|
|
confidence = np.clip(peak_power / (mean_power * 3), 0, 1)
|
|||
|
|
|
|||
|
|
return float(respiratory_rate), float(confidence)
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
return None, 0.0
|
|||
|
|
|
|||
|
|
# ==================== 3. 雷达信号处理层 ====================
|
|||
|
|
|
|||
|
|
class RadarProcessor:
|
|||
|
|
"""雷达信号实时处理器"""
|
|||
|
|
|
|||
|
|
def __init__(self, config):
|
|||
|
|
self.config = config
|
|||
|
|
self.bathroom_zone = config.radar_config['bathroom_zone']
|
|||
|
|
self.presence_history = deque(maxlen=3) # 时序平滑(3帧)
|
|||
|
|
self.current_visit_start = None
|
|||
|
|
|
|||
|
|
def detect_presence_in_bathroom(self, point_cloud):
|
|||
|
|
"""
|
|||
|
|
实时存在检测(<30ms)
|
|||
|
|
|
|||
|
|
输入:point_cloud = [(x, y, z, velocity), ...]
|
|||
|
|
返回:(是否存在, 置信度)
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
if len(point_cloud) == 0:
|
|||
|
|
self.presence_history.append(False)
|
|||
|
|
return False, 0.0
|
|||
|
|
|
|||
|
|
# 1. 静态杂波去除
|
|||
|
|
dynamic_points = [p for p in point_cloud if abs(p[3]) > 0.05]
|
|||
|
|
|
|||
|
|
# 2. 空间过滤(在厕所区域内)
|
|||
|
|
bathroom_points = []
|
|||
|
|
for p in dynamic_points:
|
|||
|
|
x, y, z = p[0], p[1], p[2]
|
|||
|
|
if (self.bathroom_zone['x_range'][0] <= x <= self.bathroom_zone['x_range'][1] and
|
|||
|
|
self.bathroom_zone['y_range'][0] <= y <= self.bathroom_zone['y_range'][1] and
|
|||
|
|
self.bathroom_zone['z_range'][0] <= z <= self.bathroom_zone['z_range'][1]):
|
|||
|
|
bathroom_points.append(p)
|
|||
|
|
|
|||
|
|
# 3. 密度判断
|
|||
|
|
point_density = len(bathroom_points) / (len(point_cloud) + 1e-6)
|
|||
|
|
is_present = point_density > self.config.radar_config['presence_threshold']
|
|||
|
|
|
|||
|
|
# 4. 时序平滑(连续3帧确认)
|
|||
|
|
self.presence_history.append(is_present)
|
|||
|
|
stable_presence = sum(self.presence_history) >= 2 # 至少2/3帧检测到
|
|||
|
|
|
|||
|
|
confidence = point_density if stable_presence else 0.0
|
|||
|
|
|
|||
|
|
return stable_presence, float(confidence)
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
return False, 0.0
|
|||
|
|
|
|||
|
|
def detect_bathroom_visit(self, timestamp, is_present):
|
|||
|
|
"""
|
|||
|
|
起夜事件检测(状态机)
|
|||
|
|
|
|||
|
|
返回:SensorEvent对象(如果访问结束)或 None
|
|||
|
|
"""
|
|||
|
|
min_duration = self.config.radar_config['min_visit_duration']
|
|||
|
|
|
|||
|
|
if is_present and self.current_visit_start is None:
|
|||
|
|
# 开始起夜
|
|||
|
|
self.current_visit_start = timestamp
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
elif not is_present and self.current_visit_start is not None:
|
|||
|
|
# 结束起夜
|
|||
|
|
duration = timestamp - self.current_visit_start
|
|||
|
|
|
|||
|
|
if duration >= min_duration:
|
|||
|
|
# 有效起夜
|
|||
|
|
event = SensorEvent(
|
|||
|
|
timestamp=self.current_visit_start,
|
|||
|
|
event_type='BATHROOM_VISIT',
|
|||
|
|
data={
|
|||
|
|
'start': self.current_visit_start,
|
|||
|
|
'end': timestamp,
|
|||
|
|
'duration': duration
|
|||
|
|
},
|
|||
|
|
quality=1.0
|
|||
|
|
)
|
|||
|
|
self.current_visit_start = None
|
|||
|
|
return event
|
|||
|
|
else:
|
|||
|
|
# 太短,忽略
|
|||
|
|
self.current_visit_start = None
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def extract_vital_signs_from_radar(self, phase_signal, fs=20):
|
|||
|
|
"""
|
|||
|
|
从雷达相位信号提取生理信号(起夜期间)
|
|||
|
|
|
|||
|
|
返回:{'hr': float, 'rr': float}
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
if len(phase_signal) < fs * 10: # 至少10秒数据
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
# 心率提取(FFT)
|
|||
|
|
freqs, psd = signal.welch(phase_signal, fs=fs, nperseg=min(256, len(phase_signal)))
|
|||
|
|
hr_mask = (freqs >= 0.8) & (freqs <= 2.5)
|
|||
|
|
hr_freqs = freqs[hr_mask]
|
|||
|
|
hr_psd = psd[hr_mask]
|
|||
|
|
|
|||
|
|
hr_peak_idx = np.argmax(hr_psd)
|
|||
|
|
hr = hr_freqs[hr_peak_idx] * 60
|
|||
|
|
|
|||
|
|
# 呼吸率提取
|
|||
|
|
rr_mask = (freqs >= 0.15) & (freqs <= 0.5)
|
|||
|
|
rr_freqs = freqs[rr_mask]
|
|||
|
|
rr_psd = psd[rr_mask]
|
|||
|
|
|
|||
|
|
rr_peak_idx = np.argmax(rr_psd)
|
|||
|
|
rr = rr_freqs[rr_peak_idx] * 60
|
|||
|
|
|
|||
|
|
# 合理性检查
|
|||
|
|
if 40 <= hr <= 130 and 6 <= rr <= 30:
|
|||
|
|
return {'hr': float(hr), 'rr': float(rr)}
|
|||
|
|
else:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
# ==================== 4. 实时风险评估引擎 ====================
|
|||
|
|
|
|||
|
|
class RealtimeRiskEngine:
|
|||
|
|
"""实时风险评估引擎(硬规则,<10ms)"""
|
|||
|
|
|
|||
|
|
def __init__(self, config, baseline_cache):
|
|||
|
|
self.config = config
|
|||
|
|
self.baseline = baseline_cache
|
|||
|
|
self.risk_cfg = config.risk_config
|
|||
|
|
|
|||
|
|
def calculate_risk_score(self, features):
|
|||
|
|
"""
|
|||
|
|
快速风险评分(硬规则)
|
|||
|
|
|
|||
|
|
输入:features = {
|
|||
|
|
'rmssd': float,
|
|||
|
|
'sdnn': float,
|
|||
|
|
'mean_hr': float,
|
|||
|
|
'hr_surge': float, # 起夜时心率上升
|
|||
|
|
'bathroom_freq_today': int
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
返回:(总风险分数0-1, 风险等级, 分项风险)
|
|||
|
|
"""
|
|||
|
|
weights = self.risk_cfg['weights']
|
|||
|
|
|
|||
|
|
# 1. RMSSD风险(35%权重)
|
|||
|
|
rmssd = features.get('rmssd', 0)
|
|||
|
|
rmssd_baseline = self.baseline.get('rmssd', 35.0)
|
|||
|
|
|
|||
|
|
if rmssd < self.risk_cfg['rmssd_high_risk']:
|
|||
|
|
rmssd_risk = 1.0
|
|||
|
|
elif rmssd < self.risk_cfg['rmssd_medium_risk']:
|
|||
|
|
rmssd_risk = 0.6
|
|||
|
|
elif rmssd < rmssd_baseline:
|
|||
|
|
rmssd_risk = 0.4
|
|||
|
|
else:
|
|||
|
|
rmssd_risk = 0.2
|
|||
|
|
|
|||
|
|
# 2. SDNN风险(15%权重)
|
|||
|
|
sdnn = features.get('sdnn', 0)
|
|||
|
|
if sdnn < 30:
|
|||
|
|
sdnn_risk = 1.0
|
|||
|
|
elif sdnn < 45:
|
|||
|
|
sdnn_risk = 0.6
|
|||
|
|
else:
|
|||
|
|
sdnn_risk = 0.2
|
|||
|
|
|
|||
|
|
# 3. 心率水平风险(20%权重)
|
|||
|
|
hr = features.get('mean_hr', 70)
|
|||
|
|
hr_baseline = self.baseline.get('mean_hr', 70.0)
|
|||
|
|
|
|||
|
|
if hr > self.risk_cfg['hr_high_risk']:
|
|||
|
|
hr_risk = 1.0
|
|||
|
|
elif hr > self.risk_cfg['hr_medium_risk']:
|
|||
|
|
hr_risk = 0.6
|
|||
|
|
elif hr > hr_baseline + 5:
|
|||
|
|
hr_risk = 0.4
|
|||
|
|
else:
|
|||
|
|
hr_risk = 0.2
|
|||
|
|
|
|||
|
|
# 4. 起夜心率上升风险(10%权重)
|
|||
|
|
hr_surge = features.get('hr_surge', 0)
|
|||
|
|
if hr_surge > self.risk_cfg['hr_surge_high_risk']:
|
|||
|
|
hr_surge_risk = 1.0
|
|||
|
|
elif hr_surge > self.risk_cfg['hr_surge_medium_risk']:
|
|||
|
|
hr_surge_risk = 0.6
|
|||
|
|
else:
|
|||
|
|
hr_surge_risk = 0.3
|
|||
|
|
|
|||
|
|
# 5. 起夜频率风险(20%权重)
|
|||
|
|
bathroom_freq = features.get('bathroom_freq_today', 0)
|
|||
|
|
bathroom_baseline = self.baseline.get('bathroom_freq', 1.5)
|
|||
|
|
|
|||
|
|
if bathroom_freq >= self.risk_cfg['bathroom_high_risk']:
|
|||
|
|
bathroom_risk = 1.0
|
|||
|
|
elif bathroom_freq >= self.risk_cfg['bathroom_medium_risk']:
|
|||
|
|
bathroom_risk = 0.6
|
|||
|
|
elif bathroom_freq > bathroom_baseline:
|
|||
|
|
bathroom_risk = 0.4
|
|||
|
|
else:
|
|||
|
|
bathroom_risk = 0.2
|
|||
|
|
|
|||
|
|
# 6. 加权综合
|
|||
|
|
total_risk = (
|
|||
|
|
weights['rmssd'] * rmssd_risk +
|
|||
|
|
weights['sdnn'] * sdnn_risk +
|
|||
|
|
weights['hr_level'] * hr_risk +
|
|||
|
|
weights['hr_surge'] * hr_surge_risk +
|
|||
|
|
weights['bathroom_freq'] * bathroom_risk
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 7. 风险等级分类
|
|||
|
|
if total_risk >= self.config.alert_config['high_risk_threshold']:
|
|||
|
|
risk_level = 'HIGH'
|
|||
|
|
elif total_risk >= self.config.alert_config['medium_risk_threshold']:
|
|||
|
|
risk_level = 'MEDIUM'
|
|||
|
|
else:
|
|||
|
|
risk_level = 'LOW'
|
|||
|
|
|
|||
|
|
# 8. 分项风险
|
|||
|
|
breakdown = {
|
|||
|
|
'rmssd_risk': rmssd_risk,
|
|||
|
|
'sdnn_risk': sdnn_risk,
|
|||
|
|
'hr_risk': hr_risk,
|
|||
|
|
'hr_surge_risk': hr_surge_risk,
|
|||
|
|
'bathroom_risk': bathroom_risk
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return float(total_risk), risk_level, breakdown
|
|||
|
|
|
|||
|
|
def should_trigger_alert(self, risk_score, risk_level, last_alert_time):
|
|||
|
|
"""
|
|||
|
|
判断是否触发预警
|
|||
|
|
|
|||
|
|
返回:(是否预警, 预警消息)
|
|||
|
|
"""
|
|||
|
|
# 冷却时间检查
|
|||
|
|
if last_alert_time is not None:
|
|||
|
|
cooldown = self.config.alert_config['alert_cooldown']
|
|||
|
|
if time.time() - last_alert_time < cooldown:
|
|||
|
|
return False, None
|
|||
|
|
|
|||
|
|
# 只对高风险预警
|
|||
|
|
if risk_level == 'HIGH':
|
|||
|
|
message = f"⚠️ 高血压风险预警!风险评分: {risk_score:.2f}"
|
|||
|
|
return True, message
|
|||
|
|
|
|||
|
|
return False, None
|
|||
|
|
|
|||
|
|
# ==================== 5. 云端通信层 ====================
|
|||
|
|
|
|||
|
|
class CloudUploader:
|
|||
|
|
"""云端数据上传器(匿名化)"""
|
|||
|
|
|
|||
|
|
def __init__(self, config):
|
|||
|
|
self.config = config
|
|||
|
|
self.upload_buffer = []
|
|||
|
|
self.last_upload_time = 0
|
|||
|
|
|
|||
|
|
def add_to_buffer(self, event):
|
|||
|
|
"""添加事件到上传缓冲区"""
|
|||
|
|
if not self.config.cloud_config['enable_upload']:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 匿名化
|
|||
|
|
if self.config.cloud_config['anonymize']:
|
|||
|
|
event = event.anonymize()
|
|||
|
|
|
|||
|
|
self.upload_buffer.append(event.to_dict())
|
|||
|
|
|
|||
|
|
def should_upload(self):
|
|||
|
|
"""判断是否应该上传"""
|
|||
|
|
if not self.config.cloud_config['enable_upload']:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
interval = self.config.cloud_config['upload_interval']
|
|||
|
|
return (time.time() - self.last_upload_time) >= interval
|
|||
|
|
|
|||
|
|
def upload(self):
|
|||
|
|
"""
|
|||
|
|
上传数据到云端
|
|||
|
|
|
|||
|
|
返回:是否成功
|
|||
|
|
"""
|
|||
|
|
if len(self.upload_buffer) == 0:
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# 构建上传数据包
|
|||
|
|
payload = {
|
|||
|
|
'device_id': config.device_id if not self.config.cloud_config['anonymize'] else 'anonymous',
|
|||
|
|
'timestamp': time.time(),
|
|||
|
|
'events': self.upload_buffer,
|
|||
|
|
'version': config.version
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 这里应该调用实际的HTTP上传接口
|
|||
|
|
# import requests
|
|||
|
|
# response = requests.post(
|
|||
|
|
# self.config.cloud_config['upload_url'],
|
|||
|
|
# json=payload,
|
|||
|
|
# headers={'Authorization': f"Bearer {self.config.cloud_config['api_key']}"}
|
|||
|
|
# )
|
|||
|
|
# success = response.status_code == 200
|
|||
|
|
|
|||
|
|
# 模拟上传成功
|
|||
|
|
print(f" ☁️ [Cloud Upload] Uploaded {len(self.upload_buffer)} events to cloud")
|
|||
|
|
|
|||
|
|
# 清空缓冲区
|
|||
|
|
self.upload_buffer = []
|
|||
|
|
self.last_upload_time = time.time()
|
|||
|
|
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f" ❌ [Cloud Upload] Failed: {e}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
# ==================== 6. 实时处理主控制器 ====================
|
|||
|
|
|
|||
|
|
class RealtimeController:
|
|||
|
|
"""实时处理主控制器"""
|
|||
|
|
|
|||
|
|
def __init__(self, config):
|
|||
|
|
self.config = config
|
|||
|
|
|
|||
|
|
# 初始化组件
|
|||
|
|
self.bcg_processor = BCGProcessor(config)
|
|||
|
|
self.radar_processor = RadarProcessor(config)
|
|||
|
|
self.baseline_cache = BaselineCache(
|
|||
|
|
os.path.join(config.cache_config['cache_dir'], config.cache_config['baseline_file'])
|
|||
|
|
)
|
|||
|
|
self.risk_engine = RealtimeRiskEngine(config, self.baseline_cache)
|
|||
|
|
self.cloud_uploader = CloudUploader(config)
|
|||
|
|
|
|||
|
|
# 数据缓冲区
|
|||
|
|
buffer_size = config.bcg_config['window_size'] * config.bcg_config['sampling_rate']
|
|||
|
|
self.bcg_buffer = RingBuffer(buffer_size)
|
|||
|
|
|
|||
|
|
# 状态变量
|
|||
|
|
self.last_process_time = 0
|
|||
|
|
self.last_alert_time = None
|
|||
|
|
self.last_log_time = 0
|
|||
|
|
self.bathroom_visits_today = []
|
|||
|
|
self.night_start_time = None
|
|||
|
|
|
|||
|
|
# 统计变量
|
|||
|
|
self.stats = {
|
|||
|
|
'total_processed': 0,
|
|||
|
|
'total_alerts': 0,
|
|||
|
|
'avg_latency': 0.0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print(f"{'='*70}")
|
|||
|
|
print(f"🚀 边缘端实时处理系统初始化成功")
|
|||
|
|
print(f"{'='*70}")
|
|||
|
|
print(f"版本: {config.version}")
|
|||
|
|
print(f"设备: {config.device_id}")
|
|||
|
|
print(f"BCG采样率: {config.bcg_config['sampling_rate']} Hz")
|
|||
|
|
print(f"处理窗口: {config.bcg_config['window_size']} 秒")
|
|||
|
|
print(f"云端上传: {'启用' if config.cloud_config['enable_upload'] else '禁用'}")
|
|||
|
|
print(f"{'='*70}")
|
|||
|
|
|
|||
|
|
def process_bcg_sample(self, sample, timestamp):
|
|||
|
|
"""
|
|||
|
|
处理单个BCG样本点(流式输入)
|
|||
|
|
|
|||
|
|
输入:
|
|||
|
|
- sample: float(BCG信号值)
|
|||
|
|
- timestamp: float(Unix时间戳)
|
|||
|
|
"""
|
|||
|
|
# 添加到缓冲区
|
|||
|
|
self.bcg_buffer.append(sample)
|
|||
|
|
|
|||
|
|
# 检查是否应该处理
|
|||
|
|
interval = self.config.realtime_config['processing_interval']
|
|||
|
|
if time.time() - self.last_process_time < interval:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 检查缓冲区是否已满
|
|||
|
|
if not self.bcg_buffer.is_full():
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 执行处理
|
|||
|
|
self._process_bcg_window(timestamp)
|
|||
|
|
self.last_process_time = time.time()
|
|||
|
|
|
|||
|
|
def process_radar_frame(self, point_cloud, timestamp):
|
|||
|
|
"""
|
|||
|
|
处理雷达点云帧
|
|||
|
|
|
|||
|
|
输入:
|
|||
|
|
- point_cloud: [(x, y, z, velocity), ...]
|
|||
|
|
- timestamp: float
|
|||
|
|
"""
|
|||
|
|
# 存在检测
|
|||
|
|
is_present, confidence = self.radar_processor.detect_presence_in_bathroom(point_cloud)
|
|||
|
|
|
|||
|
|
# 起夜事件检测
|
|||
|
|
visit_event = self.radar_processor.detect_bathroom_visit(timestamp, is_present)
|
|||
|
|
|
|||
|
|
if visit_event is not None:
|
|||
|
|
# 记录起夜事件
|
|||
|
|
self.bathroom_visits_today.append(visit_event)
|
|||
|
|
|
|||
|
|
# 上传到云端
|
|||
|
|
self.cloud_uploader.add_to_buffer(visit_event)
|
|||
|
|
|
|||
|
|
# 打印日志
|
|||
|
|
duration = visit_event.data['duration']
|
|||
|
|
print(f" 🚽 [Bathroom Visit] Duration: {duration:.0f}s | Total today: {len(self.bathroom_visits_today)}")
|
|||
|
|
|
|||
|
|
# 定期上传
|
|||
|
|
if self.cloud_uploader.should_upload():
|
|||
|
|
self.cloud_uploader.upload()
|
|||
|
|
|
|||
|
|
def _process_bcg_window(self, timestamp):
|
|||
|
|
"""处理BCG窗口数据"""
|
|||
|
|
start_time = time.time()
|
|||
|
|
|
|||
|
|
# 获取窗口数据
|
|||
|
|
bcg_signal = self.bcg_buffer.get_data()
|
|||
|
|
|
|||
|
|
# 1. 质量评估
|
|||
|
|
quality = self.bcg_processor.assess_signal_quality(bcg_signal)
|
|||
|
|
|
|||
|
|
# 检查是否离床
|
|||
|
|
if quality < self.config.bcg_config['离bed_threshold']:
|
|||
|
|
if self.config.realtime_config['enable_logging']:
|
|||
|
|
if time.time() - self.last_log_time > 60:
|
|||
|
|
print(f" ⚠️ [BCG Quality] Low quality (离床): {quality:.2f}")
|
|||
|
|
self.last_log_time = time.time()
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 2. 心率提取
|
|||
|
|
hr, hr_conf, rr_intervals = self.bcg_processor.extract_heart_rate(bcg_signal)
|
|||
|
|
|
|||
|
|
if hr is None:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 3. HRV特征
|
|||
|
|
hrv_features = None
|
|||
|
|
if rr_intervals is not None and len(rr_intervals) >= 10:
|
|||
|
|
hrv_features = self.bcg_processor.calculate_hrv_features(rr_intervals)
|
|||
|
|
|
|||
|
|
# 4. 呼吸率提取
|
|||
|
|
rr, rr_conf = self.bcg_processor.extract_respiratory_rate(bcg_signal)
|
|||
|
|
|
|||
|
|
# 5. 风险评估
|
|||
|
|
if hrv_features is not None:
|
|||
|
|
features = {
|
|||
|
|
'rmssd': hrv_features['rmssd'],
|
|||
|
|
'sdnn': hrv_features['sdnn'],
|
|||
|
|
'mean_hr': hr,
|
|||
|
|
'hr_surge': 0, # 暂时没有起夜心率数据
|
|||
|
|
'bathroom_freq_today': len(self.bathroom_visits_today)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
risk_score, risk_level, breakdown = self.risk_engine.calculate_risk_score(features)
|
|||
|
|
|
|||
|
|
# 6. 预警判断
|
|||
|
|
should_alert, alert_msg = self.risk_engine.should_trigger_alert(
|
|||
|
|
risk_score, risk_level, self.last_alert_time
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if should_alert:
|
|||
|
|
self._trigger_alert(alert_msg, risk_score, features)
|
|||
|
|
self.last_alert_time = time.time()
|
|||
|
|
|
|||
|
|
# 7. 创建事件并上传
|
|||
|
|
event = SensorEvent(
|
|||
|
|
timestamp=timestamp,
|
|||
|
|
event_type='BCG_REALTIME',
|
|||
|
|
data={
|
|||
|
|
'hr': hr,
|
|||
|
|
'hr_confidence': hr_conf,
|
|||
|
|
'rr': rr if rr else 0,
|
|||
|
|
'rmssd': hrv_features['rmssd'],
|
|||
|
|
'sdnn': hrv_features['sdnn'],
|
|||
|
|
'pnn50': hrv_features['pnn50'],
|
|||
|
|
'risk_score': risk_score,
|
|||
|
|
'risk_level': risk_level,
|
|||
|
|
'quality': quality
|
|||
|
|
},
|
|||
|
|
quality=quality
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
self.cloud_uploader.add_to_buffer(event)
|
|||
|
|
|
|||
|
|
# 8. 打印日志
|
|||
|
|
if self.config.realtime_config['enable_logging']:
|
|||
|
|
if time.time() - self.last_log_time > self.config.realtime_config['log_interval']:
|
|||
|
|
latency = (time.time() - start_time) * 1000
|
|||
|
|
self._print_status(hr, hrv_features, risk_score, risk_level, quality, latency)
|
|||
|
|
self.last_log_time = time.time()
|
|||
|
|
|
|||
|
|
# 更新统计
|
|||
|
|
self.stats['total_processed'] += 1
|
|||
|
|
latency = (time.time() - start_time) * 1000
|
|||
|
|
self.stats['avg_latency'] = (self.stats['avg_latency'] * 0.9 + latency * 0.1)
|
|||
|
|
|
|||
|
|
def _trigger_alert(self, message, risk_score, features):
|
|||
|
|
"""触发预警"""
|
|||
|
|
self.stats['total_alerts'] += 1
|
|||
|
|
|
|||
|
|
print(f"{'='*70}")
|
|||
|
|
print(f"⚠️ 警报触发 #{self.stats['total_alerts']}")
|
|||
|
|
print(f"{'='*70}")
|
|||
|
|
print(message)
|
|||
|
|
print(f"详细信息:")
|
|||
|
|
print(f" 心率: {features['mean_hr']:.1f} bpm")
|
|||
|
|
print(f" RMSSD: {features['rmssd']:.1f} ms")
|
|||
|
|
print(f" SDNN: {features['sdnn']:.1f} ms")
|
|||
|
|
print(f" 起夜次数(今晚): {features['bathroom_freq_today']}")
|
|||
|
|
print(f" 风险评分: {risk_score:.3f}")
|
|||
|
|
print(f"{'='*70}")
|
|||
|
|
|
|||
|
|
# 这里可以添加实际的预警机制(声音、推送等)
|
|||
|
|
if self.config.alert_config['enable_sound']:
|
|||
|
|
# 播放警报声音
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
if self.config.alert_config['enable_push']:
|
|||
|
|
# 发送推送通知
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def _print_status(self, hr, hrv_features, risk_score, risk_level, quality, latency):
|
|||
|
|
"""打印状态信息"""
|
|||
|
|
print(f"{'='*70}")
|
|||
|
|
print(f"📊 实时状态更新 - {datetime.now().strftime('%H:%M:%S')}")
|
|||
|
|
print(f"{'='*70}")
|
|||
|
|
print(f"生理指标:")
|
|||
|
|
print(f" 心率: {hr:.1f} bpm")
|
|||
|
|
print(f" RMSSD: {hrv_features['rmssd']:.1f} ms")
|
|||
|
|
print(f" SDNN: {hrv_features['sdnn']:.1f} ms")
|
|||
|
|
print(f" pNN50: {hrv_features['pnn50']:.3f}")
|
|||
|
|
print(f"行为指标:")
|
|||
|
|
print(f" 起夜次数(今晚): {len(self.bathroom_visits_today)}")
|
|||
|
|
print(f"风险评估:")
|
|||
|
|
print(f" 风险评分: {risk_score:.3f}")
|
|||
|
|
print(f" 风险等级: {risk_level}")
|
|||
|
|
print(f" 信号质量: {quality:.2f}")
|
|||
|
|
print(f"系统性能:")
|
|||
|
|
print(f" 处理延迟: {latency:.1f} ms")
|
|||
|
|
print(f" 累计处理: {self.stats['total_processed']} 次")
|
|||
|
|
print(f" 累计预警: {self.stats['total_alerts']} 次")
|
|||
|
|
print(f"{'='*70}")
|
|||
|
|
|
|||
|
|
def end_of_night_summary(self):
|
|||
|
|
"""夜间结束总结"""
|
|||
|
|
print(f"{'='*70}")
|
|||
|
|
print(f"🌙 夜间监测总结")
|
|||
|
|
print(f"{'='*70}")
|
|||
|
|
print(f"监测时长: {(time.time() - self.night_start_time)/3600:.1f} 小时")
|
|||
|
|
print(f"起夜次数: {len(self.bathroom_visits_today)}")
|
|||
|
|
print(f"触发预警: {self.stats['total_alerts']} 次")
|
|||
|
|
print(f"处理次数: {self.stats['total_processed']} 次")
|
|||
|
|
print(f"平均延迟: {self.stats['avg_latency']:.1f} ms")
|
|||
|
|
print(f"{'='*70}")
|
|||
|
|
|
|||
|
|
# 更新基线(如果有足够数据)
|
|||
|
|
# TODO: 计算整晚特征并更新基线
|
|||
|
|
|
|||
|
|
# 重置当日计数
|
|||
|
|
self.bathroom_visits_today = []
|
|||
|
|
self.stats['total_alerts'] = 0
|
|||
|
|
|
|||
|
|
# ==================== 7. 模拟数据生成器(测试用) ====================
|
|||
|
|
|
|||
|
|
class EdgeDataSimulator:
|
|||
|
|
"""边缘端数据模拟器(用于测试)"""
|
|||
|
|
|
|||
|
|
def __init__(self, config):
|
|||
|
|
self.config = config
|
|||
|
|
|
|||
|
|
def generate_synthetic_bcg_stream(self, duration_sec, hr=70, scenario='normal'):
|
|||
|
|
"""
|
|||
|
|
生成合成BCG数据流
|
|||
|
|
|
|||
|
|
参数:
|
|||
|
|
- duration_sec: 时长(秒)
|
|||
|
|
- hr: 心率(bpm)
|
|||
|
|
- scenario: 'normal', 'high_risk', 'bathroom_visit'
|
|||
|
|
"""
|
|||
|
|
fs = self.config.bcg_config['sampling_rate']
|
|||
|
|
total_samples = int(duration_sec * fs)
|
|||
|
|
|
|||
|
|
t = np.linspace(0, duration_sec, total_samples)
|
|||
|
|
|
|||
|
|
# 基础心跳信号
|
|||
|
|
hr_freq = hr / 60.0
|
|||
|
|
signal_bcg = np.sin(2 * np.pi * hr_freq * t)
|
|||
|
|
|
|||
|
|
# 呼吸信号
|
|||
|
|
rr_freq = 15 / 60.0
|
|||
|
|
signal_bcg += 0.5 * np.sin(2 * np.pi * rr_freq * t)
|
|||
|
|
|
|||
|
|
# 根据场景调整
|
|||
|
|
if scenario == 'high_risk':
|
|||
|
|
# 高风险:降低HRV
|
|||
|
|
signal_bcg += 0.05 * np.random.randn(total_samples)
|
|||
|
|
elif scenario == 'bathroom_visit':
|
|||
|
|
# 起夜:质量下降
|
|||
|
|
signal_bcg = signal_bcg * 0.2 + 0.8 * np.random.randn(total_samples)
|
|||
|
|
else:
|
|||
|
|
# 正常
|
|||
|
|
signal_bcg += 0.1 * np.random.randn(total_samples)
|
|||
|
|
|
|||
|
|
return signal_bcg, t
|
|||
|
|
|
|||
|
|
def generate_synthetic_radar_stream(self, duration_sec, bathroom_visits=[]):
|
|||
|
|
"""
|
|||
|
|
生成合成雷达数据流
|
|||
|
|
|
|||
|
|
参数:
|
|||
|
|
- duration_sec: 时长(秒)
|
|||
|
|
- bathroom_visits: [(start_time, end_time), ...]
|
|||
|
|
"""
|
|||
|
|
fs = self.config.radar_config['sampling_rate']
|
|||
|
|
total_frames = int(duration_sec * fs)
|
|||
|
|
|
|||
|
|
radar_frames = []
|
|||
|
|
|
|||
|
|
for i in range(total_frames):
|
|||
|
|
current_time = i / fs
|
|||
|
|
|
|||
|
|
# 检查是否在起夜时间段
|
|||
|
|
in_bathroom = False
|
|||
|
|
for start, end in bathroom_visits:
|
|||
|
|
if start <= current_time <= end:
|
|||
|
|
in_bathroom = True
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
if in_bathroom:
|
|||
|
|
# 生成厕所区域内的点云
|
|||
|
|
zone = self.config.radar_config['bathroom_zone']
|
|||
|
|
num_points = np.random.randint(10, 30)
|
|||
|
|
point_cloud = []
|
|||
|
|
for _ in range(num_points):
|
|||
|
|
x = np.random.uniform(*zone['x_range'])
|
|||
|
|
y = np.random.uniform(*zone['y_range'])
|
|||
|
|
z = np.random.uniform(*zone['z_range'])
|
|||
|
|
v = np.random.uniform(0.1, 0.5) # 有动作
|
|||
|
|
point_cloud.append((x, y, z, v))
|
|||
|
|
else:
|
|||
|
|
# 生成空点云或随机噪声
|
|||
|
|
point_cloud = []
|
|||
|
|
|
|||
|
|
radar_frames.append((current_time, point_cloud))
|
|||
|
|
|
|||
|
|
return radar_frames
|
|||
|
|
|
|||
|
|
# ==================== 8. 主程序 ====================
|
|||
|
|
|
|||
|
|
def run_realtime_simulation(duration_minutes=10):
|
|||
|
|
"""
|
|||
|
|
运行实时模拟(测试用)
|
|||
|
|
|
|||
|
|
参数:
|
|||
|
|
- duration_minutes: 模拟时长(分钟)
|
|||
|
|
"""
|
|||
|
|
print(f"{'='*70}")
|
|||
|
|
print(f"🎬 启动边缘端实时模拟")
|
|||
|
|
print(f"{'='*70}")
|
|||
|
|
print(f"模拟时长: {duration_minutes} 分钟")
|
|||
|
|
print(f"场景: 正常睡眠 + 2次起夜")
|
|||
|
|
print(f"{'='*70}")
|
|||
|
|
|
|||
|
|
# 初始化控制器
|
|||
|
|
controller = RealtimeController(config)
|
|||
|
|
controller.night_start_time = time.time()
|
|||
|
|
|
|||
|
|
# 初始化模拟器
|
|||
|
|
simulator = EdgeDataSimulator(config)
|
|||
|
|
|
|||
|
|
# 定义起夜时间(相对时间,秒)
|
|||
|
|
bathroom_visits = [
|
|||
|
|
(120, 180), # 2分钟开始,持续1分钟
|
|||
|
|
(420, 480) # 7分钟开始,持续1分钟
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
# 生成BCG数据流
|
|||
|
|
duration_sec = duration_minutes * 60
|
|||
|
|
bcg_signal, bcg_time = simulator.generate_synthetic_bcg_stream(
|
|||
|
|
duration_sec, hr=72, scenario='normal'
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 生成雷达数据流
|
|||
|
|
radar_frames = simulator.generate_synthetic_radar_stream(
|
|||
|
|
duration_sec, bathroom_visits=bathroom_visits
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
print(f"✓ 数据生成完成")
|
|||
|
|
print(f" BCG样本: {len(bcg_signal)}")
|
|||
|
|
print(f" 雷达帧: {len(radar_frames)}")
|
|||
|
|
print(f"开始实时处理...")
|
|||
|
|
|
|||
|
|
# 模拟实时处理
|
|||
|
|
bcg_fs = config.bcg_config['sampling_rate']
|
|||
|
|
radar_fs = config.radar_config['sampling_rate']
|
|||
|
|
|
|||
|
|
bcg_idx = 0
|
|||
|
|
radar_idx = 0
|
|||
|
|
start_time = time.time()
|
|||
|
|
|
|||
|
|
while bcg_idx < len(bcg_signal):
|
|||
|
|
current_time = time.time() - start_time
|
|||
|
|
|
|||
|
|
# 处理BCG样本(更高频率)
|
|||
|
|
if bcg_idx < len(bcg_signal):
|
|||
|
|
sample_time = start_time + bcg_time[bcg_idx]
|
|||
|
|
controller.process_bcg_sample(bcg_signal[bcg_idx], sample_time)
|
|||
|
|
bcg_idx += 1
|
|||
|
|
|
|||
|
|
# 处理雷达帧(较低频率)
|
|||
|
|
expected_radar_idx = int(current_time * radar_fs)
|
|||
|
|
if radar_idx < expected_radar_idx and radar_idx < len(radar_frames):
|
|||
|
|
frame_time, point_cloud = radar_frames[radar_idx]
|
|||
|
|
controller.process_radar_frame(point_cloud, start_time + frame_time)
|
|||
|
|
radar_idx += 1
|
|||
|
|
|
|||
|
|
# 模拟实时间隔(加速模拟)
|
|||
|
|
time.sleep(0.001) # 1ms(实际应该是1/250秒)
|
|||
|
|
|
|||
|
|
# 定期上传
|
|||
|
|
if controller.cloud_uploader.should_upload():
|
|||
|
|
controller.cloud_uploader.upload()
|
|||
|
|
|
|||
|
|
# 夜间总结
|
|||
|
|
controller.end_of_night_summary()
|
|||
|
|
|
|||
|
|
print(f"{'='*70}")
|
|||
|
|
print(f"✅ 模拟完成")
|
|||
|
|
print(f"{'='*70}")
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
"""主函数"""
|
|||
|
|
print(f"{'='*70}")
|
|||
|
|
print(f"🏥 高血压风险评估系统 - 边缘端模块")
|
|||
|
|
print(f"{'='*70}")
|
|||
|
|
print(f"版本: {config.version}")
|
|||
|
|
print(f"设备: {config.device_id}")
|
|||
|
|
print(f"部署模式: 混合部署(边缘实时 + 云端深度分析)")
|
|||
|
|
print(f"{'='*70}")
|
|||
|
|
|
|||
|
|
print("📋 使用说明:")
|
|||
|
|
print("1. 本代码运行在RK3588边缘设备")
|
|||
|
|
print("2. 实时处理BCG和雷达传感器数据")
|
|||
|
|
print("3. 提供<1秒延迟的实时预警")
|
|||
|
|
print("4. 定期上传匿名化数据到云端")
|
|||
|
|
print()
|
|||
|
|
print("🎮 运行模式:")
|
|||
|
|
print(" - 模拟模式:使用合成数据测试系统")
|
|||
|
|
print(" - 实际模式:连接真实传感器(需要硬件接口)")
|
|||
|
|
print()
|
|||
|
|
|
|||
|
|
# 运行模拟
|
|||
|
|
run_realtime_simulation(duration_minutes=10)
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|