feat:增加蓝牙配网功能

This commit is contained in:
yangzhijia
2026-08-12 09:20:54 +08:00
parent 2ce4ac0b9c
commit 93112dcdfd
11 changed files with 1163 additions and 561 deletions

File diff suppressed because one or more lines are too long

View File

@@ -16,10 +16,15 @@
// 1. 硬件引脚定义 (ESP32-S3)
// ============================================================
// --- R60ABD1 毫米波雷达 ---
// --- 雷达型号选择 ---
#define RADAR_TYPE_ABD1 0 // R60ABD1: 心率/呼吸精准, 跌倒一般
#define RADAR_TYPE_AFD1 1 // R60AFD1: 跌倒/姿态精准, 无心率呼吸
#define RADAR_TYPE RADAR_TYPE_AFD1 // 演示用 AFD1 (跌倒精准)
// --- 毫米波雷达 ---
#define RADAR_UART_PORT Serial2
#define RADAR_RX_PIN 17
#define RADAR_TX_PIN 1 // UART 发送 (从18挪到1)
#define RADAR_TX_PIN 1
#define RADAR_BAUD_RATE 115200
// --- 雷达备用 GPIO ---
@@ -72,8 +77,8 @@
// ============================================================
#define WIFI_AP_SSID "HealthMonitor-Setup"
#define WIFI_AP_PASSWORD "12345678"
#define WIFI_CONFIG_TIMEOUT 300
#define WIFI_AP_PASSWORD "1234567890"
#define WIFI_CONFIG_TIMEOUT 0 // 0=不超时, 一直等待配置
#define WIFI_CONNECT_TIMEOUT 15
#define WIFI_RETRY_INTERVAL 10000
@@ -130,6 +135,7 @@
#define TEMP_LOW 5.0f
#define TEMP_HIGH 45.0f
#define HUMI_LOW 15.0f
#define PRESENCE_HYSTERESIS 8 // 有人无人防抖 (PCB信号抖动时过滤)
#define HUMI_HIGH 90.0f
#define WATER_DEBOUNCE_MS 50

View File

@@ -68,6 +68,7 @@ private:
void _extractPresence(const RadarFrame& frame);
void _extractBreathing(const RadarFrame& frame);
void _extractHeartRate(const RadarFrame& frame);
void _extractFall(const RadarFrame& frame); // AFD1 跌倒
void _extractSleep(const RadarFrame& frame);
void _extractWorkStatus(const RadarFrame& frame);
void _extractDetectRange(const RadarFrame& frame);
@@ -85,6 +86,13 @@ private:
RadarCallback _callback;
uint32_t _frameCount;
uint32_t _errorCount;
uint32_t _lastHeartBeat; // 上次心率数据时间
uint32_t _lastBreathBeat; // 上次呼吸数据时间
// 有人无人防抖 (PCB 信号不稳定时过滤跳变)
bool _stablePresence; // 防抖后的稳定有人/无人
uint8_t _presenceCnt; // 连续一致计数
bool _lastRawPresence; // 上一次原始值
};
#endif // RADAR_MANAGER_H

View File

@@ -33,9 +33,10 @@ enum RadarControl : uint8_t {
CTRL_WORK_STATUS = 0x05, // 工作状态
CTRL_DETECT_RANGE = 0x07, // 探测范围
CTRL_PRESENCE = 0x80, // 人体存在
CTRL_BREATHING = 0x81, // 呼吸检测
CTRL_SLEEP = 0x84, // 睡眠监测
CTRL_HEART_RATE = 0x85, // 心率监测
CTRL_BREATHING = 0x81, // 呼吸检测 (仅 ABD1)
CTRL_FALL = 0x83, // 跌倒检测 (仅 AFD1)
CTRL_SLEEP = 0x84, // 睡眠监测 (仅 ABD1)
CTRL_HEART_RATE = 0x85, // 心率监测 (仅 ABD1)
};
// ============================================================

View File

@@ -1,10 +1,10 @@
/**
* @file wifi_manager.h
* @brief WiFi 配网管理器
* @brief WiFi 配网管理器 (AP + WebServer)
*
* 功能:
* - 自动连接已保存的 WiFi
* - 配网失败/首次启动 → AP 模式 + 强制门户页面
* - 配网失败/首次启动 → AP 模式 + Captive Portal
* - 自定义参数: MQTT 服务器/端口/用户/密码、设备名称、环境备注
* - 长按按钮重置配置
* - LED 状态指示
@@ -15,7 +15,9 @@
#define WIFI_MANAGER_H
#include <Arduino.h>
#include <WiFiManager.h>
#include <WiFi.h>
#include <WebServer.h>
#include <DNSServer.h>
#include <LittleFS.h>
#include <ArduinoJson.h>
#include "config.h"
@@ -27,7 +29,9 @@ struct DeviceConfig {
char mqtt_user[20];
char mqtt_pass[20];
char device_id[20];
char env_note[20]; // 环境备注 (如: "主卧", "客厅", "老人房")
char env_note[20]; // 环境备注
char wifi_ssid[33]; // WiFi SSID (持久化)
char wifi_pass[65]; // WiFi 密码 (持久化)
};
class WiFiMgmt {
@@ -38,100 +42,55 @@ public:
// 单例指针 (用于静态回调)
static WiFiMgmt* _instance;
/**
* @brief 初始化 WiFi 管理器
* @return true=成功
*/
bool begin();
/**
* @brief 主循环, 处理配网流程和按钮检测
* @return true=WiFi 已连接
*/
bool loop();
/**
* @brief WiFi 是否已连接
*/
bool isConnected() const;
/**
* @brief 获取设备配置
*/
bool justProvisioned(); // 刚配网成功 (读取后自动清除)
DeviceConfig getConfig() const;
/**
* @brief 获取设备 ID (用于 MQTT client_id)
*/
const char* getDeviceId() const;
/**
* @brief 获取环境备注
*/
const char* getEnvNote() const;
/**
* @brief 强制进入配网模式
*/
void startConfigPortal();
/**
* @brief 清除所有配置并重启
*/
void resetSettings();
private:
/**
* @brief 尝试连接 WiFi
*/
bool _connectWiFi();
/**
* @brief 设置 LED 模式 (线程安全, 无阻塞)
* @param mode 0=灭 1=慢闪(断连1s) 2=快闪(配网300ms) 3=常亮(已连接)
*/
void _setLEDMode(uint8_t mode);
/**
* @brief LED 闪烁独立任务 (FreeRTOS, 不受主循环阻塞影响)
*/
static void _ledTaskFunc(void* param);
/**
* @brief 检测按钮长按
*/
bool _checkButtonLongPress();
/**
* @brief 加载/保存配置到 LittleFS
*/
bool _connectWiFi(); // 仅尝试已保存凭据
bool _tryConnect(const char* ssid, const char* pass); // 尝试连接指定 WiFi
bool _loadConfig();
bool _saveConfig();
void _setLEDMode(uint8_t mode);
static void _ledTaskFunc(void* param);
bool _checkButtonLongPress();
/**
* @brief 保存 WiFi 参数回调 (WiFiManager 的回调)
*/
static void _saveParamsCallback();
static void _apCallback(WiFiManager* wm);
// Web 服务器
void _startPortal();
void _stopPortal();
void _handleRoot();
void _handleSave();
void _handleReset();
void _updateWifiScan(); // WiFi 扫描缓存
String _htmlPage(); // 返回配网 HTML 页面
WiFiManager _wm;
WebServer* _server;
DNSServer* _dns;
DeviceConfig _config;
bool _connected;
bool _portalActive;
bool _portalStopFlag; // 延迟关闭标记
uint32_t _portalStartTime;
String _pendingSSID;
String _pendingPass;
bool _provisionSuccess; // 刚配网成功标记
uint32_t _lastCheckTime;
uint32_t _buttonPressTime;
bool _buttonPressed;
// LED 独立任务 (不被 startConfigPortal 阻塞影响)
// WiFi 扫描缓存 (避免每次打开页面都扫描3-8秒)
String _wifiScanCache;
uint32_t _lastScanTime;
// LED 独立任务
TaskHandle_t _ledTaskHandle;
volatile uint8_t _ledMode; // 0=灭 1=慢闪 2=快闪 3=常亮
// WiFiManager 自定义参数
WiFiManagerParameter* _param_mqtt_server;
WiFiManagerParameter* _param_mqtt_port;
WiFiManagerParameter* _param_mqtt_user;
WiFiManagerParameter* _param_mqtt_pass;
WiFiManagerParameter* _param_device_id;
WiFiManagerParameter* _param_env_note;
};
#endif // WIFI_MANAGER_H

View File

@@ -9,6 +9,8 @@ framework = arduino
; 串口监视器
monitor_speed = 115200
monitor_filters = direct
monitor_rts = 0
monitor_dtr = 0
; Flash 配置
board_build.flash_mode = qio
@@ -25,9 +27,6 @@ build_flags =
; 库依赖
lib_deps =
; WiFi 配网 (自带 captive portal)
tzapu/WiFiManager@^2.0.17
; MQTT 客户端
knolleary/PubSubClient@^2.8
@@ -43,4 +42,4 @@ lib_deps =
; 上传配置
upload_speed = 921600
upload_port = COM16
upload_port = COM19

View File

@@ -64,9 +64,9 @@ 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);
// 有源蜂鸣器: 用 digitalWrite, 不用 PWM
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW);
pinMode(LED_ALARM_PIN, OUTPUT);
digitalWrite(LED_ALARM_PIN, LOW);
@@ -148,26 +148,26 @@ 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; // 跌倒: 连速
beeps = 0; onMs = 200; offMs = 200; pauseMs = 0; // 跌倒: 急促哔
break;
case AlarmManager::ALARM_HEART_ABNORMAL:
case AlarmManager::ALARM_BREATH_ABNORMAL:
beeps = 5; onMs = 80; offMs = 80; pauseMs = 400; // 心率/呼吸: 5连
beeps = 5; onMs = 200; offMs = 150; pauseMs = 500; // 心率/呼吸: 5连哔
break;
// 环境安全 — 次急
// 环境安全
case AlarmManager::ALARM_WATER_LEAK:
beeps = 3; onMs = 120; offMs = 100; pauseMs = 500; // 水浸: 3连哔
beeps = 3; onMs = 250; offMs = 200; pauseMs = 600; // 水浸: 3连哔
break;
// 环境舒适 — 温和
// 环境舒适
case AlarmManager::ALARM_TEMP_ABNORMAL:
case AlarmManager::ALARM_HUMI_ABNORMAL:
beeps = 2; onMs = 200; offMs = 400; pauseMs = 1200; // 温湿度: 2声长停
beeps = 2; onMs = 300; offMs = 500; pauseMs = 1500; // 温湿度: 2声长停
break;
// 设备状态 — 仅提示
// 设备状态
case AlarmManager::ALARM_SENSOR_FAULT:
beeps = 1; onMs = 300; offMs = 3000; pauseMs = 0; // 传感器: 单声提醒
beeps = 1; onMs = 500; offMs = 3000; pauseMs = 0; // 传感器: 单声提醒
break;
default:
beeps = 0; onMs = 500; offMs = 500; pauseMs = 0; // 持续
@@ -279,6 +279,11 @@ void AlarmManager::executeRestore(const AlarmCommand& cmd) {
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;
else if (strcmp(cmd.alarmType, "heart_abnormal") == 0) localType = ALARM_HEART_ABNORMAL;
else if (strcmp(cmd.alarmType, "breath_abnormal") == 0) localType = ALARM_BREATH_ABNORMAL;
else if (strcmp(cmd.alarmType, "temp_abnormal") == 0) localType = ALARM_TEMP_ABNORMAL;
else if (strcmp(cmd.alarmType, "humi_abnormal") == 0) localType = ALARM_HUMI_ABNORMAL;
else if (strcmp(cmd.alarmType, "sensor_fault") == 0) localType = ALARM_SENSOR_FAULT;
clearLocal(localType);
@@ -308,7 +313,7 @@ void AlarmManager::_setBuzzer(bool on) {
if (on) {
_buzzerOffTime = 0;
} else {
ledcWrite(BUZZER_PWM_CHANNEL, 0);
digitalWrite(BUZZER_PIN, LOW);
_buzzerOffTime = 0;
}
}
@@ -328,16 +333,20 @@ void AlarmManager::_updateBuzzer() {
// 定时关闭
if (_buzzerOffTime > 0 && millis() >= _buzzerOffTime) {
_buzzerOn = false;
ledcWrite(BUZZER_PWM_CHANNEL, 0);
digitalWrite(BUZZER_PIN, LOW);
Serial.println(F("[告警] ⏰ 蜂鸣器定时关闭"));
return;
}
uint32_t now = millis();
// 云端定时: 持续响
// 云端定时: 哔哔声 (500ms响/500ms停), 不是长鸣
if (_buzzerOffTime > 0) {
ledcWrite(BUZZER_PWM_CHANNEL, 128);
if (now - _buzzerToggleTime >= 500) {
_buzzerToggleTime = now;
_buzzerBeepState = !_buzzerBeepState;
digitalWrite(BUZZER_PIN, _buzzerBeepState ? HIGH : LOW);
}
return;
}
@@ -347,7 +356,7 @@ void AlarmManager::_updateBuzzer() {
if (now - _buzzerToggleTime >= (_buzzerBeepState ? _beepOnMs : _beepOffMs)) {
_buzzerToggleTime = now;
_buzzerBeepState = !_buzzerBeepState;
ledcWrite(BUZZER_PWM_CHANNEL, _buzzerBeepState ? 128 : 0);
digitalWrite(BUZZER_PIN, _buzzerBeepState ? HIGH : LOW);
}
} else {
// 多声一组: N声哔 + 长停顿
@@ -358,7 +367,7 @@ void AlarmManager::_updateBuzzer() {
_buzzerToggleTime = now;
if (_beepCount >= _beepsPerCycle) _beepCount = 0;
_buzzerBeepState = true;
ledcWrite(BUZZER_PWM_CHANNEL, 128);
digitalWrite(BUZZER_PIN, HIGH);
_beepCount++;
}
} else {
@@ -366,7 +375,7 @@ void AlarmManager::_updateBuzzer() {
if (now - _buzzerToggleTime >= _beepOnMs) {
_buzzerToggleTime = now;
_buzzerBeepState = false;
ledcWrite(BUZZER_PWM_CHANNEL, 0);
digitalWrite(BUZZER_PIN, LOW);
}
}
}

View File

@@ -44,13 +44,13 @@ void ButtonManager::begin() {
pinMode(BTN_ALARM_DISMISS_PIN, INPUT_PULLUP);
pinMode(BTN_PAGE_SWITCH_PIN, INPUT_PULLUP);
// 读取初始状态, 避免启动时的虚假边沿
_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;
// 强制初始化为未按下 (PCB 上电 GPIO 不稳, 忽略初始状态)
_btnProvision.rawState = false;
_btnProvision.stableState = false;
_btnDismiss.rawState = false;
_btnDismiss.stableState = false;
_btnPage.rawState = false;
_btnPage.stableState = false;
Serial.println(F("[按钮] ✅ 三按钮初始化完成 (双向去抖+超时自愈)"));
Serial.printf(" 配网: GPIO%d | 解除报警: GPIO%d | 翻页: GPIO%d\n",
@@ -64,6 +64,9 @@ void ButtonManager::begin() {
// ============================================================
void ButtonManager::loop() {
// 开机 10 秒静默 (PCB 上电 GPIO 不稳 + 强制初始未按下)
if (millis() < 10000) return;
// 按钮1: 配网 (仅长按)
_processButton(_btnProvision, false, true);

View File

@@ -49,7 +49,14 @@ bool g_localDhtFaultActive = false;
uint8_t g_tempAbnCnt = 0;
uint8_t g_humiAbnCnt = 0;
uint8_t g_radarTimeoutCnt = 0;
uint8_t g_heartAbnCnt = 0; // 心率异常防抖
uint8_t g_breathAbnCnt = 0; // 呼吸异常防抖
uint32_t g_fallDetectMs = 0; // 跌倒检测计时 (ms)
uint32_t g_fallClearMs = 0; // 跌倒清除计时 (ms)
bool g_fallDismissed = false; // 已手动解除, 不重复报警
#define ALARM_HYSTERESIS 3 // 连续3次确认才触发
#define FALL_DETECT_MS 2000 // 跌倒持续2秒才触发 (房顶安装, 过滤坐下)
#define FALL_CLEAR_MS 5000 // 跌倒消失5秒后才清除 (防误消)
// ============================================================
// 时间同步
@@ -84,14 +91,28 @@ void syncTime() {
void handleAlarmCommand(const AlarmCommand& cmd) {
if (strcmp(cmd.type, "alarm_restore") == 0) {
// 告警恢复
// 告警恢复 — 远程指令与手动按钮同等优先级
g_alarm.executeRestore(cmd);
// 清除对应本地告警状态
if (strcmp(cmd.alarmType, "fall") == 0) g_localFallActive = false;
else if (strcmp(cmd.alarmType, "water_leak") == 0) g_localWaterActive = false;
else if (strcmp(cmd.alarmType, "heart_abnormal") == 0) g_localHeartAbnActive = false;
else if (strcmp(cmd.alarmType, "breath_abnormal") == 0) g_localBreathAbnActive = false;
// 清除全部本地告警状态 (远程恢复=手动解除)
g_localFallActive = false;
g_localWaterActive = false;
g_localHeartAbnActive = false;
g_localBreathAbnActive = false;
g_localTempAbnActive = false;
g_localHumiAbnActive = false;
g_localRadarFaultActive = false;
g_localDhtFaultActive = false;
// 防抖计数器也复位
g_tempAbnCnt = 0;
g_humiAbnCnt = 0;
g_radarTimeoutCnt = 0;
g_heartAbnCnt = 0;
g_breathAbnCnt = 0;
g_fallDetectMs = 0;
g_fallClearMs = 0;
g_fallDismissed = false;
} else {
// 告警触发
g_alarm.executeAlarm(cmd);
@@ -253,49 +274,92 @@ void checkLocalAlarms(const RadarRawData& radar) {
// ─── 前置条件: 有人才检测人体生命体征 ───
bool humanPresent = radar.presence && radar.radarValid;
// ─── ★★★★★ 人身安全: 跌倒 (立即触发, 无防抖) ───
if (radar.fall && humanPresent && !g_localFallActive) {
// ─── ★★★★★ 人身安全: 跌倒 (有人+30s+2s确认, 5s清除) ───
if (radar.fall && humanPresent && !g_fallDismissed && millis() > 30000) {
if (!g_localFallActive) {
// 持续检测中: 必须满 FALL_DETECT_MS 才报警
if (g_fallDetectMs == 0) {
g_fallDetectMs = millis();
} else if (millis() - g_fallDetectMs >= FALL_DETECT_MS) {
g_localFallActive = true;
g_fallDetectMs = 0;
g_fallClearMs = 0;
g_alarm.triggerLocal(AlarmManager::ALARM_FALL, "雷达检测到跌倒");
} else if (!radar.fall && g_localFallActive) {
}
} else {
g_fallClearMs = 0; // 还在跌倒, 重置清除计时
}
} else {
g_fallDetectMs = 0;
// 解除抑制需要 fall 持续消失 FALL_CLEAR_MS (防 AFD1 频繁跳变)
if (g_fallDismissed && g_fallClearMs == 0) {
g_fallClearMs = millis();
}
if (!g_fallDismissed || millis() - g_fallClearMs >= FALL_CLEAR_MS) {
g_fallDismissed = false;
g_fallClearMs = 0;
}
if (g_localFallActive) {
// 跌倒已消失 → 延迟清除
if (g_fallClearMs == 0) {
g_fallClearMs = millis();
} else if (millis() - g_fallClearMs >= FALL_CLEAR_MS) {
g_localFallActive = false;
g_fallClearMs = 0;
g_alarm.clearLocal(AlarmManager::ALARM_FALL);
}
}
}
// ─── ★★★★★ 人身安全: 心率异常 (有人才检测) ───
if (humanPresent && radar.vitalValid && radar.heartRate > 0) {
// ─── ★★★★★ 人身安全: 心率异常 (仅 ABD1, 防抖3次, 30秒后生效) ───
#if RADAR_TYPE != RADAR_TYPE_AFD1
if (millis() > 30000 && humanPresent && radar.vitalValid && radar.heartRate > 0) {
bool heartAbn = (radar.heartRate < HEART_RATE_MIN || radar.heartRate > HEART_RATE_MAX);
if (heartAbn && !g_localHeartAbnActive) {
if (heartAbn) {
if (++g_heartAbnCnt >= ALARM_HYSTERESIS && !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) {
}
} else {
g_heartAbnCnt = 0;
if (g_localHeartAbnActive) {
g_localHeartAbnActive = false;
g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
}
}
} else if (!humanPresent && g_localHeartAbnActive) {
// 人走了 — 清除之前的报警
g_localHeartAbnActive = false;
g_heartAbnCnt = 0;
g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
}
#endif // RADAR_TYPE != AFD1
// ─── ★★★★★ 人身安全: 呼吸异常 (有人才检测) ───
if (humanPresent && radar.vitalValid && radar.breathRate > 0) {
// ─── ★★★★★ 人身安全: 呼吸异常 (仅 ABD1, 防抖3次, 30秒后生效) ───
#if RADAR_TYPE != RADAR_TYPE_AFD1
if (millis() > 30000 && humanPresent && radar.vitalValid && radar.breathRate > 0) {
bool breathAbn = (radar.breathRate < BREATH_RATE_MIN || radar.breathRate > BREATH_RATE_MAX);
if (breathAbn && !g_localBreathAbnActive) {
if (breathAbn) {
if (++g_breathAbnCnt >= ALARM_HYSTERESIS && !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) {
}
} else {
g_breathAbnCnt = 0;
if (g_localBreathAbnActive) {
g_localBreathAbnActive = false;
g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
}
}
} else if (!humanPresent && g_localBreathAbnActive) {
g_localBreathAbnActive = false;
g_breathAbnCnt = 0;
g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
}
#endif // RADAR_TYPE != AFD1
// ─── ★★★ 环境安全: 水浸 ───
bool waterLeak = !g_sensor.getRainDO() && g_sensor.isRainValid();
@@ -307,8 +371,8 @@ void checkLocalAlarms(const RadarRawData& radar) {
g_alarm.clearLocal(AlarmManager::ALARM_WATER_LEAK);
}
// ─── ★★ 环境舒适: 温度异常 (防抖3次) ───
if (g_sensor.isDhtValid()) {
// ─── ★★ 环境舒适: 温度异常 (防抖3次, 30秒启动静默) ───
if (millis() > 30000 && g_sensor.isDhtValid()) {
float temp = g_sensor.getTemp();
bool tempAbn = (temp < TEMP_LOW || temp > TEMP_HIGH);
if (tempAbn) {
@@ -345,27 +409,27 @@ void checkLocalAlarms(const RadarRawData& radar) {
}
}
// ─── 设备:雷达超时 (连续5秒无数据) ───
// ─── 设备:雷达超时 (连续5秒无数据, 仅标记, 不报警) ───
if (g_radar.isTimeout()) {
if (++g_radarTimeoutCnt >= 5 && !g_localRadarFaultActive) {
g_localRadarFaultActive = true;
g_alarm.triggerLocal(AlarmManager::ALARM_SENSOR_FAULT, "雷达超时无数据");
Serial.println(F("[系统] ⚠️ 雷达超时无数据"));
}
} else {
g_radarTimeoutCnt = 0;
if (g_localRadarFaultActive) {
g_localRadarFaultActive = false;
g_alarm.clearLocal(AlarmManager::ALARM_SENSOR_FAULT);
Serial.println(F("[系统] ✅ 雷达数据恢复"));
}
}
// ─── 设备DHT 持续故障 (已由 SensorManager 内部计数) ───
// ─── 设备DHT 持续故障 (仅标记, 不报警) ───
if (g_sensor.isDhtFault() && !g_localDhtFaultActive && millis() > 60000) {
g_localDhtFaultActive = true;
g_alarm.triggerLocal(AlarmManager::ALARM_SENSOR_FAULT, "DHT11温湿度传感器故障");
Serial.println(F("[系统] ⚠️ DHT11 温湿度传感器故障"));
} else if (!g_sensor.isDhtFault() && g_localDhtFaultActive) {
g_localDhtFaultActive = false;
g_alarm.clearLocal(AlarmManager::ALARM_SENSOR_FAULT);
Serial.println(F("[系统] ✅ DHT11 传感器恢复"));
}
}
@@ -439,6 +503,20 @@ void setup() {
});
g_buttons.onAlarmDismiss([]() {
g_alarm.clearAllLocal();
// 同步清除所有本地状态标记
g_localFallActive = false;
g_localWaterActive = false;
g_localHeartAbnActive = false;
g_localBreathAbnActive = false;
g_localTempAbnActive = false;
g_localHumiAbnActive = false;
g_localRadarFaultActive = false;
g_localDhtFaultActive = false;
g_tempAbnCnt = 0;
g_humiAbnCnt = 0;
g_radarTimeoutCnt = 0;
g_fallClearMs = 0;
g_fallDismissed = true; // 抑制重复报警, 等跌倒信号消了再恢复
});
g_buttons.onProvisionLongPress([]() {
g_wifi.startConfigPortal();
@@ -474,6 +552,9 @@ void setup() {
// 8. 传感器
g_sensor.begin();
// 8. 等雷达 + 电源稳定
delay(8000);
// 9. 雷达任务 (核心0)
xTaskCreatePinnedToCore(
radarTask, "RadarTask",
@@ -511,6 +592,13 @@ void loop() {
syncTime();
wasDisconnected = false;
}
// 配网成功后更新 MQTT 配置 (如果有改动) 并上报
if (g_wifi.justProvisioned()) {
DeviceConfig cfg = g_wifi.getConfig();
g_mqtt.setServer(cfg.mqtt_server, atoi(cfg.mqtt_port),
cfg.mqtt_user, cfg.mqtt_pass);
g_mqtt.publishProvisionStatus("success", "");
}
} else {
wasDisconnected = true;
}
@@ -523,15 +611,15 @@ void loop() {
RadarRawData radar = g_radar.getData();
checkLocalAlarms(radar);
// --- 传感器故障日志 (报警系统统一管理触发, 此处仅记录) ---
// --- 传感器状态日志 (仅记录, 不报警) ---
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_sensor.isDhtValid() && millis() > 60000) {
Serial.println(F("[系统] ⚠️ DHT11 读数失效"));
}
if (g_radar.isTimeout() && !g_localRadarFaultActive) {
Serial.println(F("[系统] ⚠️ 雷达超时无数据"));
if (g_radar.isTimeout()) {
Serial.println(F("[系统] ⚠️ 雷达数据超时"));
}
}

View File

@@ -19,6 +19,11 @@ RadarManager::RadarManager()
, _callback(nullptr)
, _frameCount(0)
, _errorCount(0)
, _lastHeartBeat(0)
, _lastBreathBeat(0)
, _stablePresence(false)
, _presenceCnt(0)
, _lastRawPresence(false)
{
memset(&_currentFrame, 0, sizeof(_currentFrame));
memset(&_data, 0, sizeof(_data));
@@ -43,6 +48,9 @@ bool RadarManager::begin() {
_resetParser();
memset(&_data, 0, sizeof(_data));
_data.lastUpdate = millis();
_stablePresence = false;
_presenceCnt = 0;
_lastRawPresence = false;
// 不给雷达发任何命令,让它自己初始化并开始自动上报
// R60ABD1 上电后通常需要 15-30 秒自检,之后自动输出数据
@@ -68,6 +76,16 @@ void RadarManager::loop() {
Serial.printf("[雷达] ✅ 开始接收数据 (首帧)\n");
}
// 心率/呼吸数据过期 (>10秒无更新 → 标记无效)
if (millis() - _lastHeartBeat > 10000) {
_data.vitalValid = false;
_data.heartRate = 0;
}
if (millis() - _lastBreathBeat > 10000) {
_data.vitalValid = false;
_data.breathRate = 0;
}
// 超时检测:如果 10 秒还没收到任何数据,打印一次
static uint32_t noDataWarn = 0;
if (_frameCount == 0 && millis() - _data.lastUpdate > 10000) {
@@ -222,6 +240,9 @@ void RadarManager::_processCompleteFrame() {
case CTRL_HEART_RATE:
_extractHeartRate(_currentFrame);
break;
case CTRL_FALL:
_extractFall(_currentFrame); // AFD1 专用跌倒
break;
case CTRL_SLEEP:
_extractSleep(_currentFrame);
break;
@@ -250,47 +271,60 @@ void RadarManager::_processCompleteFrame() {
void RadarManager::_extractPresence(const RadarFrame& frame) {
uint8_t raw = frame.data_length > 0 ? frame.data[0] : 0;
bool rawPresence = false;
// cmd 0x00 (len=1): 有人状态 (1=有人 0=无人)
if (frame.command == 0x00 && frame.data_length >= 1) {
_data.presence = (raw == 1);
rawPresence = (raw == 1);
_data.radarValid = true;
_data.lastUpdate = millis();
_notifyUpdate();
}
// cmd 0x01 (len=1): 运动状态/体动 (值越大越活跃)
else if (frame.command == 0x01 && frame.data_length >= 1) {
_data.body = raw; // 0=静止, 1+=有体动
_data.motion = (raw >= 1); // >=1 有运动
_data.body = raw;
_data.motion = (raw >= 1);
_data.radarValid = true;
_data.lastUpdate = millis();
_notifyUpdate();
return; // 运动帧不含有人信息, 跳过防抖
}
// cmd 0x03 (len=1): 有人状态备用 (非0=有人)
else if (frame.command == 0x03 && frame.data_length >= 1) {
_data.presence = (raw != 0);
rawPresence = (raw != 0);
_data.radarValid = true;
_data.lastUpdate = millis();
_notifyUpdate();
}
// cmd 0x04 (len=2): [0]=presence [1]=motion
else if (frame.command == 0x04 && frame.data_length >= 2) {
_data.presence = (frame.data[0] != 0);
rawPresence = (frame.data[0] != 0);
_data.motion = (frame.data[1] == 0);
_data.radarValid = true;
_data.lastUpdate = millis();
_notifyUpdate();
}
// cmd 0x05 (len=6): 完整包
else if (frame.command == 0x05 && frame.data_length >= 6) {
_data.presence = (frame.data[0] != 0);
rawPresence = (frame.data[0] != 0);
_data.motion = (frame.data[1] == 0);
_data.distance = ((uint16_t)frame.data[2] << 8) | frame.data[3];
_data.body = frame.data[4];
_data.radarValid = true;
_data.lastUpdate = millis();
_notifyUpdate();
} else {
return; // 未知命令
}
// 防抖: 连续 PRESENCE_HYSTERESIS 次一致才更新
if (rawPresence == _lastRawPresence) {
if (_presenceCnt < PRESENCE_HYSTERESIS) _presenceCnt++;
} else {
_presenceCnt = 0;
_lastRawPresence = rawPresence;
}
if (_presenceCnt >= PRESENCE_HYSTERESIS && rawPresence != _data.presence) {
_data.presence = rawPresence;
_stablePresence = rawPresence;
}
_notifyUpdate();
}
void RadarManager::_extractBreathing(const RadarFrame& frame) {
@@ -299,8 +333,9 @@ void RadarManager::_extractBreathing(const RadarFrame& frame) {
if (rate != 0x80 && rate != 0x00 && rate <= 40) {
_data.breathRate = rate;
_data.vitalValid = true;
_data.presence = true; // 有呼吸=有人
_data.presence = true;
_data.radarValid = true;
_lastBreathBeat = millis();
}
_data.lastUpdate = millis();
_notifyUpdate();
@@ -313,14 +348,25 @@ void RadarManager::_extractHeartRate(const RadarFrame& frame) {
if (rate != 0x80 && rate != 0x00 && rate >= 40) {
_data.heartRate = rate;
_data.vitalValid = true;
_data.presence = true; // 有心跳=有人
_data.presence = true;
_data.radarValid = true;
_lastHeartBeat = millis();
}
_data.lastUpdate = millis();
_notifyUpdate();
}
}
// AFD1 专用: 跌倒检测 (控制字 0x83)
void RadarManager::_extractFall(const RadarFrame& frame) {
if (frame.data_length >= 1) {
_data.fall = (frame.data[0] != 0);
_data.radarValid = true;
_data.lastUpdate = millis();
_notifyUpdate();
}
}
void RadarManager::_extractSleep(const RadarFrame& frame) {
// 从睡眠数据提取: cmd=0x00/0x01 为入床离床状态
if (frame.data_length >= 1) {

View File

@@ -1,36 +1,28 @@
/**
* @file wifi_manager.cpp
* @brief WiFi 配网管理器实现
* @brief WiFi 配网管理器 — AP + WebServer
*/
#include "wifi_manager.h"
// 单例指针
WiFiMgmt* WiFiMgmt::_instance = nullptr;
// ============================================================
// 构造函数 & 析构函数
// ============================================================
WiFiMgmt::WiFiMgmt()
: _connected(false)
, _lastCheckTime(0)
, _buttonPressTime(0)
, _buttonPressed(false)
, _ledTaskHandle(nullptr)
, _ledMode(0)
, _param_mqtt_server(nullptr)
, _param_mqtt_port(nullptr)
, _param_mqtt_user(nullptr)
, _param_mqtt_pass(nullptr)
, _param_device_id(nullptr)
, _param_env_note(nullptr)
: _server(nullptr), _dns(nullptr)
, _connected(false)
, _portalActive(false), _portalStopFlag(false)
, _portalStartTime(0)
, _lastCheckTime(0), _buttonPressTime(0), _buttonPressed(false)
, _lastScanTime(0)
, _provisionSuccess(false)
, _ledTaskHandle(nullptr), _ledMode(0)
{
memset(&_config, 0, sizeof(_config));
_instance = this; // 设置单例
_instance = this;
}
WiFiMgmt::~WiFiMgmt() {
_stopPortal();
_instance = nullptr;
}
@@ -39,176 +31,52 @@ WiFiMgmt::~WiFiMgmt() {
// ============================================================
bool WiFiMgmt::begin() {
Serial.println(F("\n========================================"));
Serial.println(F(" WiFi 配网管理器 初始化"));
Serial.println(F("========================================"));
Serial.println(("\n=== WiFi 配网管理器 (SmartConfig) ==="));
// 初始化引脚
pinMode(CONFIG_BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_WIFI_PIN, OUTPUT);
digitalWrite(LED_WIFI_PIN, LOW);
// 启动 LED 独立任务 (避免 startConfigPortal 阻塞影响闪烁)
xTaskCreate(_ledTaskFunc, "WiFiLed", 1024, nullptr, 1, &_ledTaskHandle);
// 初始化 LittleFS
// 冷启动延迟, 等 Flash 稳定
delay(500);
if (!LittleFS.begin(true)) {
Serial.println(F("[WiFi] LittleFS 挂载失败!"));
Serial.println(("[WiFi] LittleFS 挂载失败!"));
return false;
}
Serial.println(F("[WiFi] LittleFS 已挂载"));
Serial.println(("[WiFi] LittleFS 就绪"));
// 加载配置
if (!_loadConfig()) {
Serial.println(F("[WiFi] 使用默认配置"));
// 设置默认值
strncpy(_config.mqtt_server, MQTT_DEFAULT_SERVER, sizeof(_config.mqtt_server) - 1);
strncpy(_config.mqtt_port, MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port) - 1);
strncpy(_config.mqtt_user, MQTT_DEFAULT_USER, sizeof(_config.mqtt_user) - 1);
strncpy(_config.mqtt_pass, MQTT_DEFAULT_PASS, sizeof(_config.mqtt_pass) - 1);
strncpy(_config.device_id, MQTT_DEFAULT_DEVICE, sizeof(_config.device_id) - 1);
strncpy(_config.env_note, MQTT_DEFAULT_ENV, sizeof(_config.env_note) - 1);
Serial.println(("[WiFi] 使用默认配置"));
strncpy(_config.mqtt_server, MQTT_DEFAULT_SERVER, sizeof(_config.mqtt_server)-1);
strncpy(_config.mqtt_port, MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port)-1);
strncpy(_config.mqtt_user, MQTT_DEFAULT_USER, sizeof(_config.mqtt_user)-1);
strncpy(_config.mqtt_pass, MQTT_DEFAULT_PASS, sizeof(_config.mqtt_pass)-1);
strncpy(_config.device_id, MQTT_DEFAULT_DEVICE, sizeof(_config.device_id)-1);
strncpy(_config.env_note, MQTT_DEFAULT_ENV, sizeof(_config.env_note)-1);
memset(_config.wifi_ssid, 0, sizeof(_config.wifi_ssid));
memset(_config.wifi_pass, 0, sizeof(_config.wifi_pass));
}
// 配置 WiFiManager
_wm.setDebugOutput(false); // 关闭调试输出 (避免干扰)
_wm.setConfigPortalTimeout(WIFI_CONFIG_TIMEOUT);
_wm.setConnectTimeout(WIFI_CONNECT_TIMEOUT);
_wm.setTitle("健康监测系统 - 设备配网");
_wm.setCustomHeadElement(
"<style>"
/* ===== 字体与全局 ===== */
"*{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 自动重连
WiFi.setAutoReconnect(true);
// 注册保存回调 (必须使用静态函数, 不能使用 lambda)
_wm.setSaveConfigCallback(_saveParamsCallback);
// 创建自定义参数
_param_mqtt_server = new WiFiManagerParameter(
"mqtt_server", "MQTT 服务器地址",
_config.mqtt_server, 40);
_param_mqtt_port = new WiFiManagerParameter(
"mqtt_port", "MQTT 端口",
_config.mqtt_port, 6);
_param_mqtt_user = new WiFiManagerParameter(
"mqtt_user", "MQTT 用户名",
_config.mqtt_user, 20);
_param_mqtt_pass = new WiFiManagerParameter(
"mqtt_pass", "MQTT 密码",
_config.mqtt_pass, 20);
_param_device_id = new WiFiManagerParameter(
"device_id", "设备名称",
_config.device_id, 20);
_param_env_note = new WiFiManagerParameter(
"env_note", "🏠 环境备注 (如: 主卧/客厅/老人房)",
_config.env_note, 20);
// 注册自定义参数
_wm.addParameter(_param_mqtt_server);
_wm.addParameter(_param_mqtt_port);
_wm.addParameter(_param_mqtt_user);
_wm.addParameter(_param_mqtt_pass);
_wm.addParameter(_param_device_id);
_wm.addParameter(_param_env_note);
// 设置 AP 模式回调 (静态函数)
_wm.setAPCallback(_apCallback);
// 尝试连接 WiFi
// 有配置 → 直接连
bool hasConfig = LittleFS.exists(CONFIG_FILE_DEVICE);
Serial.printf("[WiFi] 配置文件: %s\n", hasConfig ? "存在" : "不存在");
if (hasConfig) {
_connected = _connectWiFi();
Serial.printf("[WiFi] 自动连接: %s\n", _connected ? "成功" : "失败");
} else {
Serial.println(("[WiFi] 无配置, 跳过连接"));
_connected = false;
}
if (!_connected) {
// 如果自动连接失败, 启动配网页面
Serial.println(F("[WiFi] 自动连接失败, 启动配网模式..."));
_setLEDMode(2); // 快闪 = 配网模式
_connected = _connectWiFi(); // 内部会启动配网页面
}
if (_connected) {
_setLEDMode(3); // 常亮 = 已连接
_startPortal(); // 只开 AP
} else {
_setLEDMode(3); // 常亮
}
_lastCheckTime = millis();
@@ -220,29 +88,59 @@ bool WiFiMgmt::begin() {
// ============================================================
bool WiFiMgmt::loop() {
// 检测按钮长按
// --- Portal 模式 ---
if (_portalActive) {
if (_portalStopFlag) {
_stopPortal();
_portalStopFlag = false;
if (_pendingSSID.length() > 0) {
Serial.printf("[WiFi] 尝试连接: %s\n", _pendingSSID.c_str());
_connected = _tryConnect(_pendingSSID.c_str(), _pendingPass.c_str());
_pendingSSID = ""; _pendingPass = "";
if (_connected) {
_setLEDMode(3);
_provisionSuccess = true; // 标记配网成功
} else {
_setLEDMode(2);
_startPortal();
}
}
return _connected;
}
_dns->processNextRequest();
_server->handleClient();
delay(10);
return _connected;
}
// --- 正常运行 ---
if (_checkButtonLongPress()) {
Serial.println(F("[WiFi] ⚠️ 检测到长按, 清除配置并重启..."));
resetSettings();
Serial.println(("[WiFi] 长按, 进配网模式"));
_setLEDMode(2);
WiFi.disconnect();
_startPortal();
_connected = false;
return false;
}
// 检查 WiFi 状态
if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {
_lastCheckTime = millis();
if (WiFi.status() != WL_CONNECTED) {
wl_status_t s = WiFi.status();
if (s != WL_CONNECTED) {
if (_connected) {
_connected = false;
_setLEDMode(1); // 慢闪 = 未连接
Serial.println(F("[WiFi] ⚠️ WiFi 断开, 尝试重连..."));
_connectWiFi();
} else {
_setLEDMode(1);
Serial.printf("[WiFi] 断开! status=%d RSSI=%d\n", s, WiFi.RSSI());
}
// 不手动 reconnect(), 让 WiFi.setAutoReconnect(true) 自动处理
} else if (!_connected) {
_connected = true;
_setLEDMode(3); // 常亮 = 已连接
_setLEDMode(3);
Serial.printf("[WiFi] 已连接 RSSI=%d\n", WiFi.RSSI());
}
}
// LED 由独立 FreeRTOS 任务处理 (不受 startConfigPortal 阻塞影响)
return _connected;
}
@@ -251,128 +149,272 @@ bool WiFiMgmt::loop() {
// ============================================================
bool WiFiMgmt::_connectWiFi() {
// 检查是否有已保存的配置
// autoConnect 会先尝试连接, 失败后启动 AP 配网页面
_connected = _wm.autoConnect(WIFI_AP_SSID, WIFI_AP_PASSWORD);
if (_connected) {
Serial.println(F("[WiFi] ✅ 已连接!"));
Serial.printf(" WiFi名称: %s\n", WiFi.SSID().c_str());
Serial.printf(" IP地址: %s\n", WiFi.localIP().toString().c_str());
Serial.printf(" MAC地址: %s\n", WiFi.macAddress().c_str());
// 重新加载配置 (确保获取最新值)
// autoConnect 过程中如果有保存, 回调已写入 LittleFS
_loadConfig();
// 同步自定义参数 (如果配置是从 LittleFS 加载的)
_param_mqtt_server->setValue(_config.mqtt_server, sizeof(_config.mqtt_server));
_param_mqtt_port->setValue(_config.mqtt_port, sizeof(_config.mqtt_port));
_param_mqtt_user->setValue(_config.mqtt_user, sizeof(_config.mqtt_user));
_param_mqtt_pass->setValue(_config.mqtt_pass, sizeof(_config.mqtt_pass));
_param_device_id->setValue(_config.device_id, sizeof(_config.device_id));
_param_env_note->setValue(_config.env_note, sizeof(_config.env_note));
Serial.printf(" 设备名: %s\n", _config.device_id);
Serial.printf(" 环境: %s\n", _config.env_note);
Serial.printf(" MQTT服务器: %s:%s\n", _config.mqtt_server, _config.mqtt_port);
if (_config.wifi_ssid[0] == '\0') {
Serial.println(("[WiFi] 无已存WiFi密码"));
return false;
}
Serial.printf("[WiFi] 连接: %s\n", _config.wifi_ssid);
// 崩溃重启后 WiFi 模块可能残留旧状态, 彻底重置
WiFi.disconnect(true);
delay(200);
WiFi.mode(WIFI_STA);
delay(100);
WiFi.begin(_config.wifi_ssid, _config.wifi_pass);
int retry = 0;
while (WiFi.status() != WL_CONNECTED && retry < 40) { // 20s
delay(500); Serial.print("."); yield(); retry++;
}
Serial.println();
_connected = (WiFi.status() == WL_CONNECTED);
Serial.printf("[WiFi] 连接结果: %s status=%d\n", _connected ? "成功" : "失败", WiFi.status());
if (_connected) Serial.printf("[WiFi] SSID=%s IP=%s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
return _connected;
}
// ============================================================
// 配网页面
// ============================================================
void WiFiMgmt::startConfigPortal() {
Serial.println(F("[WiFi] 🔧 手动启动配网页面..."));
_setLEDMode(2); // 快闪 = 配网模式
_wm.startConfigPortal(WIFI_AP_SSID, WIFI_AP_PASSWORD);
_connected = (WiFi.status() == WL_CONNECTED);
if (_connected) {
_setLEDMode(3); // 常亮 = 已连接
bool WiFiMgmt::_tryConnect(const char* ssid, const char* pass) {
WiFi.mode(WIFI_STA);
WiFi.persistent(true); // 确保持久化 WiFi 密码到 NVS (断电不丢)
WiFi.begin(ssid, pass);
int retry = 0;
while (WiFi.status() != WL_CONNECTED && retry < 40) { // 20s
delay(500); Serial.print("."); retry++;
}
bool ok = (WiFi.status() == WL_CONNECTED);
Serial.printf("\n[WiFi] 连接结果: %s status=%d\n", ok ? "成功!" : "失败", WiFi.status());
return ok;
}
// ============================================================
// 重置设置
// 配网 Portal (AP + WebServer)
// ============================================================
void WiFiMgmt::startConfigPortal() {
_setLEDMode(2);
_startPortal();
}
void WiFiMgmt::_startPortal() {
if (_portalActive) return;
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
delay(200);
WiFi.mode(WIFI_AP);
WiFi.softAPConfig(IPAddress(192,168,4,1), IPAddress(192,168,4,1), IPAddress(255,255,255,0));
WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);
Serial.printf("[WiFi] AP: %s IP: 192.168.4.1\n", WIFI_AP_SSID);
// DNS 劫持: 所有域名 → 192.168.4.1 → 触发手机强制门户弹窗
_dns = new DNSServer();
_dns->start(53, "*", WiFi.softAPIP());
_server = new WebServer(80);
_server->on("/", HTTP_GET, [this]() { _handleRoot(); });
_server->on("/save", HTTP_POST, [this]() { _handleSave(); });
_server->on("/reset", HTTP_GET, [this]() { _handleReset(); });
_server->onNotFound( [this]() { _handleRoot(); });
_server->begin();
_portalActive = true;
_portalStartTime = millis();
Serial.println(("[WiFi] 手机连 AP 后浏览器打开 http://192.168.4.1"));
}
void WiFiMgmt::_stopPortal() {
if (!_portalActive) return;
if (_server) { _server->stop(); delete _server; _server = nullptr; }
if (_dns) { _dns->stop(); delete _dns; _dns = nullptr; }
WiFi.softAPdisconnect(true);
_portalActive = false;
}
// ============================================================
// Web 处理器
// ============================================================
void WiFiMgmt::_handleRoot() {
_server->send(200, "text/html; charset=utf-8", _htmlPage());
}
void WiFiMgmt::_handleSave() {
if (_server->hasArg("s")) {
_pendingSSID = _server->arg("s");
_pendingPass = _server->arg("p");
// 持久化 WiFi 密码到 LittleFS
strncpy(_config.wifi_ssid, _pendingSSID.c_str(), sizeof(_config.wifi_ssid)-1);
strncpy(_config.wifi_pass, _pendingPass.c_str(), sizeof(_config.wifi_pass)-1);
if (_server->hasArg("device_id")) strncpy(_config.device_id, _server->arg("device_id").c_str(), sizeof(_config.device_id)-1);
if (_server->hasArg("env_note")) strncpy(_config.env_note, _server->arg("env_note").c_str(), sizeof(_config.env_note)-1);
if (_server->hasArg("mqtt_server")) strncpy(_config.mqtt_server, _server->arg("mqtt_server").c_str(), sizeof(_config.mqtt_server)-1);
if (_server->hasArg("mqtt_port")) strncpy(_config.mqtt_port, _server->arg("mqtt_port").c_str(), sizeof(_config.mqtt_port)-1);
if (_server->hasArg("mqtt_user")) strncpy(_config.mqtt_user, _server->arg("mqtt_user").c_str(), sizeof(_config.mqtt_user)-1);
if (_server->hasArg("mqtt_pass")) strncpy(_config.mqtt_pass, _server->arg("mqtt_pass").c_str(), sizeof(_config.mqtt_pass)-1);
_saveConfig();
_server->send(200, "text/html; charset=utf-8",
"<!DOCTYPE html><html lang='zh-CN'><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1'>"
"<title>配置完成</title><style>body{font-family:sans-serif;background:#f3faf8;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}"
".box{text-align:center;background:#fff;padding:32px;border-radius:16px;box-shadow:0 2px 12px rgba(0,0,0,0.08);max-width:340px}"
"h1{color:#12a889;font-size:20px}.ok{font-size:48px;margin:16px}"
"p{color:#8aa09a;font-size:14px;line-height:1.8}"
"</style></head><body><div class='box'><div class='ok' id='icon'>⏳</div>"
"<h1 id='title'>配置已保存</h1><p id='msg'>设备正在连接 WiFi...<br>等待指示灯<b>常亮</b></p>"
"</div><script>"
"var n=0;setInterval(function(){n++;"
"if(n>8){document.getElementById('icon').textContent='✅';"
"document.getElementById('msg').innerHTML='请切换回您的 WiFi 网络<br><b>指示灯常亮</b>即连接成功';clearInterval()}"
"},2000);</script></body></html>");
_portalStopFlag = true;
return;
}
_handleRoot();
}
void WiFiMgmt::_handleReset() {
_server->send(200, "text/html; charset=utf-8",
"<!DOCTYPE html><html lang='zh-CN'><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>已重置</title>"
"<style>body{font-family:sans-serif;background:#f3faf8;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}"
".box{text-align:center;background:#fff;padding:32px;border-radius:16px;box-shadow:0 2px 12px rgba(0,0,0,0.08)}"
"h1{color:#f53f3f;font-size:18px}p{color:#8aa09a;font-size:14px}</style></head><body><div class='box'><h1>配置已清除</h1><p>设备正在重启...</p></div></body></html>");
delay(500);
_stopPortal();
resetSettings();
}
// ============================================================
// 配网 HTML 页面 (精简版)
// ============================================================
void WiFiMgmt::_updateWifiScan() {
_wifiScanCache = "";
int n = WiFi.scanNetworks(false, true);
for (int i = 0; i < n; i++) {
_wifiScanCache += "<option value='" + WiFi.SSID(i) + "'>" + WiFi.SSID(i) + " (" + String(WiFi.RSSI(i)) + "dBm)</option>";
}
if (n == 0) _wifiScanCache = "<option value=''>未发现 WiFi</option>";
WiFi.scanDelete();
_lastScanTime = millis();
}
String WiFiMgmt::_htmlPage() {
if (_wifiScanCache.length() == 0) _updateWifiScan();
String html;
html.reserve(4096);
html += "<!DOCTYPE html><html lang='zh-CN'><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'>";
html += "<meta name='theme-color' content='#12a889'><title>设备配网</title><style>";
html += ":root{--p:#12a889;--bg:#f3faf8;--cb:#fff;--ch:#eef8f5;--cg:#e9fff8;--t:#15201d;--t2:#4e6660;--t3:#8aa09a}";
html += "*{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',sans-serif;background:var(--bg);min-height:100vh;padding:12px 16px 24px;color:var(--t);font-size:14px;line-height:1.5}";
html += ".wrap{max-width:420px;margin:0 auto}.hdr{display:flex;align-items:center;justify-content:center;position:relative;padding:20px 0 16px}";
html += ".hdricon{width:56px;height:56px;border-radius:50%;background:var(--cg);display:flex;align-items:center;justify-content:center;font-size:28px;margin-right:14px;box-shadow:0 4px 12px rgba(18,168,137,.12)}";
html += "h1{font-size:20px;font-weight:700;color:var(--t)}h3{font-size:12px;font-weight:400;color:var(--t2);margin-top:4px}";
html += ".card{background:var(--cb);border-radius:16px;padding:20px;margin-bottom:16px;box-shadow:0 2px 12px rgba(0,0,0,.06)}";
html += ".chdr{display:flex;align-items:center;gap:12px;margin-bottom:18px}.chicon{width:36px;height:36px;border-radius:10px;background:var(--cg);display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0}";
html += ".ctitle{font-size:16px;font-weight:600;color:var(--t)}.csub{font-size:12px;color:var(--t3);margin-top:2px}";
html += ".field{margin-top:14px}.field:first-child{margin-top:0}label{display:block;font-size:14px;font-weight:500;color:var(--t2);margin-bottom:8px}";
html += ".iwrap{display:flex;align-items:center;background:var(--ch);border-radius:10px;border:1px solid transparent;transition:border-color .2s}";
html += ".iicon{padding:0 10px;font-size:16px;color:var(--t3)}input,select{flex:1;width:100%;padding:12px 14px;font-size:14px;border:none;background:transparent;color:var(--t);outline:none}";
html += ".iwrap:focus-within{border-color:var(--p);background:#fff}select{-webkit-appearance:none;padding-right:30px}";
html += ".gate{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-radius:12px;background:var(--cg);margin-bottom:14px;font-size:13px}";
html += ".glabel{color:var(--t2)}.gid{color:var(--p);font-weight:600}";
html += ".btns{display:flex;flex-direction:column;gap:12px;margin-top:8px}";
html += "button{width:100%;height:48px;border:none;border-radius:24px;font-size:15px;font-weight:600;color:#fff;background:var(--p);box-shadow:0 2px 6px rgba(18,168,137,.2);cursor:pointer}";
html += "button.danger{background:#fff;color:#f53f3f;border:1px solid #ffd6d6;box-shadow:none}";
html += ".ft{text-align:center;font-size:11px;color:var(--t3);padding:20px 0 6px}";
html += "@media(prefers-color-scheme:dark){body{background:#0f1f1c;color:#e6f0ee}.card{background:#182b27}.iwrap{background:#1f3530}.iwrap:focus-within{background:#243d38}}";
html += "</style></head><body><div class='wrap'>";
// Header + 提示
html += "<div class='hdr'><div class='hdricon'>📡</div><div><h1>观照-五劳无感 设备配网</h1><h3>WiFi 联网与参数配置</h3></div></div>";
html += "<div class='gate' style='justify-content:center'><span style='color:var(--p)'>💡 也可用 <b>ESP-Touch</b> App 一键配网</span></div>";
html += "<form method='POST' action='/save'>";
html += "<div class='card'><div class='chdr'><div class='chicon'>📶</div><div><div class='ctitle'>无线网络</div><div class='csub'>选择 WiFi 或使用 SmartConfig</div></div></div>";
html += "<div class='field'><label>WiFi 名称</label><div class='iwrap'><span class='iicon'>📡</span><select name='s'>";
html += _wifiScanCache;
html += "</select></div></div>";
html += "<div class='field'><label>WiFi 密码</label><div class='iwrap'><span class='iicon'>🔒</span><input type='password' name='p' placeholder='请输入 WiFi 密码'></div></div></div>";
html += "<div class='card'><div class='chdr'><div class='chicon'>⚙️</div><div><div class='ctitle'>设备基础信息</div><div class='csub'>唯一标识与安装位置</div></div></div>";
html += "<div class='field'><label>设备编号</label><div class='iwrap'><span class='iicon'>#</span><input type='text' name='device_id' value='";
html += _config.device_id;
html += "'></div></div>";
html += "<div class='field'><label>安装环境</label><div class='iwrap'><span class='iicon'>🏠</span><input type='text' name='env_note' placeholder='如: 主卧 / 客厅 / 老人房' value='";
html += _config.env_note;
html += "'></div></div></div>";
html += "<input type='hidden' name='mqtt_server' value='";
html += _config.mqtt_server;
html += "'><input type='hidden' name='mqtt_port' value='";
html += _config.mqtt_port;
html += "'><input type='hidden' name='mqtt_user' value='";
html += _config.mqtt_user;
html += "'><input type='hidden' name='mqtt_pass' value='";
html += _config.mqtt_pass;
html += "'>";
html += "<div class='btns'><button type='submit'>保存配置并重启连接</button></div></form>";
html += "<button class='danger' onclick=\"if(confirm('确定恢复出厂?'))location.href='/reset'\">恢复出厂配置</button>";
html += "<div class='ft'>Health Monitor · Device Provisioning</div></div></body></html>";
return html;
}
// ============================================================
// 重置
// ============================================================
void WiFiMgmt::resetSettings() {
Serial.println(F("[WiFi] 清除所有配置..."));
// 删除 LittleFS 中的配置文件
Serial.println(("[WiFi] 清除所有配置..."));
if (LittleFS.exists(CONFIG_FILE_WIFI)) LittleFS.remove(CONFIG_FILE_WIFI);
if (LittleFS.exists(CONFIG_FILE_MQTT)) LittleFS.remove(CONFIG_FILE_MQTT);
if (LittleFS.exists(CONFIG_FILE_DEVICE)) LittleFS.remove(CONFIG_FILE_DEVICE);
// 清除 WiFiManager 的配置
_wm.resetSettings();
// LED 快闪提示
for (int i = 0; i < 10; i++) {
digitalWrite(LED_WIFI_PIN, !digitalRead(LED_WIFI_PIN));
delay(100);
}
Serial.println(F("[WiFi] 🔄 正在重启..."));
WiFi.disconnect(true, true);
for (int i = 0; i < 10; i++) { digitalWrite(LED_WIFI_PIN, !digitalRead(LED_WIFI_PIN)); delay(100); }
Serial.println(("[WiFi] 正在重启..."));
delay(500);
ESP.restart();
}
// ============================================================
// 按钮长按检测
// 按钮
// ============================================================
bool WiFiMgmt::_checkButtonLongPress() {
bool pressed = (digitalRead(CONFIG_BUTTON_PIN) == LOW);
if (pressed && !_buttonPressed) {
// 按下
_buttonPressed = true;
_buttonPressTime = millis();
_buttonPressed = true; _buttonPressTime = millis();
} else if (!pressed && _buttonPressed) {
// 松开
_buttonPressed = false;
if (millis() - _buttonPressTime >= BUTTON_LONG_PRESS_MS) {
return true; // 长按确认
if (millis() - _buttonPressTime >= BUTTON_LONG_PRESS_MS) return true;
}
}
return false;
}
// ============================================================
// LittleFS 配置持久化
// LittleFS
// ============================================================
bool WiFiMgmt::_loadConfig() {
if (!LittleFS.exists(CONFIG_FILE_DEVICE)) {
return false;
}
if (!LittleFS.exists(CONFIG_FILE_DEVICE)) return false;
File file = LittleFS.open(CONFIG_FILE_DEVICE, "r");
if (!file) return false;
JsonDocument doc;
DeserializationError err = deserializeJson(doc, file);
file.close();
if (err) {
Serial.printf("[WiFi] JSON 解析失败: %s\n", err.c_str());
return false;
}
strncpy(_config.mqtt_server, doc["mqtt_server"] | MQTT_DEFAULT_SERVER, sizeof(_config.mqtt_server) - 1);
strncpy(_config.mqtt_port, doc["mqtt_port"] | MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port) - 1);
strncpy(_config.mqtt_user, doc["mqtt_user"] | MQTT_DEFAULT_USER, sizeof(_config.mqtt_user) - 1);
strncpy(_config.mqtt_pass, doc["mqtt_pass"] | MQTT_DEFAULT_PASS, sizeof(_config.mqtt_pass) - 1);
strncpy(_config.device_id, doc["device_id"] | MQTT_DEFAULT_DEVICE, sizeof(_config.device_id) - 1);
strncpy(_config.env_note, doc["env_note"] | MQTT_DEFAULT_ENV, sizeof(_config.env_note) - 1);
Serial.println(F("[WiFi] ✅ 设备配置已加载"));
Serial.printf(" 设备名: %s, 环境: %s\n", _config.device_id, _config.env_note);
if (err) return false;
strncpy(_config.mqtt_server, doc["mqtt_server"] | MQTT_DEFAULT_SERVER, sizeof(_config.mqtt_server)-1);
strncpy(_config.mqtt_port, doc["mqtt_port"] | MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port)-1);
strncpy(_config.mqtt_user, doc["mqtt_user"] | MQTT_DEFAULT_USER, sizeof(_config.mqtt_user)-1);
strncpy(_config.mqtt_pass, doc["mqtt_pass"] | MQTT_DEFAULT_PASS, sizeof(_config.mqtt_pass)-1);
strncpy(_config.device_id, doc["device_id"] | MQTT_DEFAULT_DEVICE, sizeof(_config.device_id)-1);
strncpy(_config.env_note, doc["env_note"] | MQTT_DEFAULT_ENV, sizeof(_config.env_note)-1);
strncpy(_config.wifi_ssid, doc["wifi_ssid"] | "", sizeof(_config.wifi_ssid)-1);
strncpy(_config.wifi_pass, doc["wifi_pass"] | "", sizeof(_config.wifi_pass)-1);
Serial.printf("[WiFi] 配置加载, 已存WiFi: %s\n", _config.wifi_ssid[0] ? _config.wifi_ssid : "(空)");
return true;
}
@@ -384,134 +426,44 @@ bool WiFiMgmt::_saveConfig() {
doc["mqtt_pass"] = _config.mqtt_pass;
doc["device_id"] = _config.device_id;
doc["env_note"] = _config.env_note;
doc["wifi_ssid"] = _config.wifi_ssid;
doc["wifi_pass"] = _config.wifi_pass;
File file = LittleFS.open(CONFIG_FILE_DEVICE, "w");
if (!file) {
Serial.println(F("[WiFi] ❌ 无法创建配置文件"));
return false;
}
if (serializeJson(doc, file) == 0) {
Serial.println(F("[WiFi] ❌ 配置写入失败"));
if (!file) return false;
if (serializeJson(doc, file) == 0) { file.close(); return false; }
file.close();
return false;
}
file.close();
Serial.println(F("[WiFi] ✅ 设备配置已保存"));
return true;
}
// ============================================================
// WiFiManager 保存参数回调
// Getter
// ============================================================
// ============================================================
// 静态回调函数 (WiFiManager 要求 C 函数指针)
// ============================================================
void WiFiMgmt::_apCallback(WiFiManager* wm) {
Serial.println(F("[WiFi] 🔧 进入配网模式"));
Serial.printf(" AP WiFi名称: %s\n", WIFI_AP_SSID);
Serial.printf(" AP 密码: %s\n", WIFI_AP_PASSWORD);
Serial.println(F(" 手机连接此 WiFi 即可打开配网页面 (自动弹出)"));
}
void WiFiMgmt::_saveParamsCallback() {
if (!_instance) return;
Serial.println(F("[WiFi] 💾 参数已保存"));
// 从 WiFiManager 参数中读取值
strncpy(_instance->_config.mqtt_server, _instance->_param_mqtt_server->getValue(),
sizeof(_instance->_config.mqtt_server) - 1);
strncpy(_instance->_config.mqtt_port, _instance->_param_mqtt_port->getValue(),
sizeof(_instance->_config.mqtt_port) - 1);
strncpy(_instance->_config.mqtt_user, _instance->_param_mqtt_user->getValue(),
sizeof(_instance->_config.mqtt_user) - 1);
strncpy(_instance->_config.mqtt_pass, _instance->_param_mqtt_pass->getValue(),
sizeof(_instance->_config.mqtt_pass) - 1);
strncpy(_instance->_config.device_id, _instance->_param_device_id->getValue(),
sizeof(_instance->_config.device_id) - 1);
strncpy(_instance->_config.env_note, _instance->_param_env_note->getValue(),
sizeof(_instance->_config.env_note) - 1);
_instance->_saveConfig();
Serial.println(F("[WiFi] 📋 新配置:"));
Serial.printf(" MQTT: %s:%s\n",
_instance->_config.mqtt_server,
_instance->_config.mqtt_port);
Serial.printf(" 设备: %s\n", _instance->_config.device_id);
Serial.printf(" 环境: %s\n", _instance->_config.env_note);
}
bool WiFiMgmt::isConnected() const { return _connected && (WiFi.status() == WL_CONNECTED); }
bool WiFiMgmt::justProvisioned() { bool v = _provisionSuccess; _provisionSuccess = false; return v; }
DeviceConfig WiFiMgmt::getConfig() const { return _config; }
const char* WiFiMgmt::getDeviceId() const { return _config.device_id; }
const char* WiFiMgmt::getEnvNote() const { return _config.env_note; }
// ============================================================
// Getter 方法
// LED
// ============================================================
bool WiFiMgmt::isConnected() const {
return _connected && (WiFi.status() == WL_CONNECTED);
}
DeviceConfig WiFiMgmt::getConfig() const {
return _config;
}
const char* WiFiMgmt::getDeviceId() const {
return _config.device_id;
}
const char* WiFiMgmt::getEnvNote() const {
return _config.env_note;
}
// ============================================================
// LED 模式控制 (线程安全, 仅写模式变量,由独立任务执行)
// ============================================================
void WiFiMgmt::_setLEDMode(uint8_t mode) {
_ledMode = mode; // uint8_t 赋值在 ESP32 上是原子的
}
// ============================================================
// LED 闪烁独立任务 (FreeRTOS)
// 独立于主循环startConfigPortal 阻塞期间也能正常闪烁
// ============================================================
void WiFiMgmt::_setLEDMode(uint8_t mode) { _ledMode = mode; }
void WiFiMgmt::_ledTaskFunc(void* param) {
const TickType_t fastMs = pdMS_TO_TICKS(300); // 快闪 = 配网模式
const TickType_t slowMs = pdMS_TO_TICKS(1000); // 慢闪 = 未连接
const TickType_t fastMs = pdMS_TO_TICKS(300);
const TickType_t slowMs = pdMS_TO_TICKS(1000);
while (true) {
if (!_instance) {
vTaskDelay(slowMs);
continue;
}
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;
case 0: digitalWrite(LED_WIFI_PIN, LOW); vTaskDelay(slowMs); break;
case 1: 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;
}
}
}