1	/**
2	 * @file sensor_manager.cpp
3	 * @brief 传感器管理器实现 - DHT11 + 水浸
4	 */
5
6	#include "sensor_manager.h"
7
8	SensorManager::SensorManager()
9	    : _dht(nullptr)
10	    , _temp(0)
11	    , _humi(0)
12	    , _dhtValid(false)
13	    , _dhtErrorCnt(0)
14	    , _rainDO(true)       // 1=正常
15	    , _rainValid(false)
16	    , _lastRainRaw(true)
17	    , _rainChangeTime(0)
18	    , _callback(nullptr)
19	    , _lastReadTime(0)
20	{
21	}
22
23	SensorManager::~SensorManager() {
24	    if (_dht) delete _dht;
25	}
26
27	// ============================================================
28	// 初始化
29	// ============================================================
30
31	bool SensorManager::begin() {
32	    // DHT11 — 允许失败，不影响水浸
33	    _dht = new DHT(DHT11_PIN, DHT_TYPE);
34	    _dht->begin();
35	    _dhtValid = false;
36	    _dhtErrorCnt = 0;
37
38	    // 水浸传感器 — 独立初始化
39	    pinMode(WATER_SENSOR_PIN, INPUT_PULLUP);
40	    _lastRainRaw = (digitalRead(WATER_SENSOR_PIN) == HIGH);  // HIGH=正常
41	    _rainDO = _lastRainRaw;
42	    _rainValid = true;
43
44	    // 首次读取 (DHT11 可能失败)
45	    read();
46
47	    Serial.println(F("[传感器] 初始化完成"));
48	    Serial.printf("  DHT11: %s\n",
49	                  _dhtValid ? "✅ 正常" : "⚠ 未检测到 (不影响系统运行)");
50	    Serial.printf("  水浸:  %s\n",
51	                  _rainValid ? "✅ 正常" : "⚠ 未检测到");
52	    return true;  // 始终成功，不阻塞系统启动
53	}
54
55	// ============================================================
56	// 读取传感器
57	// ============================================================
58
59	void SensorManager::read() {
60	    // --- DHT11 读取 (独立，失败不影响水浸) ---
61	    if (millis() - _lastReadTime >= 2000) {
62	        _lastReadTime = millis();
63
64	        float t = _dht->readTemperature();
65	        float h = _dht->readHumidity();
66
67	        if (!isnan(t) && !isnan(h)) {
68	            if (t > -20 && t < 80 && h >= 0 && h <= 100) {
69	                _temp = t;
70	                _humi = h;
71	                _dhtValid = true;
72	                _dhtErrorCnt = 0;  // 恢复正常，清零计数
73	            } else {
74	                _dhtValid = false;
75	                _dhtErrorCnt++;
76	            }
77	        } else {
78	            _dhtValid = false;
79	            _dhtErrorCnt++;
80	        }
81
82	        // 连续失败 10 次 (~20秒) 后只报一次，避免刷屏
83	        if (_dhtErrorCnt == 10) {
84	            Serial.println(F("[传感器] ⚠ DHT11 持续读取失败 (不影响其他传感器)"));
85	        }
86	    }
87
88	    // 水浸传感器读取 (防抖)
89	    bool raw = (digitalRead(WATER_SENSOR_PIN) == HIGH);  // HIGH=正常
90	    if (raw != _lastRainRaw) {
91	        _rainChangeTime = millis();
92	        _lastRainRaw = raw;
93	    }
94
95	    if (millis() - _rainChangeTime >= WATER_DEBOUNCE_MS) {
96	        bool newState = _lastRainRaw;
97	        if (newState != _rainDO) {
98	            _rainDO = newState;
99	            _rainValid = true;
100
101	            if (!_rainDO) {
102	                Serial.println(F("[传感器] ⚠ 检测到水浸!"));
103	            } else {
104	                Serial.println(F("[传感器] ✅ 水浸解除"));
105	            }
106
107	            if (_callback) _callback();
108	        }
109	    }
110	}
111
112	// ============================================================
113	// 回调
114	// ============================================================
115
116	void SensorManager::onChange(SensorCallback cb) {
117	    _callback = cb;
118	}
119

/**
 * @file sensor_manager.cpp
 * @brief 传感器管理器实现 - DHT11 + 水浸
 */

#include "sensor_manager.h"

SensorManager::SensorManager()
    : _dht(nullptr)
    , _temp(0)
    , _humi(0)
    , _dhtValid(false)
    , _dhtErrorCnt(0)
    , _rainDO(true)       // 1=正常
    , _rainValid(false)
    , _lastRainRaw(true)
    , _rainChangeTime(0)
    , _callback(nullptr)
    , _lastReadTime(0)
{
}

SensorManager::~SensorManager() {
    if (_dht) delete _dht;
}

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

bool SensorManager::begin() {
    // DHT11 — 允许失败，不影响水浸
    _dht = new DHT(DHT11_PIN, DHT_TYPE);
    _dht->begin();
    _dhtValid = false;
    _dhtErrorCnt = 0;

    // 水浸传感器 — 独立初始化
    pinMode(WATER_SENSOR_PIN, INPUT_PULLUP);
    _lastRainRaw = (digitalRead(WATER_SENSOR_PIN) == HIGH);  // HIGH=正常
    _rainDO = _lastRainRaw;
    _rainValid = true;

    // 首次读取 (DHT11 可能失败)
    read();

    Serial.println(F("[传感器] 初始化完成"));
    Serial.printf("  DHT11: %s\n",
                  _dhtValid ? "✅ 正常" : "⚠ 未检测到 (不影响系统运行)");
    Serial.printf("  水浸:  %s\n",
                  _rainValid ? "✅ 正常" : "⚠ 未检测到");
    return true;  // 始终成功，不阻塞系统启动
}

// ============================================================
// 读取传感器
// ============================================================

void SensorManager::read() {
    // --- DHT11 读取 (独立，失败不影响水浸) ---
    if (millis() - _lastReadTime >= 2000) {
        _lastReadTime = millis();

        float t = _dht->readTemperature();
        float h = _dht->readHumidity();

        if (!isnan(t) && !isnan(h)) {
            if (t > -20 && t < 80 && h >= 0 && h <= 100) {
                _temp = t;
                _humi = h;
                _dhtValid = true;
                _dhtErrorCnt = 0;  // 恢复正常，清零计数
            } else {
                _dhtValid = false;
                _dhtErrorCnt++;
            }
        } else {
            _dhtValid = false;
            _dhtErrorCnt++;
        }

        // 连续失败 10 次 (~20秒) 后只报一次，避免刷屏
        if (_dhtErrorCnt == 10) {
            Serial.println(F("[传感器] ⚠ DHT11 持续读取失败 (不影响其他传感器)"));
        }
    }

    // 水浸传感器读取 (防抖)
    bool raw = (digitalRead(WATER_SENSOR_PIN) == HIGH);  // HIGH=正常
    if (raw != _lastRainRaw) {
        _rainChangeTime = millis();
        _lastRainRaw = raw;
    }

    if (millis() - _rainChangeTime >= WATER_DEBOUNCE_MS) {
        bool newState = _lastRainRaw;
        if (newState != _rainDO) {
            _rainDO = newState;
            _rainValid = true;

            if (!_rainDO) {
                Serial.println(F("[传感器] ⚠ 检测到水浸!"));
            } else {
                Serial.println(F("[传感器] ✅ 水浸解除"));
            }

            if (_callback) _callback();
        }
    }
}

// ============================================================
// 回调
// ============================================================

void SensorManager::onChange(SensorCallback cb) {
    _callback = cb;
}


Now I understand the issue. The water sensor (`rainDO`) is currently initialized as valid (`_rainValid = true`) in `begin()`, and it reads the actual pin. Since the water sensor isn't connected, the pin will float or read HIGH due to `INPUT_PULLUP`, giving a false "normal" reading.

The user wants the water sensor to show as "offline" (无效) when it's not connected. The key change is:
1. Make `_rainValid` default to `false` — water sensor is offline by default
2. Make it so that when water sensor is invalid/offline, the system doesn't trigger false water leak alarms

Let me also check how `rainValid` and `rainDO` are used in `main.cpp`:

In `checkLocalAlarms()`:
```cpp
bool waterLeak = !g_sensor.getRainDO() && g_sensor.isRainValid();
```
When `rainValid` is false, `waterLeak` will be false, so no false alarm. Good.

In `buildTelemetry()`:
```cpp
t.rainDO    = g_sensor.getRainDO();
t.rainValid = g_sensor.isRainValid();
```
This reports the validity to the server, so the server will know the sensor is offline. Good.

So the fix is straightforward: set `_rainValid = false` in `begin()` and in the constructor. The sensor will show as offline until a valid reading is detected.

Actually wait, let me think about this more carefully. The `_rainValid` is set to `true` in:
1. Constructor: `_rainValid(false)` — wait, line 15 says `_rainValid(false)`, so it IS initialized to false in the constructor!
2. `begin()` line 42: `_rainValid = true;` — HERE it's set to true!

So the fix is: remove `_rainValid = true;` from `begin()`. The water sensor will stay offline until a state change is actually detected.

Actually, let me reconsider. `_rainDO` defaults to `true` (正常), and `_rainValid` defaults to `false`. In `read()`, `_rainValid` becomes `true` only when the state changes via the debounce logic at line 97-108. If the sensor isn't connected, the pin reading won't change (stays HIGH due to pullup), so `_rainValid` will remain `false`.

So the only problem is line 42 in `begin()`:
```cpp
_rainValid = true;
```

This forces the water sensor to appear valid even though it hasn't actually been validated. Simply removing this line should fix it.

Let me also check what happens with the display:


问题找到了——[sensor_manager.cpp:42](src/sensor_manager.cpp#L42) 里 `begin()` 强制把水浸设为有效 (`_rainValid = true`)，即使传感器没接上也显示正常。

改动很简单：删掉这行，没接传感器就永远显示离线，直到真正检测到状态变化才设为有效。


Edit

    // 水浸传感器 — 独立初始化
    pinMode(WATER_SENSOR_PIN, INPUT_PULLUP);
    _lastRainRaw = (digitalRead(WATER_SENSOR_PIN) == HIGH);  // HIGH=正常
    _rainDO = _lastRainRaw;
    _rainValid = true;

    // 水浸传感器 — 独立初始化
    pinMode(WATER_SENSOR_PIN, INPUT_PULLUP);
    _lastRainRaw = (digitalRead(WATER_SENSOR_PIN) == HIGH);  // HIGH=正常
    _rainDO = _lastRainRaw;
    _rainValid = false;  // 默认离线，直到检测到真正的状态变化才设为有效


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

    // 水浸传感器 — 独立初始化
    pinMode(WATER_SENSOR_PIN, INPUT_PULLUP);
    _lastRainRaw = (digitalRead(WATER_SENSOR_PIN) == HIGH);  // HIGH=正常
    _rainDO = _lastRainRaw;
    _rainValid = false;  // 默认离线，直到检测到真正的状态变化才设为有效

/**
 * @file sensor_manager.cpp
 * @brief 传感器管理器实现 - DHT11 + 水浸
 */

#include "sensor_manager.h"

SensorManager::SensorManager()
    : _dht(nullptr)
    , _temp(0)
    , _humi(0)
    , _dhtValid(false)
    , _dhtErrorCnt(0)
    , _rainDO(true)       // 1=正常
    , _rainValid(false)
    , _lastRainRaw(true)
    , _rainChangeTime(0)
    , _callback(nullptr)
    , _lastReadTime(0)
{
}

SensorManager::~SensorManager() {
    if (_dht) delete _dht;
}

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

bool SensorManager::begin() {
    // DHT11 — 允许失败，不影响水浸
    _dht = new DHT(DHT11_PIN, DHT_TYPE);
    _dht->begin();
    _dhtValid = false;
    _dhtErrorCnt = 0;

    // 水浸传感器 — 独立初始化
    pinMode(WATER_SENSOR_PIN, INPUT_PULLUP);
    _lastRainRaw = (digitalRead(WATER_SENSOR_PIN) == HIGH);  // HIGH=正常
    _rainDO = _lastRainRaw;
    _rainValid = true;

    // 首次读取 (DHT11 可能失败)
    read();

    Serial.println(F("[传感器] 初始化完成"));
    Serial.printf("  DHT11: %s\n",
                  _dhtValid ? "✅ 正常" : "⚠ 未检测到 (不影响系统运行)");
    Serial.printf("  水浸:  %s\n",
                  _rainValid ? "✅ 正常" : "⚠ 未检测到");
    return true;  // 始终成功，不阻塞系统启动
}

// ============================================================
// 读取传感器
// ============================================================

void SensorManager::read() {
    // --- DHT11 读取 (独立，失败不影响水浸) ---
    if (millis() - _lastReadTime >= 2000) {
        _lastReadTime = millis();

        float t = _dht->readTemperature();
        float h = _dht->readHumidity();

        if (!isnan(t) && !isnan(h)) {
            if (t > -20 && t < 80 && h >= 0 && h <= 100) {
                _temp = t;
                _humi = h;
                _dhtValid = true;
                _dhtErrorCnt = 0;  // 恢复正常，清零计数
            } else {
                _dhtValid = false;
                _dhtErrorCnt++;
            }
        } else {
            _dhtValid = false;
            _dhtErrorCnt++;
        }

        // 连续失败 10 次 (~20秒) 后只报一次，避免刷屏
        if (_dhtErrorCnt == 10) {
            Serial.println(F("[传感器] ⚠ DHT11 持续读取失败 (不影响其他传感器)"));
        }
    }

    // 水浸传感器读取 (防抖)
    bool raw = (digitalRead(WATER_SENSOR_PIN) == HIGH);  // HIGH=正常
    if (raw != _lastRainRaw) {
        _rainChangeTime = millis();
        _lastRainRaw = raw;
    }

    if (millis() - _rainChangeTime >= WATER_DEBOUNCE_MS) {
        bool newState = _lastRainRaw;
        if (newState != _rainDO) {
            _rainDO = newState;
            _rainValid = true;

            if (!_rainDO) {
                Serial.println(F("[传感器] ⚠ 检测到水浸!"));
            } else {
                Serial.println(F("[传感器] ✅ 水浸解除"));
            }

            if (_callback) _callback();
        }
    }
}

// ============================================================
// 回调
// ============================================================

void SensorManager::onChange(SensorCallback cb) {
    _callback = cb;
}

+    _rainValid = false;  // 默认离线，直到检测到真正的状态变化才设为有效


现在，水浸没有接上，就先显示离线就行，


Let me also verify the logic chain:

1. Constructor: `_rainValid(false)` ✅
2. `begin()`: now `_rainValid = false` ✅ (just changed)
3. `read()`: `_rainValid` only becomes `true` when the pin reading actually changes (debounce logic at lines 89-109). Without a connected sensor, the pullup keeps the pin HIGH, no change is detected, so `_rainValid` stays `false` ✅
4. `checkLocalAlarms()` in main.cpp: `bool waterLeak = !g_sensor.getRainDO() && g_sensor.isRainValid();` — when `rainValid` is false, no water leak alarm triggers ✅
5. `buildTelemetry()`: reports `rainValid = false` to server, server knows the sensor is offline ✅

The change is correct and sufficient. Let me also check the `begin()` log output — it currently always prints "水浸: ✅ 正常" when `_rainValid` is true. After the change, it will print "水浸: ⚠ 未检测到".

Let me verify the log output code:
```cpp
Serial.printf("  水浸:  %s\n",
              _rainValid ? "✅ 正常" : "⚠ 未检测到");
```

With `_rainValid = false`, it will now print "⚠ 未检测到" which is correct behavior. Perfect.

The change is minimal and correct. No other changes needed.


同时检查一下日志输出——现在启动时也会正确显示水浸离线了。

看一下 [sensor_manager.cpp:48-51](src/sensor_manager.cpp#L48-L51) 的日志输出，它已经会根据 `_rainValid` 显示对应状态：

```cpp
Serial.printf("  水浸:  %s\n",
              _rainValid ? "✅ 正常" : "⚠ 未检测到");
```

后续接了水浸传感器之后，只要引脚电平发生过一次变化（比如从 `HIGH` 变成 `LOW` 又变回来），`_rainValid` 就会自动变成 `true`，系统恢复正常。


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


The user wants the WiFi LED to blink when the provision button is pressed. Let me look at the current code flow:

1. Button is pressed → `g_buttons.onProvisionLongPress` callback → `g_wifi.startConfigPortal()`
2. In `wifi_manager.cpp`, `startConfigPortal()` calls `_setLEDMode(1)` which is fast blink mode, but the blink logic is in `loop()` which may not be running because `startConfigPortal()` blocks.

Let me re-read the `wifi_manager.cpp` to understand the issue.


让我看看当前 LED 闪烁的逻辑流程。


210
211	    _lastCheckTime = millis();
212	    return _connected;
213	}
214
215	// ============================================================
216	// 主循环
217	// ============================================================
218
219	bool WiFiMgmt::loop() {
220	    // 检测按钮长按
221	    if (_checkButtonLongPress()) {
222	        Serial.println(F("[WiFi] ⚠  检测到长按, 清除配置并重启..."));
223	        resetSettings();
224	        return false;
225	    }
226
227	    // 检查 WiFi 状态
228	    if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {
229	        _lastCheckTime = millis();
230
231	        if (WiFi.status() != WL_CONNECTED) {
232	            _connected = false;
233	            _setLEDMode(0);  // 慢闪 = 未连接
234	            Serial.println(F("[WiFi] ⚠  WiFi 断开, 尝试重连..."));
235	            _connectWiFi();
236	        } else {
237	            _connected = true;
238	            _setLEDMode(2);  // 常亮
239	        }
240	    }
241
242	    // LED 闪烁处理
243	    if (!_connected) {
244	        uint32_t interval = (_wm.getConfigPortalActive()) ? 300 : 1000;
245	        if (millis() - _ledLastToggle > interval) {
246	            _ledLastToggle = millis();
247	            _ledState = !_ledState;
248	            digitalWrite(LED_WIFI_PIN, _ledState ? HIGH : LOW);
249	        }
250	    }
251
252	    return _connected;
253	}
254
255	// ============================================================
256	// WiFi 连接
257	// ============================================================
258
259	bool WiFiMgmt::_connectWiFi() {
260	    // 检查是否有已保存的配置
261	    // autoConnect 会先尝试连接, 失败后启动 AP 配网页面
262	    _connected = _wm.autoConnect(WIFI_AP_SSID, WIFI_AP_PASSWORD);
263
264	    if (_connected) {
265	        Serial.println(F("[WiFi] ✅ 已连接!"));
266	        Serial.printf("  WiFi名称: %s\n", WiFi.SSID().c_str());
267	        Serial.printf("  IP地址:   %s\n", WiFi.localIP().toString().c_str());
268	        Serial.printf("  MAC地址:  %s\n", WiFi.macAddress().c_str());
269

    _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 闪烁处理
    if (!_connected) {
        uint32_t interval = (_wm.getConfigPortalActive()) ? 300 : 1000;
        if (millis() - _ledLastToggle > interval) {
            _ledLastToggle = millis();
            _ledState = !_ledState;
            digitalWrite(LED_WIFI_PIN, _ledState ? HIGH : LOW);
        }
    }

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


Now I see the problem. The LED blinking logic is in `loop()` (line 242-250), which checks `_wm.getConfigPortalActive()` to decide blink speed. But `startConfigPortal()` is a blocking call — `_wm.startConfigPortal()` blocks the entire execution while the portal is active. So `loop()` never gets called while the portal is running, and the LED can't blink.

Looking at the code flow:
1. Button press → `startConfigPortal()` → calls `_wm.startConfigPortal()` which BLOCKS
2. Since `startConfigPortal()` blocks, the main `loop()` in `main.cpp` never runs
3. LED blinking is only handled in `loop()` → LED never blinks during config portal

The fix: I need to change the LED control approach. Since `startConfigPortal()` blocks the main loop, the LED blinking needs to happen elsewhere. Options:

1. **Use a separate FreeRTOS task for LED blinking** - Most reliable, runs independently
2. **Use the WiFiManager's config portal timeout callback** - Limited
3. **Avoid blocking by not using `startConfigPortal()` directly** - Major refactor

The best approach is option 1: create a simple LED task, or use a timer. Actually, a simpler approach is to start a FreeRTOS task just before calling `startConfigPortal()` that handles the blinking.

Actually, wait. Let me think about this more carefully. The WiFiManager's `startConfigPortal()` is indeed blocking — it runs the DNS server and web server in a loop. The main `loop()` won't return during that time.

Let me use a FreeRTOS task for LED blinking. I'll:
1. Add a simple LED blink task in `wifi_manager.cpp` that checks `_ledMode` and blinks accordingly
2. Start the task in `begin()` and suspend/resume it as needed
3. The task uses `_ledMode` (0=slow blink, 1=fast blink, 2=solid on, 3=off) and handles the actual pin toggling

Actually, even simpler: I can just use an ESP32 hardware timer or a simple task that runs the LED blink infinitely, checking a shared mode variable. Let me implement this.

Let me look at the full wifi_manager.h again and plan the changes.

The approach:
1. Add a static `_ledTaskFunc` for FreeRTOS
2. Add a `TaskHandle_t _ledTaskHandle` to the class
3. Create the task in `begin()`
4. The task reads `_ledMode` and `_connected` to blink accordingly
5. Remove the LED blink logic from `loop()`
6. In `startConfigPortal()`, set `_ledMode = 1` (fast blink) before calling the blocking `startConfigPortal()`

Actually, let me think about this even more. The task needs access to the instance variables. WiFiManager 2.0.17 uses a blocking loop, so the FreeRTOS task approach is the cleanest.

Let me write the changes.


问题在于 `startConfigPortal()` 是阻塞的——WiFiManager 内部死循环跑 DNS + Web 服务器，`loop()` 根本不执行，LED 闪烁逻辑也就跑不到。

解决方案：用 FreeRTOS 独立任务跑 LED，不受主循环阻塞影响。


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

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

#include "wifi_manager.h"

// 单例指针
WiFiMgmt* WiFiMgmt::_instance = nullptr;

// ============================================================
// 构造函数 & 析构函数
// ============================================================

WiFiMgmt::WiFiMgmt()
    : _connected(false)
    , _lastCheckTime(0)
    , _buttonPressTime(0)
    , _buttonPressed(false)
    , _ledLastToggle(0)
    , _ledState(false)
    , _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);

    // 初始化 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 闪烁处理
    if (!_connected) {
        uint32_t interval = (_wm.getConfigPortalActive()) ? 300 : 1000;
        if (millis() - _ledLastToggle > interval) {
            _ledLastToggle = millis();
            _ledState = !_ledState;
            digitalWrite(LED_WIFI_PIN, _ledState ? HIGH : LOW);
        }
    }

    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) {
    // mode 由 loop() 中的闪烁逻辑处理
    // 0=慢闪 1=快闪 2=常亮
    switch (mode) {
    case 0:
    case 1:
        // 闪烁由 loop 处理
        break;
    case 2:
        digitalWrite(LED_WIFI_PIN, HIGH);
        _ledState = true;
        break;
    }
}


Now I understand the full picture. The problem:

1. `startConfigPortal()` is blocking — `_wm.startConfigPortal()` runs a `while(true)` loop internally that handles DNS and HTTP requests
2. During this blocking call, the main `loop()` doesn't run
3. LED blinking logic is in `loop()`, so LED never blinks during config portal

The fix: Use a FreeRTOS task to handle LED blinking independently. This task runs on its own, even when `startConfigPortal()` blocks the main loop.

Here's my plan:
1. Add `_ledMode` member variable and `_ledTaskHandle` to wifi_manager.h
2. Create a static `_ledTaskFunc` that reads `_instance->_ledMode` and blinks accordingly
3. Start the LED task in `begin()`
4. Remove the LED blink logic from `loop()`
5. Update `_setLEDMode()` to set the mode for the task to read
6. The LED task handles: mode 0 = slow blink (1s), mode 1 = fast blink (300ms), mode 2 = solid on, mode 3 = off — but also, I should use a boolean for whether the portal is active

Let me design the approach:

The LED modes:
- 0: off (initial)
- 1: slow blink (1000ms) — WiFi disconnected, trying to connect
- 2: fast blink (300ms) — config portal active
- 3: solid on — WiFi connected

The LED task will:
- Read `_ledMode`
- If mode 0: LED off
- If mode 1: blink at 1000ms
- If mode 2: blink at 300ms
- If mode 3: LED on

I need to update wifi_manager.h and wifi_manager.cpp.

For the header, I need to add:
- `static void _ledTaskFunc(void* param);`
- `TaskHandle_t _ledTaskHandle;`
- `uint8_t _ledMode;` (replace the scattered led state with this)

For the cpp, I need to:
- Initialize `_ledMode = 0`, `_ledTaskHandle = nullptr`
- Create the task in `begin()`
- Rewrite `_setLEDMode()` to just set `_ledMode`
- Implement `_ledTaskFunc()` as a static function that runs the blink loop
- Remove LED blink logic from `loop()`

Let me also think about the `startConfigPortal()` flow:
1. `_setLEDMode(2)` — fast blink
2. `_wm.startConfigPortal(...)` — blocks
3. After return, check connection, `_setLEDMode(3)` if connected

Since the LED task runs independently, it will blink during step 2.

Now, there's one issue: `startConfigPortal()` is called from two places:
1. Button long press callback → `g_wifi.startConfigPortal()` (in main.cpp setup)
2. The `_connectWiFi()` internally calls `_wm.autoConnect()` which can also trigger the portal

For the `_connectWiFi()` case, `_setLEDMode(1)` is called before it, but `autoConnect()` blocks. The LED task handles this already since it reads `_ledMode`.

Let me also think about thread safety. The LED task only reads `_ledMode` — the main thread writes it. Since it's a `uint8_t`, reads/writes are atomic on ESP32. No mutex needed.

OK let me implement this. I'll also need to handle the `begin()` flow where `_connectWiFi()` is called twice in some cases — the second call's `_setLEDMode(1)` before it should be respected.

Let me write the changes now.

For wifi_manager.h, I need to add:
- `#include <freertos/FreeRTOS.h>` and `#include <freertos/task.h>` or just use the Arduino wrapper
- Actually, Arduino already provides `xTaskCreate` etc. via `Arduino.h`

Let me look at the header structure:

```cpp
// Current members related to LED:
    uint32_t        _ledLastToggle;
    bool            _ledState;
```

I'll replace these with:
```cpp
    uint8_t         _ledMode;       // 0=off 1=slow blink 2=fast blink 3=solid on
    TaskHandle_t    _ledTaskHandle;
```

And add the static task function.

Let me write the changes.


问题很清楚：`startConfigPortal()` 内部是死循环处理 DNS/HTTP，主 `loop()` 完全不跑，LED 闪烁逻辑也就没机会执行。

解决：创建一个独立的 FreeRTOS 任务来跑 LED，不管主线程是否阻塞，LED 都能闪。


    /**
     * @brief 设置 LED 状态
     * @param mode 0=慢闪(未连接) 1=快闪(配网) 2=常亮(已连接)
     */
    void _setLEDMode(uint8_t mode);

    /**
     * @brief 检测按钮长按
     */
    bool _checkButtonLongPress();

    /**
     * @brief 加载/保存配置到 LittleFS
     */
    bool _loadConfig();
    bool _saveConfig();

    /**
     * @brief 保存 WiFi 参数回调 (WiFiManager 的回调)
     */
    static void _saveParamsCallback();
    static void _apCallback(WiFiManager* wm);

    WiFiManager     _wm;
    DeviceConfig    _config;
    bool            _connected;
    uint32_t        _lastCheckTime;
    uint32_t        _buttonPressTime;
    bool            _buttonPressed;

    // LED 闪烁状态
    uint32_t        _ledLastToggle;
    bool            _ledState;

    /**
     * @brief 设置 LED 模式 (线程安全, 无阻塞)
     * @param mode 0=灭 1=慢闪(断连1s) 2=快闪(配网300ms) 3=常亮(已连接)
     */
    void _setLEDMode(uint8_t mode);

    /**
     * @brief LED 闪烁独立任务 (FreeRTOS, 不受主循环阻塞影响)
     */
    static void _ledTaskFunc(void* param);

    /**
     * @brief 检测按钮长按
     */
    bool _checkButtonLongPress();

    /**
     * @brief 加载/保存配置到 LittleFS
     */
    bool _loadConfig();
    bool _saveConfig();

    /**
     * @brief 保存 WiFi 参数回调 (WiFiManager 的回调)
     */
    static void _saveParamsCallback();
    static void _apCallback(WiFiManager* wm);

    WiFiManager     _wm;
    DeviceConfig    _config;
    bool            _connected;
    uint32_t        _lastCheckTime;
    uint32_t        _buttonPressTime;
    bool            _buttonPressed;

    // LED 独立任务 (不被 startConfigPortal 阻塞影响)
    TaskHandle_t    _ledTaskHandle;
    volatile uint8_t _ledMode;  // 0=灭 1=慢闪 2=快闪 3=常亮


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

    /**
     * @brief 设置 LED 模式 (线程安全, 无阻塞)
     * @param mode 0=灭 1=慢闪(断连1s) 2=快闪(配网300ms) 3=常亮(已连接)
     */
    void _setLEDMode(uint8_t mode);

    /**
     * @brief LED 闪烁独立任务 (FreeRTOS, 不受主循环阻塞影响)
     */
    static void _ledTaskFunc(void* param);

    /**
     * @brief 检测按钮长按
     */
    bool _checkButtonLongPress();

    /**
     * @brief 加载/保存配置到 LittleFS
     */
    bool _loadConfig();
    bool _saveConfig();

    /**
     * @brief 保存 WiFi 参数回调 (WiFiManager 的回调)
     */
    static void _saveParamsCallback();
    static void _apCallback(WiFiManager* wm);

    WiFiManager     _wm;
    DeviceConfig    _config;
    bool            _connected;
    uint32_t        _lastCheckTime;
    uint32_t        _buttonPressTime;
    bool            _buttonPressed;

    // LED 独立任务 (不被 startConfigPortal 阻塞影响)
    TaskHandle_t    _ledTaskHandle;
    volatile uint8_t _ledMode;  // 0=灭 1=慢闪 2=快闪 3=常亮

/**
 * @file wifi_manager.h
 * @brief WiFi 配网管理器
 *
 * 功能:
 *  - 自动连接已保存的 WiFi
 *  - 配网失败/首次启动 → AP 模式 + 强制门户页面
 *  - 自定义参数: MQTT 服务器/端口/用户/密码、设备名称、环境备注
 *  - 长按按钮重置配置
 *  - LED 状态指示
 *  - LittleFS 持久化存储
 */

#ifndef WIFI_MANAGER_H
#define WIFI_MANAGER_H

#include <Arduino.h>
#include <WiFiManager.h>
#include <LittleFS.h>
#include <ArduinoJson.h>
#include "config.h"

// 设备配置结构体
struct DeviceConfig {
    char mqtt_server[40];
    char mqtt_port[6];
    char mqtt_user[20];
    char mqtt_pass[20];
    char device_id[20];
    char env_note[20];      // 环境备注 (如: "主卧", "客厅", "老人房")
};

class WiFiMgmt {
public:
    WiFiMgmt();
    ~WiFiMgmt();

    // 单例指针 (用于静态回调)
    static WiFiMgmt* _instance;

    /**
     * @brief 初始化 WiFi 管理器
     * @return true=成功
     */
    bool begin();

    /**
     * @brief 主循环, 处理配网流程和按钮检测
     * @return true=WiFi 已连接
     */
    bool loop();

    /**
     * @brief WiFi 是否已连接
     */
    bool isConnected() const;

    /**
     * @brief 获取设备配置
     */
    DeviceConfig getConfig() const;

    /**
     * @brief 获取设备 ID (用于 MQTT client_id)
     */
    const char* getDeviceId() const;

    /**
     * @brief 获取环境备注
     */
    const char* getEnvNote() const;

    /**
     * @brief 强制进入配网模式
     */
    void startConfigPortal();

    /**
     * @brief 清除所有配置并重启
     */
    void resetSettings();

private:
    /**
     * @brief 尝试连接 WiFi
     */
    bool _connectWiFi();

    /**
     * @brief 设置 LED 状态
     * @param mode 0=慢闪(未连接) 1=快闪(配网) 2=常亮(已连接)
     */
    void _setLEDMode(uint8_t mode);

    /**
     * @brief 检测按钮长按
     */
    bool _checkButtonLongPress();

    /**
     * @brief 加载/保存配置到 LittleFS
     */
    bool _loadConfig();
    bool _saveConfig();

    /**
     * @brief 保存 WiFi 参数回调 (WiFiManager 的回调)
     */
    static void _saveParamsCallback();
    static void _apCallback(WiFiManager* wm);

    WiFiManager     _wm;
    DeviceConfig    _config;
    bool            _connected;
    uint32_t        _lastCheckTime;
    uint32_t        _buttonPressTime;
    bool            _buttonPressed;

    // LED 闪烁状态
    uint32_t        _ledLastToggle;
    bool            _ledState;

    // WiFiManager 自定义参数
    WiFiManagerParameter* _param_mqtt_server;
    WiFiManagerParameter* _param_mqtt_port;
    WiFiManagerParameter* _param_mqtt_user;
    WiFiManagerParameter* _param_mqtt_pass;
    WiFiManagerParameter* _param_device_id;
    WiFiManagerParameter* _param_env_note;
};

#endif // WIFI_MANAGER_H

-     * @param mode 0=慢闪(未连接) 1=快闪(配网) 2=常亮(已连接)

+     * @param mode 0=灭 1=慢闪(断连1s) 2=快闪(配网300ms) 3=常亮(已连接)

+     * @brief LED 闪烁独立任务 (FreeRTOS, 不受主循环阻塞影响)

+    static void _ledTaskFunc(void* param);

+    // LED 独立任务 (不被 startConfigPortal 阻塞影响)

+    volatile uint8_t _ledMode;  // 0=灭 1=慢闪 2=快闪 3=常亮


Now update the .cpp file. I need to:
1. Update constructor to initialize `_ledMode` and `_ledTaskHandle`
2. Add `_ledTaskFunc` implementation
3. Create the task in `begin()`
4. Replace `_setLEDMode`
5. Remove LED blink from `loop()`
6. Update `resetSettings()` (which currently uses `delay()` for LED flashing — that's fine since it's about to restart anyway)


现在更新 `.cpp` 文件——构造函数、独立 LED 任务、去掉 loop 里的旧闪烁逻辑：


WiFiMgmt::WiFiMgmt()
    : _connected(false)
    , _lastCheckTime(0)
    , _buttonPressTime(0)
    , _buttonPressed(false)
    , _ledLastToggle(0)
    , _ledState(false)
    , _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()
    : _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;  // 设置单例
}


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)

WiFiMgmt::WiFiMgmt()
    : _connected(false)
    , _lastCheckTime(0)
    , _buttonPressTime(0)
    , _buttonPressed(false)
    , _ledLastToggle(0)
    , _ledState(false)
    , _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()
    : _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;  // 设置单例
}

, _lastCheckTime(0)
, _buttonPressTime(0)
, _buttonPressed(false)
-    , _ledLastToggle(0)
-    , _ledState(false)
+    , _ledTaskHandle(nullptr)
+    , _ledMode(0)
, _param_mqtt_server(nullptr)
, _param_mqtt_port(nullptr)
, _param_mqtt_user(nullptr)


Now update `begin()` to create the LED task:


在 `begin()` 里创建 LED 任务：


    // 初始化引脚
    pinMode(CONFIG_BUTTON_PIN, INPUT_PULLUP);
    pinMode(LED_WIFI_PIN, OUTPUT);
    digitalWrite(LED_WIFI_PIN, LOW);

    // 初始化引脚
    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);


    // 初始化引脚
    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);

+    // 启动 LED 独立任务 (避免 startConfigPortal 阻塞影响闪烁)

+    xTaskCreate(_ledTaskFunc, "WiFiLed", 1024, nullptr, 1, &_ledTaskHandle);

Serial.println(F("[WiFi] ❌ LittleFS 挂载失败!"));


Now replace the LED blink logic in `loop()`:


去掉 `loop()` 里旧的 LED 闪烁逻辑：


    // LED 闪烁处理
    if (!_connected) {
        uint32_t interval = (_wm.getConfigPortalActive()) ? 300 : 1000;
        if (millis() - _ledLastToggle > interval) {
            _ledLastToggle = millis();
            _ledState = !_ledState;
            digitalWrite(LED_WIFI_PIN, _ledState ? HIGH : LOW);
        }
    }

    return _connected;
}

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


    // LED 闪烁处理
    if (!_connected) {
        uint32_t interval = (_wm.getConfigPortalActive()) ? 300 : 1000;
        if (millis() - _ledLastToggle > interval) {
            _ledLastToggle = millis();
            _ledState = !_ledState;
            digitalWrite(LED_WIFI_PIN, _ledState ? HIGH : LOW);
        }
    }

    return _connected;
}

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

-        uint32_t interval = (_wm.getConfigPortalActive()) ? 300 : 1000;

-        if (millis() - _ledLastToggle > interval) {

-            digitalWrite(LED_WIFI_PIN, _ledState ? HIGH : LOW);

+    // LED 由独立 FreeRTOS 任务处理 (不受 startConfigPortal 阻塞影响)


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


Now replace the `_setLEDMode()` function and add the `_ledTaskFunc()` implementation:


替换 `_setLEDMode()` 并添加 LED 独立任务函数：


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


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

+// LED 模式控制 (线程安全, 仅写模式变量，由独立任务执行)

-        digitalWrite(LED_WIFI_PIN, HIGH);

+    _ledMode = mode;  // uint8_t 赋值在 ESP32 上是原子的

+void WiFiMgmt::_ledTaskFunc(void* param) {

+    const TickType_t fastMs = pdMS_TO_TICKS(300);   // 快闪 = 配网模式

+    const TickType_t slowMs = pdMS_TO_TICKS(1000);  // 慢闪 = 未连接

+        uint8_t mode = _instance->_ledMode;

+            digitalWrite(LED_WIFI_PIN, LOW);

+            digitalWrite(LED_WIFI_PIN, HIGH);


Let me verify the changes by reading the full files to make sure everything is consistent.


验证一下修改后的两个文件是否一致：