feat: 按钮去抖+配网界面美化+水浸离线+LED独立任务+报警全拓展(人身优先)+有人才检测

This commit is contained in:
2026-07-31 15:58:09 +08:00
parent 49e3abbb34
commit 69e927c5f0
16 changed files with 1352 additions and 549 deletions
+208 -82
View File
@@ -1,13 +1,43 @@
/**
* @file alarm_manager.cpp
* @brief 报警管理器 - 双模式实现
* @brief 报警管理器 - 多类型分优先级实现
*
* 本地: 跌倒/水浸/心率异常/呼吸异常 → 立即蜂鸣器+LED
* 服务端: cmd/alarm, cmd/alarm/restore → 执行联动 + 回复响应
* 蜂鸣模式 (人身安全 > 环境 > 设备):
* 跌倒: 连速哔 (60ms/60ms) — 最急 ★★★★★
* 心率/呼吸: 5连急哔 (80ms/80ms) — 次急 ★★★★★
* 水浸: 3连哔 (120ms/100ms) — 中等 ★★★
* 温湿度: 2声长停 (200ms/400ms) — 温和 ★★
* 传感器: 单声长哔 (300ms/3s) — 提示 ★
*/
#include "alarm_manager.h"
// ============================================================
// 优先级定义 — 人身安全 > 环境安全 > 设备故障
// ============================================================
uint8_t AlarmManager::_priority(AlarmType t) {
switch (t) {
// ★★★★★ 人身安全 — 直接威胁生命
case ALARM_FALL: return 0;
case ALARM_HEART_ABNORMAL: return 1;
case ALARM_BREATH_ABNORMAL: return 1;
// ★★★ 环境安全 — 潜在危险
case ALARM_WATER_LEAK: return 2;
// ★★ 环境舒适 — 非紧急
case ALARM_TEMP_ABNORMAL: return 3;
case ALARM_HUMI_ABNORMAL: return 3;
// ★ 设备状态 — 仅提示
case ALARM_SENSOR_FAULT: return 4;
case ALARM_SERVER: return 5;
default: return 9;
}
}
// ============================================================
// 构造 & 析构
// ============================================================
AlarmManager::AlarmManager()
: _buzzerOn(false)
, _ledOn(false)
@@ -15,10 +45,16 @@ AlarmManager::AlarmManager()
, _buzzerToggleTime(0)
, _buzzerBeepState(false)
, _beepCount(0)
, _beepsPerCycle(3)
, _beepOnMs(150)
, _beepOffMs(100)
, _cyclePauseMs(600)
, _activeCount(0)
, _lastLEDToggle(0)
, _alarmCb(nullptr)
{
for (int i = 0; i < 4; i++) _activeAlarms[i] = ALARM_NONE;
for (uint8_t i = 0; i < MAX_ACTIVE; i++) _activeAlarms[i] = ALARM_NONE;
memset(_activeDetail, 0, sizeof(_activeDetail));
}
AlarmManager::~AlarmManager() {}
@@ -30,16 +66,12 @@ AlarmManager::~AlarmManager() {}
bool AlarmManager::begin() {
ledcSetup(BUZZER_PWM_CHANNEL, BUZZER_PWM_FREQ, BUZZER_PWM_RESOLUTION);
ledcAttachPin(BUZZER_PIN, BUZZER_PWM_CHANNEL);
ledcWrite(BUZZER_PWM_CHANNEL, 0); // 关
ledcWrite(BUZZER_PWM_CHANNEL, 0);
pinMode(LED_ALARM_PIN, OUTPUT);
digitalWrite(LED_ALARM_PIN, LOW);
_buzzerToggleTime = 0;
_buzzerBeepState = false;
_beepCount = 0;
Serial.println(F("[告警] ✅ 双模式报警 (本地+服务端)"));
Serial.println(F("[告警] ✅ 多类型分优先级报警就绪 (7种类型)"));
return true;
}
@@ -53,33 +85,129 @@ void AlarmManager::loop() {
}
// ============================================================
// 本地报警触发 (离线可用)
// 报警名称映射
// ============================================================
void AlarmManager::triggerLocal(AlarmType type) {
const char* AlarmManager::getAlarmName(AlarmType type) const {
switch (type) {
case ALARM_FALL: return "跌倒";
case ALARM_WATER_LEAK: return "水浸";
case ALARM_HEART_ABNORMAL: return "心率异常";
case ALARM_BREATH_ABNORMAL: return "呼吸异常";
case ALARM_TEMP_ABNORMAL: return "温度异常";
case ALARM_HUMI_ABNORMAL: return "湿度异常";
case ALARM_SENSOR_FAULT: return "传感器故障";
case ALARM_SERVER: return "云端指令";
default: return "未知";
}
}
// ============================================================
// 最高优先级报警
// ============================================================
AlarmManager::AlarmType AlarmManager::getHighestAlarm() const {
if (_activeCount == 0) return ALARM_NONE;
AlarmType best = ALARM_NONE;
uint8_t bestP = 99;
for (uint8_t i = 0; i < _activeCount; i++) {
uint8_t p = _priority(_activeAlarms[i]);
if (p < bestP) { bestP = p; best = _activeAlarms[i]; }
}
return best;
}
const char* AlarmManager::getActiveAlarmStr() const {
AlarmType t = getHighestAlarm();
if (t == ALARM_NONE) return "";
return getAlarmName(t);
}
// ============================================================
// 蜂鸣参数表 (按最高优先级报警驱动)
// ============================================================
uint32_t AlarmManager::_buzzerIntervalFor(AlarmType type) {
switch (type) {
case ALARM_FALL: return 200; // LED 快闪
case ALARM_WATER_LEAK: return 300;
case ALARM_HEART_ABNORMAL: return 500;
case ALARM_BREATH_ABNORMAL: return 500;
case ALARM_TEMP_ABNORMAL: return 800;
case ALARM_HUMI_ABNORMAL: return 800;
case ALARM_SENSOR_FAULT: return 1000;
default: return 500;
}
}
// ============================================================
// 更新蜂鸣参数 (根据当前最高优先级报警)
// ============================================================
static void _calcBuzzerParams(AlarmManager::AlarmType t,
uint8_t& beeps, uint32_t& onMs,
uint32_t& offMs, uint32_t& pauseMs) {
switch (t) {
// 人身安全 — 连速急哔 (最刺耳)
case AlarmManager::ALARM_FALL:
beeps = 0; onMs = 60; offMs = 60; pauseMs = 0; // 跌倒: 连速哔
break;
case AlarmManager::ALARM_HEART_ABNORMAL:
case AlarmManager::ALARM_BREATH_ABNORMAL:
beeps = 5; onMs = 80; offMs = 80; pauseMs = 400; // 心率/呼吸: 5连急哔
break;
// 环境安全 — 次急
case AlarmManager::ALARM_WATER_LEAK:
beeps = 3; onMs = 120; offMs = 100; pauseMs = 500; // 水浸: 3连哔
break;
// 环境舒适 — 温和
case AlarmManager::ALARM_TEMP_ABNORMAL:
case AlarmManager::ALARM_HUMI_ABNORMAL:
beeps = 2; onMs = 200; offMs = 400; pauseMs = 1200; // 温湿度: 2声长停
break;
// 设备状态 — 仅提示
case AlarmManager::ALARM_SENSOR_FAULT:
beeps = 1; onMs = 300; offMs = 3000; pauseMs = 0; // 传感器: 单声提醒
break;
default:
beeps = 0; onMs = 500; offMs = 500; pauseMs = 0; // 持续
break;
}
}
// ============================================================
// 本地报警触发
// ============================================================
void AlarmManager::triggerLocal(AlarmType type, const char* detail) {
// 去重
for (uint8_t i = 0; i < _activeCount; i++) {
if (_activeAlarms[i] == type) return;
}
if (_activeCount < 4) {
if (_activeCount < MAX_ACTIVE) {
_activeAlarms[_activeCount++] = type;
}
const char* name = "";
switch (type) {
case ALARM_FALL: name = "跌倒"; break;
case ALARM_WATER_LEAK: name = "水浸"; break;
case ALARM_HEART_ABNORMAL: name = "心率异常"; break;
case ALARM_BREATH_ABNORMAL: name = "呼吸异常"; break;
default: name = "未知"; break;
const char* name = getAlarmName(type);
Serial.printf("[告警] 🚨 触发: %s (活动:%d)", name, _activeCount);
if (detail && detail[0]) Serial.printf(" | %s", detail);
Serial.println();
// 更新蜂鸣参数 (按最高优先级)
AlarmType highest = getHighestAlarm();
_calcBuzzerParams(highest, _beepsPerCycle, _beepOnMs, _beepOffMs, _cyclePauseMs);
// 保存详情
if (detail) {
strncpy(_activeDetail, detail, sizeof(_activeDetail) - 1);
}
Serial.printf("[告警] 🚨 本地报警触发: %s (活动报警:%d)\n", name, _activeCount);
// 立即开启蜂鸣器和 LED
_setBuzzer(true);
_setLED(true);
// 通知外部
if (_alarmCb) _alarmCb((uint8_t)type, true, detail ? detail : name);
}
void AlarmManager::clearLocal(AlarmType type) {
@@ -94,21 +222,29 @@ void AlarmManager::clearLocal(AlarmType type) {
}
}
Serial.printf("[告警] 本地报警清除, 剩余活动:%d\n", _activeCount);
const char* name = getAlarmName(type);
Serial.printf("[告警] ✅ 清除: %s (剩余:%d)\n", name, _activeCount);
// 没有活动报警时关闭
if (_activeCount == 0) {
_setBuzzer(false);
_setLED(false);
_activeDetail[0] = '\0';
} else {
// 降级到次高优先级报警的蜂鸣参数
AlarmType highest = getHighestAlarm();
_calcBuzzerParams(highest, _beepsPerCycle, _beepOnMs, _beepOffMs, _cyclePauseMs);
}
if (_alarmCb) _alarmCb((uint8_t)type, false, name);
}
void AlarmManager::clearAllLocal() {
_activeCount = 0;
for (int i = 0; i < 4; i++) _activeAlarms[i] = ALARM_NONE;
for (uint8_t i = 0; i < MAX_ACTIVE; i++) _activeAlarms[i] = ALARM_NONE;
_activeDetail[0] = '\0';
_setBuzzer(false);
_setLED(false);
Serial.println(F("[告警] 清除所有本地报警"));
Serial.println(F("[告警] 清除所有报警"));
}
// ============================================================
@@ -116,16 +252,15 @@ void AlarmManager::clearAllLocal() {
// ============================================================
void AlarmManager::executeAlarm(const AlarmCommand& cmd) {
Serial.printf("[告警] ☁️ 服务端告警: %s (等级=%s, 蜂鸣器=%d)\n",
Serial.printf("[告警] ☁️ 服务端: %s (等级=%s 蜂鸣=%d)\n",
cmd.alarmType, cmd.alarmLevel, cmd.buzzerOn);
// 映射 alarmType 到本地类型
AlarmType localType = ALARM_SERVER;
if (strcmp(cmd.alarmType, "fall") == 0) localType = ALARM_FALL;
else if (strcmp(cmd.alarmType, "water_leak") == 0) localType = ALARM_WATER_LEAK;
if (cmd.buzzerOn) {
triggerLocal(localType);
triggerLocal(localType, cmd.alarmDesc);
if (cmd.buzzerDuration > 0) {
_buzzerOffTime = millis() + (uint32_t)cmd.buzzerDuration * 1000;
} else {
@@ -138,12 +273,8 @@ void AlarmManager::executeAlarm(const AlarmCommand& cmd) {
}
}
// ============================================================
// 服务端恢复指令 (§5.2)
// ============================================================
void AlarmManager::executeRestore(const AlarmCommand& cmd) {
Serial.printf("[告警] ☁️ 服务端恢复: %s\n", cmd.alarmType);
Serial.printf("[告警] ☁️ 恢复: %s\n", cmd.alarmType);
AlarmType localType = ALARM_SERVER;
if (strcmp(cmd.alarmType, "fall") == 0) localType = ALARM_FALL;
@@ -151,12 +282,8 @@ void AlarmManager::executeRestore(const AlarmCommand& cmd) {
clearLocal(localType);
if (!cmd.buzzerOn) {
_setBuzzer(false);
}
if (!cmd.nightLight) {
_setLED(false);
}
if (!cmd.buzzerOn) _setBuzzer(false);
if (!cmd.nightLight) _setLED(false);
}
// ============================================================
@@ -164,27 +291,11 @@ void AlarmManager::executeRestore(const AlarmCommand& cmd) {
// ============================================================
void AlarmManager::buzzerTest(uint32_t durationSec) {
Serial.printf("[告警] 🔔 蜂鸣器测试 %d秒\n", durationSec);
Serial.printf("[告警] 🔔 蜂鸣器测试 %ds\n", durationSec);
_setBuzzer(true);
_buzzerOffTime = millis() + durationSec * 1000;
}
// ============================================================
// 状态查询
// ============================================================
const char* AlarmManager::getActiveAlarmType() const {
if (_activeCount == 0) return "";
switch (_activeAlarms[0]) {
case ALARM_FALL: return "fall";
case ALARM_WATER_LEAK: return "water_leak";
case ALARM_HEART_ABNORMAL: return "heart_abnormal";
case ALARM_BREATH_ABNORMAL: return "breath_abnormal";
case ALARM_SERVER: return "server";
default: return "";
}
}
// ============================================================
// 内部控制
// ============================================================
@@ -195,7 +306,7 @@ void AlarmManager::_setBuzzer(bool on) {
_buzzerToggleTime = 0;
_beepCount = 0;
if (on) {
_buzzerOffTime = 0; // 本地报警=无限哔哔
_buzzerOffTime = 0;
} else {
ledcWrite(BUZZER_PWM_CHANNEL, 0);
_buzzerOffTime = 0;
@@ -207,57 +318,72 @@ void AlarmManager::_setLED(bool on) {
if (!on) digitalWrite(LED_ALARM_PIN, LOW);
}
// ============================================================
// 蜂鸣器更新 (优先级驱动模式)
// ============================================================
void AlarmManager::_updateBuzzer() {
// 定时关闭检查
if (_buzzerOn && _buzzerOffTime > 0 && millis() >= _buzzerOffTime) {
if (!_buzzerOn) return;
// 定时关闭
if (_buzzerOffTime > 0 && millis() >= _buzzerOffTime) {
_buzzerOn = false;
ledcWrite(BUZZER_PWM_CHANNEL, 0);
Serial.println(F("[告警] ⏰ 蜂鸣器定时关闭"));
return;
}
// 本地报警: 三连哔 停 — 循环 (哔哔哔...哔哔哔...)
if (_buzzerOn && _buzzerOffTime == 0) {
uint32_t now = millis();
// 周期: 3声哔 (每声150ms响+100ms停) + 600ms大停顿
uint32_t now = millis();
// 云端定时: 持续响
if (_buzzerOffTime > 0) {
ledcWrite(BUZZER_PWM_CHANNEL, 128);
return;
}
// 本地报警: 按优先级模式 — 哔声/停顿交替
if (_beepsPerCycle == 0) {
// 连速哔 (跌倒): 简单翻转
if (now - _buzzerToggleTime >= (_buzzerBeepState ? _beepOnMs : _beepOffMs)) {
_buzzerToggleTime = now;
_buzzerBeepState = !_buzzerBeepState;
ledcWrite(BUZZER_PWM_CHANNEL, _buzzerBeepState ? 128 : 0);
}
} else {
// 多声一组: N声哔 + 长停顿
if (!_buzzerBeepState) {
// 在停顿中,检查是否该下一声
uint32_t pause = (_beepCount >= 3) ? 600 : 100;
// 停顿中
uint32_t pause = (_beepCount >= _beepsPerCycle) ? _cyclePauseMs : _beepOffMs;
if (now - _buzzerToggleTime >= pause) {
_buzzerToggleTime = now;
if (_beepCount >= 3) _beepCount = 0; // 重置周期
if (_beepCount >= _beepsPerCycle) _beepCount = 0;
_buzzerBeepState = true;
ledcWrite(BUZZER_PWM_CHANNEL, 128);
_beepCount++;
}
} else {
// 在响中,150ms后停下
if (now - _buzzerToggleTime >= 150) {
// 响声中
if (now - _buzzerToggleTime >= _beepOnMs) {
_buzzerToggleTime = now;
_buzzerBeepState = false;
ledcWrite(BUZZER_PWM_CHANNEL, 0);
}
}
}
// 云端定时: 持续响
else if (_buzzerOn && _buzzerOffTime > 0) {
ledcWrite(BUZZER_PWM_CHANNEL, 128);
}
}
// ============================================================
// LED 更新 (按最高优先级决定闪烁频率)
// ============================================================
void AlarmManager::_updateLED() {
if (!_ledOn) {
digitalWrite(LED_ALARM_PIN, LOW);
return;
}
// 根据告警类型调整闪烁频率
uint32_t interval = 500;
for (uint8_t i = 0; i < _activeCount; i++) {
if (_activeAlarms[i] == ALARM_FALL) {
interval = 200; // 跌倒快闪
break;
}
}
AlarmType highest = getHighestAlarm();
uint32_t interval = _buzzerIntervalFor(highest);
if (millis() - _lastLEDToggle >= interval) {
_lastLEDToggle = millis();
+104 -27
View File
@@ -1,6 +1,12 @@
/**
* @file button_manager.cpp
* @brief 三按钮管理器实现 - 去抖 + 长按/短按
* @brief 三按钮管理器实现 - 双向去抖 + 长按/短按 + 超时自愈
*
* 核心改进 (v2):
* 1. 按下沿和释放沿都经过 BTN_DEBOUNCE_MS 稳定确认
* 2. 长按期间持续检测 (不再等到松手才触发)
* 3. 按下超过 BTN_STUCK_TIMEOUT_MS 自动复位, 防止状态机卡死
* 4. 用 stableState 替代瞬时 raw 驱动状态, 消除噪声误触发
*/
#include "button_manager.h"
@@ -14,9 +20,19 @@ ButtonManager::ButtonManager()
, _cbDismiss(nullptr)
, _cbPage(nullptr)
{
_btnProvision = { BTN_PROVISION_PIN, false, 0, false };
_btnDismiss = { BTN_ALARM_DISMISS_PIN, false, 0, false };
_btnPage = { BTN_PAGE_SWITCH_PIN, false, 0, false };
auto initBtn = [](ButtonState& s, uint8_t pin) {
s.pin = pin;
s.stableState = false;
s.rawState = false;
s.changeTime = 0;
s.pressTime = 0;
s.longTriggered = false;
s.shortPending = false;
};
initBtn(_btnProvision, BTN_PROVISION_PIN);
initBtn(_btnDismiss, BTN_ALARM_DISMISS_PIN);
initBtn(_btnPage, BTN_PAGE_SWITCH_PIN);
}
// ============================================================
@@ -28,9 +44,19 @@ void ButtonManager::begin() {
pinMode(BTN_ALARM_DISMISS_PIN, INPUT_PULLUP);
pinMode(BTN_PAGE_SWITCH_PIN, INPUT_PULLUP);
Serial.println(F("[按钮] ✅ 三按钮初始化完成"));
// 读取初始状态, 避免启动时的虚假边沿
_btnProvision.rawState = _readPin(BTN_PROVISION_PIN);
_btnProvision.stableState = _btnProvision.rawState;
_btnDismiss.rawState = _readPin(BTN_ALARM_DISMISS_PIN);
_btnDismiss.stableState = _btnDismiss.rawState;
_btnPage.rawState = _readPin(BTN_PAGE_SWITCH_PIN);
_btnPage.stableState = _btnPage.rawState;
Serial.println(F("[按钮] ✅ 三按钮初始化完成 (双向去抖+超时自愈)"));
Serial.printf(" 配网: GPIO%d | 解除报警: GPIO%d | 翻页: GPIO%d\n",
BTN_PROVISION_PIN, BTN_ALARM_DISMISS_PIN, BTN_PAGE_SWITCH_PIN);
Serial.printf(" 去抖:%dms | 长按:%dms | 卡死超时:%dms\n",
BTN_DEBOUNCE_MS, BTN_LONG_PRESS_MS, BTN_STUCK_TIMEOUT_MS);
}
// ============================================================
@@ -49,40 +75,91 @@ void ButtonManager::loop() {
}
// ============================================================
// 单按钮状态机
// 单按钮状态机 (基于稳定状态, 非瞬时边沿)
// ============================================================
void ButtonManager::_processButton(ButtonState& btn, bool hasShort, bool hasLong) {
bool raw = _readPin(btn.pin); // LOW = 按下
uint32_t now = millis();
bool raw = _readPin(btn.pin); // true = 按下 (LOW)
if (raw && !btn.pressed) {
// 按下沿
btn.pressed = true;
btn.pressTime = millis();
btn.longTriggered = false;
// ── 第1步: raw 状态变化时启动去抖计时 ──
if (raw != btn.rawState) {
btn.rawState = raw;
btn.changeTime = now;
// 不立即改变 stableState — 等稳定确认
}
else if (!raw && btn.pressed) {
// 松开沿
btn.pressed = false;
uint32_t holdTime = millis() - btn.pressTime;
if (holdTime < BTN_DEBOUNCE_MS) {
return; // 抖动脉冲,忽略
// ── 第2步: 去抖确认 — raw 保持稳定超过 BTN_DEBOUNCE_MS 才生效 ──
bool debounced = (now - btn.changeTime >= BTN_DEBOUNCE_MS);
bool stateChanged = (btn.rawState != btn.stableState);
if (debounced && stateChanged) {
// 状态正式切换
btn.stableState = btn.rawState;
if (btn.stableState) {
// ── 确认按下 ──
btn.pressTime = now;
btn.longTriggered = false;
btn.shortPending = hasShort; // 只有支持短按的按钮才标记待判定
} else {
// ── 确认释放 ──
uint32_t holdTime = now - btn.pressTime;
// 短按判定: 按下时长在去抖和长按阈值之间
if (btn.shortPending && holdTime >= BTN_DEBOUNCE_MS
&& holdTime < BTN_LONG_PRESS_MS && !btn.longTriggered) {
// 根据引脚分发短按回调
if (btn.pin == BTN_ALARM_DISMISS_PIN && _cbDismiss) {
_cbDismiss();
}
if (btn.pin == BTN_PAGE_SWITCH_PIN && _cbPage) {
_cbPage();
}
}
btn.shortPending = false;
btn.longTriggered = false;
}
}
if (hasLong && holdTime >= BTN_LONG_PRESS_MS && !btn.longTriggered) {
// 长按触发
// ── 第3步: 长按检测 (在按下稳定期间持续检查) ──
if (hasLong && btn.stableState && !btn.longTriggered) {
uint32_t holdTime = now - btn.pressTime;
if (holdTime >= BTN_LONG_PRESS_MS) {
btn.longTriggered = true;
if (_cbProvision) _cbProvision();
}
else if (hasShort && holdTime >= BTN_DEBOUNCE_MS && holdTime < BTN_LONG_PRESS_MS) {
// 短按触发
if (btn.pin == BTN_ALARM_DISMISS_PIN && _cbDismiss) _cbDismiss();
if (btn.pin == BTN_PAGE_SWITCH_PIN && _cbPage) _cbPage();
btn.shortPending = false; // 长按已触发, 不再判定短按
if (_cbProvision) {
_cbProvision();
}
}
}
// ── 第4步: 卡死自愈 — 按下超时强制复位 ──
if (btn.stableState && (now - btn.pressTime >= BTN_STUCK_TIMEOUT_MS)) {
Serial.printf("[按钮] ⚠️ GPIO%d 按下超时 %lums, 强制复位\n",
btn.pin, now - btn.pressTime);
_resetButton(btn);
}
}
// ============================================================
// 强制复位
// ============================================================
void ButtonManager::_resetButton(ButtonState& btn) {
btn.stableState = false;
btn.rawState = _readPin(btn.pin); // 重新同步真实硬件状态
btn.changeTime = 0;
btn.pressTime = 0;
btn.longTriggered = false;
btn.shortPending = false;
}
// ============================================================
// 读引脚 (INPUT_PULLUP → LOW = 按下)
// ============================================================
bool ButtonManager::_readPin(uint8_t pin) {
return digitalRead(pin) == LOW; // INPUT_PULLUP: LOW = 按下
return digitalRead(pin) == LOW;
}
+65 -28
View File
@@ -71,37 +71,63 @@ void DisplayManager::nextPage() {
// 主更新入口
// ============================================================
void DisplayManager::update(const TelemetryData& data, bool wifiOk, bool buzzerOn) {
void DisplayManager::update(const TelemetryData& data, bool wifiOk, bool buzzerOn,
bool hasAlarm, const char* alarmName) {
if (!_initialized) return;
// 每 5 秒刷新一次 (防止 OLED 烧屏,也避免 SPI 占用过多)
if (millis() - _lastDrawTime < 5000) return;
// 有报警时每秒刷新 (闪烁提示),否则 5 秒
uint32_t interval = hasAlarm ? 1000 : 5000;
if (millis() - _lastDrawTime < interval) return;
_lastDrawTime = millis();
_u8g2.clearBuffer();
// 活跃报警 — 所有页面顶部闪烁提示
if (hasAlarm && alarmName && alarmName[0]) {
_drawAlarmBanner(alarmName);
}
switch (_pageIndex) {
case 0: _drawPage0(data, wifiOk, buzzerOn); break;
case 1: _drawPage1(data); break;
case 2: _drawPage2(data); break;
case 3: _drawPage3(data, buzzerOn, wifiOk); break;
default: _drawPage0(data, wifiOk, buzzerOn); break;
case 0: _drawPage0(data, wifiOk, buzzerOn, hasAlarm); break;
case 1: _drawPage1(data, hasAlarm); break;
case 2: _drawPage2(data, hasAlarm); break;
case 3: _drawPage3(data, buzzerOn, wifiOk, hasAlarm); break;
default: _drawPage0(data, wifiOk, buzzerOn, hasAlarm); break;
}
_u8g2.sendBuffer();
}
// ============================================================
// 报警横幅 (顶部闪烁)
// ============================================================
void DisplayManager::_drawAlarmBanner(const char* name) {
// 每秒翻转,实现闪烁效果
bool blink = ((millis() / 1000) % 2 == 0);
_u8g2.setFont(u8g2_font_wqy12_t_gb2312);
if (blink) {
_u8g2.drawUTF8(2, 12, "🚨");
_u8g2.drawUTF8(26, 12, name);
} else {
_u8g2.drawUTF8(2, 12, " ");
}
_u8g2.drawHLine(0, 15, 128);
}
// ============================================================
// Page 0: 环境数据
// ============================================================
void DisplayManager::_drawPage0(const TelemetryData& data, bool wifiOk, bool buzzerOn) {
// 标题栏
_u8g2.setFont(u8g2_font_wqy12_t_gb2312);
_u8g2.drawUTF8(2, 12, "🏥 健康监测 V1.0");
// 分隔线
_u8g2.drawHLine(0, 15, 128);
void DisplayManager::_drawPage0(const TelemetryData& data, bool wifiOk,
bool buzzerOn, bool hasAlarm) {
// 标题栏 (报警横幅已占顶部时跳过)
if (!hasAlarm) {
_u8g2.setFont(u8g2_font_wqy12_t_gb2312);
_u8g2.drawUTF8(2, 12, "🏥 健康监测 V1.0");
_u8g2.drawHLine(0, 15, 128);
}
// 温湿度
_u8g2.setFont(u8g2_font_wqy12_t_gb2312);
@@ -144,11 +170,13 @@ void DisplayManager::_drawPage0(const TelemetryData& data, bool wifiOk, bool buz
// Page 1: 生命体征
// ============================================================
void DisplayManager::_drawPage1(const TelemetryData& data) {
void DisplayManager::_drawPage1(const TelemetryData& data, bool hasAlarm) {
// 标题栏
_u8g2.setFont(u8g2_font_wqy12_t_gb2312);
_u8g2.drawUTF8(2, 12, "🫀 生命体征");
_u8g2.drawHLine(0, 15, 128);
if (!hasAlarm) {
_u8g2.setFont(u8g2_font_wqy12_t_gb2312);
_u8g2.drawUTF8(2, 12, "🫀 生命体征");
_u8g2.drawHLine(0, 15, 128);
}
// 心率
_u8g2.drawUTF8(4, 32, "心率:");
@@ -182,10 +210,12 @@ void DisplayManager::_drawPage1(const TelemetryData& data) {
// Page 2: 人体状态
// ============================================================
void DisplayManager::_drawPage2(const TelemetryData& data) {
_u8g2.setFont(u8g2_font_wqy12_t_gb2312);
_u8g2.drawUTF8(2, 12, "人体状态");
_u8g2.drawHLine(0, 15, 128);
void DisplayManager::_drawPage2(const TelemetryData& data, bool hasAlarm) {
if (!hasAlarm) {
_u8g2.setFont(u8g2_font_wqy12_t_gb2312);
_u8g2.drawUTF8(2, 12, "人体状态");
_u8g2.drawHLine(0, 15, 128);
}
_u8g2.drawUTF8(4, 30, "有人:");
_u8g2.drawUTF8(60, 30, data.presence ? "V" : "X");
@@ -210,13 +240,20 @@ void DisplayManager::_drawPage2(const TelemetryData& data) {
// Page 3: 安全信息
// ============================================================
void DisplayManager::_drawPage3(const TelemetryData& data, bool buzzerOn, bool wifiOk) {
_u8g2.setFont(u8g2_font_wqy12_t_gb2312);
_u8g2.drawUTF8(2, 12, "安全信息");
_u8g2.drawHLine(0, 15, 128);
void DisplayManager::_drawPage3(const TelemetryData& data, bool buzzerOn,
bool wifiOk, bool hasAlarm) {
if (!hasAlarm) {
_u8g2.setFont(u8g2_font_wqy12_t_gb2312);
_u8g2.drawUTF8(2, 12, "安全信息");
_u8g2.drawHLine(0, 15, 128);
}
_u8g2.drawUTF8(4, 30, "水浸:");
_u8g2.drawUTF8(60, 30, data.rainDO ? "正常" : "!!报警");
if (!data.rainValid) {
_u8g2.drawUTF8(60, 30, "离线");
} else {
_u8g2.drawUTF8(60, 30, data.rainDO ? "正常" : "!!报警");
}
_u8g2.drawUTF8(4, 44, "蜂鸣器:");
_u8g2.drawUTF8(60, 44, buzzerOn ? "响" : "关");
+135 -38
View File
@@ -36,10 +36,20 @@ uint32_t g_sequence = 1; // 上报序号
uint32_t g_bootTime = 0; // 启动时刻
// 本地报警跟踪 (防重复触发)
bool g_localFallActive = false;
bool g_localWaterActive = false;
bool g_localHeartAbnActive = false;
bool g_localBreathAbnActive = false;
bool g_localFallActive = false;
bool g_localWaterActive = false;
bool g_localHeartAbnActive = false;
bool g_localBreathAbnActive = false;
bool g_localTempAbnActive = false;
bool g_localHumiAbnActive = false;
bool g_localRadarFaultActive = false;
bool g_localDhtFaultActive = false;
// 报警防抖计数器 (连续N次确认才触发)
uint8_t g_tempAbnCnt = 0;
uint8_t g_humiAbnCnt = 0;
uint8_t g_radarTimeoutCnt = 0;
#define ALARM_HYSTERESIS 3 // 连续3次确认才触发
// ============================================================
// 时间同步
@@ -226,53 +236,136 @@ TelemetryData buildTelemetry(const RadarRawData& radar) {
}
// ============================================================
// 本地报警检查 (离线可用)
// 本地报警回调 → MQTT 上报 (连接时)
// ============================================================
void onLocalAlarmChange(uint8_t type, bool triggered, const char* desc) {
if (g_mqtt.isConnected()) {
g_mqtt.publishLocalAlarm(type, triggered, desc);
}
}
// ============================================================
// 本地报警检查 (离线可用) — 人身安全 > 环境 > 设备
// ============================================================
void checkLocalAlarms(const RadarRawData& radar) {
uint32_t now = millis();
// ─── 前置条件: 有人才检测人体生命体征 ───
bool humanPresent = radar.presence && radar.radarValid;
// --- 跌倒检测 (本地紧急报警, 立即触发) ---
if (radar.fall && radar.radarValid && !g_localFallActive) {
// ─── ★★★★★ 人身安全: 跌倒 (立即触发, 无防抖) ───
if (radar.fall && humanPresent && !g_localFallActive) {
g_localFallActive = true;
g_alarm.triggerLocal(AlarmManager::ALARM_FALL);
g_alarm.triggerLocal(AlarmManager::ALARM_FALL, "雷达检测到跌倒");
} else if (!radar.fall && g_localFallActive) {
g_localFallActive = false;
g_alarm.clearLocal(AlarmManager::ALARM_FALL);
}
// --- 水浸检测 ---
// ─── ★★★★★ 人身安全: 心率异常 (有人才检测) ───
if (humanPresent && radar.vitalValid && radar.heartRate > 0) {
bool heartAbn = (radar.heartRate < HEART_RATE_MIN || radar.heartRate > HEART_RATE_MAX);
if (heartAbn && !g_localHeartAbnActive) {
g_localHeartAbnActive = true;
char buf[32];
snprintf(buf, sizeof(buf), "心率异常: %d bpm", radar.heartRate);
g_alarm.triggerLocal(AlarmManager::ALARM_HEART_ABNORMAL, buf);
} else if (!heartAbn && g_localHeartAbnActive) {
g_localHeartAbnActive = false;
g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
}
} else if (!humanPresent && g_localHeartAbnActive) {
// 人走了 — 清除之前的报警
g_localHeartAbnActive = false;
g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
}
// ─── ★★★★★ 人身安全: 呼吸异常 (有人才检测) ───
if (humanPresent && radar.vitalValid && radar.breathRate > 0) {
bool breathAbn = (radar.breathRate < BREATH_RATE_MIN || radar.breathRate > BREATH_RATE_MAX);
if (breathAbn && !g_localBreathAbnActive) {
g_localBreathAbnActive = true;
char buf[32];
snprintf(buf, sizeof(buf), "呼吸异常: %d 次/分", radar.breathRate);
g_alarm.triggerLocal(AlarmManager::ALARM_BREATH_ABNORMAL, buf);
} else if (!breathAbn && g_localBreathAbnActive) {
g_localBreathAbnActive = false;
g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
}
} else if (!humanPresent && g_localBreathAbnActive) {
g_localBreathAbnActive = false;
g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
}
// ─── ★★★ 环境安全: 水浸 ───
bool waterLeak = !g_sensor.getRainDO() && g_sensor.isRainValid();
if (waterLeak && !g_localWaterActive) {
g_localWaterActive = true;
g_alarm.triggerLocal(AlarmManager::ALARM_WATER_LEAK);
g_alarm.triggerLocal(AlarmManager::ALARM_WATER_LEAK, "水浸传感器触发");
} else if (!waterLeak && g_localWaterActive) {
g_localWaterActive = false;
g_alarm.clearLocal(AlarmManager::ALARM_WATER_LEAK);
}
// --- 心率异常 (离线本地判定, 阈值预留后续调整) ---
if (radar.vitalValid && radar.heartRate > 0) {
bool heartAbn = (radar.heartRate < HEART_RATE_MIN || radar.heartRate > HEART_RATE_MAX);
if (heartAbn && !g_localHeartAbnActive) {
g_localHeartAbnActive = true;
g_alarm.triggerLocal(AlarmManager::ALARM_HEART_ABNORMAL);
} else if (!heartAbn && g_localHeartAbnActive) {
g_localHeartAbnActive = false;
g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
// ─── ★★ 环境舒适: 温度异常 (防抖3次) ───
if (g_sensor.isDhtValid()) {
float temp = g_sensor.getTemp();
bool tempAbn = (temp < TEMP_LOW || temp > TEMP_HIGH);
if (tempAbn) {
if (++g_tempAbnCnt >= ALARM_HYSTERESIS && !g_localTempAbnActive) {
g_localTempAbnActive = true;
char buf[32];
snprintf(buf, sizeof(buf), "温度%.1f°C 超出范围", temp);
g_alarm.triggerLocal(AlarmManager::ALARM_TEMP_ABNORMAL, buf);
}
} else {
g_tempAbnCnt = 0;
if (g_localTempAbnActive) {
g_localTempAbnActive = false;
g_alarm.clearLocal(AlarmManager::ALARM_TEMP_ABNORMAL);
}
}
// ─── ★★ 环境舒适: 湿度异常 (防抖3次) ───
float humi = g_sensor.getHumi();
bool humiAbn = (humi < HUMI_LOW || humi > HUMI_HIGH);
if (humiAbn) {
if (++g_humiAbnCnt >= ALARM_HYSTERESIS && !g_localHumiAbnActive) {
g_localHumiAbnActive = true;
char buf[32];
snprintf(buf, sizeof(buf), "湿度%.1f%% 超出范围", humi);
g_alarm.triggerLocal(AlarmManager::ALARM_HUMI_ABNORMAL, buf);
}
} else {
g_humiAbnCnt = 0;
if (g_localHumiAbnActive) {
g_localHumiAbnActive = false;
g_alarm.clearLocal(AlarmManager::ALARM_HUMI_ABNORMAL);
}
}
}
// --- 呼吸异常 ---
if (radar.vitalValid && radar.breathRate > 0) {
bool breathAbn = (radar.breathRate < BREATH_RATE_MIN || radar.breathRate > BREATH_RATE_MAX);
if (breathAbn && !g_localBreathAbnActive) {
g_localBreathAbnActive = true;
g_alarm.triggerLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
} else if (!breathAbn && g_localBreathAbnActive) {
g_localBreathAbnActive = false;
g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
// ─── ★ 设备:雷达超时 (连续5秒无数据) ───
if (g_radar.isTimeout()) {
if (++g_radarTimeoutCnt >= 5 && !g_localRadarFaultActive) {
g_localRadarFaultActive = true;
g_alarm.triggerLocal(AlarmManager::ALARM_SENSOR_FAULT, "雷达超时无数据");
}
} else {
g_radarTimeoutCnt = 0;
if (g_localRadarFaultActive) {
g_localRadarFaultActive = false;
g_alarm.clearLocal(AlarmManager::ALARM_SENSOR_FAULT);
}
}
// ─── ★ 设备:DHT 持续故障 (已由 SensorManager 内部计数) ───
if (g_sensor.isDhtFault() && !g_localDhtFaultActive && millis() > 60000) {
g_localDhtFaultActive = true;
g_alarm.triggerLocal(AlarmManager::ALARM_SENSOR_FAULT, "DHT11温湿度传感器故障");
} else if (!g_sensor.isDhtFault() && g_localDhtFaultActive) {
g_localDhtFaultActive = false;
g_alarm.clearLocal(AlarmManager::ALARM_SENSOR_FAULT);
}
}
@@ -370,6 +463,9 @@ void setup() {
g_mqtt.onControlCommand(handleControlCommand);
g_mqtt.onProvisionCommand(handleProvisionCommand);
// 注册本地报警回调 → MQTT 上报
g_alarm.onAlarmChange(onLocalAlarmChange);
// 连接 MQTT
if (g_wifi.isConnected()) {
g_mqtt.connect();
@@ -427,15 +523,15 @@ void loop() {
RadarRawData radar = g_radar.getData();
checkLocalAlarms(radar);
// --- 传感器故障检测 (每30秒检查一次,避免刷屏) ---
static uint32_t lastFaultCheck = 0;
if (now - lastFaultCheck > 30000) {
lastFaultCheck = now;
if (!g_sensor.isDhtValid() && millis() > 60000) {
Serial.println(F("[系统] ⚠️ DHT11 未检测到 (不影响运行)"));
// --- 传感器故障日志 (报警系统统一管理触发, 此处仅记录) ---
static uint32_t lastFaultLog = 0;
if (now - lastFaultLog > 60000) {
lastFaultLog = now;
if (!g_sensor.isDhtValid() && millis() > 60000 && !g_localDhtFaultActive) {
Serial.println(F("[系统] ⚠️ DHT11 读数异常"));
}
if (g_radar.isTimeout()) {
Serial.println(F("[系统] ⚠️ 雷达超时无数据 (不影响运行)"));
if (g_radar.isTimeout() && !g_localRadarFaultActive) {
Serial.println(F("[系统] ⚠️ 雷达超时无数据"));
}
}
@@ -452,7 +548,8 @@ void loop() {
// 调试输出 + OLED 显示
printDebug(tele);
g_display.update(tele, wifiOk, g_alarm.isBuzzerOn());
g_display.update(tele, wifiOk, g_alarm.isBuzzerOn(),
g_alarm.hasActiveAlarm(), g_alarm.getActiveAlarmStr());
}
// --- 7. MQTT 配网状态 (WiFiManager 触发时) ---
+45
View File
@@ -371,6 +371,51 @@ void MQTTManager::publishProvisionResponse(const char* cmdId, bool success,
_publish(MQTT_TOPIC_RESPONSE_PROVISION, payload.c_str(), STATUS_QOS);
}
// ============================================================
// 上行: 本地报警上报 (§4.7 扩展)
// 设备本地判定报警后立即上报,格式兼容服务端告警协议
// ============================================================
void MQTTManager::publishLocalAlarm(uint8_t alarmType, bool triggered, const char* desc) {
if (!isConnected()) return;
struct tm timeinfo;
char timeStr[24];
if (getLocalTime(&timeinfo)) {
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
} else {
snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
}
// 类型映射
const char* typeStr = "unknown";
switch (alarmType) {
case 1: typeStr = "fall"; break; // ALARM_FALL
case 2: typeStr = "water_leak"; break; // ALARM_WATER_LEAK
case 3: typeStr = "heart_abnormal"; break; // ALARM_HEART_ABNORMAL
case 4: typeStr = "breath_abnormal"; break; // ALARM_BREATH_ABNORMAL
case 5: typeStr = "temp_abnormal"; break; // ALARM_TEMP_ABNORMAL
case 6: typeStr = "humi_abnormal"; break; // ALARM_HUMI_ABNORMAL
case 7: typeStr = "sensor_fault"; break; // ALARM_SENSOR_FAULT
default: typeStr = "unknown"; break;
}
JsonDocument doc;
doc["gateid"] = _gateid;
doc["type"] = triggered ? "alarm" : "alarm_restore";
doc["time"] = timeStr;
doc["alarmType"] = typeStr;
doc["source"] = "local"; // 标识本地判定
doc["desc"] = desc ? desc : "";
String payload;
serializeJson(doc, payload);
// 通过 status 通道上报本地报警
String topic = _buildTopic("status/%s/alarm");
_mqtt.publish(topic.c_str(), payload.c_str());
}
// ============================================================
// 回调注册
// ============================================================
+1 -1
View File
@@ -39,7 +39,7 @@ bool SensorManager::begin() {
pinMode(WATER_SENSOR_PIN, INPUT_PULLUP);
_lastRainRaw = (digitalRead(WATER_SENSOR_PIN) == HIGH); // HIGH=正常
_rainDO = _lastRainRaw;
_rainValid = true;
_rainValid = false; // 默认离线,直到检测到真正的状态变化才设为有效
// 首次读取 (DHT11 可能失败)
read();
+138 -33
View File
@@ -17,8 +17,8 @@ WiFiMgmt::WiFiMgmt()
, _lastCheckTime(0)
, _buttonPressTime(0)
, _buttonPressed(false)
, _ledLastToggle(0)
, _ledState(false)
, _ledTaskHandle(nullptr)
, _ledMode(0)
, _param_mqtt_server(nullptr)
, _param_mqtt_port(nullptr)
, _param_mqtt_user(nullptr)
@@ -48,6 +48,9 @@ bool WiFiMgmt::begin() {
pinMode(LED_WIFI_PIN, OUTPUT);
digitalWrite(LED_WIFI_PIN, LOW);
// 启动 LED 独立任务 (避免 startConfigPortal 阻塞影响闪烁)
xTaskCreate(_ledTaskFunc, "WiFiLed", 1024, nullptr, 1, &_ledTaskHandle);
// 初始化 LittleFS
if (!LittleFS.begin(true)) {
Serial.println(F("[WiFi] ❌ LittleFS 挂载失败!"));
@@ -71,11 +74,91 @@ bool WiFiMgmt::begin() {
_wm.setDebugOutput(false); // 关闭调试输出 (避免干扰)
_wm.setConfigPortalTimeout(WIFI_CONFIG_TIMEOUT);
_wm.setConnectTimeout(WIFI_CONNECT_TIMEOUT);
_wm.setTitle("🏥 五劳无感康养监测 - 设备配网"); // 配网页面的中文标题
_wm.setTitle("健康监测系统 - 设备配网");
_wm.setCustomHeadElement(
"<style>"
"body { font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif; }"
".msg .info { font-size: 14px; }"
/* ===== 字体与全局 ===== */
"*{margin:0;padding:0;box-sizing:border-box}"
"body{"
" font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;"
" background:linear-gradient(160deg, #f0f5ff 0%, #e8f0fe 40%, #f3f0ff 100%);"
" min-height:100vh;display:flex;flex-direction:column;align-items:center;"
" padding:20px 16px 40px;color:#1e293b"
"}"
"body::before{"
" content:'';position:fixed;top:-200px;right:-200px;"
" width:400px;height:400px;border-radius:50%;"
" background:radial-gradient(circle, rgba(99,102,241,0.08) 0%, transparent 70%);"
" pointer-events:none;z-index:0"
"}"
"body::after{"
" content:'';position:fixed;bottom:-100px;left:-100px;"
" width:300px;height:300px;border-radius:50%;"
" background:radial-gradient(circle, rgba(34,197,94,0.06) 0%, transparent 70%);"
" pointer-events:none;z-index:0"
"}"
/* ===== 头部卡片 ===== */
".msg{"
" width:100%;max-width:440px;position:relative;z-index:1;"
" background:#fff;border-radius:20px;padding:32px 24px 24px;"
" margin-bottom:16px;text-align:center;"
" box-shadow:0 1px 3px rgba(0,0,0,0.04),0 8px 24px rgba(0,0,0,0.06)"
"}"
".msg::before{"
" content:'';display:block;width:56px;height:56px;"
" margin:0 auto 16px;"
" background:linear-gradient(135deg,#6366f1,#8b5cf6);border-radius:16px;"
" box-shadow:0 8px 24px rgba(99,102,241,0.25)"
"}"
".msg h1,.msg h2{font-size:20px;font-weight:700;color:#1e293b;margin-bottom:6px}"
".msg p,.msg .info{font-size:13px;color:#94a3b8;line-height:1.5}"
/* 隐藏 WiFiManager 默认的 h1 重复标题 */
".msg h1{font-size:0;margin:0;padding:0}"
".msg h1::after{content:'健康监测系统';font-size:22px;font-weight:700;color:#1e293b}"
".msg .info{font-size:13px!important}"
/* ===== 表单卡片 ===== */
"form{"
" width:100%;max-width:440px;position:relative;z-index:1;"
" background:#fff;border-radius:20px;padding:24px;"
" box-shadow:0 1px 3px rgba(0,0,0,0.04),0 8px 24px rgba(0,0,0,0.06)"
"}"
"label{"
" display:block;font-size:11px;font-weight:600;"
" text-transform:uppercase;letter-spacing:0.8px;"
" color:#6366f1;margin:16px 0 6px"
"}"
"label:first-of-type{margin-top:0}"
"input[type=text],input[type=password],input[type=number]{"
" width:100%;height:46px;padding:0 14px;"
" border:2px solid #e2e8f0;border-radius:12px;"
" font-size:15px;color:#1e293b;background:#f8fafc;"
" outline:none;transition:all 0.2s;-webkit-appearance:none"
"}"
"input:focus{"
" border-color:#6366f1;background:#fff;"
" box-shadow:0 0 0 4px rgba(99,102,241,0.1)"
"}"
"input::placeholder{color:#cbd5e1}"
/* ===== 提交按钮 ===== */
"button[type=submit]{"
" width:100%;height:50px;margin-top:24px;"
" background:linear-gradient(135deg,#6366f1,#8b5cf6);"
" color:#fff;border:none;border-radius:14px;"
" font-size:16px;font-weight:600;letter-spacing:1px;"
" cursor:pointer;transition:all 0.2s;"
" box-shadow:0 4px 16px rgba(99,102,241,0.3)"
"}"
"button:active{transform:scale(0.97);box-shadow:0 2px 8px rgba(99,102,241,0.3)}"
/* ===== 页脚 ===== */
".msg:last-of-type{"
" background:transparent;box-shadow:none;padding:16px;margin-top:4px"
"}"
".msg:last-of-type::before{display:none}"
".msg:last-of-type p,.msg:last-of-type .info{font-size:12px;color:#94a3b8}"
/* ===== 成功/错误状态页 ===== */
".msg .success{color:#16a34a}"
".msg .error{color:#dc2626}"
"a{color:#6366f1;text-decoration:none;font-weight:500}"
"</style>"
);
WiFi.setAutoReconnect(true); // ESP32 自动重连
@@ -120,12 +203,12 @@ bool WiFiMgmt::begin() {
if (!_connected) {
// 如果自动连接失败, 启动配网页面
Serial.println(F("[WiFi] 自动连接失败, 启动配网模式..."));
_setLEDMode(1); // 快闪 = 配网模式
_setLEDMode(2); // 快闪 = 配网模式
_connected = _connectWiFi(); // 内部会启动配网页面
}
if (_connected) {
_setLEDMode(2); // 常亮 = 已连接
_setLEDMode(3); // 常亮 = 已连接
}
_lastCheckTime = millis();
@@ -150,25 +233,16 @@ bool WiFiMgmt::loop() {
if (WiFi.status() != WL_CONNECTED) {
_connected = false;
_setLEDMode(0); // 慢闪 = 未连接
_setLEDMode(1); // 慢闪 = 未连接
Serial.println(F("[WiFi] ⚠️ WiFi 断开, 尝试重连..."));
_connectWiFi();
} else {
_connected = true;
_setLEDMode(2); // 常亮
}
}
// LED 闪烁处理
if (!_connected) {
uint32_t interval = (_wm.getConfigPortalActive()) ? 300 : 1000;
if (millis() - _ledLastToggle > interval) {
_ledLastToggle = millis();
_ledState = !_ledState;
digitalWrite(LED_WIFI_PIN, _ledState ? HIGH : LOW);
_setLEDMode(3); // 常亮 = 已连接
}
}
// LED 由独立 FreeRTOS 任务处理 (不受 startConfigPortal 阻塞影响)
return _connected;
}
@@ -213,11 +287,11 @@ bool WiFiMgmt::_connectWiFi() {
void WiFiMgmt::startConfigPortal() {
Serial.println(F("[WiFi] 🔧 手动启动配网页面..."));
_setLEDMode(1);
_setLEDMode(2); // 快闪 = 配网模式
_wm.startConfigPortal(WIFI_AP_SSID, WIFI_AP_PASSWORD);
_connected = (WiFi.status() == WL_CONNECTED);
if (_connected) {
_setLEDMode(2);
_setLEDMode(3); // 常亮 = 已连接
}
}
@@ -393,20 +467,51 @@ const char* WiFiMgmt::getEnvNote() const {
}
// ============================================================
// LED 模式控制
// LED 模式控制 (线程安全, 仅写模式变量,由独立任务执行)
// ============================================================
void WiFiMgmt::_setLEDMode(uint8_t mode) {
// mode 由 loop() 中的闪烁逻辑处理
// 0=慢闪 1=快闪 2=常亮
switch (mode) {
case 0:
case 1:
// 闪烁由 loop 处理
break;
case 2:
digitalWrite(LED_WIFI_PIN, HIGH);
_ledState = true;
break;
_ledMode = mode; // uint8_t 赋值在 ESP32 上是原子的
}
// ============================================================
// LED 闪烁独立任务 (FreeRTOS)
// 独立于主循环,startConfigPortal 阻塞期间也能正常闪烁
// ============================================================
void WiFiMgmt::_ledTaskFunc(void* param) {
const TickType_t fastMs = pdMS_TO_TICKS(300); // 快闪 = 配网模式
const TickType_t slowMs = pdMS_TO_TICKS(1000); // 慢闪 = 未连接
while (true) {
if (!_instance) {
vTaskDelay(slowMs);
continue;
}
uint8_t mode = _instance->_ledMode;
switch (mode) {
case 0: // 灭
digitalWrite(LED_WIFI_PIN, LOW);
vTaskDelay(slowMs);
break;
case 1: // 慢闪 — WiFi 断连
digitalWrite(LED_WIFI_PIN, HIGH);
vTaskDelay(slowMs);
digitalWrite(LED_WIFI_PIN, LOW);
vTaskDelay(slowMs);
break;
case 2: // 快闪 — 配网模式
digitalWrite(LED_WIFI_PIN, HIGH);
vTaskDelay(fastMs);
digitalWrite(LED_WIFI_PIN, LOW);
vTaskDelay(fastMs);
break;
case 3: // 常亮 — 已连接
digitalWrite(LED_WIFI_PIN, HIGH);
vTaskDelay(slowMs);
break;
}
}
}