feat: 添加MQTT客户端任务,优化数据上报频率控制,移除冗余日志输出
This commit is contained in:
873
docs/emotion_algorithm.md
Normal file
873
docs/emotion_algorithm.md
Normal file
@@ -0,0 +1,873 @@
|
||||
# 情绪分析算法详细文档
|
||||
|
||||
## 一、概述
|
||||
|
||||
本算法基于生理信号(心率、呼吸率、心率变异性、体动数据)进行情绪状态分析,输出主要情绪、次要情绪、情绪强度、效价、唤醒度等多维度指标。
|
||||
|
||||
---
|
||||
|
||||
## 二、情绪类型定义
|
||||
|
||||
| 枚举值 | 情绪类型 | 英文名 |
|
||||
|--------|----------|--------|
|
||||
| 0 | 平静 | CALM |
|
||||
| 1 | 高兴 | HAPPY |
|
||||
| 2 | 兴奋 | EXCITED |
|
||||
| 3 | 焦虑 | ANXIOUS |
|
||||
| 4 | 愤怒 | ANGRY |
|
||||
| 5 | 悲伤 | SAD |
|
||||
| 6 | 压力 | STRESSED |
|
||||
| 7 | 放松 | RELAXED |
|
||||
| 8 | 未知 | UNKNOWN |
|
||||
|
||||
---
|
||||
|
||||
## 三、整体算法流程图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 开始分析 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 输入数据预处理 │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ 心率数据 │ │ 呼吸数据 │ │ HRV数据 │ │ 体动数据 │ │
|
||||
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ └─────────────┴─────────────┴─────────────┘ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────┐ │
|
||||
│ │ 滑动窗口平滑处理 │ │
|
||||
│ │ (WINDOW_SIZE = 15) │ │
|
||||
│ └────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 计算各情绪得分 │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ CalmScore() = 0.45×HR + 0.22×Stability + 0.27×RR │ │
|
||||
│ │ + 0.16×RR_reg + 0.10×HRV + 0.08×Move │ │
|
||||
│ │ │ │
|
||||
│ │ HappyScore() = 0.45×HR + 0.10×Variability + 0.34×RR │ │
|
||||
│ │ + 0.10×HRV + 0.08×Move │ │
|
||||
│ │ │ │
|
||||
│ │ ExcitedScore() = 0.45×HR + 0.10×Trend + 0.34×RR │ │
|
||||
│ │ + 0.10×HRV + 0.10×Move │ │
|
||||
│ │ │ │
|
||||
│ │ AnxiousScore() = 0.36×HR + 0.16×Std + 0.25×(1-HRV) │ │
|
||||
│ │ + 0.10×Stress + 0.16×(1-RR_reg) │ │
|
||||
│ │ + 0.10×Move │ │
|
||||
│ │ │ │
|
||||
│ │ AngryScore() = 0.36×HR + 0.20×Trend + 0.25×(1-HRV) │ │
|
||||
│ │ + 0.20×(1-RR_reg) + 0.10×Move │ │
|
||||
│ │ │ │
|
||||
│ │ SadScore() = 0.40×(-HR) + 0.15×(1-Std) + 0.28×(-RR) │ │
|
||||
│ │ + 0.10×RR_reg + 0.10×(1-HRV) │ │
|
||||
│ │ + 0.10×(1-Move) │ │
|
||||
│ │ │ │
|
||||
│ │ StressedScore()= 0.36×HR + 0.20×Trend + 0.25×(1-HRV) │ │
|
||||
│ │ + 0.10×Stress + 0.16×(1-RR_reg) │ │
|
||||
│ │ + 0.10×Move │ │
|
||||
│ │ │ │
|
||||
│ │ RelaxedScore() = 0.40×(-HR) + 0.22×(1-Std) + 0.16×(-RR) │ │
|
||||
│ │ + 0.10×RR_reg + 0.10×(1-Move) │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 概率归一化 #1 │
|
||||
│ │
|
||||
│ P_i = Score_i / Σ(Score_j), j = 0 to 8 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Top1 概率放大 │
|
||||
│ │
|
||||
│ Score_max = Score_max × 1.3 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 概率归一化 #2 │
|
||||
│ │
|
||||
│ P_i = Score_i / Σ(Score_j), j = 0 to 8 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 自适应平滑处理 │
|
||||
│ │
|
||||
│ diff = |P_i - P_prev_i| │
|
||||
│ α = (diff > 0.2) ? 0.6 : 0.25 │
|
||||
│ P_i = α × P_i + (1-α) × P_prev_i │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 提取 Top1 和 Top2 │
|
||||
│ │
|
||||
│ 遍历找出: maxProb(最大), secondProb(第二大) │
|
||||
│ primaryEmotion = emotion[maxIdx] │
|
||||
│ secondaryEmotion = emotion[secondIdx] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ UNKNOWN 判断 │
|
||||
│ │
|
||||
│ if (maxProb < 0.20 && (maxProb - secondProb) < 0.03) │
|
||||
│ primaryEmotion = UNKNOWN │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 负面情绪合并 │
|
||||
│ │
|
||||
│ if (primaryEmotion ∈ {ANXIOUS, ANGRY, STRESSED}) │
|
||||
│ combinedProb = P[ANXIOUS] + P[ANGRY] + P[STRESSED] │
|
||||
│ primaryEmotion = STRESSED │
|
||||
│ confidence = max(combinedProb, 各负面情绪概率) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 计算情绪强度 │
|
||||
│ │
|
||||
│ hrFactor = |HR - HR_baseline| / 40.0 │
|
||||
│ hrvFactor = 1 - sigmoid(HRV_rmssd, 0.02, 40) │
|
||||
│ rrFactor = |RR - RR_baseline| / 10.0 │
|
||||
│ │
|
||||
│ intensity = 0.4 + 0.3×clamp(hrFactor) │
|
||||
│ + 0.2×clamp(hrvFactor) + 0.1×clamp(rrFactor) │
|
||||
│ │
|
||||
│ intensity = clamp(intensity, 0.3, 1.0) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 计算情绪维度 │
|
||||
│ │
|
||||
│ valence = (P[hAPPY] + P[eXCITED] + P[rELAXED] + P[cALM]) │
|
||||
│ - (P[aNXIOUS] + P[aNGRY] + P[sAD] + P[sTRESSED]) │
|
||||
│ │
|
||||
│ arousal = (P[eXCITED] + P[aNXIOUS] + P[aNGRY]) │
|
||||
│ / (P[eXCITED] + P[aNXIOUS] + P[aNGRY] │
|
||||
│ + P[cALM] + P[rELAXED] + P[sAD]) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ UNKNOWN 强制落地 │
|
||||
│ │
|
||||
│ if (primaryEmotion == UNKNOWN) │
|
||||
│ if (arousal > 0.6) → EXCITED │
|
||||
│ else if (valence < -0.2) → STRESSED │
|
||||
│ else → CALM │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 计算压力水平 │
|
||||
│ │
|
||||
│ stressLevel = f(HRV, HR, RR, 压力指数) │
|
||||
│ anxietyLevel = f(HR, HRV, 呼吸规律性) │
|
||||
│ relaxationLevel = f(HRV, HR, 体动) │
|
||||
│ │
|
||||
│ sympatheticActivity = 1 - autonomicBalance │
|
||||
│ parasympatheticActivity = autonomicBalance │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 输出结果 │
|
||||
│ primaryEmotion, secondaryEmotion, confidence, intensity, │
|
||||
│ valence, arousal, stressLevel, anxietyLevel, │
|
||||
│ relaxationLevel, sympatheticActivity, parasympatheticActivity │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、核心计算函数详解
|
||||
|
||||
### 4.1 Sigmoid 函数
|
||||
|
||||
```cpp
|
||||
float sigmoid(float x, float k, float x0) {
|
||||
return 1.0f / (1.0f + exp(-k * (x - x0)));
|
||||
}
|
||||
```
|
||||
|
||||
**数学公式:**
|
||||
```
|
||||
σ(x; k, x₀) = 1 / (1 + e^(-k(x-x₀)))
|
||||
```
|
||||
|
||||
**参数说明:**
|
||||
- `k`: 斜率参数,控制曲线的陡峭程度
|
||||
- `x₀`: 中心点参数,曲线中点对应的x值
|
||||
|
||||
**函数图像特征:**
|
||||
- 当 `x = x₀` 时,σ = 0.5
|
||||
- 当 `k > 0` 时,曲线单调递增
|
||||
- `k` 越大,曲线越陡峭
|
||||
|
||||
---
|
||||
|
||||
### 4.2 Gaussian 函数
|
||||
|
||||
```cpp
|
||||
float gaussian(float x, float mean, float std) {
|
||||
float diff = x - mean;
|
||||
return exp(-(diff * diff) / (2 * std * std));
|
||||
}
|
||||
```
|
||||
|
||||
**数学公式:**
|
||||
```
|
||||
G(x; μ, σ) = exp(-(x-μ)² / (2σ²))
|
||||
```
|
||||
|
||||
**参数说明:**
|
||||
- `μ`: 均值,函数峰值位置
|
||||
- `σ`: 标准差,控制曲线宽度
|
||||
|
||||
**函数图像特征:**
|
||||
- 当 `x = μ` 时,G = 1(最大值)
|
||||
- `σ` 越小,曲线越尖锐
|
||||
- `σ` 越大,曲线越平缓
|
||||
|
||||
---
|
||||
|
||||
### 4.3 归一化函数
|
||||
|
||||
```cpp
|
||||
// 心率归一化
|
||||
float normalizeHR(float hr, float baseline) {
|
||||
return (hr - baseline) / baseline;
|
||||
}
|
||||
|
||||
// 呼吸率归一化
|
||||
float normalizeRR(float rr, float baseline) {
|
||||
return (rr - baseline) / baseline;
|
||||
}
|
||||
|
||||
// HRV归一化
|
||||
float normalizeHRV(float rmssd) {
|
||||
return clamp(rmssd / 100.0f, 0.0f, 1.5f);
|
||||
}
|
||||
|
||||
// 体动归一化
|
||||
float normalizeMovement(float movement) {
|
||||
return clamp(movement / 100.0f, 0.0f, 1.0f);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、各情绪评分算法
|
||||
|
||||
### 5.1 平静情绪 (Calm)
|
||||
|
||||
**适用场景:** 心率稳定、接近静息基线,呼吸规律,体动较低
|
||||
|
||||
**计算公式:**
|
||||
```
|
||||
Score_calm = 0.45 × G(|HR_norm|, 0, 0.3)
|
||||
+ 0.22 × (1 - clamp(HR_std / 12, 0, 1))
|
||||
+ 0.27 × G(|RR_norm|, 0, 0.5)
|
||||
+ 0.16 × RR_regularity
|
||||
+ 0.10 × σ(HRV_norm, 2.0, 0.7)
|
||||
+ 0.08 × G(Move_norm, 0.15, 0.2)
|
||||
```
|
||||
|
||||
**权重分析:**
|
||||
| 指标 | 权重 | 说明 |
|
||||
|------|------|------|
|
||||
| 心率偏离度 | 0.45 | 最重要,心率需接近基线 |
|
||||
| 心率稳定性 | 0.22 | 心率变化小 |
|
||||
| 呼吸率偏离度 | 0.27 | 呼吸需正常 |
|
||||
| 呼吸规律性 | 0.16 | 呼吸规律 |
|
||||
| HRV水平 | 0.10 | HRV正常或较高 |
|
||||
| 体动水平 | 0.08 | 体动低 |
|
||||
|
||||
---
|
||||
|
||||
### 5.2 高兴情绪 (Happy)
|
||||
|
||||
**适用场景:** 心率略高于基线、有适度变异性,呼吸正常,HRV较高
|
||||
|
||||
**计算公式:**
|
||||
```
|
||||
Score_happy = 0.45 × G(HR_norm, 0.3, 0.25)
|
||||
+ 0.10 × [1.5 < HR_std < 10 ? 1 : 0]
|
||||
+ 0.34 × G(|RR_norm|, 0, 0.5)
|
||||
+ 0.10 × σ(HRV_norm, 2.0, 0.9)
|
||||
+ 0.08 × G(Move_norm, 0.45, 0.25)
|
||||
```
|
||||
|
||||
**权重分析:**
|
||||
| 指标 | 权重 | 说明 |
|
||||
|------|------|------|
|
||||
| 心率偏离度 | 0.45 | 略高于基线,Gaussian中心0.3 |
|
||||
| 心率变异性 | 0.10 | 适度变异(1.5-10) |
|
||||
| 呼吸率偏离度 | 0.34 | 呼吸正常 |
|
||||
| HRV水平 | 0.10 | HRV较高 |
|
||||
| 体动水平 | 0.08 | 适中活跃 |
|
||||
|
||||
---
|
||||
|
||||
### 5.3 兴奋情绪 (Excited)
|
||||
|
||||
**适用场景:** 心率显著升高且趋势上升,呼吸加快,HRV中等,体动较高
|
||||
|
||||
**计算公式:**
|
||||
```
|
||||
Score_excited = 0.45 × σ(HR_norm, 2.0, 0.6)
|
||||
+ 0.10 × [HR_trend > 1.5 ? 1 : 0]
|
||||
+ 0.34 × σ(RR_norm, 2.0, 0.5)
|
||||
+ 0.10 × G(HRV_norm, 0.7, 0.4)
|
||||
+ 0.10 × σ(Move_norm, 2.0, 0.6)
|
||||
```
|
||||
|
||||
**权重分析:**
|
||||
| 指标 | 权重 | 说明 |
|
||||
|------|------|------|
|
||||
| 心率偏离度 | 0.45 | 显著升高( sigmoid中心0.6) |
|
||||
| 心率趋势 | 0.10 | 趋势上升加分 |
|
||||
| 呼吸率偏离度 | 0.34 | 呼吸加快 |
|
||||
| HRV水平 | 0.10 | 中等HRV |
|
||||
| 体动水平 | 0.10 | 高活跃 |
|
||||
|
||||
---
|
||||
|
||||
### 5.4 焦虑情绪 (Anxious)
|
||||
|
||||
**适用场景:** 心率升高但变异小,HRV低,压力指数高,呼吸不规律
|
||||
|
||||
**计算公式:**
|
||||
```
|
||||
Score_anxious = 0.36 × σ(HR_norm, 2.0, 0.4)
|
||||
+ 0.16 × (1 - σ(HR_std / 6, 2.0, 0.5))
|
||||
+ 0.25 × (1 - σ(HRV_norm, 2.0, 0.6))
|
||||
+ 0.10 × σ(HRV.stressIndex / 30, 2.0, 0.5)
|
||||
+ 0.16 × (1 - RR_regularity)
|
||||
+ 0.10 × G(Move_norm, 0.55, 0.3)
|
||||
```
|
||||
|
||||
**权重分析:**
|
||||
| 指标 | 权重 | 说明 |
|
||||
|------|------|------|
|
||||
| 心率偏离度 | 0.36 | 升高( sigmoid中心0.4,较早触发) |
|
||||
| 心率稳定性 | 0.16 | 变异小(焦虑时心率僵化)|
|
||||
| HRV水平 | 0.25 | HRV低(高权重)|
|
||||
| 压力指数 | 0.10 | 压力指数高 |
|
||||
| 呼吸规律性 | 0.16 | 呼吸不规律 |
|
||||
| 体动水平 | 0.10 | 躁动状态 |
|
||||
|
||||
---
|
||||
|
||||
### 5.5 愤怒情绪 (Angry)
|
||||
|
||||
**适用场景:** 心率快速升高且趋势明显,HRV低,呼吸不规律,体动高
|
||||
|
||||
**计算公式:**
|
||||
```
|
||||
Score_angry = 0.36 × σ(HR_norm, 2.0, 0.75)
|
||||
+ 0.20 × σ(HR_trend / 4, 2.0, 0.5)
|
||||
+ 0.25 × (1 - σ(HRV_norm, 2.0, 0.5))
|
||||
+ 0.20 × (1 - RR_regularity)
|
||||
+ 0.10 × σ(Move_norm, 2.0, 0.7)
|
||||
```
|
||||
|
||||
**权重分析:**
|
||||
| 指标 | 权重 | 说明 |
|
||||
|------|------|------|
|
||||
| 心率偏离度 | 0.36 | 显著升高( sigmoid中心0.75) |
|
||||
| 心率趋势 | 0.20 | 快速上升趋势 |
|
||||
| HRV水平 | 0.25 | HRV低 |
|
||||
| 呼吸规律性 | 0.20 | 不规律 |
|
||||
| 体动水平 | 0.10 | 高活跃 |
|
||||
|
||||
---
|
||||
|
||||
### 5.6 悲伤情绪 (Sad)
|
||||
|
||||
**适用场景:** 心率偏低,呼吸浅慢,HRV低,体动低
|
||||
|
||||
**计算公式:**
|
||||
```
|
||||
Score_sad = 0.40 × σ(-HR_norm, 2.0, 0.2)
|
||||
+ 0.15 × (1 - σ(HR_std / 4, 2.0, 0.3))
|
||||
+ 0.28 × σ(-RR_norm, 2.0, 0.3)
|
||||
+ 0.10 × RR_regularity
|
||||
+ 0.10 × (1 - σ(HRV_norm, 2.0, 0.4))
|
||||
+ 0.10 × (1 - σ(Move_norm, 2.0, 0.15))
|
||||
```
|
||||
|
||||
**权重分析:**
|
||||
| 指标 | 权重 | 说明 |
|
||||
|------|------|------|
|
||||
| 心率偏离度 | 0.40 | 心率偏低(负偏离)|
|
||||
| 心率稳定性 | 0.15 | 变化小 |
|
||||
| 呼吸率偏离度 | 0.28 | 呼吸浅慢(负偏离)|
|
||||
| 呼吸规律性 | 0.10 | 规律 |
|
||||
| HRV水平 | 0.10 | HRV低 |
|
||||
| 体动水平 | 0.10 | 低活动 |
|
||||
|
||||
---
|
||||
|
||||
### 5.7 压力情绪 (Stressed)
|
||||
|
||||
**适用场景:** 心率升高,HRV低,压力指数高,呼吸不规律
|
||||
|
||||
**计算公式:**
|
||||
```
|
||||
Score_stressed = 0.36 × σ(HR_norm, 2.0, 0.5)
|
||||
+ 0.20 × σ(HR_trend / 3, 2.0, 0.5)
|
||||
+ 0.25 × (1 - σ(HRV_norm, 2.0, 0.4))
|
||||
+ 0.10 × σ(HRV.stressIndex / 25, 2.0, 0.5)
|
||||
+ 0.16 × (1 - RR_regularity)
|
||||
+ 0.10 × G(Move_norm, 0.6, 0.25)
|
||||
```
|
||||
|
||||
**权重分析:**
|
||||
| 指标 | 权重 | 说明 |
|
||||
|------|------|------|
|
||||
| 心率偏离度 | 0.36 | 中度升高 |
|
||||
| 心率趋势 | 0.20 | 上升趋势 |
|
||||
| HRV水平 | 0.25 | HRV低(高权重)|
|
||||
| 压力指数 | 0.10 | 压力指数高 |
|
||||
| 呼吸规律性 | 0.16 | 不规律 |
|
||||
| 体动水平 | 0.10 | 适中 |
|
||||
|
||||
---
|
||||
|
||||
### 5.8 放松情绪 (Relaxed)
|
||||
|
||||
**适用场景:** 心率偏低,变异适中,呼吸慢且规律,体动极低
|
||||
|
||||
**计算公式:**
|
||||
```
|
||||
Score_relaxed = 0.40 × σ(-HR_norm, 2.0, 0.25)
|
||||
+ 0.22 × (1 - σ(HR_std / 8, 2.0, 0.4))
|
||||
+ 0.16 × σ(-RR_norm, 2.0, 0.3)
|
||||
+ 0.10 × RR_regularity
|
||||
+ 0.10 × (1 - σ(Move_norm, 2.0, 0.15))
|
||||
```
|
||||
|
||||
**权重分析:**
|
||||
| 指标 | 权重 | 说明 |
|
||||
|------|------|------|
|
||||
| 心率偏离度 | 0.40 | 心率偏低(负偏离)|
|
||||
| 心率稳定性 | 0.22 | 稳定 |
|
||||
| 呼吸率偏离度 | 0.16 | 呼吸慢(负偏离)|
|
||||
| 呼吸规律性 | 0.10 | 规律 |
|
||||
| 体动水平 | 0.10 | 极低活动 |
|
||||
|
||||
---
|
||||
|
||||
## 六、次要情绪算法
|
||||
|
||||
### 6.1 计算原理
|
||||
|
||||
次要情绪是概率第二大的情绪类型,不具有独立的评分模型。
|
||||
|
||||
```
|
||||
secondaryEmotion = argmax_i (P_i),其中 i ≠ primaryEmotion
|
||||
```
|
||||
|
||||
### 6.2 提取算法
|
||||
|
||||
```cpp
|
||||
int secondIdx = 0;
|
||||
float maxProb = emotionProbs[0], secondProb = 0;
|
||||
maxIdx = 0;
|
||||
|
||||
for (int i = 1; i < 9; i++) {
|
||||
if (emotionProbs[i] > maxProb) {
|
||||
secondProb = maxProb;
|
||||
secondIdx = maxIdx;
|
||||
maxProb = emotionProbs[i];
|
||||
maxIdx = i;
|
||||
} else if (emotionProbs[i] > secondProb) {
|
||||
secondProb = emotionProbs[i];
|
||||
secondIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
result.primaryEmotion = (EmotionType)maxIdx;
|
||||
result.secondaryEmotion = (EmotionType)secondIdx;
|
||||
result.confidence = maxProb;
|
||||
```
|
||||
|
||||
### 6.3 输出格式
|
||||
|
||||
```cpp
|
||||
Serial.printf("主要情绪:%s (置信度: %.1f%%);\n",
|
||||
EMOTION_NAMES[emotionResult.primaryEmotion],
|
||||
emotionResult.confidence * 100);
|
||||
|
||||
Serial.printf("次要情绪: %s倾向\n",
|
||||
EMOTION_NAMES[emotionResult.secondaryEmotion]);
|
||||
|
||||
Serial.printf("主要情绪得分:%.3f, 次要情绪得分:%.3f, 差值: %.3f\n",
|
||||
emotionResult.confidence,
|
||||
emotionResult.confidence * 0.8f, // 估算的次要得分
|
||||
emotionResult.confidence * 0.2f);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、概率处理流程
|
||||
|
||||
### 7.1 归一化公式
|
||||
|
||||
```cpp
|
||||
void SimpleEmotionAnalyzer::normalizeProbabilities() {
|
||||
float sum = 0;
|
||||
for (int i = 0; i < 9; i++) {
|
||||
sum += emotionProbs[i];
|
||||
}
|
||||
if (sum > 0) {
|
||||
for (int i = 0; i < 9; i++) {
|
||||
emotionProbs[i] /= sum;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**数学表达:**
|
||||
```
|
||||
P_i = Score_i / Σ Score_j, j ∈ {0,1,2,...,8}
|
||||
```
|
||||
|
||||
### 7.2 Top1放大机制
|
||||
|
||||
```cpp
|
||||
// 找出最大概率索引
|
||||
int maxIdx = 0;
|
||||
for (int i = 1; i < 9; i++) {
|
||||
if (emotionProbs[i] > emotionProbs[maxIdx]) {
|
||||
maxIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
// Top1放大1.3倍
|
||||
emotionProbs[maxIdx] *= 1.3f;
|
||||
|
||||
// 再次归一化
|
||||
normalizeProbabilities();
|
||||
```
|
||||
|
||||
**目的:** 强行制造"赢家",增强主要情绪的区分度
|
||||
|
||||
### 7.3 自适应平滑
|
||||
|
||||
```cpp
|
||||
void SimpleEmotionAnalyzer::smoothProbabilities() {
|
||||
for (int i = 0; i < 9; i++) {
|
||||
float diff = fabs(emotionProbs[i] - prevProbs[i]);
|
||||
float adaptiveAlpha = (diff > 0.2f) ? 0.6f : 0.25f;
|
||||
|
||||
emotionProbs[i] = adaptiveAlpha * emotionProbs[i] +
|
||||
(1.0f - adaptiveAlpha) * prevProbs[i];
|
||||
prevProbs[i] = emotionProbs[i];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**数学表达:**
|
||||
```
|
||||
P_i(t) = α × P_i(t) + (1-α) × P_i(t-1)
|
||||
|
||||
其中:
|
||||
α = 0.6, 当 |P_i(t) - P_i(t-1)| > 0.2(变化快 → 快响应)
|
||||
α = 0.25, 当 |P_i(t) - P_i(t-1)| ≤ 0.2(变化慢 → 适度抑制)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、情绪强度计算
|
||||
|
||||
### 8.1 计算公式
|
||||
|
||||
```cpp
|
||||
float hrFactor = fabs(hrData.bpmSmoothed - baseline.hrResting) / 40.0f;
|
||||
float hrvFactor = hrvData.isValid ? (1.0f - sigmoid(hrvData.rmssd, 0.02f, 40)) : 0.5f;
|
||||
float rrFactor = rrData.isValid ? fabs(rrData.rateSmoothed - baseline.rrResting) / 10.0f : 0.3f;
|
||||
|
||||
result.intensity = 0.4f
|
||||
+ 0.3f * constrain_value(hrFactor, 0.0f, 1.0f)
|
||||
+ 0.2f * constrain_value(hrvFactor, 0.0f, 1.0f)
|
||||
+ 0.1f * constrain_value(rrFactor, 0.0f, 1.0f);
|
||||
|
||||
result.intensity = constrain_value(result.intensity, 0.3f, 1.0f);
|
||||
```
|
||||
|
||||
### 8.2 公式表达
|
||||
|
||||
```
|
||||
intensity = clamp(0.4 + 0.3×clamp(hrFactor) + 0.2×clamp(hrvFactor) + 0.1×clamp(rrFactor), 0.3, 1.0)
|
||||
|
||||
其中:
|
||||
hrFactor = |HR - HR_baseline| / 40
|
||||
hrvFactor = 1 - σ(HRV_rmssd, 0.02, 40)
|
||||
rrFactor = |RR - RR_baseline| / 10
|
||||
```
|
||||
|
||||
### 8.3 权重分析
|
||||
|
||||
| 因素 | 权重 | 说明 |
|
||||
|------|------|------|
|
||||
| 基线偏移 | 0.4 | 基础强度 |
|
||||
| 心率偏移 | 0.3 | 心率偏离基线程度 |
|
||||
| HRV偏离 | 0.2 | HRV偏离正常程度 |
|
||||
| 呼吸偏移 | 0.1 | 呼吸偏离程度 |
|
||||
|
||||
---
|
||||
|
||||
## 九、情绪维度计算
|
||||
|
||||
### 9.1 效价 (Valence)
|
||||
|
||||
**定义:** 情绪的正面/负面倾向
|
||||
|
||||
```cpp
|
||||
void SimpleEmotionAnalyzer::calculateDimensions(const HeartRateData& hr,
|
||||
const RespirationData& rr) {
|
||||
float positive = emotionProbs[EMOTION_HAPPY] + emotionProbs[EMOTION_EXCITED] +
|
||||
emotionProbs[EMOTION_RELAXED] + emotionProbs[EMOTION_CALM];
|
||||
float negative = emotionProbs[EMOTION_ANXIOUS] + emotionProbs[EMOTION_ANGRY] +
|
||||
emotionProbs[EMOTION_SAD] + emotionProbs[EMOTION_STRESSED];
|
||||
|
||||
lastResult.valence = (positive - negative);
|
||||
}
|
||||
```
|
||||
|
||||
**公式:**
|
||||
```
|
||||
valence = (P_happy + P_excited + P_relaxed + P_calm)
|
||||
- (P_anxious + P_angry + P_sad + P_stressed)
|
||||
|
||||
范围:[-1, +1]
|
||||
-1 = 完全负面情绪
|
||||
0 = 中性
|
||||
+1 = 完全正面情绪
|
||||
```
|
||||
|
||||
### 9.2 唤醒度 (Arousal)
|
||||
|
||||
**定义:** 情绪的激活程度
|
||||
|
||||
```cpp
|
||||
float highArousal = emotionProbs[EMOTION_EXCITED] + emotionProbs[EMOTION_ANXIOUS] +
|
||||
emotionProbs[EMOTION_ANGRY];
|
||||
float lowArousal = emotionProbs[EMOTION_CALM] + emotionProbs[EMOTION_RELAXED] +
|
||||
emotionProbs[EMOTION_SAD];
|
||||
float total = highArousal + lowArousal;
|
||||
|
||||
lastResult.arousal = total > 0 ? highArousal / total : 0.5f;
|
||||
```
|
||||
|
||||
**公式:**
|
||||
```
|
||||
arousal = (P_excited + P_anxious + P_angry)
|
||||
/ (P_excited + P_anxious + P_angry + P_calm + P_relaxed + P_sad)
|
||||
|
||||
范围:[0, 1]
|
||||
0 = 低唤醒(平静、放松、悲伤)
|
||||
1 = 高唤醒(兴奋、焦虑、愤怒)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、压力评估计算
|
||||
|
||||
### 10.1 压力水平 (Stress Level)
|
||||
|
||||
```cpp
|
||||
void SimpleEmotionAnalyzer::calculateStressLevels(const HeartRateData& hr,
|
||||
const RespirationData& rr,
|
||||
const HRVEstimate& hrv) {
|
||||
float stressScore = 0;
|
||||
|
||||
// HRV压力指数
|
||||
if (hrv.isValid) {
|
||||
stressScore += 0.4f * sigmoid(hrv.stressIndex / 30.0f, 2.0f, 0.5f);
|
||||
}
|
||||
|
||||
// 心率偏离
|
||||
if (hr.isValid) {
|
||||
float hrNorm = fabs(normalizeHR(hr.bpmSmoothed, baseline.hrResting));
|
||||
stressScore += 0.35f * sigmoid(hrNorm, 2.0f, 0.5f);
|
||||
}
|
||||
|
||||
// 呼吸不规律
|
||||
if (rr.isValid) {
|
||||
float irregularity = 1.0f - rr.regularity;
|
||||
stressScore += 0.25f * irregularity;
|
||||
}
|
||||
|
||||
result.stressLevel = constrain_value(stressScore * 100.0f, 0.0f, 100.0f);
|
||||
}
|
||||
```
|
||||
|
||||
### 10.2 焦虑水平 (Anxiety Level)
|
||||
|
||||
```cpp
|
||||
float anxietyScore = 0;
|
||||
|
||||
if (hr.isValid) {
|
||||
float hrNorm = normalizeHR(hr.bpmSmoothed, baseline.hrResting);
|
||||
anxietyScore += 0.4f * sigmoid(hrNorm, 2.0f, 0.4f);
|
||||
anxietyScore += 0.2f * (1.0f - sigmoid(hr.bpmStd / 6.0f, 2.0f, 0.5f));
|
||||
}
|
||||
|
||||
if (hrv.isValid) {
|
||||
anxietyScore += 0.25f * (1.0f - sigmoid(hrv.rmssd / 50.0f, 2.0f, 0.5f));
|
||||
}
|
||||
|
||||
if (rr.isValid) {
|
||||
anxietyScore += 0.15f * (1.0f - rr.regularity);
|
||||
}
|
||||
|
||||
result.anxietyLevel = constrain_value(anxietyScore * 100.0f, 0.0f, 100.0f);
|
||||
```
|
||||
|
||||
### 10.3 放松水平 (Relaxation Level)
|
||||
|
||||
```cpp
|
||||
float relaxationScore = 0;
|
||||
|
||||
if (hrv.isValid) {
|
||||
relaxationScore += 0.45f * sigmoid(hrv.rmssd / 80.0f, 2.0f, 0.6f);
|
||||
}
|
||||
|
||||
if (hr.isValid) {
|
||||
float hrNorm = -normalizeHR(hr.bpmSmoothed, baseline.hrResting);
|
||||
relaxationScore += 0.30f * sigmoid(hrNorm, 2.0f, 0.25f);
|
||||
relaxationScore += 0.15f * (1.0f - sigmoid(hr.bpmStd / 10.0f, 2.0f, 0.5f));
|
||||
}
|
||||
|
||||
if (movement.isValid) {
|
||||
relaxationScore += 0.10f * (1.0f - sigmoid(movement.movementSmoothed / 30.0f,
|
||||
2.0f, 0.5f));
|
||||
}
|
||||
|
||||
result.relaxationLevel = constrain_value(relaxationScore * 100.0f, 0.0f, 100.0f);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十一、特殊处理规则
|
||||
|
||||
### 11.1 UNKNOWN 判断条件
|
||||
|
||||
```cpp
|
||||
if (maxProb < 0.20f && (maxProb - secondProb) < 0.03f) {
|
||||
result.primaryEmotion = EMOTION_UNKNOWN;
|
||||
result.confidence = maxProb;
|
||||
}
|
||||
```
|
||||
|
||||
**触发条件(同时满足):**
|
||||
1. 最大概率 < 0.20(低置信度)
|
||||
2. 最大概率与第二大概率差值 < 0.03(多情绪接近)
|
||||
|
||||
### 11.2 负面情绪合并
|
||||
|
||||
```cpp
|
||||
if (result.primaryEmotion == EMOTION_ANXIOUS ||
|
||||
result.primaryEmotion == EMOTION_ANGRY ||
|
||||
result.primaryEmotion == EMOTION_STRESSED) {
|
||||
|
||||
float combinedProb = emotionProbs[EMOTION_ANXIOUS] +
|
||||
emotionProbs[EMOTION_ANGRY] +
|
||||
emotionProbs[EMOTION_STRESSED];
|
||||
result.primaryEmotion = EMOTION_STRESSED;
|
||||
result.confidence = max(combinedProb,
|
||||
max(emotionProbs[EMOTION_ANXIOUS],
|
||||
max(emotionProbs[EMOTION_ANGRY],
|
||||
emotionProbs[EMOTION_STRESSED])));
|
||||
}
|
||||
```
|
||||
|
||||
**逻辑:** 焦虑、愤怒、压力三种情绪合并为"压力"情绪
|
||||
|
||||
### 11.3 UNKNOWN 强制落地
|
||||
|
||||
```cpp
|
||||
if (result.primaryEmotion == EMOTION_UNKNOWN) {
|
||||
if (result.arousal > 0.6f) {
|
||||
result.primaryEmotion = EMOTION_EXCITED;
|
||||
} else if (result.valence < -0.2f) {
|
||||
result.primaryEmotion = EMOTION_STRESSED;
|
||||
} else {
|
||||
result.primaryEmotion = EMOTION_CALM;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**规则:**
|
||||
| 条件 | 映射结果 |
|
||||
|------|----------|
|
||||
| arousal > 0.6 | EXCITED(高唤醒→兴奋)|
|
||||
| valence < -0.2 且 arousal ≤ 0.6 | STRESSED(负面→压力)|
|
||||
| 其他 | CALM(默认平静)|
|
||||
|
||||
---
|
||||
|
||||
## 十二、输出数据结构
|
||||
|
||||
```cpp
|
||||
struct EmotionResult {
|
||||
EmotionType primaryEmotion; // 主要情绪
|
||||
EmotionType secondaryEmotion; // 次要情绪
|
||||
float confidence; // 置信度 0-1
|
||||
float intensity; // 强度 0-1
|
||||
|
||||
float valence; // 效价 -1到+1
|
||||
float arousal; // 唤醒度 0-1
|
||||
|
||||
float stressLevel; // 压力水平 0-100
|
||||
float anxietyLevel; // 焦虑水平 0-100
|
||||
float relaxationLevel; // 放松水平 0-100
|
||||
|
||||
float sympatheticActivity; // 交感神经活动
|
||||
float parasympatheticActivity; // 副交感神经活动
|
||||
|
||||
bool isValid; // 数据有效性
|
||||
unsigned long timestamp; // 时间戳
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十三、算法特点总结
|
||||
|
||||
| 特点 | 说明 |
|
||||
|------|------|
|
||||
| 多指标融合 | 综合心率、呼吸、HRV、体动4类数据 |
|
||||
| 权重分配 | 不同情绪对各指标的权重不同 |
|
||||
| 非线性函数 | 使用Sigmoid和Gaussian模拟生理反应 |
|
||||
| 自适应平滑 | 根据变化速度动态调整平滑系数 |
|
||||
| Top1放大 | 增强主要情绪区分度 |
|
||||
| 负面情绪合并 | 焦虑/愤怒/压力统一为压力评估 |
|
||||
| UNKNOWN处理 | 低置信度时标记为未知并强制落地 |
|
||||
21
extra_script.py
Normal file
21
extra_script.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
Import("env")
|
||||
|
||||
# 修复中文路径问题
|
||||
def fix_chinese_path(source, target, env):
|
||||
project_dir = Path(env["PROJECT_DIR"])
|
||||
build_dir = Path(env["BUILD_DIR"])
|
||||
|
||||
# 确保构建目录存在
|
||||
build_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 设置环境变量以支持UTF-8
|
||||
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
||||
|
||||
return None
|
||||
|
||||
# 在构建前执行
|
||||
env.AddPreAction("buildprog", fix_chinese_path)
|
||||
BIN
github_guide.md
Normal file
BIN
github_guide.md
Normal file
Binary file not shown.
@@ -5,13 +5,17 @@
|
||||
; Library options: dependencies, extra library storages
|
||||
; Advanced options: extra scripting
|
||||
;
|
||||
; Please visit documentation for the other options and examples
|
||||
; Please visit documentation for other options and examples
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[env:freenove_esp32_s3_wroom]
|
||||
platform = espressif32
|
||||
board = freenove_esp32_s3_wroom
|
||||
framework = arduino
|
||||
extra_scripts = pre:extra_script.py
|
||||
build_flags =
|
||||
-Wl,-Map=/dev/null
|
||||
lib_deps =
|
||||
bblanchon/ArduinoJson@^7.4.2
|
||||
emelianov/modbus-esp8266@^4.1.0
|
||||
knolleary/PubSubClient@^2.8.0
|
||||
|
||||
193
src/config.h
Normal file
193
src/config.h
Normal file
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* @file config.h
|
||||
* @brief ESP32情绪分析系统配置文件
|
||||
* @description 适配雷达传感器串口数据输入
|
||||
*/
|
||||
|
||||
#ifndef CONFIG_H
|
||||
#define CONFIG_H
|
||||
|
||||
// ==================== 串口配置 ====================
|
||||
// 雷达传感器串口 (UART2)
|
||||
#define RADAR_UART_NUM UART_NUM_2
|
||||
#define RADAR_RX_PIN 16 // 接收引脚
|
||||
#define RADAR_TX_PIN 17 // 发送引脚
|
||||
#define RADAR_BAUD_RATE 115200 // 波特率
|
||||
|
||||
// 调试串口 (USB)
|
||||
#define DEBUG_UART_NUM UART_NUM_0
|
||||
#define DEBUG_BAUD_RATE 115200
|
||||
|
||||
// ==================== 数据协议配置 ====================
|
||||
// 支持的协议格式
|
||||
#define PROTOCOL_JSON 0 // JSON格式
|
||||
#define PROTOCOL_BINARY 1 // 二进制格式
|
||||
#define PROTOCOL_TEXT 2 // 文本格式
|
||||
|
||||
// 当前使用的协议
|
||||
#define CURRENT_PROTOCOL PROTOCOL_JSON
|
||||
|
||||
// JSON数据字段名 (根据您的雷达协议修改)
|
||||
#define JSON_FIELD_HEART_RATE "heart_rate"
|
||||
#define JSON_FIELD_RESPIRATION "respiration_rate"
|
||||
#define JSON_FIELD_HR_QUALITY "hr_quality"
|
||||
#define JSON_FIELD_RR_QUALITY "rr_quality"
|
||||
|
||||
// 文本格式示例: "HR:72,RR:16\n"
|
||||
#define TEXT_HR_PREFIX "HR:"
|
||||
#define TEXT_RR_PREFIX "RR:"
|
||||
|
||||
// 二进制协议帧头帧尾
|
||||
#define BINARY_FRAME_HEADER 0xAA
|
||||
#define BINARY_FRAME_TAIL 0x55
|
||||
|
||||
// ==================== 采样配置 ====================
|
||||
#define SAMPLE_RATE_HZ 50 // 数据更新率 50Hz
|
||||
#define SAMPLE_INTERVAL_MS 20 // 采样间隔 20ms
|
||||
|
||||
// 数据缓冲大小
|
||||
#define RX_BUFFER_SIZE 512 // 串口接收缓冲
|
||||
#define DATA_BUFFER_SIZE 300 // 数据存储缓冲
|
||||
|
||||
// ==================== 心率参数 ====================
|
||||
// 正常心率范围
|
||||
#define HR_MIN_NORMAL 40 // 最低正常心率 BPM
|
||||
#define HR_MAX_NORMAL 200 // 最高正常心率 BPM
|
||||
#define HR_RESTING_MIN 50 // 静息心率下限
|
||||
#define HR_RESTING_MAX 90 // 静息心率上限
|
||||
|
||||
// 异常检测阈值
|
||||
#define HR_SUDDEN_CHANGE 30 // 突然变化阈值 BPM
|
||||
#define HR_STABLE_WINDOW 10 // 稳定性检测窗口(秒)
|
||||
|
||||
// ==================== 呼吸参数 ====================
|
||||
// 正常呼吸频率范围 (次/分钟)
|
||||
#define RR_MIN_NORMAL 8 // 最低正常呼吸频率
|
||||
#define RR_MAX_NORMAL 30 // 最高正常呼吸频率
|
||||
#define RR_RESTING_MIN 12 // 静息呼吸频率下限
|
||||
#define RR_RESTING_MAX 20 // 静息呼吸频率上限
|
||||
|
||||
// 异常检测阈值
|
||||
#define RR_SUDDEN_CHANGE 10 // 突然变化阈值
|
||||
#define RR_STABLE_WINDOW 15 // 稳定性检测窗口(秒)
|
||||
|
||||
// ==================== HRV参数 ====================
|
||||
// HRV分析窗口
|
||||
#define HRV_WINDOW_BEATS 50 // HRV分析所需心跳数
|
||||
#define HRV_MIN_BEATS 10 // 最小心跳数要求
|
||||
#define HRV_ESTIMATION_WINDOW 60 // HRV估算窗口(秒)
|
||||
|
||||
// HRV指标范围 (RMSSD, ms)
|
||||
#define HRV_VERY_LOW 20 // 极低HRV
|
||||
#define HRV_LOW 50 // 低HRV
|
||||
#define HRV_NORMAL 100 // 正常HRV
|
||||
#define HRV_HIGH 150 // 高HRV
|
||||
|
||||
// ==================== 情绪分类阈值 ====================
|
||||
// 基于心率的情绪阈值
|
||||
#define EMOTION_HR_LOW 60 // 低心率阈值
|
||||
#define EMOTION_HR_NORMAL 80 // 正常心率阈值
|
||||
#define EMOTION_HR_ELEVATED 100 // 升高心率阈值
|
||||
#define EMOTION_HR_HIGH 120 // 高心率阈值
|
||||
|
||||
// 基于呼吸的情绪阈值
|
||||
#define EMOTION_RR_SLOW 10 // 缓慢呼吸
|
||||
#define EMOTION_RR_NORMAL 16 // 正常呼吸
|
||||
#define EMOTION_RR_FAST 22 // 快速呼吸
|
||||
|
||||
// HRV情绪阈值
|
||||
#define EMOTION_HRV_LOW 30 // 低HRV (压力)
|
||||
#define EMOTION_HRV_NORMAL 60 // 正常HRV
|
||||
#define EMOTION_HRV_HIGH 100 // 高HRV (放松)
|
||||
|
||||
// ==================== 数据平滑参数 ====================
|
||||
// 指数移动平均系数
|
||||
#define EMA_ALPHA_HR 0.15f // 心率平滑系数
|
||||
#define EMA_ALPHA_RR 0.10f // 呼吸平滑系数
|
||||
|
||||
// 异常值过滤
|
||||
#define OUTLIER_FILTER_ENABLED true // 启用异常值过滤
|
||||
#define OUTLIER_THRESHOLD 3.0f // 异常值标准差倍数
|
||||
|
||||
// ==================== 情绪定义 ====================
|
||||
enum EmotionType {
|
||||
EMOTION_CALM = 0, // 平静
|
||||
EMOTION_HAPPY, // 快乐
|
||||
EMOTION_EXCITED, // 兴奋
|
||||
EMOTION_ANXIOUS, // 焦虑
|
||||
EMOTION_ANGRY, // 愤怒
|
||||
EMOTION_SAD, // 悲伤
|
||||
EMOTION_STRESSED, // 压力
|
||||
EMOTION_RELAXED, // 放松
|
||||
EMOTION_UNKNOWN // 未知
|
||||
};
|
||||
|
||||
// 情绪名称字符串
|
||||
static const char* EMOTION_NAMES[] = {
|
||||
"平静",
|
||||
"快乐",
|
||||
"兴奋",
|
||||
"焦虑",
|
||||
"愤怒",
|
||||
"悲伤",
|
||||
"压力",
|
||||
"放松",
|
||||
"未知"
|
||||
};
|
||||
|
||||
// 情绪描述
|
||||
static const char* EMOTION_DESCRIPTIONS[] = {
|
||||
"心情平静,情绪稳定",
|
||||
"心情愉悦,积极正向",
|
||||
"情绪高涨,充满活力",
|
||||
"感到焦虑或不安",
|
||||
"情绪激动,可能有愤怒",
|
||||
"情绪低落,可能感到悲伤",
|
||||
"压力较大,需要注意休息",
|
||||
"身心放松,状态良好",
|
||||
"情绪状态未知"
|
||||
};
|
||||
|
||||
// 情绪建议
|
||||
static const char* EMOTION_SUGGESTIONS[] = {
|
||||
"状态良好,继续保持",
|
||||
"心情不错,享受当下",
|
||||
"精力充沛,注意适当调节",
|
||||
"建议深呼吸放松,或进行冥想",
|
||||
"建议冷静下来,可以尝试深呼吸",
|
||||
"可以尝试与朋友交流,或做些喜欢的事",
|
||||
"建议休息放松,避免过度工作",
|
||||
"状态很好,继续保持",
|
||||
""
|
||||
};
|
||||
|
||||
// ==================== 调试配置 ====================
|
||||
#define DEBUG_SERIAL true // 启用串口调试
|
||||
#define DEBUG_VERBOSE false // 详细调试信息
|
||||
#define DEBUG_PROTOCOL true // 显示协议解析信息
|
||||
|
||||
// ==================== 工具函数宏 ====================
|
||||
#define constrain_value(x, min, max) ((x) < (min) ? (min) : ((x) > (max) ? (max) : (x)))
|
||||
#define map_value(x, in_min, in_max, out_min, out_max) \
|
||||
(((x) - (in_min)) * ((out_max) - (out_min)) / ((in_max) - (in_min)) + (out_min))
|
||||
|
||||
// 调试宏
|
||||
#if DEBUG_SERIAL
|
||||
#define DEBUG_PRINT(x) Serial.print(x)
|
||||
#define DEBUG_PRINTLN(x) Serial.println(x)
|
||||
#define DEBUG_PRINTF(fmt, ...) Serial.printf(fmt, ##__VA_ARGS__)
|
||||
#else
|
||||
#define DEBUG_PRINT(x)
|
||||
#define DEBUG_PRINTLN(x)
|
||||
#define DEBUG_PRINTF(fmt, ...)
|
||||
#endif
|
||||
|
||||
#if DEBUG_VERBOSE
|
||||
#define VERBOSE_PRINT(x) Serial.print(x)
|
||||
#define VERBOSE_PRINTLN(x) Serial.println(x)
|
||||
#else
|
||||
#define VERBOSE_PRINT(x)
|
||||
#define VERBOSE_PRINTLN(x)
|
||||
#endif
|
||||
|
||||
#endif // CONFIG_H
|
||||
356
src/data_processor.cpp
Normal file
356
src/data_processor.cpp
Normal file
@@ -0,0 +1,356 @@
|
||||
/**
|
||||
* @file data_processor.cpp
|
||||
* @brief 数据处理模块实现
|
||||
*/
|
||||
|
||||
#include "data_processor.h"
|
||||
#include <math.h>
|
||||
|
||||
// ==================== 心率数据处理器实现 ====================
|
||||
|
||||
HeartRateProcessor::HeartRateProcessor(int histSize)
|
||||
: historySize(histSize), historyIndex(0), historyCount(0),
|
||||
lastSmoothed(72), alpha(EMA_ALPHA_HR), bpmSum(0), bpmSumSq(0),
|
||||
lastValidBpm(72), lastValidTime(0) {
|
||||
bpmHistory = new float[historySize];
|
||||
memset(bpmHistory, 0, historySize * sizeof(float));
|
||||
}
|
||||
|
||||
HeartRateProcessor::~HeartRateProcessor() {
|
||||
delete[] bpmHistory;
|
||||
}
|
||||
|
||||
void HeartRateProcessor::addData(float bpm, float confidence) {
|
||||
// 数据验证
|
||||
if (bpm < HR_MIN_NORMAL || bpm > HR_MAX_NORMAL) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 突变检测
|
||||
if (historyCount > 0) {
|
||||
float lastBpm = bpmHistory[(historyIndex - 1 + historySize) % historySize];
|
||||
if (fabs(bpm - lastBpm) > HR_SUDDEN_CHANGE) {
|
||||
// 可能是异常值,进行平滑处理
|
||||
bpm = lastSmoothed + (bpm - lastSmoothed) * 0.3f;
|
||||
}
|
||||
}
|
||||
|
||||
// 存储数据
|
||||
bpmHistory[historyIndex] = bpm;
|
||||
historyIndex = (historyIndex + 1) % historySize;
|
||||
if (historyCount < historySize) historyCount++;
|
||||
|
||||
// 更新统计
|
||||
bpmSum += bpm;
|
||||
bpmSumSq += bpm * bpm;
|
||||
|
||||
// 平滑处理
|
||||
lastSmoothed = alpha * bpm + (1.0f - alpha) * lastSmoothed;
|
||||
|
||||
lastValidBpm = bpm;
|
||||
lastValidTime = millis();
|
||||
}
|
||||
|
||||
HeartRateData HeartRateProcessor::getData() {
|
||||
HeartRateData data;
|
||||
memset(&data, 0, sizeof(data));
|
||||
|
||||
if (historyCount == 0) {
|
||||
data.isValid = false;
|
||||
return data;
|
||||
}
|
||||
|
||||
// 当前值
|
||||
data.bpm = bpmHistory[(historyIndex - 1 + historySize) % historySize];
|
||||
data.bpmSmoothed = lastSmoothed;
|
||||
|
||||
// 计算均值
|
||||
data.bpmMean = bpmSum / historyCount;
|
||||
|
||||
// 计算标准差
|
||||
float variance = (bpmSumSq / historyCount) - (data.bpmMean * data.bpmMean);
|
||||
data.bpmStd = variance > 0 ? sqrt(variance) : 0;
|
||||
|
||||
// 计算最值
|
||||
data.bpmMin = 300;
|
||||
data.bpmMax = 0;
|
||||
for (int i = 0; i < historyCount; i++) {
|
||||
if (bpmHistory[i] < data.bpmMin) data.bpmMin = bpmHistory[i];
|
||||
if (bpmHistory[i] > data.bpmMax) data.bpmMax = bpmHistory[i];
|
||||
}
|
||||
|
||||
// 计算趋势
|
||||
data.trend = calculateTrend();
|
||||
|
||||
// 计算变异性
|
||||
data.variability = calculateVariability();
|
||||
|
||||
// 数据质量评估
|
||||
if (millis() - lastValidTime < 3000) {
|
||||
data.quality = 0.8f + 0.2f * (1.0f - data.bpmStd / 30.0f);
|
||||
data.quality = constrain_value(data.quality, 0.0f, 1.0f);
|
||||
} else {
|
||||
data.quality = 0.3f;
|
||||
}
|
||||
|
||||
data.isValid = true;
|
||||
data.timestamp = millis();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
HRVEstimate HeartRateProcessor::estimateHRV() {
|
||||
HRVEstimate hrv;
|
||||
memset(&hrv, 0, sizeof(hrv));
|
||||
|
||||
if (historyCount < 10) {
|
||||
hrv.isValid = false;
|
||||
return hrv;
|
||||
}
|
||||
|
||||
// 从心率变异性估算HRV
|
||||
// 这是一个近似方法,真实的HRV需要RR间期数据
|
||||
|
||||
// 估算RMSSD:基于心率标准差
|
||||
HeartRateData hrData = getData();
|
||||
float hrVariability = hrData.bpmStd;
|
||||
|
||||
// 经验公式:RMSSD ≈ 心率标准差 * 某个系数
|
||||
// 这个系数需要根据实际情况调整
|
||||
hrv.rmssd = hrVariability * 8.0f; // 经验系数
|
||||
|
||||
// 估算SDNN
|
||||
hrv.sdnn = hrVariability * 10.0f;
|
||||
|
||||
// 压力指数
|
||||
if (hrv.rmssd > 0) {
|
||||
hrv.stressIndex = 1000.0f / hrv.rmssd;
|
||||
} else {
|
||||
hrv.stressIndex = 50;
|
||||
}
|
||||
|
||||
// 自主神经平衡(使用 sigmoid 归一化,正常范围 0.3~0.7)
|
||||
// rmssd 正常值:20-50ms,映射到 autonomicBalance 0.3-0.7
|
||||
if (hrv.rmssd > 0) {
|
||||
float normalized = (hrv.rmssd - 20.0f) / 30.0f; // 归一化到 0~1
|
||||
hrv.autonomicBalance = 0.3f + 0.4f * constrain_value(normalized, 0.0f, 1.0f);
|
||||
} else {
|
||||
hrv.autonomicBalance = 0.5f; // 默认中性值
|
||||
}
|
||||
|
||||
hrv.isValid = true;
|
||||
return hrv;
|
||||
}
|
||||
|
||||
void HeartRateProcessor::reset() {
|
||||
historyIndex = 0;
|
||||
historyCount = 0;
|
||||
lastSmoothed = 72;
|
||||
bpmSum = 0;
|
||||
bpmSumSq = 0;
|
||||
memset(bpmHistory, 0, historySize * sizeof(float));
|
||||
}
|
||||
|
||||
float HeartRateProcessor::calculateVariability() {
|
||||
if (historyCount < 3) return 0;
|
||||
|
||||
// 计算相邻值差异的均方根
|
||||
float sumSqDiff = 0;
|
||||
int count = 0;
|
||||
|
||||
for (int i = 1; i < historyCount; i++) {
|
||||
int idx1 = (historyIndex - i - 1 + historySize) % historySize;
|
||||
int idx2 = (historyIndex - i + historySize) % historySize;
|
||||
float diff = bpmHistory[idx1] - bpmHistory[idx2];
|
||||
sumSqDiff += diff * diff;
|
||||
count++;
|
||||
}
|
||||
|
||||
return count > 0 ? sqrt(sumSqDiff / count) : 0;
|
||||
}
|
||||
|
||||
float HeartRateProcessor::calculateTrend() {
|
||||
if (historyCount < 10) return 0;
|
||||
|
||||
// 计算前半和后半的均值差
|
||||
int half = historyCount / 2;
|
||||
float firstHalf = 0, secondHalf = 0;
|
||||
|
||||
for (int i = 0; i < half; i++) {
|
||||
int idx = (historyIndex - historyCount + i + historySize) % historySize;
|
||||
firstHalf += bpmHistory[idx];
|
||||
}
|
||||
firstHalf /= half;
|
||||
|
||||
for (int i = historyCount - half; i < historyCount; i++) {
|
||||
int idx = (historyIndex - historyCount + i + historySize) % historySize;
|
||||
secondHalf += bpmHistory[idx];
|
||||
}
|
||||
secondHalf /= half;
|
||||
|
||||
return secondHalf - firstHalf;
|
||||
}
|
||||
|
||||
// ==================== 呼吸数据处理器实现 ====================
|
||||
|
||||
RespirationProcessor::RespirationProcessor(int histSize)
|
||||
: historySize(histSize), historyIndex(0), historyCount(0),
|
||||
lastSmoothed(16), alpha(EMA_ALPHA_RR), lastValidRate(16), lastValidTime(0) {
|
||||
rateHistory = new float[historySize];
|
||||
memset(rateHistory, 0, historySize * sizeof(float));
|
||||
}
|
||||
|
||||
RespirationProcessor::~RespirationProcessor() {
|
||||
delete[] rateHistory;
|
||||
}
|
||||
|
||||
void RespirationProcessor::addData(float rate, float confidence) {
|
||||
// 数据验证
|
||||
if (rate < RR_MIN_NORMAL || rate > RR_MAX_NORMAL) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 突变检测
|
||||
if (historyCount > 0) {
|
||||
float lastRate = rateHistory[(historyIndex - 1 + historySize) % historySize];
|
||||
if (fabs(rate - lastRate) > RR_SUDDEN_CHANGE) {
|
||||
rate = lastSmoothed + (rate - lastSmoothed) * 0.3f;
|
||||
}
|
||||
}
|
||||
|
||||
// 存储数据
|
||||
rateHistory[historyIndex] = rate;
|
||||
historyIndex = (historyIndex + 1) % historySize;
|
||||
if (historyCount < historySize) historyCount++;
|
||||
|
||||
// 平滑处理
|
||||
lastSmoothed = alpha * rate + (1.0f - alpha) * lastSmoothed;
|
||||
|
||||
lastValidRate = rate;
|
||||
lastValidTime = millis();
|
||||
}
|
||||
|
||||
RespirationData RespirationProcessor::getData() {
|
||||
RespirationData data;
|
||||
memset(&data, 0, sizeof(data));
|
||||
|
||||
if (historyCount == 0) {
|
||||
data.isValid = false;
|
||||
return data;
|
||||
}
|
||||
|
||||
// 当前值
|
||||
data.rate = rateHistory[(historyIndex - 1 + historySize) % historySize];
|
||||
data.rateSmoothed = lastSmoothed;
|
||||
|
||||
// 计算均值
|
||||
float sum = 0;
|
||||
for (int i = 0; i < historyCount; i++) {
|
||||
sum += rateHistory[i];
|
||||
}
|
||||
data.rateMean = sum / historyCount;
|
||||
|
||||
// 计算标准差
|
||||
float sumSq = 0;
|
||||
for (int i = 0; i < historyCount; i++) {
|
||||
float diff = rateHistory[i] - data.rateMean;
|
||||
sumSq += diff * diff;
|
||||
}
|
||||
data.rateStd = sqrt(sumSq / historyCount);
|
||||
|
||||
// 计算规律性
|
||||
data.regularity = calculateRegularity();
|
||||
|
||||
// 计算变异性
|
||||
data.variability = calculateVariability();
|
||||
|
||||
// 数据质量评估
|
||||
if (millis() - lastValidTime < 5000) {
|
||||
data.quality = 0.7f + 0.3f * data.regularity;
|
||||
data.quality = constrain_value(data.quality, 0.0f, 1.0f);
|
||||
} else {
|
||||
data.quality = 0.3f;
|
||||
}
|
||||
|
||||
data.isValid = true;
|
||||
data.timestamp = millis();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
void RespirationProcessor::reset() {
|
||||
historyIndex = 0;
|
||||
historyCount = 0;
|
||||
lastSmoothed = 16;
|
||||
memset(rateHistory, 0, historySize * sizeof(float));
|
||||
}
|
||||
|
||||
float RespirationProcessor::calculateRegularity() {
|
||||
if (historyCount < 5) return 0.8f;
|
||||
|
||||
// 计算变异系数
|
||||
float sum = 0;
|
||||
for (int i = 0; i < historyCount; i++) {
|
||||
sum += rateHistory[i];
|
||||
}
|
||||
float mean = sum / historyCount;
|
||||
|
||||
float sumSq = 0;
|
||||
for (int i = 0; i < historyCount; i++) {
|
||||
float diff = rateHistory[i] - mean;
|
||||
sumSq += diff * diff;
|
||||
}
|
||||
float std = sqrt(sumSq / historyCount);
|
||||
|
||||
float cv = (mean > 0) ? std / mean : 0;
|
||||
|
||||
// 转换为规律性(CV越小,规律性越高)
|
||||
return constrain_value(1.0f - cv * 3.0f, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
float RespirationProcessor::calculateVariability() {
|
||||
if (historyCount < 3) return 0;
|
||||
|
||||
float sumDiff = 0;
|
||||
for (int i = 1; i < historyCount; i++) {
|
||||
int idx1 = (historyIndex - i - 1 + historySize) % historySize;
|
||||
int idx2 = (historyIndex - i + historySize) % historySize;
|
||||
sumDiff += fabs(rateHistory[idx1] - rateHistory[idx2]);
|
||||
}
|
||||
|
||||
return sumDiff / (historyCount - 1);
|
||||
}
|
||||
|
||||
// ==================== 综合数据处理器实现 ====================
|
||||
|
||||
PhysioDataProcessor::PhysioDataProcessor() {
|
||||
hrProcessor = new HeartRateProcessor(100);
|
||||
rrProcessor = new RespirationProcessor(50);
|
||||
}
|
||||
|
||||
PhysioDataProcessor::~PhysioDataProcessor() {
|
||||
delete hrProcessor;
|
||||
delete rrProcessor;
|
||||
}
|
||||
|
||||
void PhysioDataProcessor::update(float hr, float rr, float hrConf, float rrConf) {
|
||||
hrProcessor->addData(hr, hrConf);
|
||||
rrProcessor->addData(rr, rrConf);
|
||||
}
|
||||
|
||||
HeartRateData PhysioDataProcessor::getHeartRateData() {
|
||||
return hrProcessor->getData();
|
||||
}
|
||||
|
||||
RespirationData PhysioDataProcessor::getRespirationData() {
|
||||
return rrProcessor->getData();
|
||||
}
|
||||
|
||||
HRVEstimate PhysioDataProcessor::getHRVEstimate() {
|
||||
return hrProcessor->estimateHRV();
|
||||
}
|
||||
|
||||
void PhysioDataProcessor::reset() {
|
||||
hrProcessor->reset();
|
||||
rrProcessor->reset();
|
||||
}
|
||||
174
src/data_processor.h
Normal file
174
src/data_processor.h
Normal file
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* @file data_processor.h
|
||||
* @brief 数据处理模块头文件
|
||||
* @description 处理雷达传感器提供的心率和呼吸数据
|
||||
*/
|
||||
|
||||
#ifndef DATA_PROCESSOR_H
|
||||
#define DATA_PROCESSOR_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "config.h"
|
||||
|
||||
// ==================== 心率数据结构 ====================
|
||||
|
||||
/**
|
||||
* @brief 心率处理后的数据
|
||||
*/
|
||||
struct HeartRateData {
|
||||
float bpm; // 当前心率 BPM
|
||||
float bpmSmoothed; // 平滑后心率
|
||||
float bpmMean; // 平均心率
|
||||
float bpmStd; // 心率标准差
|
||||
float bpmMin; // 最小心率
|
||||
float bpmMax; // 最大心率
|
||||
float trend; // 趋势(上升/下降)
|
||||
float variability; // 心率变异性
|
||||
float quality; // 数据质量 0-1
|
||||
bool isValid; // 数据是否有效
|
||||
unsigned long timestamp; // 时间戳
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 呼吸处理后的数据
|
||||
*/
|
||||
struct RespirationData {
|
||||
float rate; // 呼吸频率 次/分钟
|
||||
float rateSmoothed; // 平滑后呼吸频率
|
||||
float rateMean; // 平均呼吸频率
|
||||
float rateStd; // 呼吸频率标准差
|
||||
float regularity; // 呼吸规律性 0-1
|
||||
float variability; // 呼吸变异性
|
||||
float quality; // 数据质量 0-1
|
||||
bool isValid; // 数据是否有效
|
||||
unsigned long timestamp; // 时间戳
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief HRV估算数据
|
||||
*/
|
||||
struct HRVEstimate {
|
||||
float rmssd; // 估算的RMSSD
|
||||
float sdnn; // 估算的SDNN
|
||||
float stressIndex; // 压力指数
|
||||
float autonomicBalance; // 自主神经平衡
|
||||
bool isValid; // 是否有效
|
||||
};
|
||||
|
||||
// ==================== 心率数据处理器 ====================
|
||||
|
||||
/**
|
||||
* @brief 心率数据处理器
|
||||
*/
|
||||
class HeartRateProcessor {
|
||||
private:
|
||||
// 历史数据缓冲
|
||||
float* bpmHistory;
|
||||
int historySize;
|
||||
int historyIndex;
|
||||
int historyCount;
|
||||
|
||||
// 平滑滤波
|
||||
float lastSmoothed;
|
||||
float alpha;
|
||||
|
||||
// 统计数据
|
||||
float bpmSum;
|
||||
float bpmSumSq;
|
||||
|
||||
// 上一次有效值
|
||||
float lastValidBpm;
|
||||
unsigned long lastValidTime;
|
||||
|
||||
public:
|
||||
HeartRateProcessor(int histSize = 100);
|
||||
~HeartRateProcessor();
|
||||
|
||||
// 添加新的心率数据
|
||||
void addData(float bpm, float confidence = 80);
|
||||
|
||||
// 获取处理后的数据
|
||||
HeartRateData getData();
|
||||
|
||||
// 计算HRV估算
|
||||
HRVEstimate estimateHRV();
|
||||
|
||||
// 重置
|
||||
void reset();
|
||||
|
||||
private:
|
||||
void updateStatistics();
|
||||
float calculateVariability();
|
||||
float calculateTrend();
|
||||
};
|
||||
|
||||
// ==================== 呼吸数据处理器 ====================
|
||||
|
||||
/**
|
||||
* @brief 呼吸数据处理器
|
||||
*/
|
||||
class RespirationProcessor {
|
||||
private:
|
||||
// 历史数据缓冲
|
||||
float* rateHistory;
|
||||
int historySize;
|
||||
int historyIndex;
|
||||
int historyCount;
|
||||
|
||||
// 平滑滤波
|
||||
float lastSmoothed;
|
||||
float alpha;
|
||||
|
||||
// 上一次有效值
|
||||
float lastValidRate;
|
||||
unsigned long lastValidTime;
|
||||
|
||||
public:
|
||||
RespirationProcessor(int histSize = 50);
|
||||
~RespirationProcessor();
|
||||
|
||||
// 添加新的呼吸数据
|
||||
void addData(float rate, float confidence = 80);
|
||||
|
||||
// 获取处理后的数据
|
||||
RespirationData getData();
|
||||
|
||||
// 重置
|
||||
void reset();
|
||||
|
||||
private:
|
||||
float calculateRegularity();
|
||||
float calculateVariability();
|
||||
};
|
||||
|
||||
// ==================== 综合数据处理 ====================
|
||||
|
||||
/**
|
||||
* @brief 生理数据综合处理器
|
||||
*/
|
||||
class PhysioDataProcessor {
|
||||
private:
|
||||
HeartRateProcessor* hrProcessor;
|
||||
RespirationProcessor* rrProcessor;
|
||||
|
||||
public:
|
||||
PhysioDataProcessor();
|
||||
~PhysioDataProcessor();
|
||||
|
||||
// 更新数据
|
||||
void update(float hr, float rr, float hrConf = 80, float rrConf = 80);
|
||||
|
||||
// 获取心率数据
|
||||
HeartRateData getHeartRateData();
|
||||
|
||||
// 获取呼吸数据
|
||||
RespirationData getRespirationData();
|
||||
|
||||
// 获取HRV估算
|
||||
HRVEstimate getHRVEstimate();
|
||||
|
||||
// 重置
|
||||
void reset();
|
||||
};
|
||||
|
||||
#endif // DATA_PROCESSOR_H
|
||||
838
src/emotion_analyzer_simple.cpp
Normal file
838
src/emotion_analyzer_simple.cpp
Normal file
@@ -0,0 +1,838 @@
|
||||
/**
|
||||
* @file emotion_analyzer_simple.cpp
|
||||
* @brief 简化版情绪分析器实现
|
||||
*/
|
||||
|
||||
#include "emotion_analyzer_simple.h"
|
||||
#include <math.h>
|
||||
|
||||
// ==================== 情绪分析器实现 ====================
|
||||
|
||||
SimpleEmotionAnalyzer::SimpleEmotionAnalyzer(int histSize)
|
||||
: smoothingFactor(0.15f), historySize(histSize), historyIndex(0), historyCount(0) {
|
||||
|
||||
memset(emotionProbs, 0, sizeof(emotionProbs));
|
||||
memset(prevProbs, 0, sizeof(prevProbs));
|
||||
memset(hrWindow, 0, sizeof(hrWindow));
|
||||
memset(rrWindow, 0, sizeof(rrWindow));
|
||||
memset(hrvWindow, 0, sizeof(hrvWindow));
|
||||
windowIndex = 0;
|
||||
windowCount = 0;
|
||||
emotionHistory = new EmotionType[historySize];
|
||||
memset(emotionHistory, 0, historySize * sizeof(EmotionType));
|
||||
|
||||
memset(&baseline, 0, sizeof(baseline));
|
||||
baseline.hrResting = 72;
|
||||
baseline.rrResting = 16;
|
||||
baseline.isColdStarting = true;
|
||||
baseline.coldStartHrSum = 0;
|
||||
baseline.coldStartRrSum = 0;
|
||||
baseline.coldStartCount = 0;
|
||||
|
||||
memset(&lastResult, 0, sizeof(lastResult));
|
||||
}
|
||||
|
||||
SimpleEmotionAnalyzer::~SimpleEmotionAnalyzer() {
|
||||
delete[] emotionHistory;
|
||||
}
|
||||
|
||||
EmotionResult SimpleEmotionAnalyzer::analyze(const HeartRateData& hrData,
|
||||
const RespirationData& rrData,
|
||||
const HRVEstimate& hrvData,
|
||||
const BodyMovementData& movementData) {
|
||||
EmotionResult result;
|
||||
memset(&result, 0, sizeof(result));
|
||||
result.timestamp = millis();
|
||||
|
||||
// 检查数据有效性
|
||||
if (!hrData.isValid && !rrData.isValid) {
|
||||
result.isValid = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
// 时间窗口平滑(减少抖动)
|
||||
float smoothedHR = getSmoothedHR(hrData);
|
||||
float smoothedRR = getSmoothedRR(rrData);
|
||||
float smoothedHRV = getSmoothedHRV(hrvData);
|
||||
|
||||
// 统一推进窗口索引(确保 HR/RR/HRV 同步)
|
||||
windowIndex = (windowIndex + 1) % WINDOW_SIZE;
|
||||
if (windowCount < WINDOW_SIZE - 1) windowCount++;
|
||||
|
||||
HeartRateData smoothHrData = hrData;
|
||||
if (hrData.isValid) {
|
||||
smoothHrData.bpmSmoothed = smoothedHR;
|
||||
}
|
||||
|
||||
RespirationData smoothRrData = rrData;
|
||||
if (rrData.isValid) {
|
||||
smoothRrData.rateSmoothed = smoothedRR;
|
||||
}
|
||||
|
||||
HRVEstimate smoothHrvData = hrvData;
|
||||
if (hrvData.isValid) {
|
||||
smoothHrvData.rmssd = smoothedHRV;
|
||||
}
|
||||
|
||||
// 计算各情绪得分
|
||||
emotionProbs[EMOTION_CALM] = calculateCalmScore(smoothHrData, smoothRrData, smoothHrvData, movementData);
|
||||
emotionProbs[EMOTION_HAPPY] = calculateHappyScore(smoothHrData, smoothRrData, smoothHrvData, movementData);
|
||||
emotionProbs[EMOTION_EXCITED] = calculateExcitedScore(smoothHrData, smoothRrData, smoothHrvData, movementData);
|
||||
emotionProbs[EMOTION_ANXIOUS] = calculateAnxiousScore(smoothHrData, smoothRrData, smoothHrvData, movementData);
|
||||
emotionProbs[EMOTION_ANGRY] = calculateAngryScore(smoothHrData, smoothRrData, smoothHrvData, movementData);
|
||||
emotionProbs[EMOTION_SAD] = calculateSadScore(smoothHrData, smoothRrData, smoothHrvData, movementData);
|
||||
emotionProbs[EMOTION_STRESSED] = calculateStressedScore(smoothHrData, smoothRrData, smoothHrvData, movementData);
|
||||
emotionProbs[EMOTION_RELAXED] = calculateRelaxedScore(smoothHrData, smoothRrData, smoothHrvData, movementData);
|
||||
emotionProbs[EMOTION_UNKNOWN] = 0.02f;
|
||||
|
||||
// 归一化
|
||||
normalizeProbabilities();
|
||||
|
||||
// Top1 放大(强行制造赢家)
|
||||
int maxIdx = 0;
|
||||
for (int i = 1; i < 9; i++) {
|
||||
if (emotionProbs[i] > emotionProbs[maxIdx]) {
|
||||
maxIdx = i;
|
||||
}
|
||||
}
|
||||
emotionProbs[maxIdx] *= 1.3f;
|
||||
|
||||
// 再次归一化
|
||||
normalizeProbabilities();
|
||||
|
||||
smoothProbabilities();
|
||||
|
||||
// 找出主要情绪(同时找第二大概率)
|
||||
int secondIdx = 0;
|
||||
float maxProb = emotionProbs[0], secondProb = 0;
|
||||
maxIdx = 0; // 重置 maxIdx
|
||||
for (int i = 1; i < 9; i++) {
|
||||
if (emotionProbs[i] > maxProb) {
|
||||
secondProb = maxProb;
|
||||
secondIdx = maxIdx;
|
||||
maxProb = emotionProbs[i];
|
||||
maxIdx = i;
|
||||
} else if (emotionProbs[i] > secondProb) {
|
||||
secondProb = emotionProbs[i];
|
||||
secondIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
result.primaryEmotion = (EmotionType)maxIdx;
|
||||
result.secondaryEmotion = (EmotionType)secondIdx;
|
||||
result.confidence = maxProb;
|
||||
|
||||
// 智能 UNKNOWN 判断:低置信度 且 多情绪接近(改为 AND)
|
||||
if (maxProb < 0.20f && (maxProb - secondProb) < 0.03f) {
|
||||
result.primaryEmotion = EMOTION_UNKNOWN;
|
||||
result.confidence = maxProb;
|
||||
}
|
||||
|
||||
if (result.primaryEmotion == EMOTION_ANXIOUS ||
|
||||
result.primaryEmotion == EMOTION_ANGRY ||
|
||||
result.primaryEmotion == EMOTION_STRESSED) {
|
||||
|
||||
float combinedProb = emotionProbs[EMOTION_ANXIOUS] +
|
||||
emotionProbs[EMOTION_ANGRY] +
|
||||
emotionProbs[EMOTION_STRESSED];
|
||||
result.primaryEmotion = EMOTION_STRESSED;
|
||||
result.confidence = max(combinedProb,
|
||||
max(emotionProbs[EMOTION_ANXIOUS],
|
||||
max(emotionProbs[EMOTION_ANGRY],
|
||||
emotionProbs[EMOTION_STRESSED])));
|
||||
}
|
||||
|
||||
float hrFactor = fabs(hrData.bpmSmoothed - baseline.hrResting) / 40.0f;
|
||||
float hrvFactor = hrvData.isValid ? (1.0f - sigmoid(hrvData.rmssd, 0.02f, 40)) : 0.5f;
|
||||
float rrFactor = rrData.isValid ? fabs(rrData.rateSmoothed - baseline.rrResting) / 10.0f : 0.3f;
|
||||
|
||||
result.intensity = 0.4f
|
||||
+ 0.3f * constrain_value(hrFactor, 0.0f, 1.0f)
|
||||
+ 0.2f * constrain_value(hrvFactor, 0.0f, 1.0f)
|
||||
+ 0.1f * constrain_value(rrFactor, 0.0f, 1.0f);
|
||||
result.intensity = constrain_value(result.intensity, 0.3f, 1.0f);
|
||||
|
||||
// 计算情绪维度
|
||||
calculateDimensions(hrData, rrData);
|
||||
result.valence = lastResult.valence;
|
||||
result.arousal = lastResult.arousal;
|
||||
|
||||
// 强制分类:UNKNOWN 必须落地(根据 arousal/valence 强制选择)
|
||||
if (result.primaryEmotion == EMOTION_UNKNOWN) {
|
||||
if (result.arousal > 0.6f) {
|
||||
result.primaryEmotion = EMOTION_EXCITED;
|
||||
} else if (result.valence < -0.2f) {
|
||||
result.primaryEmotion = EMOTION_STRESSED;
|
||||
} else {
|
||||
result.primaryEmotion = EMOTION_CALM;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算压力水平
|
||||
calculateStressLevels(hrData, rrData, hrvData);
|
||||
result.stressLevel = lastResult.stressLevel;
|
||||
result.anxietyLevel = lastResult.anxietyLevel;
|
||||
result.relaxationLevel = lastResult.relaxationLevel;
|
||||
|
||||
// 自主神经活动
|
||||
if (hrvData.isValid) {
|
||||
result.sympatheticActivity = 1.0f - hrvData.autonomicBalance;
|
||||
result.parasympatheticActivity = hrvData.autonomicBalance;
|
||||
} else {
|
||||
result.sympatheticActivity = 0.5f;
|
||||
result.parasympatheticActivity = 0.5f;
|
||||
}
|
||||
|
||||
result.isValid = true;
|
||||
|
||||
// 记录历史
|
||||
emotionHistory[historyIndex] = result.primaryEmotion;
|
||||
historyIndex = (historyIndex + 1) % historySize;
|
||||
if (historyCount < historySize) historyCount++;
|
||||
|
||||
lastResult = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
void SimpleEmotionAnalyzer::setBaseline(const UserBaseline& bl) {
|
||||
baseline = bl;
|
||||
}
|
||||
|
||||
void SimpleEmotionAnalyzer::calibrateBaseline(const HeartRateData& hrData,
|
||||
const RespirationData& rrData,
|
||||
const BodyMovementData& movementData) {
|
||||
bool isRestingState = true;
|
||||
|
||||
if (movementData.isValid && movementData.movementSmoothed > 30) {
|
||||
isRestingState = false;
|
||||
}
|
||||
|
||||
if (hrData.isValid && hrData.bpmStd > 8.0f) {
|
||||
isRestingState = false;
|
||||
}
|
||||
|
||||
if (!isRestingState) return;
|
||||
|
||||
// 冷启动阶段:收集前30秒数据用于初始化基线
|
||||
if (baseline.isColdStarting && baseline.coldStartCount < UserBaseline::COLD_START_SAMPLES) {
|
||||
if (hrData.isValid) {
|
||||
baseline.coldStartHrSum += hrData.bpmSmoothed;
|
||||
}
|
||||
if (rrData.isValid) {
|
||||
baseline.coldStartRrSum += rrData.rateSmoothed;
|
||||
}
|
||||
baseline.coldStartCount++;
|
||||
|
||||
// 冷启动完成:使用平均值初始化基线
|
||||
if (baseline.coldStartCount >= UserBaseline::COLD_START_SAMPLES) {
|
||||
if (hrData.isValid && baseline.coldStartHrSum > 0) {
|
||||
baseline.hrResting = baseline.coldStartHrSum / baseline.coldStartCount;
|
||||
}
|
||||
if (rrData.isValid && baseline.coldStartRrSum > 0) {
|
||||
baseline.rrResting = baseline.coldStartRrSum / baseline.coldStartCount;
|
||||
}
|
||||
baseline.isColdStarting = false;
|
||||
baseline.isCalibrated = true;
|
||||
baseline.calibrationTime = millis();
|
||||
}
|
||||
return; // 冷启动期间不进行正常校准
|
||||
}
|
||||
|
||||
// 正常校准模式(指数移动平均)
|
||||
if (hrData.isValid) {
|
||||
baseline.hrResting = baseline.hrResting * 0.7f + hrData.bpmSmoothed * 0.3f;
|
||||
if (hrData.bpmMin > 0 && hrData.bpmMin < baseline.hrMin) {
|
||||
baseline.hrMin = hrData.bpmMin;
|
||||
}
|
||||
if (hrData.bpmMax > baseline.hrMax) {
|
||||
baseline.hrMax = hrData.bpmMax;
|
||||
}
|
||||
}
|
||||
|
||||
if (rrData.isValid) {
|
||||
baseline.rrResting = baseline.rrResting * 0.7f + rrData.rateSmoothed * 0.3f;
|
||||
}
|
||||
|
||||
baseline.isCalibrated = true;
|
||||
baseline.calibrationTime = millis();
|
||||
}
|
||||
|
||||
EmotionType SimpleEmotionAnalyzer::getRecentDominantEmotion(int seconds) {
|
||||
int counts[9] = {0};
|
||||
int entries = min(historyCount, (int)(seconds / 2)); // 假设每2秒一个记录
|
||||
|
||||
for (int i = 0; i < entries; i++) {
|
||||
int idx = (historyIndex - 1 - i + historySize) % historySize;
|
||||
counts[emotionHistory[idx]]++;
|
||||
}
|
||||
|
||||
int maxCount = 0;
|
||||
EmotionType dominant = EMOTION_CALM;
|
||||
for (int i = 0; i < 9; i++) {
|
||||
if (counts[i] > maxCount) {
|
||||
maxCount = counts[i];
|
||||
dominant = (EmotionType)i;
|
||||
}
|
||||
}
|
||||
|
||||
return dominant;
|
||||
}
|
||||
|
||||
void SimpleEmotionAnalyzer::reset() {
|
||||
memset(emotionProbs, 0, sizeof(emotionProbs));
|
||||
memset(prevProbs, 0, sizeof(prevProbs));
|
||||
memset(hrWindow, 0, sizeof(hrWindow));
|
||||
memset(rrWindow, 0, sizeof(rrWindow));
|
||||
memset(hrvWindow, 0, sizeof(hrvWindow));
|
||||
windowIndex = 0;
|
||||
windowCount = 0;
|
||||
memset(emotionHistory, 0, historySize * sizeof(EmotionType));
|
||||
historyIndex = 0;
|
||||
historyCount = 0;
|
||||
memset(&lastResult, 0, sizeof(lastResult));
|
||||
}
|
||||
|
||||
// ==================== 情绪规则匹配 ====================
|
||||
|
||||
float SimpleEmotionAnalyzer::calculateCalmScore(const HeartRateData& hr,
|
||||
const RespirationData& rr,
|
||||
const HRVEstimate& hrv,
|
||||
const BodyMovementData& movement) {
|
||||
float score = 0;
|
||||
|
||||
// 心率接近基线,变化小
|
||||
if (hr.isValid) {
|
||||
float hrNorm = fabs(normalizeHR(hr.bpmSmoothed, baseline.hrResting));
|
||||
float hrScore = gaussian(hrNorm, 0, 0.3f);
|
||||
float stabilityScore = 1.0f - constrain_value(hr.bpmStd / 12.0f, 0.0f, 1.0f);
|
||||
score += 0.45f * hrScore + 0.22f * stabilityScore;
|
||||
}
|
||||
|
||||
// 呼吸规律,正常频率
|
||||
if (rr.isValid) {
|
||||
float rrNorm = fabs(normalizeRR(rr.rateSmoothed, baseline.rrResting));
|
||||
float rateScore = gaussian(rrNorm, 0, 0.5f);
|
||||
float regularityScore = rr.regularity;
|
||||
score += 0.27f * rateScore + 0.16f * regularityScore;
|
||||
}
|
||||
|
||||
// HRV正常或较高
|
||||
if (hrv.isValid) {
|
||||
float hrvNorm = normalizeHRV(hrv.rmssd);
|
||||
float hrvScore = sigmoid(hrvNorm, 2.0f, 0.7f);
|
||||
score += 0.10f * hrvScore;
|
||||
}
|
||||
|
||||
// 体动低或适中(平静状态)
|
||||
if (movement.isValid) {
|
||||
float moveNorm = normalizeMovement(movement.movementSmoothed);
|
||||
float movementScore = gaussian(moveNorm, 0.15f, 0.2f);
|
||||
score += 0.08f * movementScore;
|
||||
}
|
||||
|
||||
return constrain_value(score, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
float SimpleEmotionAnalyzer::calculateHappyScore(const HeartRateData& hr,
|
||||
const RespirationData& rr,
|
||||
const HRVEstimate& hrv,
|
||||
const BodyMovementData& movement) {
|
||||
float score = 0;
|
||||
|
||||
// 心率略高于基线
|
||||
if (hr.isValid) {
|
||||
float hrNorm = normalizeHR(hr.bpmSmoothed, baseline.hrResting);
|
||||
float hrScore = gaussian(hrNorm, 0.3f, 0.25f);
|
||||
score += 0.45f * hrScore;
|
||||
|
||||
// 有适度变异性
|
||||
if (hr.bpmStd > 1.5f && hr.bpmStd < 10) {
|
||||
score += 0.10f;
|
||||
}
|
||||
}
|
||||
|
||||
// 呼吸正常
|
||||
if (rr.isValid) {
|
||||
float rrNorm = fabs(normalizeRR(rr.rateSmoothed, baseline.rrResting));
|
||||
float rateScore = gaussian(rrNorm, 0, 0.5f);
|
||||
score += 0.34f * rateScore;
|
||||
}
|
||||
|
||||
// HRV较高
|
||||
if (hrv.isValid) {
|
||||
float hrvNorm = normalizeHRV(hrv.rmssd);
|
||||
float hrvScore = sigmoid(hrvNorm, 2.0f, 0.9f);
|
||||
score += 0.10f * hrvScore;
|
||||
}
|
||||
|
||||
// 体动适中到较高(开心的活跃状态)
|
||||
if (movement.isValid) {
|
||||
float moveNorm = normalizeMovement(movement.movementSmoothed);
|
||||
float movementScore = gaussian(moveNorm, 0.45f, 0.25f);
|
||||
score += 0.08f * movementScore;
|
||||
}
|
||||
|
||||
return constrain_value(score, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
float SimpleEmotionAnalyzer::calculateExcitedScore(const HeartRateData& hr,
|
||||
const RespirationData& rr,
|
||||
const HRVEstimate& hrv,
|
||||
const BodyMovementData& movement) {
|
||||
float score = 0;
|
||||
|
||||
// 心率显著升高
|
||||
if (hr.isValid) {
|
||||
float hrNorm = normalizeHR(hr.bpmSmoothed, baseline.hrResting);
|
||||
float hrScore = sigmoid(hrNorm, 2.0f, 0.6f);
|
||||
score += 0.45f * hrScore;
|
||||
|
||||
// 趋势上升
|
||||
if (hr.trend > 1.5f) {
|
||||
score += 0.10f;
|
||||
}
|
||||
}
|
||||
|
||||
// 呼吸加快但规律
|
||||
if (rr.isValid) {
|
||||
float rrNorm = normalizeRR(rr.rateSmoothed, baseline.rrResting);
|
||||
float rateScore = sigmoid(rrNorm, 2.0f, 0.5f);
|
||||
score += 0.34f * rateScore;
|
||||
}
|
||||
|
||||
// HRV中等
|
||||
if (hrv.isValid) {
|
||||
float hrvNorm = normalizeHRV(hrv.rmssd);
|
||||
float hrvScore = gaussian(hrvNorm, 0.7f, 0.4f);
|
||||
score += 0.10f * hrvScore;
|
||||
}
|
||||
|
||||
// 体动较高(兴奋的活跃状态)
|
||||
if (movement.isValid) {
|
||||
float moveNorm = normalizeMovement(movement.movementSmoothed);
|
||||
float movementScore = sigmoid(moveNorm, 2.0f, 0.6f);
|
||||
score += 0.10f * movementScore;
|
||||
}
|
||||
|
||||
return constrain_value(score, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
float SimpleEmotionAnalyzer::calculateAnxiousScore(const HeartRateData& hr,
|
||||
const RespirationData& rr,
|
||||
const HRVEstimate& hrv,
|
||||
const BodyMovementData& movement) {
|
||||
float score = 0;
|
||||
|
||||
// 心率升高,变异小
|
||||
if (hr.isValid) {
|
||||
float hrNorm = normalizeHR(hr.bpmSmoothed, baseline.hrResting);
|
||||
float hrScore = sigmoid(hrNorm, 2.0f, 0.4f);
|
||||
float stdScore = 1.0f - sigmoid(hr.bpmStd / 6.0f, 2.0f, 0.5f);
|
||||
score += 0.36f * hrScore + 0.16f * stdScore;
|
||||
}
|
||||
|
||||
// HRV低
|
||||
if (hrv.isValid) {
|
||||
float hrvNorm = normalizeHRV(hrv.rmssd);
|
||||
float hrvScore = 1.0f - sigmoid(hrvNorm, 2.0f, 0.6f);
|
||||
score += 0.25f * hrvScore;
|
||||
|
||||
// 压力指数高
|
||||
float stressScore = sigmoid(hrv.stressIndex / 30.0f, 2.0f, 0.5f);
|
||||
score += 0.10f * stressScore;
|
||||
}
|
||||
|
||||
// 呼吸不规律
|
||||
if (rr.isValid) {
|
||||
float irregularityScore = 1.0f - rr.regularity;
|
||||
score += 0.16f * irregularityScore;
|
||||
}
|
||||
|
||||
// 体动适中到较高(焦虑的躁动状态)
|
||||
if (movement.isValid) {
|
||||
float moveNorm = normalizeMovement(movement.movementSmoothed);
|
||||
float movementScore = gaussian(moveNorm, 0.55f, 0.3f);
|
||||
score += 0.10f * movementScore;
|
||||
}
|
||||
|
||||
return constrain_value(score, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
float SimpleEmotionAnalyzer::calculateAngryScore(const HeartRateData& hr,
|
||||
const RespirationData& rr,
|
||||
const HRVEstimate& hrv,
|
||||
const BodyMovementData& movement) {
|
||||
float score = 0;
|
||||
|
||||
// 心率快速升高
|
||||
if (hr.isValid) {
|
||||
float hrNorm = normalizeHR(hr.bpmSmoothed, baseline.hrResting);
|
||||
float hrScore = sigmoid(hrNorm, 2.0f, 0.75f);
|
||||
float trendScore = sigmoid(hr.trend / 4.0f, 2.0f, 0.5f);
|
||||
score += 0.36f * hrScore + 0.20f * trendScore;
|
||||
}
|
||||
|
||||
// HRV低
|
||||
if (hrv.isValid) {
|
||||
float hrvNorm = normalizeHRV(hrv.rmssd);
|
||||
float hrvScore = 1.0f - sigmoid(hrvNorm, 2.0f, 0.5f);
|
||||
score += 0.25f * hrvScore;
|
||||
}
|
||||
|
||||
// 呼吸浅或不规律
|
||||
if (rr.isValid) {
|
||||
float irregularityScore = 1.0f - rr.regularity;
|
||||
score += 0.20f * irregularityScore;
|
||||
}
|
||||
|
||||
// 体动高(愤怒的激动状态)
|
||||
if (movement.isValid) {
|
||||
float moveNorm = normalizeMovement(movement.movementSmoothed);
|
||||
float movementScore = sigmoid(moveNorm, 2.0f, 0.7f);
|
||||
score += 0.10f * movementScore;
|
||||
}
|
||||
|
||||
return constrain_value(score, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
float SimpleEmotionAnalyzer::calculateSadScore(const HeartRateData& hr,
|
||||
const RespirationData& rr,
|
||||
const HRVEstimate& hrv,
|
||||
const BodyMovementData& movement) {
|
||||
float score = 0;
|
||||
|
||||
// 心率偏低
|
||||
if (hr.isValid) {
|
||||
float hrNorm = -normalizeHR(hr.bpmSmoothed, baseline.hrResting);
|
||||
float hrScore = sigmoid(hrNorm, 2.0f, 0.2f);
|
||||
score += 0.40f * hrScore;
|
||||
}
|
||||
|
||||
// 呼吸浅慢
|
||||
if (rr.isValid) {
|
||||
float rrNorm = -normalizeRR(rr.rateSmoothed, baseline.rrResting);
|
||||
float rateScore = sigmoid(rrNorm, 2.0f, 0.3f);
|
||||
float varScore = 1.0f - constrain_value(rr.variability / 4.5f, 0.0f, 1.0f);
|
||||
score += 0.28f * rateScore + 0.20f * varScore;
|
||||
}
|
||||
|
||||
// HRV可能降低
|
||||
if (hrv.isValid) {
|
||||
float hrvNorm = normalizeHRV(hrv.rmssd);
|
||||
float hrvScore = 1.0f - sigmoid(hrvNorm, 2.0f, 0.9f);
|
||||
score += 0.10f * hrvScore;
|
||||
}
|
||||
|
||||
// 体动低(悲伤的无精打采状态)
|
||||
if (movement.isValid) {
|
||||
float moveNorm = normalizeMovement(movement.movementSmoothed);
|
||||
float movementScore = 1.0f - sigmoid(moveNorm, 2.0f, 0.2f);
|
||||
score += 0.10f * movementScore;
|
||||
}
|
||||
|
||||
return constrain_value(score, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
float SimpleEmotionAnalyzer::calculateStressedScore(const HeartRateData& hr,
|
||||
const RespirationData& rr,
|
||||
const HRVEstimate& hrv,
|
||||
const BodyMovementData& movement) {
|
||||
float score = 0;
|
||||
|
||||
// 心率持续偏高
|
||||
if (hr.isValid) {
|
||||
float hrNorm = normalizeHR(hr.bpmSmoothed, baseline.hrResting);
|
||||
float hrScore = sigmoid(hrNorm, 2.0f, 0.4f);
|
||||
score += 0.36f * hrScore;
|
||||
}
|
||||
|
||||
// HRV显著降低
|
||||
if (hrv.isValid) {
|
||||
float hrvNorm = normalizeHRV(hrv.rmssd);
|
||||
float hrvScore = 1.0f - sigmoid(hrvNorm, 2.0f, 0.6f);
|
||||
float stressScore = sigmoid(hrv.stressIndex / 35.0f, 2.0f, 0.5f);
|
||||
score += 0.34f * hrvScore + 0.10f * stressScore;
|
||||
}
|
||||
|
||||
// 呼吸浅快
|
||||
if (rr.isValid) {
|
||||
float rrNorm = normalizeRR(rr.rateSmoothed, baseline.rrResting);
|
||||
float rateScore = sigmoid(rrNorm, 2.0f, 0.5f);
|
||||
score += 0.20f * rateScore;
|
||||
}
|
||||
|
||||
// 体动适中(压力的紧张状态)
|
||||
if (movement.isValid) {
|
||||
float moveNorm = normalizeMovement(movement.movementSmoothed);
|
||||
float movementScore = gaussian(moveNorm, 0.4f, 0.25f);
|
||||
score += 0.08f * movementScore;
|
||||
}
|
||||
|
||||
return constrain_value(score, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
float SimpleEmotionAnalyzer::calculateRelaxedScore(const HeartRateData& hr,
|
||||
const RespirationData& rr,
|
||||
const HRVEstimate& hrv,
|
||||
const BodyMovementData& movement) {
|
||||
float score = 0;
|
||||
|
||||
// 心率低且稳定
|
||||
if (hr.isValid) {
|
||||
float hrNorm = -normalizeHR(hr.bpmSmoothed, baseline.hrResting);
|
||||
float hrScore = sigmoid(hrNorm, 2.0f, 0.25f);
|
||||
float stabilityScore = 1.0f - constrain_value(hr.bpmStd / 8.0f, 0.0f, 1.0f);
|
||||
score += 0.31f * hrScore + 0.16f * stabilityScore;
|
||||
}
|
||||
|
||||
// HRV高
|
||||
if (hrv.isValid) {
|
||||
float hrvNorm = normalizeHRV(hrv.rmssd);
|
||||
float hrvScore = sigmoid(hrvNorm, 2.0f, 1.0f);
|
||||
score += 0.28f * hrvScore;
|
||||
}
|
||||
|
||||
// 呼吸慢且规律
|
||||
if (rr.isValid) {
|
||||
float rrNorm = -normalizeRR(rr.rateSmoothed, baseline.rrResting);
|
||||
float rateScore = sigmoid(rrNorm, 2.0f, 0.3f);
|
||||
float regularityScore = rr.regularity;
|
||||
score += 0.16f * rateScore + 0.10f * regularityScore;
|
||||
}
|
||||
|
||||
// 体动低(放松的静止状态)
|
||||
if (movement.isValid) {
|
||||
float moveNorm = normalizeMovement(movement.movementSmoothed);
|
||||
float movementScore = 1.0f - sigmoid(moveNorm, 2.0f, 0.15f);
|
||||
score += 0.10f * movementScore;
|
||||
}
|
||||
|
||||
return constrain_value(score, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
// ==================== 辅助函数 ====================
|
||||
|
||||
float SimpleEmotionAnalyzer::sigmoid(float x, float k, float x0) {
|
||||
return 1.0f / (1.0f + exp(-k * (x - x0)));
|
||||
}
|
||||
|
||||
float SimpleEmotionAnalyzer::gaussian(float x, float mean, float std) {
|
||||
float diff = x - mean;
|
||||
return exp(-(diff * diff) / (2 * std * std));
|
||||
}
|
||||
|
||||
void SimpleEmotionAnalyzer::normalizeProbabilities() {
|
||||
float sum = 0;
|
||||
for (int i = 0; i < 9; i++) {
|
||||
sum += emotionProbs[i];
|
||||
}
|
||||
if (sum > 0) {
|
||||
for (int i = 0; i < 9; i++) {
|
||||
emotionProbs[i] /= sum;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SimpleEmotionAnalyzer::smoothProbabilities() {
|
||||
for (int i = 0; i < 9; i++) {
|
||||
// 自适应平滑:变化快→快响应,变化慢→适度抑制
|
||||
float diff = fabs(emotionProbs[i] - prevProbs[i]);
|
||||
float adaptiveAlpha = (diff > 0.2f) ? 0.6f : 0.25f;
|
||||
|
||||
emotionProbs[i] = adaptiveAlpha * emotionProbs[i] +
|
||||
(1.0f - adaptiveAlpha) * prevProbs[i];
|
||||
prevProbs[i] = emotionProbs[i];
|
||||
}
|
||||
}
|
||||
|
||||
void SimpleEmotionAnalyzer::calculateDimensions(const HeartRateData& hr,
|
||||
const RespirationData& rr) {
|
||||
// 效价:正面到负面
|
||||
float positive = emotionProbs[EMOTION_HAPPY] + emotionProbs[EMOTION_EXCITED] +
|
||||
emotionProbs[EMOTION_RELAXED] + emotionProbs[EMOTION_CALM];
|
||||
float negative = emotionProbs[EMOTION_ANXIOUS] + emotionProbs[EMOTION_ANGRY] +
|
||||
emotionProbs[EMOTION_SAD] + emotionProbs[EMOTION_STRESSED];
|
||||
|
||||
lastResult.valence = (positive - negative);
|
||||
|
||||
// 唤醒度
|
||||
float highArousal = emotionProbs[EMOTION_EXCITED] + emotionProbs[EMOTION_ANXIOUS] +
|
||||
emotionProbs[EMOTION_ANGRY];
|
||||
float lowArousal = emotionProbs[EMOTION_CALM] + emotionProbs[EMOTION_RELAXED] +
|
||||
emotionProbs[EMOTION_SAD];
|
||||
float total = highArousal + lowArousal;
|
||||
|
||||
lastResult.arousal = total > 0 ? highArousal / total : 0.5f;
|
||||
}
|
||||
|
||||
void SimpleEmotionAnalyzer::calculateStressLevels(const HeartRateData& hr,
|
||||
const RespirationData& rr,
|
||||
const HRVEstimate& hrv) {
|
||||
// 压力水平(标准化归一化模型)
|
||||
float hrNorm = 0, hrvNorm = 0, rrNorm = 0;
|
||||
|
||||
if (hr.isValid) {
|
||||
hrNorm = normalizeHR(hr.bpmSmoothed, baseline.hrResting);
|
||||
hrNorm = constrain_value(hrNorm, -1.0f, 1.0f);
|
||||
}
|
||||
|
||||
if (hrv.isValid) {
|
||||
hrvNorm = 1.0f - normalizeHRV(hrv.rmssd);
|
||||
hrvNorm = constrain_value(hrvNorm, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
if (rr.isValid) {
|
||||
rrNorm = normalizeRR(rr.rateSmoothed, baseline.rrResting);
|
||||
rrNorm = constrain_value(rrNorm, -1.0f, 1.0f);
|
||||
}
|
||||
|
||||
// 标准化权重:HR(35%) + HRV(40%) + RR(25%)
|
||||
lastResult.stressLevel = constrain_value(
|
||||
(0.35f * sigmoid(hrNorm, 2.0f, 0.3f) +
|
||||
0.40f * hrvNorm +
|
||||
0.25f * sigmoid(rrNorm, 2.0f, 0.3f)) * 100.0f,
|
||||
0.0f, 100.0f
|
||||
);
|
||||
|
||||
// 焦虑水平
|
||||
lastResult.anxietyLevel = emotionProbs[EMOTION_ANXIOUS] * 70 +
|
||||
emotionProbs[EMOTION_STRESSED] * 50 +
|
||||
(1.0f - rr.regularity) * 20;
|
||||
lastResult.anxietyLevel = constrain_value(lastResult.anxietyLevel, 0.0f, 100.0f);
|
||||
|
||||
// 放松水平
|
||||
lastResult.relaxationLevel = emotionProbs[EMOTION_RELAXED] * 70 +
|
||||
emotionProbs[EMOTION_CALM] * 50;
|
||||
if (hrv.isValid) {
|
||||
lastResult.relaxationLevel += hrv.rmssd / 150.0f * 20;
|
||||
}
|
||||
lastResult.relaxationLevel = constrain_value(lastResult.relaxationLevel, 0.0f, 100.0f);
|
||||
}
|
||||
|
||||
// ==================== 时间窗口平滑函数 ====================
|
||||
|
||||
float SimpleEmotionAnalyzer::getSmoothedHR(const HeartRateData& hr) {
|
||||
if (!hr.isValid) return baseline.hrResting;
|
||||
|
||||
hrWindow[windowIndex] = hr.bpmSmoothed;
|
||||
|
||||
float sum = 0;
|
||||
int count = min(windowCount + 1, WINDOW_SIZE);
|
||||
for (int i = 0; i < count; i++) {
|
||||
int idx = (windowIndex - i + WINDOW_SIZE) % WINDOW_SIZE;
|
||||
sum += hrWindow[idx];
|
||||
}
|
||||
|
||||
return count > 0 ? sum / count : hr.bpmSmoothed;
|
||||
}
|
||||
|
||||
float SimpleEmotionAnalyzer::getSmoothedRR(const RespirationData& rr) {
|
||||
if (!rr.isValid) return baseline.rrResting;
|
||||
|
||||
rrWindow[windowIndex] = rr.rateSmoothed;
|
||||
|
||||
float sum = 0;
|
||||
int count = min(windowCount + 1, WINDOW_SIZE);
|
||||
for (int i = 0; i < count; i++) {
|
||||
int idx = (windowIndex - i + WINDOW_SIZE) % WINDOW_SIZE;
|
||||
sum += rrWindow[idx];
|
||||
}
|
||||
|
||||
return count > 0 ? sum / count : rr.rateSmoothed;
|
||||
}
|
||||
|
||||
float SimpleEmotionAnalyzer::getSmoothedHRV(const HRVEstimate& hrv) {
|
||||
if (!hrv.isValid) return 40.0f;
|
||||
|
||||
hrvWindow[windowIndex] = hrv.rmssd;
|
||||
|
||||
float sum = 0;
|
||||
int count = min(windowCount + 1, WINDOW_SIZE);
|
||||
for (int i = 0; i < count; i++) {
|
||||
int idx = (windowIndex - i + WINDOW_SIZE) % WINDOW_SIZE;
|
||||
sum += hrvWindow[idx];
|
||||
}
|
||||
|
||||
return count > 0 ? sum / count : hrv.rmssd;
|
||||
}
|
||||
|
||||
// ==================== 归一化辅助函数 ====================
|
||||
|
||||
float SimpleEmotionAnalyzer::normalizeHR(float hr, float baselineHR) {
|
||||
return (hr - baselineHR) / 20.0f;
|
||||
}
|
||||
|
||||
float SimpleEmotionAnalyzer::normalizeRR(float rr, float baselineRR) {
|
||||
return (rr - baselineRR) / 4.0f;
|
||||
}
|
||||
|
||||
float SimpleEmotionAnalyzer::normalizeHRV(float hrv) {
|
||||
return hrv / 50.0f;
|
||||
}
|
||||
|
||||
float SimpleEmotionAnalyzer::normalizeMovement(float movement) {
|
||||
return movement / 100.0f;
|
||||
}
|
||||
|
||||
// ==================== 输出格式化工具实现 ====================
|
||||
|
||||
String EmotionOutput::toBrief(const EmotionResult& result) {
|
||||
if (!result.isValid) return "检测中...";
|
||||
return String(EMOTION_NAMES[result.primaryEmotion]) + " " +
|
||||
String((int)(result.confidence * 100)) + "%";
|
||||
}
|
||||
|
||||
String EmotionOutput::toDetailed(const EmotionResult& result) {
|
||||
String output = "";
|
||||
|
||||
if (!result.isValid) {
|
||||
return "数据无效,无法分析情绪";
|
||||
}
|
||||
|
||||
output += "情绪: " + String(EMOTION_NAMES[result.primaryEmotion]);
|
||||
output += " (" + String((int)(result.confidence * 100)) + "%)";
|
||||
output += "\n强度: " + String((int)(result.intensity * 100)) + "%";
|
||||
output += "\n\n维度分析:";
|
||||
output += "\n 效价: " + String(result.valence, 2);
|
||||
output += "\n 唤醒: " + String(result.arousal, 2);
|
||||
output += "\n\n指标:";
|
||||
output += "\n 压力: " + String((int)result.stressLevel);
|
||||
output += "\n 焦虑: " + String((int)result.anxietyLevel);
|
||||
output += "\n 放松: " + String((int)result.relaxationLevel);
|
||||
output += "\n\n描述: " + String(EMOTION_DESCRIPTIONS[result.primaryEmotion]);
|
||||
output += "\n建议: " + String(EMOTION_SUGGESTIONS[result.primaryEmotion]);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
String EmotionOutput::toJson(const EmotionResult& result) {
|
||||
String json = "{";
|
||||
json += "\"emotion\":\"" + String(EMOTION_NAMES[result.primaryEmotion]) + "\",";
|
||||
json += "\"confidence\":" + String(result.confidence, 3) + ",";
|
||||
json += "\"intensity\":" + String(result.intensity, 3) + ",";
|
||||
json += "\"valence\":" + String(result.valence, 3) + ",";
|
||||
json += "\"arousal\":" + String(result.arousal, 3) + ",";
|
||||
json += "\"stressLevel\":" + String(result.stressLevel, 1) + ",";
|
||||
json += "\"anxietyLevel\":" + String(result.anxietyLevel, 1) + ",";
|
||||
json += "\"relaxationLevel\":" + String(result.relaxationLevel, 1) + ",";
|
||||
json += "\"sympatheticActivity\":" + String(result.sympatheticActivity, 3) + ",";
|
||||
json += "\"parasympatheticActivity\":" + String(result.parasympatheticActivity, 3) + ",";
|
||||
json += "\"isValid\":" + String(result.isValid ? "true" : "false") + ",";
|
||||
json += "\"timestamp\":" + String(result.timestamp);
|
||||
json += "}";
|
||||
return json;
|
||||
}
|
||||
|
||||
String EmotionOutput::toCsv(const EmotionResult& result, unsigned long timestamp) {
|
||||
String csv = "";
|
||||
csv += String(timestamp) + ",";
|
||||
csv += String(result.primaryEmotion) + ",";
|
||||
csv += String(result.confidence, 3) + ",";
|
||||
csv += String(result.intensity, 3) + ",";
|
||||
csv += String(result.valence, 3) + ",";
|
||||
csv += String(result.arousal, 3) + ",";
|
||||
csv += String(result.stressLevel, 1) + ",";
|
||||
csv += String(result.anxietyLevel, 1) + ",";
|
||||
csv += String(result.relaxationLevel, 1);
|
||||
return csv;
|
||||
}
|
||||
189
src/emotion_analyzer_simple.h
Normal file
189
src/emotion_analyzer_simple.h
Normal file
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* @file emotion_analyzer_simple.h
|
||||
* @brief 简化版情绪分析器头文件
|
||||
* @description 适配雷达传感器数据,分析用户情绪状态
|
||||
*/
|
||||
|
||||
#ifndef EMOTION_ANALYZER_SIMPLE_H
|
||||
#define EMOTION_ANALYZER_SIMPLE_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "config.h"
|
||||
#include "data_processor.h"
|
||||
|
||||
// ==================== 情绪分析结果 ====================
|
||||
|
||||
/**
|
||||
* @brief 情绪分析结果
|
||||
*/
|
||||
struct EmotionResult {
|
||||
// 主要情绪
|
||||
EmotionType primaryEmotion;
|
||||
EmotionType secondaryEmotion; // 次要情绪
|
||||
float confidence; // 置信度 0-1
|
||||
float intensity; // 强度 0-1
|
||||
|
||||
// 情绪维度
|
||||
float valence; // 效价 -1到+1
|
||||
float arousal; // 唤醒度 0-1
|
||||
|
||||
// 压力评估
|
||||
float stressLevel; // 压力水平 0-100
|
||||
float anxietyLevel; // 焦虑水平 0-100
|
||||
float relaxationLevel; // 放松水平 0-100
|
||||
|
||||
// 自主神经评估
|
||||
float sympatheticActivity; // 交感神经活动
|
||||
float parasympatheticActivity; // 副交感神经活动
|
||||
|
||||
// 数据有效性
|
||||
bool isValid;
|
||||
unsigned long timestamp;
|
||||
};
|
||||
|
||||
// ==================== 用户基线 ====================
|
||||
|
||||
/**
|
||||
* @brief 用户生理基线
|
||||
*/
|
||||
struct UserBaseline {
|
||||
float hrResting; // 静息心率
|
||||
float hrMin; // 最小心率
|
||||
float hrMax; // 最大心率
|
||||
float rrResting; // 静息呼吸频率
|
||||
bool isCalibrated;
|
||||
unsigned long calibrationTime;
|
||||
|
||||
// 冷启动缓冲
|
||||
static const int COLD_START_SAMPLES = 15; // 约30秒数据(2秒/样本)
|
||||
float coldStartHrSum;
|
||||
float coldStartRrSum;
|
||||
int coldStartCount;
|
||||
bool isColdStarting;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 体动数据
|
||||
*/
|
||||
struct BodyMovementData {
|
||||
uint8_t movement; // 体动参数 0-100
|
||||
float movementSmoothed; // 平滑后体动
|
||||
float movementMean; // 平均体动
|
||||
float movementStd; // 体动标准差
|
||||
float activityLevel; // 活动水平 0-1
|
||||
bool isValid; // 数据是否有效
|
||||
unsigned long timestamp; // 时间戳
|
||||
};
|
||||
|
||||
// ==================== 情绪分析器 ====================
|
||||
|
||||
/**
|
||||
* @brief 简化版情绪分析器
|
||||
*/
|
||||
class SimpleEmotionAnalyzer {
|
||||
private:
|
||||
// 用户基线
|
||||
UserBaseline baseline;
|
||||
|
||||
// 上一次结果
|
||||
EmotionResult lastResult;
|
||||
|
||||
// 情绪概率缓冲(用于平滑)
|
||||
float emotionProbs[9];
|
||||
float smoothingFactor;
|
||||
float prevProbs[9];
|
||||
|
||||
// 历史情绪记录
|
||||
EmotionType* emotionHistory;
|
||||
int historySize;
|
||||
int historyIndex;
|
||||
int historyCount;
|
||||
|
||||
// 时间窗口缓冲(用于输入数据平滑)
|
||||
static const int WINDOW_SIZE = 15;
|
||||
float hrWindow[WINDOW_SIZE];
|
||||
float rrWindow[WINDOW_SIZE];
|
||||
float hrvWindow[WINDOW_SIZE];
|
||||
int windowIndex;
|
||||
int windowCount;
|
||||
|
||||
public:
|
||||
SimpleEmotionAnalyzer(int histSize = 30);
|
||||
~SimpleEmotionAnalyzer();
|
||||
|
||||
// 分析情绪
|
||||
EmotionResult analyze(const HeartRateData& hrData,
|
||||
const RespirationData& rrData,
|
||||
const HRVEstimate& hrvData,
|
||||
const BodyMovementData& movementData);
|
||||
|
||||
// 设置基线
|
||||
void setBaseline(const UserBaseline& bl);
|
||||
UserBaseline getBaseline() const { return baseline; }
|
||||
|
||||
// 自动校准基线
|
||||
void calibrateBaseline(const HeartRateData& hrData,
|
||||
const RespirationData& rrData,
|
||||
const BodyMovementData& movementData);
|
||||
|
||||
// 设置平滑因子
|
||||
void setSmoothing(float factor) { smoothingFactor = constrain_value(factor, 0.0f, 1.0f); }
|
||||
|
||||
// 获取最近的主要情绪
|
||||
EmotionType getRecentDominantEmotion(int seconds = 60);
|
||||
|
||||
// 重置
|
||||
void reset();
|
||||
|
||||
private:
|
||||
// 情绪规则匹配
|
||||
float calculateCalmScore(const HeartRateData& hr, const RespirationData& rr, const HRVEstimate& hrv, const BodyMovementData& movement);
|
||||
float calculateHappyScore(const HeartRateData& hr, const RespirationData& rr, const HRVEstimate& hrv, const BodyMovementData& movement);
|
||||
float calculateExcitedScore(const HeartRateData& hr, const RespirationData& rr, const HRVEstimate& hrv, const BodyMovementData& movement);
|
||||
float calculateAnxiousScore(const HeartRateData& hr, const RespirationData& rr, const HRVEstimate& hrv, const BodyMovementData& movement);
|
||||
float calculateAngryScore(const HeartRateData& hr, const RespirationData& rr, const HRVEstimate& hrv, const BodyMovementData& movement);
|
||||
float calculateSadScore(const HeartRateData& hr, const RespirationData& rr, const HRVEstimate& hrv, const BodyMovementData& movement);
|
||||
float calculateStressedScore(const HeartRateData& hr, const RespirationData& rr, const HRVEstimate& hrv, const BodyMovementData& movement);
|
||||
float calculateRelaxedScore(const HeartRateData& hr, const RespirationData& rr, const HRVEstimate& hrv, const BodyMovementData& movement);
|
||||
|
||||
// 辅助函数
|
||||
float sigmoid(float x, float k = 1.0f, float x0 = 0.0f);
|
||||
float gaussian(float x, float mean, float std);
|
||||
void normalizeProbabilities();
|
||||
void smoothProbabilities();
|
||||
void calculateDimensions(const HeartRateData& hr, const RespirationData& rr);
|
||||
void calculateStressLevels(const HeartRateData& hr, const RespirationData& rr, const HRVEstimate& hrv);
|
||||
|
||||
// 时间窗口平滑
|
||||
float getSmoothedHR(const HeartRateData& hr);
|
||||
float getSmoothedRR(const RespirationData& rr);
|
||||
float getSmoothedHRV(const HRVEstimate& hrv);
|
||||
|
||||
// 归一化辅助函数(统一尺度)
|
||||
float normalizeHR(float hr, float baseline);
|
||||
float normalizeRR(float rr, float baseline);
|
||||
float normalizeHRV(float hrv);
|
||||
float normalizeMovement(float movement);
|
||||
};
|
||||
|
||||
// ==================== 输出格式化工具 ====================
|
||||
|
||||
/**
|
||||
* @brief 情绪结果格式化工具
|
||||
*/
|
||||
class EmotionOutput {
|
||||
public:
|
||||
// 简洁输出
|
||||
static String toBrief(const EmotionResult& result);
|
||||
|
||||
// 详细输出
|
||||
static String toDetailed(const EmotionResult& result);
|
||||
|
||||
// JSON输出
|
||||
static String toJson(const EmotionResult& result);
|
||||
|
||||
// CSV输出
|
||||
static String toCsv(const EmotionResult& result, unsigned long timestamp);
|
||||
};
|
||||
|
||||
#endif // EMOTION_ANALYZER_SIMPLE_H
|
||||
638
src/main.cpp
638
src/main.cpp
@@ -5,616 +5,76 @@
|
||||
#include <Preferences.h>
|
||||
#include "wifi_manager.h"
|
||||
#include "radar_manager.h"
|
||||
|
||||
// ESP32 GPIO控制演示
|
||||
#define BOOT_BUTTON_PIN 0 // Boot按钮引脚
|
||||
#define NETWORK_LED_PIN 5 // 网络状态LED指示灯开发板48引脚,雷达板5引脚
|
||||
#define CONFIG_CLEAR_PIN 4 // 配置清除指示灯
|
||||
#define GPIO8 8 // 自定义GPIO8
|
||||
#define GPIO9 9 // 自定义GPIO9
|
||||
|
||||
uint8_t WiFi_Connect_First_bit = 1; // WiFi首次连接标志位
|
||||
|
||||
// 配置清除指示灯状态枚举
|
||||
enum ConfigClearStatus {
|
||||
CONFIG_NORMAL, // 正常运行 - LOW
|
||||
CONFIG_PREPARING, // 准备清除 - HIGH
|
||||
CONFIG_CLEARING, // 清除过程中 - 呼吸灯
|
||||
CONFIG_COMPLETED // 清除完成 - 快速闪烁3次
|
||||
};
|
||||
|
||||
NetworkStatus currentNetworkStatus = NET_INITIAL; // 当前网络状态
|
||||
unsigned long lastBlinkTime = 0; // 上次LED闪烁时间
|
||||
bool ledState = false; // LED状态
|
||||
int breatheValue = 0; // 呼吸灯当前亮度值
|
||||
bool breatheIncreasing = true; // 呼吸灯是否在增加亮度
|
||||
|
||||
ConfigClearStatus currentConfigClearStatus = CONFIG_NORMAL; // 当前配置清除状态
|
||||
unsigned long lastConfigBlinkTime = 0; // 上次配置清除LED闪烁时间
|
||||
bool configLedState = false; // 配置清除LED状态
|
||||
int configBreatheValue = 0; // 配置清除呼吸灯当前亮度值
|
||||
bool configBreatheIncreasing = true; // 配置清除呼吸灯是否在增加亮度
|
||||
|
||||
const int SLOW_BLINK_INTERVAL = 1000; // 慢闪间隔(毫秒)
|
||||
const int FAST_BLINK_INTERVAL = 200; // 快闪间隔(毫秒)
|
||||
const int BREATHE_INTERVAL = 40; // 呼吸灯更新间隔(毫秒)
|
||||
const int BREATHE_MIN = 0; // 呼吸灯最小亮度值
|
||||
const int BREATHE_MAX = 155; // 呼吸灯最大亮度值
|
||||
const int BREATHE_STEP = 5; // 呼吸灯亮度步进值
|
||||
|
||||
const uint16_t MIN_DEVICE_ID = 1000; // 最小设备ID
|
||||
const uint16_t MAX_DEVICE_ID = 1999; // 最大设备ID
|
||||
|
||||
const unsigned long CLEAR_CONFIG_DURATION = 3000; // 清除配置持续时间(毫秒)
|
||||
#include "data_processor.h"
|
||||
#include "emotion_analyzer_simple.h"
|
||||
#include "tasks_manager.h"
|
||||
|
||||
Preferences preferences; // Flash存储对象
|
||||
WiFiManager wifiManager; // WiFi管理器对象
|
||||
|
||||
uint16_t currentDeviceId = 0000; // 当前设备ID
|
||||
bool clearConfigRequested = false; // 清除配置请求标志
|
||||
bool forceLedOff = false; // 强制关闭LED标志
|
||||
|
||||
void configClearLedTask(void *parameter);
|
||||
void bootButtonMonitorTask(void *parameter);
|
||||
void checkBootButton();
|
||||
void clearStoredConfig();
|
||||
void ledControlTask(void *parameter);
|
||||
void setNetworkStatus(NetworkStatus status);
|
||||
void wifiMonitorTask(void *parameter);
|
||||
void WiFiEvent(WiFiEvent_t event);
|
||||
void loadDeviceId();
|
||||
void saveDeviceId();
|
||||
|
||||
String getFieldNameByProtocolId(int protocolId);
|
||||
|
||||
/**
|
||||
* @brief 根据协议ID获取字段名称
|
||||
* 将协议ID映射到对应的字段名称,用于数据序列化和反序列化
|
||||
* @param protocolId 协议ID
|
||||
* @return 对应的字段名称字符串
|
||||
*/
|
||||
String getFieldNameByProtocolId(int protocolId) {
|
||||
switch(protocolId) {
|
||||
case 1:
|
||||
return "heartRate";
|
||||
case 2:
|
||||
return "breathingRate";
|
||||
case 13:
|
||||
return "personDetected";
|
||||
case 14:
|
||||
return "humanActivity";
|
||||
case 15:
|
||||
return "humanDistance";
|
||||
case 16:
|
||||
return "humanPosition";
|
||||
case 17:
|
||||
return "sleepState";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查Boot按钮状态
|
||||
* 在启动时检查Boot按钮是否被按下,如果按下则进入配置清除流程
|
||||
*/
|
||||
void checkBootButton() {
|
||||
Serial.println("🔍 检查Boot按钮状态...");
|
||||
|
||||
pinMode(BOOT_BUTTON_PIN, INPUT_PULLUP);
|
||||
|
||||
delay(10);
|
||||
|
||||
int buttonState = digitalRead(BOOT_BUTTON_PIN);
|
||||
Serial.printf("📊 Boot按钮状态: %s\n", buttonState == LOW ? "按下" : "释放");
|
||||
|
||||
if (buttonState == LOW) {
|
||||
Serial.println("⚠️ 检测到Boot按钮按下,请释放按钮后继续启动");
|
||||
Serial.println("⏰ 等待按钮释放...");
|
||||
|
||||
while (digitalRead(BOOT_BUTTON_PIN) == LOW) {
|
||||
delay(100);
|
||||
}
|
||||
|
||||
Serial.println("✅ Boot按钮已释放,正常启动");
|
||||
} else {
|
||||
Serial.println("✅ Boot按钮未按下,正常启动");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 加载设备ID
|
||||
* 从Flash中读取保存的设备ID
|
||||
*/
|
||||
void loadDeviceId() {
|
||||
currentDeviceId = preferences.getUShort("deviceId", 1001);
|
||||
Serial.printf("从Flash加载设备ID: %u\n", currentDeviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 保存设备ID
|
||||
* 将设备ID保存到Flash中
|
||||
*/
|
||||
void saveDeviceId() {
|
||||
preferences.putUShort("deviceId", currentDeviceId);
|
||||
Serial.printf("设备ID已保存到Flash: %u\n", currentDeviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 清除存储的配置
|
||||
* 清除Flash中保存的所有配置,包括设备ID和WiFi配置
|
||||
*/
|
||||
void clearStoredConfig() {
|
||||
Serial.println("🧹 开始清除存储的配置...");
|
||||
|
||||
uint16_t oldDeviceId = preferences.getUShort("deviceId", 0);
|
||||
|
||||
preferences.remove("deviceId");
|
||||
preferences.remove("wifi_first");
|
||||
|
||||
wifiManager.clearAllConfigs();
|
||||
|
||||
Serial.println("✅ 配置已清除完成");
|
||||
Serial.printf("🗑️ 被清除的设备ID: %u\n", oldDeviceId);
|
||||
|
||||
currentDeviceId = 1001;
|
||||
WiFi_Connect_First_bit = 1;
|
||||
|
||||
WiFi.disconnect(true);
|
||||
setNetworkStatus(NET_DISCONNECTED);
|
||||
|
||||
Serial.println("🔄 已清除Flash与内存中的配置,请重新配置WiFi和设备ID");
|
||||
|
||||
if (deviceConnected) {
|
||||
sendStatusToBLE();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 配置清除LED控制任务
|
||||
* 根据配置清除状态控制CONFIG_CLEAR_PIN引脚的LED显示
|
||||
* @param parameter 任务参数(未使用)
|
||||
*/
|
||||
void configClearLedTask(void *parameter) {
|
||||
while (1) {
|
||||
switch (currentConfigClearStatus) {
|
||||
case CONFIG_NORMAL:
|
||||
analogWrite(CONFIG_CLEAR_PIN, 0);
|
||||
break;
|
||||
|
||||
case CONFIG_PREPARING:
|
||||
analogWrite(CONFIG_CLEAR_PIN, 255);
|
||||
break;
|
||||
|
||||
case CONFIG_CLEARING:
|
||||
if (millis() - lastConfigBlinkTime >= BREATHE_INTERVAL) {
|
||||
analogWrite(CONFIG_CLEAR_PIN, configBreatheValue);
|
||||
|
||||
if (configBreatheIncreasing) {
|
||||
configBreatheValue += 5;
|
||||
if (configBreatheValue >= BREATHE_MAX) {
|
||||
configBreatheValue = BREATHE_MAX;
|
||||
configBreatheIncreasing = false;
|
||||
}
|
||||
} else {
|
||||
configBreatheValue -= 5;
|
||||
if (configBreatheValue <= BREATHE_MIN) {
|
||||
configBreatheValue = BREATHE_MIN;
|
||||
configBreatheIncreasing = true;
|
||||
}
|
||||
}
|
||||
lastConfigBlinkTime = millis();
|
||||
}
|
||||
break;
|
||||
|
||||
case CONFIG_COMPLETED:
|
||||
if (millis() - lastConfigBlinkTime >= FAST_BLINK_INTERVAL) {
|
||||
configLedState = !configLedState;
|
||||
digitalWrite(CONFIG_CLEAR_PIN, configLedState ? HIGH : LOW);
|
||||
lastConfigBlinkTime = millis();
|
||||
|
||||
static int blinkCount = 0;
|
||||
blinkCount++;
|
||||
|
||||
if (blinkCount >= 6) {
|
||||
blinkCount = 0;
|
||||
currentConfigClearStatus = CONFIG_NORMAL;
|
||||
digitalWrite(CONFIG_CLEAR_PIN, LOW);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief BOOT按钮监控任务
|
||||
* 持续监控BOOT按钮状态,检测长按3秒事件并触发配置清除
|
||||
* @param parameter 任务参数(未使用)
|
||||
*/
|
||||
void bootButtonMonitorTask(void *parameter) {
|
||||
Serial.println("🔍 启动BOOT按钮监控任务...");
|
||||
|
||||
pinMode(BOOT_BUTTON_PIN, INPUT_PULLUP);
|
||||
|
||||
unsigned long buttonPressStartTime = 0;
|
||||
bool buttonPressed = false;
|
||||
|
||||
while (1) {
|
||||
int buttonState = digitalRead(BOOT_BUTTON_PIN);
|
||||
|
||||
if (buttonState == LOW && !buttonPressed) {
|
||||
buttonPressed = true;
|
||||
buttonPressStartTime = millis();
|
||||
Serial.println("⚠️ 检测到BOOT按钮按下,长按3秒将清除配置");
|
||||
|
||||
currentConfigClearStatus = CONFIG_PREPARING;
|
||||
}
|
||||
else if (buttonState == HIGH && buttonPressed) {
|
||||
if (!clearConfigRequested) {
|
||||
currentConfigClearStatus = CONFIG_NORMAL;
|
||||
Serial.println("❌ 按钮释放,取消清除操作");
|
||||
}
|
||||
buttonPressed = false;
|
||||
}
|
||||
|
||||
if (buttonPressed && (millis() - buttonPressStartTime >= CLEAR_CONFIG_DURATION)) {
|
||||
if (!clearConfigRequested) {
|
||||
clearConfigRequested = true;
|
||||
forceLedOff = true;
|
||||
ledcWrite(0, 0);
|
||||
Serial.println("✅ 长按3秒确认,将清除配置");
|
||||
Serial.println("💡 网络LED已强制熄灭");
|
||||
|
||||
clearStoredConfig();
|
||||
|
||||
Serial.println("🔄 配置清除完成,LED将闪烁3次表示完成...");
|
||||
|
||||
analogWrite(CONFIG_CLEAR_PIN, 0);
|
||||
Serial.println("🔄 系统即将重启...");
|
||||
|
||||
vTaskDelay(1000 / portTICK_PERIOD_MS);
|
||||
ESP.restart();
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(50 / portTICK_PERIOD_MS);
|
||||
|
||||
esp_task_wdt_reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief LED控制任务
|
||||
* 根据网络状态控制NETWORK_LED_PIN引脚的LED显示
|
||||
* 支持慢闪、快闪和呼吸灯效果
|
||||
* @param parameter 任务参数(未使用)
|
||||
*/
|
||||
void ledControlTask(void *parameter) {
|
||||
while (1) {
|
||||
if (forceLedOff) {
|
||||
ledcWrite(0, 0);
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (currentNetworkStatus) {
|
||||
case NET_INITIAL:
|
||||
case NET_DISCONNECTED:
|
||||
if (millis() - lastBlinkTime >= SLOW_BLINK_INTERVAL) {
|
||||
ledState = !ledState;
|
||||
if(ledState) {
|
||||
ledcWrite(0, 255);
|
||||
} else {
|
||||
ledcWrite(0, 0);
|
||||
}
|
||||
lastBlinkTime = millis();
|
||||
}
|
||||
break;
|
||||
|
||||
case NET_CONNECTING:
|
||||
if (millis() - lastBlinkTime >= FAST_BLINK_INTERVAL) {
|
||||
ledState = !ledState;
|
||||
if(ledState) {
|
||||
ledcWrite(0, 255);
|
||||
} else {
|
||||
ledcWrite(0, 0);
|
||||
}
|
||||
lastBlinkTime = millis();
|
||||
}
|
||||
break;
|
||||
|
||||
case NET_CONNECTED:
|
||||
if (millis() - lastBlinkTime >= BREATHE_INTERVAL) {
|
||||
ledcWrite(0, breatheValue);
|
||||
|
||||
if (breatheIncreasing) {
|
||||
breatheValue += BREATHE_STEP;
|
||||
if (breatheValue >= BREATHE_MAX) {
|
||||
breatheValue = BREATHE_MAX;
|
||||
breatheIncreasing = false;
|
||||
}
|
||||
} else {
|
||||
breatheValue -= BREATHE_STEP;
|
||||
if (breatheValue <= BREATHE_MIN) {
|
||||
breatheValue = BREATHE_MIN;
|
||||
breatheIncreasing = true;
|
||||
}
|
||||
}
|
||||
lastBlinkTime = millis();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置网络状态
|
||||
* 更新当前网络状态,并重置呼吸灯参数
|
||||
* @param status 网络状态
|
||||
*/
|
||||
void setNetworkStatus(NetworkStatus status) {
|
||||
currentNetworkStatus = status;
|
||||
|
||||
if (status == NET_CONNECTED) {
|
||||
breatheValue = BREATHE_MIN;
|
||||
breatheIncreasing = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief WiFi事件处理函数
|
||||
* 处理WiFi连接状态变化事件,更新网络状态和LED显示
|
||||
* @param event WiFi事件类型
|
||||
*/
|
||||
void WiFiEvent(WiFiEvent_t event) {
|
||||
switch (event) {
|
||||
case ARDUINO_EVENT_WIFI_STA_START:
|
||||
setNetworkStatus(NET_INITIAL);
|
||||
break;
|
||||
|
||||
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
|
||||
setNetworkStatus(NET_CONNECTING);
|
||||
break;
|
||||
|
||||
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
|
||||
setNetworkStatus(NET_CONNECTED);
|
||||
break;
|
||||
|
||||
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
|
||||
setNetworkStatus(NET_DISCONNECTED);
|
||||
break;
|
||||
|
||||
case ARDUINO_EVENT_WIFI_STA_STOP:
|
||||
setNetworkStatus(NET_DISCONNECTED);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief WiFi监控任务
|
||||
* 定期更新WiFi管理器状态,处理WiFi重连等逻辑
|
||||
* @param parameter 任务参数(未使用)
|
||||
*/
|
||||
void wifiMonitorTask(void *parameter) {
|
||||
Serial.println("📡 WiFi监控任务启动");
|
||||
|
||||
while(1) {
|
||||
wifiManager.update();
|
||||
vTaskDelay(500 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 系统初始化函数
|
||||
* 初始化所有硬件外设、任务和通信模块
|
||||
*/
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
checkBootButton();
|
||||
|
||||
analogWrite(CONFIG_CLEAR_PIN, 0);
|
||||
|
||||
Serial.println("🚀 ESP32-R60ABD1系统启动");
|
||||
Serial.println("🔧 初始化系统组件...");
|
||||
|
||||
pinMode(BOOT_BUTTON_PIN, INPUT);
|
||||
pinMode(NETWORK_LED_PIN, OUTPUT);
|
||||
pinMode(CONFIG_CLEAR_PIN, OUTPUT);
|
||||
pinMode(GPIO8, OUTPUT);
|
||||
pinMode(GPIO9, OUTPUT);
|
||||
|
||||
digitalWrite(CONFIG_CLEAR_PIN, LOW);
|
||||
digitalWrite(GPIO8, LOW);
|
||||
digitalWrite(GPIO9, LOW);
|
||||
digitalWrite(NETWORK_LED_PIN, LOW);
|
||||
digitalWrite(CONFIG_CLEAR_PIN, LOW);
|
||||
|
||||
ledcSetup(0, 5000, 8);
|
||||
ledcSetup(1, 5000, 8);
|
||||
ledcAttachPin(NETWORK_LED_PIN, 0);
|
||||
ledcAttachPin(CONFIG_CLEAR_PIN, 1);
|
||||
|
||||
WiFi.onEvent(WiFiEvent);
|
||||
|
||||
setNetworkStatus(NET_INITIAL);
|
||||
|
||||
esp_task_wdt_init(30, true);
|
||||
esp_task_wdt_add(NULL);
|
||||
|
||||
preferences.begin("radar_data", false);
|
||||
|
||||
wifiManager.begin();
|
||||
|
||||
Serial.println("💾 加载设备配置...");
|
||||
loadDeviceId();
|
||||
|
||||
Serial.println("🏗️ 初始化雷达管理器...");
|
||||
initRadarManager();
|
||||
|
||||
xTaskCreate(
|
||||
configClearLedTask,
|
||||
"Config Clear LED Task",
|
||||
2048,
|
||||
NULL,
|
||||
1,
|
||||
NULL
|
||||
);
|
||||
|
||||
xTaskCreate(
|
||||
bootButtonMonitorTask,
|
||||
"Boot Button Monitor Task",
|
||||
2048,
|
||||
NULL,
|
||||
1,
|
||||
NULL
|
||||
);
|
||||
|
||||
xTaskCreate(
|
||||
ledControlTask,
|
||||
"LED Control Task",
|
||||
2048,
|
||||
NULL,
|
||||
1,
|
||||
NULL
|
||||
);
|
||||
|
||||
xTaskCreate(
|
||||
wifiMonitorTask,
|
||||
"WiFi Monitor Task",
|
||||
4096,
|
||||
NULL,
|
||||
2,
|
||||
NULL
|
||||
);
|
||||
|
||||
Serial.println("✅ FreeRTOS任务创建成功");
|
||||
|
||||
Serial.println("📶 初始化BLE服务...");
|
||||
String deviceName = "Radar_" + String(currentDeviceId);
|
||||
BLEDevice::init(deviceName.c_str());
|
||||
pServer = BLEDevice::createServer();
|
||||
pServer->setCallbacks(new MyServerCallbacks());
|
||||
|
||||
BLEService *pService = pServer->createService(SERVICE_UUID);
|
||||
pCharacteristic = pService->createCharacteristic(
|
||||
CHARACTERISTIC_UUID,
|
||||
BLECharacteristic::PROPERTY_READ |
|
||||
BLECharacteristic::PROPERTY_WRITE |
|
||||
BLECharacteristic::PROPERTY_NOTIFY
|
||||
);
|
||||
pCharacteristic->setCallbacks(new MyCallbacks());
|
||||
pCharacteristic->addDescriptor(new BLE2902());
|
||||
|
||||
pService->start();
|
||||
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
|
||||
pAdvertising->addServiceUUID(SERVICE_UUID);
|
||||
pAdvertising->setScanResponse(true);
|
||||
pAdvertising->setMinPreferred(0x06);
|
||||
pAdvertising->setMinPreferred(0x12);
|
||||
BLEDevice::startAdvertising();
|
||||
|
||||
Serial.println(String("BLE已启动,设备名称: Radar_") + String(currentDeviceId));
|
||||
|
||||
Serial.println("🌐 检查WiFi配置...");
|
||||
if (wifiManager.getSavedNetworkCount() > 0) {
|
||||
Serial.printf("💾 检测到 %d 个已保存的WiFi配置,尝试连接...\n", wifiManager.getSavedNetworkCount());
|
||||
if (wifiManager.initializeWiFi()) {
|
||||
Serial.println("✅ WiFi连接成功!");
|
||||
} else {
|
||||
Serial.println("❌ WiFi连接失败,请通过BLE重新配置");
|
||||
}
|
||||
} else {
|
||||
Serial.println("⚠️ 未检测到WiFi配置,请通过BLE进行网络配置");
|
||||
}
|
||||
|
||||
size_t wifi_first_len = preferences.getBytes("wifi_first", &WiFi_Connect_First_bit, sizeof(WiFi_Connect_First_bit));
|
||||
if (wifi_first_len == sizeof(WiFi_Connect_First_bit)) {
|
||||
Serial.printf("从Flash读取 WiFi_Connect_First_bit: %u\n", WiFi_Connect_First_bit);
|
||||
} else {
|
||||
Serial.println("Flash中无 wifi_first 条目,保留内存中原始值");
|
||||
}
|
||||
|
||||
if(WiFi_Connect_First_bit == 0)
|
||||
{
|
||||
unsigned long wifiWaitStart = millis();
|
||||
unsigned long lastWifiWaitPrint = 0;
|
||||
const unsigned long WIFI_WAIT_TIMEOUT = 15000;
|
||||
while (WiFi.status() != WL_CONNECTED && (millis() - wifiWaitStart) < WIFI_WAIT_TIMEOUT) {
|
||||
if (millis() - lastWifiWaitPrint >= 1000) {
|
||||
Serial.println("等待WiFi连接...");
|
||||
lastWifiWaitPrint = millis();
|
||||
}
|
||||
yield();
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
Serial.println("WiFi连接成功!");
|
||||
initR60ABD1();
|
||||
|
||||
Serial.println("🎉 系统初始化完成,等待雷达数据...");
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.println("🌅 启动时发送睡眠数据到数据库");
|
||||
Serial.begin(115200); // 初始化串口通信
|
||||
checkBootButton(); // 检查Boot按钮状态
|
||||
analogWrite(CONFIG_CLEAR_PIN, 0); // 关闭配置清除指示灯
|
||||
WiFi.onEvent(WiFiEvent); // 注册WiFi事件处理函数
|
||||
setNetworkStatus(NET_INITIAL); // 初始化网络状态
|
||||
esp_task_wdt_init(30, true); // 初始化看门狗定时器
|
||||
esp_task_wdt_add(NULL); // 将主任务添加到看门狗
|
||||
preferences.begin("radar_data", false); // 初始化Flash存储
|
||||
loadDeviceId(); // 加载设备ID
|
||||
initRadarManager(); // 初始化雷达管理器
|
||||
initAllTasks(); // 创建所有FreeRTOS任务
|
||||
if (WiFi.status() == WL_CONNECTED) // 启动时发送睡眠数据
|
||||
sendSleepDataToInfluxDB();
|
||||
}
|
||||
Serial.println("✅ 系统初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 主循环函数
|
||||
* 处理BLE连接状态和定期发送雷达命令
|
||||
* 主循环只负责看门狗重置,保持空闲
|
||||
*/
|
||||
void loop() {
|
||||
esp_task_wdt_reset();
|
||||
|
||||
if (!deviceConnected && oldDeviceConnected) {
|
||||
vTaskDelay(500 / portTICK_PERIOD_MS);
|
||||
pServer->startAdvertising();
|
||||
Serial.println("开始BLE广播");
|
||||
oldDeviceConnected = deviceConnected;
|
||||
}
|
||||
if (deviceConnected && !oldDeviceConnected) {
|
||||
oldDeviceConnected = deviceConnected;
|
||||
}
|
||||
{
|
||||
static const uint8_t radar_cmds[][3] = {
|
||||
{0x84, 0x81, 0x0F},
|
||||
{0x84, 0x8D, 0x0F},
|
||||
{0x84, 0x8F, 0x0F},
|
||||
{0x84, 0x8E, 0x0F},
|
||||
{0x84, 0x91, 0x0F},
|
||||
{0x84, 0x92, 0x0F},
|
||||
{0x84, 0x83, 0x0F},
|
||||
{0x84, 0x84, 0x0F},
|
||||
{0x84, 0x85, 0x0F},
|
||||
{0x84, 0x86, 0x0F},
|
||||
{0x84, 0x90, 0x0F}
|
||||
};
|
||||
|
||||
const size_t cmdCount = sizeof(radar_cmds) / sizeof(radar_cmds[0]);
|
||||
static size_t cmdIndex = 0;
|
||||
static unsigned long lastCmdMillis = 0;
|
||||
const unsigned long CMD_INTERVAL = 2000UL;
|
||||
|
||||
unsigned long now = millis();
|
||||
if (now - lastCmdMillis >= CMD_INTERVAL) {
|
||||
sendRadarCommand(radar_cmds[cmdIndex][0], radar_cmds[cmdIndex][1], radar_cmds[cmdIndex][2]);
|
||||
lastCmdMillis = now;
|
||||
cmdIndex++;
|
||||
if (cmdIndex >= cmdCount) cmdIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
processBLEConfig();
|
||||
esp_task_wdt_reset();
|
||||
esp_task_wdt_reset();
|
||||
|
||||
esp_task_wdt_reset();
|
||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查Boot按钮状态
|
||||
* 在启动时检查Boot按钮是否松开,等待松开后再启动避免频繁重启
|
||||
*/
|
||||
void checkBootButton() {
|
||||
pinMode(BOOT_BUTTON_PIN, INPUT_PULLUP);
|
||||
delay(10);
|
||||
|
||||
if (digitalRead(BOOT_BUTTON_PIN) == LOW) {
|
||||
Serial.println("⚠️ 检测到Boot按钮按下,请释放按钮后继续启动");
|
||||
while (digitalRead(BOOT_BUTTON_PIN) == LOW) {
|
||||
delay(50);
|
||||
}
|
||||
Serial.println("✅ Boot按钮已释放,正常启动");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 加载设备ID
|
||||
* 从Flash中读取保存的设备ID,如果Flash中没有则使用默认值1001并保存
|
||||
*/
|
||||
void loadDeviceId() {
|
||||
if (preferences.isKey("deviceId")) {
|
||||
currentDeviceId = preferences.getUShort("deviceId", 1001);
|
||||
} else {
|
||||
currentDeviceId = 1001;
|
||||
preferences.putUShort("deviceId", currentDeviceId);
|
||||
Serial.printf("Flash中无设备ID,使用默认值1001并保存\n");
|
||||
}
|
||||
Serial.printf("从Flash加载设备ID: %u\n", currentDeviceId);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
584
src/mqtt.cpp
Normal file
584
src/mqtt.cpp
Normal file
@@ -0,0 +1,584 @@
|
||||
#include "mqtt.h"
|
||||
#include "wifi_manager.h"
|
||||
#include "radar_manager.h"
|
||||
#include <mbedtls/md5.h>
|
||||
|
||||
extern uint64_t device_sn;
|
||||
extern Preferences preferences;
|
||||
extern WiFiManager wifiManager;
|
||||
extern String getDeviceMacAddress();
|
||||
|
||||
TaskHandle_t mqttTaskHandle = NULL;
|
||||
|
||||
const char* mqttServer = "www.lmhrt.cn";
|
||||
const int mqttPort = 1883;
|
||||
const char* mqttDeviceModel = "radar_1.0";
|
||||
const char* mqttProductKey = "your_product_key";
|
||||
const char* mqttProductSecret = "your_product_secret";
|
||||
|
||||
String deviceMacAddress = "";
|
||||
|
||||
WiFiClient mqttWiFiClient;
|
||||
PubSubClient mqttClient(mqttWiFiClient);
|
||||
|
||||
static uint32_t mqttMessageId = 1;
|
||||
|
||||
unsigned long lastSleepDataTime = 0;
|
||||
const unsigned long SLEEP_DATA_INTERVAL = 10000;
|
||||
unsigned long lastDailyDataTime = 0;
|
||||
const unsigned long DAILY_DATA_INTERVAL = 5000;
|
||||
|
||||
String getMqttDeviceName() {
|
||||
if (device_sn != 0) {
|
||||
return String((unsigned long long)device_sn);
|
||||
}
|
||||
|
||||
String fallback = getDeviceMacAddress();
|
||||
fallback.replace(":", "");
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取MQTT客户端ID
|
||||
* 按照enjoy-iot规范构建客户端ID
|
||||
*
|
||||
* 格式:{productKey}_{deviceName}_{model}
|
||||
*
|
||||
* @return 客户端ID字符串
|
||||
* @example "radar_2024_12345678_v1"
|
||||
*/
|
||||
String getMqttClientId() {
|
||||
return String(mqttProductKey) + "_" + getMqttDeviceName() + "_" + String(mqttDeviceModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取MQTT下行订阅主题
|
||||
* 用于订阅平台下发的所有指令
|
||||
*
|
||||
* 格式:/sys/{productKey}/{deviceName}/c/#
|
||||
* - /sys/ 系统主题前缀
|
||||
* - {productKey} 产品标识
|
||||
* - {deviceName} 设备名称
|
||||
* - /c/ 下行指令标识 (command)
|
||||
* - # 通配符,匹配所有子主题
|
||||
*
|
||||
* @return 订阅主题字符串
|
||||
* @example "/sys/radar_2024/12345678/c/#"
|
||||
*/
|
||||
String getMqttSubscribeTopic() {
|
||||
return String("/sys/") + mqttProductKey + "/" + getMqttDeviceName() + "/c/#";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取MQTT属性上报主题
|
||||
* 用于设备向平台上报属性数据
|
||||
*
|
||||
* 格式:/sys/{productKey}/{deviceName}/s/event/property/post
|
||||
* - /sys/ 系统主题前缀
|
||||
* - {productKey} 产品标识
|
||||
* - {deviceName} 设备名称
|
||||
* - /s/ 上行状态标识 (status)
|
||||
* - /event/property/post 属性上报事件
|
||||
*
|
||||
* @return 上报主题字符串
|
||||
* @example "/sys/radar_2024/12345678/s/event/property/post"
|
||||
*/
|
||||
String getMqttPropertyPostTopic() {
|
||||
return String("/sys/") + mqttProductKey + "/" + getMqttDeviceName() + "/s/event/property/post";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 生成下一个MQTT消息ID
|
||||
* 每次调用返回递增的ID,用于消息追踪和请求-响应匹配
|
||||
*
|
||||
* @return 消息ID字符串
|
||||
* @example "1" -> "2" -> "3" ...
|
||||
*/
|
||||
static String nextMqttMessageId() {
|
||||
return String(mqttMessageId++);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 计算MQTT连接密码
|
||||
* 按照enjoy-iot规范,使用MD5计算密码
|
||||
*
|
||||
* 公式:password = MD5(productSecret + clientId)
|
||||
*
|
||||
* @param clientId 客户端ID
|
||||
* @return 32位小写十六进制MD5字符串
|
||||
* @example MD5("abc123" + "radar_2024_12345678_v1") -> "a1b2c3d4e5f6..."
|
||||
*/
|
||||
String makeMqttPassword(const String& clientId) {
|
||||
String raw = String(mqttProductSecret) + clientId;
|
||||
|
||||
unsigned char digest[16];
|
||||
mbedtls_md5_context ctx;
|
||||
mbedtls_md5_init(&ctx);
|
||||
mbedtls_md5_starts_ret(&ctx);
|
||||
mbedtls_md5_update_ret(&ctx, (const unsigned char*)raw.c_str(), raw.length());
|
||||
mbedtls_md5_finish_ret(&ctx, digest);
|
||||
mbedtls_md5_free(&ctx);
|
||||
|
||||
char md5str[33];
|
||||
for (int i = 0; i < 16; i++) {
|
||||
sprintf(&md5str[i * 2], "%02x", digest[i]);
|
||||
}
|
||||
md5str[32] = '\0';
|
||||
return String(md5str);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 统一属性上报函数
|
||||
* 封装enjoy-iot规范的属性上报格式,被sendDailyDataToMQTT和sendSleepDataToMQTT调用
|
||||
*
|
||||
* 载荷格式:
|
||||
* {
|
||||
* "id": "消息ID",
|
||||
* "method": "thing.event.property.post",
|
||||
* "params": {
|
||||
* "deviceId": "设备ID",
|
||||
* "reportType": "daily/sleep",
|
||||
* ...业务字段...
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @param params 业务参数JSON对象(调用者填充业务字段)
|
||||
* @param reportType 上报类型 ("daily" 或 "sleep")
|
||||
* @return true 发布成功,false 发布失败
|
||||
*/
|
||||
static bool publishPropertyReport(JsonDocument& params, const char* reportType) {
|
||||
JsonDocument payloadDoc;
|
||||
|
||||
payloadDoc["id"] = nextMqttMessageId();
|
||||
|
||||
payloadDoc["method"] = "thing.event.property.post";
|
||||
|
||||
params["deviceId"] = String((unsigned long long)device_sn);
|
||||
params["reportType"] = reportType;
|
||||
|
||||
payloadDoc["params"] = params;
|
||||
|
||||
String topic = getMqttPropertyPostTopic();
|
||||
|
||||
String payload;
|
||||
serializeJson(payloadDoc, payload);
|
||||
|
||||
return mqttClient.publish(topic.c_str(), payload.c_str());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 构建回复主题
|
||||
* 将下行请求主题转换为上行回复主题
|
||||
*
|
||||
* 转换规则:
|
||||
* - /c/ 替换为 /s/
|
||||
* - 末尾添加 _reply
|
||||
*
|
||||
* @param requestTopic 请求主题
|
||||
* @return 回复主题字符串
|
||||
* @example "/sys/.../c/service/property/set" -> "/sys/.../s/service/property/set_reply"
|
||||
*/
|
||||
String buildReplyTopic(const char* requestTopic) {
|
||||
String topic = String(requestTopic);
|
||||
topic.replace("/c/", "/s/");
|
||||
topic += "_reply";
|
||||
return topic;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 发送MQTT回复
|
||||
* 统一处理平台下发指令的回复
|
||||
*
|
||||
* 回复格式:
|
||||
* {
|
||||
* "id": "原请求ID",
|
||||
* "method": "原method_reply",
|
||||
* "code": 0, // 0=成功,其他=失败
|
||||
* "data": { ... }
|
||||
* }
|
||||
*
|
||||
* @param requestTopic 请求主题
|
||||
* @param requestId 请求ID
|
||||
* @param requestMethod 请求方法
|
||||
* @param code 状态码 (0=成功,-1=失败)
|
||||
* @param data 回复数据
|
||||
* @return true 发送成功,false 发送失败
|
||||
*/
|
||||
bool publishMqttReply(const char* requestTopic,
|
||||
const char* requestId,
|
||||
const char* requestMethod,
|
||||
int code,
|
||||
JsonVariant data) {
|
||||
JsonDocument replyDoc;
|
||||
replyDoc["id"] = requestId ? requestId : "";
|
||||
replyDoc["method"] = String(requestMethod ? requestMethod : "") + "_reply";
|
||||
replyDoc["code"] = code;
|
||||
replyDoc["data"] = data;
|
||||
|
||||
String replyTopic = buildReplyTopic(requestTopic);
|
||||
String payload;
|
||||
serializeJson(replyDoc, payload);
|
||||
|
||||
Serial.printf("[MQTT] reply topic: %s\n", replyTopic.c_str());
|
||||
Serial.printf("[MQTT] reply payload: %s\n", payload.c_str());
|
||||
|
||||
return mqttClient.publish(replyTopic.c_str(), payload.c_str());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief MQTT消息回调函数
|
||||
* 处理平台下发的指令,支持属性设置、属性读取和自定义服务
|
||||
*
|
||||
* 支持的method:
|
||||
* - thing.service.property.set: 设置设备属性(如continuousSendEnabled、continuousSendInterval)
|
||||
* - thing.service.property.get: 读取设备属性(返回当前传感器数据和配置)
|
||||
* - thing.service.*: 自定义服务(预留扩展)
|
||||
*
|
||||
* @param topic 消息主题
|
||||
* @param payload 消息载荷
|
||||
* @param length 载荷长度
|
||||
*/
|
||||
void mqttMessageCallback(char* topic, byte* payload, unsigned int length) {
|
||||
String message;
|
||||
for (unsigned int i = 0; i < length; i++) {
|
||||
message += (char)payload[i];
|
||||
}
|
||||
|
||||
Serial.printf("[MQTT] 收到主题: %s\n", topic);
|
||||
Serial.printf("[MQTT] 收到内容: %s\n", message.c_str());
|
||||
|
||||
JsonDocument doc;
|
||||
DeserializationError err = deserializeJson(doc, message);
|
||||
if (err) {
|
||||
Serial.printf("[MQTT] JSON解析失败: %s\n", err.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
const char* method = doc["method"] | "";
|
||||
const char* id = doc["id"] | "";
|
||||
JsonObject params = doc["params"].as<JsonObject>();
|
||||
|
||||
if (strcmp(method, "thing.service.property.set") == 0) {
|
||||
bool ok = true;
|
||||
|
||||
if (params["continuousSendEnabled"].is<bool>()) {
|
||||
continuousSendEnabled = params["continuousSendEnabled"].as<bool>();
|
||||
}
|
||||
|
||||
if (params["continuousSendInterval"].is<unsigned long>()) {
|
||||
continuousSendInterval = params["continuousSendInterval"].as<unsigned long>();
|
||||
}
|
||||
|
||||
JsonDocument replyData;
|
||||
replyData["success"] = ok;
|
||||
|
||||
if (publishMqttReply(topic, id, method, ok ? 0 : -1, replyData.as<JsonVariant>())) {
|
||||
Serial.println("[MQTT] property.set reply 发送成功");
|
||||
} else {
|
||||
Serial.println("[MQTT] property.set reply 发送失败");
|
||||
}
|
||||
|
||||
} else if (strcmp(method, "thing.service.property.get") == 0) {
|
||||
JsonDocument replyData;
|
||||
|
||||
replyData["heartRate"] = sensorData.heart_rate;
|
||||
replyData["breathingRate"] = sensorData.breath_rate;
|
||||
replyData["personDetected"] = sensorData.presence;
|
||||
replyData["humanActivity"] = sensorData.motion;
|
||||
replyData["humanDistance"] = sensorData.distance;
|
||||
replyData["sleepState"] = sensorData.sleep_state;
|
||||
replyData["continuousSendEnabled"] = continuousSendEnabled;
|
||||
replyData["continuousSendInterval"] = continuousSendInterval;
|
||||
|
||||
if (publishMqttReply(topic, id, method, 0, replyData.as<JsonVariant>())) {
|
||||
Serial.println("[MQTT] property.get reply 发送成功");
|
||||
} else {
|
||||
Serial.println("[MQTT] property.get reply 发送失败");
|
||||
}
|
||||
|
||||
} else if (strncmp(method, "thing.service.", 14) == 0) {
|
||||
JsonDocument replyData;
|
||||
bool ok = true;
|
||||
|
||||
replyData["success"] = ok;
|
||||
|
||||
if (publishMqttReply(topic, id, method, ok ? 0 : -1, replyData.as<JsonVariant>())) {
|
||||
Serial.printf("[MQTT] service reply 发送成功, method=%s\n", method);
|
||||
} else {
|
||||
Serial.printf("[MQTT] service reply 发送失败, method=%s\n", method);
|
||||
}
|
||||
|
||||
} else {
|
||||
Serial.printf("[MQTT] 暂不支持 method=%s\n", method);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 初始化MQTT客户端
|
||||
* 配置MQTT服务器地址、端口、缓冲区大小和消息回调函数
|
||||
*
|
||||
* 初始化内容:
|
||||
* 1. 获取设备MAC地址
|
||||
* 2. 设置MQTT服务器地址和端口
|
||||
* 3. 设置消息缓冲区大小为1024字节
|
||||
* 4. 注册下行消息回调函数
|
||||
*/
|
||||
void initMQTT() {
|
||||
deviceMacAddress = getDeviceMacAddress();
|
||||
mqttClient.setServer(mqttServer, mqttPort);
|
||||
mqttClient.setBufferSize(MQTT_MAX_PACKET_SIZE);
|
||||
mqttClient.setCallback(mqttMessageCallback);
|
||||
|
||||
Serial.printf("[MQTT] broker: %s:%d\n", mqttServer, mqttPort);
|
||||
Serial.printf("[MQTT] clientId: %s\n", getMqttClientId().c_str());
|
||||
Serial.printf("[MQTT] username: %s\n", getMqttDeviceName().c_str());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 连接MQTT服务器
|
||||
* 使用enjoy-iot规范的身份认证方式连接,并订阅下行主题
|
||||
*
|
||||
* 注意:此函数同时承担首次连接和断线重连的职责
|
||||
* - 首次连接:checkMQTTStatus() 检测到未连接时调用
|
||||
* - 断线重连:checkMQTTStatus() 检测到断开时调用
|
||||
*
|
||||
* 连接流程:
|
||||
* 1. 检查WiFi是否已连接
|
||||
* 2. 计算clientId、username、password
|
||||
* 3. 连接MQTT服务器
|
||||
* 4. 订阅下行主题 /sys/{productKey}/{deviceName}/c/#
|
||||
*
|
||||
* 密码计算:password = MD5(productSecret + clientId)
|
||||
*/
|
||||
void reconnectMQTT() {
|
||||
if (!WiFi.isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String clientId = getMqttClientId();
|
||||
String username = getMqttDeviceName();
|
||||
String password = makeMqttPassword(clientId);
|
||||
|
||||
bool connected = mqttClient.connect(clientId.c_str(), username.c_str(), password.c_str());
|
||||
|
||||
if (connected) {
|
||||
Serial.printf("[MQTT] 连接成功, clientId=%s\n", clientId.c_str());
|
||||
|
||||
String subTopic = getMqttSubscribeTopic();
|
||||
mqttClient.subscribe(subTopic.c_str());
|
||||
Serial.printf("[MQTT] 已订阅: %s\n", subTopic.c_str());
|
||||
} else {
|
||||
Serial.printf("[MQTT] 连接失败, state=%d\n", mqttClient.state());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查MQTT连接状态
|
||||
* 如果未连接则尝试重连,并保持心跳
|
||||
*
|
||||
* 重连策略:
|
||||
* - 仅在WiFi已连接时尝试重连
|
||||
* - 每5秒尝试一次重连,避免频繁重连
|
||||
* - 调用mqttClient.loop()保持心跳和处理消息
|
||||
*/
|
||||
void checkMQTTStatus() {
|
||||
if (!mqttClient.connected() && WiFi.isConnected()) {
|
||||
static unsigned long lastReconnectAttempt = 0;
|
||||
unsigned long now = millis();
|
||||
if (now - lastReconnectAttempt > 5000) {
|
||||
lastReconnectAttempt = now;
|
||||
reconnectMQTT();
|
||||
}
|
||||
}
|
||||
mqttClient.loop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 发送日常数据到MQTT
|
||||
* 上报当前雷达监测的实时状态数据
|
||||
*
|
||||
* 上报字段:
|
||||
* - heartRate: 心率
|
||||
* - breathingRate: 呼吸率
|
||||
* - personDetected: 人体存在
|
||||
* - humanActivity: 人体活动
|
||||
* - humanDistance: 人体距离
|
||||
* - sleepState: 睡眠状态
|
||||
* - humanPositionX/Y/Z: 人体坐标
|
||||
* - heartbeatWaveform: 心跳波形
|
||||
* - breathingWaveform: 呼吸波形
|
||||
* - abnormalState: 异常状态
|
||||
* - bedStatus: 床状态
|
||||
* - struggleAlert: 挣扎警报
|
||||
* - noOneAlert: 无人警报
|
||||
*
|
||||
* 触发条件:mqttTask中每次取到数据时调用
|
||||
*/
|
||||
void sendDailyDataToMQTT() {
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkMQTTStatus();
|
||||
|
||||
if (!mqttClient.connected()) {
|
||||
Serial.println("[MQTT] 未连接,跳过发送日常数据");
|
||||
return;
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
|
||||
if (sensorData.heart_rate > 0) {
|
||||
doc["heartRate"] = sensorData.heart_rate;
|
||||
}
|
||||
|
||||
if (sensorData.breath_rate > 0) {
|
||||
doc["breathingRate"] = sensorData.breath_rate;
|
||||
}
|
||||
|
||||
doc["personDetected"] = sensorData.presence;
|
||||
doc["humanActivity"] = sensorData.motion;
|
||||
|
||||
if (sensorData.distance > 0) {
|
||||
doc["humanDistance"] = sensorData.distance;
|
||||
}
|
||||
|
||||
doc["sleepState"] = sensorData.sleep_state;
|
||||
doc["humanPositionX"] = sensorData.pos_x;
|
||||
doc["humanPositionY"] = sensorData.pos_y;
|
||||
doc["humanPositionZ"] = sensorData.pos_z;
|
||||
doc["heartbeatWaveform"] = (int)sensorData.heart_waveform[0];
|
||||
doc["breathingWaveform"] = (int)sensorData.breath_waveform[0];
|
||||
doc["abnormalState"] = sensorData.abnormal_state;
|
||||
doc["bedStatus"] = sensorData.bed_status;
|
||||
doc["struggleAlert"] = sensorData.struggle_alert;
|
||||
doc["noOneAlert"] = sensorData.no_one_alert;
|
||||
|
||||
if (publishPropertyReport(doc, "daily")) {
|
||||
Serial.println("[MQTT] 日常数据上报成功");
|
||||
} else {
|
||||
Serial.printf("[MQTT] 日常数据上报失败, state=%d\n", mqttClient.state());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 发送睡眠数据到MQTT
|
||||
* 上报睡眠统计数据和质量评估
|
||||
*
|
||||
* 上报字段:
|
||||
* - sleepQualityScore: 睡眠质量评分
|
||||
* - sleepQualityGrade: 睡眠质量等级
|
||||
* - totalSleepDuration: 总睡眠时长
|
||||
* - awakeDurationRatio: 清醒时长比例
|
||||
* - lightSleepRatio: 浅睡比例
|
||||
* - deepSleepRatio: 深睡比例
|
||||
* - outOfBedDuration: 离床时长
|
||||
* - outOfBedCount: 离床次数
|
||||
* - turnCount: 翻身次数
|
||||
* - avgBreathingRate: 平均呼吸率
|
||||
* - avgHeartRate: 平均心率
|
||||
* - apneaCount: 呼吸暂停次数
|
||||
* - awakeDuration: 清醒时长
|
||||
* - lightSleepDuration: 浅睡时长
|
||||
* - deepSleepDuration: 深睡时长
|
||||
*
|
||||
* 触发条件:
|
||||
* - mqttTask中每10秒调用一次
|
||||
* - 仅在sleep_state为0(深睡)或1(浅睡)时上报
|
||||
*/
|
||||
void sendSleepDataToMQTT() {
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
Serial.println("[MQTT] WiFi未连接,跳过发送睡眠数据");
|
||||
return;
|
||||
}
|
||||
|
||||
checkMQTTStatus();
|
||||
|
||||
if (!mqttClient.connected()) {
|
||||
Serial.println("[MQTT] MQTT未连接,跳过发送睡眠数据");
|
||||
return;
|
||||
}
|
||||
|
||||
if (sensorData.sleep_state != 0 && sensorData.sleep_state != 1) {
|
||||
Serial.printf("[MQTT] 当前不是睡眠状态,sleep_state=%d\n", sensorData.sleep_state);
|
||||
return;
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
doc["sleepQualityScore"] = sensorData.sleep_score;
|
||||
doc["sleepQualityGrade"] = sensorData.sleep_grade;
|
||||
doc["totalSleepDuration"] = sensorData.sleep_total_time;
|
||||
doc["awakeDurationRatio"] = sensorData.awake_ratio;
|
||||
doc["lightSleepRatio"] = sensorData.light_sleep_ratio;
|
||||
doc["deepSleepRatio"] = sensorData.deep_sleep_ratio;
|
||||
doc["outOfBedDuration"] = sensorData.bed_Out_Time;
|
||||
doc["outOfBedCount"] = sensorData.turn_count;
|
||||
doc["turnCount"] = sensorData.turnover_count;
|
||||
doc["avgBreathingRate"] = sensorData.avg_breath_rate;
|
||||
doc["avgHeartRate"] = sensorData.avg_heart_rate;
|
||||
doc["apneaCount"] = sensorData.apnea_count;
|
||||
doc["abnormalState"] = sensorData.abnormal_state;
|
||||
doc["bodyMovement"] = sensorData.body_movement;
|
||||
doc["breathStatus"] = sensorData.breath_status;
|
||||
doc["sleepState"] = sensorData.sleep_state;
|
||||
doc["largeMoveRatio"] = sensorData.large_move_ratio;
|
||||
doc["smallMoveRatio"] = sensorData.small_move_ratio;
|
||||
doc["struggleAlert"] = sensorData.struggle_alert;
|
||||
doc["noOneAlert"] = sensorData.no_one_alert;
|
||||
doc["awakeDuration"] = sensorData.awake_time;
|
||||
doc["lightSleepDuration"] = sensorData.light_sleep_time;
|
||||
doc["deepSleepDuration"] = sensorData.deep_sleep_time;
|
||||
|
||||
if (publishPropertyReport(doc, "sleep")) {
|
||||
Serial.println("[MQTT] 睡眠数据上报成功");
|
||||
} else {
|
||||
Serial.printf("[MQTT] 睡眠数据上报失败, state=%d\n", mqttClient.state());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief MQTT任务
|
||||
* 在FreeRTOS任务中处理所有MQTT功能:
|
||||
* - 保持MQTT连接心跳
|
||||
* - 处理MQTT消息回调
|
||||
* - 定时发送日常数据和睡眠数据
|
||||
* @param parameter 任务参数(未使用)
|
||||
*/
|
||||
void mqttTask(void *parameter) {
|
||||
Serial.println("📡 MQTT任务启动");
|
||||
|
||||
initMQTT();
|
||||
|
||||
while (1) {
|
||||
esp_task_wdt_reset();
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
checkMQTTStatus();
|
||||
|
||||
if (mqttClient.connected()) {
|
||||
unsigned long currentTime = millis();
|
||||
|
||||
if (sensorData.heart_rate == 0 || sensorData.breath_rate == 0) {
|
||||
Serial.println("⚠️ 心率和呼吸率都为0,跳过发送数据到MQTT");
|
||||
} else if (currentTime - lastDailyDataTime >= DAILY_DATA_INTERVAL) {
|
||||
sendDailyDataToMQTT();
|
||||
lastDailyDataTime = currentTime;
|
||||
}
|
||||
|
||||
esp_task_wdt_reset();
|
||||
|
||||
if (currentTime - lastSleepDataTime >= SLEEP_DATA_INTERVAL) {
|
||||
sendSleepDataToMQTT();
|
||||
lastSleepDataTime = currentTime;
|
||||
}
|
||||
} else {
|
||||
static unsigned long lastWifiCheck = 0;
|
||||
if (millis() - lastWifiCheck > 10000) {
|
||||
lastWifiCheck = millis();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
51
src/mqtt.h
Normal file
51
src/mqtt.h
Normal file
@@ -0,0 +1,51 @@
|
||||
#ifndef MQTT_MANAGER_H
|
||||
#define MQTT_MANAGER_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <WiFi.h>
|
||||
#include <PubSubClient.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <esp_task_wdt.h>
|
||||
#include <Preferences.h>
|
||||
|
||||
#undef MQTT_MAX_PACKET_SIZE
|
||||
#define MQTT_MAX_PACKET_SIZE 1024
|
||||
|
||||
class WiFiManager;
|
||||
|
||||
extern uint64_t device_sn;
|
||||
extern Preferences preferences;
|
||||
extern WiFiManager wifiManager;
|
||||
extern bool continuousSendEnabled;
|
||||
extern unsigned long continuousSendInterval;
|
||||
|
||||
extern const char* mqttServer;
|
||||
extern const int mqttPort;
|
||||
extern const char* mqttProductKey;
|
||||
extern const char* mqttDeviceModel;
|
||||
extern const char* mqttProductSecret;
|
||||
extern String deviceMacAddress;
|
||||
|
||||
extern WiFiClient mqttWiFiClient;
|
||||
extern PubSubClient mqttClient;
|
||||
|
||||
extern TaskHandle_t mqttTaskHandle;
|
||||
|
||||
void mqttTask(void *parameter);
|
||||
String getMqttDeviceName();
|
||||
String getMqttClientId();
|
||||
String getMqttSubscribeTopic();
|
||||
String getMqttPropertyPostTopic();
|
||||
String makeMqttPassword(const String& clientId);
|
||||
String buildReplyTopic(const char* requestTopic);
|
||||
bool publishMqttReply(const char* requestTopic, const char* requestId, const char* requestMethod, int code, JsonVariant data);
|
||||
void mqttMessageCallback(char* topic, byte* payload, unsigned int length);
|
||||
void initMQTT();
|
||||
void reconnectMQTT();
|
||||
void checkMQTTStatus();
|
||||
void sendDailyDataToMQTT();
|
||||
void sendSleepDataToMQTT();
|
||||
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ class WiFiManager;
|
||||
#define SERVICE_UUID "a8c1e5c0-3d5d-4a9d-8d5e-7c8b6a4e2f1a" // BLE服务UUID
|
||||
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8" // BLE特征值UUID
|
||||
#define UART_RX_BUFFER_SIZE 4096 // UART接收缓冲区大小
|
||||
#define QUEUE_SIZE 50 // 队列大小
|
||||
#define QUEUE_SIZE 200 // 队列大小(增加到200以防止溢出)
|
||||
#define TASK_STACK_SIZE 8192 // 任务堆栈大小
|
||||
|
||||
#define FRAME_HEADER1 0x53 // 帧头字节1
|
||||
@@ -177,6 +177,7 @@ extern String completeData; // 完整数据
|
||||
extern unsigned long lastReceiveTime; // 上次接收数据时间
|
||||
extern bool continuousSendEnabled; // 持续发送使能标志
|
||||
extern unsigned long continuousSendInterval; // 持续发送间隔
|
||||
extern unsigned long lastSleepDataTime; // 上次发送睡眠数据时间
|
||||
extern BLEFlowController bleFlow; // BLE流控制器
|
||||
extern unsigned long lastSensorUpdate; // 上次传感器更新时间
|
||||
extern LastSentData lastSentData; // 上次发送的数据
|
||||
@@ -217,10 +218,7 @@ bool processEchoRequest(JsonDocument& doc);
|
||||
void sendRawEchoResponse(const String& rawData);
|
||||
void sendStatusToBLE();
|
||||
|
||||
void loadDeviceId();
|
||||
void saveDeviceId();
|
||||
|
||||
void sendDailyDataToInfluxDB(String dailyDataLine);
|
||||
bool sendDailyDataToInfluxDB(String dailyDataLine);
|
||||
void sendSleepDataToInfluxDB();
|
||||
|
||||
class MyServerCallbacks: public BLEServerCallbacks {
|
||||
|
||||
978
src/sleep_analyzer.cpp
Normal file
978
src/sleep_analyzer.cpp
Normal file
@@ -0,0 +1,978 @@
|
||||
#include "sleep_analyzer.h"
|
||||
|
||||
const float SleepAnalyzer::SLEEPINESS_THRESHOLD = 0.6f;
|
||||
const float SleepAnalyzer::BASELINE_MOVEMENT_THRESHOLD = 0.2f;
|
||||
const float SleepAnalyzer::BASELINE_HR_STABILITY_THRESHOLD = 5.0f;
|
||||
const float SleepAnalyzer::BASELINE_RR_STABILITY_THRESHOLD = 2.0f;
|
||||
const float SleepAnalyzer::EMA_ALPHA = 0.2f;
|
||||
const float SleepAnalyzer::CONFIDENCE_MARGIN = 0.1f;
|
||||
const float SleepAnalyzer::BASELINE_BETA = 0.01f;
|
||||
const float SleepAnalyzer::HYSTERESIS_ENTER_DEEP = 0.7f;
|
||||
const float SleepAnalyzer::HYSTERESIS_EXIT_DEEP = 0.5f;
|
||||
const float SleepAnalyzer::HYSTERESIS_ENTER_REM = 0.6f;
|
||||
const float SleepAnalyzer::HYSTERESIS_EXIT_REM = 0.4f;
|
||||
|
||||
SleepAnalyzer::SleepAnalyzer() {
|
||||
reset();
|
||||
}
|
||||
|
||||
SleepAnalyzer::~SleepAnalyzer() {
|
||||
}
|
||||
|
||||
void SleepAnalyzer::reset() {
|
||||
currentState = SLEEP_NO_PERSON;
|
||||
pendingState = SLEEP_NO_PERSON;
|
||||
memset(&stats, 0, sizeof(SleepStatistics));
|
||||
memset(&score, 0, sizeof(SleepScore));
|
||||
memset(&cycle, 0, sizeof(SleepCycle));
|
||||
|
||||
stateEnterTime = millis();
|
||||
pendingStateTime = 0;
|
||||
noPersonTimer = 0;
|
||||
outOfBedTimer = 0;
|
||||
sleepinessDuration = 0;
|
||||
awakeDuration = 0;
|
||||
deepSleepDuration = 0;
|
||||
lightSleepDuration = 0;
|
||||
remSleepDuration = 0;
|
||||
movementHighDuration = 0;
|
||||
gettingUpDuration = 0;
|
||||
deepStableDuration = 0;
|
||||
|
||||
baselineHR = 70.0f;
|
||||
baselineRR = 16.0f;
|
||||
baselineCalibrated = false;
|
||||
baselineSampleCount = 0;
|
||||
baselineHRSum = 0;
|
||||
baselineRRSum = 0;
|
||||
lastBaselineHR = 0;
|
||||
lastBaselineRR = 0;
|
||||
hrStabilitySum = 0;
|
||||
rrStabilitySum = 0;
|
||||
stabilitySampleCount = 0;
|
||||
|
||||
currentSleepiness = 0;
|
||||
currentDeepScore = 0;
|
||||
currentLightScore = 0;
|
||||
currentAwakeScore = 0;
|
||||
currentRemScore = 0;
|
||||
|
||||
lastRRValue = 0;
|
||||
wasAsleep = false;
|
||||
}
|
||||
|
||||
PresenceData SleepAnalyzer::evaluatePresence() {
|
||||
PresenceData p;
|
||||
memset(&p, 0, sizeof(PresenceData));
|
||||
|
||||
if (sensorData.heart_valid || sensorData.breath_valid) {
|
||||
p.isPresent = true;
|
||||
p.confidence = 0.9f;
|
||||
}
|
||||
|
||||
if (sensorData.presence == 1) {
|
||||
p.isPresent = true;
|
||||
p.confidence = max(p.confidence, 0.7f);
|
||||
}
|
||||
|
||||
if (sensorData.distance > 0) {
|
||||
p.distance = sensorData.distance;
|
||||
if (p.distance > 20 && p.distance < 100) {
|
||||
p.isPresent = true;
|
||||
}
|
||||
} else {
|
||||
p.distance = -1;
|
||||
}
|
||||
|
||||
p.motionEnergy = sensorData.body_movement / 100.0f;
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
float SleepAnalyzer::sigmoid(float x) {
|
||||
return 1.0f / (1.0f + expf(-x));
|
||||
}
|
||||
|
||||
float SleepAnalyzer::emaSmooth(float input, float last, float alpha) {
|
||||
return alpha * input + (1.0f - alpha) * last;
|
||||
}
|
||||
|
||||
float SleepAnalyzer::updateBaseline(float current, float input, float beta) {
|
||||
return (1.0f - beta) * current + beta * input;
|
||||
}
|
||||
|
||||
bool SleepAnalyzer::isBestScore(float score, float s2, float s3, float s4, float margin) {
|
||||
return (score > s2 + margin && score > s3 + margin && score > s4 + margin);
|
||||
}
|
||||
|
||||
float SleepAnalyzer::normalizeHR(float hr) {
|
||||
if (!baselineCalibrated) return 0.0f;
|
||||
float norm = (hr - baselineHR) / 20.0f;
|
||||
return constrain_value(norm, -1.0f, 1.0f);
|
||||
}
|
||||
|
||||
float SleepAnalyzer::normalizeRR(float rr) {
|
||||
if (!baselineCalibrated) return 0.0f;
|
||||
float norm = (rr - baselineRR) / 4.0f;
|
||||
return constrain_value(norm, -1.0f, 1.0f);
|
||||
}
|
||||
|
||||
float SleepAnalyzer::normalizeHRV(float hrv) {
|
||||
float norm = hrv / 50.0f;
|
||||
return constrain_value(norm, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
float SleepAnalyzer::normalizeMovement(float movement) {
|
||||
return constrain_value(movement / 100.0f, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
void SleepAnalyzer::calibrateBaseline(const HeartRateData& hrData,
|
||||
const RespirationData& rrData,
|
||||
const BodyMovementData& movementData) {
|
||||
if (!hrData.isValid || !rrData.isValid) return;
|
||||
if (currentState != SLEEP_AWAKE && currentState != SLEEP_IN_BED) return;
|
||||
|
||||
if (movementData.isValid && normalizeMovement(movementData.movement) > BASELINE_MOVEMENT_THRESHOLD) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (baselineCalibrated) {
|
||||
float hrDiff = fabs(hrData.bpmSmoothed - lastBaselineHR);
|
||||
float rrDiff = fabs(rrData.rateSmoothed - lastBaselineRR);
|
||||
hrStabilitySum += hrDiff;
|
||||
rrStabilitySum += rrDiff;
|
||||
stabilitySampleCount++;
|
||||
|
||||
if (stabilitySampleCount >= 5) {
|
||||
float avgHrDiff = hrStabilitySum / stabilitySampleCount;
|
||||
float avgRrDiff = rrStabilitySum / stabilitySampleCount;
|
||||
if (avgHrDiff > BASELINE_HR_STABILITY_THRESHOLD ||
|
||||
avgRrDiff > BASELINE_RR_STABILITY_THRESHOLD) {
|
||||
hrStabilitySum = 0;
|
||||
rrStabilitySum = 0;
|
||||
stabilitySampleCount = 0;
|
||||
return;
|
||||
}
|
||||
hrStabilitySum = 0;
|
||||
rrStabilitySum = 0;
|
||||
stabilitySampleCount = 0;
|
||||
}
|
||||
|
||||
baselineHR = updateBaseline(baselineHR, hrData.bpmSmoothed, BASELINE_BETA);
|
||||
baselineRR = updateBaseline(baselineRR, rrData.rateSmoothed, BASELINE_BETA);
|
||||
lastBaselineHR = hrData.bpmSmoothed;
|
||||
lastBaselineRR = rrData.rateSmoothed;
|
||||
return;
|
||||
}
|
||||
|
||||
baselineHRSum += hrData.bpmSmoothed;
|
||||
baselineRRSum += rrData.rateSmoothed;
|
||||
baselineSampleCount++;
|
||||
|
||||
if (baselineSampleCount >= 30) {
|
||||
lastBaselineHR = baselineHR;
|
||||
lastBaselineRR = baselineRR;
|
||||
baselineHR = baselineHRSum / baselineSampleCount;
|
||||
baselineRR = baselineRRSum / baselineSampleCount;
|
||||
baselineCalibrated = true;
|
||||
baselineHRSum = 0;
|
||||
baselineRRSum = 0;
|
||||
baselineSampleCount = 0;
|
||||
Serial.printf("📐 睡眠基线校准完成: HR=%.1f, RR=%.1f\n", baselineHR, baselineRR);
|
||||
}
|
||||
}
|
||||
|
||||
float SleepAnalyzer::calculateSleepinessScore(const HeartRateData& hrData,
|
||||
const RespirationData& rrData,
|
||||
const HRVEstimate& hrvData,
|
||||
const BodyMovementData& movementData) {
|
||||
float hrSleepFactor = 0.5f, hrvNorm = 0, rrStable = 0, moveNorm = 0;
|
||||
|
||||
if (hrData.isValid) {
|
||||
float hrNorm = normalizeHR(hrData.bpmSmoothed);
|
||||
hrSleepFactor = (1.0f - hrNorm) * 0.5f;
|
||||
}
|
||||
if (hrvData.isValid) {
|
||||
hrvNorm = normalizeHRV(hrvData.rmssd);
|
||||
}
|
||||
if (rrData.isValid) {
|
||||
rrStable = 1.0f - constrain_value(rrData.variability / 5.0f, 0.0f, 1.0f);
|
||||
}
|
||||
if (movementData.isValid) {
|
||||
moveNorm = normalizeMovement(movementData.movement);
|
||||
}
|
||||
|
||||
float x = 3.0f * (hrSleepFactor - 0.5f)
|
||||
+ 2.5f * (hrvNorm - 0.5f)
|
||||
+ 2.0f * (rrStable - 0.5f)
|
||||
+ 2.5f * (0.5f - moveNorm);
|
||||
|
||||
return sigmoid(x);
|
||||
}
|
||||
|
||||
float SleepAnalyzer::calculateDeepSleepScore(const HeartRateData& hrData,
|
||||
const RespirationData& rrData,
|
||||
const HRVEstimate& hrvData,
|
||||
const BodyMovementData& movementData) {
|
||||
if (movementData.isValid && movementData.movement > DEEP_SLEEP_HARD_MOVEMENT_LIMIT) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
float hrSleepFactor = 0.5f, hrvNorm = 0, moveNorm = 0;
|
||||
|
||||
if (hrData.isValid) {
|
||||
float hrNorm = normalizeHR(hrData.bpmSmoothed);
|
||||
hrSleepFactor = (1.0f - hrNorm) * 0.5f;
|
||||
}
|
||||
if (hrvData.isValid) {
|
||||
hrvNorm = normalizeHRV(hrvData.rmssd);
|
||||
}
|
||||
if (movementData.isValid) {
|
||||
moveNorm = normalizeMovement(movementData.movement);
|
||||
}
|
||||
|
||||
float x = 4.0f * (hrSleepFactor - 0.5f)
|
||||
+ 3.0f * (hrvNorm - 0.5f)
|
||||
+ 2.0f * (0.5f - moveNorm);
|
||||
|
||||
return sigmoid(x);
|
||||
}
|
||||
|
||||
float SleepAnalyzer::calculateLightSleepScore(const HeartRateData& hrData,
|
||||
const RespirationData& rrData,
|
||||
const HRVEstimate& hrvData,
|
||||
const BodyMovementData& movementData) {
|
||||
float hrMid = 0, hrvMid = 0, moveMid = 0, rrStable = 0;
|
||||
|
||||
if (hrData.isValid) {
|
||||
float hrNorm = normalizeHR(hrData.bpmSmoothed);
|
||||
float hrSleepFactor = (1.0f - hrNorm) * 0.5f;
|
||||
hrMid = 1.0f - fabs(hrSleepFactor - 0.5f) * 2.0f;
|
||||
}
|
||||
if (hrvData.isValid) {
|
||||
float hrvNorm = normalizeHRV(hrvData.rmssd);
|
||||
hrvMid = 1.0f - fabs(hrvNorm - 0.5f) * 2.0f;
|
||||
}
|
||||
if (movementData.isValid) {
|
||||
float moveNorm = normalizeMovement(movementData.movement);
|
||||
if (moveNorm >= 0.1f && moveNorm <= 0.4f) {
|
||||
moveMid = 1.0f - fabs(moveNorm - 0.25f) * 4.0f;
|
||||
}
|
||||
}
|
||||
if (rrData.isValid) {
|
||||
rrStable = rrData.regularity;
|
||||
}
|
||||
|
||||
float light = 0.3f * hrMid
|
||||
+ 0.3f * hrvMid
|
||||
+ 0.2f * moveMid
|
||||
+ 0.2f * rrStable;
|
||||
|
||||
return constrain_value(light, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
float SleepAnalyzer::calculateAwakeScore(const HeartRateData& hrData,
|
||||
const RespirationData& rrData,
|
||||
const HRVEstimate& hrvData,
|
||||
const BodyMovementData& movementData) {
|
||||
float moveNorm = 0, hrAwakeFactor = 0.5f, rrVar = 0;
|
||||
|
||||
if (movementData.isValid) {
|
||||
moveNorm = normalizeMovement(movementData.movement);
|
||||
}
|
||||
if (hrData.isValid) {
|
||||
float hrNorm = normalizeHR(hrData.bpmSmoothed);
|
||||
hrAwakeFactor = (hrNorm + 1.0f) * 0.5f;
|
||||
}
|
||||
if (rrData.isValid) {
|
||||
rrVar = constrain_value(rrData.variability / 5.0f, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
float x = 3.0f * (moveNorm - 0.3f)
|
||||
+ 2.0f * (hrAwakeFactor - 0.5f)
|
||||
+ 1.5f * (rrVar - 0.3f);
|
||||
|
||||
return sigmoid(x);
|
||||
}
|
||||
|
||||
float SleepAnalyzer::calculateRemScore(const HeartRateData& hrData,
|
||||
const RespirationData& rrData,
|
||||
const HRVEstimate& hrvData,
|
||||
const BodyMovementData& movementData) {
|
||||
float hrHigh = 0, hrvLow = 0, rrUnstable = 0, moveLow = 0, rrChange = 0;
|
||||
|
||||
if (hrData.isValid) {
|
||||
float hrNorm = normalizeHR(hrData.bpmSmoothed);
|
||||
hrHigh = constrain_value(hrNorm, 0.0f, 1.0f);
|
||||
}
|
||||
if (hrvData.isValid) {
|
||||
float hrvNorm = normalizeHRV(hrvData.rmssd);
|
||||
hrvLow = 1.0f - hrvNorm;
|
||||
}
|
||||
if (rrData.isValid) {
|
||||
rrUnstable = constrain_value(rrData.variability / 5.0f, 0.0f, 1.0f);
|
||||
if (lastRRValue > 0) {
|
||||
float rrDiff = fabs(rrData.rateSmoothed - lastRRValue);
|
||||
rrChange = constrain_value(rrDiff / 3.0f, 0.0f, 1.0f);
|
||||
}
|
||||
lastRRValue = rrData.rateSmoothed;
|
||||
}
|
||||
if (movementData.isValid) {
|
||||
float moveNorm = normalizeMovement(movementData.movement);
|
||||
moveLow = 1.0f - moveNorm;
|
||||
}
|
||||
|
||||
if (movementData.isValid && movementData.movement > DEEP_SLEEP_HARD_MOVEMENT_LIMIT) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
float x = 2.5f * (hrHigh - 0.3f)
|
||||
+ 2.0f * (hrvLow - 0.3f)
|
||||
+ 1.5f * (rrUnstable - 0.3f)
|
||||
+ 2.0f * (rrChange - 0.3f)
|
||||
+ 2.0f * (moveLow - 0.5f);
|
||||
|
||||
return sigmoid(x);
|
||||
}
|
||||
|
||||
bool SleepAnalyzer::tryTransitionTo(SleepState target, unsigned long confirmMs) {
|
||||
if (pendingState != target) {
|
||||
pendingState = target;
|
||||
pendingStateTime = millis();
|
||||
return false;
|
||||
}
|
||||
if (millis() - pendingStateTime >= confirmMs) {
|
||||
currentState = target;
|
||||
stateEnterTime = millis();
|
||||
pendingState = target;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void SleepAnalyzer::updateSleepCycle() {
|
||||
if (currentState == SLEEP_DEEP_SLEEP && !cycle.inDeepPhase) {
|
||||
cycle.inDeepPhase = true;
|
||||
if (cycle.cycleStartTime == 0) {
|
||||
cycle.cycleStartTime = millis();
|
||||
}
|
||||
}
|
||||
|
||||
if (currentState == SLEEP_REM_SLEEP && !cycle.inRemPhase) {
|
||||
cycle.inRemPhase = true;
|
||||
}
|
||||
|
||||
if (currentState == SLEEP_LIGHT_SLEEP && cycle.inDeepPhase) {
|
||||
cycle.inDeepPhase = false;
|
||||
cycle.lastDeepEndTime = millis();
|
||||
}
|
||||
|
||||
if (currentState == SLEEP_LIGHT_SLEEP && cycle.inRemPhase) {
|
||||
cycle.inRemPhase = false;
|
||||
cycle.lastRemEndTime = millis();
|
||||
}
|
||||
|
||||
if ((currentState == SLEEP_AWAKE || currentState == SLEEP_OUT_OF_BED) &&
|
||||
(cycle.inDeepPhase || cycle.inRemPhase) &&
|
||||
cycle.cycleStartTime > 0) {
|
||||
cycle.cycleCount++;
|
||||
cycle.inDeepPhase = false;
|
||||
cycle.inRemPhase = false;
|
||||
cycle.cycleStartTime = millis();
|
||||
stats.sleepCycles = cycle.cycleCount;
|
||||
Serial.printf("🔄 完成第 %d 个睡眠周期\n", cycle.cycleCount);
|
||||
}
|
||||
}
|
||||
|
||||
void SleepAnalyzer::updateState(PresenceData& presence,
|
||||
const HeartRateData& hrData,
|
||||
const RespirationData& rrData,
|
||||
const HRVEstimate& hrvData,
|
||||
const BodyMovementData& movementData) {
|
||||
unsigned long now = millis();
|
||||
|
||||
if (currentState != SLEEP_NO_PERSON && currentState != SLEEP_SESSION_END &&
|
||||
(now - stateEnterTime) < MIN_STATE_DWELL_MS) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (currentState) {
|
||||
case SLEEP_NO_PERSON:
|
||||
if (presence.isPresent) {
|
||||
noPersonTimer = 0;
|
||||
if (presence.distance > 0 && presence.distance > 80) {
|
||||
currentState = SLEEP_OUT_OF_BED;
|
||||
} else {
|
||||
currentState = SLEEP_IN_BED;
|
||||
}
|
||||
stateEnterTime = now;
|
||||
pendingState = currentState;
|
||||
Serial.printf("🔄 状态切换: 无人 → %s\n", SLEEP_STATE_NAMES[currentState]);
|
||||
}
|
||||
break;
|
||||
|
||||
case SLEEP_IN_BED:
|
||||
if (!presence.isPresent) {
|
||||
noPersonTimer += 1000;
|
||||
bool hrExists = hrData.isValid && hrData.bpmSmoothed > 0;
|
||||
if (noPersonTimer > NO_PERSON_END_SECONDS * 1000 && !hrExists) {
|
||||
currentState = SLEEP_SESSION_END;
|
||||
stateEnterTime = now;
|
||||
pendingState = currentState;
|
||||
if (wasAsleep) {
|
||||
calculateSleepScore();
|
||||
}
|
||||
Serial.println("🔄 状态切换: 在床 → 会话结束");
|
||||
} else if (noPersonTimer > NO_PERSON_END_SECONDS * 1000 && hrExists) {
|
||||
Serial.println("⚠️ 检测到无人但HR存在,可能遮挡,暂不结束会话");
|
||||
} else if (noPersonTimer > OUT_OF_BED_SECONDS * 1000) {
|
||||
currentState = SLEEP_OUT_OF_BED;
|
||||
stateEnterTime = now;
|
||||
pendingState = currentState;
|
||||
Serial.println("🔄 状态切换: 在床 → 离床");
|
||||
}
|
||||
} else {
|
||||
noPersonTimer = 0;
|
||||
float rawSleepiness = calculateSleepinessScore(hrData, rrData, hrvData, movementData);
|
||||
currentSleepiness = emaSmooth(rawSleepiness, currentSleepiness, EMA_ALPHA);
|
||||
float movement = movementData.isValid ? movementData.movement : 100;
|
||||
|
||||
if (currentSleepiness > SLEEPINESS_THRESHOLD &&
|
||||
movement < SLEEPINESS_MOVEMENT_THRESHOLD) {
|
||||
sleepinessDuration += 1000;
|
||||
if (sleepinessDuration >= SLEEPINESS_MIN_SECONDS * 1000) {
|
||||
currentState = SLEEP_LIGHT_SLEEP;
|
||||
stateEnterTime = now;
|
||||
pendingState = currentState;
|
||||
stats.sleepStartTime = now;
|
||||
stats.sleepLatency = (now - stats.sessionStartTime) / 1000;
|
||||
wasAsleep = true;
|
||||
cycle.cycleStartTime = now;
|
||||
Serial.printf("🔄 状态切换: 在床 → 浅睡 (入睡耗时:%lus)\n", stats.sleepLatency);
|
||||
}
|
||||
} else {
|
||||
sleepinessDuration = 0;
|
||||
if (movement >= MOVEMENT_HIGH_THRESHOLD) {
|
||||
currentState = SLEEP_AWAKE;
|
||||
stateEnterTime = now;
|
||||
pendingState = currentState;
|
||||
stats.sessionStartTime = now;
|
||||
Serial.println("🔄 状态切换: 在床 → 清醒");
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case SLEEP_AWAKE:
|
||||
if (!presence.isPresent) {
|
||||
noPersonTimer += 1000;
|
||||
bool hrExists = hrData.isValid && hrData.bpmSmoothed > 0;
|
||||
if (noPersonTimer > NO_PERSON_END_SECONDS * 1000 && !hrExists) {
|
||||
currentState = SLEEP_SESSION_END;
|
||||
stateEnterTime = now;
|
||||
pendingState = currentState;
|
||||
if (wasAsleep) {
|
||||
calculateSleepScore();
|
||||
}
|
||||
Serial.println("🔄 状态切换: 清醒 → 会话结束");
|
||||
} else if (noPersonTimer > NO_PERSON_END_SECONDS * 1000 && hrExists) {
|
||||
Serial.println("⚠️ 检测到无人但HR存在,可能遮挡,暂不结束会话");
|
||||
} else if (noPersonTimer > OUT_OF_BED_SECONDS * 1000) {
|
||||
currentState = SLEEP_OUT_OF_BED;
|
||||
stateEnterTime = now;
|
||||
pendingState = currentState;
|
||||
Serial.println("🔄 状态切换: 清醒 → 离床");
|
||||
}
|
||||
} else {
|
||||
noPersonTimer = 0;
|
||||
float rawSleepiness = calculateSleepinessScore(hrData, rrData, hrvData, movementData);
|
||||
currentSleepiness = emaSmooth(rawSleepiness, currentSleepiness, EMA_ALPHA);
|
||||
float movement = movementData.isValid ? movementData.movement : 100;
|
||||
|
||||
if (wasAsleep && movement >= GETTING_UP_MOVEMENT_THRESHOLD) {
|
||||
gettingUpDuration += 1000;
|
||||
if (gettingUpDuration >= GETTING_UP_MIN_SECONDS * 1000) {
|
||||
currentState = SLEEP_GETTING_UP;
|
||||
stateEnterTime = now;
|
||||
pendingState = currentState;
|
||||
gettingUpDuration = 0;
|
||||
Serial.println("🔄 状态切换: 清醒 → 起床");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
gettingUpDuration = 0;
|
||||
}
|
||||
|
||||
if (currentSleepiness > SLEEPINESS_THRESHOLD &&
|
||||
movement < SLEEPINESS_MOVEMENT_THRESHOLD) {
|
||||
sleepinessDuration += 1000;
|
||||
if (sleepinessDuration >= SLEEPINESS_MIN_SECONDS * 1000) {
|
||||
currentState = SLEEP_LIGHT_SLEEP;
|
||||
stateEnterTime = now;
|
||||
pendingState = currentState;
|
||||
stats.sleepStartTime = now;
|
||||
stats.sleepLatency = (now - stats.sessionStartTime) / 1000;
|
||||
wasAsleep = true;
|
||||
cycle.cycleStartTime = now;
|
||||
Serial.printf("🔄 状态切换: 清醒 → 浅睡 (入睡耗时:%lus)\n", stats.sleepLatency);
|
||||
}
|
||||
} else {
|
||||
sleepinessDuration = 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case SLEEP_LIGHT_SLEEP:
|
||||
if (!presence.isPresent) {
|
||||
noPersonTimer += 1000;
|
||||
if (noPersonTimer > OUT_OF_BED_SECONDS * 1000) {
|
||||
currentState = SLEEP_OUT_OF_BED;
|
||||
stateEnterTime = now;
|
||||
pendingState = currentState;
|
||||
stats.wakeCount++;
|
||||
Serial.println("🔄 状态切换: 浅睡 → 离床");
|
||||
}
|
||||
} else {
|
||||
noPersonTimer = 0;
|
||||
|
||||
if (movementData.isValid && movementData.movement > FAST_AWAKE_MOVEMENT_THRESHOLD) {
|
||||
currentState = SLEEP_AWAKE;
|
||||
stateEnterTime = now;
|
||||
pendingState = SLEEP_AWAKE;
|
||||
stats.wakeCount++;
|
||||
awakeDuration = 0;
|
||||
deepSleepDuration = 0;
|
||||
deepStableDuration = 0;
|
||||
Serial.println("🔄 状态切换: 浅睡 → 清醒 (快速触发:体动大)");
|
||||
break;
|
||||
}
|
||||
|
||||
float rawDeep = calculateDeepSleepScore(hrData, rrData, hrvData, movementData);
|
||||
float rawLight = calculateLightSleepScore(hrData, rrData, hrvData, movementData);
|
||||
float rawAwake = calculateAwakeScore(hrData, rrData, hrvData, movementData);
|
||||
float rawRem = calculateRemScore(hrData, rrData, hrvData, movementData);
|
||||
|
||||
currentDeepScore = emaSmooth(rawDeep, currentDeepScore, EMA_ALPHA);
|
||||
currentLightScore = emaSmooth(rawLight, currentLightScore, EMA_ALPHA);
|
||||
currentAwakeScore = emaSmooth(rawAwake, currentAwakeScore, EMA_ALPHA);
|
||||
currentRemScore = emaSmooth(rawRem, currentRemScore, EMA_ALPHA);
|
||||
|
||||
if (isBestScore(currentAwakeScore, currentDeepScore, currentLightScore, currentRemScore, CONFIDENCE_MARGIN)) {
|
||||
awakeDuration += 1000;
|
||||
deepSleepDuration = 0;
|
||||
deepStableDuration = 0;
|
||||
if (tryTransitionTo(SLEEP_AWAKE, AWAKE_SLOW_CONFIRM_SECONDS * 1000)) {
|
||||
stats.wakeCount++;
|
||||
awakeDuration = 0;
|
||||
Serial.println("🔄 状态切换: 浅睡 → 清醒 (慢速触发)");
|
||||
}
|
||||
} else if (isBestScore(currentDeepScore, currentAwakeScore, currentLightScore, currentRemScore, CONFIDENCE_MARGIN) &&
|
||||
currentDeepScore >= HYSTERESIS_ENTER_DEEP) {
|
||||
deepSleepDuration += 1000;
|
||||
if (movementData.isValid && movementData.movement < DEEP_SLEEP_HARD_MOVEMENT_LIMIT) {
|
||||
deepStableDuration += 1000;
|
||||
} else {
|
||||
deepStableDuration = 0;
|
||||
}
|
||||
awakeDuration = 0;
|
||||
if (deepStableDuration >= DEEP_STABLE_MIN_SECONDS * 1000 &&
|
||||
tryTransitionTo(SLEEP_DEEP_SLEEP, DEEP_SLEEP_CONFIRM_SECONDS * 1000)) {
|
||||
deepSleepDuration = 0;
|
||||
Serial.println("🔄 状态切换: 浅睡 → 深睡 (稳定≥5分钟)");
|
||||
}
|
||||
} else if (isBestScore(currentRemScore, currentAwakeScore, currentDeepScore, currentLightScore, CONFIDENCE_MARGIN) &&
|
||||
currentRemScore >= HYSTERESIS_ENTER_REM) {
|
||||
remSleepDuration += 1000;
|
||||
deepSleepDuration = 0;
|
||||
deepStableDuration = 0;
|
||||
awakeDuration = 0;
|
||||
if (tryTransitionTo(SLEEP_REM_SLEEP, REM_CONFIRM_SECONDS * 1000)) {
|
||||
remSleepDuration = 0;
|
||||
Serial.println("🔄 状态切换: 浅睡 → REM");
|
||||
}
|
||||
} else {
|
||||
deepSleepDuration = 0;
|
||||
awakeDuration = 0;
|
||||
deepStableDuration = 0;
|
||||
pendingState = SLEEP_LIGHT_SLEEP;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case SLEEP_DEEP_SLEEP:
|
||||
if (!presence.isPresent) {
|
||||
noPersonTimer += 1000;
|
||||
if (noPersonTimer > OUT_OF_BED_SECONDS * 1000) {
|
||||
currentState = SLEEP_OUT_OF_BED;
|
||||
stateEnterTime = now;
|
||||
pendingState = currentState;
|
||||
stats.wakeCount++;
|
||||
Serial.println("🔄 状态切换: 深睡 → 离床");
|
||||
}
|
||||
} else {
|
||||
noPersonTimer = 0;
|
||||
|
||||
if (movementData.isValid && movementData.movement > FAST_AWAKE_MOVEMENT_THRESHOLD) {
|
||||
currentState = SLEEP_AWAKE;
|
||||
stateEnterTime = now;
|
||||
pendingState = SLEEP_AWAKE;
|
||||
stats.wakeCount++;
|
||||
lightSleepDuration = 0;
|
||||
movementHighDuration = 0;
|
||||
Serial.println("🔄 状态切换: 深睡 → 清醒 (快速触发:体动大)");
|
||||
break;
|
||||
}
|
||||
|
||||
if (movementData.isValid && movementData.movement > DEEP_SLEEP_HARD_MOVEMENT_LIMIT) {
|
||||
lightSleepDuration += 1000;
|
||||
if (tryTransitionTo(SLEEP_LIGHT_SLEEP, LIGHT_SLEEP_CONFIRM_SECONDS * 1000)) {
|
||||
lightSleepDuration = 0;
|
||||
Serial.println("🔄 状态切换: 深睡 → 浅睡 (体动超限)");
|
||||
}
|
||||
} else {
|
||||
lightSleepDuration = 0;
|
||||
pendingState = SLEEP_DEEP_SLEEP;
|
||||
}
|
||||
|
||||
if (movementData.isValid && movementData.movement > MOVEMENT_HIGH_THRESHOLD) {
|
||||
movementHighDuration += 1000;
|
||||
if (tryTransitionTo(SLEEP_AWAKE, AWAKE_CONFIRM_SECONDS * 1000)) {
|
||||
stats.wakeCount++;
|
||||
movementHighDuration = 0;
|
||||
Serial.println("🔄 状态切换: 深睡 → 清醒");
|
||||
}
|
||||
} else {
|
||||
movementHighDuration = 0;
|
||||
}
|
||||
|
||||
float rawRem = calculateRemScore(hrData, rrData, hrvData, movementData);
|
||||
float rawLight = calculateLightSleepScore(hrData, rrData, hrvData, movementData);
|
||||
currentRemScore = emaSmooth(rawRem, currentRemScore, EMA_ALPHA);
|
||||
currentLightScore = emaSmooth(rawLight, currentLightScore, EMA_ALPHA);
|
||||
|
||||
if (currentRemScore > currentLightScore && currentRemScore > HYSTERESIS_EXIT_DEEP &&
|
||||
currentDeepScore < HYSTERESIS_EXIT_DEEP) {
|
||||
remSleepDuration += 1000;
|
||||
if (tryTransitionTo(SLEEP_REM_SLEEP, REM_CONFIRM_SECONDS * 1000)) {
|
||||
remSleepDuration = 0;
|
||||
Serial.println("🔄 状态切换: 深睡 → REM");
|
||||
}
|
||||
} else {
|
||||
remSleepDuration = 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case SLEEP_REM_SLEEP:
|
||||
if (!presence.isPresent) {
|
||||
noPersonTimer += 1000;
|
||||
if (noPersonTimer > OUT_OF_BED_SECONDS * 1000) {
|
||||
currentState = SLEEP_OUT_OF_BED;
|
||||
stateEnterTime = now;
|
||||
pendingState = currentState;
|
||||
stats.wakeCount++;
|
||||
Serial.println("🔄 状态切换: REM → 离床");
|
||||
}
|
||||
} else {
|
||||
noPersonTimer = 0;
|
||||
|
||||
if (movementData.isValid && movementData.movement > FAST_AWAKE_MOVEMENT_THRESHOLD) {
|
||||
currentState = SLEEP_AWAKE;
|
||||
stateEnterTime = now;
|
||||
pendingState = SLEEP_AWAKE;
|
||||
stats.wakeCount++;
|
||||
Serial.println("🔄 状态切换: REM → 清醒 (快速触发:体动大)");
|
||||
break;
|
||||
}
|
||||
|
||||
float rawRem = calculateRemScore(hrData, rrData, hrvData, movementData);
|
||||
float rawLight = calculateLightSleepScore(hrData, rrData, hrvData, movementData);
|
||||
float rawDeep = calculateDeepSleepScore(hrData, rrData, hrvData, movementData);
|
||||
float rawAwake = calculateAwakeScore(hrData, rrData, hrvData, movementData);
|
||||
|
||||
currentRemScore = emaSmooth(rawRem, currentRemScore, EMA_ALPHA);
|
||||
currentLightScore = emaSmooth(rawLight, currentLightScore, EMA_ALPHA);
|
||||
currentDeepScore = emaSmooth(rawDeep, currentDeepScore, EMA_ALPHA);
|
||||
currentAwakeScore = emaSmooth(rawAwake, currentAwakeScore, EMA_ALPHA);
|
||||
|
||||
if (isBestScore(currentAwakeScore, currentRemScore, currentLightScore, currentDeepScore, CONFIDENCE_MARGIN)) {
|
||||
awakeDuration += 1000;
|
||||
if (tryTransitionTo(SLEEP_AWAKE, AWAKE_SLOW_CONFIRM_SECONDS * 1000)) {
|
||||
stats.wakeCount++;
|
||||
awakeDuration = 0;
|
||||
Serial.println("🔄 状态切换: REM → 清醒");
|
||||
}
|
||||
} else if (isBestScore(currentLightScore, currentRemScore, currentAwakeScore, currentDeepScore, CONFIDENCE_MARGIN) &&
|
||||
currentRemScore < HYSTERESIS_EXIT_REM) {
|
||||
if (tryTransitionTo(SLEEP_LIGHT_SLEEP, LIGHT_SLEEP_CONFIRM_SECONDS * 1000)) {
|
||||
Serial.println("🔄 状态切换: REM → 浅睡");
|
||||
}
|
||||
} else if (isBestScore(currentDeepScore, currentRemScore, currentLightScore, currentAwakeScore, CONFIDENCE_MARGIN) &&
|
||||
currentDeepScore >= HYSTERESIS_ENTER_DEEP) {
|
||||
deepStableDuration += 1000;
|
||||
if (deepStableDuration >= DEEP_STABLE_MIN_SECONDS * 1000 &&
|
||||
tryTransitionTo(SLEEP_DEEP_SLEEP, DEEP_SLEEP_CONFIRM_SECONDS * 1000)) {
|
||||
deepStableDuration = 0;
|
||||
Serial.println("🔄 状态切换: REM → 深睡 (稳定≥5分钟)");
|
||||
}
|
||||
} else {
|
||||
awakeDuration = 0;
|
||||
deepStableDuration = 0;
|
||||
pendingState = SLEEP_REM_SLEEP;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case SLEEP_OUT_OF_BED:
|
||||
if (presence.isPresent) {
|
||||
noPersonTimer = 0;
|
||||
currentState = SLEEP_IN_BED;
|
||||
stateEnterTime = now;
|
||||
pendingState = currentState;
|
||||
Serial.println("🔄 状态切换: 离床 → 在床");
|
||||
} else {
|
||||
noPersonTimer += 1000;
|
||||
bool hrExists = hrData.isValid && hrData.bpmSmoothed > 0;
|
||||
if (noPersonTimer > NO_PERSON_END_SECONDS * 1000 && !hrExists) {
|
||||
currentState = SLEEP_SESSION_END;
|
||||
stateEnterTime = now;
|
||||
pendingState = currentState;
|
||||
if (wasAsleep) {
|
||||
calculateSleepScore();
|
||||
}
|
||||
Serial.println("🔄 状态切换: 离床 → 会话结束");
|
||||
} else if (noPersonTimer > NO_PERSON_END_SECONDS * 1000 && hrExists) {
|
||||
Serial.println("⚠️ 检测到无人但HR存在,可能遮挡,暂不结束会话");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case SLEEP_GETTING_UP:
|
||||
if (!presence.isPresent) {
|
||||
noPersonTimer += 1000;
|
||||
bool hrExists = hrData.isValid && hrData.bpmSmoothed > 0;
|
||||
if (noPersonTimer > NO_PERSON_END_SECONDS * 1000 && !hrExists) {
|
||||
currentState = SLEEP_SESSION_END;
|
||||
stateEnterTime = now;
|
||||
pendingState = currentState;
|
||||
if (wasAsleep) {
|
||||
calculateSleepScore();
|
||||
}
|
||||
Serial.println("🔄 状态切换: 起床 → 会话结束");
|
||||
}
|
||||
} else {
|
||||
float movement = movementData.isValid ? movementData.movement : 0;
|
||||
if (movement < GETTING_UP_MOVEMENT_THRESHOLD) {
|
||||
gettingUpDuration += 1000;
|
||||
if (gettingUpDuration >= 60000) {
|
||||
currentState = SLEEP_AWAKE;
|
||||
stateEnterTime = now;
|
||||
pendingState = currentState;
|
||||
gettingUpDuration = 0;
|
||||
Serial.println("🔄 状态切换: 起床 → 清醒 (重新躺下)");
|
||||
}
|
||||
} else {
|
||||
gettingUpDuration = 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case SLEEP_SESSION_END:
|
||||
if (presence.isPresent) {
|
||||
currentState = SLEEP_IN_BED;
|
||||
stateEnterTime = now;
|
||||
pendingState = currentState;
|
||||
noPersonTimer = 0;
|
||||
Serial.println("🔄 状态切换: 会话结束 → 在床(新会话)");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SleepAnalyzer::updateStatistics(unsigned long dt) {
|
||||
switch (currentState) {
|
||||
case SLEEP_LIGHT_SLEEP:
|
||||
stats.lightSleepTime += dt;
|
||||
stats.totalSleepTime += dt;
|
||||
break;
|
||||
case SLEEP_DEEP_SLEEP:
|
||||
stats.deepSleepTime += dt;
|
||||
stats.totalSleepTime += dt;
|
||||
break;
|
||||
case SLEEP_REM_SLEEP:
|
||||
stats.remSleepTime += dt;
|
||||
stats.totalSleepTime += dt;
|
||||
break;
|
||||
case SLEEP_AWAKE:
|
||||
stats.awakeTime += dt;
|
||||
break;
|
||||
case SLEEP_OUT_OF_BED:
|
||||
stats.outOfBedTime += dt;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SleepAnalyzer::calculateSleepScore() {
|
||||
float totalHours = stats.totalSleepTime / 3600000.0f;
|
||||
if (totalHours >= 7.0f && totalHours <= 9.0f) {
|
||||
score.durationScore = 18.0f;
|
||||
} else if (totalHours >= 6.0f && totalHours < 7.0f) {
|
||||
score.durationScore = 13.0f;
|
||||
} else if (totalHours > 9.0f && totalHours <= 10.0f) {
|
||||
score.durationScore = 13.0f;
|
||||
} else {
|
||||
score.durationScore = 5.0f;
|
||||
}
|
||||
|
||||
float deepRatio = (stats.totalSleepTime > 0) ?
|
||||
(float)stats.deepSleepTime / stats.totalSleepTime : 0;
|
||||
if (deepRatio > 0.2f) {
|
||||
score.deepScore = 14.0f;
|
||||
} else if (deepRatio > 0.15f) {
|
||||
score.deepScore = 11.0f;
|
||||
} else if (deepRatio > 0.1f) {
|
||||
score.deepScore = 7.0f;
|
||||
} else {
|
||||
score.deepScore = 3.0f;
|
||||
}
|
||||
|
||||
if (stats.wakeCount <= 1) {
|
||||
score.continuityScore = 11.0f;
|
||||
} else if (stats.wakeCount <= 3) {
|
||||
score.continuityScore = 7.0f;
|
||||
} else if (stats.wakeCount <= 5) {
|
||||
score.continuityScore = 4.0f;
|
||||
} else {
|
||||
score.continuityScore = 2.0f;
|
||||
}
|
||||
|
||||
score.physiologyScore = 7.0f;
|
||||
|
||||
float latencyMin = stats.sleepLatency / 60.0f;
|
||||
if (latencyMin < 20.0f) {
|
||||
score.latencyScore = 8.0f;
|
||||
} else if (latencyMin < 30.0f) {
|
||||
score.latencyScore = 6.0f;
|
||||
} else if (latencyMin < 45.0f) {
|
||||
score.latencyScore = 3.0f;
|
||||
} else {
|
||||
score.latencyScore = 1.0f;
|
||||
}
|
||||
|
||||
float sleepEfficiency = 0;
|
||||
if (stats.totalSleepTime + stats.awakeTime > 0) {
|
||||
sleepEfficiency = (float)stats.totalSleepTime / (stats.totalSleepTime + stats.awakeTime);
|
||||
}
|
||||
if (sleepEfficiency > 0.9f) {
|
||||
score.efficiencyScore = 14.0f;
|
||||
} else if (sleepEfficiency > 0.8f) {
|
||||
score.efficiencyScore = 10.0f;
|
||||
} else if (sleepEfficiency > 0.7f) {
|
||||
score.efficiencyScore = 6.0f;
|
||||
} else {
|
||||
score.efficiencyScore = 3.0f;
|
||||
}
|
||||
|
||||
float remRatio = (stats.totalSleepTime > 0) ?
|
||||
(float)stats.remSleepTime / stats.totalSleepTime : 0;
|
||||
float cycleScoreVal = 0;
|
||||
if (stats.sleepCycles >= 4) {
|
||||
cycleScoreVal = 20.0f;
|
||||
} else if (stats.sleepCycles >= 3) {
|
||||
cycleScoreVal = 15.0f;
|
||||
} else if (stats.sleepCycles >= 2) {
|
||||
cycleScoreVal = 10.0f;
|
||||
} else if (stats.sleepCycles >= 1) {
|
||||
cycleScoreVal = 6.0f;
|
||||
} else {
|
||||
cycleScoreVal = 2.0f;
|
||||
}
|
||||
if (remRatio >= 0.2f && remRatio <= 0.25f) {
|
||||
cycleScoreVal += 8.0f;
|
||||
} else if (remRatio >= 0.15f) {
|
||||
cycleScoreVal += 5.0f;
|
||||
} else if (remRatio > 0) {
|
||||
cycleScoreVal += 2.0f;
|
||||
}
|
||||
score.cycleScore = constrain_value(cycleScoreVal, 0.0f, 28.0f);
|
||||
|
||||
float rawTotal = score.durationScore + score.deepScore +
|
||||
score.continuityScore + score.physiologyScore +
|
||||
score.latencyScore + score.efficiencyScore +
|
||||
score.cycleScore;
|
||||
score.totalScore = constrain_value(rawTotal / 100.0f * 100.0f, 0.0f, 100.0f);
|
||||
|
||||
Serial.println("━━━━━━━━━━ 睡眠评分 ━━━━━━━━━━");
|
||||
Serial.printf(" 时长评分: %.0f/18\n", score.durationScore);
|
||||
Serial.printf(" 深睡评分: %.0f/14\n", score.deepScore);
|
||||
Serial.printf(" 连续性评分: %.0f/11\n", score.continuityScore);
|
||||
Serial.printf(" 生理质量评分: %.0f/7\n", score.physiologyScore);
|
||||
Serial.printf(" 入睡速度评分: %.0f/8\n", score.latencyScore);
|
||||
Serial.printf(" 睡眠效率评分: %.0f/14 (效率:%.0f%%)\n", score.efficiencyScore, sleepEfficiency * 100);
|
||||
Serial.printf(" 周期评分: %.0f/28 (周期数:%d, REM占比:%.0f%%)\n",
|
||||
score.cycleScore, stats.sleepCycles, remRatio * 100);
|
||||
Serial.printf(" 总分: %.0f/100\n", score.totalScore);
|
||||
Serial.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
}
|
||||
|
||||
void SleepAnalyzer::update(const HeartRateData& hrData,
|
||||
const RespirationData& rrData,
|
||||
const HRVEstimate& hrvData,
|
||||
const BodyMovementData& movementData) {
|
||||
calibrateBaseline(hrData, rrData, movementData);
|
||||
|
||||
PresenceData presence = evaluatePresence();
|
||||
|
||||
updateState(presence, hrData, rrData, hrvData, movementData);
|
||||
|
||||
updateSleepCycle();
|
||||
|
||||
updateStatistics(1000);
|
||||
}
|
||||
|
||||
void SleepAnalyzer::printState() {
|
||||
unsigned long stateDuration = (millis() - stateEnterTime) / 1000;
|
||||
Serial.printf("🛏️ 睡眠状态: %s | 持续: %02lu:%02lu:%02lu | 困倦度: %.2f | 周期: %d\n",
|
||||
SLEEP_STATE_NAMES[currentState],
|
||||
stateDuration / 3600, (stateDuration % 3600) / 60, stateDuration % 60,
|
||||
currentSleepiness, cycle.cycleCount);
|
||||
}
|
||||
|
||||
void SleepAnalyzer::printStatistics() {
|
||||
Serial.println("━━━━━━━━━━ 睡眠统计 ━━━━━━━━━━");
|
||||
Serial.printf(" 总睡眠: %02lu:%02lu:%02lu\n",
|
||||
stats.totalSleepTime / 3600000,
|
||||
(stats.totalSleepTime % 3600000) / 60000,
|
||||
(stats.totalSleepTime % 60000) / 1000);
|
||||
Serial.printf(" 深睡: %02lu:%02lu:%02lu\n",
|
||||
stats.deepSleepTime / 3600000,
|
||||
(stats.deepSleepTime % 3600000) / 60000,
|
||||
(stats.deepSleepTime % 60000) / 1000);
|
||||
Serial.printf(" 浅睡: %02lu:%02lu:%02lu\n",
|
||||
stats.lightSleepTime / 3600000,
|
||||
(stats.lightSleepTime % 3600000) / 60000,
|
||||
(stats.lightSleepTime % 60000) / 1000);
|
||||
Serial.printf(" REM: %02lu:%02lu:%02lu\n",
|
||||
stats.remSleepTime / 3600000,
|
||||
(stats.remSleepTime % 3600000) / 60000,
|
||||
(stats.remSleepTime % 60000) / 1000);
|
||||
Serial.printf(" 清醒: %02lu:%02lu:%02lu\n",
|
||||
stats.awakeTime / 3600000,
|
||||
(stats.awakeTime % 3600000) / 60000,
|
||||
(stats.awakeTime % 60000) / 1000);
|
||||
Serial.printf(" 离床: %02lu:%02lu:%02lu\n",
|
||||
stats.outOfBedTime / 3600000,
|
||||
(stats.outOfBedTime % 3600000) / 60000,
|
||||
(stats.outOfBedTime % 60000) / 1000);
|
||||
Serial.printf(" 醒来次数: %d\n", stats.wakeCount);
|
||||
Serial.printf(" 入睡耗时: %lus\n", stats.sleepLatency);
|
||||
Serial.printf(" 睡眠周期: %d\n", stats.sleepCycles);
|
||||
float sleepEfficiency = 0;
|
||||
if (stats.totalSleepTime + stats.awakeTime > 0) {
|
||||
sleepEfficiency = (float)stats.totalSleepTime / (stats.totalSleepTime + stats.awakeTime);
|
||||
}
|
||||
Serial.printf(" 睡眠效率: %.0f%%\n", sleepEfficiency * 100);
|
||||
Serial.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
}
|
||||
223
src/sleep_analyzer.h
Normal file
223
src/sleep_analyzer.h
Normal file
@@ -0,0 +1,223 @@
|
||||
#ifndef SLEEP_ANALYZER_H
|
||||
#define SLEEP_ANALYZER_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "config.h"
|
||||
#include "data_processor.h"
|
||||
#include "emotion_analyzer_simple.h"
|
||||
#include "radar_manager.h"
|
||||
|
||||
enum SleepState {
|
||||
SLEEP_NO_PERSON = 0,
|
||||
SLEEP_IN_BED,
|
||||
SLEEP_AWAKE,
|
||||
SLEEP_LIGHT_SLEEP,
|
||||
SLEEP_DEEP_SLEEP,
|
||||
SLEEP_REM_SLEEP,
|
||||
SLEEP_OUT_OF_BED,
|
||||
SLEEP_GETTING_UP,
|
||||
SLEEP_SESSION_END
|
||||
};
|
||||
|
||||
static const char* SLEEP_STATE_NAMES[] = {
|
||||
"无人",
|
||||
"在床",
|
||||
"清醒",
|
||||
"浅睡",
|
||||
"深睡",
|
||||
"REM",
|
||||
"离床",
|
||||
"起床",
|
||||
"会话结束"
|
||||
};
|
||||
|
||||
struct PresenceData {
|
||||
bool isPresent;
|
||||
float distance;
|
||||
float confidence;
|
||||
float motionEnergy;
|
||||
};
|
||||
|
||||
struct SleepCycle {
|
||||
int cycleCount;
|
||||
unsigned long cycleStartTime;
|
||||
bool inDeepPhase;
|
||||
bool inRemPhase;
|
||||
unsigned long lastDeepEndTime;
|
||||
unsigned long lastRemEndTime;
|
||||
};
|
||||
|
||||
struct SleepStatistics {
|
||||
unsigned long totalSleepTime;
|
||||
unsigned long deepSleepTime;
|
||||
unsigned long lightSleepTime;
|
||||
unsigned long remSleepTime;
|
||||
unsigned long awakeTime;
|
||||
unsigned long outOfBedTime;
|
||||
unsigned long sleepLatency;
|
||||
int wakeCount;
|
||||
int sleepCycles;
|
||||
unsigned long sessionStartTime;
|
||||
unsigned long sleepStartTime;
|
||||
unsigned long lastWakeTime;
|
||||
};
|
||||
|
||||
struct SleepScore {
|
||||
float durationScore;
|
||||
float deepScore;
|
||||
float continuityScore;
|
||||
float physiologyScore;
|
||||
float latencyScore;
|
||||
float efficiencyScore;
|
||||
float cycleScore;
|
||||
float totalScore;
|
||||
};
|
||||
|
||||
struct TrainingData {
|
||||
float hr;
|
||||
float rr;
|
||||
float hrv;
|
||||
float movement;
|
||||
int label;
|
||||
};
|
||||
|
||||
class SleepAnalyzer {
|
||||
private:
|
||||
static const float EMA_ALPHA;
|
||||
static const float CONFIDENCE_MARGIN;
|
||||
static const float BASELINE_BETA;
|
||||
static const int MIN_STATE_DWELL_MS = 10000;
|
||||
static const float HYSTERESIS_ENTER_DEEP;
|
||||
static const float HYSTERESIS_EXIT_DEEP;
|
||||
static const float HYSTERESIS_ENTER_REM;
|
||||
static const float HYSTERESIS_EXIT_REM;
|
||||
|
||||
SleepState currentState;
|
||||
SleepState pendingState;
|
||||
SleepStatistics stats;
|
||||
SleepScore score;
|
||||
SleepCycle cycle;
|
||||
|
||||
unsigned long stateEnterTime;
|
||||
unsigned long pendingStateTime;
|
||||
unsigned long noPersonTimer;
|
||||
unsigned long outOfBedTimer;
|
||||
unsigned long sleepinessDuration;
|
||||
unsigned long awakeDuration;
|
||||
unsigned long deepSleepDuration;
|
||||
unsigned long lightSleepDuration;
|
||||
unsigned long remSleepDuration;
|
||||
unsigned long movementHighDuration;
|
||||
unsigned long gettingUpDuration;
|
||||
unsigned long deepStableDuration;
|
||||
|
||||
float baselineHR;
|
||||
float baselineRR;
|
||||
bool baselineCalibrated;
|
||||
int baselineSampleCount;
|
||||
float baselineHRSum;
|
||||
float baselineRRSum;
|
||||
float lastBaselineHR;
|
||||
float lastBaselineRR;
|
||||
float hrStabilitySum;
|
||||
float rrStabilitySum;
|
||||
int stabilitySampleCount;
|
||||
|
||||
float lastRRValue;
|
||||
|
||||
static const int DEEP_SLEEP_CONFIRM_SECONDS = 60;
|
||||
static const int LIGHT_SLEEP_CONFIRM_SECONDS = 30;
|
||||
static const int AWAKE_CONFIRM_SECONDS = 15;
|
||||
static const int AWAKE_SLOW_CONFIRM_SECONDS = 30;
|
||||
static const int NO_PERSON_END_SECONDS = 600;
|
||||
static const int OUT_OF_BED_SECONDS = 30;
|
||||
static const int SLEEPINESS_MIN_SECONDS = 300;
|
||||
static const int SLEEPINESS_MAX_SECONDS = 600;
|
||||
static const int MOVEMENT_HIGH_THRESHOLD = 50;
|
||||
static const int DEEP_SLEEP_MOVEMENT_THRESHOLD = 10;
|
||||
static const int DEEP_SLEEP_HARD_MOVEMENT_LIMIT = 15;
|
||||
static const int FAST_AWAKE_MOVEMENT_THRESHOLD = 60;
|
||||
static const int SLEEPINESS_MOVEMENT_THRESHOLD = 10;
|
||||
static const int DEEP_STABLE_MIN_SECONDS = 300;
|
||||
static const int REM_CONFIRM_SECONDS = 60;
|
||||
static const int GETTING_UP_MIN_SECONDS = 300;
|
||||
static const int GETTING_UP_MOVEMENT_THRESHOLD = 30;
|
||||
static const float SLEEPINESS_THRESHOLD;
|
||||
static const float BASELINE_MOVEMENT_THRESHOLD;
|
||||
static const float BASELINE_HR_STABILITY_THRESHOLD;
|
||||
static const float BASELINE_RR_STABILITY_THRESHOLD;
|
||||
|
||||
float currentSleepiness;
|
||||
float currentDeepScore;
|
||||
float currentLightScore;
|
||||
float currentAwakeScore;
|
||||
float currentRemScore;
|
||||
|
||||
bool wasAsleep;
|
||||
|
||||
PresenceData evaluatePresence();
|
||||
float sigmoid(float x);
|
||||
float emaSmooth(float input, float last, float alpha);
|
||||
float updateBaseline(float current, float input, float beta);
|
||||
float calculateSleepinessScore(const HeartRateData& hrData,
|
||||
const RespirationData& rrData,
|
||||
const HRVEstimate& hrvData,
|
||||
const BodyMovementData& movementData);
|
||||
float calculateDeepSleepScore(const HeartRateData& hrData,
|
||||
const RespirationData& rrData,
|
||||
const HRVEstimate& hrvData,
|
||||
const BodyMovementData& movementData);
|
||||
float calculateLightSleepScore(const HeartRateData& hrData,
|
||||
const RespirationData& rrData,
|
||||
const HRVEstimate& hrvData,
|
||||
const BodyMovementData& movementData);
|
||||
float calculateAwakeScore(const HeartRateData& hrData,
|
||||
const RespirationData& rrData,
|
||||
const HRVEstimate& hrvData,
|
||||
const BodyMovementData& movementData);
|
||||
float calculateRemScore(const HeartRateData& hrData,
|
||||
const RespirationData& rrData,
|
||||
const HRVEstimate& hrvData,
|
||||
const BodyMovementData& movementData);
|
||||
|
||||
void updateState(PresenceData& presence,
|
||||
const HeartRateData& hrData,
|
||||
const RespirationData& rrData,
|
||||
const HRVEstimate& hrvData,
|
||||
const BodyMovementData& movementData);
|
||||
void updateStatistics(unsigned long dt);
|
||||
void updateSleepCycle();
|
||||
void calculateSleepScore();
|
||||
void calibrateBaseline(const HeartRateData& hrData,
|
||||
const RespirationData& rrData,
|
||||
const BodyMovementData& movementData);
|
||||
|
||||
float normalizeHR(float hr);
|
||||
float normalizeRR(float rr);
|
||||
float normalizeHRV(float hrv);
|
||||
float normalizeMovement(float movement);
|
||||
|
||||
bool tryTransitionTo(SleepState target, unsigned long confirmMs);
|
||||
bool isBestScore(float score, float s2, float s3, float s4, float margin);
|
||||
|
||||
public:
|
||||
SleepAnalyzer();
|
||||
~SleepAnalyzer();
|
||||
|
||||
void update(const HeartRateData& hrData,
|
||||
const RespirationData& rrData,
|
||||
const HRVEstimate& hrvData,
|
||||
const BodyMovementData& movementData);
|
||||
|
||||
SleepState getCurrentState() const { return currentState; }
|
||||
SleepStatistics getStatistics() const { return stats; }
|
||||
SleepScore getScore() const { return score; }
|
||||
SleepCycle getCycle() const { return cycle; }
|
||||
float getSleepiness() const { return currentSleepiness; }
|
||||
|
||||
void reset();
|
||||
void printState();
|
||||
void printStatistics();
|
||||
};
|
||||
|
||||
#endif
|
||||
650
src/tasks_manager.cpp
Normal file
650
src/tasks_manager.cpp
Normal file
@@ -0,0 +1,650 @@
|
||||
#include "tasks_manager.h"
|
||||
#include "wifi_manager.h"
|
||||
#include "radar_manager.h"
|
||||
#include "data_processor.h"
|
||||
#include "emotion_analyzer_simple.h"
|
||||
#include <BLEDevice.h>
|
||||
#include <esp_task_wdt.h>
|
||||
|
||||
NetworkStatus currentNetworkStatus = NET_INITIAL;
|
||||
unsigned long lastBlinkTime = 0;
|
||||
bool ledState = false;
|
||||
int breatheValue = 0;
|
||||
bool breatheIncreasing = true;
|
||||
uint8_t WiFi_Connect_First_bit = 1;
|
||||
uint64_t device_sn = 0;
|
||||
|
||||
PhysioDataProcessor* physioProcessor;
|
||||
SimpleEmotionAnalyzer* emotionAnalyzer;
|
||||
|
||||
bool clearConfigRequested = false;
|
||||
bool forceLedOff = false;
|
||||
|
||||
/**
|
||||
* @brief 加载设备SN
|
||||
* 从Flash中读取保存的设备SN(支持64位雪花算法ID)
|
||||
*/
|
||||
void loadDeviceSN() {
|
||||
device_sn = preferences.getULong64("deviceSn", 0);
|
||||
Serial.printf("从Flash加载设备SN: %llu\n", device_sn);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 保存设备SN
|
||||
* 将设备SN保存到Flash中(支持64位雪花算法ID)
|
||||
*/
|
||||
void saveDeviceId() {
|
||||
preferences.putULong64("deviceSn", device_sn);
|
||||
Serial.printf("设备SN已保存到Flash: %llu\n", device_sn);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 计算CRC32哈希值
|
||||
* @param data 输入数据指针
|
||||
* @param length 数据长度
|
||||
* @return CRC32哈希值
|
||||
*/
|
||||
uint32_t calculateCRC32(const uint8_t* data, size_t length) {
|
||||
uint32_t crc = 0xFFFFFFFF;
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
crc ^= data[i];
|
||||
for (int j = 0; j < 8; j++) {
|
||||
crc = (crc >> 1) ^ (0xEDB88320 & -(crc & 1));
|
||||
}
|
||||
}
|
||||
return ~crc;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 生成设备唯一标识哈希
|
||||
* 将 device_sn + MAC 地址拼接后计算 CRC32 哈希
|
||||
* @return 4字节哈希值
|
||||
*/
|
||||
uint32_t generateDeviceHash() {
|
||||
uint8_t mac[6];
|
||||
esp_read_mac(mac, ESP_MAC_WIFI_STA);
|
||||
|
||||
char macHex[13];
|
||||
snprintf(macHex, sizeof(macHex), "%02X%02X%02X%02X%02X%02X",
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
|
||||
char snStr[21];
|
||||
snprintf(snStr, sizeof(snStr), "%llu", device_sn);
|
||||
|
||||
String hashInput = String("SN") + String(snStr) + String("|") + String(macHex);
|
||||
|
||||
uint32_t hash = calculateCRC32((const uint8_t*)hashInput.c_str(), hashInput.length());
|
||||
|
||||
Serial.printf("🔐 [HASH] 输入: %s, 哈希: 0x%08X\n", hashInput.c_str(), hash);
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 构建BLE厂商数据
|
||||
* 构造BLE广播厂商数据,包含FF FF标识和SN哈希值
|
||||
* @return 9字节厂商数据字符串
|
||||
*/
|
||||
std::string buildBLEManufacturerData() {
|
||||
std::string manufacturerData;
|
||||
manufacturerData.reserve(9);
|
||||
|
||||
manufacturerData.push_back(static_cast<char>(0xFF));
|
||||
manufacturerData.push_back(static_cast<char>(0xFF));
|
||||
manufacturerData.push_back('R');
|
||||
manufacturerData.push_back(0x01);
|
||||
manufacturerData.push_back(0x00);
|
||||
|
||||
uint32_t snHash = generateDeviceHash();
|
||||
manufacturerData.push_back(static_cast<char>((snHash >> 24) & 0xFF));
|
||||
manufacturerData.push_back(static_cast<char>((snHash >> 16) & 0xFF));
|
||||
manufacturerData.push_back(static_cast<char>((snHash >> 8) & 0xFF));
|
||||
manufacturerData.push_back(static_cast<char>(snHash & 0xFF));
|
||||
|
||||
return manufacturerData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 刷新BLE广播数据
|
||||
* 更新BLE广播的厂商数据和设备名称,使用SN码作为设备名
|
||||
*/
|
||||
void refreshBLEAdvertisingData() {
|
||||
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
|
||||
if (pAdvertising == nullptr) {
|
||||
Serial.println("⚠️ [BLE] 广播对象为空,无法刷新广播数据");
|
||||
return;
|
||||
}
|
||||
|
||||
char snName[32];
|
||||
if (device_sn > 0) {
|
||||
snprintf(snName, sizeof(snName), "Radar_%llu", device_sn);
|
||||
} else {
|
||||
String macAddr = getDeviceMacAddress();
|
||||
macAddr.replace(":", "");
|
||||
snprintf(snName, sizeof(snName), "Radar_%s", macAddr.c_str());
|
||||
}
|
||||
|
||||
BLEAdvertisementData advertisementData;
|
||||
advertisementData.setFlags(0x06);
|
||||
advertisementData.setCompleteServices(BLEUUID(SERVICE_UUID));
|
||||
advertisementData.setManufacturerData(buildBLEManufacturerData());
|
||||
|
||||
BLEAdvertisementData scanResponseData;
|
||||
scanResponseData.setName(snName);
|
||||
|
||||
pAdvertising->setAdvertisementData(advertisementData);
|
||||
pAdvertising->setScanResponseData(scanResponseData);
|
||||
pAdvertising->setScanResponse(true);
|
||||
pAdvertising->setMinPreferred(0x06);
|
||||
pAdvertising->setMinPreferred(0x12);
|
||||
|
||||
Serial.printf("📡 [BLE] 已刷新广播 ManufacturerData, device_sn=%llu\n", device_sn);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取设备MAC地址
|
||||
* 读取WiFi STA接口的MAC地址并格式化为字符串
|
||||
* @return MAC地址字符串,格式为 XX:XX:XX:XX:XX:XX
|
||||
*/
|
||||
String getDeviceMacAddress() {
|
||||
uint8_t mac[6];
|
||||
esp_read_mac(mac, ESP_MAC_WIFI_STA);
|
||||
char macStr[18];
|
||||
snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X",
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
return String(macStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置网络状态
|
||||
* 更新当前网络状态,并重置呼吸灯参数
|
||||
* @param status 网络状态
|
||||
*/
|
||||
void setNetworkStatus(NetworkStatus status) {
|
||||
currentNetworkStatus = status;
|
||||
|
||||
if (status == NET_CONNECTED) {
|
||||
breatheValue = BREATHE_MIN;
|
||||
breatheIncreasing = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 清除存储的配置
|
||||
* 清除Flash中保存的WiFi配置和设备ID,重置WiFi连接状态
|
||||
*/
|
||||
void clearStoredConfig() {
|
||||
Serial.println("🧹 开始清除存储的配置...");
|
||||
|
||||
uint16_t oldDeviceId = preferences.getUShort("deviceId", 0);
|
||||
|
||||
preferences.remove("deviceId");
|
||||
preferences.remove("wifi_first");
|
||||
|
||||
wifiManager.clearAllConfigs();
|
||||
|
||||
Serial.println("✅ 配置已清除完成");
|
||||
Serial.printf("🗑️ 被清除的设备ID: %u\n", oldDeviceId);
|
||||
|
||||
WiFi_Connect_First_bit = 1;
|
||||
|
||||
WiFi.disconnect(true);
|
||||
setNetworkStatus(NET_DISCONNECTED);
|
||||
|
||||
Serial.println("🔄 已清除Flash与内存中的配置,请重新配置WiFi和设备ID");
|
||||
|
||||
if (deviceConnected) {
|
||||
sendStatusToBLE();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief BOOT按钮监控任务
|
||||
* 监控BOOT按钮按下事件,长按3秒清除存储的配置
|
||||
* @param parameter 任务参数(未使用)
|
||||
*/
|
||||
void bootButtonMonitorTask(void *parameter) {
|
||||
Serial.println("🔍 启动BOOT按钮监控任务...");
|
||||
|
||||
pinMode(CONFIG_CLEAR_PIN, OUTPUT);
|
||||
digitalWrite(CONFIG_CLEAR_PIN, LOW);
|
||||
|
||||
unsigned long buttonPressStartTime = 0;
|
||||
bool buttonPressed = false;
|
||||
|
||||
while (1) {
|
||||
int buttonState = digitalRead(BOOT_BUTTON_PIN);
|
||||
|
||||
if (buttonState == LOW && !buttonPressed) {
|
||||
buttonPressed = true;
|
||||
buttonPressStartTime = millis();
|
||||
Serial.println("⚠️ 检测到BOOT按钮按下,长按3秒将清除配置");
|
||||
|
||||
digitalWrite(CONFIG_CLEAR_PIN, HIGH);
|
||||
}
|
||||
else if (buttonState == HIGH && buttonPressed) {
|
||||
if (!clearConfigRequested) {
|
||||
digitalWrite(CONFIG_CLEAR_PIN, LOW);
|
||||
Serial.println("❌ 按钮释放,取消清除操作");
|
||||
}
|
||||
buttonPressed = false;
|
||||
}
|
||||
|
||||
if (buttonPressed && (millis() - buttonPressStartTime >= CLEAR_CONFIG_DURATION)) {
|
||||
if (!clearConfigRequested) {
|
||||
clearConfigRequested = true;
|
||||
forceLedOff = true;
|
||||
|
||||
clearStoredConfig();
|
||||
|
||||
Serial.println("🔄 系统即将重启...");
|
||||
|
||||
vTaskDelay(1000 / portTICK_PERIOD_MS);
|
||||
digitalWrite(CONFIG_CLEAR_PIN, LOW);
|
||||
ledcWrite(0, 0);
|
||||
ESP.restart();
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(50 / portTICK_PERIOD_MS);
|
||||
esp_task_wdt_reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 睡眠分析任务
|
||||
* 每秒更新生理数据、运行睡眠状态机、输出睡眠状态到串口
|
||||
* @param parameter 任务参数(未使用)
|
||||
*/
|
||||
void sleepAnalysisTask(void *parameter) {
|
||||
SleepAnalyzer* sleepAnalyzer = new SleepAnalyzer();
|
||||
|
||||
static unsigned long lastSleepAnalysisTime = 0;
|
||||
const unsigned long SLEEP_ANALYSIS_INTERVAL = 1000;
|
||||
static unsigned long lastStatsPrintTime = 0;
|
||||
const unsigned long STATS_PRINT_INTERVAL = 30000;
|
||||
|
||||
while (1) {
|
||||
unsigned long currentTime = millis();
|
||||
|
||||
if (currentTime - lastSleepAnalysisTime >= SLEEP_ANALYSIS_INTERVAL) {
|
||||
lastSleepAnalysisTime = currentTime;
|
||||
|
||||
if (sensorData.heart_valid || sensorData.breath_valid) {
|
||||
float hr = sensorData.heart_valid ? sensorData.heart_rate : 0;
|
||||
float rr = sensorData.breath_valid ? sensorData.breath_rate : 0;
|
||||
|
||||
if (hr > 0 || rr > 0) {
|
||||
physioProcessor->update(hr, rr,
|
||||
sensorData.heart_valid ? 80 : 0,
|
||||
sensorData.breath_valid ? 80 : 0);
|
||||
|
||||
HeartRateData hrData = physioProcessor->getHeartRateData();
|
||||
RespirationData rrData = physioProcessor->getRespirationData();
|
||||
HRVEstimate hrvData = physioProcessor->getHRVEstimate();
|
||||
|
||||
BodyMovementData movementData;
|
||||
memset(&movementData, 0, sizeof(BodyMovementData));
|
||||
movementData.movement = sensorData.body_movement;
|
||||
movementData.movementSmoothed = sensorData.body_movement;
|
||||
movementData.movementMean = sensorData.body_movement;
|
||||
movementData.activityLevel = sensorData.body_movement / 100.0f;
|
||||
movementData.isValid = (sensorData.body_movement >= 0 && sensorData.body_movement <= 100);
|
||||
movementData.timestamp = currentTime;
|
||||
|
||||
sleepAnalyzer->update(hrData, rrData, hrvData, movementData);
|
||||
|
||||
sleepAnalyzer->printState();
|
||||
|
||||
if (currentTime - lastStatsPrintTime >= STATS_PRINT_INTERVAL) {
|
||||
lastStatsPrintTime = currentTime;
|
||||
sleepAnalyzer->printStatistics();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(50 / portTICK_PERIOD_MS);
|
||||
esp_task_wdt_reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief LED控制任务
|
||||
* 根据网络状态控制LED显示:断开时慢闪、连接中快闪、已连接时呼吸灯效果
|
||||
* @param parameter 任务参数(未使用)
|
||||
*/
|
||||
void ledControlTask(void *parameter) {
|
||||
Serial.println("💡 启动LED控制任务...");
|
||||
|
||||
pinMode(NETWORK_LED_PIN, OUTPUT);
|
||||
digitalWrite(NETWORK_LED_PIN, LOW);
|
||||
ledcSetup(0, 5000, 8);
|
||||
ledcAttachPin(NETWORK_LED_PIN, 0);
|
||||
|
||||
while (1) {
|
||||
if (forceLedOff) {
|
||||
ledcWrite(0, 0);
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (currentNetworkStatus) {
|
||||
case NET_INITIAL:
|
||||
case NET_DISCONNECTED:
|
||||
if (millis() - lastBlinkTime >= SLOW_BLINK_INTERVAL) {
|
||||
ledState = !ledState;
|
||||
if(ledState) {
|
||||
ledcWrite(0, 255);
|
||||
} else {
|
||||
ledcWrite(0, 0);
|
||||
}
|
||||
lastBlinkTime = millis();
|
||||
}
|
||||
break;
|
||||
|
||||
case NET_CONNECTING:
|
||||
if (millis() - lastBlinkTime >= FAST_BLINK_INTERVAL) {
|
||||
ledState = !ledState;
|
||||
if(ledState) {
|
||||
ledcWrite(0, 255);
|
||||
} else {
|
||||
ledcWrite(0, 0);
|
||||
}
|
||||
lastBlinkTime = millis();
|
||||
}
|
||||
break;
|
||||
|
||||
case NET_CONNECTED:
|
||||
if (millis() - lastBlinkTime >= BREATHE_INTERVAL) {
|
||||
ledcWrite(0, breatheValue);
|
||||
|
||||
if (breatheIncreasing) {
|
||||
breatheValue += BREATHE_STEP;
|
||||
if (breatheValue >= BREATHE_MAX) {
|
||||
breatheValue = BREATHE_MAX;
|
||||
breatheIncreasing = false;
|
||||
}
|
||||
} else {
|
||||
breatheValue -= BREATHE_STEP;
|
||||
if (breatheValue <= BREATHE_MIN) {
|
||||
breatheValue = BREATHE_MIN;
|
||||
breatheIncreasing = true;
|
||||
}
|
||||
}
|
||||
lastBlinkTime = millis();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
void WiFiEvent(WiFiEvent_t event) {
|
||||
switch (event) {
|
||||
case ARDUINO_EVENT_WIFI_STA_START:
|
||||
setNetworkStatus(NET_INITIAL);
|
||||
break;
|
||||
|
||||
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
|
||||
setNetworkStatus(NET_CONNECTING);
|
||||
break;
|
||||
|
||||
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
|
||||
setNetworkStatus(NET_CONNECTED);
|
||||
break;
|
||||
|
||||
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
|
||||
setNetworkStatus(NET_DISCONNECTED);
|
||||
break;
|
||||
|
||||
case ARDUINO_EVENT_WIFI_STA_STOP:
|
||||
setNetworkStatus(NET_DISCONNECTED);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief WiFi监控任务
|
||||
* 初始化WiFi并定期打印连接状态和信号强度
|
||||
* @param parameter 任务参数(未使用)
|
||||
*/
|
||||
void wifiMonitorTask(void *parameter) {
|
||||
Serial.println("📡 WiFi监控任务启动");
|
||||
|
||||
wifiManager.begin();
|
||||
|
||||
if (wifiManager.getSavedNetworkCount() > 0) {
|
||||
Serial.printf("💾 检测到 %d 个已保存的WiFi配置,尝试连接...\n", wifiManager.getSavedNetworkCount());
|
||||
if (wifiManager.initializeWiFi()) {
|
||||
Serial.println("✅ WiFi连接成功!");
|
||||
} else {
|
||||
Serial.println("❌ WiFi连接失败,请通过BLE重新配置");
|
||||
}
|
||||
} else {
|
||||
Serial.println("⚠️ 未检测到WiFi配置,请通过BLE进行网络配置");
|
||||
}
|
||||
|
||||
size_t wifi_first_len = preferences.getBytes("wifi_first", &WiFi_Connect_First_bit, sizeof(WiFi_Connect_First_bit));
|
||||
if (wifi_first_len == sizeof(WiFi_Connect_First_bit)) {
|
||||
Serial.printf("从Flash读取 WiFi_Connect_First_bit: %u\n", WiFi_Connect_First_bit);
|
||||
} else {
|
||||
Serial.println("Flash中无 wifi_first 条目,保留内存中原始值");
|
||||
}
|
||||
|
||||
if(WiFi_Connect_First_bit == 0)
|
||||
{
|
||||
unsigned long wifiWaitStart = millis();
|
||||
unsigned long lastWifiWaitPrint = 0;
|
||||
const unsigned long WIFI_WAIT_TIMEOUT = 15000;
|
||||
while (WiFi.status() != WL_CONNECTED && (millis() - wifiWaitStart) < WIFI_WAIT_TIMEOUT) {
|
||||
if (millis() - lastWifiWaitPrint >= 1000) {
|
||||
lastWifiWaitPrint = millis();
|
||||
}
|
||||
yield();
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
Serial.println("📡 WiFi初始化完成,开始监控...");
|
||||
|
||||
while(1) {
|
||||
static unsigned long lastPrint = 0;
|
||||
if (millis() - lastPrint > 30000) {
|
||||
Serial.printf("📡 WiFi状态: %d, RSSI: %d dBm\n",
|
||||
WiFi.status(), WiFi.RSSI());
|
||||
lastPrint = millis();
|
||||
}
|
||||
vTaskDelay(5000 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief BLE配置处理任务
|
||||
* 处理BLE配置命令,监控设备连接状态并管理广播
|
||||
* @param parameter 任务参数(未使用)
|
||||
*/
|
||||
void bleConfigTask(void *parameter) {
|
||||
Serial.println("📡 BLE配置处理任务启动");
|
||||
|
||||
char snName[32];
|
||||
if (device_sn > 0) {
|
||||
snprintf(snName, sizeof(snName), "Radar_%llu", device_sn);
|
||||
} else {
|
||||
String macAddr = getDeviceMacAddress();
|
||||
macAddr.replace(":", "");
|
||||
snprintf(snName, sizeof(snName), "Radar_%s", macAddr.c_str());
|
||||
}
|
||||
BLEDevice::init(snName);
|
||||
pServer = BLEDevice::createServer();
|
||||
pServer->setCallbacks(new MyServerCallbacks());
|
||||
|
||||
BLEService *pService = pServer->createService(SERVICE_UUID);
|
||||
pCharacteristic = pService->createCharacteristic(
|
||||
CHARACTERISTIC_UUID,
|
||||
BLECharacteristic::PROPERTY_READ |
|
||||
BLECharacteristic::PROPERTY_WRITE |
|
||||
BLECharacteristic::PROPERTY_NOTIFY
|
||||
);
|
||||
pCharacteristic->setCallbacks(new MyCallbacks());
|
||||
pCharacteristic->addDescriptor(new BLE2902());
|
||||
|
||||
pService->start();
|
||||
refreshBLEAdvertisingData();
|
||||
BLEDevice::startAdvertising();
|
||||
|
||||
Serial.println(String("✅ BLE已启动,设备名称: ") + snName);
|
||||
|
||||
while(1) {
|
||||
processBLEConfig();
|
||||
|
||||
if (!deviceConnected && oldDeviceConnected) {
|
||||
vTaskDelay(500 / portTICK_PERIOD_MS);
|
||||
pServer->startAdvertising();
|
||||
Serial.println("开始BLE广播");
|
||||
oldDeviceConnected = deviceConnected;
|
||||
}
|
||||
if (deviceConnected && !oldDeviceConnected) {
|
||||
oldDeviceConnected = deviceConnected;
|
||||
}
|
||||
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 雷达命令发送任务
|
||||
* 每2秒轮流向雷达模组发送11条不同命令,查询心率、呼吸率、睡眠状态等数据
|
||||
* @param parameter 任务参数(未使用)
|
||||
*/
|
||||
void radarCmdTask(void *parameter) {
|
||||
Serial.println("📡 雷达命令发送任务启动");
|
||||
initR60ABD1();
|
||||
|
||||
static const uint8_t radar_cmds[][3] = {
|
||||
{0x84, 0x81, 0x0F}, // 0x81: 查询心率/呼吸率
|
||||
{0x84, 0x8D, 0x0F}, // 0x8D: 查询睡眠状态
|
||||
{0x84, 0x8F, 0x0F}, // 0x8F: 查询体动数据
|
||||
{0x84, 0x8E, 0x0F}, // 0x8E: 查询人员存在
|
||||
{0x84, 0x91, 0x0F}, // 0x91: 查询呼吸波形
|
||||
{0x84, 0x92, 0x0F}, // 0x92: 查询呼吸波形(备用)
|
||||
{0x84, 0x83, 0x0F}, // 0x83: 查询心跳波形
|
||||
{0x84, 0x84, 0x0F}, // 0x84: 查询心跳波形(备用)
|
||||
{0x84, 0x85, 0x0F}, // 0x85: 查询心跳波形(扩展)
|
||||
{0x84, 0x86, 0x0F}, // 0x86: 查询心跳波形(扩展)
|
||||
{0x84, 0x90, 0x0F} // 0x90: 查询综合状态
|
||||
};
|
||||
|
||||
static size_t cmdIndex = 0;
|
||||
static unsigned long lastCmdMillis = 0;
|
||||
const unsigned long CMD_INTERVAL = 2000UL;
|
||||
|
||||
while (1) {
|
||||
unsigned long now = millis();
|
||||
|
||||
if (now - lastCmdMillis >= CMD_INTERVAL) {
|
||||
sendRadarCommand(
|
||||
radar_cmds[cmdIndex][0],
|
||||
radar_cmds[cmdIndex][1],
|
||||
radar_cmds[cmdIndex][2]
|
||||
);
|
||||
|
||||
lastCmdMillis = now;
|
||||
cmdIndex++;
|
||||
if (cmdIndex >= sizeof(radar_cmds) / sizeof(radar_cmds[0])) {
|
||||
cmdIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||
esp_task_wdt_reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 情绪分析任务
|
||||
* 每秒更新生理数据、校准基线、分析情绪并输出结果
|
||||
* @param parameter 任务参数(未使用)
|
||||
*/
|
||||
void emotionAnalysisTask(void *parameter) {
|
||||
physioProcessor = new PhysioDataProcessor();
|
||||
emotionAnalyzer = new SimpleEmotionAnalyzer(60);
|
||||
|
||||
static unsigned long lastEmotionAnalysisTime = 0;
|
||||
const unsigned long EMOTION_ANALYSIS_INTERVAL = 1000;
|
||||
|
||||
while (1) {
|
||||
unsigned long currentTime = millis();
|
||||
|
||||
if (currentTime - lastEmotionAnalysisTime >= EMOTION_ANALYSIS_INTERVAL) {
|
||||
lastEmotionAnalysisTime = currentTime;
|
||||
|
||||
if (sensorData.heart_valid || sensorData.breath_valid) {
|
||||
float hr = sensorData.heart_valid ? sensorData.heart_rate : 0;
|
||||
float rr = sensorData.breath_valid ? sensorData.breath_rate : 0;
|
||||
|
||||
if (hr > 0 || rr > 0) {
|
||||
physioProcessor->update(hr, rr,
|
||||
sensorData.heart_valid ? 80 : 0,
|
||||
sensorData.breath_valid ? 80 : 0);
|
||||
|
||||
HeartRateData hrData = physioProcessor->getHeartRateData();
|
||||
RespirationData rrData = physioProcessor->getRespirationData();
|
||||
HRVEstimate hrvData = physioProcessor->getHRVEstimate();
|
||||
|
||||
BodyMovementData movementData;
|
||||
memset(&movementData, 0, sizeof(BodyMovementData));
|
||||
movementData.movement = sensorData.body_movement;
|
||||
movementData.movementSmoothed = sensorData.body_movement;
|
||||
movementData.movementMean = sensorData.body_movement;
|
||||
movementData.activityLevel = sensorData.body_movement / 100.0f;
|
||||
movementData.isValid = (sensorData.body_movement >= 0 && sensorData.body_movement <= 100);
|
||||
movementData.timestamp = currentTime;
|
||||
|
||||
if (hrData.isValid && rrData.isValid) {
|
||||
emotionAnalyzer->calibrateBaseline(hrData, rrData, movementData);
|
||||
}
|
||||
|
||||
EmotionResult emotionResult = emotionAnalyzer->analyze(hrData, rrData, hrvData, movementData);
|
||||
|
||||
if (emotionResult.isValid) {
|
||||
Serial.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
Serial.printf("主要情绪:%s (置信度: %.1f%%);",
|
||||
EMOTION_NAMES[emotionResult.primaryEmotion],
|
||||
emotionResult.confidence * 100);
|
||||
Serial.printf("次要情绪: %s倾向\n",
|
||||
EMOTION_NAMES[emotionResult.secondaryEmotion]);
|
||||
Serial.printf("情绪强度:%.1f%% ", emotionResult.intensity * 100);
|
||||
Serial.printf("效价:%.2f(负面到正面) ", emotionResult.valence);
|
||||
Serial.printf("唤醒度:%.2f(平静到激动)\n", emotionResult.arousal);
|
||||
Serial.printf("压力水平:%.1f ", emotionResult.stressLevel);
|
||||
Serial.printf("焦虑水平:%.1f ", emotionResult.anxietyLevel);
|
||||
Serial.printf("放松水平:%.1f ", emotionResult.relaxationLevel);
|
||||
Serial.printf("交感神经活动:%.2f ", emotionResult.sympatheticActivity);
|
||||
Serial.printf("副交感神经活动:%.2f\n", emotionResult.parasympatheticActivity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(50 / portTICK_PERIOD_MS);
|
||||
esp_task_wdt_reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 初始化所有FreeRTOS任务
|
||||
* 创建并启动所有后台任务:BOOT按钮监控、LED控制、WiFi监控、MQTT、BLE配置、雷达命令发送、情绪分析、睡眠分析
|
||||
*/
|
||||
void initAllTasks() {
|
||||
loadDeviceSN();
|
||||
xTaskCreate(bootButtonMonitorTask, "Boot Button Monitor Task", 2048, NULL, 1, NULL);
|
||||
xTaskCreate(ledControlTask, "LED Control Task", 2048, NULL, 1, NULL);
|
||||
xTaskCreate(wifiMonitorTask, "WiFi Monitor Task", 4096, NULL, 2, NULL);
|
||||
xTaskCreatePinnedToCore(mqttTask, "MQTT Task", 8192, NULL, 2, &mqttTaskHandle, 1);
|
||||
xTaskCreate(bleConfigTask, "BLE Config Task", 4096, NULL, 1, NULL);
|
||||
xTaskCreate(radarCmdTask, "Radar Cmd Task", 2048, NULL, 2, NULL);
|
||||
xTaskCreate(emotionAnalysisTask, "Emotion Analysis Task", 4096, NULL, 1, NULL);
|
||||
xTaskCreate(sleepAnalysisTask, "Sleep Analysis Task", 4096, NULL, 1, NULL);
|
||||
}
|
||||
73
src/tasks_manager.h
Normal file
73
src/tasks_manager.h
Normal file
@@ -0,0 +1,73 @@
|
||||
#ifndef TASKS_MANAGER_H
|
||||
#define TASKS_MANAGER_H
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <BLEDevice.h>
|
||||
#include <BLEServer.h>
|
||||
#include <BLEUtils.h>
|
||||
#include <BLE2902.h>
|
||||
#include <WiFi.h>
|
||||
#include <Arduino.h>
|
||||
#include <Preferences.h>
|
||||
#include "wifi_manager.h"
|
||||
#include "data_processor.h"
|
||||
#include "emotion_analyzer_simple.h"
|
||||
#include "sleep_analyzer.h"
|
||||
#include "mqtt.h"
|
||||
|
||||
// ESP32 GPIO控制演示
|
||||
#define BOOT_BUTTON_PIN 0 // Boot按钮引脚
|
||||
#define NETWORK_LED_PIN 5 // 网络状态LED指示灯开发板48引脚,雷达板5引脚
|
||||
#define CONFIG_CLEAR_PIN 4 // 配置清除指示灯
|
||||
#define GPIO8 8 // 自定义GPIO8
|
||||
#define GPIO9 9 // 自定义GPIO9
|
||||
|
||||
#define CLEAR_CONFIG_DURATION 3000 // 清除配置持续时间(毫秒)
|
||||
#define SLOW_BLINK_INTERVAL 1000 // 慢闪间隔(毫秒)
|
||||
#define FAST_BLINK_INTERVAL 200 // 快闪间隔(毫秒)
|
||||
#define BREATHE_INTERVAL 40 // 呼吸灯更新间隔(毫秒)
|
||||
#define BREATHE_MIN 0 // 呼吸灯最小亮度值
|
||||
#define BREATHE_MAX 155 // 呼吸灯最大亮度值
|
||||
#define BREATHE_STEP 5 // 呼吸灯亮度步进值
|
||||
|
||||
extern Preferences preferences;
|
||||
extern WiFiManager wifiManager;
|
||||
extern uint16_t currentDeviceId;
|
||||
extern uint64_t device_sn;
|
||||
extern bool deviceConnected;
|
||||
extern bool oldDeviceConnected;
|
||||
extern BLEServer* pServer;
|
||||
extern BLECharacteristic* pCharacteristic;
|
||||
extern NetworkStatus currentNetworkStatus;
|
||||
extern unsigned long lastBlinkTime;
|
||||
extern bool ledState;
|
||||
extern int breatheValue;
|
||||
extern bool breatheIncreasing;
|
||||
extern uint8_t WiFi_Connect_First_bit;
|
||||
extern void sendRadarCommand(uint8_t cmd, uint8_t subCmd, uint8_t param);
|
||||
extern void initR60ABD1();
|
||||
extern void processBLEConfig();
|
||||
extern void sendStatusToBLE();
|
||||
extern void sendSleepDataToInfluxDB();
|
||||
extern void setNetworkStatus(NetworkStatus status);
|
||||
extern void clearStoredConfig();
|
||||
extern void loadDeviceId();
|
||||
extern void saveDeviceId();
|
||||
extern uint32_t generateDeviceHash();
|
||||
extern std::string buildBLEManufacturerData();
|
||||
extern void refreshBLEAdvertisingData();
|
||||
extern String getDeviceMacAddress();
|
||||
|
||||
void initAllTasks();
|
||||
void WiFiEvent(WiFiEvent_t event);
|
||||
|
||||
void bootButtonMonitorTask(void *parameter);
|
||||
void ledControlTask(void *parameter);
|
||||
void wifiMonitorTask(void *parameter);
|
||||
void bleConfigTask(void *parameter);
|
||||
void radarCmdTask(void *parameter);
|
||||
void emotionAnalysisTask(void *parameter);
|
||||
void sleepAnalysisTask(void *parameter);
|
||||
|
||||
#endif
|
||||
@@ -14,6 +14,24 @@ WiFiManager::WiFiManager() {
|
||||
currentState = WIFI_IDLE;
|
||||
lastReconnectAttempt = 0;
|
||||
isScanning = false;
|
||||
manualConfigActive = false;
|
||||
scanInProgress = false;
|
||||
reconnectTaskHandle = NULL;
|
||||
|
||||
// 创建扫描信号量(二值信号量)
|
||||
scanSemaphore = xSemaphoreCreateBinary();
|
||||
if (scanSemaphore == NULL) {
|
||||
Serial.println("❌ 创建扫描信号量失败");
|
||||
} else {
|
||||
// 初始状态:信号量不可用
|
||||
xSemaphoreTake(scanSemaphore, 0);
|
||||
}
|
||||
|
||||
// 创建状态互斥锁
|
||||
stateMutex = xSemaphoreCreateMutex();
|
||||
if (stateMutex == NULL) {
|
||||
Serial.println("❌ 创建状态互斥锁失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,6 +41,48 @@ WiFiManager::WiFiManager() {
|
||||
void WiFiManager::begin() {
|
||||
preferences.begin("wifi_manager", false);
|
||||
loadWiFiConfigs();
|
||||
|
||||
// 注册WiFi事件监听器
|
||||
WiFi.onEvent([this](WiFiEvent_t event, WiFiEventInfo_t info) {
|
||||
switch (event) {
|
||||
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
|
||||
Serial.println("⚠️ [WiFi事件] WiFi断开连接");
|
||||
if (currentState == WIFI_CONNECTED) {
|
||||
currentState = WIFI_DISCONNECTED;
|
||||
setNetworkStatus(NET_DISCONNECTED);
|
||||
Serial.println("🔄 [WiFi事件] 已触发重连标志");
|
||||
}
|
||||
break;
|
||||
|
||||
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
|
||||
break;
|
||||
|
||||
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
|
||||
currentState = WIFI_CONNECTED;
|
||||
setNetworkStatus(NET_CONNECTED);
|
||||
break;
|
||||
|
||||
case ARDUINO_EVENT_WIFI_STA_LOST_IP:
|
||||
Serial.println("⚠️ [WiFi事件] 丢失IP地址");
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 创建重连任务(低优先级,会被扫描任务抢占)
|
||||
if (reconnectTaskHandle == NULL) {
|
||||
xTaskCreate(
|
||||
reconnectTask,
|
||||
"WiFi Reconnect Task",
|
||||
4096,
|
||||
this,
|
||||
1, // 低优先级
|
||||
&reconnectTaskHandle
|
||||
);
|
||||
Serial.println("✅ WiFi重连任务已创建");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,8 +185,6 @@ bool WiFiManager::saveWiFiConfig(const char* ssid, const char* password) {
|
||||
* @return 是否连接成功
|
||||
*/
|
||||
bool WiFiManager::connectToNetwork(const char* ssid, const char* password) {
|
||||
Serial.printf("🌐 [WiFi] 尝试连接到 SSID: %s\n", ssid);
|
||||
|
||||
currentState = WIFI_CONNECTING;
|
||||
setNetworkStatus(NET_CONNECTING);
|
||||
|
||||
@@ -139,7 +197,6 @@ bool WiFiManager::connectToNetwork(const char* ssid, const char* password) {
|
||||
|
||||
while (WiFi.status() != WL_CONNECTED && (millis() - startTime) < WIFI_CONNECT_TIMEOUT) {
|
||||
if (millis() - lastStatusPrint >= 500) {
|
||||
Serial.printf("[WiFi] 连接中,状态: %d\n", WiFi.status());
|
||||
lastStatusPrint = millis();
|
||||
}
|
||||
yield();
|
||||
@@ -154,6 +211,12 @@ bool WiFiManager::connectToNetwork(const char* ssid, const char* password) {
|
||||
currentState = WIFI_CONNECTED;
|
||||
setNetworkStatus(NET_CONNECTED);
|
||||
|
||||
// 清除手动配置标志位,恢复WiFi重连机制
|
||||
if (manualConfigActive) {
|
||||
manualConfigActive = false;
|
||||
Serial.println("🔧 [WiFi] 手动配置完成,WiFi重连机制已恢复");
|
||||
}
|
||||
|
||||
// 向蓝牙发送当前连接的WiFi配置信息
|
||||
if (deviceConnected) {
|
||||
JsonDocument doc;
|
||||
@@ -203,10 +266,16 @@ bool WiFiManager::connectToNetwork(const char* ssid, const char* password) {
|
||||
* @return 是否成功连接到匹配的网络
|
||||
*/
|
||||
bool WiFiManager::scanAndMatchNetworks() {
|
||||
// 首先检查是否有已保存的WiFi网络
|
||||
if (savedNetworkCount == 0) {
|
||||
Serial.println("⚠️ [WiFi] 没有已保存的WiFi网络,跳过重连扫描");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isScanning) {
|
||||
Serial.println("⏳ [WiFi] 正在扫描中,等待扫描完成...");
|
||||
int waitCount = 0;
|
||||
while (isScanning && waitCount < 50) {
|
||||
while (isScanning && waitCount < 100) {
|
||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||
waitCount++;
|
||||
}
|
||||
@@ -223,25 +292,87 @@ bool WiFiManager::scanAndMatchNetworks() {
|
||||
currentState = WIFI_SCANNING;
|
||||
isScanning = true;
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.println("📶 WiFi已连接,断开后扫描");
|
||||
WiFi.disconnect(false);
|
||||
vTaskDelay(200 / portTICK_PERIOD_MS);
|
||||
// 先完全断开并重置WiFi
|
||||
Serial.println("🔄 [WiFi] 重置WiFi硬件...");
|
||||
WiFi.disconnect(true); // true = 关闭WiFi radio
|
||||
vTaskDelay(500 / portTICK_PERIOD_MS);
|
||||
|
||||
// 重新初始化WiFi
|
||||
WiFi.mode(WIFI_STA);
|
||||
vTaskDelay(300 / portTICK_PERIOD_MS);
|
||||
|
||||
// 扫描重试机制
|
||||
int n = -1;
|
||||
int scanRetryCount = 0;
|
||||
const int MAX_SCAN_RETRIES = 5;
|
||||
|
||||
while (n <= 0 && scanRetryCount < MAX_SCAN_RETRIES && !manualConfigActive) {
|
||||
if (scanRetryCount > 0) {
|
||||
Serial.printf("🔄 [WiFi] 扫描重试 %d/%d...\n", scanRetryCount, MAX_SCAN_RETRIES);
|
||||
// 重试前再次重置WiFi
|
||||
WiFi.disconnect(true);
|
||||
vTaskDelay(500 / portTICK_PERIOD_MS);
|
||||
WiFi.mode(WIFI_STA);
|
||||
vTaskDelay(300 / portTICK_PERIOD_MS);
|
||||
}
|
||||
|
||||
WiFi.mode(WIFI_STA);
|
||||
vTaskDelay(200 / portTICK_PERIOD_MS);
|
||||
n = WiFi.scanNetworks(false, true, false, 1000); // 增加扫描时间到1秒
|
||||
Serial.printf("🔍 扫描结果: %d 个WiFi网络 (尝试 %d/%d), WiFi状态: %d\n",
|
||||
n, scanRetryCount + 1, MAX_SCAN_RETRIES, WiFi.status());
|
||||
scanRetryCount++;
|
||||
}
|
||||
|
||||
int n = WiFi.scanNetworks();
|
||||
Serial.printf("🔍 扫描到 %d 个WiFi网络\n", n);
|
||||
|
||||
if (n <= 0) {
|
||||
Serial.println("❌ 未扫描到任何WiFi网络或扫描失败");
|
||||
// 检查是否被蓝牙配网中断
|
||||
if (manualConfigActive) {
|
||||
Serial.println("🔧 [WiFi] 重连扫描被蓝牙配网中断");
|
||||
currentState = WIFI_DISCONNECTED;
|
||||
isScanning = false;
|
||||
WiFi.scanDelete();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (n <= 0) {
|
||||
Serial.println("❌ 多次重试后仍未扫描到WiFi网络,WiFi硬件可能异常");
|
||||
currentState = WIFI_DISCONNECTED;
|
||||
isScanning = false;
|
||||
// 清理WiFi状态
|
||||
WiFi.scanDelete();
|
||||
// 尝试完全重置WiFi
|
||||
WiFi.mode(WIFI_OFF);
|
||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||
WiFi.mode(WIFI_STA);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 发送扫描结果到蓝牙(重连时的扫描也发送)
|
||||
// 再次检查是否被中断
|
||||
if (manualConfigActive) {
|
||||
Serial.println("🔧 [WiFi] 重连扫描被蓝牙配网中断");
|
||||
currentState = WIFI_DISCONNECTED;
|
||||
isScanning = false;
|
||||
WiFi.scanDelete();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (deviceConnected) {
|
||||
String wifiList = String("{\"type\":\"scanWiFiResult\",\"success\":true,\"source\":\"reconnect\",\"count\":") + String(n) + String(",\"networks\":[");
|
||||
bool first = true;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (WiFi.RSSI(i) >= MIN_RSSI_THRESHOLD) {
|
||||
if (!first) {
|
||||
wifiList += ",";
|
||||
}
|
||||
wifiList += String("{\"ssid\":\"") + WiFi.SSID(i) + String("\",\"rssi\":") +
|
||||
String(WiFi.RSSI(i)) + String(",\"channel\":") +
|
||||
String(WiFi.channel(i)) + String("}");
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
wifiList += "]}";
|
||||
Serial.printf("📱 [BLE] 发送重连扫描结果,共 %d 个网络\n", n);
|
||||
sendJSONDataToBLE(wifiList);
|
||||
}
|
||||
|
||||
// 收集所有匹配的、信号强度符合要求的网络
|
||||
struct CandidateNetwork {
|
||||
const char* ssid;
|
||||
@@ -257,8 +388,6 @@ bool WiFiManager::scanAndMatchNetworks() {
|
||||
for (int j = 0; j < n; j++) {
|
||||
if (WiFi.SSID(j) == String(savedNetworks[i].ssid)) {
|
||||
int rssi = WiFi.RSSI(j);
|
||||
Serial.printf("📶 找到匹配网络: %s, 信号: %d dBm\n",
|
||||
savedNetworks[i].ssid, rssi);
|
||||
|
||||
// 检查信号强度是否符合要求
|
||||
if (rssi >= MIN_RSSI_THRESHOLD) {
|
||||
@@ -268,8 +397,6 @@ bool WiFiManager::scanAndMatchNetworks() {
|
||||
availableNetworks[availableCount].password = savedNetworks[i].password;
|
||||
availableNetworks[availableCount].rssi = rssi;
|
||||
availableCount++;
|
||||
Serial.printf("✅ 添加到候选列表: %s, 信号: %d dBm\n",
|
||||
savedNetworks[i].ssid, rssi);
|
||||
}
|
||||
} else {
|
||||
Serial.printf("⚠️ 信号强度过低,跳过\n");
|
||||
@@ -309,6 +436,14 @@ bool WiFiManager::scanAndMatchNetworks() {
|
||||
|
||||
// 依次尝试连接所有可用网络
|
||||
for (int i = 0; i < availableCount; i++) {
|
||||
// 检查是否被蓝牙配网中断
|
||||
if (manualConfigActive) {
|
||||
Serial.println("🔧 [WiFi] 重连被蓝牙配网中断");
|
||||
currentState = WIFI_DISCONNECTED;
|
||||
isScanning = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("🔄 [%d/%d] 尝试连接: %s (信号: %d dBm)\n",
|
||||
i + 1, availableCount,
|
||||
availableNetworks[i].ssid,
|
||||
@@ -332,24 +467,9 @@ bool WiFiManager::scanAndMatchNetworks() {
|
||||
}
|
||||
|
||||
Serial.println("❌ 所有可用网络均连接失败");
|
||||
currentState = WIFI_DISCONNECTED;
|
||||
currentState = WIFI_DISCONNECTED;// 扫描失败,设置为断开连接状态
|
||||
isScanning = false;
|
||||
|
||||
// 向蓝牙发送所有WiFi连接失败信息
|
||||
if (deviceConnected) {
|
||||
String wifiList = "{\"type\":\"wifiConnected\",\"success\":false,\"message\":\"所有保存的WiFi均连接失败\",\"networks\":[";
|
||||
bool first = true;
|
||||
for (int i = 0; i < availableCount; i++) {
|
||||
if (!first) wifiList += ",";
|
||||
wifiList += "{\"ssid\":\"" + String(availableNetworks[i].ssid) + "\",\"rssi\":" + String(availableNetworks[i].rssi) + "}";
|
||||
first = false;
|
||||
}
|
||||
wifiList += "],\"count\":" + String(availableCount) + "}";
|
||||
|
||||
sendJSONDataToBLE(wifiList);
|
||||
Serial.printf("📱 [BLE] 发送所有WiFi连接失败信息: %s\n", wifiList.c_str());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -405,28 +525,27 @@ void WiFiManager::scanAndSendResults() {
|
||||
Serial.println("📱 [BLE-WiFi] 开始WiFi扫描...");
|
||||
isScanning = true;
|
||||
|
||||
// 确保完全断开WiFi
|
||||
// 如果已连接,先断开以确保扫描质量
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.println("📶 WiFi已连接,断开后扫描");
|
||||
Serial.println("📶 WiFi已连接,断开后进行同步扫描");
|
||||
WiFi.disconnect(true);
|
||||
vTaskDelay(500 / portTICK_PERIOD_MS);
|
||||
}
|
||||
|
||||
// 更完整的WiFi初始化
|
||||
WiFi.mode(WIFI_OFF);
|
||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||
// 使用同步扫描,确保扫描完整完成,避免时序竞争
|
||||
Serial.println("🔍 [WiFi] 使用同步扫描...");
|
||||
int n = WiFi.scanNetworks(false, true, false, 300); // false = 同步扫描
|
||||
|
||||
WiFi.mode(WIFI_STA);
|
||||
vTaskDelay(500 / portTICK_PERIOD_MS);
|
||||
// 如果同步扫描失败,使用主动扫描重试
|
||||
if (n <= 0) {
|
||||
Serial.println("🔄 同步扫描失败,重试主动扫描...");
|
||||
|
||||
// 尝试多次扫描,提高成功率
|
||||
int n = 0;
|
||||
int retryCount = 3;
|
||||
|
||||
while (n <= 0 && retryCount > 0) {
|
||||
Serial.printf("🔍 扫描WiFi网络 (尝试 %d/3)...\n", 4 - retryCount);
|
||||
// 使用更可靠的扫描参数:被动扫描,包含隐藏SSID,更长的扫描时间
|
||||
n = WiFi.scanNetworks(false, true, true, 5000); // 5000ms扫描时间
|
||||
Serial.printf("🔍 同步扫描WiFi网络 (尝试 %d/3)...\n", 4 - retryCount);
|
||||
|
||||
// 使用主动扫描重试,增加扫描时间
|
||||
n = WiFi.scanNetworks(false, true, false, 800);
|
||||
|
||||
if (n <= 0) {
|
||||
Serial.printf("❌ 扫描失败,返回值: %d, 剩余重试: %d\n", n, retryCount - 1);
|
||||
@@ -434,6 +553,7 @@ void WiFiManager::scanAndSendResults() {
|
||||
vTaskDelay(1000 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Serial.printf("🔍 最终扫描到 %d 个WiFi网络\n", n);
|
||||
|
||||
@@ -507,18 +627,6 @@ void WiFiManager::scanAndSendResults() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 开始配网模式
|
||||
* 进入配网模式,扫描WiFi网络并发送结果给客户端
|
||||
* @return 是否成功进入配网模式
|
||||
*/
|
||||
bool WiFiManager::startConfiguration() {
|
||||
Serial.println("⚙️ [WiFi] 开始配网模式...");
|
||||
currentState = WIFI_CONFIGURING;
|
||||
scanAndSendResults();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 处理配网数据
|
||||
* 处理从客户端收到的WiFi配网信息,先扫描WiFi是否有匹配的网络,再尝试连接并保存
|
||||
@@ -527,13 +635,57 @@ bool WiFiManager::startConfiguration() {
|
||||
* @return 是否配置成功
|
||||
*/
|
||||
bool WiFiManager::handleConfigurationData(const char* ssid, const char* password) {
|
||||
// 立即设置手动配置标志位,暂停所有WiFi操作
|
||||
manualConfigActive = true;
|
||||
|
||||
Serial.println("🔧 [WiFi] 手动配置模式已激活,立即停止所有WiFi操作");
|
||||
|
||||
// 立即断开当前WiFi连接
|
||||
WiFi.disconnect(true);
|
||||
|
||||
// 如果正在扫描,立即停止扫描
|
||||
if (isScanning || scanInProgress) {
|
||||
Serial.println("🛑 [WiFi] 检测到正在扫描,立即停止");
|
||||
WiFi.scanDelete();
|
||||
isScanning = false;
|
||||
scanInProgress = false;
|
||||
currentState = WIFI_DISCONNECTED;
|
||||
vTaskDelay(200 / portTICK_PERIOD_MS);
|
||||
}
|
||||
|
||||
Serial.printf("📱 [BLE-WiFi] 收到配网信息: SSID='%s'\n", ssid);
|
||||
|
||||
if (ssid == nullptr || password == nullptr || strlen(ssid) == 0) {
|
||||
Serial.println("❌ 配网参数无效");
|
||||
if (ssid == nullptr || strlen(ssid) == 0) {
|
||||
Serial.println("❌ 配网参数无效:SSID为空");
|
||||
manualConfigActive = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果密码为空,尝试从已保存的WiFi中查找密码
|
||||
const char* actualPassword = password;
|
||||
char savedPassword[64] = {0};
|
||||
bool foundSavedPassword = false;
|
||||
|
||||
if (password == nullptr || strlen(password) == 0) {
|
||||
Serial.println("🔑 密码为空,尝试从已保存的WiFi中查找...");
|
||||
|
||||
for (int i = 0; i < savedNetworkCount; i++) {
|
||||
if (strcmp(savedNetworks[i].ssid, ssid) == 0) {
|
||||
strncpy(savedPassword, savedNetworks[i].password, 63);
|
||||
savedPassword[63] = '\0';
|
||||
actualPassword = savedPassword;
|
||||
foundSavedPassword = true;
|
||||
Serial.printf("✅ 找到已保存的WiFi密码: %s\n", ssid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundSavedPassword) {
|
||||
Serial.println("⚠️ 未找到已保存的密码,将尝试无密码连接");
|
||||
actualPassword = "";
|
||||
}
|
||||
}
|
||||
|
||||
if (isScanning) {
|
||||
Serial.println("⏳ [WiFi] 正在扫描中,等待扫描完成...");
|
||||
int waitCount = 0;
|
||||
@@ -548,6 +700,7 @@ bool WiFiManager::handleConfigurationData(const char* ssid, const char* password
|
||||
String errorMsg = String("{\"type\":\"wifiConfigResult\",\"success\":false,\"message\":\"等待扫描超时,请稍后再试\"}");
|
||||
sendJSONDataToBLE(errorMsg);
|
||||
}
|
||||
manualConfigActive = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -559,10 +712,35 @@ bool WiFiManager::handleConfigurationData(const char* ssid, const char* password
|
||||
currentState = WIFI_SCANNING;
|
||||
isScanning = true;
|
||||
|
||||
int n = WiFi.scanNetworks();
|
||||
Serial.printf("🔍 扫描到 %d 个WiFi网络\n", n);
|
||||
// 重新初始化WiFi
|
||||
WiFi.mode(WIFI_STA);
|
||||
vTaskDelay(200 / portTICK_PERIOD_MS);
|
||||
|
||||
if (n == 0) {
|
||||
// 扫描重试机制
|
||||
int n = -1;
|
||||
int scanRetryCount = 0;
|
||||
const int MAX_SCAN_RETRIES = 3;
|
||||
|
||||
while (n <= 0 && scanRetryCount < MAX_SCAN_RETRIES && manualConfigActive) {
|
||||
if (scanRetryCount > 0) {
|
||||
Serial.printf("🔄 [WiFi] 扫描重试 %d/%d...\n", scanRetryCount, MAX_SCAN_RETRIES);
|
||||
vTaskDelay(500 / portTICK_PERIOD_MS);
|
||||
}
|
||||
|
||||
n = WiFi.scanNetworks(false, true, false, 500);
|
||||
Serial.printf("🔍 扫描结果: %d 个WiFi网络 (尝试 %d/%d)\n", n, scanRetryCount + 1, MAX_SCAN_RETRIES);
|
||||
scanRetryCount++;
|
||||
}
|
||||
|
||||
// 检查是否被中断
|
||||
if (!manualConfigActive) {
|
||||
Serial.println("⚠️ [WiFi] 配网被中断");
|
||||
WiFi.scanDelete();
|
||||
isScanning = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (n <= 0) {
|
||||
Serial.println("❌ 未扫描到任何WiFi网络");
|
||||
WiFi.scanDelete();
|
||||
isScanning = false;
|
||||
@@ -572,6 +750,8 @@ bool WiFiManager::handleConfigurationData(const char* ssid, const char* password
|
||||
String resultMsg = String("{\"type\":\"wifiConfigResult\",\"success\":false,\"message\":\"未扫描到任何WiFi网络,请检查设备位置\"}");
|
||||
sendJSONDataToBLE(resultMsg);
|
||||
}
|
||||
vTaskDelay(3000 / portTICK_PERIOD_MS);
|
||||
manualConfigActive = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -610,9 +790,16 @@ bool WiFiManager::handleConfigurationData(const char* ssid, const char* password
|
||||
isScanning = false;
|
||||
currentState = WIFI_DISCONNECTED;
|
||||
|
||||
if (manualConfigActive) {
|
||||
manualConfigActive = false;
|
||||
Serial.println("🔧 [WiFi] 手动配置失败,WiFi重连机制已恢复");
|
||||
}
|
||||
|
||||
if (deviceConnected) {
|
||||
sendJSONDataToBLE(errorMsg);
|
||||
}
|
||||
vTaskDelay(3000 / portTICK_PERIOD_MS);
|
||||
manualConfigActive = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -621,15 +808,16 @@ bool WiFiManager::handleConfigurationData(const char* ssid, const char* password
|
||||
vTaskDelay(300 / portTICK_PERIOD_MS);
|
||||
|
||||
// 尝试连接到指定网络
|
||||
if (connectToNetwork(ssid, password)) {
|
||||
if (connectToNetwork(ssid, actualPassword)) {
|
||||
// 连接成功后保存配置
|
||||
if (saveWiFiConfig(ssid, password)) {
|
||||
if (saveWiFiConfig(ssid, actualPassword)) {
|
||||
Serial.println("✅ WiFi配置成功并已保存");
|
||||
|
||||
if (deviceConnected) {
|
||||
String resultMsg = String("{\"type\":\"wifiConfigResult\",\"success\":true,\"message\":\"WiFi配置成功\"}");
|
||||
sendJSONDataToBLE(resultMsg);
|
||||
}
|
||||
manualConfigActive = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -638,10 +826,17 @@ bool WiFiManager::handleConfigurationData(const char* ssid, const char* password
|
||||
isScanning = false;
|
||||
currentState = WIFI_DISCONNECTED;
|
||||
|
||||
if (manualConfigActive) {
|
||||
manualConfigActive = false;
|
||||
Serial.println("🔧 [WiFi] 手动配置失败,WiFi重连机制已恢复");
|
||||
}
|
||||
|
||||
if (deviceConnected) {
|
||||
String resultMsg = String("{\"type\":\"wifiConfigResult\",\"success\":false,\"message\":\"WiFi配置失败,请检查密码是否正确\"}");
|
||||
sendJSONDataToBLE(resultMsg);
|
||||
}
|
||||
vTaskDelay(3000 / portTICK_PERIOD_MS);
|
||||
manualConfigActive = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -650,43 +845,95 @@ bool WiFiManager::handleConfigurationData(const char* ssid, const char* password
|
||||
* 检查WiFi连接状态,当断开连接时尝试重连
|
||||
*/
|
||||
void WiFiManager::handleReconnect() {
|
||||
Serial.println("📞 [handleReconnect] 进入重连处理");
|
||||
Serial.printf("📞 [handleReconnect] manualConfigActive: %d, scanInProgress: %d\n",
|
||||
manualConfigActive, scanInProgress);
|
||||
|
||||
// 手动配置模式下暂停自动重连
|
||||
if (manualConfigActive) {
|
||||
Serial.println("⏸️ [handleReconnect] 手动配置模式,跳过重连");
|
||||
return;
|
||||
}
|
||||
|
||||
// 扫描进行中,暂停重连
|
||||
if (scanInProgress) {
|
||||
Serial.println("⏸️ [handleReconnect] 扫描进行中,跳过重连");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否有已保存的WiFi网络
|
||||
if (savedNetworkCount == 0) {
|
||||
Serial.println("⚠️ [handleReconnect] 没有已保存的WiFi网络,跳过重连");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查当前是否已连接
|
||||
if (currentState == WIFI_CONNECTED) {
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.println("✅ [handleReconnect] WiFi已连接,无需重连");
|
||||
return;
|
||||
}
|
||||
// 连接已断开
|
||||
if (xSemaphoreTake(stateMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
|
||||
currentState = WIFI_DISCONNECTED;
|
||||
setNetworkStatus(NET_DISCONNECTED);
|
||||
Serial.println("⚠️ WiFi连接断开");
|
||||
|
||||
// 扫描并发送WiFi列表到蓝牙
|
||||
scanAndSendResults();
|
||||
xSemaphoreGive(stateMutex);
|
||||
}
|
||||
Serial.println("⚠️ [handleReconnect] WiFi连接断开,开始重连");
|
||||
}
|
||||
|
||||
// 处理重连逻辑
|
||||
if (currentState == WIFI_DISCONNECTED) {
|
||||
unsigned long currentTime = millis();
|
||||
Serial.println("🔄 [handleReconnect] 开始快速重连...");
|
||||
|
||||
// 按照设定的间隔尝试重连
|
||||
if (currentTime - lastReconnectAttempt >= WIFI_RECONNECT_INTERVAL) {
|
||||
lastReconnectAttempt = currentTime;
|
||||
// 直接尝试重连已保存的网络(不扫描,更快速)
|
||||
for (int i = 0; i < savedNetworkCount; i++) {
|
||||
Serial.printf("🔄 [handleReconnect] 尝试重连: %s\n", savedNetworks[i].ssid);
|
||||
|
||||
// 使用简化的重连方式
|
||||
WiFi.mode(WIFI_STA);
|
||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||
WiFi.begin(savedNetworks[i].ssid, savedNetworks[i].password);
|
||||
|
||||
// 等待连接
|
||||
unsigned long startTime = millis();
|
||||
while (WiFi.status() != WL_CONNECTED && (millis() - startTime) < 8000) {
|
||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||
|
||||
// 检查是否被中断
|
||||
if (manualConfigActive) {
|
||||
Serial.println("🔧 [handleReconnect] 被蓝牙配网中断");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.printf("✅ [handleReconnect] 重连成功: %s\n", savedNetworks[i].ssid);
|
||||
Serial.printf("🌐 IP地址: %s\n", WiFi.localIP().toString().c_str());
|
||||
Serial.printf("📶 信号强度: %d dBm\n", WiFi.RSSI());
|
||||
|
||||
currentState = WIFI_CONNECTED;
|
||||
setNetworkStatus(NET_CONNECTED);
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("❌ [handleReconnect] 重连失败: %s (状态:%d)\n",
|
||||
savedNetworks[i].ssid, WiFi.status());
|
||||
WiFi.disconnect(false); // false = 不关闭radio
|
||||
vTaskDelay(200 / portTICK_PERIOD_MS);
|
||||
}
|
||||
|
||||
// 所有网络都尝试失败
|
||||
Serial.println("❌ [handleReconnect] 所有已保存的网络都重连失败");
|
||||
|
||||
// 尝试完整扫描匹配(作为后备方案)
|
||||
Serial.println("🔍 [handleReconnect] 尝试扫描匹配...");
|
||||
if (scanAndMatchNetworks()) {
|
||||
Serial.println("✅ WiFi重连成功");
|
||||
Serial.println("✅ [handleReconnect] 扫描匹配成功");
|
||||
} else {
|
||||
Serial.println("❌ WiFi重连失败,2秒后重试");
|
||||
Serial.println("❌ [handleReconnect] 扫描匹配失败,稍后重试");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取当前WiFi状态
|
||||
* @return 当前的WiFi管理器状态
|
||||
*/
|
||||
WiFiManagerState WiFiManager::getState() {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -697,16 +944,6 @@ bool WiFiManager::isConnected() {
|
||||
return currentState == WIFI_CONNECTED && WiFi.status() == WL_CONNECTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 断开WiFi连接
|
||||
* 主动断开当前的WiFi连接
|
||||
*/
|
||||
void WiFiManager::disconnect() {
|
||||
WiFi.disconnect(true);
|
||||
currentState = WIFI_DISCONNECTED;
|
||||
setNetworkStatus(NET_DISCONNECTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 添加WiFi配置
|
||||
* 向保存的配置中添加新的WiFi网络
|
||||
@@ -774,9 +1011,152 @@ void WiFiManager::getSavedNetworks() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 更新WiFi管理器状态
|
||||
* 定期调用此函数,处理WiFi重连等状态管理
|
||||
* @brief 重连任务 - 后台持续运行
|
||||
* 优先级低,会被扫描任务抢占
|
||||
* @param parameter WiFiManager实例指针
|
||||
*/
|
||||
void WiFiManager::update() {
|
||||
handleReconnect();
|
||||
void WiFiManager::reconnectTask(void* parameter) {
|
||||
WiFiManager* manager = (WiFiManager*)parameter;
|
||||
|
||||
Serial.println("📡 [重连任务] 启动");
|
||||
Serial.printf("📡 [重连任务] 初始状态: %d, manualConfigActive: %d\n",
|
||||
manager->currentState, manager->manualConfigActive);
|
||||
|
||||
while (true) {
|
||||
// 等待扫描信号量,最多等500ms
|
||||
// 如果收到信号量,说明有扫描请求,暂停重连
|
||||
if (xSemaphoreTake(manager->scanSemaphore, pdMS_TO_TICKS(500)) == pdTRUE) {
|
||||
// 收到扫描请求,进入等待状态
|
||||
Serial.println("🛑 [重连任务] 检测到扫描请求,暂停重连");
|
||||
|
||||
// 等待扫描完成信号(通过scanInProgress标志)
|
||||
int waitCount = 0;
|
||||
while (manager->scanInProgress) {
|
||||
if (waitCount % 10 == 0) {
|
||||
Serial.printf("⏳ [重连任务] 等待扫描完成... (%d秒)\n", waitCount / 10);
|
||||
}
|
||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||
waitCount++;
|
||||
}
|
||||
|
||||
Serial.println("▶️ [重连任务] 扫描结束,恢复重连");
|
||||
Serial.printf("▶️ [重连任务] 当前状态: %d, WiFi状态: %d\n",
|
||||
manager->currentState, WiFi.status());
|
||||
|
||||
// 关键修复:清空信号量,确保下次真正等待新的扫描请求
|
||||
// 因为xSemaphoreGive会使信号量变为可用,需要再Take一次清空
|
||||
xSemaphoreTake(manager->scanSemaphore, 0); // 非阻塞清空
|
||||
Serial.println("✅ [重连任务] 信号量已清空,继续循环");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 没有扫描请求,执行正常重连逻辑
|
||||
if (manager->currentState == WIFI_DISCONNECTED &&
|
||||
!manager->manualConfigActive) {
|
||||
|
||||
unsigned long currentTime = millis();
|
||||
unsigned long timeSinceLastAttempt = currentTime - manager->lastReconnectAttempt;
|
||||
|
||||
// 断开后立即尝试重连(前3次),之后按间隔重连
|
||||
static int quickRetryCount = 0;
|
||||
bool shouldRetry = false;
|
||||
|
||||
if (quickRetryCount < 3) {
|
||||
// 前3次快速重连(每次1秒间隔)
|
||||
shouldRetry = (timeSinceLastAttempt >= 1000);
|
||||
if (shouldRetry) {
|
||||
quickRetryCount++;
|
||||
Serial.printf("🚀 [重连任务] 快速重连 #%d\n", quickRetryCount);
|
||||
}
|
||||
} else {
|
||||
// 之后按正常间隔重连
|
||||
shouldRetry = (timeSinceLastAttempt >= WIFI_RECONNECT_INTERVAL);
|
||||
}
|
||||
|
||||
if (shouldRetry) {
|
||||
manager->lastReconnectAttempt = currentTime;
|
||||
Serial.println("🔄 [重连任务] 尝试重连...");
|
||||
Serial.printf("🔄 [重连任务] 距上次尝试: %lu ms, WiFi状态: %d\n",
|
||||
timeSinceLastAttempt, WiFi.status());
|
||||
manager->handleReconnect();
|
||||
|
||||
// 如果重连成功,重置快速重连计数
|
||||
if (manager->currentState == WIFI_CONNECTED) {
|
||||
quickRetryCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查实际WiFi状态
|
||||
if (manager->currentState == WIFI_CONNECTED && WiFi.status() != WL_CONNECTED) {
|
||||
if (xSemaphoreTake(manager->stateMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
|
||||
manager->currentState = WIFI_DISCONNECTED;
|
||||
setNetworkStatus(NET_DISCONNECTED);
|
||||
xSemaphoreGive(manager->stateMutex);
|
||||
Serial.println("⚠️ [重连任务] 检测到连接断开");
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 启动扫描 - 抢占式
|
||||
* 通过信号量通知重连任务暂停,然后执行扫描
|
||||
* @param timeoutMs 扫描超时时间(毫秒)
|
||||
* @return 是否成功启动扫描
|
||||
*/
|
||||
bool WiFiManager::startScan(uint32_t timeoutMs) {
|
||||
Serial.println("🔔 [startScan] 进入扫描启动函数");
|
||||
Serial.printf("🔔 [startScan] scanInProgress: %d, currentState: %d\n",
|
||||
scanInProgress, currentState);
|
||||
|
||||
// 检查是否已在扫描中
|
||||
if (scanInProgress) {
|
||||
Serial.println("⏳ [startScan] 扫描已在进行中");
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.println("🔔 [startScan] 发送扫描请求信号...");
|
||||
|
||||
// 设置扫描标志(重连任务会检测这个标志)
|
||||
scanInProgress = true;
|
||||
Serial.println("✅ [startScan] scanInProgress 已设置为 true");
|
||||
|
||||
// 释放信号量,通知重连任务暂停
|
||||
xSemaphoreGive(scanSemaphore);
|
||||
Serial.println("✅ [startScan] 信号量已释放");
|
||||
|
||||
// 短暂延迟,确保重连任务收到信号并暂停
|
||||
vTaskDelay(200 / portTICK_PERIOD_MS);
|
||||
|
||||
// 获取状态锁,修改状态
|
||||
if (xSemaphoreTake(stateMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
|
||||
currentState = WIFI_SCANNING;
|
||||
xSemaphoreGive(stateMutex);
|
||||
Serial.println("✅ [startScan] 状态已设置为 WIFI_SCANNING");
|
||||
}
|
||||
|
||||
Serial.println("🔍 [startScan] 重连任务已暂停,开始扫描...");
|
||||
|
||||
// 执行实际扫描(此时重连任务已暂停)
|
||||
scanAndSendResults();
|
||||
|
||||
// 扫描完成,清除标志,恢复重连任务
|
||||
scanInProgress = false;
|
||||
Serial.println("✅ [startScan] scanInProgress 已设置为 false");
|
||||
|
||||
// 恢复状态
|
||||
if (xSemaphoreTake(stateMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
|
||||
if (currentState == WIFI_SCANNING) {
|
||||
currentState = (WiFi.status() == WL_CONNECTED) ? WIFI_CONNECTED : WIFI_DISCONNECTED;
|
||||
}
|
||||
xSemaphoreGive(stateMutex);
|
||||
Serial.printf("✅ [startScan] 状态已恢复为: %d, WiFi状态: %d\n",
|
||||
currentState, WiFi.status());
|
||||
}
|
||||
|
||||
Serial.println("✅ [startScan] 扫描完成,重连任务已恢复");
|
||||
return true;
|
||||
}
|
||||
@@ -5,6 +5,9 @@
|
||||
#include <WiFi.h>
|
||||
#include <Preferences.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <FreeRTOS.h>
|
||||
#include <semphr.h>
|
||||
#include <task.h>
|
||||
|
||||
/**
|
||||
* @brief 最大WiFi网络配置数量
|
||||
@@ -24,14 +27,14 @@
|
||||
* 定义WiFi连接的最大等待时间,超过此时间认为连接失败
|
||||
* 单位:毫秒
|
||||
*/
|
||||
#define WIFI_CONNECT_TIMEOUT 3000
|
||||
#define WIFI_CONNECT_TIMEOUT 10000
|
||||
|
||||
/**
|
||||
* @brief WiFi重连间隔时间
|
||||
* 定义WiFi断开后,尝试重新连接的时间间隔
|
||||
* 单位:毫秒
|
||||
*/
|
||||
#define WIFI_RECONNECT_INTERVAL 2000
|
||||
#define WIFI_RECONNECT_INTERVAL 3000
|
||||
|
||||
/**
|
||||
* @brief WiFi网络信息结构
|
||||
@@ -90,33 +93,46 @@ private:
|
||||
WiFiManagerState currentState; // 当前WiFi管理器状态
|
||||
unsigned long lastReconnectAttempt; // 上次尝试重连的时间
|
||||
bool isScanning; // 是否正在扫描
|
||||
bool manualConfigActive; // 手动配置标志位(setWiFiConfig 配置时为True,暂停重连)
|
||||
bool lastScanHadAvailableNetwork; // 上次扫描是否有可用的已保存网络
|
||||
|
||||
// FreeRTOS资源
|
||||
TaskHandle_t reconnectTaskHandle; // 重连任务句柄
|
||||
SemaphoreHandle_t scanSemaphore; // 扫描信号量(二值)
|
||||
SemaphoreHandle_t stateMutex; // 状态互斥锁
|
||||
volatile bool scanInProgress; // 扫描进行标志
|
||||
|
||||
// 重连任务函数(静态)
|
||||
static void reconnectTask(void* parameter);
|
||||
|
||||
bool scanAndMatchNetworks(); // 扫描并匹配网络
|
||||
bool connectToNetwork(const char* ssid, const char* password); // 连接到指定网络
|
||||
void sendScanResultsViaBLE(); // 发送扫描结果到BLE
|
||||
bool saveWiFiConfig(const char* ssid, const char* password); // 保存WiFi配置
|
||||
bool loadWiFiConfigs(); // 加载WiFi配置
|
||||
|
||||
public:
|
||||
|
||||
|
||||
WiFiManager(); // 构造函数
|
||||
void begin(); // 初始化WiFi管理器
|
||||
|
||||
bool initializeWiFi(); // 初始化WiFi连接
|
||||
bool startConfiguration(); // 开始配网模式
|
||||
bool handleConfigurationData(const char* ssid, const char* password); // 处理配网数据
|
||||
void handleReconnect(); // 处理重连
|
||||
|
||||
WiFiManagerState getState(); // 获取当前状态
|
||||
bool isConnected(); // 检查是否已连接
|
||||
void disconnect(); // 断开连接
|
||||
|
||||
// 扫描接口(会阻塞重连任务)
|
||||
bool startScan(uint32_t timeoutMs = 30000);
|
||||
void scanAndSendResults(); // 扫描并发送结果
|
||||
bool addWiFiConfig(const char* ssid, const char* password); // 添加WiFi配置
|
||||
void clearAllConfigs(); // 清除所有配置
|
||||
int getSavedNetworkCount(); // 获取已保存的网络数量
|
||||
void getSavedNetworks(); // 获取已保存的WiFi网络列表
|
||||
|
||||
void update(); // 更新WiFi管理器状态
|
||||
// 简化update(不再处理重连,重连在独立任务)
|
||||
void update() {} // 更新WiFi管理器状态
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
214
睡眠方案.txt
Normal file
214
睡眠方案.txt
Normal file
@@ -0,0 +1,214 @@
|
||||
🧠 一、系统总体架构(通用版)
|
||||
雷达输入(两种类型)
|
||||
↓
|
||||
统一存在检测层(Presence Layer)
|
||||
↓
|
||||
生理信号层(HR / RR / HRV / Movement)
|
||||
↓
|
||||
睡眠状态机(核心)
|
||||
↓
|
||||
睡眠分期
|
||||
↓
|
||||
事件检测(入睡 / 醒来 / 离床)
|
||||
↓
|
||||
统计 + 评分
|
||||
🧩 二、雷达适配层设计(重点)
|
||||
|
||||
你要做的是:统一接口,底层适配
|
||||
|
||||
✅ 统一输出结构
|
||||
typedef struct {
|
||||
bool isPresent; // 是否有人
|
||||
float distance; // 距离(无距离雷达填-1)
|
||||
float confidence; // 存在置信度 0~1
|
||||
float motionEnergy; // 微动能量(关键!)
|
||||
} PresenceData;
|
||||
🟢 情况1:支持距离雷达(如FMCW)
|
||||
isPresent = (distance > 20cm && distance < 100cm)
|
||||
&& (energy > threshold);
|
||||
|
||||
confidence = energy归一化;
|
||||
|
||||
👉 优势:
|
||||
|
||||
可以判断是否在床上
|
||||
可以做“离床”精准检测
|
||||
🔵 情况2:不支持距离(如存在检测雷达)
|
||||
|
||||
👉 用“微动 + 呼吸”判断
|
||||
|
||||
isPresent = (motionEnergy > lowThreshold)
|
||||
|| (检测到呼吸信号);
|
||||
|
||||
confidence = motionEnergy归一化;
|
||||
⚠️ 关键优化(必须做)
|
||||
|
||||
👉 防误判(静止误判无人):
|
||||
|
||||
if (HR 或 RR 有效)
|
||||
isPresent = true;
|
||||
🚶 三、有人 / 离床 / 无人逻辑(统一)
|
||||
✅ 状态定义
|
||||
NO_PERSON
|
||||
IN_BED
|
||||
OUT_OF_BED
|
||||
🧾 通用逻辑
|
||||
if (!presence.isPresent) {
|
||||
noPersonTimer += dt;
|
||||
|
||||
if (noPersonTimer > 10min) {
|
||||
state = NO_PERSON;
|
||||
endSleepSession();
|
||||
} else {
|
||||
state = OUT_OF_BED;
|
||||
awakeTime += dt;
|
||||
}
|
||||
|
||||
} else {
|
||||
noPersonTimer = 0;
|
||||
|
||||
// 有距离版本
|
||||
if (presence.distance > 80cm)
|
||||
state = OUT_OF_BED;
|
||||
else
|
||||
state = IN_BED;
|
||||
}
|
||||
😴 四、入睡判断(统一方案)
|
||||
✅ 输入
|
||||
HR(心率)
|
||||
RR(呼吸)
|
||||
HRV
|
||||
Movement(0~100)
|
||||
📐 Sleepiness Score
|
||||
S = 0.35*(1 - HR_norm)
|
||||
+ 0.25*(HRV_norm)
|
||||
+ 0.20*(1 - RR_var)
|
||||
+ 0.20*(1 - Movement_norm)
|
||||
🧾 判定
|
||||
if (S > 0.6 持续 5~10分钟)
|
||||
→ 入睡
|
||||
⚠️ 雷达适配补充
|
||||
|
||||
👉 无距离雷达必须加:
|
||||
|
||||
if (!presence.isPresent)
|
||||
不允许入睡
|
||||
🌙 五、睡眠分期(核心)
|
||||
🧠 特征统一归一化
|
||||
HR_norm = (HR - baselineHR)/20
|
||||
RR_norm = (RR - baselineRR)/4
|
||||
HRV_norm = HRV / 50
|
||||
Move_norm= Movement / 100
|
||||
🟢 深睡(Deep Sleep)
|
||||
DeepScore =
|
||||
0.4*(1 - HR_norm)
|
||||
+ 0.3*(HRV_norm)
|
||||
+ 0.2*(1 - Move_norm)
|
||||
+ 0.1*(RR稳定)
|
||||
|
||||
条件加强版:
|
||||
|
||||
Movement < 10
|
||||
HRV > baseline
|
||||
🔵 浅睡(Light Sleep)
|
||||
LightScore =
|
||||
0.3*(HR适中)
|
||||
+ 0.3*(HRV中)
|
||||
+ 0.2*(Move 10~40)
|
||||
+ 0.2*(RR稳定)
|
||||
🔴 清醒(Awake)
|
||||
AwakeScore =
|
||||
0.5*(Move_norm)
|
||||
+ 0.3*(HR_norm)
|
||||
+ 0.2*(RR波动)
|
||||
🧾 分类
|
||||
max(Deep, Light, Awake)
|
||||
⏰ 六、醒来判断
|
||||
✅ 强规则(推荐)
|
||||
if (Movement > 50 持续 2分钟)
|
||||
→ 醒来
|
||||
✅ 融合规则
|
||||
if (HR ↑ && RR ↑ && Movement ↑)
|
||||
→ 醒来
|
||||
🚪 七、离床判断(通用版)
|
||||
🟢 有距离雷达
|
||||
if (distance > 80cm)
|
||||
OUT_OF_BED
|
||||
🔵 无距离雷达
|
||||
if (!presence.isPresent 持续 > 30秒)
|
||||
OUT_OF_BED
|
||||
🧾 最终结束
|
||||
if (无人 > 10分钟)
|
||||
→ 结束睡眠
|
||||
📊 八、睡眠统计
|
||||
|
||||
记录:
|
||||
|
||||
totalSleepTime
|
||||
deepSleepTime
|
||||
lightSleepTime
|
||||
awakeTime
|
||||
outOfBedTime
|
||||
sleepLatency(入睡时间)
|
||||
wakeCount
|
||||
⭐ 九、睡眠评分系统
|
||||
🎯 总分100
|
||||
1️⃣ 时长(30)
|
||||
7~9小时 → 满分
|
||||
2️⃣ 深睡比例(25)
|
||||
Deep / Total > 20%
|
||||
3️⃣ 连续性(20)
|
||||
醒来少 → 高分
|
||||
4️⃣ 生理质量(15)
|
||||
HRV高 + HR稳定
|
||||
5️⃣ 入睡速度(10)
|
||||
<20分钟
|
||||
📐 总公式
|
||||
Score =
|
||||
0.3*duration +
|
||||
0.25*deep +
|
||||
0.2*continuity +
|
||||
0.15*physiology +
|
||||
0.1*latency
|
||||
🔁 十、完整状态机(统一)
|
||||
NO_PERSON
|
||||
↓
|
||||
IN_BED
|
||||
↓
|
||||
AWAKE
|
||||
↓ (满足入睡)
|
||||
LIGHT_SLEEP
|
||||
↓
|
||||
DEEP_SLEEP
|
||||
↑↓
|
||||
LIGHT_SLEEP
|
||||
↓
|
||||
AWAKE
|
||||
↓
|
||||
OUT_OF_BED
|
||||
↓(10min)
|
||||
END
|
||||
⚙️ 十一、ESP32建议架构(实战)
|
||||
🧵 任务划分(FreeRTOS)
|
||||
Task1:雷达采集(Presence)
|
||||
Task2:HR/RR/HRV计算
|
||||
Task3:睡眠状态机(核心)
|
||||
Task4:UI / MQTT上传
|
||||
⏱ 更新周期
|
||||
Presence:10~20Hz
|
||||
HR/RR:1Hz
|
||||
睡眠分析:1Hz
|
||||
🚀 十二、关键工程优化(你必须做)
|
||||
1️⃣ 防误判“无人”
|
||||
if (HR有效 || RR有效)
|
||||
强制 presence = true
|
||||
2️⃣ 防抖动(超级关键)
|
||||
状态必须持续 N 秒才切换
|
||||
3️⃣ 数据无效保护
|
||||
if (!HRV valid)
|
||||
不参与深睡判断
|
||||
✅ 总结(最核心一句话)
|
||||
|
||||
👉 这套方案本质是:
|
||||
|
||||
“雷达判断人 → 生理判断睡 → 时间保证稳定 → 状态机做最终决策”
|
||||
Reference in New Issue
Block a user