Files
health-wellness-device-2/ai/sessions/861a1013-414b-44f3-b2a2-ee22ec67de60/tool-results/bdihd8suq.txt
2026-08-12 09:22:28 +08:00

2420 lines
107 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
warning: in the working copy of 'include/config.h', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'include/mqtt_manager.h', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'include/radar_protocol.h', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'include/wifi_manager.h', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'src/wifi_manager.cpp', LF will be replaced by CRLF the next time Git touches it
diff --git a/include/config.h b/include/config.h
index a7207c1..6d2b281 100644
--- a/include/config.h
+++ b/include/config.h
@@ -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_ABD1 // ABD1 (心率/呼吸/睡眠)
+
+// --- 毫米波雷达 ---
#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
@@ -115,8 +120,9 @@
// 6. 上报配置 (协议规范 §4.1, §7.2)
// ============================================================
-#define REPORT_INTERVAL_MS 1000 // 遥测上报周期 1秒
+#define REPORT_INTERVAL_MS 1000 // 遥测上报周期 1秒 (有人时)
#define REPORT_QOS 0 // 遥测 QoS 0
+#define PRESENCE_HEARTBEAT_MS 60000 // 无人心跳间隔 60秒 (4G省流量)
#define STATUS_QOS 1 // 状态/响应 QoS 1
// ============================================================
@@ -130,6 +136,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
@@ -167,10 +174,11 @@
struct TelemetryData {
// --- 设备标识 ---
- char saleid[12]; // 站点编号 "10100001"
- char gateid[16]; // 网关编号 "10100001001"
- char meterId[8]; // 节点ID "ENV001"
+ char saleid[12]; // 站点编号 (已弃用)
+ char gateid[16]; // 网关编号
+ char meterId[8]; // 节点ID (已弃用, 新用 gateid+后缀)
char meterName[16]; // 设备型号 "RadarDHT11"
+ char deviceType[16]; // 设备类型枚举 bedroom/bathroom/...
// --- 环境数据 (DHT11) ---
float temp; // 温度 ℃
@@ -187,6 +195,8 @@ struct TelemetryData {
uint8_t heartRate; // 心率 bpm (无效=0)
uint8_t breathRate; // 呼吸 次/分 (无效=0)
uint8_t body; // 人体能量/体征值
+ uint8_t sleepState; // 睡眠状态 0=清醒 1=浅睡 2=深睡
+ uint8_t sleepScore; // 睡眠评分 0~100
bool radarValid; // 雷达数据有效性
bool vitalValid; // 心率呼吸有效性
diff --git a/include/mqtt_manager.h b/include/mqtt_manager.h
index e830043..fb51036 100644
--- a/include/mqtt_manager.h
+++ b/include/mqtt_manager.h
@@ -83,6 +83,9 @@ public:
/** 本地报警上报 status/{gateid}/alarm (§4.7 扩展) */
void publishLocalAlarm(uint8_t alarmType, bool triggered, const char* desc);
+ /** 无人极简报文 (仅 Presence+RadarValid, 省4G流量) */
+ void publishPresenceOnly(bool presence, bool radarValid, uint32_t sequence);
+
// ============================================================
// 回调注册 (协议规范 §5)
// ============================================================
diff --git a/include/radar_manager.h b/include/radar_manager.h
index 30935fd..adbdb08 100644
--- a/include/radar_manager.h
+++ b/include/radar_manager.h
@@ -21,6 +21,8 @@ struct RadarRawData {
uint8_t breathRate; // 呼吸
uint8_t body; // 体动/能量值
uint16_t distance; // 距离 cm
+ uint8_t sleepState; // 睡眠状态: 0=清醒 1=浅睡 2=深睡
+ uint8_t sleepScore; // 睡眠评分: 0~100
bool radarValid; // 雷达有效性
bool vitalValid; // 生命体征有效性
@@ -68,6 +70,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 +88,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
diff --git a/include/radar_protocol.h b/include/radar_protocol.h
index 205fc8e..302d625 100644
--- a/include/radar_protocol.h
+++ b/include/radar_protocol.h
@@ -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)
};
// ============================================================
@@ -73,6 +74,7 @@ enum SleepStatus : uint8_t {
SLEEP_AWAKE = 0, // 清醒
SLEEP_LIGHT = 1, // 浅睡
SLEEP_DEEP = 2, // 深睡
+ SLEEP_ABSENT = 3, // 离床/无人/无效
};
// ============================================================
diff --git a/include/wifi_manager.h b/include/wifi_manager.h
index 3e3a904..b6c327e 100644
--- a/include/wifi_manager.h
+++ b/include/wifi_manager.h
@@ -1,10 +1,10 @@
/**
* @file wifi_manager.h
- * @brief WiFi 配网管理器
+ * @brief WiFi 配网管理器 (AP + WebServer)
*
* 功能:
* - 自动连接已保存的 WiFi
- * - 配网失败/首次启动 → AP 模式 + 强制门户页面
+ * - 配网失败/首次启动 → AP 模式 + Captive Portal
* - 自定义参数: MQTT 服务器/端口/用户/密码、设备名称、环境备注
* - 长按按钮重置配置
* - LED 状态指示
@@ -15,8 +15,14 @@
#define WIFI_MANAGER_H
#include <Arduino.h>
-#include <WiFiManager.h>
+#include <WiFi.h>
+#include <WebServer.h>
+#include <DNSServer.h>
#include <LittleFS.h>
+#include <BLEDevice.h>
+#include <BLEServer.h>
+#include <BLEUtils.h>
+#include <BLE2902.h>
#include <ArduinoJson.h>
#include "config.h"
@@ -27,7 +33,10 @@ struct DeviceConfig {
char mqtt_user[20];
char mqtt_pass[20];
char device_id[20];
- char env_note[20]; // 环境备注 (如: "主卧", "客厅", "老人房")
+ char env_note[20]; // 环境备注 (房间名)
+ char env_type[16]; // 环境类型 bedroom/bathroom/...
+ char wifi_ssid[33]; // WiFi SSID
+ char wifi_pass[65]; // WiFi 密码
};
class WiFiMgmt {
@@ -38,100 +47,66 @@ 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 handleBLEProvision(const char* json); // BLE 配网回调
void resetSettings();
private:
- /**
- * @brief 尝试连接 WiFi
- */
- bool _connectWiFi();
-
- /**
- * @brief 设置 LED 模式 (线程安全, 无阻塞)
- * @param mode 0=灭 1=慢闪(断连1s) 2=快闪(配网300ms) 3=常亮(已连接)
- */
+ bool _connectWiFi(); // 仅尝试已保存凭据
+ bool _tryConnect(const char* ssid, const char* pass); // 尝试连接指定 WiFi
+ bool _loadConfig();
+ bool _saveConfig();
void _setLEDMode(uint8_t mode);
-
- /**
- * @brief LED 闪烁独立任务 (FreeRTOS, 不受主循环阻塞影响)
- */
static void _ledTaskFunc(void* param);
-
- /**
- * @brief 检测按钮长按
- */
bool _checkButtonLongPress();
- /**
- * @brief 加载/保存配置到 LittleFS
- */
- bool _loadConfig();
- bool _saveConfig();
-
- /**
- * @brief 保存 WiFi 参数回调 (WiFiManager 的回调)
- */
- static void _saveParamsCallback();
- static void _apCallback(WiFiManager* wm);
-
- WiFiManager _wm;
- DeviceConfig _config;
- bool _connected;
- uint32_t _lastCheckTime;
- uint32_t _buttonPressTime;
- bool _buttonPressed;
-
- // LED 独立任务 (不被 startConfigPortal 阻塞影响)
+ // Web 服务器
+ void _startPortal();
+ void _stopPortal();
+ void _handleRoot();
+ void _handleSave();
+ void _handleReset();
+ void _updateWifiScan(); // WiFi 扫描缓存
+ String _htmlPage(); // 返回配网 HTML 页面
+
+ // BLE 蓝牙配网
+ void _startBLE();
+ void _stopBLE();
+
+ 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;
+
+ // 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;
+ // BLE
+ BLEServer* _bleServer;
+ BLECharacteristic* _bleInfoChar; // READ: 设备信息
+ BLECharacteristic* _bleChar; // WRITE+NOTIFY: 配网
+ BLEAdvertising* _bleAdv;
};
#endif // WIFI_MANAGER_H
diff --git a/include/wm_strings_zh.h b/include/wm_strings_zh.h
deleted file mode 100644
index e3398b2..0000000
--- a/include/wm_strings_zh.h
+++ /dev/null
@@ -1,262 +0,0 @@
-/**
- * wm_strings_zh.h
- * 中文语言包 for WiFiManager
- * 基于 wm_strings_en.h 翻译
- *
- * WiFiManager, a library for the ESP8266/Arduino platform
- * for configuration of WiFi credentials using a Captive Portal
- *
- * @author Creator tzapu
- * @author tablatronix
- * @version 0.0.0
- * @license MIT
- */
-
-#ifndef _WM_STRINGS_ZH_H_
-#define _WM_STRINGS_ZH_H_
-
-// strings files must include a consts file!
-#include "wm_consts_en.h" // include constants, tokens, routes
-
-const char WM_LANGUAGE[] PROGMEM = "zh-CN"; // i18n lang code
-
-const char HTTP_HEAD_START[] PROGMEM = "<!DOCTYPE html>"
-"<html lang='zh-CN'><head>"
-"<meta name='format-detection' content='telephone=no'>"
-"<meta charset='UTF-8'>"
-"<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'/>"
-"<title>{v}</title>";
-
-const char HTTP_SCRIPT[] PROGMEM = "<script>function c(l){"
-"document.getElementById('s').value=l.getAttribute('data-ssid')||l.innerText||l.textContent;"
-"p = l.nextElementSibling.classList.contains('l');"
-"document.getElementById('p').disabled = !p;"
-"if(p)document.getElementById('p').focus();};"
-"function f() {var x = document.getElementById('p');x.type==='password'?x.type='text':x.type='password';}"
-"</script>";
-
-const char HTTP_HEAD_END[] PROGMEM = "</head><body class='{c}'><div class='wrap'>"; // {c} = _bodyclass
-
-const char HTTP_ROOT_MAIN[] PROGMEM = "<h1>{t}</h1><h3>{v}</h3>";
-
-const char * const HTTP_PORTAL_MENU[] PROGMEM = {
-"<form action='/wifi' method='get'><button>配置 WiFi</button></form><br/>\n", // MENU_WIFI
-"<form action='/0wifi' method='get'><button>配置 WiFi (不扫描)</button></form><br/>\n", // MENU_WIFINOSCAN
-"<form action='/info' method='get'><button>系统信息</button></form><br/>\n", // MENU_INFO
-"<form action='/param' method='get'><button>参数设置</button></form><br/>\n", // MENU_PARAM
-"<form action='/close' method='get'><button>关闭页面</button></form><br/>\n", // MENU_CLOSE
-"<form action='/restart' method='get'><button>重启设备</button></form><br/>\n", // MENU_RESTART
-"<form action='/exit' method='get'><button>退出配网</button></form><br/>\n", // MENU_EXIT
-"<form action='/erase' method='get'><button class='D'>清除配置</button></form><br/>\n", // MENU_ERASE
-"<form action='/update' method='get'><button>固件升级</button></form><br/>\n", // MENU_UPDATE
-"<hr><br/>" // MENU_SEP
-};
-
-const char HTTP_PORTAL_OPTIONS[] PROGMEM = "";
-const char HTTP_ITEM_QI[] PROGMEM = "<div role='img' aria-label='{r}%' title='{r}%' class='q q-{q} {i} {h}'></div>";
-const char HTTP_ITEM_QP[] PROGMEM = "<div class='q {h}'>{r}%</div>";
-const char HTTP_ITEM[] PROGMEM = "<div><a href='#p' onclick='c(this)' data-ssid='{V}'>{v}</a>{qi}{qp}</div>";
-
-const char HTTP_FORM_START[] PROGMEM = "<form method='POST' action='{v}'>";
-const char HTTP_FORM_WIFI[] PROGMEM = "<label for='s'>WiFi 名称 (SSID)</label><input id='s' name='s' maxlength='32' autocorrect='off' autocapitalize='none' placeholder='{v}'><br/><label for='p'>WiFi 密码</label><input id='p' name='p' maxlength='64' type='password' placeholder='{p}'><input type='checkbox' id='showpass' onclick='f()'> <label for='showpass'>显示密码</label><br/>";
-const char HTTP_FORM_WIFI_END[] PROGMEM = "";
-const char HTTP_FORM_STATIC_HEAD[] PROGMEM = "<hr><br/>";
-const char HTTP_FORM_END[] PROGMEM = "<br/><br/><button type='submit'>保存配置</button></form>";
-const char HTTP_FORM_LABEL[] PROGMEM = "<label for='{i}'>{t}</label>";
-const char HTTP_FORM_PARAM_HEAD[] PROGMEM = "<hr><br/>";
-const char HTTP_FORM_PARAM[] PROGMEM = "<br/><input id='{i}' name='{n}' maxlength='{l}' value='{v}' {c}>\n";
-
-const char HTTP_SCAN_LINK[] PROGMEM = "<br/><form action='/wifi?refresh=1' method='POST'><button name='refresh' value='1'>重新扫描</button></form>";
-const char HTTP_SAVED[] PROGMEM = "<div class='msg'>正在保存配置<br/>尝试连接 WiFi...<br/>如果连接失败,请重新连接 AP 再试一次</div>";
-const char HTTP_PARAMSAVED[] PROGMEM = "<div class='msg S'>配置已保存<br/></div>";
-const char HTTP_END[] PROGMEM = "</div></body></html>";
-const char HTTP_ERASEBTN[] PROGMEM = "<br/><form action='/erase' method='get'><button class='D'>清除 WiFi 配置</button></form>";
-const char HTTP_UPDATEBTN[] PROGMEM = "<br/><form action='/update' method='get'><button>固件升级</button></form>";
-const char HTTP_BACKBTN[] PROGMEM = "<hr><br/><form action='/' method='get'><button>返回首页</button></form>";
-
-const char HTTP_STATUS_ON[] PROGMEM = "<div class='msg S'><strong>已连接</strong> {v}<br/><em><small>IP 地址: {i}</small></em></div>";
-const char HTTP_STATUS_OFF[] PROGMEM = "<div class='msg {c}'><strong>未连接</strong> {v}{r}</div>";
-const char HTTP_STATUS_OFFPW[] PROGMEM = "<br/>WiFi 密码错误";
-const char HTTP_STATUS_OFFNOAP[] PROGMEM = "<br/>未找到该 WiFi";
-const char HTTP_STATUS_OFFFAIL[] PROGMEM = "<br/>无法连接";
-const char HTTP_STATUS_NONE[] PROGMEM = "<div class='msg'>未设置 WiFi</div>";
-const char HTTP_BR[] PROGMEM = "<br/>";
-
-const char HTTP_STYLE[] PROGMEM = "<style>"
-".c,body{text-align:center;font-family:'PingFang SC','Microsoft YaHei','Helvetica Neue',sans-serif}div,input,select{padding:5px;font-size:1em;margin:5px 0;box-sizing:border-box}"
-"input,button,select,.msg{border-radius:.3rem;width: 100%}input[type=radio],input[type=checkbox]{width:auto}"
-"button,input[type='button'],input[type='submit']{cursor:pointer;border:0;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%}"
-"input[type='file']{border:1px solid #1fa3ec}"
-".wrap {text-align:left;display:inline-block;min-width:260px;max-width:500px}"
-"a{color:#000;font-weight:700;text-decoration:none}a:hover{color:#1fa3ec;text-decoration:underline}"
-".q{height:16px;margin:0;padding:0 5px;text-align:right;min-width:38px;float:right}.q.q-0:after{background-position-x:0}.q.q-1:after{background-position-x:-16px}.q.q-2:after{background-position-x:-32px}.q.q-3:after{background-position-x:-48px}.q.q-4:after{background-position-x:-64px}.q.l:before{background-position-x:-80px;padding-right:5px}.ql .q{float:left}.q:after,.q:before{content:'';width:16px;height:16px;display:inline-block;background-repeat:no-repeat;background-position: 16px 0;"
-"background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAAAQCAMAAADeZIrLAAAAJFBMVEX///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADHJj5lAAAAC3RSTlMAIjN3iJmqu8zd7vF8pzcAAABsSURBVHja7Y1BCsAwCASNSVo3/v+/BUEiXnIoXkoX5jAQMxTHzK9cVSnvDxwD8bFx8PhZ9q8FmghXBhqA1faxk92PsxvRc2CCCFdhQCbRkLoAQ3q/wWUBqG35ZxtVzW4Ed6LngPyBU2CobdIDQ5oPWI5nCUwAAAAASUVORK5CYII=');}"
-"@media (-webkit-min-device-pixel-ratio: 2),(min-resolution: 192dpi){.q:before,.q:after {"
-"background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALwAAAAgCAMAAACfM+KhAAAALVBMVEX///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAOrOgAAAADnRSTlMAESIzRGZ3iJmqu8zd7gKjCLQAAACmSURBVHgB7dDBCoMwEEXRmKlVY3L//3NLhyzqIqSUggy8uxnhCR5Mo8xLt+14aZ7wwgsvvPA/ofv9+44334UXXngvb6XsFhO/VoC2RsSv9J7x8BnYLW+AjT56ud/uePMdb7IP8Bsc/e7h8Cfk912ghsNXWPpDC4hvN+D1560A1QPORyh84VKLjjdvfPFm++i9EWq0348XXnjhhT+4dIbCW+WjZim9AKk4UZMnnCEuAAAAAElFTkSuQmCC');"
-"background-size: 95px 16px;}}"
-".msg{padding:20px;margin:20px 0;border:1px solid #eee;border-left-width:5px;border-left-color:#777}.msg h4{margin-top:0;margin-bottom:5px}.msg.P{border-left-color:#1fa3ec}.msg.P h4{color:#1fa3ec}.msg.D{border-left-color:#dc3630}.msg.D h4{color:#dc3630}.msg.S{border-left-color: #5cb85c}.msg.S h4{color: #5cb85c}"
-"dt{font-weight:bold}dd{margin:0;padding:0 0 0.5em 0;min-height:12px}"
-"td{vertical-align: top;}"
-".h{display:none}"
-"button{transition: 0s opacity;transition-delay: 3s;transition-duration: 0s;cursor: pointer}"
-"button.D{background-color:#dc3630}"
-"button:active{opacity:50% !important;cursor:wait;transition-delay: 0s}"
-"body.invert,body.invert a,body.invert h1 {background-color:#060606;color:#fff;}"
-"body.invert .msg{color:#fff;background-color:#282828;border-top:1px solid #555;border-right:1px solid #555;border-bottom:1px solid #555;}"
-"body.invert .q[role=img]{-webkit-filter:invert(1);filter:invert(1);}"
-":disabled {opacity: 0.5;}"
-"</style>";
-
-#ifndef WM_NOHELP
-const char HTTP_HELP[] PROGMEM =
- "<br/><h3>可用页面</h3><hr>"
- "<table class='table'>"
- "<thead><tr><th>页面</th><th>功能</th></tr></thead><tbody>"
- "<tr><td><a href='/'>/</a></td>"
- "<td>主菜单页面</td></tr>"
- "<tr><td><a href='/wifi'>/wifi</a></td>"
- "<td>显示 WiFi 扫描结果并配置连接 (/0wifi 不扫描)</td></tr>"
- "<tr><td><a href='/wifisave'>/wifisave</a></td>"
- "<td>保存 WiFi 配置并连接设备</td></tr>"
- "<tr><td><a href='/param'>/param</a></td>"
- "<td>参数设置页面</td></tr>"
- "<tr><td><a href='/info'>/info</a></td>"
- "<td>系统信息页面</td></tr>"
- "<tr><td><a href='/u'>/u</a></td>"
- "<td>固件 OTA 升级</td></tr>"
- "<tr><td><a href='/close'>/close</a></td>"
- "<td>关闭弹出窗口,配网服务保持运行</td></tr>"
- "<tr><td>/exit</td>"
- "<td>退出配网模式,配网服务将关闭</td></tr>"
- "<tr><td>/restart</td>"
- "<td>重启设备</td></tr>"
- "<tr><td>/erase</td>"
- "<td>清除 WiFi 配置并重启。清除后设备不会自动连接,需重新配网</td></tr>"
- "</table>"
- "<p/>GitHub <a href='https://github.com/tzapu/WiFiManager'>https://github.com/tzapu/WiFiManager</a>.";
-#else
-const char HTTP_HELP[] PROGMEM = "";
-#endif
-
-const char HTTP_UPDATE[] PROGMEM = "上传新固件<br/><form method='POST' action='u' enctype='multipart/form-data' onchange=\"(function(el){document.getElementById('uploadbin').style.display = el.value=='' ? 'none' : 'initial';})(this)\"><input type='file' name='update' accept='.bin,application/octet-stream'><button id='uploadbin' type='submit' class='h D'>开始升级</button></form><small><a href='http://192.168.4.1/update' target='_blank'>* 如果在配网页面内无法使用,请在浏览器中打开 http://192.168.4.1</a><small>";
-const char HTTP_UPDATE_FAIL[] PROGMEM = "<div class='msg D'><strong>升级失败!</strong><Br/>请重启设备后重试</div>";
-const char HTTP_UPDATE_SUCCESS[] PROGMEM = "<div class='msg S'><strong>升级成功!</strong><Br/>设备正在重启...</div>";
-
-#ifdef WM_JSTEST
-const char HTTP_JS[] PROGMEM =
-"<script>function postAjax(url, data, success) {"
-" var params = typeof data == 'string' ? data : Object.keys(data).map("
-" function(k){ return encodeURIComponent(k) + '=' + encodeURIComponent(data[k]) }"
-" ).join('&');"
-" var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject(\"Microsoft.XMLHTTP\");"
-" xhr.open('POST', url);"
-" xhr.onreadystatechange = function() {"
-" if (xhr.readyState>3 && xhr.status==200) { success(xhr.responseText); }"
-" };"
-" xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');"
-" xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');"
-" xhr.send(params);"
-" return xhr;}"
-"postAjax('/status', 'p1=1&p2=Hello+World', function(data){ console.log(data); });"
-"postAjax('/status', { p1: 1, p2: 'Hello World' }, function(data){ console.log(data); });"
-"</script>";
-#endif
-
-// Info html
-#ifdef ESP32
- const char HTTP_INFO_esphead[] PROGMEM = "<h3>ESP32 信息</h3><hr><dl>";
- const char HTTP_INFO_chiprev[] PROGMEM = "<dt>芯片版本</dt><dd>{1}</dd>";
- const char HTTP_INFO_lastreset[] PROGMEM = "<dt>上次复位原因</dt><dd>CPU0: {1}<br/>CPU1: {2}</dd>";
- const char HTTP_INFO_aphost[] PROGMEM = "<dt>AP 主机名</dt><dd>{1}</dd>";
- const char HTTP_INFO_psrsize[] PROGMEM = "<dt>PSRAM 大小</dt><dd>{1} 字节</dd>";
- const char HTTP_INFO_temp[] PROGMEM = "<dt>芯片温度</dt><dd>{1}°C / {2}°F</dd>";
- const char HTTP_INFO_hall[] PROGMEM = "<dt>霍尔传感器</dt><dd>{1}</dd>";
-#else
- const char HTTP_INFO_esphead[] PROGMEM = "<h3>ESP8266 信息</h3><hr><dl>";
- const char HTTP_INFO_fchipid[] PROGMEM = "<dt>Flash 芯片 ID</dt><dd>{1}</dd>";
- const char HTTP_INFO_corever[] PROGMEM = "<dt>Core 版本</dt><dd>{1}</dd>";
- const char HTTP_INFO_bootver[] PROGMEM = "<dt>Boot 版本</dt><dd>{1}</dd>";
- const char HTTP_INFO_lastreset[] PROGMEM = "<dt>上次复位原因</dt><dd>{1}</dd>";
- const char HTTP_INFO_flashsize[] PROGMEM = "<dt>实际 Flash 大小</dt><dd>{1} 字节</dd>";
-#endif
-
-const char HTTP_INFO_memsmeter[] PROGMEM = "<br/><progress value='{1}' max='{2}'></progress></dd>";
-const char HTTP_INFO_memsketch[] PROGMEM = "<dt>内存 - 程序大小</dt><dd>已用 / 总计<br/>{1} / {2} 字节";
-const char HTTP_INFO_freeheap[] PROGMEM = "<dt>内存 - 可用堆</dt><dd>{1} 字节</dd>";
-const char HTTP_INFO_wifihead[] PROGMEM = "<br/><h3>WiFi 信息</h3><hr>";
-const char HTTP_INFO_uptime[] PROGMEM = "<dt>运行时间</dt><dd>{1} 分 {2} 秒</dd>";
-const char HTTP_INFO_chipid[] PROGMEM = "<dt>芯片 ID</dt><dd>{1}</dd>";
-const char HTTP_INFO_idesize[] PROGMEM = "<dt>Flash 大小</dt><dd>{1} 字节</dd>";
-const char HTTP_INFO_sdkver[] PROGMEM = "<dt>SDK 版本</dt><dd>{1}</dd>";
-const char HTTP_INFO_cpufreq[] PROGMEM = "<dt>CPU 频率</dt><dd>{1}MHz</dd>";
-const char HTTP_INFO_apip[] PROGMEM = "<dt>AP IP 地址</dt><dd>{1}</dd>";
-const char HTTP_INFO_apmac[] PROGMEM = "<dt>AP MAC 地址</dt><dd>{1}</dd>";
-const char HTTP_INFO_apssid[] PROGMEM = "<dt>AP SSID</dt><dd>{1}</dd>";
-const char HTTP_INFO_apbssid[] PROGMEM = "<dt>BSSID</dt><dd>{1}</dd>";
-const char HTTP_INFO_stassid[] PROGMEM = "<dt>Station SSID</dt><dd>{1}</dd>";
-const char HTTP_INFO_staip[] PROGMEM = "<dt>Station IP</dt><dd>{1}</dd>";
-const char HTTP_INFO_stagw[] PROGMEM = "<dt>网关地址</dt><dd>{1}</dd>";
-const char HTTP_INFO_stasub[] PROGMEM = "<dt>子网掩码</dt><dd>{1}</dd>";
-const char HTTP_INFO_dnss[] PROGMEM = "<dt>DNS 服务器</dt><dd>{1}</dd>";
-const char HTTP_INFO_host[] PROGMEM = "<dt>主机名</dt><dd>{1}</dd>";
-const char HTTP_INFO_stamac[] PROGMEM = "<dt>Station MAC</dt><dd>{1}</dd>";
-const char HTTP_INFO_conx[] PROGMEM = "<dt>连接状态</dt><dd>{1}</dd>";
-const char HTTP_INFO_autoconx[] PROGMEM = "<dt>自动连接</dt><dd>{1}</dd>";
-
-const char HTTP_INFO_aboutver[] PROGMEM = "<dt>WiFiManager 版本</dt><dd>{1}</dd>";
-const char HTTP_INFO_aboutarduino[] PROGMEM = "<dt>Arduino 版本</dt><dd>{1}</dd>";
-const char HTTP_INFO_aboutsdk[] PROGMEM = "<dt>ESP-SDK/IDF 版本</dt><dd>{1}</dd>";
-const char HTTP_INFO_aboutdate[] PROGMEM = "<dt>构建日期</dt><dd>{1}</dd>";
-
-// ============================================================
-// 文本常量
-// ============================================================
-const char S_brand[] PROGMEM = "健康监测配网";
-const char S_debugPrefix[] PROGMEM = "*wm:";
-const char S_y[] PROGMEM = "是";
-const char S_n[] PROGMEM = "否";
-const char S_enable[] PROGMEM = "已启用";
-const char S_disable[] PROGMEM = "已禁用";
-const char S_GET[] PROGMEM = "GET";
-const char S_POST[] PROGMEM = "POST";
-const char S_NA[] PROGMEM = "未知";
-const char S_passph[] PROGMEM = "********";
-const char S_titlewifisaved[] PROGMEM = "WiFi 已保存";
-const char S_titlewifisettings[] PROGMEM = "设置已保存";
-const char S_titlewifi[] PROGMEM = "配置 WiFi";
-const char S_titleinfo[] PROGMEM = "系统信息";
-const char S_titleparam[] PROGMEM = "参数设置";
-const char S_titleparamsaved[] PROGMEM = "设置已保存";
-const char S_titleexit[] PROGMEM = "退出配网";
-const char S_titlereset[] PROGMEM = "重置";
-const char S_titleerase[] PROGMEM = "清除配置";
-const char S_titleclose[] PROGMEM = "关闭页面";
-const char S_options[] PROGMEM = "选项";
-const char S_nonetworks[] PROGMEM = "未发现 WiFi 网络,请点击刷新重新扫描。";
-const char S_staticip[] PROGMEM = "静态 IP";
-const char S_staticgw[] PROGMEM = "静态网关";
-const char S_staticdns[] PROGMEM = "静态 DNS";
-const char S_subnet[] PROGMEM = "子网掩码";
-const char S_exiting[] PROGMEM = "正在退出配网模式";
-const char S_resetting[] PROGMEM = "设备将在几秒后重启";
-const char S_closing[] PROGMEM = "可以关闭此页面,配网服务将继续运行";
-const char S_error[] PROGMEM = "发生错误";
-const char S_notfound[] PROGMEM = "页面未找到\n\n";
-const char S_uri[] PROGMEM = "URI: ";
-const char S_method[] PROGMEM = "\n请求方法: ";
-const char S_args[] PROGMEM = "\n参数: ";
-const char S_parampre[] PROGMEM = "param_";
-
-// debug strings
-const char D_HR[] PROGMEM = "--------------------";
-
-// softap ssid default prefix
-#ifdef ESP8266
- const char S_ssidpre[] PROGMEM = "ESP";
-#elif defined(ESP32)
- const char S_ssidpre[] PROGMEM = "ESP32";
-#else
- const char S_ssidpre[] PROGMEM = "WM";
-#endif
-
-#endif
diff --git a/platformio.ini b/platformio.ini
index 1c12f78..250f8dd 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -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 = COM18
diff --git a/src/alarm_manager.cpp b/src/alarm_manager.cpp
index 7bae9d0..fbeb265 100644
--- a/src/alarm_manager.cpp
+++ b/src/alarm_manager.cpp
@@ -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,29 +148,29 @@ 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; // 持续
+ beeps = 0; onMs = 500; offMs = 500; pauseMs = 0; // 持续
break;
}
}
@@ -277,8 +277,13 @@ void AlarmManager::executeRestore(const AlarmCommand& cmd) {
Serial.printf("[告警] ☁️ 恢复: %s\n", cmd.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 (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);
}
}
}
diff --git a/src/button_manager.cpp b/src/button_manager.cpp
index 8b9fdce..bda20e7 100644
--- a/src/button_manager.cpp
+++ b/src/button_manager.cpp
@@ -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);
diff --git a/src/main.cpp b/src/main.cpp
index d85963b..42d9d3e 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -49,7 +49,14 @@ bool g_localDhtFaultActive = false;
uint8_t g_tempAbnCnt = 0;
uint8_t g_humiAbnCnt = 0;
uint8_t g_radarTimeoutCnt = 0;
-#define ALARM_HYSTERESIS 3 // 连续3次确认才触发
+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);
@@ -201,11 +222,13 @@ TelemetryData buildTelemetry(const RadarRawData& radar) {
memset(&t, 0, sizeof(t));
// 设备标识
- strncpy(t.saleid, SALEID_DEFAULT, sizeof(t.saleid) - 1);
strncpy(t.gateid, g_mqtt.getGateId(), sizeof(t.gateid) - 1);
- strncpy(t.meterId, METER_ID_DEFAULT, sizeof(t.meterId) - 1);
strncpy(t.meterName, METER_NAME_DEFAULT, sizeof(t.meterName) - 1);
+ // 环境类型: 从配网配置读取 (默认 bedroom)
+ DeviceConfig cfg = g_wifi.getConfig();
+ strncpy(t.deviceType, cfg.env_type[0] ? cfg.env_type : "bedroom", sizeof(t.deviceType)-1);
+
// DHT11
t.temp = g_sensor.getTemp();
t.humi = g_sensor.getHumi();
@@ -219,6 +242,8 @@ TelemetryData buildTelemetry(const RadarRawData& radar) {
t.heartRate = radar.heartRate;
t.breathRate = radar.breathRate;
t.body = radar.body;
+ t.sleepState = radar.sleepState;
+ t.sleepScore = radar.sleepScore;
t.radarValid = radar.radarValid;
t.vitalValid = radar.vitalValid;
@@ -250,54 +275,105 @@ void onLocalAlarmChange(uint8_t type, bool triggered, const char* desc) {
// ============================================================
void checkLocalAlarms(const RadarRawData& radar) {
+ // 环境类型 → 报警开关
+ const char* env = g_wifi.getConfig().env_type;
+ bool enFall = (strcmp(env, "bathroom") == 0 || strcmp(env, "hallway") == 0 || strcmp(env, "livingroom") == 0);
+ bool enVital = (strcmp(env, "bedroom") == 0);
+ bool enWater = (strcmp(env, "bathroom") == 0);
+ bool enEnv = (strcmp(env, "bedroom") == 0 || strcmp(env, "balcony") == 0 || strcmp(env, "kitchen") == 0);
+
// ─── 前置条件: 有人才检测人体生命体征 ───
bool humanPresent = radar.presence && radar.radarValid;
- // ─── ★★★★★ 人身安全: 跌倒 (立即触发, 无防抖) ───
- if (radar.fall && humanPresent && !g_localFallActive) {
- g_localFallActive = true;
- g_alarm.triggerLocal(AlarmManager::ALARM_FALL, "雷达检测到跌倒");
- } else if (!radar.fall && g_localFallActive) {
- g_localFallActive = false;
- g_alarm.clearLocal(AlarmManager::ALARM_FALL);
+ // ─── ★★★★★ 人身安全: 跌倒 (仅浴室/走廊/客厅) ───
+ if (enFall && 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 {
+ 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次, 30s) ───
+#if RADAR_TYPE != RADAR_TYPE_AFD1
+ if (enVital && millis() > 30000 && 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);
+ 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 {
+ 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 (enVital && millis() > 30000 && 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);
+ 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 {
+ 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
- // ─── ★★★ 环境安全: 水浸 ───
+ // ─── ★★★ 环境安全: 水浸 (仅浴室) ───
+ if (enWater) {
bool waterLeak = !g_sensor.getRainDO() && g_sensor.isRainValid();
if (waterLeak && !g_localWaterActive) {
g_localWaterActive = true;
@@ -306,9 +382,10 @@ void checkLocalAlarms(const RadarRawData& radar) {
g_localWaterActive = false;
g_alarm.clearLocal(AlarmManager::ALARM_WATER_LEAK);
}
+ } // enWater
- // ─── ★★ 环境舒适: 温度异常 (防抖3次) ───
- if (g_sensor.isDhtValid()) {
+ // ─── ★★ 环境舒适: 温湿度异常 (仅卧室/阳台/厨房, 防抖3次, 30s) ───
+ if (enEnv && millis() > 30000 && g_sensor.isDhtValid()) {
float temp = g_sensor.getTemp();
bool tempAbn = (temp < TEMP_LOW || temp > TEMP_HIGH);
if (tempAbn) {
@@ -345,27 +422,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 +516,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 +565,9 @@ void setup() {
// 8. 传感器
g_sensor.begin();
+ // 8. 等雷达 + 电源稳定
+ delay(8000);
+
// 9. 雷达任务 (核心0)
xTaskCreatePinnedToCore(
radarTask, "RadarTask",
@@ -511,6 +605,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,34 +624,57 @@ 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("[系统] ⚠️ 雷达数据超时"));
}
}
// --- 5. 报警模块更新 (定时器/闪烁) ---
g_alarm.loop();
- // --- 6. 遥测上报 (1秒周期) ---
- if (now - g_lastReportTime >= REPORT_INTERVAL_MS) {
- g_lastReportTime = now;
+ // --- 6. 遥测上报 ---
+ // 有人时: 每秒全量上报
+ // 无人时: 仅状态变化时发极简报文 + 60秒心跳 (省4G流量)
+ static bool lastPresence = false;
+ static uint32_t lastHeartbeat = 0; // 无人心跳计时
+
+ if (radar.presence) {
+ // ── 有人: 每秒完整上报 ──
+ if (now - g_lastReportTime >= REPORT_INTERVAL_MS) {
+ g_lastReportTime = now;
+ TelemetryData tele = buildTelemetry(radar);
+ g_mqtt.publishAllPoints(tele, g_sequence);
+ g_sequence++;
+ lastHeartbeat = now; // 有人时重置心跳计时
+
+ printDebug(tele);
+ g_display.update(tele, wifiOk, g_alarm.isBuzzerOn(),
+ g_alarm.hasActiveAlarm(), g_alarm.getActiveAlarmStr());
+ }
+ } else {
+ // ── 无人: 状态翻转发一次极简报文, 之后每60秒心跳 ──
+ if (lastPresence != radar.presence || now - lastHeartbeat >= PRESENCE_HEARTBEAT_MS) {
+ g_mqtt.publishPresenceOnly(radar.presence, radar.radarValid, g_sequence);
+ g_sequence++;
+ lastHeartbeat = now;
+
+ // 调试输出
+ Serial.printf("[上报] 无人极简报文 (Presence=0) 序号:%d\n", g_sequence - 1);
+ }
+ // OLED 还是照常刷新 (显示无人状态)
TelemetryData tele = buildTelemetry(radar);
- g_mqtt.publishAllPoints(tele, g_sequence);
- g_sequence++;
-
- // 调试输出 + OLED 显示
- printDebug(tele);
g_display.update(tele, wifiOk, g_alarm.isBuzzerOn(),
g_alarm.hasActiveAlarm(), g_alarm.getActiveAlarmStr());
}
+ lastPresence = radar.presence;
// --- 7. MQTT 配网状态 (WiFiManager 触发时) ---
// WiFiManager 的 captive portal 启动/关闭时更新状态
diff --git a/src/mqtt_manager.cpp b/src/mqtt_manager.cpp
index 01afb19..e307c16 100644
--- a/src/mqtt_manager.cpp
+++ b/src/mqtt_manager.cpp
@@ -178,20 +178,23 @@ void MQTTManager::publishAllPoints(const TelemetryData& d, uint32_t sequence) {
snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
}
- // 构建 JSON (协议规范 §4.1)
+ // 构建 JSON (协议规范 V0.08)
JsonDocument doc;
- doc["saleid"] = d.saleid;
doc["gateid"] = d.gateid;
doc["type"] = "report";
doc["time"] = timeStr;
doc["sequence"] = String(sequence);
doc["source"] = "da";
+ // meter.id = gateid + 后缀 (如 10100001001 → 10100001001001)
+ String meterId = String(d.gateid) + "001";
+
JsonArray meter = doc["meter"].to<JsonArray>();
JsonObject m = meter.add<JsonObject>();
- m["id"] = d.meterId;
+ m["id"] = meterId;
m["status"] = "1";
m["name"] = d.meterName;
+ m["type"] = d.deviceType; // 设备类型枚举
JsonObject v = m["values"].to<JsonObject>();
// 环境数据
@@ -205,6 +208,8 @@ void MQTTManager::publishAllPoints(const TelemetryData& d, uint32_t sequence) {
v["Resident"] = d.resident ? "1" : "0";
// 生命体征
v["Body"] = String(d.body);
+ v["SleepState"] = String(d.sleepState);
+ v["SleepScore"] = String(d.sleepScore);
v["HeartRate"] = String(d.heartRate);
v["BreathRate"] = String(d.breathRate);
v["RadarValid"] = d.radarValid ? "1" : "0";
@@ -416,6 +421,47 @@ void MQTTManager::publishLocalAlarm(uint8_t alarmType, bool triggered, const cha
_mqtt.publish(topic.c_str(), payload.c_str());
}
+// ============================================================
+// 上行: 无人极简报文 (省4G流量, 仅 Presence + RadarValid)
+// ============================================================
+
+void MQTTManager::publishPresenceOnly(bool presence, bool radarValid, uint32_t sequence) {
+ 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");
+ }
+
+ // 极简 JSON: ~120 字节 vs 完整报文 ~500 字节
+ JsonDocument doc;
+ doc["gateid"] = _gateid;
+ doc["type"] = "report";
+ doc["time"] = timeStr;
+ doc["sequence"] = String(sequence);
+ doc["source"] = "da";
+
+ String meterId = String(_gateid) + "001";
+
+ JsonArray meter = doc["meter"].to<JsonArray>();
+ JsonObject m = meter.add<JsonObject>();
+ m["id"] = meterId;
+ m["status"] = "1";
+ m["name"] = METER_NAME_DEFAULT;
+
+ JsonObject v = m["values"].to<JsonObject>();
+ v["Presence"] = presence ? "1" : "0";
+ v["RadarValid"] = radarValid ? "1" : "0";
+
+ String payload;
+ serializeJson(doc, payload);
+
+ _mqtt.publish("report/allpoints", payload.c_str());
+}
+
// ============================================================
// 回调注册
// ============================================================
diff --git a/src/radar_manager.cpp b/src/radar_manager.cpp
index ccd534f..1884d23 100644
--- a/src/radar_manager.cpp
+++ b/src/radar_manager.cpp
@@ -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);
- _data.motion = (frame.data[1] == 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) {
@@ -333,16 +379,15 @@ void RadarManager::_extractSleep(const RadarFrame& frame) {
}
if (frame.data_length >= 3) {
- // data[0]: 入床/离床
- // data[1]: 睡眠状态
- // data[2]: 评分
-
- // 跌倒检测: 可能在睡眠异常标志中
- if (frame.data_length >= 4) {
- _data.fall = (frame.data[3] & 0x01) != 0;
- _data.resident = (frame.data[3] & 0x02) != 0;
- }
+ _data.sleepState = frame.data[1]; // 0=清醒 1=浅睡 2=深睡
+ _data.sleepScore = frame.data[2]; // 0~100
+ _data.lastUpdate = millis();
+ _notifyUpdate();
+ }
+ if (frame.data_length >= 4) {
+ _data.fall = (frame.data[3] & 0x01) != 0;
+ _data.resident = (frame.data[3] & 0x02) != 0;
_data.lastUpdate = millis();
_notifyUpdate();
}
diff --git a/src/wifi_manager.cpp b/src/wifi_manager.cpp
index f33eecd..d9da352 100644
--- a/src/wifi_manager.cpp
+++ b/src/wifi_manager.cpp
@@ -1,36 +1,40 @@
/**
* @file wifi_manager.cpp
- * @brief WiFi 配网管理器实现
+ * @brief WiFi 配网管理器 — AP + WebServer
*/
#include "wifi_manager.h"
-// 单例指针
-WiFiMgmt* WiFiMgmt::_instance = nullptr;
+// BLE 配网回调: 小程序写入 JSON → 触发配网
+class BLEProvisionCB : public BLECharacteristicCallbacks {
+ void onWrite(BLECharacteristic* ch) override {
+ std::string val = ch->getValue();
+ if (val.length() > 0 && WiFiMgmt::_instance) {
+ Serial.printf("[BLE] 收到配网数据 (%d 字节)\n", val.length());
+ WiFiMgmt::_instance->handleBLEProvision(val.c_str());
+ }
+ }
+};
-// ============================================================
-// 构造函数 & 析构函数
-// ============================================================
+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)
+ , _bleServer(nullptr), _bleInfoChar(nullptr), _bleChar(nullptr), _bleAdv(nullptr)
{
memset(&_config, 0, sizeof(_config));
- _instance = this; // 设置单例
+ _instance = this;
}
WiFiMgmt::~WiFiMgmt() {
+ _stopPortal();
_instance = nullptr;
}
@@ -39,176 +43,53 @@ 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);
+ strncpy(_config.env_type, "bedroom", sizeof(_config.env_type)-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 自动重连
-
- // 注册保存回调 (必须使用静态函数, 不能使用 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
- _connected = _connectWiFi();
+ WiFi.setAutoReconnect(true);
+
+ // 有配置 → 直接连
+ 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 +101,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) {
- _connected = false;
- _setLEDMode(1); // 慢闪 = 未连接
- Serial.println(F("[WiFi] ⚠️ WiFi 断开, 尝试重连..."));
- _connectWiFi();
- } else {
+ wl_status_t s = WiFi.status();
+ if (s != WL_CONNECTED) {
+ if (_connected) {
+ _connected = false;
+ _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 +162,375 @@ 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;
}
+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() {
- Serial.println(F("[WiFi] 🔧 手动启动配网页面..."));
- _setLEDMode(2); // 快闪 = 配网模式
- _wm.startConfigPortal(WIFI_AP_SSID, WIFI_AP_PASSWORD);
- _connected = (WiFi.status() == WL_CONNECTED);
- if (_connected) {
- _setLEDMode(3); // 常亮 = 已连接
+ _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"));
+
+ _startBLE(); // 同时启动 BLE 配网广播
+}
+
+void WiFiMgmt::_stopPortal() {
+ if (!_portalActive) return;
+ _stopBLE();
+ 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::resetSettings() {
- Serial.println(F("[WiFi] 清除所有配置..."));
+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();
+}
- // 删除 LittleFS 中的配置文件
- 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);
+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;
+}
+
+// ============================================================
+// BLE 蓝牙配网
+// ============================================================
+
+void WiFiMgmt::_startBLE() {
+ if (_bleServer != nullptr) return; // 已启动
+
+ BLEDevice::init("HealthMonitor");
+ _bleServer = BLEDevice::createServer();
+
+ BLEService* svc = _bleServer->createService("4fafc201-1fb5-459e-8fcc-c5c9c331914b");
+
+ // 特征1: 设备信息 (READ) — 小程序连接后读取设备编号等
+ _bleInfoChar = svc->createCharacteristic(
+ "4fafc202-1fb5-459e-8fcc-c5c9c331914b",
+ BLECharacteristic::PROPERTY_READ
+ );
+ // 设备信息 JSON (最多 512 字节)
+ JsonDocument info;
+ info["device_id"] = _config.device_id;
+ info["mqtt_server"] = _config.mqtt_server;
+ info["mqtt_port"] = _config.mqtt_port;
+ info["fw_version"] = FW_VERSION;
+ String infoJson;
+ serializeJson(info, infoJson);
+ _bleInfoChar->setValue(infoJson.c_str());
+
+ // 特征2: 配网写入 (WRITE + NOTIFY) — 小程序发配网数据, 设备回复
+ _bleChar = svc->createCharacteristic(
+ "4fafc203-1fb5-459e-8fcc-c5c9c331914b",
+ BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY
+ );
+ _bleChar->setCallbacks(new BLEProvisionCB());
+ _bleChar->addDescriptor(new BLE2902()); // 启用 notify
+ _bleChar->setValue(""); // 初始为空
+
+ svc->start();
+
+ _bleAdv = _bleServer->getAdvertising();
+ _bleAdv->addServiceUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b");
+ _bleAdv->setScanResponse(true);
+ _bleAdv->start();
+
+ Serial.printf("[BLE] ✅ 配网广播: HealthMonitor device_id=%s\n", _config.device_id);
+}
+
+void WiFiMgmt::_stopBLE() {
+ if (!_bleServer) return;
+ if (_bleAdv) { _bleAdv->stop(); _bleAdv = nullptr; }
+ BLEDevice::deinit(false);
+ _bleServer = nullptr;
+ _bleChar = nullptr;
+ Serial.println("[BLE] 已停止");
+}
+
+void WiFiMgmt::handleBLEProvision(const char* json) {
+ JsonDocument doc;
+ DeserializationError err = deserializeJson(doc, json);
+ if (err) {
+ Serial.printf("[BLE] JSON 解析失败: %s\n", err.c_str());
+ if (_bleChar) { _bleChar->setValue("ERR_JSON"); _bleChar->notify(); }
+ return;
+ }
- // 清除 WiFiManager 的配置
- _wm.resetSettings();
+ // 提取 WiFi 凭证
+ const char* ssid = doc["s"] | "";
+ const char* pass = doc["p"] | "";
- // LED 快闪提示
- for (int i = 0; i < 10; i++) {
- digitalWrite(LED_WIFI_PIN, !digitalRead(LED_WIFI_PIN));
- delay(100);
+ if (strlen(ssid) == 0) {
+ Serial.println("[BLE] 未提供 WiFi SSID");
+ if (_bleChar) { _bleChar->setValue("ERR_NO_SSID"); _bleChar->notify(); }
+ return;
}
- Serial.println(F("[WiFi] 🔄 正在重启..."));
+ _pendingSSID = ssid;
+ _pendingPass = pass;
+
+ // 保存到 LittleFS (和 Web 配网完全相同的逻辑)
+ strncpy(_config.wifi_ssid, ssid, sizeof(_config.wifi_ssid) - 1);
+ strncpy(_config.wifi_pass, pass, sizeof(_config.wifi_pass) - 1);
+
+ if (doc.containsKey("device_id")) strncpy(_config.device_id, doc["device_id"] | "", sizeof(_config.device_id) - 1);
+ if (doc.containsKey("env_note")) strncpy(_config.env_note, doc["env_note"] | "", sizeof(_config.env_note) - 1);
+ if (doc.containsKey("env_type")) strncpy(_config.env_type, doc["env_type"] | "", sizeof(_config.env_type) - 1);
+ if (doc.containsKey("mqtt_server")) strncpy(_config.mqtt_server, doc["mqtt_server"] | "", sizeof(_config.mqtt_server) - 1);
+ if (doc.containsKey("mqtt_port")) strncpy(_config.mqtt_port, doc["mqtt_port"] | "", sizeof(_config.mqtt_port) - 1);
+ if (doc.containsKey("mqtt_user")) strncpy(_config.mqtt_user, doc["mqtt_user"] | "", sizeof(_config.mqtt_user) - 1);
+ if (doc.containsKey("mqtt_pass")) strncpy(_config.mqtt_pass, doc["mqtt_pass"] | "", sizeof(_config.mqtt_pass) - 1);
+
+ _saveConfig();
+ _portalStopFlag = true; // 触发延迟关闭 AP+BLE → 连接 WiFi
+
+ Serial.printf("[BLE] ✅ 配网完成, SSID=%s\n", ssid);
+
+ // 回复 OK
+ if (_bleChar) { _bleChar->setValue("OK"); _bleChar->notify(); }
+ delay(100);
+}
+
+// ============================================================
+// 重置
+// ============================================================
+
+void WiFiMgmt::resetSettings() {
+ 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);
+ 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.env_type, doc["env_type"] | "bedroom", sizeof(_config.env_type)-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 +542,45 @@ bool WiFiMgmt::_saveConfig() {
doc["mqtt_pass"] = _config.mqtt_pass;
doc["device_id"] = _config.device_id;
doc["env_note"] = _config.env_note;
-
+ doc["env_type"] = _config.env_type;
+ 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] ❌ 配置写入失败"));
- file.close();
- return false;
- }
-
+ if (!file) return false;
+ if (serializeJson(doc, file) == 0) { file.close(); return false; }
file.close();
- Serial.println(F("[WiFi] ✅ 设备配置已保存"));
return true;
}
// ============================================================
-// WiFiManager 保存参数回调
-// ============================================================
-
-// ============================================================
-// 静态回调函数 (WiFiManager 要求 C 函数指针)
+// Getter
// ============================================================
-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;
}
}
}