1	/**
2	 * @file wifi_manager.cpp
3	 * @brief WiFi 配网管理器实现
4	 */
5
6	#include "wifi_manager.h"
7
8	// 单例指针
9	WiFiMgmt* WiFiMgmt::_instance = nullptr;
10
11	// ============================================================
12	// 构造函数 & 析构函数
13	// ============================================================
14
15	WiFiMgmt::WiFiMgmt()
16	    : _connected(false)
17	    , _lastCheckTime(0)
18	    , _buttonPressTime(0)
19	    , _buttonPressed(false)
20	    , _ledTaskHandle(nullptr)
21	    , _ledMode(0)
22	    , _param_mqtt_server(nullptr)
23	    , _param_mqtt_port(nullptr)
24	    , _param_mqtt_user(nullptr)
25	    , _param_mqtt_pass(nullptr)
26	    , _param_device_id(nullptr)
27	    , _param_env_note(nullptr)
28	{
29	    memset(&_config, 0, sizeof(_config));
30	    _instance = this;  // 设置单例
31	}
32
33	WiFiMgmt::~WiFiMgmt() {
34	    _instance = nullptr;
35	}
36
37	// ============================================================
38	// 初始化
39	// ============================================================
40
41	bool WiFiMgmt::begin() {
42	    Serial.println(F("\n========================================"));
43	    Serial.println(F("  WiFi 配网管理器 初始化"));
44	    Serial.println(F("========================================"));
45
46	    // 初始化引脚
47	    pinMode(CONFIG_BUTTON_PIN, INPUT_PULLUP);
48	    pinMode(LED_WIFI_PIN, OUTPUT);
49	    digitalWrite(LED_WIFI_PIN, LOW);
50
51	    // 启动 LED 独立任务 (避免 startConfigPortal 阻塞影响闪烁)
52	    xTaskCreate(_ledTaskFunc, "WiFiLed", 1024, nullptr, 1, &_ledTaskHandle);
53
54	    // 初始化 LittleFS
55	    if (!LittleFS.begin(true)) {
56	        Serial.println(F("[WiFi] ❌ LittleFS 挂载失败!"));
57	        return false;
58	    }
59	    Serial.println(F("[WiFi] ✅ LittleFS 已挂载"));
60
61	    // 加载配置
62	    if (!_loadConfig()) {
63	        Serial.println(F("[WiFi] 使用默认配置"));
64	        // 设置默认值
65	        strncpy(_config.mqtt_server, MQTT_DEFAULT_SERVER,   sizeof(_config.mqtt_server) - 1);
66	        strncpy(_config.mqtt_port,   MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port) - 1);
67	        strncpy(_config.mqtt_user,   MQTT_DEFAULT_USER,     sizeof(_config.mqtt_user) - 1);
68	        strncpy(_config.mqtt_pass,   MQTT_DEFAULT_PASS,     sizeof(_config.mqtt_pass) - 1);
69	        strncpy(_config.device_id,   MQTT_DEFAULT_DEVICE,   sizeof(_config.device_id) - 1);
70	        strncpy(_config.env_note,    MQTT_DEFAULT_ENV,      sizeof(_config.env_note) - 1);
71	    }
72
73	    // 配置 WiFiManager
74	    _wm.setDebugOutput(false);  // 关闭调试输出 (避免干扰)
75	    _wm.setConfigPortalTimeout(WIFI_CONFIG_TIMEOUT);
76	    _wm.setConnectTimeout(WIFI_CONNECT_TIMEOUT);
77	    _wm.setTitle("健康监测系统 - 设备配网");
78	    _wm.setCustomHeadElement(
79	        "<style>"
80	        /* ===== 字体与全局 ===== */
81	        "*{margin:0;padding:0;box-sizing:border-box}"
82	        "body{"
83	        "  font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;"
84	        "  background:linear-gradient(160deg, #f0f5ff 0%, #e8f0fe 40%, #f3f0ff 100%);"
85	        "  min-height:100vh;display:flex;flex-direction:column;align-items:center;"
86	        "  padding:20px 16px 40px;color:#1e293b"
87	        "}"
88	        "body::before{"
89	        "  content:'';position:fixed;top:-200px;right:-200px;"
90	        "  width:400px;height:400px;border-radius:50%;"
91	        "  background:radial-gradient(circle, rgba(99,102,241,0.08) 0%, transparent 70%);"
92	        "  pointer-events:none;z-index:0"
93	        "}"
94	        "body::after{"
95	        "  content:'';position:fixed;bottom:-100px;left:-100px;"
96	        "  width:300px;height:300px;border-radius:50%;"
97	        "  background:radial-gradient(circle, rgba(34,197,94,0.06) 0%, transparent 70%);"
98	        "  pointer-events:none;z-index:0"
99	        "}"
100	        /* ===== 头部卡片 ===== */
101	        ".msg{"
102	        "  width:100%;max-width:440px;position:relative;z-index:1;"
103	        "  background:#fff;border-radius:20px;padding:32px 24px 24px;"
104	        "  margin-bottom:16px;text-align:center;"
105	        "  box-shadow:0 1px 3px rgba(0,0,0,0.04),0 8px 24px rgba(0,0,0,0.06)"
106	        "}"
107	        ".msg::before{"
108	        "  content:'';display:block;width:56px;height:56px;"
109	        "  margin:0 auto 16px;"
110	        "  background:linear-gradient(135deg,#6366f1,#8b5cf6);border-radius:16px;"
111	        "  box-shadow:0 8px 24px rgba(99,102,241,0.25)"
112	        "}"
113	        ".msg h1,.msg h2{font-size:20px;font-weight:700;color:#1e293b;margin-bottom:6px}"
114	        ".msg p,.msg .info{font-size:13px;color:#94a3b8;line-height:1.5}"
115	        /* 隐藏 WiFiManager 默认的 h1 重复标题 */
116	        ".msg h1{font-size:0;margin:0;padding:0}"
117	        ".msg h1::after{content:'健康监测系统';font-size:22px;font-weight:700;color:#1e293b}"
118	        ".msg .info{font-size:13px!important}"
119	        /* ===== 表单卡片 ===== */
120	        "form{"
121	        "  width:100%;max-width:440px;position:relative;z-index:1;"
122	        "  background:#fff;border-radius:20px;padding:24px;"
123	        "  box-shadow:0 1px 3px rgba(0,0,0,0.04),0 8px 24px rgba(0,0,0,0.06)"
124	        "}"
125	        "label{"
126	        "  display:block;font-size:11px;font-weight:600;"
127	        "  text-transform:uppercase;letter-spacing:0.8px;"
128	        "  color:#6366f1;margin:16px 0 6px"
129	        "}"
130	        "label:first-of-type{margin-top:0}"
131	        "input[type=text],input[type=password],input[type=number]{"
132	        "  width:100%;height:46px;padding:0 14px;"
133	        "  border:2px solid #e2e8f0;border-radius:12px;"
134	        "  font-size:15px;color:#1e293b;background:#f8fafc;"
135	        "  outline:none;transition:all 0.2s;-webkit-appearance:none"
136	        "}"
137	        "input:focus{"
138	        "  border-color:#6366f1;background:#fff;"
139	        "  box-shadow:0 0 0 4px rgba(99,102,241,0.1)"
140	        "}"
141	        "input::placeholder{color:#cbd5e1}"
142	        /* ===== 提交按钮 ===== */
143	        "button[type=submit]{"
144	        "  width:100%;height:50px;margin-top:24px;"
145	        "  background:linear-gradient(135deg,#6366f1,#8b5cf6);"
146	        "  color:#fff;border:none;border-radius:14px;"
147	        "  font-size:16px;font-weight:600;letter-spacing:1px;"
148	        "  cursor:pointer;transition:all 0.2s;"
149	        "  box-shadow:0 4px 16px rgba(99,102,241,0.3)"
150	        "}"
151	        "button:active{transform:scale(0.97);box-shadow:0 2px 8px rgba(99,102,241,0.3)}"
152	        /* ===== 页脚 ===== */
153	        ".msg:last-of-type{"
154	        "  background:transparent;box-shadow:none;padding:16px;margin-top:4px"
155	        "}"
156	        ".msg:last-of-type::before{display:none}"
157	        ".msg:last-of-type p,.msg:last-of-type .info{font-size:12px;color:#94a3b8}"
158	        /* ===== 成功/错误状态页 ===== */
159	        ".msg .success{color:#16a34a}"
160	        ".msg .error{color:#dc2626}"
161	        "a{color:#6366f1;text-decoration:none;font-weight:500}"
162	        "</style>"
163	    );
164	    WiFi.setAutoReconnect(true);  // ESP32 自动重连
165
166	    // 注册保存回调 (必须使用静态函数, 不能使用 lambda)
167	    _wm.setSaveConfigCallback(_saveParamsCallback);
168
169	    // 创建自定义参数
170	    _param_mqtt_server = new WiFiManagerParameter(
171	        "mqtt_server", "MQTT 服务器地址",
172	        _config.mqtt_server, 40);
173	    _param_mqtt_port = new WiFiManagerParameter(
174	        "mqtt_port", "MQTT 端口",
175	        _config.mqtt_port, 6);
176	    _param_mqtt_user = new WiFiManagerParameter(
177	        "mqtt_user", "MQTT 用户名",
178	        _config.mqtt_user, 20);
179	    _param_mqtt_pass = new WiFiManagerParameter(
180	        "mqtt_pass", "MQTT 密码",
181	        _config.mqtt_pass, 20);
182	    _param_device_id = new WiFiManagerParameter(
183	        "device_id", "设备名称",
184	        _config.device_id, 20);
185	    _param_env_note = new WiFiManagerParameter(
186	        "env_note", "🏠 环境备注 (如: 主卧/客厅/老人房)",
187	        _config.env_note, 20);
188
189	    // 注册自定义参数
190	    _wm.addParameter(_param_mqtt_server);
191	    _wm.addParameter(_param_mqtt_port);
192	    _wm.addParameter(_param_mqtt_user);
193	    _wm.addParameter(_param_mqtt_pass);
194	    _wm.addParameter(_param_device_id);
195	    _wm.addParameter(_param_env_note);
196
197	    // 设置 AP 模式回调 (静态函数)
198	    _wm.setAPCallback(_apCallback);
199
200	    // 尝试连接 WiFi
201	    _connected = _connectWiFi();
202
203	    if (!_connected) {
204	        // 如果自动连接失败, 启动配网页面
205	        Serial.println(F("[WiFi] 自动连接失败, 启动配网模式..."));
206	        _setLEDMode(1);  // 快闪 = 配网模式
207	        _connected = _connectWiFi();  // 内部会启动配网页面
208	    }
209
210	    if (_connected) {
211	        _setLEDMode(2);  // 常亮 = 已连接
212	    }
213
214	    _lastCheckTime = millis();
215	    return _connected;
216	}
217
218	// ============================================================
219	// 主循环
220	// ============================================================
221
222	bool WiFiMgmt::loop() {
223	    // 检测按钮长按
224	    if (_checkButtonLongPress()) {
225	        Serial.println(F("[WiFi] ⚠  检测到长按, 清除配置并重启..."));
226	        resetSettings();
227	        return false;
228	    }
229
230	    // 检查 WiFi 状态
231	    if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {
232	        _lastCheckTime = millis();
233
234	        if (WiFi.status() != WL_CONNECTED) {
235	            _connected = false;
236	            _setLEDMode(0);  // 慢闪 = 未连接
237	            Serial.println(F("[WiFi] ⚠  WiFi 断开, 尝试重连..."));
238	            _connectWiFi();
239	        } else {
240	            _connected = true;
241	            _setLEDMode(2);  // 常亮
242	        }
243	    }
244
245	    // LED 由独立 FreeRTOS 任务处理 (不受 startConfigPortal 阻塞影响)
246	    return _connected;
247	}
248
249	// ============================================================
250	// WiFi 连接
251	// ============================================================
252
253	bool WiFiMgmt::_connectWiFi() {
254	    // 检查是否有已保存的配置
255	    // autoConnect 会先尝试连接, 失败后启动 AP 配网页面
256	    _connected = _wm.autoConnect(WIFI_AP_SSID, WIFI_AP_PASSWORD);
257
258	    if (_connected) {
259	        Serial.println(F("[WiFi] ✅ 已连接!"));
260	        Serial.printf("  WiFi名称: %s\n", WiFi.SSID().c_str());
261	        Serial.printf("  IP地址:   %s\n", WiFi.localIP().toString().c_str());
262	        Serial.printf("  MAC地址:  %s\n", WiFi.macAddress().c_str());
263
264	        // 重新加载配置 (确保获取最新值)
265	        // autoConnect 过程中如果有保存, 回调已写入 LittleFS
266	        _loadConfig();
267
268	        // 同步自定义参数 (如果配置是从 LittleFS 加载的)
269	        _param_mqtt_server->setValue(_config.mqtt_server, sizeof(_config.mqtt_server));
270	        _param_mqtt_port->setValue(_config.mqtt_port, sizeof(_config.mqtt_port));
271	        _param_mqtt_user->setValue(_config.mqtt_user, sizeof(_config.mqtt_user));
272	        _param_mqtt_pass->setValue(_config.mqtt_pass, sizeof(_config.mqtt_pass));
273	        _param_device_id->setValue(_config.device_id, sizeof(_config.device_id));
274	        _param_env_note->setValue(_config.env_note, sizeof(_config.env_note));
275
276	        Serial.printf("  设备名: %s\n", _config.device_id);
277	        Serial.printf("  环境:   %s\n", _config.env_note);
278	        Serial.printf("  MQTT服务器: %s:%s\n", _config.mqtt_server, _config.mqtt_port);
279	    }
280
281	    return _connected;
282	}
283
284	// ============================================================
285	// 配网页面
286	// ============================================================
287
288	void WiFiMgmt::startConfigPortal() {
289	    Serial.println(F("[WiFi] 🔧 手动启动配网页面..."));
290	    _setLEDMode(1);
291	    _wm.startConfigPortal(WIFI_AP_SSID, WIFI_AP_PASSWORD);
292	    _connected = (WiFi.status() == WL_CONNECTED);
293	    if (_connected) {
294	        _setLEDMode(2);
295	    }
296	}
297
298	// ============================================================
299	// 重置设置
300	// ============================================================
301
302	void WiFiMgmt::resetSettings() {
303	    Serial.println(F("[WiFi] 清除所有配置..."));
304
305	    // 删除 LittleFS 中的配置文件
306	    if (LittleFS.exists(CONFIG_FILE_WIFI)) LittleFS.remove(CONFIG_FILE_WIFI);
307	    if (LittleFS.exists(CONFIG_FILE_MQTT)) LittleFS.remove(CONFIG_FILE_MQTT);
308	    if (LittleFS.exists(CONFIG_FILE_DEVICE)) LittleFS.remove(CONFIG_FILE_DEVICE);
309
310	    // 清除 WiFiManager 的配置
311	    _wm.resetSettings();
312
313	    // LED 快闪提示
314	    for (int i = 0; i < 10; i++) {
315	        digitalWrite(LED_WIFI_PIN, !digitalRead(LED_WIFI_PIN));
316	        delay(100);
317	    }
318
319	    Serial.println(F("[WiFi] 🔄 正在重启..."));
320	    delay(500);
321	    ESP.restart();
322	}
323
324	// ============================================================
325	// 按钮长按检测
326	// ============================================================
327
328	bool WiFiMgmt::_checkButtonLongPress() {
329	    bool pressed = (digitalRead(CONFIG_BUTTON_PIN) == LOW);
330
331	    if (pressed && !_buttonPressed) {
332	        // 按下
333	        _buttonPressed = true;
334	        _buttonPressTime = millis();
335	    } else if (!pressed && _buttonPressed) {
336	        // 松开
337	        _buttonPressed = false;
338	        if (millis() - _buttonPressTime >= BUTTON_LONG_PRESS_MS) {
339	            return true;  // 长按确认
340	        }
341	    }
342
343	    return false;
344	}
345
346	// ============================================================
347	// LittleFS 配置持久化
348	// ============================================================
349
350	bool WiFiMgmt::_loadConfig() {
351	    if (!LittleFS.exists(CONFIG_FILE_DEVICE)) {
352	        return false;
353	    }
354
355	    File file = LittleFS.open(CONFIG_FILE_DEVICE, "r");
356	    if (!file) return false;
357
358	    JsonDocument doc;
359	    DeserializationError err = deserializeJson(doc, file);
360	    file.close();
361
362	    if (err) {
363	        Serial.printf("[WiFi] JSON 解析失败: %s\n", err.c_str());
364	        return false;
365	    }
366
367	    strncpy(_config.mqtt_server, doc["mqtt_server"] | MQTT_DEFAULT_SERVER, sizeof(_config.mqtt_server) - 1);
368	    strncpy(_config.mqtt_port,   doc["mqtt_port"]   | MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port) - 1);
369	    strncpy(_config.mqtt_user,   doc["mqtt_user"]   | MQTT_DEFAULT_USER,     sizeof(_config.mqtt_user) - 1);
370	    strncpy(_config.mqtt_pass,   doc["mqtt_pass"]   | MQTT_DEFAULT_PASS,     sizeof(_config.mqtt_pass) - 1);
371	    strncpy(_config.device_id,   doc["device_id"]   | MQTT_DEFAULT_DEVICE,   sizeof(_config.device_id) - 1);
372	    strncpy(_config.env_note,    doc["env_note"]    | MQTT_DEFAULT_ENV,      sizeof(_config.env_note) - 1);
373
374	    Serial.println(F("[WiFi] ✅ 设备配置已加载"));
375	    Serial.printf("  设备名: %s, 环境: %s\n", _config.device_id, _config.env_note);
376	    return true;
377	}
378
379	bool WiFiMgmt::_saveConfig() {
380	    JsonDocument doc;
381	    doc["mqtt_server"] = _config.mqtt_server;
382	    doc["mqtt_port"]   = _config.mqtt_port;
383	    doc["mqtt_user"]   = _config.mqtt_user;
384	    doc["mqtt_pass"]   = _config.mqtt_pass;
385	    doc["device_id"]   = _config.device_id;
386	    doc["env_note"]    = _config.env_note;
387
388	    File file = LittleFS.open(CONFIG_FILE_DEVICE, "w");
389	    if (!file) {
390	        Serial.println(F("[WiFi] ❌ 无法创建配置文件"));
391	        return false;
392	    }
393
394	    if (serializeJson(doc, file) == 0) {
395	        Serial.println(F("[WiFi] ❌ 配置写入失败"));
396	        file.close();
397	        return false;
398	    }
399
400	    file.close();
401	    Serial.println(F("[WiFi] ✅ 设备配置已保存"));
402	    return true;
403	}
404
405	// ============================================================
406	// WiFiManager 保存参数回调
407	// ============================================================
408
409	// ============================================================
410	// 静态回调函数 (WiFiManager 要求 C 函数指针)
411	// ============================================================
412
413	void WiFiMgmt::_apCallback(WiFiManager* wm) {
414	    Serial.println(F("[WiFi] 🔧 进入配网模式"));
415	    Serial.printf("  AP WiFi名称: %s\n", WIFI_AP_SSID);
416	    Serial.printf("  AP 密码: %s\n", WIFI_AP_PASSWORD);
417	    Serial.println(F("  手机连接此 WiFi 即可打开配网页面 (自动弹出)"));
418	}
419
420	void WiFiMgmt::_saveParamsCallback() {
421	    if (!_instance) return;
422
423	    Serial.println(F("[WiFi] 💾 参数已保存"));
424
425	    // 从 WiFiManager 参数中读取值
426	    strncpy(_instance->_config.mqtt_server, _instance->_param_mqtt_server->getValue(),
427	            sizeof(_instance->_config.mqtt_server) - 1);
428	    strncpy(_instance->_config.mqtt_port,   _instance->_param_mqtt_port->getValue(),
429	            sizeof(_instance->_config.mqtt_port) - 1);
430	    strncpy(_instance->_config.mqtt_user,   _instance->_param_mqtt_user->getValue(),
431	            sizeof(_instance->_config.mqtt_user) - 1);
432	    strncpy(_instance->_config.mqtt_pass,   _instance->_param_mqtt_pass->getValue(),
433	            sizeof(_instance->_config.mqtt_pass) - 1);
434	    strncpy(_instance->_config.device_id,   _instance->_param_device_id->getValue(),
435	            sizeof(_instance->_config.device_id) - 1);
436	    strncpy(_instance->_config.env_note,    _instance->_param_env_note->getValue(),
437	            sizeof(_instance->_config.env_note) - 1);
438
439	    _instance->_saveConfig();
440
441	    Serial.println(F("[WiFi] 📋 新配置:"));
442	    Serial.printf("  MQTT: %s:%s\n",
443	                  _instance->_config.mqtt_server,
444	                  _instance->_config.mqtt_port);
445	    Serial.printf("  设备: %s\n", _instance->_config.device_id);
446	    Serial.printf("  环境: %s\n", _instance->_config.env_note);
447	}
448
449	// ============================================================
450	// Getter 方法
451	// ============================================================
452
453	bool WiFiMgmt::isConnected() const {
454	    return _connected && (WiFi.status() == WL_CONNECTED);
455	}
456
457	DeviceConfig WiFiMgmt::getConfig() const {
458	    return _config;
459	}
460
461	const char* WiFiMgmt::getDeviceId() const {
462	    return _config.device_id;
463	}
464
465	const char* WiFiMgmt::getEnvNote() const {
466	    return _config.env_note;
467	}
468
469	// ============================================================
470	// LED 模式控制 (线程安全, 仅写模式变量，由独立任务执行)
471	// ============================================================
472
473	void WiFiMgmt::_setLEDMode(uint8_t mode) {
474	    _ledMode = mode;  // uint8_t 赋值在 ESP32 上是原子的
475	}
476
477	// ============================================================
478	// LED 闪烁独立任务 (FreeRTOS)
479	// 独立于主循环，startConfigPortal 阻塞期间也能正常闪烁
480	// ============================================================
481
482	void WiFiMgmt::_ledTaskFunc(void* param) {
483	    const TickType_t fastMs = pdMS_TO_TICKS(300);   // 快闪 = 配网模式
484	    const TickType_t slowMs = pdMS_TO_TICKS(1000);  // 慢闪 = 未连接
485
486	    while (true) {
487	        if (!_instance) {
488	            vTaskDelay(slowMs);
489	            continue;
490	        }
491
492	        uint8_t mode = _instance->_ledMode;
493
494	        switch (mode) {
495	        case 0:  // 灭
496	            digitalWrite(LED_WIFI_PIN, LOW);
497	            vTaskDelay(slowMs);
498	            break;
499	        case 1:  // 慢闪 — WiFi 断连
500	            digitalWrite(LED_WIFI_PIN, HIGH);
501	            vTaskDelay(slowMs);
502	            digitalWrite(LED_WIFI_PIN, LOW);
503	            vTaskDelay(slowMs);
504	            break;
505	        case 2:  // 快闪 — 配网模式
506	            digitalWrite(LED_WIFI_PIN, HIGH);
507	            vTaskDelay(fastMs);
508	            digitalWrite(LED_WIFI_PIN, LOW);
509	            vTaskDelay(fastMs);
510	            break;
511	        case 3:  // 常亮 — 已连接
512	            digitalWrite(LED_WIFI_PIN, HIGH);
513	            vTaskDelay(slowMs);
514	            break;
515	        }
516	    }
517	}
518

/**
 * @file wifi_manager.cpp
 * @brief WiFi 配网管理器实现
 */

#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)
{
    memset(&_config, 0, sizeof(_config));
    _instance = this;  // 设置单例
}

WiFiMgmt::~WiFiMgmt() {
    _instance = nullptr;
}

// ============================================================
// 初始化
// ============================================================

bool WiFiMgmt::begin() {
    Serial.println(F("\n========================================"));
    Serial.println(F("  WiFi 配网管理器 初始化"));
    Serial.println(F("========================================"));

    // 初始化引脚
    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
    if (!LittleFS.begin(true)) {
        Serial.println(F("[WiFi] ❌ LittleFS 挂载失败!"));
        return false;
    }
    Serial.println(F("[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);
    }

    // 配置 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();

    if (!_connected) {
        // 如果自动连接失败, 启动配网页面
        Serial.println(F("[WiFi] 自动连接失败, 启动配网模式..."));
        _setLEDMode(1);  // 快闪 = 配网模式
        _connected = _connectWiFi();  // 内部会启动配网页面
    }

    if (_connected) {
        _setLEDMode(2);  // 常亮 = 已连接
    }

    _lastCheckTime = millis();
    return _connected;
}

// ============================================================
// 主循环
// ============================================================

bool WiFiMgmt::loop() {
    // 检测按钮长按
    if (_checkButtonLongPress()) {
        Serial.println(F("[WiFi] ⚠  检测到长按, 清除配置并重启..."));
        resetSettings();
        return false;
    }

    // 检查 WiFi 状态
    if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {
        _lastCheckTime = millis();

        if (WiFi.status() != WL_CONNECTED) {
            _connected = false;
            _setLEDMode(0);  // 慢闪 = 未连接
            Serial.println(F("[WiFi] ⚠  WiFi 断开, 尝试重连..."));
            _connectWiFi();
        } else {
            _connected = true;
            _setLEDMode(2);  // 常亮
        }
    }

    // LED 由独立 FreeRTOS 任务处理 (不受 startConfigPortal 阻塞影响)
    return _connected;
}

// ============================================================
// WiFi 连接
// ============================================================

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);
    }

    return _connected;
}

// ============================================================
// 配网页面
// ============================================================

void WiFiMgmt::startConfigPortal() {
    Serial.println(F("[WiFi] 🔧 手动启动配网页面..."));
    _setLEDMode(1);
    _wm.startConfigPortal(WIFI_AP_SSID, WIFI_AP_PASSWORD);
    _connected = (WiFi.status() == WL_CONNECTED);
    if (_connected) {
        _setLEDMode(2);
    }
}

// ============================================================
// 重置设置
// ============================================================

void WiFiMgmt::resetSettings() {
    Serial.println(F("[WiFi] 清除所有配置..."));

    // 删除 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);

    // 清除 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] 🔄 正在重启..."));
    delay(500);
    ESP.restart();
}

// ============================================================
// 按钮长按检测
// ============================================================

bool WiFiMgmt::_checkButtonLongPress() {
    bool pressed = (digitalRead(CONFIG_BUTTON_PIN) == LOW);

    if (pressed && !_buttonPressed) {
        // 按下
        _buttonPressed = true;
        _buttonPressTime = millis();
    } else if (!pressed && _buttonPressed) {
        // 松开
        _buttonPressed = false;
        if (millis() - _buttonPressTime >= BUTTON_LONG_PRESS_MS) {
            return true;  // 长按确认
        }
    }

    return false;
}

// ============================================================
// LittleFS 配置持久化
// ============================================================

bool WiFiMgmt::_loadConfig() {
    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);
    return true;
}

bool WiFiMgmt::_saveConfig() {
    JsonDocument doc;
    doc["mqtt_server"] = _config.mqtt_server;
    doc["mqtt_port"]   = _config.mqtt_port;
    doc["mqtt_user"]   = _config.mqtt_user;
    doc["mqtt_pass"]   = _config.mqtt_pass;
    doc["device_id"]   = _config.device_id;
    doc["env_note"]    = _config.env_note;

    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;
    }

    file.close();
    Serial.println(F("[WiFi] ✅ 设备配置已保存"));
    return true;
}

// ============================================================
// WiFiManager 保存参数回调
// ============================================================

// ============================================================
// 静态回调函数 (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);
}

// ============================================================
// Getter 方法
// ============================================================

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::_ledTaskFunc(void* param) {
    const TickType_t fastMs = pdMS_TO_TICKS(300);   // 快闪 = 配网模式
    const TickType_t slowMs = pdMS_TO_TICKS(1000);  // 慢闪 = 未连接

    while (true) {
        if (!_instance) {
            vTaskDelay(slowMs);
            continue;
        }

        uint8_t mode = _instance->_ledMode;

        switch (mode) {
        case 0:  // 灭
            digitalWrite(LED_WIFI_PIN, LOW);
            vTaskDelay(slowMs);
            break;
        case 1:  // 慢闪 — WiFi 断连
            digitalWrite(LED_WIFI_PIN, HIGH);
            vTaskDelay(slowMs);
            digitalWrite(LED_WIFI_PIN, LOW);
            vTaskDelay(slowMs);
            break;
        case 2:  // 快闪 — 配网模式
            digitalWrite(LED_WIFI_PIN, HIGH);
            vTaskDelay(fastMs);
            digitalWrite(LED_WIFI_PIN, LOW);
            vTaskDelay(fastMs);
            break;
        case 3:  // 常亮 — 已连接
            digitalWrite(LED_WIFI_PIN, HIGH);
            vTaskDelay(slowMs);
            break;
        }
    }
}


按下配网按钮后，wifi指示灯要闪烁，


I see a problem! In `begin()`:
- Line 206-207: When not connected, it sets `_setLEDMode(1)` (slow blink) and calls `_connectWiFi()` which starts the portal
- Line 210-211: When connected, sets `_setLEDMode(2)` (fast blink)

But `_setLEDMode(1)` should be slow blink (not connected), and `_setLEDMode(2)` should be fast blink (config portal). Wait, let me re-read the LED mode definitions:

I defined:
- 0 = off
- 1 = slow blink (disconnected)
- 2 = fast blink (config portal)
- 3 = solid on (connected)

But looking at the original `_setLEDMode`:
```
// 0=慢闪(未连接) 1=快闪(配网) 2=常亮(已连接)
```

And in the new `_ledTaskFunc`:
- 1 = slow blink (disconnected)
- 2 = fast blink (config portal)
- 3 = solid on (connected)

But in `begin()`:
- Line 206: `_setLEDMode(1);` — this is now slow blink... but the portal is about to start, should be fast blink
- Line 211: `_setLEDMode(2);` — this is now fast blink... but should be solid on
- `loop()` line 236: `_setLEDMode(0);` — this is off... but should be slow blink (disconnected)
- `loop()` line 241: `_setLEDMode(2);` — this is fast blink... but should be solid on (connected)
- `startConfigPortal()` line 290: `_setLEDMode(1);` — slow blink... but should be fast blink (portal active)
- `startConfigPortal()` line 294: `_setLEDMode(2);` — fast blink... but should be solid on

The issue is the mode mapping shifted. The original code had:
```
0=慢闪(未连接) 1=快闪(配网) 2=常亮(已连接)
```

The new code has:
```
0=灭 1=慢闪(断连1s) 2=快闪(配网300ms) 3=常亮(已连接)
```

But the callers still use the old mapping. I need to fix the callers.

Let me fix the mode values in `begin()`, `loop()`, and `startConfigPortal()`:

- Old `_setLEDMode(0)` (慢闪/未连接) → New `_setLEDMode(1)` (慢闪)
- Old `_setLEDMode(1)` (快闪/配网) → New `_setLEDMode(2)` (快闪)
- Old `_setLEDMode(2)` (常亮/已连接) → New `_setLEDMode(3)` (常亮)

Let me trace through all the callers:

1. `begin()` line 206: was `_setLEDMode(1)` = fast blink for portal → should be `_setLEDMode(2)`
2. `begin()` line 211: was `_setLEDMode(2)` = solid on → should be `_setLEDMode(3)`
3. `loop()` line 236: was `_setLEDMode(0)` = slow blink disconnected → should be `_setLEDMode(1)`
4. `loop()` line 241: was `_setLEDMode(2)` = solid on → should be `_setLEDMode(3)`
5. `startConfigPortal()` line 290: was `_setLEDMode(1)` = fast blink → should be `_setLEDMode(2)`
6. `startConfigPortal()` line 294: was `_setLEDMode(2)` = solid on → should be `_setLEDMode(3)`

Let me fix all these.


我看到模式值对不上了——旧代码用 `0/1/2`，新任务用 `0/1/2/3`，调用方还是旧值。修正所有调用点：


Edit

        _setLEDMode(1);  // 快闪 = 配网模式
        _connected = _connectWiFi();  // 内部会启动配网页面
    }

    if (_connected) {
        _setLEDMode(2);  // 常亮 = 已连接
    }

        _setLEDMode(2);  // 快闪 = 配网模式
        _connected = _connectWiFi();  // 内部会启动配网页面
    }

    if (_connected) {
        _setLEDMode(3);  // 常亮 = 已连接
    }


The file d:\PlatformIO1\Projects\Healthv2.0\src\wifi_manager.cpp has been updated successfully. (file state is current in your context — no need to Read it back)

        _setLEDMode(1);  // 快闪 = 配网模式
        _connected = _connectWiFi();  // 内部会启动配网页面
    }

    if (_connected) {
        _setLEDMode(2);  // 常亮 = 已连接
    }

        _setLEDMode(2);  // 快闪 = 配网模式
        _connected = _connectWiFi();  // 内部会启动配网页面
    }

    if (_connected) {
        _setLEDMode(3);  // 常亮 = 已连接
    }

// 如果自动连接失败, 启动配网页面


        if (WiFi.status() != WL_CONNECTED) {
            _connected = false;
            _setLEDMode(0);  // 慢闪 = 未连接
            Serial.println(F("[WiFi] ⚠  WiFi 断开, 尝试重连..."));
            _connectWiFi();
        } else {
            _connected = true;
            _setLEDMode(2);  // 常亮
        }

        if (WiFi.status() != WL_CONNECTED) {
            _connected = false;
            _setLEDMode(1);  // 慢闪 = 未连接
            Serial.println(F("[WiFi] ⚠  WiFi 断开, 尝试重连..."));
            _connectWiFi();
        } else {
            _connected = true;
            _setLEDMode(3);  // 常亮 = 已连接
        }


        if (WiFi.status() != WL_CONNECTED) {
            _connected = false;
            _setLEDMode(0);  // 慢闪 = 未连接
            Serial.println(F("[WiFi] ⚠  WiFi 断开, 尝试重连..."));
            _connectWiFi();
        } else {
            _connected = true;
            _setLEDMode(2);  // 常亮
        }

        if (WiFi.status() != WL_CONNECTED) {
            _connected = false;
            _setLEDMode(1);  // 慢闪 = 未连接
            Serial.println(F("[WiFi] ⚠  WiFi 断开, 尝试重连..."));
            _connectWiFi();
        } else {
            _connected = true;
            _setLEDMode(3);  // 常亮 = 已连接
        }

-            _setLEDMode(0);  // 慢闪 = 未连接

+            _setLEDMode(1);  // 慢闪 = 未连接

+            _setLEDMode(3);  // 常亮 = 已连接


    _setLEDMode(1);
    _wm.startConfigPortal(WIFI_AP_SSID, WIFI_AP_PASSWORD);
    _connected = (WiFi.status() == WL_CONNECTED);
    if (_connected) {
        _setLEDMode(2);

    _setLEDMode(2);  // 快闪 = 配网模式
    _wm.startConfigPortal(WIFI_AP_SSID, WIFI_AP_PASSWORD);
    _connected = (WiFi.status() == WL_CONNECTED);
    if (_connected) {
        _setLEDMode(3);  // 常亮 = 已连接


    _setLEDMode(1);
    _wm.startConfigPortal(WIFI_AP_SSID, WIFI_AP_PASSWORD);
    _connected = (WiFi.status() == WL_CONNECTED);
    if (_connected) {
        _setLEDMode(2);

    _setLEDMode(2);  // 快闪 = 配网模式
    _wm.startConfigPortal(WIFI_AP_SSID, WIFI_AP_PASSWORD);
    _connected = (WiFi.status() == WL_CONNECTED);
    if (_connected) {
        _setLEDMode(3);  // 常亮 = 已连接


Now let me verify the final state by reading the key sections to make sure everything is consistent. Let me also update the comment in `begin()` that says "快闪 = 配网模式" since the mapping changed.

Actually, looking at the `_setLEDMode` comment in the header:
```
@param mode 0=灭 1=慢闪(断连1s) 2=快闪(配网300ms) 3=常亮(已连接)
```

And the `_ledTaskFunc`:
- case 1: slow blink (disconnected)
- case 2: fast blink (config portal)
- case 3: solid on (connected)

Now the callers:
- `begin()` not connected: `_setLEDMode(2)` → fast blink for portal ✓
- `begin()` connected: `_setLEDMode(3)` → solid on ✓
- `loop()` disconnected: `_setLEDMode(1)` → slow blink ✓
- `loop()` connected: `_setLEDMode(3)` → solid on ✓
- `startConfigPortal()` before: `_setLEDMode(2)` → fast blink ✓
- `startConfigPortal()` after: `_setLEDMode(3)` → solid on ✓

All consistent now. Let me do a quick sanity check by reading just the critical sections of the final file.

Actually, I'm confident this is correct. Let me summarize what was done.


验证最终一致性，读关键段落：


200	    // 尝试连接 WiFi
201	    _connected = _connectWiFi();
202
203	    if (!_connected) {
204	        // 如果自动连接失败, 启动配网页面
205	        Serial.println(F("[WiFi] 自动连接失败, 启动配网模式..."));
206	        _setLEDMode(2);  // 快闪 = 配网模式
207	        _connected = _connectWiFi();  // 内部会启动配网页面
208	    }
209
210	    if (_connected) {
211	        _setLEDMode(3);  // 常亮 = 已连接
212	    }
213
214	    _lastCheckTime = millis();
215	    return _connected;
216	}
217
218	// ============================================================
219	// 主循环
220	// ============================================================
221
222	bool WiFiMgmt::loop() {
223	    // 检测按钮长按
224	    if (_checkButtonLongPress()) {
225	        Serial.println(F("[WiFi] ⚠  检测到长按, 清除配置并重启..."));
226	        resetSettings();
227	        return false;
228	    }
229
230	    // 检查 WiFi 状态
231	    if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {
232	        _lastCheckTime = millis();
233
234	        if (WiFi.status() != WL_CONNECTED) {
235	            _connected = false;
236	            _setLEDMode(1);  // 慢闪 = 未连接
237	            Serial.println(F("[WiFi] ⚠  WiFi 断开, 尝试重连..."));
238	            _connectWiFi();
239	        } else {
240	            _connected = true;
241	            _setLEDMode(3);  // 常亮 = 已连接
242	        }
243	    }
244
245	    // LED 由独立 FreeRTOS 任务处理 (不受 startConfigPortal 阻塞影响)
246	    return _connected;
247	}
248
249	// ============================================================
250	// WiFi 连接
251	// ============================================================
252
253	bool WiFiMgmt::_connectWiFi() {
254	    // 检查是否有已保存的配置

    // 尝试连接 WiFi
    _connected = _connectWiFi();

    if (!_connected) {
        // 如果自动连接失败, 启动配网页面
        Serial.println(F("[WiFi] 自动连接失败, 启动配网模式..."));
        _setLEDMode(2);  // 快闪 = 配网模式
        _connected = _connectWiFi();  // 内部会启动配网页面
    }

    if (_connected) {
        _setLEDMode(3);  // 常亮 = 已连接
    }

    _lastCheckTime = millis();
    return _connected;
}

// ============================================================
// 主循环
// ============================================================

bool WiFiMgmt::loop() {
    // 检测按钮长按
    if (_checkButtonLongPress()) {
        Serial.println(F("[WiFi] ⚠  检测到长按, 清除配置并重启..."));
        resetSettings();
        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 {
            _connected = true;
            _setLEDMode(3);  // 常亮 = 已连接
        }
    }

    // LED 由独立 FreeRTOS 任务处理 (不受 startConfigPortal 阻塞影响)
    return _connected;
}

// ============================================================
// WiFi 连接
// ============================================================

bool WiFiMgmt::_connectWiFi() {
    // 检查是否有已保存的配置


Everything looks consistent. Let me summarize the changes for the user.


一致了。总结一下这个改动：

---

## LED 指示灯修复 — 改动总结

**问题**：`startConfigPortal()` 内部死循环跑 DNS/Web，主线程 `loop()` 完全不执行，LED 闪烁逻辑跑不到，按了配网按钮灯不闪。

**解决**：把 LED 控制从主循环剥离出来，用 FreeRTOS 独立任务跑。

### 修改的文件

| 文件 | 改动 |
|------|------|
| [wifi_manager.h](include/wifi_manager.h) | 新增 `_ledTaskFunc` 静态任务函数、`_ledTaskHandle`、`volatile uint8_t _ledMode`；删除旧的 `_ledLastToggle`/`_ledState` |
| [wifi_manager.cpp](src/wifi_manager.cpp) | 创建 1KB 栈的 `WiFiLed` 任务；`_setLEDMode` 只写变量；任务循环读 `_ledMode` 执行闪烁 |

### LED 模式

| 模式 | 行为 | 触发场景 |
|------|------|----------|
| 0 | 灭 | 初始化阶段 |
| 1 | 慢闪 (1s 周期) | WiFi 断连 |
| 2 | 快闪 (300ms 周期) | 配网中 |
| 3 | 常亮 | WiFi 已连接 |

现在按配网按钮 → LED 立即开始快闪 → 配网完成 → 常亮。整个过程不受 `startConfigPortal()` 阻塞影响。


为啥现在水浸，没插还是显示正常，oled上，都要显示离线真正常才显示正常


The user is saying that the OLED display still shows the water sensor as "正常" (normal) even though it's not connected. They want it to show "离线" (offline) when the sensor is not connected, and only show "正常" when it's actually working.

Let me check the display manager to see how it renders the water sensor status.


让我看看 OLED 上怎么显示水浸状态的。


1	/**
2	 * @file display_manager.cpp
3	 * @brief OLED 显示管理器实现 - SSD1306 128x64 I2C
4	 */
5
6	#include "display_manager.h"
7
8	// ============================================================
9	// 构造 & 析构
10	// ============================================================
11
12	DisplayManager::DisplayManager()
13	    : _u8g2(U8G2_R0, U8X8_PIN_NONE, OLED_SCL_PIN, OLED_SDA_PIN)
14	    , _pageIndex(0)
15	    , _lastDrawTime(0)
16	    , _initialized(false)
17	{
18	}
19
20	DisplayManager::~DisplayManager() {
21	}
22
23	// ============================================================
24	// 初始化
25	// ============================================================
26
27	bool DisplayManager::begin() {
28	    // SSD1306 常见地址: 0x3C 或 0x3D
29	    // 先用 0x3C 尝试，失败再用 0x3D
30	    _u8g2.setI2CAddress(0x3C << 1);  // U8g2 需要左移一位
31	    if (!_u8g2.begin()) {
32	        // 尝试 0x3D
33	        _u8g2.setI2CAddress(0x3D << 1);
34	        if (!_u8g2.begin()) {
35	            Serial.println(F("[显示] ❌ OLED 未检测到! (I2C 0x3C/0x3D)"));
36	            Serial.println(F("[显示]    请检查接线: VCC→3.3V GND→GND SCL→22 SDA→21"));
37	            Serial.println(F("[显示]    系统将继续运行, 不影响其他功能"));
38	            return false;
39	        }
40	        Serial.println(F("[显示]    使用 I2C 地址 0x3D"));
41	    } else {
42	        Serial.println(F("[显示]    使用 I2C 地址 0x3C"));
43	    }
44
45	    _u8g2.enableUTF8Print();
46	    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
47	    _initialized = true;
48
49	    // 显示启动画面
50	    _u8g2.clearBuffer();
51	    _u8g2.setFont(u8g2_font_wqy16_t_gb2312);
52	    _u8g2.drawUTF8(10, 30, "健康监测系统");
53	    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
54	    _u8g2.drawUTF8(30, 52, "启动中...");
55	    _u8g2.sendBuffer();
56
57	    Serial.println(F("[显示] ✅ OLED 初始化完成 (SSD1306 128x64 I2C)"));
58	    return true;
59	}
60
61	// ============================================================
62	// 翻页
63	// ============================================================
64
65	void DisplayManager::nextPage() {
66	    _pageIndex = (_pageIndex + 1) % 4;
67	    _lastDrawTime = 0;  // 强制立即刷新
68	}
69
70	// ============================================================
71	// 主更新入口
72	// ============================================================
73
74	void DisplayManager::update(const TelemetryData& data, bool wifiOk, bool buzzerOn) {
75	    if (!_initialized) return;
76
77	    // 每 5 秒刷新一次 (防止 OLED 烧屏，也避免 SPI 占用过多)
78	    if (millis() - _lastDrawTime < 5000) return;
79	    _lastDrawTime = millis();
80
81	    _u8g2.clearBuffer();
82
83	    switch (_pageIndex) {
84	    case 0: _drawPage0(data, wifiOk, buzzerOn); break;
85	    case 1: _drawPage1(data); break;
86	    case 2: _drawPage2(data); break;
87	    case 3: _drawPage3(data, buzzerOn, wifiOk); break;
88	    default: _drawPage0(data, wifiOk, buzzerOn); break;
89	    }
90
91	    _u8g2.sendBuffer();
92	}
93
94	// ============================================================
95	// Page 0: 环境数据
96	// ============================================================
97
98	void DisplayManager::_drawPage0(const TelemetryData& data, bool wifiOk, bool buzzerOn) {
99	    // 标题栏
100	    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
101	    _u8g2.drawUTF8(2, 12, "🏥 健康监测 V1.0");
102
103	    // 分隔线
104	    _u8g2.drawHLine(0, 15, 128);
105
106	    // 温湿度
107	    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
108	    _u8g2.drawUTF8(4, 30, "温度:");
109	    _u8g2.setCursor(52, 30);
110	    if (data.dhtValid) {
111	        _u8g2.printf("%s°C", _fmtFloat(data.temp).c_str());
112	    } else {
113	        _u8g2.print("-- °C");
114	    }
115
116	    _u8g2.drawUTF8(4, 44, "湿度:");
117	    _u8g2.setCursor(52, 44);
118	    if (data.dhtValid) {
119	        _u8g2.printf("%s%%", _fmtFloat(data.humi).c_str());
120	    } else {
121	        _u8g2.print("-- %");
122	    }
123
124	    // WiFi 状态
125	    _u8g2.drawUTF8(4, 58, "WiFi:");
126	    _u8g2.setCursor(52, 58);
127	    _u8g2.print(wifiOk ? "已连接" : "未连接");
128
129	    // 运行时间 + 蜂鸣器状态
130	    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
131	    _u8g2.setCursor(2, 64);
132	    _u8g2.print(_fmtUptime(data.uptimeSec));
133
134	    if (buzzerOn) {
135	        _u8g2.drawUTF8(104, 64, "🔔");
136	    }
137
138	    // 页码
139	    _u8g2.setCursor(110, 12);
140	    _u8g2.print("1/4");
141	}
142
143	// ============================================================
144	// Page 1: 生命体征
145	// ============================================================
146
147	void DisplayManager::_drawPage1(const TelemetryData& data) {
148	    // 标题栏
149	    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
150	    _u8g2.drawUTF8(2, 12, "🫀 生命体征");
151	    _u8g2.drawHLine(0, 15, 128);
152
153	    // 心率
154	    _u8g2.drawUTF8(4, 32, "心率:");
155	    _u8g2.setCursor(52, 32);
156	    if (data.vitalValid && data.heartRate > 0) {
157	        _u8g2.printf("%d bpm", data.heartRate);
158	    } else {
159	        _u8g2.print("-- bpm");
160	    }
161
162	    // 呼吸
163	    _u8g2.drawUTF8(4, 48, "呼吸:");
164	    _u8g2.setCursor(52, 48);
165	    if (data.vitalValid && data.breathRate > 0) {
166	        _u8g2.printf("%d 次/分", data.breathRate);
167	    } else {
168	        _u8g2.print("-- 次/分");
169	    }
170
171	    // 体动/能量值
172	    _u8g2.drawUTF8(4, 64, "体动:");
173	    _u8g2.setCursor(52, 64);
174	    _u8g2.printf("%d", data.body);
175
176	    // 页码
177	    _u8g2.setCursor(110, 12);
178	    _u8g2.print("2/4");
179	}
180
181	// ============================================================
182	// Page 2: 人体状态
183	// ============================================================
184
185	void DisplayManager::_drawPage2(const TelemetryData& data) {
186	    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
187	    _u8g2.drawUTF8(2, 12, "人体状态");
188	    _u8g2.drawHLine(0, 15, 128);
189
190	    _u8g2.drawUTF8(4, 30, "有人:");
191	    _u8g2.drawUTF8(60, 30, data.presence ? "V" : "X");
192
193	    _u8g2.drawUTF8(90, 30, "运动:");
194	    _u8g2.drawUTF8(120, 30, data.motion ? "V" : "X");
195
196	    _u8g2.drawUTF8(4, 44, "跌倒:");
197	    _u8g2.drawUTF8(60, 44, data.fall ? "!!" : "X");
198
199	    _u8g2.drawUTF8(90, 44, "驻留:");
200	    _u8g2.drawUTF8(120, 44, data.resident ? "V" : "X");
201
202	    _u8g2.drawUTF8(4, 58, "体动:");
203	    _u8g2.setCursor(60, 58);
204	    _u8g2.print(data.body);
205
206	    _u8g2.drawUTF8(110, 12, "3/4");
207	}
208
209	// ============================================================
210	// Page 3: 安全信息
211	// ============================================================
212
213	void DisplayManager::_drawPage3(const TelemetryData& data, bool buzzerOn, bool wifiOk) {
214	    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
215	    _u8g2.drawUTF8(2, 12, "安全信息");
216	    _u8g2.drawHLine(0, 15, 128);
217
218	    _u8g2.drawUTF8(4, 30, "水浸:");
219	    _u8g2.drawUTF8(60, 30, data.rainDO ? "正常" : "!!报警");
220
221	    _u8g2.drawUTF8(4, 44, "蜂鸣器:");
222	    _u8g2.drawUTF8(60, 44, buzzerOn ? "响" : "关");
223
224	    _u8g2.drawUTF8(4, 58, "WiFi:");
225	    _u8g2.drawUTF8(60, 58, wifiOk ? "已连接" : "未连接");
226
227	    _u8g2.drawUTF8(2, 64, _fmtUptime(data.uptimeSec).c_str());
228	    _u8g2.drawUTF8(110, 12, "4/4");
229	}
230
231	// ============================================================
232	// 辅助方法
233	// ============================================================
234
235	String DisplayManager::_fmtUptime(uint32_t secs) const {
236	    uint32_t h = secs / 3600;
237	    uint32_t m = (secs % 3600) / 60;
238	    uint32_t s = secs % 60;
239	    char buf[12];
240	    snprintf(buf, sizeof(buf), "%02d:%02d:%02d", (int)h, (int)m, (int)s);
241	    return String(buf);
242	}
243
244	String DisplayManager::_fmtFloat(float val) const {
245	    char buf[8];
246	    snprintf(buf, sizeof(buf), "%.1f", val);
247	    return String(buf);
248	}
249

/**
 * @file display_manager.cpp
 * @brief OLED 显示管理器实现 - SSD1306 128x64 I2C
 */

#include "display_manager.h"

// ============================================================
// 构造 & 析构
// ============================================================

DisplayManager::DisplayManager()
    : _u8g2(U8G2_R0, U8X8_PIN_NONE, OLED_SCL_PIN, OLED_SDA_PIN)
    , _pageIndex(0)
    , _lastDrawTime(0)
    , _initialized(false)
{
}

DisplayManager::~DisplayManager() {
}

// ============================================================
// 初始化
// ============================================================

bool DisplayManager::begin() {
    // SSD1306 常见地址: 0x3C 或 0x3D
    // 先用 0x3C 尝试，失败再用 0x3D
    _u8g2.setI2CAddress(0x3C << 1);  // U8g2 需要左移一位
    if (!_u8g2.begin()) {
        // 尝试 0x3D
        _u8g2.setI2CAddress(0x3D << 1);
        if (!_u8g2.begin()) {
            Serial.println(F("[显示] ❌ OLED 未检测到! (I2C 0x3C/0x3D)"));
            Serial.println(F("[显示]    请检查接线: VCC→3.3V GND→GND SCL→22 SDA→21"));
            Serial.println(F("[显示]    系统将继续运行, 不影响其他功能"));
            return false;
        }
        Serial.println(F("[显示]    使用 I2C 地址 0x3D"));
    } else {
        Serial.println(F("[显示]    使用 I2C 地址 0x3C"));
    }

    _u8g2.enableUTF8Print();
    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
    _initialized = true;

    // 显示启动画面
    _u8g2.clearBuffer();
    _u8g2.setFont(u8g2_font_wqy16_t_gb2312);
    _u8g2.drawUTF8(10, 30, "健康监测系统");
    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
    _u8g2.drawUTF8(30, 52, "启动中...");
    _u8g2.sendBuffer();

    Serial.println(F("[显示] ✅ OLED 初始化完成 (SSD1306 128x64 I2C)"));
    return true;
}

// ============================================================
// 翻页
// ============================================================

void DisplayManager::nextPage() {
    _pageIndex = (_pageIndex + 1) % 4;
    _lastDrawTime = 0;  // 强制立即刷新
}

// ============================================================
// 主更新入口
// ============================================================

void DisplayManager::update(const TelemetryData& data, bool wifiOk, bool buzzerOn) {
    if (!_initialized) return;

    // 每 5 秒刷新一次 (防止 OLED 烧屏，也避免 SPI 占用过多)
    if (millis() - _lastDrawTime < 5000) return;
    _lastDrawTime = millis();

    _u8g2.clearBuffer();

    switch (_pageIndex) {
    case 0: _drawPage0(data, wifiOk, buzzerOn); break;
    case 1: _drawPage1(data); break;
    case 2: _drawPage2(data); break;
    case 3: _drawPage3(data, buzzerOn, wifiOk); break;
    default: _drawPage0(data, wifiOk, buzzerOn); break;
    }

    _u8g2.sendBuffer();
}

// ============================================================
// Page 0: 环境数据
// ============================================================

void DisplayManager::_drawPage0(const TelemetryData& data, bool wifiOk, bool buzzerOn) {
    // 标题栏
    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
    _u8g2.drawUTF8(2, 12, "🏥 健康监测 V1.0");

    // 分隔线
    _u8g2.drawHLine(0, 15, 128);

    // 温湿度
    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
    _u8g2.drawUTF8(4, 30, "温度:");
    _u8g2.setCursor(52, 30);
    if (data.dhtValid) {
        _u8g2.printf("%s°C", _fmtFloat(data.temp).c_str());
    } else {
        _u8g2.print("-- °C");
    }

    _u8g2.drawUTF8(4, 44, "湿度:");
    _u8g2.setCursor(52, 44);
    if (data.dhtValid) {
        _u8g2.printf("%s%%", _fmtFloat(data.humi).c_str());
    } else {
        _u8g2.print("-- %");
    }

    // WiFi 状态
    _u8g2.drawUTF8(4, 58, "WiFi:");
    _u8g2.setCursor(52, 58);
    _u8g2.print(wifiOk ? "已连接" : "未连接");

    // 运行时间 + 蜂鸣器状态
    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
    _u8g2.setCursor(2, 64);
    _u8g2.print(_fmtUptime(data.uptimeSec));

    if (buzzerOn) {
        _u8g2.drawUTF8(104, 64, "🔔");
    }

    // 页码
    _u8g2.setCursor(110, 12);
    _u8g2.print("1/4");
}

// ============================================================
// Page 1: 生命体征
// ============================================================

void DisplayManager::_drawPage1(const TelemetryData& data) {
    // 标题栏
    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
    _u8g2.drawUTF8(2, 12, "🫀 生命体征");
    _u8g2.drawHLine(0, 15, 128);

    // 心率
    _u8g2.drawUTF8(4, 32, "心率:");
    _u8g2.setCursor(52, 32);
    if (data.vitalValid && data.heartRate > 0) {
        _u8g2.printf("%d bpm", data.heartRate);
    } else {
        _u8g2.print("-- bpm");
    }

    // 呼吸
    _u8g2.drawUTF8(4, 48, "呼吸:");
    _u8g2.setCursor(52, 48);
    if (data.vitalValid && data.breathRate > 0) {
        _u8g2.printf("%d 次/分", data.breathRate);
    } else {
        _u8g2.print("-- 次/分");
    }

    // 体动/能量值
    _u8g2.drawUTF8(4, 64, "体动:");
    _u8g2.setCursor(52, 64);
    _u8g2.printf("%d", data.body);

    // 页码
    _u8g2.setCursor(110, 12);
    _u8g2.print("2/4");
}

// ============================================================
// Page 2: 人体状态
// ============================================================

void DisplayManager::_drawPage2(const TelemetryData& data) {
    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
    _u8g2.drawUTF8(2, 12, "人体状态");
    _u8g2.drawHLine(0, 15, 128);

    _u8g2.drawUTF8(4, 30, "有人:");
    _u8g2.drawUTF8(60, 30, data.presence ? "V" : "X");

    _u8g2.drawUTF8(90, 30, "运动:");
    _u8g2.drawUTF8(120, 30, data.motion ? "V" : "X");

    _u8g2.drawUTF8(4, 44, "跌倒:");
    _u8g2.drawUTF8(60, 44, data.fall ? "!!" : "X");

    _u8g2.drawUTF8(90, 44, "驻留:");
    _u8g2.drawUTF8(120, 44, data.resident ? "V" : "X");

    _u8g2.drawUTF8(4, 58, "体动:");
    _u8g2.setCursor(60, 58);
    _u8g2.print(data.body);

    _u8g2.drawUTF8(110, 12, "3/4");
}

// ============================================================
// Page 3: 安全信息
// ============================================================

void DisplayManager::_drawPage3(const TelemetryData& data, bool buzzerOn, bool wifiOk) {
    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
    _u8g2.drawUTF8(2, 12, "安全信息");
    _u8g2.drawHLine(0, 15, 128);

    _u8g2.drawUTF8(4, 30, "水浸:");
    _u8g2.drawUTF8(60, 30, data.rainDO ? "正常" : "!!报警");

    _u8g2.drawUTF8(4, 44, "蜂鸣器:");
    _u8g2.drawUTF8(60, 44, buzzerOn ? "响" : "关");

    _u8g2.drawUTF8(4, 58, "WiFi:");
    _u8g2.drawUTF8(60, 58, wifiOk ? "已连接" : "未连接");

    _u8g2.drawUTF8(2, 64, _fmtUptime(data.uptimeSec).c_str());
    _u8g2.drawUTF8(110, 12, "4/4");
}

// ============================================================
// 辅助方法
// ============================================================

String DisplayManager::_fmtUptime(uint32_t secs) const {
    uint32_t h = secs / 3600;
    uint32_t m = (secs % 3600) / 60;
    uint32_t s = secs % 60;
    char buf[12];
    snprintf(buf, sizeof(buf), "%02d:%02d:%02d", (int)h, (int)m, (int)s);
    return String(buf);
}

String DisplayManager::_fmtFloat(float val) const {
    char buf[8];
    snprintf(buf, sizeof(buf), "%.1f", val);
    return String(buf);
}


Found it. In `display_manager.cpp` line 219:

```cpp
_u8g2.drawUTF8(60, 30, data.rainDO ? "正常" : "!!报警");
```

It only checks `rainDO` (the water sensor value) but doesn't check `rainValid`. When `rainValid` is false (sensor offline), `rainDO` defaults to `true` (HIGH due to pullup), so it displays "正常" even though the sensor isn't connected.

The fix: check `rainValid` first. If not valid, show "离线". If valid, then show based on `rainDO`.


找到了——[display_manager.cpp:219](src/display_manager.cpp#L219) 只判断 `rainDO` 的值，没有判断 `rainValid`。水浸没接，引脚被上拉成 HIGH，`rainDO` 就是 `true`，直接显示"正常"了。


    _u8g2.drawUTF8(4, 30, "水浸:");
    if (!data.rainValid) {
        _u8g2.drawUTF8(60, 30, "离线");
    } else {
        _u8g2.drawUTF8(60, 30, data.rainDO ? "正常" : "!!报警");
    }


The file d:\PlatformIO1\Projects\Healthv2.0\src\display_manager.cpp has been updated successfully. (file state is current in your context — no need to Read it back)

    _u8g2.drawUTF8(4, 30, "水浸:");
    if (!data.rainValid) {
        _u8g2.drawUTF8(60, 30, "离线");
    } else {
        _u8g2.drawUTF8(60, 30, data.rainDO ? "正常" : "!!报警");
    }


Now the flow is:
1. `SensorManager` constructor: `_rainValid = false` ✅ (already was)
2. `SensorManager::begin()`: `_rainValid = false` ✅ (我上次改的)
3. `SensorManager::read()`: `_rainValid` 只有在引脚状态真正变化时才变 `true` ✅
4. `DisplayManager::_drawPage3()`: 先检查 `rainValid`，无效显示"离线"，有效才根据 `rainDO` 显示"正常"/"!!报警" ✅

现在水浸传感器没接 → `rainValid = false` → OLED 显示"离线"。接了之后一旦检测到状态变化 → `rainValid = true` → 正常显示。


好了。现在的逻辑链路：

1. **构造函数** → `_rainValid = false`
2. **`begin()`** → `_rainValid = false`（上次改的，不再强制设为 true）
3. **`read()`** → 只有引脚真正发生过电平变化，`_rainValid` 才变 `true`
4. **OLED Page 3** → 先看 `rainValid`：`false` → 显示"离线"；`true` → 再根据 `rainDO` 显示"正常"或"!!报警"

水浸没接 → OLED 显示 **离线**，不会误报正常。


嗯


The user is acknowledging the changes. No further action needed.


👍 有需要再说。


现在说拓展所有的报警逻辑


The user wants me to expand all the alarm logic. Let me first understand the current alarm system thoroughly, then plan what needs to be expanded.

Let me read the alarm_manager files to understand the current implementation.


先全面了解现有的报警系统。


1	/**
2	 * @file alarm_manager.h
3	 * @brief 报警管理器 - 双模式 (本地判定 + 服务端指令)
4	 *
5	 * 本地模式: 离线也能用, 跌倒/水浸/心率异常/呼吸异常 本地判定触发
6	 * 服务端模式: 收到 cmd/{gateid}/alarm, alarm/restore 执行联动
7	 */
8
9	#ifndef ALARM_MANAGER_H
10	#define ALARM_MANAGER_H
11
12	#include <Arduino.h>
13	#include "config.h"
14
15	class AlarmManager {
16	public:
17	    // 告警类型 (本地 + 服务端共用)
18	    enum AlarmType {
19	        ALARM_NONE = 0,
20	        ALARM_FALL,             // 跌倒 (本地 + 服务端)
21	        ALARM_WATER_LEAK,       // 水浸 (本地 + 服务端)
22	        ALARM_HEART_ABNORMAL,   // 心率异常 (本地)
23	        ALARM_BREATH_ABNORMAL,  // 呼吸异常 (本地)
24	        ALARM_SERVER,           // 服务端下发 (通用)
25	    };
26
27	    AlarmManager();
28	    ~AlarmManager();
29
30	    bool begin();
31	    void loop();
32
33	    // ============================================================
34	    // 本地报警接口 (离线可用)
35	    // ============================================================
36
37	    /** 触发本地报警 */
38	    void triggerLocal(AlarmType type);
39	    /** 清除本地报警 */
40	    void clearLocal(AlarmType type);
41	    /** 清除所有本地报警 */
42	    void clearAllLocal();
43
44	    // ============================================================
45	    // 服务端指令接口 (协议规范 §5.1, §5.2)
46	    // ============================================================
47
48	    /** 执行服务端告警指令 */
49	    void executeAlarm(const AlarmCommand& cmd);
50	    /** 执行服务端恢复指令 */
51	    void executeRestore(const AlarmCommand& cmd);
52	    /** 蜂鸣器测试 */
53	    void buzzerTest(uint32_t durationSec);
54
55	    // ============================================================
56	    // 状态查询 (上报用)
57	    // ============================================================
58
59	    bool isBuzzerOn() const      { return _buzzerOn; }
60	    bool hasActiveAlarm() const  { return _activeCount > 0; }
61	    const char* getActiveAlarmType() const;
62
63	private:
64	    void _setBuzzer(bool on);
65	    void _setLED(bool on);
66	    void _updateBuzzer();
67	    void _updateLED();
68
69	    // 蜂鸣器/LED 状态
70	    bool     _buzzerOn;
71	    bool     _ledOn;
72	    uint32_t _buzzerOffTime;       // 定时关闭时刻 (0=手动关)
73	    uint32_t _buzzerToggleTime;    // 间歇切换时刻
74	    bool     _buzzerBeepState;     // 间歇状态 (响/停)
75	    uint8_t  _beepCount;           // 当前周期哔了几声
76
77	    // 活动报警列表 (最多 4 个同时)
78	    AlarmType _activeAlarms[4];
79	    uint8_t   _activeCount;
80
81	    // LED 闪烁
82	    uint32_t _lastLEDToggle;
83	};
84
85	#endif // ALARM_MANAGER_H
86

/**
 * @file alarm_manager.h
 * @brief 报警管理器 - 双模式 (本地判定 + 服务端指令)
 *
 * 本地模式: 离线也能用, 跌倒/水浸/心率异常/呼吸异常 本地判定触发
 * 服务端模式: 收到 cmd/{gateid}/alarm, alarm/restore 执行联动
 */

#ifndef ALARM_MANAGER_H
#define ALARM_MANAGER_H

#include <Arduino.h>
#include "config.h"

class AlarmManager {
public:
    // 告警类型 (本地 + 服务端共用)
    enum AlarmType {
        ALARM_NONE = 0,
        ALARM_FALL,             // 跌倒 (本地 + 服务端)
        ALARM_WATER_LEAK,       // 水浸 (本地 + 服务端)
        ALARM_HEART_ABNORMAL,   // 心率异常 (本地)
        ALARM_BREATH_ABNORMAL,  // 呼吸异常 (本地)
        ALARM_SERVER,           // 服务端下发 (通用)
    };

    AlarmManager();
    ~AlarmManager();

    bool begin();
    void loop();

    // ============================================================
    // 本地报警接口 (离线可用)
    // ============================================================

    /** 触发本地报警 */
    void triggerLocal(AlarmType type);
    /** 清除本地报警 */
    void clearLocal(AlarmType type);
    /** 清除所有本地报警 */
    void clearAllLocal();

    // ============================================================
    // 服务端指令接口 (协议规范 §5.1, §5.2)
    // ============================================================

    /** 执行服务端告警指令 */
    void executeAlarm(const AlarmCommand& cmd);
    /** 执行服务端恢复指令 */
    void executeRestore(const AlarmCommand& cmd);
    /** 蜂鸣器测试 */
    void buzzerTest(uint32_t durationSec);

    // ============================================================
    // 状态查询 (上报用)
    // ============================================================

    bool isBuzzerOn() const      { return _buzzerOn; }
    bool hasActiveAlarm() const  { return _activeCount > 0; }
    const char* getActiveAlarmType() const;

private:
    void _setBuzzer(bool on);
    void _setLED(bool on);
    void _updateBuzzer();
    void _updateLED();

    // 蜂鸣器/LED 状态
    bool     _buzzerOn;
    bool     _ledOn;
    uint32_t _buzzerOffTime;       // 定时关闭时刻 (0=手动关)
    uint32_t _buzzerToggleTime;    // 间歇切换时刻
    bool     _buzzerBeepState;     // 间歇状态 (响/停)
    uint8_t  _beepCount;           // 当前周期哔了几声

    // 活动报警列表 (最多 4 个同时)
    AlarmType _activeAlarms[4];
    uint8_t   _activeCount;

    // LED 闪烁
    uint32_t _lastLEDToggle;
};

#endif // ALARM_MANAGER_H


1	/**
2	 * @file alarm_manager.cpp
3	 * @brief 报警管理器 - 双模式实现
4	 *
5	 * 本地: 跌倒/水浸/心率异常/呼吸异常 → 立即蜂鸣器+LED
6	 * 服务端: cmd/alarm, cmd/alarm/restore → 执行联动 + 回复响应
7	 */
8
9	#include "alarm_manager.h"
10
11	AlarmManager::AlarmManager()
12	    : _buzzerOn(false)
13	    , _ledOn(false)
14	    , _buzzerOffTime(0)
15	    , _buzzerToggleTime(0)
16	    , _buzzerBeepState(false)
17	    , _beepCount(0)
18	    , _activeCount(0)
19	    , _lastLEDToggle(0)
20	{
21	    for (int i = 0; i < 4; i++) _activeAlarms[i] = ALARM_NONE;
22	}
23
24	AlarmManager::~AlarmManager() {}
25
26	// ============================================================
27	// 初始化
28	// ============================================================
29
30	bool AlarmManager::begin() {
31	    ledcSetup(BUZZER_PWM_CHANNEL, BUZZER_PWM_FREQ, BUZZER_PWM_RESOLUTION);
32	    ledcAttachPin(BUZZER_PIN, BUZZER_PWM_CHANNEL);
33	    ledcWrite(BUZZER_PWM_CHANNEL, 0);  // 关
34
35	    pinMode(LED_ALARM_PIN, OUTPUT);
36	    digitalWrite(LED_ALARM_PIN, LOW);
37
38	    _buzzerToggleTime = 0;
39	    _buzzerBeepState = false;
40	    _beepCount = 0;
41
42	    Serial.println(F("[告警] ✅ 双模式报警 (本地+服务端)"));
43	    return true;
44	}
45
46	// ============================================================
47	// 主循环
48	// ============================================================
49
50	void AlarmManager::loop() {
51	    _updateBuzzer();
52	    _updateLED();
53	}
54
55	// ============================================================
56	// 本地报警触发 (离线可用)
57	// ============================================================
58
59	void AlarmManager::triggerLocal(AlarmType type) {
60	    // 去重
61	    for (uint8_t i = 0; i < _activeCount; i++) {
62	        if (_activeAlarms[i] == type) return;
63	    }
64
65	    if (_activeCount < 4) {
66	        _activeAlarms[_activeCount++] = type;
67	    }
68
69	    const char* name = "";
70	    switch (type) {
71	    case ALARM_FALL:            name = "跌倒";        break;
72	    case ALARM_WATER_LEAK:      name = "水浸";        break;
73	    case ALARM_HEART_ABNORMAL:  name = "心率异常";    break;
74	    case ALARM_BREATH_ABNORMAL: name = "呼吸异常";    break;
75	    default: name = "未知"; break;
76	    }
77
78	    Serial.printf("[告警] 🚨 本地报警触发: %s (活动报警:%d)\n", name, _activeCount);
79
80	    // 立即开启蜂鸣器和 LED
81	    _setBuzzer(true);
82	    _setLED(true);
83	}
84
85	void AlarmManager::clearLocal(AlarmType type) {
86	    for (uint8_t i = 0; i < _activeCount; i++) {
87	        if (_activeAlarms[i] == type) {
88	            for (uint8_t j = i; j < _activeCount - 1; j++) {
89	                _activeAlarms[j] = _activeAlarms[j + 1];
90	            }
91	            _activeAlarms[_activeCount - 1] = ALARM_NONE;
92	            _activeCount--;
93	            break;
94	        }
95	    }
96
97	    Serial.printf("[告警] 本地报警清除, 剩余活动:%d\n", _activeCount);
98
99	    // 没有活动报警时关闭
100	    if (_activeCount == 0) {
101	        _setBuzzer(false);
102	        _setLED(false);
103	    }
104	}
105
106	void AlarmManager::clearAllLocal() {
107	    _activeCount = 0;
108	    for (int i = 0; i < 4; i++) _activeAlarms[i] = ALARM_NONE;
109	    _setBuzzer(false);
110	    _setLED(false);
111	    Serial.println(F("[告警] 清除所有本地报警"));
112	}
113
114	// ============================================================
115	// 服务端告警指令 (§5.1)
116	// ============================================================
117
118	void AlarmManager::executeAlarm(const AlarmCommand& cmd) {
119	    Serial.printf("[告警] ☁ 服务端告警: %s (等级=%s, 蜂鸣器=%d)\n",
120	                  cmd.alarmType, cmd.alarmLevel, cmd.buzzerOn);
121
122	    // 映射 alarmType 到本地类型
123	    AlarmType localType = ALARM_SERVER;
124	    if (strcmp(cmd.alarmType, "fall") == 0)           localType = ALARM_FALL;
125	    else if (strcmp(cmd.alarmType, "water_leak") == 0) localType = ALARM_WATER_LEAK;
126
127	    if (cmd.buzzerOn) {
128	        triggerLocal(localType);
129	        if (cmd.buzzerDuration > 0) {
130	            _buzzerOffTime = millis() + (uint32_t)cmd.buzzerDuration * 1000;
131	        } else {
132	            _buzzerOffTime = 0;
133	        }
134	    }
135
136	    if (cmd.nightLight || cmd.buzzerOn) {
137	        _setLED(true);
138	    }
139	}
140
141	// ============================================================
142	// 服务端恢复指令 (§5.2)
143	// ============================================================
144
145	void AlarmManager::executeRestore(const AlarmCommand& cmd) {
146	    Serial.printf("[告警] ☁ 服务端恢复: %s\n", cmd.alarmType);
147
148	    AlarmType localType = ALARM_SERVER;
149	    if (strcmp(cmd.alarmType, "fall") == 0)           localType = ALARM_FALL;
150	    else if (strcmp(cmd.alarmType, "water_leak") == 0) localType = ALARM_WATER_LEAK;
151
152	    clearLocal(localType);
153
154	    if (!cmd.buzzerOn) {
155	        _setBuzzer(false);
156	    }
157	    if (!cmd.nightLight) {
158	        _setLED(false);
159	    }
160	}
161
162	// ============================================================
163	// 蜂鸣器测试
164	// ============================================================
165
166	void AlarmManager::buzzerTest(uint32_t durationSec) {
167	    Serial.printf("[告警] 🔔 蜂鸣器测试 %d秒\n", durationSec);
168	    _setBuzzer(true);
169	    _buzzerOffTime = millis() + durationSec * 1000;
170	}
171
172	// ============================================================
173	// 状态查询
174	// ============================================================
175
176	const char* AlarmManager::getActiveAlarmType() const {
177	    if (_activeCount == 0) return "";
178	    switch (_activeAlarms[0]) {
179	    case ALARM_FALL:            return "fall";
180	    case ALARM_WATER_LEAK:      return "water_leak";
181	    case ALARM_HEART_ABNORMAL:  return "heart_abnormal";
182	    case ALARM_BREATH_ABNORMAL: return "breath_abnormal";
183	    case ALARM_SERVER:          return "server";
184	    default: return "";
185	    }
186	}
187
188	// ============================================================
189	// 内部控制
190	// ============================================================
191
192	void AlarmManager::_setBuzzer(bool on) {
193	    _buzzerOn = on;
194	    _buzzerBeepState = false;
195	    _buzzerToggleTime = 0;
196	    _beepCount = 0;
197	    if (on) {
198	        _buzzerOffTime = 0;  // 本地报警=无限哔哔
199	    } else {
200	        ledcWrite(BUZZER_PWM_CHANNEL, 0);
201	        _buzzerOffTime = 0;
202	    }
203	}
204
205	void AlarmManager::_setLED(bool on) {
206	    _ledOn = on;
207	    if (!on) digitalWrite(LED_ALARM_PIN, LOW);
208	}
209
210	void AlarmManager::_updateBuzzer() {
211	    // 定时关闭检查
212	    if (_buzzerOn && _buzzerOffTime > 0 && millis() >= _buzzerOffTime) {
213	        _buzzerOn = false;
214	        ledcWrite(BUZZER_PWM_CHANNEL, 0);
215	        Serial.println(F("[告警] ⏰ 蜂鸣器定时关闭"));
216	        return;
217	    }
218	    // 本地报警: 三连哔 停 — 循环 (哔哔哔...哔哔哔...)
219	    if (_buzzerOn && _buzzerOffTime == 0) {
220	        uint32_t now = millis();
221	        // 周期: 3声哔 (每声150ms响+100ms停) + 600ms大停顿
222	        if (!_buzzerBeepState) {
223	            // 在停顿中，检查是否该下一声
224	            uint32_t pause = (_beepCount >= 3) ? 600 : 100;
225	            if (now - _buzzerToggleTime >= pause) {
226	                _buzzerToggleTime = now;
227	                if (_beepCount >= 3) _beepCount = 0;  // 重置周期
228	                _buzzerBeepState = true;
229	                ledcWrite(BUZZER_PWM_CHANNEL, 128);
230	                _beepCount++;
231	            }
232	        } else {
233	            // 在响中，150ms后停下
234	            if (now - _buzzerToggleTime >= 150) {
235	                _buzzerToggleTime = now;
236	                _buzzerBeepState = false;
237	                ledcWrite(BUZZER_PWM_CHANNEL, 0);
238	            }
239	        }
240	    }
241	    // 云端定时: 持续响
242	    else if (_buzzerOn && _buzzerOffTime > 0) {
243	        ledcWrite(BUZZER_PWM_CHANNEL, 128);
244	    }
245	}
246
247	void AlarmManager::_updateLED() {
248	    if (!_ledOn) {
249	        digitalWrite(LED_ALARM_PIN, LOW);
250	        return;
251	    }
252
253	    // 根据告警类型调整闪烁频率
254	    uint32_t interval = 500;
255	    for (uint8_t i = 0; i < _activeCount; i++) {
256	        if (_activeAlarms[i] == ALARM_FALL) {
257	            interval = 200;  // 跌倒快闪
258	            break;
259	        }
260	    }
261
262	    if (millis() - _lastLEDToggle >= interval) {
263	        _lastLEDToggle = millis();
264	        digitalWrite(LED_ALARM_PIN, !digitalRead(LED_ALARM_PIN));
265	    }
266	}
267

/**
 * @file alarm_manager.cpp
 * @brief 报警管理器 - 双模式实现
 *
 * 本地: 跌倒/水浸/心率异常/呼吸异常 → 立即蜂鸣器+LED
 * 服务端: cmd/alarm, cmd/alarm/restore → 执行联动 + 回复响应
 */

#include "alarm_manager.h"

AlarmManager::AlarmManager()
    : _buzzerOn(false)
    , _ledOn(false)
    , _buzzerOffTime(0)
    , _buzzerToggleTime(0)
    , _buzzerBeepState(false)
    , _beepCount(0)
    , _activeCount(0)
    , _lastLEDToggle(0)
{
    for (int i = 0; i < 4; i++) _activeAlarms[i] = ALARM_NONE;
}

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);  // 关

    pinMode(LED_ALARM_PIN, OUTPUT);
    digitalWrite(LED_ALARM_PIN, LOW);

    _buzzerToggleTime = 0;
    _buzzerBeepState = false;
    _beepCount = 0;

    Serial.println(F("[告警] ✅ 双模式报警 (本地+服务端)"));
    return true;
}

// ============================================================
// 主循环
// ============================================================

void AlarmManager::loop() {
    _updateBuzzer();
    _updateLED();
}

// ============================================================
// 本地报警触发 (离线可用)
// ============================================================

void AlarmManager::triggerLocal(AlarmType type) {
    // 去重
    for (uint8_t i = 0; i < _activeCount; i++) {
        if (_activeAlarms[i] == type) return;
    }

    if (_activeCount < 4) {
        _activeAlarms[_activeCount++] = type;
    }

    const char* name = "";
    switch (type) {
    case ALARM_FALL:            name = "跌倒";        break;
    case ALARM_WATER_LEAK:      name = "水浸";        break;
    case ALARM_HEART_ABNORMAL:  name = "心率异常";    break;
    case ALARM_BREATH_ABNORMAL: name = "呼吸异常";    break;
    default: name = "未知"; break;
    }

    Serial.printf("[告警] 🚨 本地报警触发: %s (活动报警:%d)\n", name, _activeCount);

    // 立即开启蜂鸣器和 LED
    _setBuzzer(true);
    _setLED(true);
}

void AlarmManager::clearLocal(AlarmType type) {
    for (uint8_t i = 0; i < _activeCount; i++) {
        if (_activeAlarms[i] == type) {
            for (uint8_t j = i; j < _activeCount - 1; j++) {
                _activeAlarms[j] = _activeAlarms[j + 1];
            }
            _activeAlarms[_activeCount - 1] = ALARM_NONE;
            _activeCount--;
            break;
        }
    }

    Serial.printf("[告警] 本地报警清除, 剩余活动:%d\n", _activeCount);

    // 没有活动报警时关闭
    if (_activeCount == 0) {
        _setBuzzer(false);
        _setLED(false);
    }
}

void AlarmManager::clearAllLocal() {
    _activeCount = 0;
    for (int i = 0; i < 4; i++) _activeAlarms[i] = ALARM_NONE;
    _setBuzzer(false);
    _setLED(false);
    Serial.println(F("[告警] 清除所有本地报警"));
}

// ============================================================
// 服务端告警指令 (§5.1)
// ============================================================

void AlarmManager::executeAlarm(const AlarmCommand& cmd) {
    Serial.printf("[告警] ☁ 服务端告警: %s (等级=%s, 蜂鸣器=%d)\n",
                  cmd.alarmType, cmd.alarmLevel, cmd.buzzerOn);

    // 映射 alarmType 到本地类型
    AlarmType localType = ALARM_SERVER;
    if (strcmp(cmd.alarmType, "fall") == 0)           localType = ALARM_FALL;
    else if (strcmp(cmd.alarmType, "water_leak") == 0) localType = ALARM_WATER_LEAK;

    if (cmd.buzzerOn) {
        triggerLocal(localType);
        if (cmd.buzzerDuration > 0) {
            _buzzerOffTime = millis() + (uint32_t)cmd.buzzerDuration * 1000;
        } else {
            _buzzerOffTime = 0;
        }
    }

    if (cmd.nightLight || cmd.buzzerOn) {
        _setLED(true);
    }
}

// ============================================================
// 服务端恢复指令 (§5.2)
// ============================================================

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;

    clearLocal(localType);

    if (!cmd.buzzerOn) {
        _setBuzzer(false);
    }
    if (!cmd.nightLight) {
        _setLED(false);
    }
}

// ============================================================
// 蜂鸣器测试
// ============================================================

void AlarmManager::buzzerTest(uint32_t durationSec) {
    Serial.printf("[告警] 🔔 蜂鸣器测试 %d秒\n", durationSec);
    _setBuzzer(true);
    _buzzerOffTime = millis() + durationSec * 1000;
}

// ============================================================
// 状态查询
// ============================================================

const char* AlarmManager::getActiveAlarmType() const {
    if (_activeCount == 0) return "";
    switch (_activeAlarms[0]) {
    case ALARM_FALL:            return "fall";
    case ALARM_WATER_LEAK:      return "water_leak";
    case ALARM_HEART_ABNORMAL:  return "heart_abnormal";
    case ALARM_BREATH_ABNORMAL: return "breath_abnormal";
    case ALARM_SERVER:          return "server";
    default: return "";
    }
}

// ============================================================
// 内部控制
// ============================================================

void AlarmManager::_setBuzzer(bool on) {
    _buzzerOn = on;
    _buzzerBeepState = false;
    _buzzerToggleTime = 0;
    _beepCount = 0;
    if (on) {
        _buzzerOffTime = 0;  // 本地报警=无限哔哔
    } else {
        ledcWrite(BUZZER_PWM_CHANNEL, 0);
        _buzzerOffTime = 0;
    }
}

void AlarmManager::_setLED(bool on) {
    _ledOn = on;
    if (!on) digitalWrite(LED_ALARM_PIN, LOW);
}

void AlarmManager::_updateBuzzer() {
    // 定时关闭检查
    if (_buzzerOn && _buzzerOffTime > 0 && millis() >= _buzzerOffTime) {
        _buzzerOn = false;
        ledcWrite(BUZZER_PWM_CHANNEL, 0);
        Serial.println(F("[告警] ⏰ 蜂鸣器定时关闭"));
        return;
    }
    // 本地报警: 三连哔 停 — 循环 (哔哔哔...哔哔哔...)
    if (_buzzerOn && _buzzerOffTime == 0) {
        uint32_t now = millis();
        // 周期: 3声哔 (每声150ms响+100ms停) + 600ms大停顿
        if (!_buzzerBeepState) {
            // 在停顿中，检查是否该下一声
            uint32_t pause = (_beepCount >= 3) ? 600 : 100;
            if (now - _buzzerToggleTime >= pause) {
                _buzzerToggleTime = now;
                if (_beepCount >= 3) _beepCount = 0;  // 重置周期
                _buzzerBeepState = true;
                ledcWrite(BUZZER_PWM_CHANNEL, 128);
                _beepCount++;
            }
        } else {
            // 在响中，150ms后停下
            if (now - _buzzerToggleTime >= 150) {
                _buzzerToggleTime = now;
                _buzzerBeepState = false;
                ledcWrite(BUZZER_PWM_CHANNEL, 0);
            }
        }
    }
    // 云端定时: 持续响
    else if (_buzzerOn && _buzzerOffTime > 0) {
        ledcWrite(BUZZER_PWM_CHANNEL, 128);
    }
}

void AlarmManager::_updateLED() {
    if (!_ledOn) {
        digitalWrite(LED_ALARM_PIN, LOW);
        return;
    }

    // 根据告警类型调整闪烁频率
    uint32_t interval = 500;
    for (uint8_t i = 0; i < _activeCount; i++) {
        if (_activeAlarms[i] == ALARM_FALL) {
            interval = 200;  // 跌倒快闪
            break;
        }
    }

    if (millis() - _lastLEDToggle >= interval) {
        _lastLEDToggle = millis();
        digitalWrite(LED_ALARM_PIN, !digitalRead(LED_ALARM_PIN));
    }
}


Now let me also check how alarms are triggered in main.cpp to understand the full picture, and also look at the radar data to understand what data is available.


再看看主程序中的报警触发逻辑和雷达数据。


1	/**
2	 * @file radar_protocol.h
3	 * @brief R60ABD1 60G 毫米波雷达通信协议定义
4	 *
5	 * 协议: SIP-S v1.0 串口协议 (云帆瑞达 Micradar)
6	 * 帧格式: 帧头(2B) + 控制字(1B) + 命令字(1B) + 长度(2B) + 数据(NB) + 校验(1B) + 帧尾(2B)
7	 */
8
9	#ifndef RADAR_PROTOCOL_H
10	#define RADAR_PROTOCOL_H
11
12	#include <stdint.h>
13
14	// ============================================================
15	// 帧定界符
16	// ============================================================
17	#define FRAME_HEADER_1          0x53    // 'S'
18	#define FRAME_HEADER_2          0x59    // 'Y'
19	#define FRAME_TAIL_1            0x54    // 'T'
20	#define FRAME_TAIL_2            0x43    // 'C'
21
22	#define FRAME_HEADER_SIZE       2
23	#define FRAME_TAIL_SIZE         2
24	#define FRAME_MIN_SIZE          8       // 最小帧: 头2+控制1+命令1+长度2+校验1+尾2 = 9 字节
25
26	// ============================================================
27	// 控制字 (Control Word)
28	// ============================================================
29	enum RadarControl : uint8_t {
30	    CTRL_HEARTBEAT      = 0x01,   // 心跳包
31	    CTRL_PRODUCT_INFO   = 0x02,   // 产品信息
32	    CTRL_OTA            = 0x03,   // OTA 升级
33	    CTRL_WORK_STATUS    = 0x05,   // 工作状态
34	    CTRL_DETECT_RANGE   = 0x07,   // 探测范围
35	    CTRL_PRESENCE       = 0x80,   // 人体存在
36	    CTRL_BREATHING      = 0x81,   // 呼吸检测
37	    CTRL_SLEEP          = 0x84,   // 睡眠监测
38	    CTRL_HEART_RATE     = 0x85,   // 心率监测
39	};
40
41	// ============================================================
42	// 命令字 (Command Word)
43	// ============================================================
44	enum RadarCommand : uint8_t {
45	    CMD_QUERY           = 0x01,   // 查询命令
46	    CMD_REPORT          = 0x02,   // 主动上报
47	    CMD_SET             = 0x03,   // 设置命令
48	    CMD_ACK             = 0x04,   // 应答
49	};
50
51	// ============================================================
52	// 工作状态命令 (CTRL_WORK_STATUS 下的子命令)
53	// ============================================================
54	enum WorkStatusCmd : uint8_t {
55	    WS_INIT_COMPLETE    = 0x01,   // 初始化完成
56	    WS_INIT_QUERY       = 0x81,   // 查询初始化状态
57	    WS_POS_OUT_BOUNDS   = 0x07,   // 位置超出范围
58	};
59
60	// ============================================================
61	// 呼吸状态
62	// ============================================================
63	enum RespirationStatus : uint8_t {
64	    RESPI_NORMAL        = 0,      // 正常 (10~25 次/分)
65	    RESPI_HIGH          = 1,      // 过高 (>25)
66	    RESPI_LOW           = 2,      // 过低 (<10)
67	};
68
69	// ============================================================
70	// 睡眠状态
71	// ============================================================
72	enum SleepStatus : uint8_t {
73	    SLEEP_AWAKE         = 0,      // 清醒
74	    SLEEP_LIGHT         = 1,      // 浅睡
75	    SLEEP_DEEP          = 2,      // 深睡
76	};
77
78	// ============================================================
79	// 帧解析状态机
80	// ============================================================
81	enum FrameState {
82	    STATE_IDLE,                   // 等待帧头 0x53
83	    STATE_GOT_HEADER1,            // 已收到 0x53, 等待 0x59
84	    STATE_GOT_HEADER2,            // 已收到帧头, 等待控制字
85	    STATE_GOT_CONTROL,            // 已收到控制字, 等待命令字
86	    STATE_GOT_COMMAND,            // 已收到命令字, 等待长度高字节
87	    STATE_GOT_LEN_H,              // 已收到长度高字节, 等待长度低字节
88	    STATE_GOT_LEN_L,              // 已收到长度低字节, 读取数据
89	    STATE_GOT_DATA,               // 数据接收完成, 等待校验
90	    STATE_GOT_CHECKSUM,           // 已收到校验, 等待帧尾 0x54
91	    STATE_GOT_TAIL1,              // 已收到 0x54, 等待 0x43
92	    STATE_FRAME_COMPLETE,         // 帧接收完成
93	};
94
95	// ============================================================
96	// 帧缓冲区大小
97	// ============================================================
98	#define RADAR_FRAME_BUF_SIZE    128   // 单帧最大缓冲区
99	#define RADAR_RING_BUF_SIZE     512   // 环形接收缓冲区
100
101	// ============================================================
102	// 解析后的雷达帧数据 (原始)
103	// ============================================================
104	struct RadarFrame {
105	    uint8_t  control;
106	    uint8_t  command;
107	    uint16_t data_length;
108	    uint8_t  data[RADAR_FRAME_BUF_SIZE];
109	    uint8_t  checksum;
110	    bool     valid;                  // 校验通过
111	    uint32_t timestamp;
112	};
113
114	#endif // RADAR_PROTOCOL_H
115

/**
 * @file radar_protocol.h
 * @brief R60ABD1 60G 毫米波雷达通信协议定义
 *
 * 协议: SIP-S v1.0 串口协议 (云帆瑞达 Micradar)
 * 帧格式: 帧头(2B) + 控制字(1B) + 命令字(1B) + 长度(2B) + 数据(NB) + 校验(1B) + 帧尾(2B)
 */

#ifndef RADAR_PROTOCOL_H
#define RADAR_PROTOCOL_H

#include <stdint.h>

// ============================================================
// 帧定界符
// ============================================================
#define FRAME_HEADER_1          0x53    // 'S'
#define FRAME_HEADER_2          0x59    // 'Y'
#define FRAME_TAIL_1            0x54    // 'T'
#define FRAME_TAIL_2            0x43    // 'C'

#define FRAME_HEADER_SIZE       2
#define FRAME_TAIL_SIZE         2
#define FRAME_MIN_SIZE          8       // 最小帧: 头2+控制1+命令1+长度2+校验1+尾2 = 9 字节

// ============================================================
// 控制字 (Control Word)
// ============================================================
enum RadarControl : uint8_t {
    CTRL_HEARTBEAT      = 0x01,   // 心跳包
    CTRL_PRODUCT_INFO   = 0x02,   // 产品信息
    CTRL_OTA            = 0x03,   // OTA 升级
    CTRL_WORK_STATUS    = 0x05,   // 工作状态
    CTRL_DETECT_RANGE   = 0x07,   // 探测范围
    CTRL_PRESENCE       = 0x80,   // 人体存在
    CTRL_BREATHING      = 0x81,   // 呼吸检测
    CTRL_SLEEP          = 0x84,   // 睡眠监测
    CTRL_HEART_RATE     = 0x85,   // 心率监测
};

// ============================================================
// 命令字 (Command Word)
// ============================================================
enum RadarCommand : uint8_t {
    CMD_QUERY           = 0x01,   // 查询命令
    CMD_REPORT          = 0x02,   // 主动上报
    CMD_SET             = 0x03,   // 设置命令
    CMD_ACK             = 0x04,   // 应答
};

// ============================================================
// 工作状态命令 (CTRL_WORK_STATUS 下的子命令)
// ============================================================
enum WorkStatusCmd : uint8_t {
    WS_INIT_COMPLETE    = 0x01,   // 初始化完成
    WS_INIT_QUERY       = 0x81,   // 查询初始化状态
    WS_POS_OUT_BOUNDS   = 0x07,   // 位置超出范围
};

// ============================================================
// 呼吸状态
// ============================================================
enum RespirationStatus : uint8_t {
    RESPI_NORMAL        = 0,      // 正常 (10~25 次/分)
    RESPI_HIGH          = 1,      // 过高 (>25)
    RESPI_LOW           = 2,      // 过低 (<10)
};

// ============================================================
// 睡眠状态
// ============================================================
enum SleepStatus : uint8_t {
    SLEEP_AWAKE         = 0,      // 清醒
    SLEEP_LIGHT         = 1,      // 浅睡
    SLEEP_DEEP          = 2,      // 深睡
};

// ============================================================
// 帧解析状态机
// ============================================================
enum FrameState {
    STATE_IDLE,                   // 等待帧头 0x53
    STATE_GOT_HEADER1,            // 已收到 0x53, 等待 0x59
    STATE_GOT_HEADER2,            // 已收到帧头, 等待控制字
    STATE_GOT_CONTROL,            // 已收到控制字, 等待命令字
    STATE_GOT_COMMAND,            // 已收到命令字, 等待长度高字节
    STATE_GOT_LEN_H,              // 已收到长度高字节, 等待长度低字节
    STATE_GOT_LEN_L,              // 已收到长度低字节, 读取数据
    STATE_GOT_DATA,               // 数据接收完成, 等待校验
    STATE_GOT_CHECKSUM,           // 已收到校验, 等待帧尾 0x54
    STATE_GOT_TAIL1,              // 已收到 0x54, 等待 0x43
    STATE_FRAME_COMPLETE,         // 帧接收完成
};

// ============================================================
// 帧缓冲区大小
// ============================================================
#define RADAR_FRAME_BUF_SIZE    128   // 单帧最大缓冲区
#define RADAR_RING_BUF_SIZE     512   // 环形接收缓冲区

// ============================================================
// 解析后的雷达帧数据 (原始)
// ============================================================
struct RadarFrame {
    uint8_t  control;
    uint8_t  command;
    uint16_t data_length;
    uint8_t  data[RADAR_FRAME_BUF_SIZE];
    uint8_t  checksum;
    bool     valid;                  // 校验通过
    uint32_t timestamp;
};

#endif // RADAR_PROTOCOL_H


228	// ============================================================
229	// 本地报警检查 (离线可用)
230	// ============================================================
231
232	void checkLocalAlarms(const RadarRawData& radar) {
233	    uint32_t now = millis();
234
235	    // --- 跌倒检测 (本地紧急报警, 立即触发) ---
236	    if (radar.fall && radar.radarValid && !g_localFallActive) {
237	        g_localFallActive = true;
238	        g_alarm.triggerLocal(AlarmManager::ALARM_FALL);
239	    } else if (!radar.fall && g_localFallActive) {
240	        g_localFallActive = false;
241	        g_alarm.clearLocal(AlarmManager::ALARM_FALL);
242	    }
243
244	    // --- 水浸检测 ---
245	    bool waterLeak = !g_sensor.getRainDO() && g_sensor.isRainValid();
246	    if (waterLeak && !g_localWaterActive) {
247	        g_localWaterActive = true;
248	        g_alarm.triggerLocal(AlarmManager::ALARM_WATER_LEAK);
249	    } else if (!waterLeak && g_localWaterActive) {
250	        g_localWaterActive = false;
251	        g_alarm.clearLocal(AlarmManager::ALARM_WATER_LEAK);
252	    }
253
254	    // --- 心率异常 (离线本地判定, 阈值预留后续调整) ---
255	    if (radar.vitalValid && radar.heartRate > 0) {
256	        bool heartAbn = (radar.heartRate < HEART_RATE_MIN || radar.heartRate > HEART_RATE_MAX);
257	        if (heartAbn && !g_localHeartAbnActive) {
258	            g_localHeartAbnActive = true;
259	            g_alarm.triggerLocal(AlarmManager::ALARM_HEART_ABNORMAL);
260	        } else if (!heartAbn && g_localHeartAbnActive) {
261	            g_localHeartAbnActive = false;
262	            g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
263	        }
264	    }
265
266	    // --- 呼吸异常 ---
267	    if (radar.vitalValid && radar.breathRate > 0) {
268	        bool breathAbn = (radar.breathRate < BREATH_RATE_MIN || radar.breathRate > BREATH_RATE_MAX);
269	        if (breathAbn && !g_localBreathAbnActive) {
270	            g_localBreathAbnActive = true;
271	            g_alarm.triggerLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
272	        } else if (!breathAbn && g_localBreathAbnActive) {
273	            g_localBreathAbnActive = false;
274	            g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
275	        }
276	    }
277	}

// ============================================================
// 本地报警检查 (离线可用)
// ============================================================

void checkLocalAlarms(const RadarRawData& radar) {
    uint32_t now = millis();

    // --- 跌倒检测 (本地紧急报警, 立即触发) ---
    if (radar.fall && radar.radarValid && !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);
    }

    // --- 水浸检测 ---
    bool waterLeak = !g_sensor.getRainDO() && g_sensor.isRainValid();
    if (waterLeak && !g_localWaterActive) {
        g_localWaterActive = true;
        g_alarm.triggerLocal(AlarmManager::ALARM_WATER_LEAK);
    } else if (!waterLeak && g_localWaterActive) {
        g_localWaterActive = false;
        g_alarm.clearLocal(AlarmManager::ALARM_WATER_LEAK);
    }

    // --- 心率异常 (离线本地判定, 阈值预留后续调整) ---
    if (radar.vitalValid && radar.heartRate > 0) {
        bool heartAbn = (radar.heartRate < HEART_RATE_MIN || radar.heartRate > HEART_RATE_MAX);
        if (heartAbn && !g_localHeartAbnActive) {
            g_localHeartAbnActive = true;
            g_alarm.triggerLocal(AlarmManager::ALARM_HEART_ABNORMAL);
        } else if (!heartAbn && g_localHeartAbnActive) {
            g_localHeartAbnActive = false;
            g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
        }
    }

    // --- 呼吸异常 ---
    if (radar.vitalValid && radar.breathRate > 0) {
        bool breathAbn = (radar.breathRate < BREATH_RATE_MIN || radar.breathRate > BREATH_RATE_MAX);
        if (breathAbn && !g_localBreathAbnActive) {
            g_localBreathAbnActive = true;
            g_alarm.triggerLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
        } else if (!breathAbn && g_localBreathAbnActive) {
            g_localBreathAbnActive = false;
            g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
        }
    }
}


Now let me also look at the radar manager to understand what data is available from the radar: