1	/**
2	 * @file wifi_manager.cpp
3	 * @brief WiFi 配网管理器 — AP + WebServer
4	 */
5
6	#include "wifi_manager.h"
7
8	// BLE 配网回调: 小程序写入 JSON → 仅在 BLE 栈中暂存, 不直接处理
9	class BLEProvisionCB : public BLECharacteristicCallbacks {
10	    void onWrite(BLECharacteristic* ch) override {
11	        std::string val = ch->getValue();
12	        if (val.length() > 0 && WiFiMgmt::_instance) {
13	            Serial.printf("[BLE] 收到配网数据 (%d 字节)\n", val.length());
14	            // 仅暂存 → 在 loop() 里处理 (BTC_TASK 栈太小, 写 Flash 会栈溢出)
15	            WiFiMgmt::_instance->enqueueBLEJson(val.c_str());
16	        }
17	    }
18	};
19
20	WiFiMgmt* WiFiMgmt::_instance = nullptr;
21
22	WiFiMgmt::WiFiMgmt()
23	    : _server(nullptr), _dns(nullptr)
24	    , _connected(false)
25	    , _portalActive(false), _portalStopFlag(false)
26	    , _portalStartTime(0)
27	    , _lastCheckTime(0), _buttonPressTime(0), _buttonPressed(false)
28	    , _lastScanTime(0)
29	    , _provisionSuccess(false)
30	    , _ledTaskHandle(nullptr), _ledMode(0)
31	    , _bleServer(nullptr), _bleInfoChar(nullptr), _bleChar(nullptr), _bleAdv(nullptr)
32	    , _blePendingFlag(false)
33	{
34	    memset(&_config, 0, sizeof(_config));
35	    _instance = this;
36	}
37
38	WiFiMgmt::~WiFiMgmt() {
39	    _stopPortal();
40	    _instance = nullptr;
41	}
42
43	// ============================================================
44	// 初始化
45	// ============================================================
46
47	bool WiFiMgmt::begin() {
48	    Serial.println(("\n=== WiFi 配网管理器 (SmartConfig) ==="));
49
50	    pinMode(CONFIG_BUTTON_PIN, INPUT_PULLUP);
51	    pinMode(LED_WIFI_PIN, OUTPUT);
52	    digitalWrite(LED_WIFI_PIN, LOW);
53
54	    xTaskCreate(_ledTaskFunc, "WiFiLed", 1024, nullptr, 1, &_ledTaskHandle);
55
56	    // 冷启动延迟, 等 Flash 稳定
57	    delay(500);
58	    if (!LittleFS.begin(true)) {
59	        Serial.println(("[WiFi] LittleFS 挂载失败!"));
60	        return false;
61	    }
62	    Serial.println(("[WiFi] LittleFS 就绪"));
63
64	    if (!_loadConfig()) {
65	        Serial.println(("[WiFi] 使用默认配置"));
66	        strncpy(_config.mqtt_server, MQTT_DEFAULT_SERVER, sizeof(_config.mqtt_server)-1);
67	        strncpy(_config.mqtt_port,   MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port)-1);
68	        strncpy(_config.mqtt_user,   MQTT_DEFAULT_USER, sizeof(_config.mqtt_user)-1);
69	        strncpy(_config.mqtt_pass,   MQTT_DEFAULT_PASS, sizeof(_config.mqtt_pass)-1);
70	        strncpy(_config.device_id,   MQTT_DEFAULT_DEVICE, sizeof(_config.device_id)-1);
71	        strncpy(_config.env_note,    MQTT_DEFAULT_ENV, sizeof(_config.env_note)-1);
72	        strncpy(_config.env_type,    "bedroom",        sizeof(_config.env_type)-1);
73	        memset(_config.wifi_ssid, 0, sizeof(_config.wifi_ssid));
74	        memset(_config.wifi_pass, 0, sizeof(_config.wifi_pass));
75	    }
76
77	    WiFi.setAutoReconnect(true);
78
79	    // 有配置 → 直接连
80	    bool hasConfig = LittleFS.exists(CONFIG_FILE_DEVICE);
81	    Serial.printf("[WiFi] 配置文件: %s\n", hasConfig ? "存在" : "不存在");
82	    if (hasConfig) {
83	        _connected = _connectWiFi();
84	        Serial.printf("[WiFi] 自动连接: %s\n", _connected ? "成功" : "失败");
85	    } else {
86	        Serial.println(("[WiFi] 无配置, 跳过连接"));
87	        _connected = false;
88	    }
89
90	    if (!_connected) {
91	        _setLEDMode(2);  // 快闪 = 配网模式
92	        _startPortal();  // 只开 AP
93	    } else {
94	        _setLEDMode(3);  // 常亮
95	    }
96
97	    _lastCheckTime = millis();
98	    return _connected;
99	}
100
101	// ============================================================
102	// 主循环
103	// ============================================================
104
105	bool WiFiMgmt::loop() {
106	    // --- BLE 待处理数据 (从 loop 上下文安全处理, 不占 BTC_TASK 栈) ---
107	    _processBLEPending();
108
109	    // --- Portal 模式 ---
110	    if (_portalActive) {
111	        if (_portalStopFlag) {
112	            _stopPortal();
113	            _portalStopFlag = false;
114	            if (_pendingSSID.length() > 0) {
115	                Serial.printf("[WiFi] 尝试连接: %s\n", _pendingSSID.c_str());
116	                _connected = _tryConnect(_pendingSSID.c_str(), _pendingPass.c_str());
117	                _pendingSSID = ""; _pendingPass = "";
118	                if (_connected) {
119	                    _setLEDMode(3);
120	                    _provisionSuccess = true;  // 标记配网成功
121	                } else {
122	                    _setLEDMode(2);
123	                    _startPortal();
124	                }
125	            }
126	            return _connected;
127	        }
128
129	        _dns->processNextRequest();
130	        _server->handleClient();
131	        delay(10);
132	        return _connected;
133	    }
134
135	    // --- 正常运行 ---
136	    if (_checkButtonLongPress()) {
137	        Serial.println(("[WiFi] 长按, 进配网模式"));
138	        _setLEDMode(2);
139	        WiFi.disconnect();
140	        _startPortal();
141	        _connected = false;
142	        return false;
143	    }
144
145	    if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {
146	        _lastCheckTime = millis();
147	        wl_status_t s = WiFi.status();
148	        if (s != WL_CONNECTED) {
149	            if (_connected) {
150	                _connected = false;
151	                _setLEDMode(1);
152	                Serial.printf("[WiFi] 断开! status=%d RSSI=%d\n", s, WiFi.RSSI());
153	            }
154	            // 不手动 reconnect(), 让 WiFi.setAutoReconnect(true) 自动处理
155	        } else if (!_connected) {
156	            _connected = true;
157	            _setLEDMode(3);
158	            Serial.printf("[WiFi] 已连接 RSSI=%d\n", WiFi.RSSI());
159	        }
160	    }
161
162	    return _connected;
163	}
164
165	// ============================================================
166	// WiFi 连接
167	// ============================================================
168
169	bool WiFiMgmt::_connectWiFi() {
170	    if (_config.wifi_ssid[0] == '\0') {
171	        Serial.println(("[WiFi] 无已存WiFi密码"));
172	        return false;
173	    }
174	    Serial.printf("[WiFi] 连接: %s\n", _config.wifi_ssid);
175
176	    // 崩溃重启后 WiFi 模块可能残留旧状态, 彻底重置
177	    WiFi.disconnect(true);
178	    delay(200);
179	    WiFi.mode(WIFI_STA);
180	    delay(100);
181	    WiFi.begin(_config.wifi_ssid, _config.wifi_pass);
182	    int retry = 0;
183	    while (WiFi.status() != WL_CONNECTED && retry < 40) {  // 20s
184	        delay(500); Serial.print("."); yield(); retry++;
185	    }
186	    Serial.println();
187	    _connected = (WiFi.status() == WL_CONNECTED);
188	    Serial.printf("[WiFi] 连接结果: %s  status=%d\n", _connected ? "成功" : "失败", WiFi.status());
189	    if (_connected) Serial.printf("[WiFi] SSID=%s IP=%s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
190	    return _connected;
191	}
192
193	bool WiFiMgmt::_tryConnect(const char* ssid, const char* pass) {
194	    // ★ 先从 AP 模式彻底断开再切 STA，和 _connectWiFi() 保持一致
195	    WiFi.disconnect(true);
196	    delay(200);
197	    WiFi.mode(WIFI_STA);
198	    delay(100);
199	    WiFi.persistent(true);   // 确保持久化 WiFi 密码到 NVS (断电不丢)
200	    WiFi.begin(ssid, pass);
201	    int retry = 0;
202	    while (WiFi.status() != WL_CONNECTED && retry < 40) {  // 20s
203	        delay(500); Serial.print("."); retry++;
204	    }
205	    bool ok = (WiFi.status() == WL_CONNECTED);
206	    Serial.printf("\n[WiFi] 连接结果: %s  status=%d\n", ok ? "成功!" : "失败", WiFi.status());
207	    return ok;
208	}
209
210	// ============================================================
211	// 配网 Portal (AP + WebServer)
212	// ============================================================
213
214	void WiFiMgmt::startConfigPortal() {
215	    _setLEDMode(2);
216	    _startPortal();
217	}
218
219	void WiFiMgmt::_startPortal() {
220	    if (_portalActive) return;
221
222	    WiFi.disconnect(true);
223	    delay(200);
224	    WiFi.mode(WIFI_AP);
225	    WiFi.softAPConfig(IPAddress(192,168,4,1), IPAddress(192,168,4,1), IPAddress(255,255,255,0));
226	    WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);
227	    Serial.printf("[WiFi] AP: %s  IP: 192.168.4.1\n", WIFI_AP_SSID);
228
229	    // DNS 劫持: 所有域名 → 192.168.4.1 → 触发手机强制门户弹窗
230	    _dns = new DNSServer();
231	    _dns->start(53, "*", WiFi.softAPIP());
232
233	    _server = new WebServer(80);
234	    _server->on("/",      HTTP_GET,  [this]() { _handleRoot(); });
235	    _server->on("/save",  HTTP_POST, [this]() { _handleSave(); });
236	    _server->on("/reset", HTTP_GET,  [this]() { _handleReset(); });
237	    _server->onNotFound(  [this]() { _handleRoot(); });
238	    _server->begin();
239
240	    _portalActive = true;
241	    _portalStartTime = millis();
242
243	    Serial.println(("[WiFi] 手机连 AP 后浏览器打开 http://192.168.4.1"));
244
245	    _startBLE();  // 同时启动 BLE 配网广播
246	}
247
248	void WiFiMgmt::_stopPortal() {
249	    if (!_portalActive) return;
250	    _stopBLE();
251	    if (_server) { _server->stop(); delete _server; _server = nullptr; }
252	    if (_dns)    { _dns->stop();    delete _dns;    _dns = nullptr; }
253	    WiFi.softAPdisconnect(true);
254	    _portalActive = false;
255	}
256
257	// ============================================================
258	// Web 处理器
259	// ============================================================
260
261	void WiFiMgmt::_handleRoot() {
262	    _server->send(200, "text/html; charset=utf-8", _htmlPage());
263	}
264
265	void WiFiMgmt::_handleSave() {
266	    if (_server->hasArg("s")) {
267	        _pendingSSID = _server->arg("s");
268	        _pendingPass = _server->arg("p");
269	        // 持久化 WiFi 密码到 LittleFS
270	        strncpy(_config.wifi_ssid, _pendingSSID.c_str(), sizeof(_config.wifi_ssid)-1);
271	        strncpy(_config.wifi_pass, _pendingPass.c_str(), sizeof(_config.wifi_pass)-1);
272	        if (_server->hasArg("device_id"))  strncpy(_config.device_id, _server->arg("device_id").c_str(), sizeof(_config.device_id)-1);
273	        if (_server->hasArg("env_type"))   strncpy(_config.env_type,  _server->arg("env_type").c_str(),  sizeof(_config.env_type)-1);
274	        if (_server->hasArg("env_note"))   strncpy(_config.env_note,  _server->arg("env_note").c_str(),  sizeof(_config.env_note)-1);
275	        if (_server->hasArg("mqtt_server")) strncpy(_config.mqtt_server, _server->arg("mqtt_server").c_str(), sizeof(_config.mqtt_server)-1);
276	        if (_server->hasArg("mqtt_port"))  strncpy(_config.mqtt_port,  _server->arg("mqtt_port").c_str(), sizeof(_config.mqtt_port)-1);
277	        if (_server->hasArg("mqtt_user"))  strncpy(_config.mqtt_user,  _server->arg("mqtt_user").c_str(), sizeof(_config.mqtt_user)-1);
278	        if (_server->hasArg("mqtt_pass"))  strncpy(_config.mqtt_pass,  _server->arg("mqtt_pass").c_str(), sizeof(_config.mqtt_pass)-1);
279	        _saveConfig();
280
281	        _server->send(200, "text/html; charset=utf-8",
282	            "<!DOCTYPE html><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1'>"
283	            "<title>配置完成</title>

⏳
"
288	            "
配置已保存

设备正在连接 WiFi...
等待指示灯常亮
"
289	            "
");
294
295	        _portalStopFlag = true;
296	        return;
297	    }
298	    _handleRoot();
299	}
300
301	void WiFiMgmt::_handleReset() {
302	    _server->send(200, "text/html; charset=utf-8",
303	        "<!DOCTYPE html><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>已重置</title>"
304	        "

配置已清除

设备正在重启...

");
307	    delay(500);
308	    _stopPortal();
309	    resetSettings();
310	}
311
312	// ============================================================
313	// 配网 HTML 页面 (精简版)
314	// ============================================================
315
316	void WiFiMgmt::_updateWifiScan() {
317	    _wifiScanCache = "";
318	    int n = WiFi.scanNetworks(false, true);
319	    for (int i = 0; i < n; i++) {
320	        _wifiScanCache += "<option value='" + WiFi.SSID(i) + "'>" + WiFi.SSID(i) + " (" + String(WiFi.RSSI(i)) + "dBm)</option>";
321	    }
322	    if (n == 0) _wifiScanCache = "<option value=''>未发现 WiFi</option>";
323	    WiFi.scanDelete();
324	    _lastScanTime = millis();
325	}
326
327	String WiFiMgmt::_htmlPage() {
328	    if (_wifiScanCache.length() == 0) _updateWifiScan();
329
330	    String html;
331	    html.reserve(4096);
332	    html += "<!DOCTYPE html><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'>";
333	    html += "<meta name='theme-color' content='#12a889'><title>设备配网</title>
";
354
355	    // Header + 提示
356	    html += "

📡

观照-五劳无感 设备配网

WiFi 联网与参数配置

";
357	    html += "
💡 也可用 ESP-Touch App 一键配网
";
358
359	    html += "<form method='POST' action='/save'>";
360	    html += "

📶

无线网络

选择 WiFi 或使用 SmartConfig

";
361	    html += "
WiFi 名称
📡<select name='s'>";
362	    html += _wifiScanCache;
363	    html += "</select>

";
364	    html += "
WiFi 密码
🔒<input type='password' name='p' placeholder='请输入 WiFi 密码'>

";
365
366	    html += "

⚙

设备基础信息

唯一标识与安装位置

";
367	    html += "
设备编号
#<input type='text' name='device_id' value='";
368	    html += _config.device_id;
369	    html += "'>

";
370	    html += "
环境类型
🏷<select name='env_type'>";
371	    const char* envTypes[] = {"bedroom","livingroom","bathroom","hallway","balcony","kitchen"};
372	    const char* envLabels[] = {"🛏 卧室","🛋 客厅","🛁 浴室/卫生间","🚶 走廊","🌿 阳台","🍳 厨房"};
373	    for (int i = 0; i < 6; i++) {
374	        html += "<option value='";
375	        html += envTypes[i];
376	        html += "'";
377	        if (strcmp(_config.env_type, envTypes[i]) == 0 || (strlen(_config.env_type) == 0 && i == 0))
378	            html += " selected";
379	        html += ">";
380	        html += envLabels[i];
381	        html += "</option>";
382	    }
383	    html += "</select>

";
384	    html += "
位置备注
🏠<input type='text' name='env_note' placeholder='如: 主卧 / 客厅 / 老人房' value='";
385	    html += _config.env_note;
386	    html += "'>

";
387
388	    html += "<input type='hidden' name='mqtt_server' value='";
389	    html += _config.mqtt_server;
390	    html += "'><input type='hidden' name='mqtt_port' value='";
391	    html += _config.mqtt_port;
392	    html += "'><input type='hidden' name='mqtt_user' value='";
393	    html += _config.mqtt_user;
394	    html += "'><input type='hidden' name='mqtt_pass' value='";
395	    html += _config.mqtt_pass;
396	    html += "'>";
397
398	    html += "
<button type='submit'>保存配置并重启连接</button>
</form>";
399	    html += "<button class='danger' onclick=\"if(confirm('确定恢复出厂?'))location.href='/reset'\">恢复出厂配置</button>";
400	    html += "
Health Monitor · Device Provisioning

";
401
402	    return html;
403	}
404
405	// ============================================================
406	// BLE 蓝牙配网
407	// ============================================================
408
409	void WiFiMgmt::_startBLE() {
410	    // 如果 BLE 栈已在运行, 仅重启广播 (避免 deinit→init 失败)
411	    if (_bleServer != nullptr) {
412	        if (_bleAdv) {
413	            // 重设广告数据 (stop 后 start 需要重新配置)
414	            BLEAdvertisementData advData;
415	            advData.setCompleteServices(BLEUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b"));
416	            advData.setShortName("HealthMon");
417	            _bleAdv->setAdvertisementData(advData);
418	            _bleAdv->setScanResponse(false);
419	            _bleAdv->start();
420	        }
421	        Serial.println("[BLE] ✅ 广播已重开");
422	        return;
423	    }
424
425	    BLEDevice::init("HealthMonitor");
426	    _bleServer = BLEDevice::createServer();
427
428	    BLEService* svc = _bleServer->createService("4fafc201-1fb5-459e-8fcc-c5c9c331914b");
429
430	    // 特征1: 设备信息 (READ) — 小程序连接后读取设备编号等
431	    _bleInfoChar = svc->createCharacteristic(
432	        "4fafc202-1fb5-459e-8fcc-c5c9c331914b",
433	        BLECharacteristic::PROPERTY_READ
434	    );
435	    // 设备信息 JSON (最多 512 字节)
436	    JsonDocument info;
437	    info["device_id"]   = _config.device_id;
438	    info["mqtt_server"] = _config.mqtt_server;
439	    info["mqtt_port"]   = _config.mqtt_port;
440	    info["fw_version"]  = FW_VERSION;
441	    String infoJson;
442	    serializeJson(info, infoJson);
443	    _bleInfoChar->setValue(infoJson.c_str());
444
445	    // 特征2: 配网写入 (WRITE + NOTIFY) — 小程序发配网数据, 设备回复
446	    _bleChar = svc->createCharacteristic(
447	        "4fafc203-1fb5-459e-8fcc-c5c9c331914b",
448	        BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY
449	    );
450	    _bleChar->setCallbacks(new BLEProvisionCB());
451	    _bleChar->addDescriptor(new BLE2902());  // 启用 notify
452	    _bleChar->setValue("");  // 初始为空
453
454	    svc->start();
455
456	    _bleAdv = _bleServer->getAdvertising();
457	    BLEAdvertisementData advData;
458	    advData.setCompleteServices(BLEUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b"));
459	    advData.setShortName("HealthMon");
460	    _bleAdv->setAdvertisementData(advData);
461	    _bleAdv->setScanResponse(false);  // 不需要 scan response, 全在主包
462	    _bleAdv->start();
463
464	    Serial.printf("[BLE] ✅ 配网广播: HealthMonitor  device_id=%s\n", _config.device_id);
465	}
466
467	void WiFiMgmt::_stopBLE() {
468	    if (!_bleServer) return;
469
470	    // ★ 核心修复: 先断开所有 BLE 客户端连接,
471	    //    否则手机端缓存连接状态, 下次扫描不到设备
472	    size_t connCount = _bleServer->getConnectedCount();
473	    if (connCount > 0) {
474	        Serial.printf("[BLE] 断开 %d 个已连接客户端\n", connCount);
475	        for (size_t i = 0; i < connCount; i++) {
476	            _bleServer->disconnect(i);
477	        }
478	        delay(300);  // 等待 BLE 协议栈完成断开流程
479	    }
480
481	    if (_bleAdv) { _bleAdv->stop(); }
482	    Serial.println("[BLE] 广播已暂停");
483	}
484
485	// BLE 线程安全入队 (仅暂存, 不在 BTC_TASK 回调里处理)
486	void WiFiMgmt::enqueueBLEJson(const char* json) {
487	    _blePendingJson  = json;
488	    _blePendingFlag  = true;
489	}
490
491	// 在 loop() 上下文中处理 BLE 配网数据 (安全 — 不在 BTC_TASK 栈中)
492	void WiFiMgmt::_processBLEPending() {
493	    if (!_blePendingFlag) return;
494	    _blePendingFlag = false;
495	    Serial.printf("[BLE] loop 处理配网: %s\n", _blePendingJson.c_str());
496	    handleBLEProvision(_blePendingJson.c_str());
497	    _blePendingJson = "";
498	}
499
500	void WiFiMgmt::handleBLEProvision(const char* json) {
501	    JsonDocument doc;
502	    DeserializationError err = deserializeJson(doc, json);
503	    if (err) {
504	        Serial.printf("[BLE] JSON 解析失败: %s\n", err.c_str());
505	        if (_bleChar) { _bleChar->setValue("ERR_JSON"); _bleChar->notify(); }
506	        return;
507	    }
508
509	    // 提取 WiFi 凭证
510	    const char* ssid = doc["s"] | "";
511	    const char* pass = doc["p"] | "";
512
513	    if (strlen(ssid) == 0) {
514	        Serial.println("[BLE] 未提供 WiFi SSID");
515	        if (_bleChar) { _bleChar->setValue("ERR_NO_SSID"); _bleChar->notify(); }
516	        return;
517	    }
518
519	    _pendingSSID = ssid;
520	    _pendingPass = pass;
521
522	    // 保存到 LittleFS (和 Web 配网完全相同的逻辑)
523	    strncpy(_config.wifi_ssid, ssid, sizeof(_config.wifi_ssid) - 1);
524	    strncpy(_config.wifi_pass, pass, sizeof(_config.wifi_pass) - 1);
525
526	    if (doc.containsKey("device_id"))   strncpy(_config.device_id,   doc["device_id"]   | "", sizeof(_config.device_id) - 1);
527	    if (doc.containsKey("env_note"))    strncpy(_config.env_note,    doc["env_note"]    | "", sizeof(_config.env_note) - 1);
528	    if (doc.containsKey("env_type"))    strncpy(_config.env_type,    doc["env_type"]    | "", sizeof(_config.env_type) - 1);
529	    if (doc.containsKey("mqtt_server")) strncpy(_config.mqtt_server, doc["mqtt_server"] | "", sizeof(_config.mqtt_server) - 1);
530	    if (doc.containsKey("mqtt_port"))   strncpy(_config.mqtt_port,   doc["mqtt_port"]   | "", sizeof(_config.mqtt_port) - 1);
531	    if (doc.containsKey("mqtt_user"))   strncpy(_config.mqtt_user,   doc["mqtt_user"]   | "", sizeof(_config.mqtt_user) - 1);
532	    if (doc.containsKey("mqtt_pass"))   strncpy(_config.mqtt_pass,   doc["mqtt_pass"]   | "", sizeof(_config.mqtt_pass) - 1);
533
534	    _saveConfig();
535	    _portalStopFlag = true;  // 触发延迟关闭 AP+BLE → 连接 WiFi
536
537	    Serial.printf("[BLE] ✅ 配网完成, SSID=%s\n", ssid);
538
539	    // 回复 OK
540	    if (_bleChar) { _bleChar->setValue("OK"); _bleChar->notify(); }
541	    delay(100);
542	}
543
544	// ============================================================
545	// 重置
546	// ============================================================
547
548	void WiFiMgmt::resetSettings() {
549	    Serial.println(("[WiFi] 清除所有配置..."));
550	    if (LittleFS.exists(CONFIG_FILE_WIFI))   LittleFS.remove(CONFIG_FILE_WIFI);
551	    if (LittleFS.exists(CONFIG_FILE_MQTT))   LittleFS.remove(CONFIG_FILE_MQTT);
552	    if (LittleFS.exists(CONFIG_FILE_DEVICE)) LittleFS.remove(CONFIG_FILE_DEVICE);
553	    WiFi.disconnect(true, true);
554	    for (int i = 0; i < 10; i++) { digitalWrite(LED_WIFI_PIN, !digitalRead(LED_WIFI_PIN)); delay(100); }
555	    Serial.println(("[WiFi] 正在重启..."));
556	    delay(500);
557	    ESP.restart();
558	}
559
560	// ============================================================
561	// 按钮
562	// ============================================================
563
564	bool WiFiMgmt::_checkButtonLongPress() {
565	    bool pressed = (digitalRead(CONFIG_BUTTON_PIN) == LOW);
566	    if (pressed && !_buttonPressed) {
567	        _buttonPressed = true; _buttonPressTime = millis();
568	    } else if (!pressed && _buttonPressed) {
569	        _buttonPressed = false;
570	        if (millis() - _buttonPressTime >= BUTTON_LONG_PRESS_MS) return true;
571	    }
572	    return false;
573	}
574
575	// ============================================================
576	// LittleFS
577	// ============================================================
578
579	bool WiFiMgmt::_loadConfig() {
580	    if (!LittleFS.exists(CONFIG_FILE_DEVICE)) return false;
581	    File file = LittleFS.open(CONFIG_FILE_DEVICE, "r");
582	    if (!file) return false;
583	    JsonDocument doc;
584	    DeserializationError err = deserializeJson(doc, file);
585	    file.close();
586	    if (err) return false;
587	    strncpy(_config.mqtt_server, doc["mqtt_server"] | MQTT_DEFAULT_SERVER, sizeof(_config.mqtt_server)-1);
588	    strncpy(_config.mqtt_port, doc["mqtt_port"] | MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port)-1);
589	    strncpy(_config.mqtt_user, doc["mqtt_user"] | MQTT_DEFAULT_USER, sizeof(_config.mqtt_user)-1);
590	    strncpy(_config.mqtt_pass, doc["mqtt_pass"] | MQTT_DEFAULT_PASS, sizeof(_config.mqtt_pass)-1);
591	    strncpy(_config.device_id, doc["device_id"] | MQTT_DEFAULT_DEVICE, sizeof(_config.device_id)-1);
592	    strncpy(_config.env_note, doc["env_note"] | MQTT_DEFAULT_ENV, sizeof(_config.env_note)-1);
593	    strncpy(_config.env_type, doc["env_type"] | "bedroom", sizeof(_config.env_type)-1);
594	    strncpy(_config.wifi_ssid, doc["wifi_ssid"] | "", sizeof(_config.wifi_ssid)-1);
595	    strncpy(_config.wifi_pass, doc["wifi_pass"] | "", sizeof(_config.wifi_pass)-1);
596	    Serial.printf("[WiFi] 配置加载, 已存WiFi: %s\n", _config.wifi_ssid[0] ? _config.wifi_ssid : "(空)");
597	    return true;
598	}
599
600	bool WiFiMgmt::_saveConfig() {
601	    JsonDocument doc;
602	    doc["mqtt_server"] = _config.mqtt_server;
603	    doc["mqtt_port"]   = _config.mqtt_port;
604	    doc["mqtt_user"]   = _config.mqtt_user;
605	    doc["mqtt_pass"]   = _config.mqtt_pass;
606	    doc["device_id"]   = _config.device_id;
607	    doc["env_note"]    = _config.env_note;
608	    doc["env_type"]    = _config.env_type;
609	    doc["wifi_ssid"]   = _config.wifi_ssid;
610	    doc["wifi_pass"]   = _config.wifi_pass;
611	    File file = LittleFS.open(CONFIG_FILE_DEVICE, "w");
612	    if (!file) return false;
613	    if (serializeJson(doc, file) == 0) { file.close(); return false; }
614	    file.close();
615	    return true;
616	}
617
618	// ============================================================
619	// Getter
620	// ============================================================
621
622	bool WiFiMgmt::isConnected() const { return _connected && (WiFi.status() == WL_CONNECTED); }
623	bool WiFiMgmt::justProvisioned() { bool v = _provisionSuccess; _provisionSuccess = false; return v; }
624	DeviceConfig WiFiMgmt::getConfig() const { return _config; }
625	const char* WiFiMgmt::getDeviceId() const { return _config.device_id; }
626	const char* WiFiMgmt::getEnvNote() const { return _config.env_note; }
627
628	// ============================================================
629	// LED
630	// ============================================================
631
632	void WiFiMgmt::_setLEDMode(uint8_t mode) { _ledMode = mode; }
633
634	void WiFiMgmt::_ledTaskFunc(void* param) {
635	    const TickType_t fastMs = pdMS_TO_TICKS(300);
636	    const TickType_t slowMs = pdMS_TO_TICKS(1000);
637	    while (true) {
638	        if (!_instance) { vTaskDelay(slowMs); continue; }
639	        uint8_t mode = _instance->_ledMode;
640	        switch (mode) {
641	        case 0: digitalWrite(LED_WIFI_PIN, LOW); vTaskDelay(slowMs); break;
642	        case 1: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(slowMs);
643	                digitalWrite(LED_WIFI_PIN, LOW);  vTaskDelay(slowMs); break;
644	        case 2: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(fastMs);
645	                digitalWrite(LED_WIFI_PIN, LOW);  vTaskDelay(fastMs); break;
646	        case 3: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(slowMs); break;
647	        }
648	    }
649	}
650

/**
 * @file wifi_manager.cpp
 * @brief WiFi 配网管理器 — AP + WebServer
 */

#include "wifi_manager.h"

// BLE 配网回调: 小程序写入 JSON → 仅在 BLE 栈中暂存, 不直接处理
class BLEProvisionCB : public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic* ch) override {
        std::string val = ch->getValue();
        if (val.length() > 0 && WiFiMgmt::_instance) {
            Serial.printf("[BLE] 收到配网数据 (%d 字节)\n", val.length());
            // 仅暂存 → 在 loop() 里处理 (BTC_TASK 栈太小, 写 Flash 会栈溢出)
            WiFiMgmt::_instance->enqueueBLEJson(val.c_str());
        }
    }
};

WiFiMgmt* WiFiMgmt::_instance = nullptr;

WiFiMgmt::WiFiMgmt()
    : _server(nullptr), _dns(nullptr)
    , _connected(false)
    , _portalActive(false), _portalStopFlag(false)
    , _portalStartTime(0)
    , _lastCheckTime(0), _buttonPressTime(0), _buttonPressed(false)
    , _lastScanTime(0)
    , _provisionSuccess(false)
    , _ledTaskHandle(nullptr), _ledMode(0)
    , _bleServer(nullptr), _bleInfoChar(nullptr), _bleChar(nullptr), _bleAdv(nullptr)
    , _blePendingFlag(false)
{
    memset(&_config, 0, sizeof(_config));
    _instance = this;
}

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

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

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

    pinMode(CONFIG_BUTTON_PIN, INPUT_PULLUP);
    pinMode(LED_WIFI_PIN, OUTPUT);
    digitalWrite(LED_WIFI_PIN, LOW);

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

    // 冷启动延迟, 等 Flash 稳定
    delay(500);
    if (!LittleFS.begin(true)) {
        Serial.println(("[WiFi] LittleFS 挂载失败!"));
        return false;
    }
    Serial.println(("[WiFi] LittleFS 就绪"));

    if (!_loadConfig()) {
        Serial.println(("[WiFi] 使用默认配置"));
        strncpy(_config.mqtt_server, MQTT_DEFAULT_SERVER, sizeof(_config.mqtt_server)-1);
        strncpy(_config.mqtt_port,   MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port)-1);
        strncpy(_config.mqtt_user,   MQTT_DEFAULT_USER, sizeof(_config.mqtt_user)-1);
        strncpy(_config.mqtt_pass,   MQTT_DEFAULT_PASS, sizeof(_config.mqtt_pass)-1);
        strncpy(_config.device_id,   MQTT_DEFAULT_DEVICE, sizeof(_config.device_id)-1);
        strncpy(_config.env_note,    MQTT_DEFAULT_ENV, sizeof(_config.env_note)-1);
        strncpy(_config.env_type,    "bedroom",        sizeof(_config.env_type)-1);
        memset(_config.wifi_ssid, 0, sizeof(_config.wifi_ssid));
        memset(_config.wifi_pass, 0, sizeof(_config.wifi_pass));
    }

    WiFi.setAutoReconnect(true);

    // 有配置 → 直接连
    bool hasConfig = LittleFS.exists(CONFIG_FILE_DEVICE);
    Serial.printf("[WiFi] 配置文件: %s\n", hasConfig ? "存在" : "不存在");
    if (hasConfig) {
        _connected = _connectWiFi();
        Serial.printf("[WiFi] 自动连接: %s\n", _connected ? "成功" : "失败");
    } else {
        Serial.println(("[WiFi] 无配置, 跳过连接"));
        _connected = false;
    }

    if (!_connected) {
        _setLEDMode(2);  // 快闪 = 配网模式
        _startPortal();  // 只开 AP
    } else {
        _setLEDMode(3);  // 常亮
    }

    _lastCheckTime = millis();
    return _connected;
}

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

bool WiFiMgmt::loop() {
    // --- BLE 待处理数据 (从 loop 上下文安全处理, 不占 BTC_TASK 栈) ---
    _processBLEPending();

    // --- Portal 模式 ---
    if (_portalActive) {
        if (_portalStopFlag) {
            _stopPortal();
            _portalStopFlag = false;
            if (_pendingSSID.length() > 0) {
                Serial.printf("[WiFi] 尝试连接: %s\n", _pendingSSID.c_str());
                _connected = _tryConnect(_pendingSSID.c_str(), _pendingPass.c_str());
                _pendingSSID = ""; _pendingPass = "";
                if (_connected) {
                    _setLEDMode(3);
                    _provisionSuccess = true;  // 标记配网成功
                } else {
                    _setLEDMode(2);
                    _startPortal();
                }
            }
            return _connected;
        }

        _dns->processNextRequest();
        _server->handleClient();
        delay(10);
        return _connected;
    }

    // --- 正常运行 ---
    if (_checkButtonLongPress()) {
        Serial.println(("[WiFi] 长按, 进配网模式"));
        _setLEDMode(2);
        WiFi.disconnect();
        _startPortal();
        _connected = false;
        return false;
    }

    if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {
        _lastCheckTime = millis();
        wl_status_t s = WiFi.status();
        if (s != WL_CONNECTED) {
            if (_connected) {
                _connected = false;
                _setLEDMode(1);
                Serial.printf("[WiFi] 断开! status=%d RSSI=%d\n", s, WiFi.RSSI());
            }
            // 不手动 reconnect(), 让 WiFi.setAutoReconnect(true) 自动处理
        } else if (!_connected) {
            _connected = true;
            _setLEDMode(3);
            Serial.printf("[WiFi] 已连接 RSSI=%d\n", WiFi.RSSI());
        }
    }

    return _connected;
}

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

bool WiFiMgmt::_connectWiFi() {
    if (_config.wifi_ssid[0] == '\0') {
        Serial.println(("[WiFi] 无已存WiFi密码"));
        return false;
    }
    Serial.printf("[WiFi] 连接: %s\n", _config.wifi_ssid);

    // 崩溃重启后 WiFi 模块可能残留旧状态, 彻底重置
    WiFi.disconnect(true);
    delay(200);
    WiFi.mode(WIFI_STA);
    delay(100);
    WiFi.begin(_config.wifi_ssid, _config.wifi_pass);
    int retry = 0;
    while (WiFi.status() != WL_CONNECTED && retry < 40) {  // 20s
        delay(500); Serial.print("."); yield(); retry++;
    }
    Serial.println();
    _connected = (WiFi.status() == WL_CONNECTED);
    Serial.printf("[WiFi] 连接结果: %s  status=%d\n", _connected ? "成功" : "失败", WiFi.status());
    if (_connected) Serial.printf("[WiFi] SSID=%s IP=%s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
    return _connected;
}

bool WiFiMgmt::_tryConnect(const char* ssid, const char* pass) {
    // ★ 先从 AP 模式彻底断开再切 STA，和 _connectWiFi() 保持一致
    WiFi.disconnect(true);
    delay(200);
    WiFi.mode(WIFI_STA);
    delay(100);
    WiFi.persistent(true);   // 确保持久化 WiFi 密码到 NVS (断电不丢)
    WiFi.begin(ssid, pass);
    int retry = 0;
    while (WiFi.status() != WL_CONNECTED && retry < 40) {  // 20s
        delay(500); Serial.print("."); retry++;
    }
    bool ok = (WiFi.status() == WL_CONNECTED);
    Serial.printf("\n[WiFi] 连接结果: %s  status=%d\n", ok ? "成功!" : "失败", WiFi.status());
    return ok;
}

// ============================================================
// 配网 Portal (AP + WebServer)
// ============================================================

void WiFiMgmt::startConfigPortal() {
    _setLEDMode(2);
    _startPortal();
}

void WiFiMgmt::_startPortal() {
    if (_portalActive) return;

    WiFi.disconnect(true);
    delay(200);
    WiFi.mode(WIFI_AP);
    WiFi.softAPConfig(IPAddress(192,168,4,1), IPAddress(192,168,4,1), IPAddress(255,255,255,0));
    WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);
    Serial.printf("[WiFi] AP: %s  IP: 192.168.4.1\n", WIFI_AP_SSID);

    // DNS 劫持: 所有域名 → 192.168.4.1 → 触发手机强制门户弹窗
    _dns = new DNSServer();
    _dns->start(53, "*", WiFi.softAPIP());

    _server = new WebServer(80);
    _server->on("/",      HTTP_GET,  [this]() { _handleRoot(); });
    _server->on("/save",  HTTP_POST, [this]() { _handleSave(); });
    _server->on("/reset", HTTP_GET,  [this]() { _handleReset(); });
    _server->onNotFound(  [this]() { _handleRoot(); });
    _server->begin();

    _portalActive = true;
    _portalStartTime = millis();

    Serial.println(("[WiFi] 手机连 AP 后浏览器打开 http://192.168.4.1"));

    _startBLE();  // 同时启动 BLE 配网广播
}

void WiFiMgmt::_stopPortal() {
    if (!_portalActive) return;
    _stopBLE();
    if (_server) { _server->stop(); delete _server; _server = nullptr; }
    if (_dns)    { _dns->stop();    delete _dns;    _dns = nullptr; }
    WiFi.softAPdisconnect(true);
    _portalActive = false;
}

// ============================================================
// Web 处理器
// ============================================================

void WiFiMgmt::_handleRoot() {
    _server->send(200, "text/html; charset=utf-8", _htmlPage());
}

void WiFiMgmt::_handleSave() {
    if (_server->hasArg("s")) {
        _pendingSSID = _server->arg("s");
        _pendingPass = _server->arg("p");
        // 持久化 WiFi 密码到 LittleFS
        strncpy(_config.wifi_ssid, _pendingSSID.c_str(), sizeof(_config.wifi_ssid)-1);
        strncpy(_config.wifi_pass, _pendingPass.c_str(), sizeof(_config.wifi_pass)-1);
        if (_server->hasArg("device_id"))  strncpy(_config.device_id, _server->arg("device_id").c_str(), sizeof(_config.device_id)-1);
        if (_server->hasArg("env_type"))   strncpy(_config.env_type,  _server->arg("env_type").c_str(),  sizeof(_config.env_type)-1);
        if (_server->hasArg("env_note"))   strncpy(_config.env_note,  _server->arg("env_note").c_str(),  sizeof(_config.env_note)-1);
        if (_server->hasArg("mqtt_server")) strncpy(_config.mqtt_server, _server->arg("mqtt_server").c_str(), sizeof(_config.mqtt_server)-1);
        if (_server->hasArg("mqtt_port"))  strncpy(_config.mqtt_port,  _server->arg("mqtt_port").c_str(), sizeof(_config.mqtt_port)-1);
        if (_server->hasArg("mqtt_user"))  strncpy(_config.mqtt_user,  _server->arg("mqtt_user").c_str(), sizeof(_config.mqtt_user)-1);
        if (_server->hasArg("mqtt_pass"))  strncpy(_config.mqtt_pass,  _server->arg("mqtt_pass").c_str(), sizeof(_config.mqtt_pass)-1);
        _saveConfig();

        _server->send(200, "text/html; charset=utf-8",
            "<!DOCTYPE html><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1'>"
            "<title>配置完成</title>

⏳
"
            "
配置已保存

设备正在连接 WiFi...
等待指示灯常亮
"
            "
");

        _portalStopFlag = true;
        return;
    }
    _handleRoot();
}

void WiFiMgmt::_handleReset() {
    _server->send(200, "text/html; charset=utf-8",
        "<!DOCTYPE html><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>已重置</title>"
        "

配置已清除

设备正在重启...

");
    delay(500);
    _stopPortal();
    resetSettings();
}

// ============================================================
// 配网 HTML 页面 (精简版)
// ============================================================

void WiFiMgmt::_updateWifiScan() {
    _wifiScanCache = "";
    int n = WiFi.scanNetworks(false, true);
    for (int i = 0; i < n; i++) {
        _wifiScanCache += "<option value='" + WiFi.SSID(i) + "'>" + WiFi.SSID(i) + " (" + String(WiFi.RSSI(i)) + "dBm)</option>";
    }
    if (n == 0) _wifiScanCache = "<option value=''>未发现 WiFi</option>";
    WiFi.scanDelete();
    _lastScanTime = millis();
}

String WiFiMgmt::_htmlPage() {
    if (_wifiScanCache.length() == 0) _updateWifiScan();

    String html;
    html.reserve(4096);
    html += "<!DOCTYPE html><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'>";
    html += "<meta name='theme-color' content='#12a889'><title>设备配网</title>
";

    // Header + 提示
    html += "

📡

观照-五劳无感 设备配网

WiFi 联网与参数配置

";
    html += "
💡 也可用 ESP-Touch App 一键配网
";

    html += "<form method='POST' action='/save'>";
    html += "

📶

无线网络

选择 WiFi 或使用 SmartConfig

";
    html += "
WiFi 名称
📡<select name='s'>";
    html += _wifiScanCache;
    html += "</select>

";
    html += "
WiFi 密码
🔒<input type='password' name='p' placeholder='请输入 WiFi 密码'>

";

    html += "

⚙

设备基础信息

唯一标识与安装位置

";
    html += "
设备编号
#<input type='text' name='device_id' value='";
    html += _config.device_id;
    html += "'>

";
    html += "
环境类型
🏷<select name='env_type'>";
    const char* envTypes[] = {"bedroom","livingroom","bathroom","hallway","balcony","kitchen"};
    const char* envLabels[] = {"🛏 卧室","🛋 客厅","🛁 浴室/卫生间","🚶 走廊","🌿 阳台","🍳 厨房"};
    for (int i = 0; i < 6; i++) {
        html += "<option value='";
        html += envTypes[i];
        html += "'";
        if (strcmp(_config.env_type, envTypes[i]) == 0 || (strlen(_config.env_type) == 0 && i == 0))
            html += " selected";
        html += ">";
        html += envLabels[i];
        html += "</option>";
    }
    html += "</select>

";
    html += "
位置备注
🏠<input type='text' name='env_note' placeholder='如: 主卧 / 客厅 / 老人房' value='";
    html += _config.env_note;
    html += "'>

";

    html += "<input type='hidden' name='mqtt_server' value='";
    html += _config.mqtt_server;
    html += "'><input type='hidden' name='mqtt_port' value='";
    html += _config.mqtt_port;
    html += "'><input type='hidden' name='mqtt_user' value='";
    html += _config.mqtt_user;
    html += "'><input type='hidden' name='mqtt_pass' value='";
    html += _config.mqtt_pass;
    html += "'>";

    html += "
<button type='submit'>保存配置并重启连接</button>
</form>";
    html += "<button class='danger' onclick=\"if(confirm('确定恢复出厂?'))location.href='/reset'\">恢复出厂配置</button>";
    html += "
Health Monitor · Device Provisioning

";

    return html;
}

// ============================================================
// BLE 蓝牙配网
// ============================================================

void WiFiMgmt::_startBLE() {
    // 如果 BLE 栈已在运行, 仅重启广播 (避免 deinit→init 失败)
    if (_bleServer != nullptr) {
        if (_bleAdv) {
            // 重设广告数据 (stop 后 start 需要重新配置)
            BLEAdvertisementData advData;
            advData.setCompleteServices(BLEUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b"));
            advData.setShortName("HealthMon");
            _bleAdv->setAdvertisementData(advData);
            _bleAdv->setScanResponse(false);
            _bleAdv->start();
        }
        Serial.println("[BLE] ✅ 广播已重开");
        return;
    }

    BLEDevice::init("HealthMonitor");
    _bleServer = BLEDevice::createServer();

    BLEService* svc = _bleServer->createService("4fafc201-1fb5-459e-8fcc-c5c9c331914b");

    // 特征1: 设备信息 (READ) — 小程序连接后读取设备编号等
    _bleInfoChar = svc->createCharacteristic(
        "4fafc202-1fb5-459e-8fcc-c5c9c331914b",
        BLECharacteristic::PROPERTY_READ
    );
    // 设备信息 JSON (最多 512 字节)
    JsonDocument info;
    info["device_id"]   = _config.device_id;
    info["mqtt_server"] = _config.mqtt_server;
    info["mqtt_port"]   = _config.mqtt_port;
    info["fw_version"]  = FW_VERSION;
    String infoJson;
    serializeJson(info, infoJson);
    _bleInfoChar->setValue(infoJson.c_str());

    // 特征2: 配网写入 (WRITE + NOTIFY) — 小程序发配网数据, 设备回复
    _bleChar = svc->createCharacteristic(
        "4fafc203-1fb5-459e-8fcc-c5c9c331914b",
        BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY
    );
    _bleChar->setCallbacks(new BLEProvisionCB());
    _bleChar->addDescriptor(new BLE2902());  // 启用 notify
    _bleChar->setValue("");  // 初始为空

    svc->start();

    _bleAdv = _bleServer->getAdvertising();
    BLEAdvertisementData advData;
    advData.setCompleteServices(BLEUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b"));
    advData.setShortName("HealthMon");
    _bleAdv->setAdvertisementData(advData);
    _bleAdv->setScanResponse(false);  // 不需要 scan response, 全在主包
    _bleAdv->start();

    Serial.printf("[BLE] ✅ 配网广播: HealthMonitor  device_id=%s\n", _config.device_id);
}

void WiFiMgmt::_stopBLE() {
    if (!_bleServer) return;

    // ★ 核心修复: 先断开所有 BLE 客户端连接,
    //    否则手机端缓存连接状态, 下次扫描不到设备
    size_t connCount = _bleServer->getConnectedCount();
    if (connCount > 0) {
        Serial.printf("[BLE] 断开 %d 个已连接客户端\n", connCount);
        for (size_t i = 0; i < connCount; i++) {
            _bleServer->disconnect(i);
        }
        delay(300);  // 等待 BLE 协议栈完成断开流程
    }

    if (_bleAdv) { _bleAdv->stop(); }
    Serial.println("[BLE] 广播已暂停");
}

// BLE 线程安全入队 (仅暂存, 不在 BTC_TASK 回调里处理)
void WiFiMgmt::enqueueBLEJson(const char* json) {
    _blePendingJson  = json;
    _blePendingFlag  = true;
}

// 在 loop() 上下文中处理 BLE 配网数据 (安全 — 不在 BTC_TASK 栈中)
void WiFiMgmt::_processBLEPending() {
    if (!_blePendingFlag) return;
    _blePendingFlag = false;
    Serial.printf("[BLE] loop 处理配网: %s\n", _blePendingJson.c_str());
    handleBLEProvision(_blePendingJson.c_str());
    _blePendingJson = "";
}

void WiFiMgmt::handleBLEProvision(const char* json) {
    JsonDocument doc;
    DeserializationError err = deserializeJson(doc, json);
    if (err) {
        Serial.printf("[BLE] JSON 解析失败: %s\n", err.c_str());
        if (_bleChar) { _bleChar->setValue("ERR_JSON"); _bleChar->notify(); }
        return;
    }

    // 提取 WiFi 凭证
    const char* ssid = doc["s"] | "";
    const char* pass = doc["p"] | "";

    if (strlen(ssid) == 0) {
        Serial.println("[BLE] 未提供 WiFi SSID");
        if (_bleChar) { _bleChar->setValue("ERR_NO_SSID"); _bleChar->notify(); }
        return;
    }

    _pendingSSID = ssid;
    _pendingPass = pass;

    // 保存到 LittleFS (和 Web 配网完全相同的逻辑)
    strncpy(_config.wifi_ssid, ssid, sizeof(_config.wifi_ssid) - 1);
    strncpy(_config.wifi_pass, pass, sizeof(_config.wifi_pass) - 1);

    if (doc.containsKey("device_id"))   strncpy(_config.device_id,   doc["device_id"]   | "", sizeof(_config.device_id) - 1);
    if (doc.containsKey("env_note"))    strncpy(_config.env_note,    doc["env_note"]    | "", sizeof(_config.env_note) - 1);
    if (doc.containsKey("env_type"))    strncpy(_config.env_type,    doc["env_type"]    | "", sizeof(_config.env_type) - 1);
    if (doc.containsKey("mqtt_server")) strncpy(_config.mqtt_server, doc["mqtt_server"] | "", sizeof(_config.mqtt_server) - 1);
    if (doc.containsKey("mqtt_port"))   strncpy(_config.mqtt_port,   doc["mqtt_port"]   | "", sizeof(_config.mqtt_port) - 1);
    if (doc.containsKey("mqtt_user"))   strncpy(_config.mqtt_user,   doc["mqtt_user"]   | "", sizeof(_config.mqtt_user) - 1);
    if (doc.containsKey("mqtt_pass"))   strncpy(_config.mqtt_pass,   doc["mqtt_pass"]   | "", sizeof(_config.mqtt_pass) - 1);

    _saveConfig();
    _portalStopFlag = true;  // 触发延迟关闭 AP+BLE → 连接 WiFi

    Serial.printf("[BLE] ✅ 配网完成, SSID=%s\n", ssid);

    // 回复 OK
    if (_bleChar) { _bleChar->setValue("OK"); _bleChar->notify(); }
    delay(100);
}

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

void WiFiMgmt::resetSettings() {
    Serial.println(("[WiFi] 清除所有配置..."));
    if (LittleFS.exists(CONFIG_FILE_WIFI))   LittleFS.remove(CONFIG_FILE_WIFI);
    if (LittleFS.exists(CONFIG_FILE_MQTT))   LittleFS.remove(CONFIG_FILE_MQTT);
    if (LittleFS.exists(CONFIG_FILE_DEVICE)) LittleFS.remove(CONFIG_FILE_DEVICE);
    WiFi.disconnect(true, true);
    for (int i = 0; i < 10; i++) { digitalWrite(LED_WIFI_PIN, !digitalRead(LED_WIFI_PIN)); delay(100); }
    Serial.println(("[WiFi] 正在重启..."));
    delay(500);
    ESP.restart();
}

// ============================================================
// 按钮
// ============================================================

bool WiFiMgmt::_checkButtonLongPress() {
    bool pressed = (digitalRead(CONFIG_BUTTON_PIN) == LOW);
    if (pressed && !_buttonPressed) {
        _buttonPressed = true; _buttonPressTime = millis();
    } 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) return false;
    strncpy(_config.mqtt_server, doc["mqtt_server"] | MQTT_DEFAULT_SERVER, sizeof(_config.mqtt_server)-1);
    strncpy(_config.mqtt_port, doc["mqtt_port"] | MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port)-1);
    strncpy(_config.mqtt_user, doc["mqtt_user"] | MQTT_DEFAULT_USER, sizeof(_config.mqtt_user)-1);
    strncpy(_config.mqtt_pass, doc["mqtt_pass"] | MQTT_DEFAULT_PASS, sizeof(_config.mqtt_pass)-1);
    strncpy(_config.device_id, doc["device_id"] | MQTT_DEFAULT_DEVICE, sizeof(_config.device_id)-1);
    strncpy(_config.env_note, doc["env_note"] | MQTT_DEFAULT_ENV, sizeof(_config.env_note)-1);
    strncpy(_config.env_type, doc["env_type"] | "bedroom", sizeof(_config.env_type)-1);
    strncpy(_config.wifi_ssid, doc["wifi_ssid"] | "", sizeof(_config.wifi_ssid)-1);
    strncpy(_config.wifi_pass, doc["wifi_pass"] | "", sizeof(_config.wifi_pass)-1);
    Serial.printf("[WiFi] 配置加载, 已存WiFi: %s\n", _config.wifi_ssid[0] ? _config.wifi_ssid : "(空)");
    return true;
}

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;
    doc["env_type"]    = _config.env_type;
    doc["wifi_ssid"]   = _config.wifi_ssid;
    doc["wifi_pass"]   = _config.wifi_pass;
    File file = LittleFS.open(CONFIG_FILE_DEVICE, "w");
    if (!file) return false;
    if (serializeJson(doc, file) == 0) { file.close(); return false; }
    file.close();
    return true;
}

// ============================================================
// Getter
// ============================================================

bool WiFiMgmt::isConnected() const { return _connected && (WiFi.status() == WL_CONNECTED); }
bool WiFiMgmt::justProvisioned() { bool v = _provisionSuccess; _provisionSuccess = false; return v; }
DeviceConfig WiFiMgmt::getConfig() const { return _config; }
const char* WiFiMgmt::getDeviceId() const { return _config.device_id; }
const char* WiFiMgmt::getEnvNote() const { return _config.env_note; }

// ============================================================
// LED
// ============================================================

void WiFiMgmt::_setLEDMode(uint8_t mode) { _ledMode = mode; }

void WiFiMgmt::_ledTaskFunc(void* param) {
    const TickType_t fastMs = pdMS_TO_TICKS(300);
    const TickType_t slowMs = pdMS_TO_TICKS(1000);
    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: 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;
        }
    }
}


1	/**
2	 * @file wifi_manager.h
3	 * @brief WiFi 配网管理器 (AP + WebServer)
4	 *
5	 * 功能:
6	 *  - 自动连接已保存的 WiFi
7	 *  - 配网失败/首次启动 → AP 模式 + Captive Portal
8	 *  - 自定义参数: MQTT 服务器/端口/用户/密码、设备名称、环境备注
9	 *  - 长按按钮重置配置
10	 *  - LED 状态指示
11	 *  - LittleFS 持久化存储
12	 */
13
14	#ifndef WIFI_MANAGER_H
15	#define WIFI_MANAGER_H
16
17	#include <Arduino.h>
18	#include <WiFi.h>
19	#include <WebServer.h>
20	#include <DNSServer.h>
21	#include <LittleFS.h>
22	#include <BLEDevice.h>
23	#include <BLEServer.h>
24	#include <BLEUtils.h>
25	#include <BLE2902.h>
26	#include <ArduinoJson.h>
27	#include "config.h"
28
29	// 设备配置结构体
30	struct DeviceConfig {
31	    char mqtt_server[40];
32	    char mqtt_port[6];
33	    char mqtt_user[20];
34	    char mqtt_pass[20];
35	    char device_id[20];
36	    char env_note[20];      // 环境备注 (房间名)
37	    char env_type[16];      // 环境类型 bedroom/bathroom/...
38	    char wifi_ssid[33];     // WiFi SSID
39	    char wifi_pass[65];     // WiFi 密码
40	};
41
42	class WiFiMgmt {
43	public:
44	    WiFiMgmt();
45	    ~WiFiMgmt();
46
47	    // 单例指针 (用于静态回调)
48	    static WiFiMgmt* _instance;
49
50	    bool begin();
51	    bool loop();
52	    bool isConnected() const;
53	    bool justProvisioned();     // 刚配网成功 (读取后自动清除)
54	    DeviceConfig getConfig() const;
55	    const char* getDeviceId() const;
56	    const char* getEnvNote() const;
57	    void startConfigPortal();
58	    void handleBLEProvision(const char* json);  // BLE 配网回调
59	    void enqueueBLEJson(const char* json);       // BLE 线程安全入队 (仅暂存)
60	    void resetSettings();
61
62	private:
63	    bool _connectWiFi();          // 仅尝试已保存凭据
64	    bool _tryConnect(const char* ssid, const char* pass);  // 尝试连接指定 WiFi
65	    bool _loadConfig();
66	    bool _saveConfig();
67	    void _setLEDMode(uint8_t mode);
68	    static void _ledTaskFunc(void* param);
69	    bool _checkButtonLongPress();
70
71	    // Web 服务器
72	    void _startPortal();
73	    void _stopPortal();
74	    void _handleRoot();
75	    void _handleSave();
76	    void _handleReset();
77	    void _updateWifiScan();    // WiFi 扫描缓存
78	    String _htmlPage();        // 返回配网 HTML 页面
79
80	    // BLE 蓝牙配网
81	    void _startBLE();
82	    void _stopBLE();
83
84	    WebServer*     _server;
85	    DNSServer*     _dns;
86	    DeviceConfig   _config;
87	    bool           _connected;
88	    bool           _portalActive;
89	    bool           _portalStopFlag;     // 延迟关闭标记
90	    uint32_t       _portalStartTime;
91	    String         _pendingSSID;
92	    String         _pendingPass;
93	    bool           _provisionSuccess;  // 刚配网成功标记
94	    uint32_t       _lastCheckTime;
95	    uint32_t       _buttonPressTime;
96	    bool           _buttonPressed;
97
98	    // WiFi 扫描缓存 (避免每次打开页面都扫描3-8秒)
99	    String         _wifiScanCache;
100	    uint32_t       _lastScanTime;
101
102	    // LED 独立任务
103	    TaskHandle_t    _ledTaskHandle;
104	    volatile uint8_t _ledMode;  // 0=灭 1=慢闪 2=快闪 3=常亮
105
106	    // BLE
107	    BLEServer*           _bleServer;
108	    BLECharacteristic*   _bleInfoChar;   // READ: 设备信息
109	    BLECharacteristic*   _bleChar;       // WRITE+NOTIFY: 配网
110	    BLEAdvertising*      _bleAdv;
111
112	    // BLE 回调保护: 不在 BTC_TASK 栈里做 Flash 写操作
113	    String        _blePendingJson;      // 待处理的 BLE 配网 JSON
114	    bool          _blePendingFlag;      // 有未处理的 BLE 数据
115	    void _processBLEPending();          // 在 loop() 上下文处理
116	};
117
118	#endif // WIFI_MANAGER_H
119

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

#ifndef WIFI_MANAGER_H
#define WIFI_MANAGER_H

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <DNSServer.h>
#include <LittleFS.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <ArduinoJson.h>
#include "config.h"

// 设备配置结构体
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];      // 环境备注 (房间名)
    char env_type[16];      // 环境类型 bedroom/bathroom/...
    char wifi_ssid[33];     // WiFi SSID
    char wifi_pass[65];     // WiFi 密码
};

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

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

    bool begin();
    bool loop();
    bool isConnected() const;
    bool justProvisioned();     // 刚配网成功 (读取后自动清除)
    DeviceConfig getConfig() const;
    const char* getDeviceId() const;
    const char* getEnvNote() const;
    void startConfigPortal();
    void handleBLEProvision(const char* json);  // BLE 配网回调
    void enqueueBLEJson(const char* json);       // BLE 线程安全入队 (仅暂存)
    void resetSettings();

private:
    bool _connectWiFi();          // 仅尝试已保存凭据
    bool _tryConnect(const char* ssid, const char* pass);  // 尝试连接指定 WiFi
    bool _loadConfig();
    bool _saveConfig();
    void _setLEDMode(uint8_t mode);
    static void _ledTaskFunc(void* param);
    bool _checkButtonLongPress();

    // Web 服务器
    void _startPortal();
    void _stopPortal();
    void _handleRoot();
    void _handleSave();
    void _handleReset();
    void _updateWifiScan();    // WiFi 扫描缓存
    String _htmlPage();        // 返回配网 HTML 页面

    // BLE 蓝牙配网
    void _startBLE();
    void _stopBLE();

    WebServer*     _server;
    DNSServer*     _dns;
    DeviceConfig   _config;
    bool           _connected;
    bool           _portalActive;
    bool           _portalStopFlag;     // 延迟关闭标记
    uint32_t       _portalStartTime;
    String         _pendingSSID;
    String         _pendingPass;
    bool           _provisionSuccess;  // 刚配网成功标记
    uint32_t       _lastCheckTime;
    uint32_t       _buttonPressTime;
    bool           _buttonPressed;

    // WiFi 扫描缓存 (避免每次打开页面都扫描3-8秒)
    String         _wifiScanCache;
    uint32_t       _lastScanTime;

    // LED 独立任务
    TaskHandle_t    _ledTaskHandle;
    volatile uint8_t _ledMode;  // 0=灭 1=慢闪 2=快闪 3=常亮

    // BLE
    BLEServer*           _bleServer;
    BLECharacteristic*   _bleInfoChar;   // READ: 设备信息
    BLECharacteristic*   _bleChar;       // WRITE+NOTIFY: 配网
    BLEAdvertising*      _bleAdv;

    // BLE 回调保护: 不在 BTC_TASK 栈里做 Flash 写操作
    String        _blePendingJson;      // 待处理的 BLE 配网 JSON
    bool          _blePendingFlag;      // 有未处理的 BLE 数据
    void _processBLEPending();          // 在 loop() 上下文处理
};

#endif // WIFI_MANAGER_H


现在是，蓝牙配网成功后。蓝牙不直接发送报文，重启后才行


Let me analyze the bug. The user says: after Bluetooth (BLE) provisioning succeeds, the device doesn't send MQTT messages directly. It only works after a restart.

Let me trace the BLE provisioning flow:

1. BLE data arrives → `BLEProvisionCB::onWrite()` → `enqueueBLEJson()` sets `_blePendingFlag = true`
2. In `loop()` → `_processBLEPending()` → `handleBLEProvision()`
3. `handleBLEProvision()` saves config, sets `_portalStopFlag = true`
4. Back in `loop()`, when `_portalStopFlag` is true:
   - `_stopPortal()` is called
   - `_tryConnect()` is called with the new WiFi credentials
   - If successful, `_provisionSuccess = true`
   - Returns `_connected = true`

5. Then in `main.cpp` loop():
```cpp
// 配网成功后: 先更新 MQTT 配置, 再强制重连
if (g_wifi.justProvisioned()) {
    DeviceConfig cfg = g_wifi.getConfig();
    g_mqtt.disconnect();
    g_mqtt.setServer(cfg.mqtt_server, atoi(cfg.mqtt_port),
                     cfg.mqtt_user, cfg.mqtt_pass);
    g_mqtt.connect();
    g_mqtt.publishProvisionStatus("success", "");
}
```

Wait, but this code path only happens when `_portalActive` is false AND `_provisionSuccess` is true. Let me trace more carefully.

Actually wait - when BLE provisioning happens, `_portalStopFlag = true` is set. Then in the next `loop()` call:
```cpp
if (_portalActive) {
    if (_portalStopFlag) {
        _stopPortal();
        _portalStopFlag = false;
        if (_pendingSSID.length() > 0) {
            _connected = _tryConnect(_pendingSSID.c_str(), _pendingPass.c_str());
            _pendingSSID = ""; _pendingPass = "";
            if (_connected) {
                _setLEDMode(3);
                _provisionSuccess = true;  // 标记配网成功
            }
        }
    }
    return _connected;
}
```

Then in the next `loop()` call, since `_portalActive` is now false (portal was stopped), it goes to the normal flow.

But wait - in the normal flow:
```cpp
if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {
    _lastCheckTime = millis();
    ...
}
```

And then back in `main.cpp` loop:
```cpp
if (g_wifi.justProvisioned()) {
    // This should be called
}
```

Hmm, actually the issue might be different. Let me look more carefully...

Actually wait. In `main.cpp` loop(), the `justProvisioned()` check is inside:
```cpp
if (wifiOk) {
    g_mqtt.loop();
    ...
    if (g_wifi.justProvisioned()) {
        ...
    }
}
```

And `wifiOk` is `g_wifi.loop()`. But the problem might be that `_processBLEPending()` is called at the start of `WiFiMgmt::loop()`, BEFORE the portal handling. Let's trace through:

1. First call to `loop()`: `_processBLEPending()` → `handleBLEProvision()` → sets `_portalStopFlag = true`. Then since `_portalActive` is true, it processes `_portalStopFlag`, stops portal, connects WiFi, sets `_provisionSuccess = true`, returns `_connected`.

2. BUT `_processBLEPending()` might not have been called yet if `_blePendingFlag` was just set in the BLE callback! The callback runs on the BTC_TASK, and `loop()` runs on the main task (core 1). There might be a timing issue.

Actually wait, let me re-read the `_processBLEPending()` flow:

```cpp
bool WiFiMgmt::loop() {
    // --- BLE 待处理数据 (从 loop 上下文安全处理, 不占 BTC_TASK 栈) ---
    _processBLEPending();

    // --- Portal 模式 ---
    if (_portalActive) {
        ...
    }
    ...
}
```

So `_processBLEPending()` is called first, which calls `handleBLEProvision()`, which sets `_portalStopFlag = true`. Then the portal handling code runs in the same `loop()` call, stops portal, connects WiFi, sets `_provisionSuccess = true`.

Then in `main.cpp`:
```cpp
bool wifiOk = g_wifi.loop();  // returns connected status
```

Wait - but in the portal handling block, if `_portalStopFlag` is true, it does:
```cpp
if (_portalStopFlag) {
    _stopPortal();
    _portalStopFlag = false;
    if (_pendingSSID.length() > 0) {
        _connected = _tryConnect(...);
        ...
        if (_connected) {
            _provisionSuccess = true;
        }
    }
    return _connected;
}
```

And `_tryConnect` calls `WiFi.begin(ssid, pass)` and waits up to 20 seconds!

So `g_wifi.loop()` blocks for up to 20 seconds during WiFi connection. That's fine for the first call.

Then in `main.cpp`, the code checks:
```cpp
bool wifiOk = g_wifi.loop();  // Should return true if connected

if (wifiOk) {
    g_mqtt.loop();
    ...
    if (g_wifi.justProvisioned()) {
        DeviceConfig cfg = g_wifi.getConfig();
        g_mqtt.disconnect();
        g_mqtt.setServer(cfg.mqtt_server, atoi(cfg.mqtt_port),
                         cfg.mqtt_user, cfg.mqtt_pass);
        g_mqtt.connect();
        g_mqtt.publishProvisionStatus("success", "");
    }
}
```

This should work... but wait. Let me think about this more carefully.

Actually, I think the problem might be more subtle. When BLE provisioning happens during normal operation (WiFi is already connected), `_portalActive` is false. So `handleBLEProvision()` sets `_portalStopFlag = true`, but since we're NOT in portal mode, the code falls through to the normal flow. `_portalStopFlag` doesn't get checked in the normal flow!

Let me re-read `loop()`:

```cpp
bool WiFiMgmt::loop() {
    _processBLEPending();

    if (_portalActive) {
        // handles _portalStopFlag here
        ...
        return _connected;
    }

    // --- 正常运行 ---
    // _portalStopFlag is NOT checked here!
    if (_checkButtonLongPress()) { ... }
    ...
    return _connected;
}
```

AH HA! That's the bug! When BLE provisioning happens during normal operation (WiFi already connected, `_portalActive == false`), `handleBLEProvision()` sets `_portalStopFlag = true` and saves the new SSID/password. But the normal flow in `loop()` NEVER checks `_portalStopFlag` — it only checks it inside the `if (_portalActive)` block!

So:
1. Device is already on WiFi (say, a previous network)
2. User provisions via BLE with new WiFi credentials
3. `handleBLEProvision()` saves new config, sets `_portalStopFlag = true`, `_pendingSSID` and `_pendingPass` are set
4. `loop()` runs: `_processBLEPending()` does nothing (already processed), `_portalActive` is false
5. The `_portalStopFlag` check is NEVER reached
6. The device stays on the old WiFi, never connects to the new one
7. `_provisionSuccess` is never set to true
8. The MQTT reconnect never happens

Wait, but actually let me reconsider. The user said "蓝牙配网成功后" - meaning BLE provisioning succeeds. But then "蓝牙不直接发送报文，重启后才行" - after BLE provisioning, it doesn't send messages directly, only after reboot.

Actually, there could be another scenario: what if the device was NOT on WiFi before BLE provisioning (first boot, no config)? Then:
1. `begin()`: no config → `_connected = false`, `_startPortal()` is called → `_portalActive = true`, BLE starts
2. BLE data arrives → `_portalStopFlag = true`
3. `loop()`: `_processBLEPending()` processes BLE data, then portal handling stops portal, connects WiFi, sets `_provisionSuccess = true`
4. Back in main loop: `justProvisioned()` returns true, reconfigures MQTT, connects...

This should work in the first-boot scenario. But there might be a corner case.

Wait, actually, let me look at `_processBLEPending()` and `handleBLEProvision()` more carefully:

```cpp
void WiFiMgmt::handleBLEProvision(const char* json) {
    ...
    _portalStopFlag = true;  // 触发延迟关闭 AP+BLE → 连接 WiFi
    ...
}
```

And `_portalStopFlag` is only checked in the portal mode section:
```cpp
if (_portalActive) {
    if (_portalStopFlag) {
        _stopPortal();
        _portalStopFlag = false;
        if (_pendingSSID.length() > 0) {
            _connected = _tryConnect(...);
            ...
        }
    }
    return _connected;
}
```

So the flow works when portal is active. But if device already has WiFi:
1. Portal is NOT active
2. BLE provisioning comes in
3. `_portalStopFlag` is set but never checked
4. WiFi is never reconnected to new credentials

That's definitely a bug for the "re-provisioning" case.

But the user's description sounds like it's about the first-time provisioning case. Let me think again...

Actually, maybe the issue is different. Let me look at `main.cpp` loop again:

```cpp
void loop() {
    ...
    bool wifiOk = g_wifi.loop();

    if (wifiOk) {
        g_mqtt.loop();
        ...
        if (g_wifi.justProvisioned()) {
            DeviceConfig cfg = g_wifi.getConfig();
            g_mqtt.disconnect();
            g_mqtt.setServer(cfg.mqtt_server, atoi(cfg.mqtt_port),
                             cfg.mqtt_user, cfg.mqtt_pass);
            g_mqtt.connect();
            g_mqtt.publishProvisionStatus("success", "");
        }
    }
    ...
}
```

Wait, the `g_wifi.loop()` call during the portal handling blocks for up to 20 seconds (in `_tryConnect`). So on that call, `wifiOk` is the result. If connection succeeds, `wifiOk = true`, and then `justProvisioned()` returns true and the MQTT reconnect happens.

But wait, `_tryConnect` calls `WiFi.begin()` and waits. But the device might have previously been connected to a different WiFi. The `_tryConnect` uses `WiFi.disconnect(true)` first, then `WiFi.mode(WIFI_STA)`, then `WiFi.begin(ssid, pass)`. This should work.

Hmm, let me reconsider. Actually, there might be a race condition:

When `_portalActive` is true and BLE data comes in, `handleBLEProvision()` sets `_portalStopFlag = true`. On the next `loop()` call, the portal handling code:
1. Calls `_stopPortal()` which disconnects all BLE clients
2. Calls `_tryConnect()` which blocks for up to 20 seconds

But the BLE `onWrite` callback runs on the BTC_TASK. If `_stopPortal()` disconnects the BLE client while `onWrite` was being processed... that shouldn't be an issue since `_processBLEPending()` already finished.

Actually, I just realized there might be another issue. Let me look at when the device already has a working WiFi connection (not in portal mode):

1. BLE data comes in → `_blePendingFlag = true`
2. `loop()` → `_processBLEPending()` → `handleBLEProvision()` → saves new config to LittleFS, sets `_portalStopFlag = true`, sets `_pendingSSID` and `_pendingPass`
3. BUT `_portalActive` is false! So `_portalStopFlag` is never checked.
4. The device stays on the old WiFi.
5. `_provisionSuccess` is never set.

So the MQTT messages continue on the old connection. When the user restarts, `begin()` loads the new config and connects to the new WiFi. That explains the symptom!

But actually, the user said "配网成功后蓝牙不直接发送报文" - this sounds like the BLE itself doesn't send a response message back. Let me re-read...

Actually "蓝牙不直接发送报文" could mean "the device doesn't send MQTT messages directly after BLE provisioning, only after reboot". So the MQTT messages don't flow until reboot.

This matches the bug I found: when `_portalActive` is false (device already on WiFi), BLE provisioning doesn't trigger WiFi reconnection or MQTT reconfiguration, and `_provisionSuccess` is never set. So the main loop's `justProvisioned()` check never fires.

But wait, there's another issue even when `_portalActive` is true. Let me trace more carefully...

Actually, let me also consider: what if the device doesn't have WiFi config (first boot)?
- `begin()`: no config, `_connected = false`, starts portal → `_portalActive = true`, BLE starts
- BLE data comes in → processed, `_portalStopFlag = true`
- `loop()`: portal handling stops portal, connects WiFi, `_provisionSuccess = true`
- Next `loop()`: `_portalActive = false`, goes to normal flow
- But wait, will the WiFi check timer fire? `_lastCheckTime` was set in `begin()`. The normal flow only updates `_connected` when `WIFI_RETRY_INTERVAL` has passed.

Hmm, actually in this case `_connected` was already set to true by `_tryConnect()`. So `wifiOk` returns true, `justProvisioned()` returns true, MQTT reconnects. This case should work.

Let me think about what happens in the normal (non-portal) BLE provisioning flow more carefully.

When `_portalActive` is false and BLE provisioning comes in:
1. `handleBLEProvision()` saves new config
2. Sets `_portalStopFlag = true` — but this flag is only checked inside `if (_portalActive)` block!
3. The device stays on the current WiFi (if any)
4. `_provisionSuccess` is never set

This is clearly a bug. The fix should handle `_portalStopFlag` (or better, a separate flag for BLE-triggered WiFi reconnect) in the normal flow too.

Actually, looking at this more carefully, I think the fix should be: in `handleBLEProvision()`, after saving the config, if the portal is NOT active, we should directly trigger a WiFi reconnection instead of just setting `_portalStopFlag`. Or better yet, we should handle the `_portalStopFlag` (or a new dedicated flag) in the normal flow of `loop()`.

Let me also check: is there another scenario? What if there's no existing WiFi connection AND no portal active? That shouldn't happen because `begin()` starts the portal when there's no connection.

OK so let me propose the fix. The core issue is:

**In `handleBLEProvision()`, when `_portalActive` is false, setting `_portalStopFlag = true` has no effect because the normal flow of `loop()` never checks it.**

The fix should make BLE provisioning work in both cases (portal active and portal not active). The simplest approach: in `handleBLEProvision()`, if portal is not active, directly attempt to connect to the new WiFi and mark `_provisionSuccess = true`.

But wait, `_tryConnect` blocks for 20 seconds, and `handleBLEProvision` is called from `_processBLEPending` which is called from `loop()`. Blocking `loop()` for 20 seconds is not ideal but it's the same behavior as the portal case.

Actually, a better approach: add a `_bleProvisioned` flag and handle it in the normal flow of `loop()`, similar to how `_portalStopFlag` is handled in the portal flow.

Let me write the fix:

In `handleBLEProvision()`:
```cpp
void WiFiMgmt::handleBLEProvision(const char* json) {
    // ... existing code to save config ...
    _saveConfig();

    if (_portalActive) {
        _portalStopFlag = true;  // portal flow handles it
    } else {
        // Direct reconnect when not in portal mode
        _pendingSSID = ssid;
        _pendingPass = pass;
        _bleProvisionedFlag = true;  // new flag for normal flow
    }
    // ...
}
```

In `loop()`, after the normal flow section, or in the normal flow section:
```cpp
// --- 正常运行 ---
if (_bleProvisionedFlag) {
    _bleProvisionedFlag = false;
    if (_pendingSSID.length() > 0) {
        Serial.printf("[WiFi] BLE配网: 连接 %s\n", _pendingSSID.c_str());
        WiFi.disconnect(true);
        delay(200);
        WiFi.mode(WIFI_STA);
        delay(100);
        _connected = _tryConnect(_pendingSSID.c_str(), _pendingPass.c_str());
        _pendingSSID = ""; _pendingPass = "";
        if (_connected) {
            _setLEDMode(3);
            _provisionSuccess = true;
        } else {
            _setLEDMode(2);
            _startPortal();
        }
    }
    return _connected;
}
```

Hmm, but this duplicates a lot of code from the portal handling. Let me think of a cleaner approach.

Actually, the cleanest fix might be to extract the "connect and mark provisioned" logic and call it from both places. But since this is embedded C++, let me keep it simple.

Actually, the simplest fix: in the normal flow of `loop()`, also check `_portalStopFlag` (or rename it to something more generic). But the issue is that `_portalStopFlag` is set by `handleBLEProvision()` and checked in the portal flow. If portal is not active, we need to handle it in the normal flow.

Let me look at the code structure again:

```cpp
bool WiFiMgmt::loop() {
    _processBLEPending();

    if (_portalActive) {
        // handles _portalStopFlag
        if (_portalStopFlag) {
            _stopPortal();
            _portalStopFlag = false;
            if (_pendingSSID.length() > 0) {
                _connected = _tryConnect(...);
                if (_connected) {
                    _setLEDMode(3);
                    _provisionSuccess = true;
                }
            }
            return _connected;
        }
        _dns->processNextRequest();
        _server->handleClient();
        delay(10);
        return _connected;
    }

    // --- 正常运行 ---
    // Check button long press
    // Check WiFi status
    ...
    return _connected;
}
```

The simplest fix: move the `_portalStopFlag` check to BEFORE the `if (_portalActive)` block, or check it in the normal flow too. Actually, the cleanest approach is to check it at the beginning, before the portal check:

```cpp
bool WiFiMgmt::loop() {
    _processBLEPending();

    // Handle pending WiFi connection request (from BLE or Web portal)
    if (_portalStopFlag) {
        if (_portalActive) {
            _stopPortal();
        }
        _portalStopFlag = false;
        if (_pendingSSID.length() > 0) {
            Serial.printf("[WiFi] 配网连接: %s\n", _pendingSSID.c_str());
            _connected = _tryConnect(_pendingSSID.c_str(), _pendingPass.c_str());
            _pendingSSID = ""; _pendingPass = "";
            if (_connected) {
                _setLEDMode(3);
                _provisionSuccess = true;
            } else if (!_portalActive) {
                _setLEDMode(2);
                _startPortal();
            }
        }
        return _connected;
    }

    if (_portalActive) {
        _dns->processNextRequest();
        _server->handleClient();
        delay(10);
        return _connected;
    }

    // --- 正常运行 ---
    ...
}
```

Wait, but there's an issue. In `handleBLEProvision()`, the `_pendingSSID` is set to `ssid` and `_pendingPass` is set to `pass`. Then `_portalStopFlag = true`. But the SSID and pass are C strings (`const char*`), and the String assignment copies them. That should be fine.

Actually wait, I just realized something. In `_processBLEPending()`:
```cpp
void WiFiMgmt::_processBLEPending() {
    if (!_blePendingFlag) return;
    _blePendingFlag = false;
    handleBLEProvision(_blePendingJson.c_str());
    _blePendingJson = "";
}
```

And `handleBLEProvision` does:
```cpp
_pendingSSID = ssid;   // ssid is from doc["s"] | ""
_pendingPass = pass;   // pass is from doc["p"] | ""
```

These are String copies, so they persist after the JSON document is destroyed. Good.

OK, so let me also make sure about another thing. When BLE provisioning happens and `_portalActive` is false, the flow would be:

1. `handleBLEProvision()` saves config, sets `_pendingSSID`, `_pendingPass`, `_portalStopFlag = true`, notifies BLE client "OK"
2. Next `loop()` call: `_processBLEPending()` does nothing (flag already cleared)
3. `_portalStopFlag` is true → stops portal (no-op since not active), calls `_tryConnect()`, sets `_provisionSuccess = true`
4. Returns `_connected`
5. In `main.cpp`: `wifiOk = true`, `justProvisioned()` returns true → MQTT reconnects

This should work! But there's a subtle issue: in `handleBLEProvision()`, when we set `_pendingSSID` and `_pendingPass`, we also do:
```cpp
_pendingSSID = ssid;
_pendingPass = pass;
```

But these are set BEFORE `_portalStopFlag = true`. And in the original code, `_portalStopFlag` handling also reads `_pendingSSID` and `_pendingPass`. So the data should be there.

Wait, but there's another issue. In `_processBLEPending()`:
```cpp
void WiFiMgmt::_processBLEPending() {
    if (!_blePendingFlag) return;
    _blePendingFlag = false;
    Serial.printf("[BLE] loop 处理配网: %s\n", _blePendingJson.c_str());
    handleBLEProvision(_blePendingJson.c_str());
    _blePendingJson = "";
}
```

`handleBLEProvision` is called and it may call `_bleChar->notify()`. After that, `_blePendingJson = ""`. That's fine.

But there's another thing I want to verify: in `handleBLEProvision`, when `_portalActive` is true, after setting `_portalStopFlag = true`, the next `loop()` call will process it. But in the SAME `loop()` call, after `_processBLEPending()` returns, the portal handling code runs. Wait, let me re-read:

```cpp
bool WiFiMgmt::loop() {
    _processBLEPending();  // This calls handleBLEProvision which sets _portalStopFlag = true

    if (_portalActive) {
        if (_portalStopFlag) {  // TRUE! Handled in same loop() call
            ...
        }
        ...
    }
}
```

So in the portal case, it's handled in the SAME `loop()` call! That's good. The `_tryConnect` blocks for up to 20 seconds, but that's the same behavior as before.

OK so my proposed fix is to move the `_portalStopFlag` check OUTSIDE the `if (_portalActive)` block, so it handles both cases:

1. Portal active → stop portal, connect WiFi (existing behavior)
2. Portal not active → directly connect to new WiFi (new behavior)

Let me write the actual fix now.

Actually, I also want to double check: is there a scenario where `_portalStopFlag` is true but `_pendingSSID` is empty? Looking at the code:

In `_handleSave()` (Web portal):
```cpp
if (_server->hasArg("s")) {
    _pendingSSID = _server->arg("s");
    _pendingPass = _server->arg("p");
    ...
    _portalStopFlag = true;
}
```

In `handleBLEProvision()`:
```cpp
if (strlen(ssid) == 0) {
    // returns early with error
    return;
}
_pendingSSID = ssid;
_pendingPass = pass;
...
_portalStopFlag = true;
```

In both cases, `_pendingSSID` should be non-empty when `_portalStopFlag` is set. Good.

Now, there's also the flow in `main.cpp` to consider. After WiFi connects in `loop()`, the main loop does:

```cpp
bool wifiOk = g_wifi.loop();

if (wifiOk) {
    g_mqtt.loop();
    ...
    if (g_wifi.justProvisioned()) {
        DeviceConfig cfg = g_wifi.getConfig();
        g_mqtt.disconnect();
        g_mqtt.setServer(cfg.mqtt_server, atoi(cfg.mqtt_port),
                         cfg.mqtt_user, cfg.mqtt_pass);
        g_mqtt.connect();
        g_mqtt.publishProvisionStatus("success", "");
    }
}
```

Wait, but `publishProvisionStatus` publishes an MQTT message. And `g_mqtt.connect()` was just called. Is `connect()` synchronous? If it's async, the `publishProvisionStatus` might fail because the connection isn't established yet.

But that's a separate issue from what the user is reporting. The user says messages don't flow until reboot. Let me focus on the main issue.

Actually, wait. I just realized there's another possible issue. Let me re-read `main.cpp` loop():

```cpp
// 配网成功后: 先更新 MQTT 配置, 再强制重连
if (g_wifi.justProvisioned()) {
    DeviceConfig cfg = g_wifi.getConfig();
    g_mqtt.disconnect();
    g_mqtt.setServer(cfg.mqtt_server, atoi(cfg.mqtt_port),
                     cfg.mqtt_user, cfg.mqtt_pass);
    g_mqtt.connect();
    g_mqtt.publishProvisionStatus("success", "");
}
```

This is inside `if (wifiOk)` block. But `wifiOk = g_wifi.loop()`. When `_portalStopFlag` triggers the reconnect flow and returns `_connected`, `wifiOk` gets the right value. Then `justProvisioned()` returns true. Then the MQTT reconnect happens.

This should work for the portal case. And with my fix, it should also work for the non-portal case.

But wait, there's yet another subtle bug. In `main.cpp` loop:

```cpp
bool wifiOk = g_wifi.loop();

if (wifiOk) {
    g_mqtt.loop();

    static bool wasDisconnected = true;
    if (g_mqtt.isConnected()) {
        if (wasDisconnected) {
            syncTime();
            wasDisconnected = false;
        }
    } else {
        wasDisconnected = true;
    }

    if (g_wifi.justProvisioned()) {
        ...
        g_mqtt.connect();
        g_mqtt.publishProvisionStatus("success", "");
    }
}
```

After `g_mqtt.connect()` in the `justProvisioned()` block, the MQTT connection might not be established yet. Then in the NEXT loop iteration, `g_mqtt.isConnected()` might be true (if the connection completed), and `wasDisconnected` would still be true (set in the previous iteration when MQTT was not connected), so `syncTime()` would be called and `wasDisconnected = false`. That's a minor inefficiency but not a bug.

The real question is: does `g_mqtt.connect()` block until connected, or is it async? If async, `publishProvisionStatus` would fail. And then in subsequent loop iterations, the main code would try to publish telemetry, which might work if the connection completed.

But actually, re-reading the user's complaint: "蓝牙配网成功后，蓝牙不直接发送报文，重启后才行" - the key word is "不直接" (not directly/immediately). This suggests it works eventually or after reboot. Let me think about what could cause this...

Actually, I think I've been overthinking this. Let me check if `_portalStopFlag` handling is the issue. Let me also check the `main.cpp` `handleProvisionCommand`:

```cpp
void handleProvisionCommand(const ProvisionCommand& cmd) {
    // MQTT-based provisioning
    if (strlen(cmd.wifi_ssid) == 0) { ... return; }

    g_mqtt.publishProvisionStatus("wifi_connecting", "");
    WiFi.begin(cmd.wifi_ssid, cmd.wifi_password);
    // waits for connection...
    if (WiFi.status() == WL_CONNECTED) {
        // update MQTT config
        if (strlen(cmd.mqtt_broker) > 0) {
            g_mqtt.setServer(cmd.mqtt_broker, cmd.mqtt_port, ...);
        }
        g_mqtt.publishProvisionResponse(cmd.cmdId, true, ...);
        g_mqtt.disconnect();
        g_mqtt.connect();
        g_mqtt.publishOnline();
    }
}
```

This is for MQTT-based provisioning and seems to work correctly.

OK so let me focus on the BLE case. I believe the bug is that when BLE provisioning happens and `_portalActive` is false, `_portalStopFlag` is never processed in the normal flow.

But wait, there might also be a second case: even when `_portalActive` is true (first boot), after the portal stops and WiFi connects, the MQTT reconnect might have issues. Let me check the `g_mqtt.connect()` behavior.

Let me read the MQTT manager.

Actually, let me just focus on the fix. The main issue I identified is real: `_portalStopFlag` is not checked in the normal flow of `loop()`. Let me also handle the case where WiFi needs to be reconnected for new credentials.

Let me now write the fix. I'll restructure the `loop()` function to handle `_portalStopFlag` regardless of whether portal is active.

Actually, I realize I should also look at this from another angle. When BLE provisioning succeeds on a device that already has WiFi:

The flow is:
1. BLE data → `handleBLEProvision()` saves config, sets `_portalStopFlag = true`
2. `loop()`: `_processBLEPending()` does nothing, portal handling skipped (portal not active)
3. Normal flow: WiFi status check, button check — `_portalStopFlag` never checked
4. `justProvisioned()` never returns true
5. MQTT never reconnects with new config
6. Device continues to use old WiFi and old MQTT settings
7. After reboot, `begin()` loads new config and everything works

This matches the user's description! The fix is clear.

But I also want to handle this cleanly. Let me propose:

Option A: Move `_portalStopFlag` check before the portal block, handle it in both cases
Option B: Add a separate `_bleReconnectFlag` specifically for BLE provisioning
Option C: In `handleBLEProvision()`, handle reconnection directly

I think Option A is cleanest — fewer flags, unified logic.

Let me write the implementation.

Wait, one more thing to check. In `handleBLEProvision()`:
```cpp
_pendingSSID = ssid;
_pendingPass = pass;
```

These are Arduino String assignments. They should work fine.

OK let me also think about what happens in `main.cpp` after `g_wifi.loop()` returns. The `loop()` returns `_connected` which is true after successful reconnection. Then:

```cpp
bool wifiOk = g_wifi.loop();
if (wifiOk) {
    g_mqtt.loop();
    if (g_wifi.justProvisioned()) {
        // MQTT reconnect
    }
}
```

And then later in the same loop iteration:
```cpp
if (radar.presence) {
    if (now - g_lastReportTime >= REPORT_INTERVAL_MS) {
        g_mqtt.publishAllPoints(tele, g_sequence);
    }
}
```

If MQTT just reconnected, `publishAllPoints` might or might not work depending on whether `connect()` is synchronous. This is a secondary concern.

Let me now write the fix.

Actually, I realize I need to also handle one more edge case. When the device is in portal mode (AP active), the BLE data arrives and `_portalStopFlag` is set. This is handled in the SAME loop() call because `_processBLEPending()` runs first, sets the flag, then the portal handling sees it. But with my proposed change, if I move the `_portalStopFlag` check before `if (_portalActive)`, the portal would still need to be stopped. Let me structure the code carefully.

Here's my plan for the fix:

```cpp
bool WiFiMgmt::loop() {
    _processBLEPending();

    // Handle pending WiFi connection request (from BLE or Web portal)
    if (_portalStopFlag) {
        if (_portalActive) {
            _stopPortal();
        }
        _portalStopFlag = false;
        if (_pendingSSID.length() > 0) {
            Serial.printf("[WiFi] 配网连接: %s\n", _pendingSSID.c_str());
            _connected = _tryConnect(_pendingSSID.c_str(), _pendingPass.c_str());
            _pendingSSID = ""; _pendingPass = "";
            if (_connected) {
                _setLEDMode(3);
                _provisionSuccess = true;
            } else {
                _setLEDMode(2);
                _startPortal();  // Failed - restart portal for retry
            }
        }
        return _connected;
    }

    if (_portalActive) {
        _dns->processNextRequest();
        _server->handleClient();
        delay(10);
        return _connected;
    }

    // --- 正常运行 ---
    ...
}
```

This way, regardless of whether the portal is active or not, `_portalStopFlag` is always processed. When portal is active, it stops the portal first, then connects. When portal is not active (normal BLE provisioning), it just connects.

This looks correct to me. Let me implement it.


让我仔细分析 BLE 配网的代码流程，找出问题所在。