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

⏳
"
264	            "
配置已保存

设备正在连接 WiFi...
等待指示灯常亮
"
265	            "
");
270
271	        _portalStopFlag = true;
272	        return;
273	    }
274	    _handleRoot();
275	}
276
277	void WiFiMgmt::_handleReset() {
278	    _server->send(200, "text/html; charset=utf-8",
279	        "<!DOCTYPE html><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>已重置</title>"
280	        "

配置已清除

设备正在重启...

");
283	    delay(500);
284	    _stopPortal();
285	    resetSettings();
286	}
287
288	// ============================================================
289	// 配网 HTML 页面 (精简版)
290	// ============================================================
291
292	void WiFiMgmt::_updateWifiScan() {
293	    _wifiScanCache = "";
294	    int n = WiFi.scanNetworks(false, true);
295	    for (int i = 0; i < n; i++) {
296	        _wifiScanCache += "<option value='" + WiFi.SSID(i) + "'>" + WiFi.SSID(i) + " (" + String(WiFi.RSSI(i)) + "dBm)</option>";
297	    }
298	    if (n == 0) _wifiScanCache = "<option value=''>未发现 WiFi</option>";
299	    WiFi.scanDelete();
300	    _lastScanTime = millis();
301	}
302
303	String WiFiMgmt::_htmlPage() {
304	    if (_wifiScanCache.length() == 0) _updateWifiScan();
305
306	    String html;
307	    html.reserve(4096);
308	    html += "<!DOCTYPE html><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'>";
309	    html += "<meta name='theme-color' content='#12a889'><title>设备配网</title>
";
330
331	    // Header + 提示
332	    html += "

📡

观照-五劳无感 设备配网

WiFi 联网与参数配置

";
333	    html += "
💡 也可用 ESP-Touch App 一键配网
";
334
335	    html += "<form method='POST' action='/save'>";
336	    html += "

📶

无线网络

选择 WiFi 或使用 SmartConfig

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

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

";
341
342	    html += "

⚙

设备基础信息

唯一标识与安装位置

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

";
346	    html += "
安装环境
🏠<input type='text' name='env_note' placeholder='如: 主卧 / 客厅 / 老人房' value='";
347	    html += _config.env_note;
348	    html += "'>

";
349
350	    html += "<input type='hidden' name='mqtt_server' value='";
351	    html += _config.mqtt_server;
352	    html += "'><input type='hidden' name='mqtt_port' value='";
353	    html += _config.mqtt_port;
354	    html += "'><input type='hidden' name='mqtt_user' value='";
355	    html += _config.mqtt_user;
356	    html += "'><input type='hidden' name='mqtt_pass' value='";
357	    html += _config.mqtt_pass;
358	    html += "'>";
359
360	    html += "
<button type='submit'>保存配置并重启连接</button>
</form>";
361	    html += "<button class='danger' onclick=\"if(confirm('确定恢复出厂?'))location.href='/reset'\">恢复出厂配置</button>";
362	    html += "
Health Monitor · Device Provisioning

";
363
364	    return html;
365	}
366
367	// ============================================================
368	// 重置
369	// ============================================================
370
371	void WiFiMgmt::resetSettings() {
372	    Serial.println(("[WiFi] 清除所有配置..."));
373	    if (LittleFS.exists(CONFIG_FILE_WIFI))   LittleFS.remove(CONFIG_FILE_WIFI);
374	    if (LittleFS.exists(CONFIG_FILE_MQTT))   LittleFS.remove(CONFIG_FILE_MQTT);
375	    if (LittleFS.exists(CONFIG_FILE_DEVICE)) LittleFS.remove(CONFIG_FILE_DEVICE);
376	    WiFi.disconnect(true, true);
377	    for (int i = 0; i < 10; i++) { digitalWrite(LED_WIFI_PIN, !digitalRead(LED_WIFI_PIN)); delay(100); }
378	    Serial.println(("[WiFi] 正在重启..."));
379	    delay(500);
380	    ESP.restart();
381	}
382
383	// ============================================================
384	// 按钮
385	// ============================================================
386
387	bool WiFiMgmt::_checkButtonLongPress() {
388	    bool pressed = (digitalRead(CONFIG_BUTTON_PIN) == LOW);
389	    if (pressed && !_buttonPressed) {
390	        _buttonPressed = true; _buttonPressTime = millis();
391	    } else if (!pressed && _buttonPressed) {
392	        _buttonPressed = false;
393	        if (millis() - _buttonPressTime >= BUTTON_LONG_PRESS_MS) return true;
394	    }
395	    return false;
396	}
397
398	// ============================================================
399	// LittleFS
400	// ============================================================
401
402	bool WiFiMgmt::_loadConfig() {
403	    if (!LittleFS.exists(CONFIG_FILE_DEVICE)) return false;
404	    File file = LittleFS.open(CONFIG_FILE_DEVICE, "r");
405	    if (!file) return false;
406	    JsonDocument doc;
407	    DeserializationError err = deserializeJson(doc, file);
408	    file.close();
409	    if (err) return false;
410	    strncpy(_config.mqtt_server, doc["mqtt_server"] | MQTT_DEFAULT_SERVER, sizeof(_config.mqtt_server)-1);
411	    strncpy(_config.mqtt_port, doc["mqtt_port"] | MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port)-1);
412	    strncpy(_config.mqtt_user, doc["mqtt_user"] | MQTT_DEFAULT_USER, sizeof(_config.mqtt_user)-1);
413	    strncpy(_config.mqtt_pass, doc["mqtt_pass"] | MQTT_DEFAULT_PASS, sizeof(_config.mqtt_pass)-1);
414	    strncpy(_config.device_id, doc["device_id"] | MQTT_DEFAULT_DEVICE, sizeof(_config.device_id)-1);
415	    strncpy(_config.env_note, doc["env_note"] | MQTT_DEFAULT_ENV, sizeof(_config.env_note)-1);
416	    strncpy(_config.env_type, doc["env_type"] | "bedroom", sizeof(_config.env_type)-1);
417	    strncpy(_config.wifi_ssid, doc["wifi_ssid"] | "", sizeof(_config.wifi_ssid)-1);
418	    strncpy(_config.wifi_pass, doc["wifi_pass"] | "", sizeof(_config.wifi_pass)-1);
419	    Serial.printf("[WiFi] 配置加载, 已存WiFi: %s\n", _config.wifi_ssid[0] ? _config.wifi_ssid : "(空)");
420	    return true;
421	}
422
423	bool WiFiMgmt::_saveConfig() {
424	    JsonDocument doc;
425	    doc["mqtt_server"] = _config.mqtt_server;
426	    doc["mqtt_port"]   = _config.mqtt_port;
427	    doc["mqtt_user"]   = _config.mqtt_user;
428	    doc["mqtt_pass"]   = _config.mqtt_pass;
429	    doc["device_id"]   = _config.device_id;
430	    doc["env_note"]    = _config.env_note;
431	    doc["env_type"]    = _config.env_type;
432	    doc["wifi_ssid"]   = _config.wifi_ssid;
433	    doc["wifi_pass"]   = _config.wifi_pass;
434	    File file = LittleFS.open(CONFIG_FILE_DEVICE, "w");
435	    if (!file) return false;
436	    if (serializeJson(doc, file) == 0) { file.close(); return false; }
437	    file.close();
438	    return true;
439	}
440
441	// ============================================================
442	// Getter
443	// ============================================================
444
445	bool WiFiMgmt::isConnected() const { return _connected && (WiFi.status() == WL_CONNECTED); }
446	bool WiFiMgmt::justProvisioned() { bool v = _provisionSuccess; _provisionSuccess = false; return v; }
447	DeviceConfig WiFiMgmt::getConfig() const { return _config; }
448	const char* WiFiMgmt::getDeviceId() const { return _config.device_id; }
449	const char* WiFiMgmt::getEnvNote() const { return _config.env_note; }
450
451	// ============================================================
452	// LED
453	// ============================================================
454
455	void WiFiMgmt::_setLEDMode(uint8_t mode) { _ledMode = mode; }
456
457	void WiFiMgmt::_ledTaskFunc(void* param) {
458	    const TickType_t fastMs = pdMS_TO_TICKS(300);
459	    const TickType_t slowMs = pdMS_TO_TICKS(1000);
460	    while (true) {
461	        if (!_instance) { vTaskDelay(slowMs); continue; }
462	        uint8_t mode = _instance->_ledMode;
463	        switch (mode) {
464	        case 0: digitalWrite(LED_WIFI_PIN, LOW); vTaskDelay(slowMs); break;
465	        case 1: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(slowMs);
466	                digitalWrite(LED_WIFI_PIN, LOW);  vTaskDelay(slowMs); break;
467	        case 2: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(fastMs);
468	                digitalWrite(LED_WIFI_PIN, LOW);  vTaskDelay(fastMs); break;
469	        case 3: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(slowMs); break;
470	        }
471	    }
472	}
473

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

#include "wifi_manager.h"

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)
{
    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() {
    // --- 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) {
    WiFi.mode(WIFI_STA);
    WiFi.persistent(true);   // 确保持久化 WiFi 密码到 NVS (断电不丢)
    WiFi.begin(ssid, pass);
    int retry = 0;
    while (WiFi.status() != WL_CONNECTED && retry < 40) {  // 20s
        delay(500); Serial.print("."); retry++;
    }
    bool ok = (WiFi.status() == WL_CONNECTED);
    Serial.printf("\n[WiFi] 连接结果: %s  status=%d\n", ok ? "成功!" : "失败", WiFi.status());
    return ok;
}

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

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

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

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

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

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

    _portalActive = true;
    _portalStartTime = millis();

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

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

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

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

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

        _server->send(200, "text/html; charset=utf-8",
            "<!DOCTYPE html><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 += "
安装环境
🏠<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;
}

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

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 <ArduinoJson.h>
23	#include "config.h"
24
25	// 设备配置结构体
26	struct DeviceConfig {
27	    char mqtt_server[40];
28	    char mqtt_port[6];
29	    char mqtt_user[20];
30	    char mqtt_pass[20];
31	    char device_id[20];
32	    char env_note[20];      // 环境备注 (房间名)
33	    char env_type[16];      // 环境类型 bedroom/bathroom/...
34	    char wifi_ssid[33];     // WiFi SSID
35	    char wifi_pass[65];     // WiFi 密码
36	};
37
38	class WiFiMgmt {
39	public:
40	    WiFiMgmt();
41	    ~WiFiMgmt();
42
43	    // 单例指针 (用于静态回调)
44	    static WiFiMgmt* _instance;
45
46	    bool begin();
47	    bool loop();
48	    bool isConnected() const;
49	    bool justProvisioned();     // 刚配网成功 (读取后自动清除)
50	    DeviceConfig getConfig() const;
51	    const char* getDeviceId() const;
52	    const char* getEnvNote() const;
53	    void startConfigPortal();
54	    void resetSettings();
55
56	private:
57	    bool _connectWiFi();          // 仅尝试已保存凭据
58	    bool _tryConnect(const char* ssid, const char* pass);  // 尝试连接指定 WiFi
59	    bool _loadConfig();
60	    bool _saveConfig();
61	    void _setLEDMode(uint8_t mode);
62	    static void _ledTaskFunc(void* param);
63	    bool _checkButtonLongPress();
64
65	    // Web 服务器
66	    void _startPortal();
67	    void _stopPortal();
68	    void _handleRoot();
69	    void _handleSave();
70	    void _handleReset();
71	    void _updateWifiScan();    // WiFi 扫描缓存
72	    String _htmlPage();        // 返回配网 HTML 页面
73
74	    WebServer*     _server;
75	    DNSServer*     _dns;
76	    DeviceConfig   _config;
77	    bool           _connected;
78	    bool           _portalActive;
79	    bool           _portalStopFlag;     // 延迟关闭标记
80	    uint32_t       _portalStartTime;
81	    String         _pendingSSID;
82	    String         _pendingPass;
83	    bool           _provisionSuccess;  // 刚配网成功标记
84	    uint32_t       _lastCheckTime;
85	    uint32_t       _buttonPressTime;
86	    bool           _buttonPressed;
87
88	    // WiFi 扫描缓存 (避免每次打开页面都扫描3-8秒)
89	    String         _wifiScanCache;
90	    uint32_t       _lastScanTime;
91
92	    // LED 独立任务
93	    TaskHandle_t    _ledTaskHandle;
94	    volatile uint8_t _ledMode;  // 0=灭 1=慢闪 2=快闪 3=常亮
95	};
96
97	#endif // WIFI_MANAGER_H
98

/**
 * @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 <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 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 页面

    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=常亮
};

#endif // WIFI_MANAGER_H


行给我加个蓝牙配网的测试版本


The user wants me to add BLE provisioning as a test version. Let me plan the changes:

1. Add BLE provisioning directly into `wifi_manager` — when AP starts, BLE also starts broadcasting
2. A BLE characteristic that accepts JSON write → same save logic as Web
3. Need to:
   - Add `#include <BLEDevice.h>` etc. to wifi_manager
   - Add `_startBLE()` / `_stopBLE()` methods
   - Modify `_startPortal()` to also start BLE
   - Modify `_stopPortal()` to also stop BLE
   - Add BLE callback class for handling write

Let me now implement this. I'll add BLE directly into wifi_manager rather than creating a separate file, to keep it simple and use the same save logic.

Key points:
- BLE device name: "HealthMonitor" (same as WiFi SSID concept)
- Service UUID: custom, like `4fafc201-1fb5-459e-8fcc-c5c9c331914b`
- Characteristic UUID: `4fafc202-1fb5-459e-8fcc-c5c9c331914b` (WRITE property)
- On write: parse JSON, save config, stop portal + BLE, connect WiFi

I need to add BLE libraries - on ESP32 they should be built-in with the Arduino framework. Let me check platformio.ini for the framework.

Actually wait, I need to check if PlatformIO has the BLE libraries available. On ESP32 Arduino framework, the BLE libraries are included by default. But the user might need to check that their platformio.ini has the right framework.

Let me also check platformio.ini.


1	; PlatformIO Project Configuration File
2	; ESP32-S3 + R60ABD1 60G 毫米波雷达 健康监测系统
3
4	[env:esp32-s3-devkitc-1]
5	platform = espressif32
6	board = esp32-s3-devkitc-1
7	framework = arduino
8
9	; 串口监视器
10	monitor_speed = 115200
11	monitor_filters = direct
12	monitor_rts = 0
13	monitor_dtr = 0
14
15	; Flash 配置
16	board_build.flash_mode = qio
17	board_build.filesystem = littlefs
18	board_build.partitions = default_8MB.csv
19	board_build.arduino.memory_type = qio_opi
20
21	; 编译选项 - 优化速度
22	build_flags =
23	    -Os
24	    -DCORE_DEBUG_LEVEL=0
25	    -DARDUINO_USB_CDC_ON_BOOT=1
26	    -DBOARD_HAS_PSRAM
27
28	; 库依赖
29	lib_deps =
30	    ; MQTT 客户端
31	    knolleary/PubSubClient@^2.8
32
33	    ; JSON 解析 (更省内存)
34	    bblanchon/ArduinoJson@^7.1.0
35
36	    ; DHT11 温湿度传感器
37	    adafruit/DHT sensor library@^1.4.6
38	    adafruit/Adafruit Unified Sensor@^1.1.14
39
40	    ; OLED 显示屏 (SSD1306 128x64 I2C)
41	    olikraus/U8g2@^2.35.30
42
43	; 上传配置
44	upload_speed = 921600
45	upload_port = COM18
46

; PlatformIO Project Configuration File
; ESP32-S3 + R60ABD1 60G 毫米波雷达 健康监测系统

[env:esp32-s3-devkitc-1]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino

; 串口监视器
monitor_speed = 115200
monitor_filters = direct
monitor_rts = 0
monitor_dtr = 0

; Flash 配置
board_build.flash_mode = qio
board_build.filesystem = littlefs
board_build.partitions = default_8MB.csv
board_build.arduino.memory_type = qio_opi

; 编译选项 - 优化速度
build_flags =
    -Os
    -DCORE_DEBUG_LEVEL=0
    -DARDUINO_USB_CDC_ON_BOOT=1
    -DBOARD_HAS_PSRAM

; 库依赖
lib_deps =
    ; MQTT 客户端
    knolleary/PubSubClient@^2.8

    ; JSON 解析 (更省内存)
    bblanchon/ArduinoJson@^7.1.0

    ; DHT11 温湿度传感器
    adafruit/DHT sensor library@^1.4.6
    adafruit/Adafruit Unified Sensor@^1.1.14

    ; OLED 显示屏 (SSD1306 128x64 I2C)
    olikraus/U8g2@^2.35.30

; 上传配置
upload_speed = 921600
upload_port = COM18


OK, so the framework is `arduino` on `espressif32`. The ESP32 Arduino framework includes BLE libraries (BLEDevice, BLEServer, BLECharacteristicCallbacks, etc.) by default — no additional library dependencies needed.

Now let me plan the implementation:

1. **wifi_manager.h** — Add:
   - `_startBLE()` / `_stopBLE()` declarations
   - BLE callback forward declaration
   - BLE-related members (server, service, characteristic pointers)

2. **wifi_manager.cpp** — Add:
   - Include `<BLEDevice.h>`, `<BLEServer.h>`, `<BLE2902.h>` headers
   - BLE callback class that handles onWrite
   - `_startBLE()` — init BLE device, create server/service/characteristic, start advertising
   - `_stopBLE()` — stop advertising, cleanup
   - Modify `_startPortal()` — call `_startBLE()` after starting AP
   - Modify `_stopPortal()` — call `_stopBLE()` before stopping AP

Let me implement this now.

Actually, I need to be careful about the BLE callback. The callback class needs access to WiFiMgmt to call its save methods. Since WiFiMgmt has a static `_instance` pointer, this should work.

Let me write the code:

For wifi_manager.h, I need to add:
```cpp
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>

// Forward declare the callback class
class BLEProvisionCallbacks;

// In the class:
void _startBLE();
void _stopBLE();
void _handleBLEProvision(const char* json);

BLEServer*           _bleServer;
BLECharacteristic*   _bleChar;
BLEAdvertising*      _bleAdv;
```

For wifi_manager.cpp, I need to add:
```cpp
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

// BLE 配网回调类
class BLEProvisionCallbacks : public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic* ch) {
        std::string value = ch->getValue();
        if (value.length() > 0 && WiFiMgmt::_instance) {
            WiFiMgmt::_instance->_handleBLEProvision(value.c_str());
        }
    }
};

void WiFiMgmt::_startBLE() {
    BLEDevice::init("HealthMonitor");
    _bleServer = BLEDevice::createServer();

    BLEService* svc = _bleServer->createService("4fafc201-1fb5-459e-8fcc-c5c9c331914b");
    _bleChar = svc->createCharacteristic(
        "4fafc202-1fb5-459e-8fcc-c5c9c331914b",
        BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY
    );
    _bleChar->setCallbacks(new BLEProvisionCallbacks());
    _bleChar->addDescriptor(new BLE2902());
    svc->start();

    _bleAdv = _bleServer->getAdvertising();
    _bleAdv->addServiceUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b");
    _bleAdv->start();

    Serial.println("[BLE] 配网广播已启动: HealthMonitor");
}

void WiFiMgmt::_stopBLE() {
    if (_bleAdv) { _bleAdv->stop(); _bleAdv = nullptr; }
    // BLE deinit will happen when server is deleted
    BLEDevice::deinit(false);
    _bleServer = nullptr;
}

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

    // 和 Web 配网完全相同的处理逻辑
    const char* ssid = doc["s"] | "";
    const char* pass = doc["p"] | "";

    _pendingSSID = ssid;
    _pendingPass = pass;

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

    Serial.println("[BLE] 配网参数已接收, 即将连接 WiFi...");

    // 回复 "OK" 给小程序确认
    _bleChar->setValue("OK");
    _bleChar->notify();
}
```

Now I also need to add the `_handleBLEProvision` declaration to the header file. Let me also make sure `_pendingSSID` and `_pendingPass` and `_portalStopFlag` are accessible.

Wait, looking at the header, `_pendingSSID`, `_pendingPass`, `_portalStopFlag` are private members. The BLE callback class is a friend/accesses via `_instance` which is a public static pointer. Actually, `_handleBLEProvision` will be a member function of `WiFiMgmt`, so it has access to all private members. That's fine.

Let me also think about whether I need to use `_pendingSSID` and `_pendingPass`. Looking at the current `_handleSave()`, it sets:
1. `_pendingSSID` / `_pendingPass`
2. `_config.wifi_ssid` / `_config.wifi_pass`
3. `_config.device_id` / `_config.env_note` / etc.
4. Calls `_saveConfig()`
5. Sets `_portalStopFlag = true`

Then in `loop()`, when `_portalStopFlag` is true:
```cpp
if (_pendingSSID.length() > 0) {
    _connected = _tryConnect(_pendingSSID.c_str(), _pendingPass.c_str());
    _pendingSSID = ""; _pendingPass = "";
    ...
}
```

So I need to follow the same pattern in `_handleBLEProvision`. Let me write it.

Actually, let me re-read the loop logic more carefully:

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

So when portal stops, it tries to connect. If connection fails, it restarts the portal. Good.

One issue: when BLE provisioning is used, the portal stop also stops BLE. Then if WiFi connection fails, portal restarts → BLE also restarts. That's the right behavior.

But wait, there's a subtle issue. When `_startPortal()` is called again after connect failure, it calls `_startBLE()` again. But `BLEDevice::deinit(false)` was called in `_stopPortal()`. Calling `BLEDevice::init()` again might cause issues. Let me check...

Actually, `BLEDevice::init()` is typically only called once. Calling it again after `deinit()` should work on ESP32, but to be safe, let me handle this. Looking at ESP32 BLE Arduino API, `BLEDevice::deinit()` should reset all BLE state. So calling `init()` again should work.

Actually, let me be more careful. I'll handle the case where BLE might already be initialized. But for simplicity in a test version, let's just do it and see.

One more thing: I should not call `_startBLE()` if BLE is already active. Let me add a check:

```cpp
void WiFiMgmt::_startBLE() {
    if (_bleServer != nullptr) return;  // 已经启动了
    ...
}
```

OK let me now write all the changes. Let me also need to check if there's a compile flag issue - the BLE libraries on ESP32-S3 require BLE to be enabled in menuconfig. In PlatformIO, it's on by default for ESP32-S3.

Actually, I realize I also need to handle the `_bleServer`, `_bleChar`, `_bleAdv` initialization in the constructor. Let me add them to the constructor initializer list.

Let me now write all the code.

For the UUIDs, I'll use the standard BLE UUID format: `0000xxxx-0000-1000-8000-00805f9b34fb`. But actually custom UUIDs are fine. Let me use simple ones:

Service: `"4fafc201-1fb5-459e-8fcc-c5c9c331914b"`
Characteristic: `"4fafc202-1fb5-459e-8fcc-c5c9c331914b"`

These are random UUIDs that won't conflict with any standard BLE services.

OK, let me write the changes now.


TodoWrite

wifi_manager.h: 添加 BLE 相关声明

添加 BLE 头文件声明

wifi_manager.cpp: 实现 BLE 配网逻辑

编译验证


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

wifi_manager.h: 添加 BLE 相关声明

添加 BLE 头文件声明

wifi_manager.cpp: 实现 BLE 配网逻辑

编译验证


先改头文件，加 BLE 声明：


Edit

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <DNSServer.h>
#include <LittleFS.h>
#include <ArduinoJson.h>
#include "config.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"


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)

#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"

/**
 * @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 <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 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 页面

    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=常亮
};

#endif // WIFI_MANAGER_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"


Now add the BLE method declarations to the header file. I need to add `_startBLE()`, `_stopBLE()`, `_handleBLEProvision()` and the BLE member variables.


    /** 蓝牙配网 BLE — 小程序/APP 写入 JSON */
    void _startBLE();                      ///< 启动 BLE 广播
    void _stopBLE();                       ///< 停止 BLE
    void handleBLEProvision(const char* json); ///< 解析并保存配网 JSON

    BLEServer*           _bleServer;       ///< BLE 服务器
    BLECharacteristic*   _bleChar;         ///< 配网写入特征
    BLEAdvertising*      _bleAdv;          ///< 广播

    // BLE 配网
    void _startBLE();
    void _stopBLE();
    void handleBLEProvision(const char* json);

    BLEServer*           _bleServer;
    BLECharacteristic*   _bleChar;
    BLEAdvertising*      _bleAdv;


<tool_use_error>String to replace not found in file.
String:     /** 蓝牙配网 BLE — 小程序/APP 写入 JSON */
    void _startBLE();                      ///< 启动 BLE 广播
    void _stopBLE();                       ///< 停止 BLE
    void handleBLEProvision(const char* json); ///< 解析并保存配网 JSON

    BLEServer*           _bleServer;       ///< BLE 服务器
    BLECharacteristic*   _bleChar;         ///< 配网写入特征
    BLEAdvertising*      _bleAdv;          ///< 广播
(note: Edit also tried swapping \uXXXX escapes and their characters; neither form matched, so the mismatch is likely elsewhere in old_string. Re-read the file and copy the exact surrounding text.)</tool_use_error>

Error: String to replace not found in file.
String:     /** 蓝牙配网 BLE — 小程序/APP 写入 JSON */
    void _startBLE();                      ///< 启动 BLE 广播
    void _stopBLE();                       ///< 停止 BLE
    void handleBLEProvision(const char* json); ///< 解析并保存配网 JSON

    BLEServer*           _bleServer;       ///< BLE 服务器
    BLECharacteristic*   _bleChar;         ///< 配网写入特征
    BLEAdvertising*      _bleAdv;          ///< 广播
(note: Edit also tried swapping \uXXXX escapes and their characters; neither form matched, so the mismatch is likely elsewhere in old_string. Re-read the file and copy the exact surrounding text.)


Hmm, the old_string didn't match. Let me read the file again to see the exact current state.


75	    void _updateWifiScan();    // WiFi 扫描缓存
76	    String _htmlPage();        // 返回配网 HTML 页面
77
78	    WebServer*     _server;
79	    DNSServer*     _dns;
80	    DeviceConfig   _config;
81	    bool           _connected;
82	    bool           _portalActive;
83	    bool           _portalStopFlag;     // 延迟关闭标记
84	    uint32_t       _portalStartTime;
85	    String         _pendingSSID;
86	    String         _pendingPass;
87	    bool           _provisionSuccess;  // 刚配网成功标记
88	    uint32_t       _lastCheckTime;
89	    uint32_t       _buttonPressTime;
90	    bool           _buttonPressed;
91
92	    // WiFi 扫描缓存 (避免每次打开页面都扫描3-8秒)
93	    String         _wifiScanCache;
94	    uint32_t       _lastScanTime;

    void _updateWifiScan();    // WiFi 扫描缓存
    String _htmlPage();        // 返回配网 HTML 页面

    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;


I see - I was trying to add the declarations after `_handleReset()` and `_updateWifiScan()` but I need to find the right place. Let me add the BLE methods between the `_htmlPage()` declaration and the private member variables.


    void _updateWifiScan();    // WiFi 扫描缓存
    String _htmlPage();        // 返回配网 HTML 页面

    WebServer*     _server;

    void _updateWifiScan();    // WiFi 扫描缓存
    String _htmlPage();        // 返回配网 HTML 页面

    // BLE 蓝牙配网
    void _startBLE();
    void _stopBLE();
    void handleBLEProvision(const char* json);

    WebServer*     _server;


    void _updateWifiScan();    // WiFi 扫描缓存
    String _htmlPage();        // 返回配网 HTML 页面

    // BLE 蓝牙配网
    void _startBLE();
    void _stopBLE();
    void handleBLEProvision(const char* json);

    WebServer*     _server;

/**
 * @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 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 页面

    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=常亮
};

#endif // WIFI_MANAGER_H

String _htmlPage();        // 返回配网 HTML 页面

+    void handleBLEProvision(const char* json);


Now add the BLE member variables after the last private member.


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

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

    // BLE
    BLEServer*           _bleServer;
    BLECharacteristic*   _bleChar;
    BLEAdvertising*      _bleAdv;
};


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

    // BLE
    BLEServer*           _bleServer;
    BLECharacteristic*   _bleChar;
    BLEAdvertising*      _bleAdv;
};

/**
 * @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 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();
    void handleBLEProvision(const char* json);

    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=常亮
};

#endif // WIFI_MANAGER_H


Good, header file done. Now let me work on the cpp file. I need to:

1. Add BLE includes at the top
2. Add BLE callback class
3. Initialize BLE members in constructor
4. Add _startBLE(), _stopBLE(), handleBLEProvision() implementations
5. Modify _startPortal() to also start BLE
6. Modify _stopPortal() to also stop BLE

Let me also think about the BLE callback class - it needs to access WiFiMgmt::_instance. Since _instance is public static, the callback can use it. But handleBLEProvision needs to be called from the callback, and it needs to be public or the callback class needs to be a friend.

Actually, I already declared `handleBLEProvision` as private in the header. Let me make it public so the BLE callback can access it via the instance pointer. Wait, actually the callback class is a free-standing class, not a member of WiFiMgmt. It can still call `_instance->handleBLEProvision(...)` if the method is public. But since I already declared it as private...

Let me just move it to public section. Or better yet, since the callback goes through `_instance` which gives full access, and the method needs to be callable from outside the class, let me just make it public.

Actually wait - I can keep it private and use a friend declaration. But that's overkill. Let me just make handleBLEProvision public.

Let me re-read the header to see where I placed it.


把头文件里的 `handleBLEProvision` 改成 public：


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

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


I see that `handleBLEProvision` is in the private section (after `_updateWifiScan`). I need to move it to the public section. Let me add it to the public methods and remove it from private.


    void startConfigPortal();
    void handleBLEProvision(const char* json);  // BLE 配网回调


    void startConfigPortal();
    void handleBLEProvision(const char* json);  // BLE 配网回调

/**
 * @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 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();
    void handleBLEProvision(const char* json);

    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*   _bleChar;
    BLEAdvertising*      _bleAdv;
};

#endif // WIFI_MANAGER_H

+    void handleBLEProvision(const char* json);  // BLE 配网回调


    // BLE 蓝牙配网
    void _startBLE();
    void _stopBLE();
    void handleBLEProvision(const char* json);

    WebServer*     _server;

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

    WebServer*     _server;


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

    WebServer*     _server;

/**
 * @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 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();
    void handleBLEProvision(const char* json);

    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*   _bleChar;
    BLEAdvertising*      _bleAdv;
};

#endif // WIFI_MANAGER_H

-    void handleBLEProvision(const char* json);


wifi_manager.h: 添加 BLE 相关声明

添加 BLE 头文件声明

wifi_manager.cpp: 实现 BLE 配网逻辑

编译验证


行给我加个蓝牙配网的测试版本


Good, header file is done. Now let me work on wifi_manager.cpp. I need to:

1. Add BLE callback class at the top of the file
2. Initialize BLE members in constructor: `_bleServer(nullptr), _bleChar(nullptr), _bleAdv(nullptr)`
3. Add `_startBLE()`, `_stopBLE()`, `handleBLEProvision()` implementations
4. Modify `_startPortal()` to also start BLE
5. Modify `_stopPortal()` to also stop BLE

Let me do this step by step.


TodoWrite

添加 BLE 头文件声明

wifi_manager.h: 添加 BLE 相关声明

wifi_manager.cpp: 实现 BLE 配网逻辑

编译验证


wifi_manager.h: 添加 BLE 相关声明

添加 BLE 头文件声明

wifi_manager.cpp: 实现 BLE 配网逻辑

编译验证

wifi_manager.h: 添加 BLE 相关声明

添加 BLE 头文件声明

wifi_manager.cpp: 实现 BLE 配网逻辑

编译验证


Now edit wifi_manager.cpp. Let me add the BLE callback class after the includes, and modify the constructor.


现在改 `wifi_manager.cpp`，先加 BLE 回调类：


Edit

#include "wifi_manager.h"

// BLE 配网回调: 小程序写入 JSON → 触发配网
class BLEProvisionCB : public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic* ch) override {
        std::string val = ch->getValue();
        if (val.length() > 0 && WiFiMgmt::_instance) {
            Serial.printf("[BLE] 收到配网数据 (%d 字节)\n", val.length());
            WiFiMgmt::_instance->handleBLEProvision(val.c_str());
        }
    }
};


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)

#include "wifi_manager.h"

// BLE 配网回调: 小程序写入 JSON → 触发配网
class BLEProvisionCB : public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic* ch) override {
        std::string val = ch->getValue();
        if (val.length() > 0 && WiFiMgmt::_instance) {
            Serial.printf("[BLE] 收到配网数据 (%d 字节)\n", val.length());
            WiFiMgmt::_instance->handleBLEProvision(val.c_str());
        }
    }
};

+// BLE 配网回调: 小程序写入 JSON → 触发配网

+class BLEProvisionCB : public BLECharacteristicCallbacks {

+    void onWrite(BLECharacteristic* ch) override {

+        std::string val = ch->getValue();

+        if (val.length() > 0 && WiFiMgmt::_instance) {

+            Serial.printf("[BLE] 收到配网数据 (%d 字节)\n", val.length());

+            WiFiMgmt::_instance->handleBLEProvision(val.c_str());

WiFiMgmt* WiFiMgmt::_instance = nullptr;


Now add BLE member initialization to the constructor.


初始化 BLE 成员：


    , _provisionSuccess(false)
    , _ledTaskHandle(nullptr), _ledMode(0)

    , _provisionSuccess(false)
    , _ledTaskHandle(nullptr), _ledMode(0)
    , _bleServer(nullptr), _bleChar(nullptr), _bleAdv(nullptr)


    , _provisionSuccess(false)
    , _ledTaskHandle(nullptr), _ledMode(0)
    , _bleServer(nullptr), _bleChar(nullptr), _bleAdv(nullptr)

+    , _bleServer(nullptr), _bleChar(nullptr), _bleAdv(nullptr)


Now modify `_startPortal()` to also start BLE:


在 `_startPortal()` 末尾加上 BLE 启动：


    _portalActive = true;
    _portalStartTime = millis();

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

    _portalActive = true;
    _portalStartTime = millis();

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

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


    _portalActive = true;
    _portalStartTime = millis();

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

    _portalActive = true;
    _portalStartTime = millis();

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

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


Now modify `_stopPortal()` to also stop BLE:


在 `_stopPortal()` 里加上 BLE 停止：


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

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


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

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


Now I need to add the BLE method implementations at the end of the file, before the `// ============================================================` section. Let me find a good place to add them.


现在加 BLE 三个方法的实现。在 `_htmlPage()` 之后插入：


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

void WiFiMgmt::_startBLE() {
    if (_bleServer != nullptr) return;  // 已启动

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

    BLEService* svc = _bleServer->createService("4fafc201-1fb5-459e-8fcc-c5c9c331914b");
    _bleChar = svc->createCharacteristic(
        "4fafc202-1fb5-459e-8fcc-c5c9c331914b",
        BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY
    );
    _bleChar->setCallbacks(new BLEProvisionCB());
    _bleChar->addDescriptor(new BLE2902());  // 启用 notify
    svc->start();

    _bleAdv = _bleServer->getAdvertising();
    _bleAdv->addServiceUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b");
    _bleAdv->setScanResponse(true);
    _bleAdv->start();

    Serial.println("[BLE] ✅ 配网广播: HealthMonitor");
}

void WiFiMgmt::_stopBLE() {
    if (!_bleServer) return;
    if (_bleAdv) { _bleAdv->stop(); _bleAdv = nullptr; }
    BLEDevice::deinit(false);
    _bleServer = nullptr;
    _bleChar = nullptr;
    Serial.println("[BLE] 已停止");
}

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

    // 提取 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);
}

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


行给我加个蓝牙配网的测试版本


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

void WiFiMgmt::_startBLE() {
    if (_bleServer != nullptr) return;  // 已启动

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

    BLEService* svc = _bleServer->createService("4fafc201-1fb5-459e-8fcc-c5c9c331914b");
    _bleChar = svc->createCharacteristic(
        "4fafc202-1fb5-459e-8fcc-c5c9c331914b",
        BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY
    );
    _bleChar->setCallbacks(new BLEProvisionCB());
    _bleChar->addDescriptor(new BLE2902());  // 启用 notify
    svc->start();

    _bleAdv = _bleServer->getAdvertising();
    _bleAdv->addServiceUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b");
    _bleAdv->setScanResponse(true);
    _bleAdv->start();

    Serial.println("[BLE] ✅ 配网广播: HealthMonitor");
}

void WiFiMgmt::_stopBLE() {
    if (!_bleServer) return;
    if (_bleAdv) { _bleAdv->stop(); _bleAdv = nullptr; }
    BLEDevice::deinit(false);
    _bleServer = nullptr;
    _bleChar = nullptr;
    Serial.println("[BLE] 已停止");
}

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

    // 提取 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);
}

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

+    if (_bleServer != nullptr) return;  // 已启动

+    _bleServer = BLEDevice::createServer();

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

+    _bleChar = svc->createCharacteristic(

+        BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY

+    _bleChar->setCallbacks(new BLEProvisionCB());

+    _bleChar->addDescriptor(new BLE2902());  // 启用 notify

+    _bleAdv = _bleServer->getAdvertising();

+    _bleAdv->addServiceUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b");

+    Serial.println("[BLE] ✅ 配网广播: HealthMonitor");

+    if (_bleAdv) { _bleAdv->stop(); _bleAdv = nullptr; }

+void WiFiMgmt::handleBLEProvision(const char* json) {

+    DeserializationError err = deserializeJson(doc, json);

+        Serial.printf("[BLE] JSON 解析失败: %s\n", err.c_str());

+        if (_bleChar) { _bleChar->setValue("ERR_JSON"); _bleChar->notify(); }

+        Serial.println("[BLE] 未提供 WiFi SSID");

+        if (_bleChar) { _bleChar->setValue("ERR_NO_SSID"); _bleChar->notify(); }

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

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

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

+    if (_bleChar) { _bleChar->setValue("OK"); _bleChar->notify(); }


wifi_manager.h: 添加 BLE 相关声明

添加 BLE 头文件声明

wifi_manager.cpp: 实现 BLE 配网逻辑

编译验证


All the code changes are done. Let me now try to compile to verify everything works. But pio is not available in the shell environment. Let me at least verify the files look correct by reading the final state of the key sections.

Actually, let me also check if the `#include <BLE2902.h>` path is correct. On ESP32 Arduino, it's typically `#include <BLE2902.h>` (without a subdirectory). This should work since BLE libraries are included with the ESP32 Arduino framework.

Let me also think about potential issues:
1. BLE needs heap memory - the BLE stack takes about 100KB. On ESP32-S3 with PSRAM, this should be fine.
2. BLE and WiFi coexistence - ESP32 handles this via time-slicing. Should be fine for provisioning use case.
3. The callback `BLEProvisionCB` is heap-allocated with `new` - memory leak, but it's only created once during portal startup, so it's fine.
4. The `BLE2902` descriptor is also heap-allocated - same reasoning.

Let me verify the final state of the files and mark the task done.


TodoWrite

添加 BLE 头文件声明

wifi_manager.h: 添加 BLE 相关声明

wifi_manager.cpp: 实现 BLE 配网逻辑

编译验证


wifi_manager.h: 添加 BLE 相关声明

添加 BLE 头文件声明

wifi_manager.cpp: 实现 BLE 配网逻辑

编译验证

wifi_manager.h: 添加 BLE 相关声明

添加 BLE 头文件声明

wifi_manager.cpp: 实现 BLE 配网逻辑

编译验证


Let me verify the final state of both files, and also try to compile.


最后确认下文件完整性：


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 resetSettings();
60
61	private:
62	    bool _connectWiFi();          // 仅尝试已保存凭据
63	    bool _tryConnect(const char* ssid, const char* pass);  // 尝试连接指定 WiFi
64	    bool _loadConfig();
65	    bool _saveConfig();
66	    void _setLEDMode(uint8_t mode);
67	    static void _ledTaskFunc(void* param);
68	    bool _checkButtonLongPress();
69
70	    // Web 服务器
71	    void _startPortal();
72	    void _stopPortal();
73	    void _handleRoot();
74	    void _handleSave();
75	    void _handleReset();
76	    void _updateWifiScan();    // WiFi 扫描缓存
77	    String _htmlPage();        // 返回配网 HTML 页面
78
79	    // BLE 蓝牙配网
80	    void _startBLE();
81	    void _stopBLE();
82
83	    WebServer*     _server;
84	    DNSServer*     _dns;
85	    DeviceConfig   _config;
86	    bool           _connected;
87	    bool           _portalActive;
88	    bool           _portalStopFlag;     // 延迟关闭标记
89	    uint32_t       _portalStartTime;
90	    String         _pendingSSID;
91	    String         _pendingPass;
92	    bool           _provisionSuccess;  // 刚配网成功标记
93	    uint32_t       _lastCheckTime;
94	    uint32_t       _buttonPressTime;
95	    bool           _buttonPressed;
96
97	    // WiFi 扫描缓存 (避免每次打开页面都扫描3-8秒)
98	    String         _wifiScanCache;
99	    uint32_t       _lastScanTime;
100
101	    // LED 独立任务
102	    TaskHandle_t    _ledTaskHandle;
103	    volatile uint8_t _ledMode;  // 0=灭 1=慢闪 2=快闪 3=常亮
104
105	    // BLE
106	    BLEServer*           _bleServer;
107	    BLECharacteristic*   _bleChar;
108	    BLEAdvertising*      _bleAdv;
109	};
110
111	#endif // WIFI_MANAGER_H
112

/**
 * @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 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*   _bleChar;
    BLEAdvertising*      _bleAdv;
};

#endif // WIFI_MANAGER_H


Header looks good. Let me try compiling now.


PowerShell

cd "d:\PlatformIO1\Projects\Healthv2.0"; & "C:\Users\杨志佳\.platformio\penv\Scripts\platformio.exe" run 2>&1

PlatformIO 编译验证


Error: Exit code 1
& : The term 'C:\Users\杨志佳\.platformio\penv\Scripts\platformio.exe' is not recognized as the name of a cmdlet, function
, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is
correct and try again.
At line:1 char:345
+ ... lthv2.0"; & "C:\Users\杨志佳\.platformio\penv\Scripts\platformio.exe" ru ...
+                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (C:\Users\杨志佳\.p...\platformio.exe:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException


Can't find pio executable. Let me try to find it.


Get-Command pio -ErrorAction SilentlyContinue; Get-Command platformio -ErrorAction SilentlyContinue; where.exe pio 2>$null; where.exe platformio 2>$null; dir "$env:USERPROFILE\.platformio\penv\Scripts" 2>$null | Select-Object -First 5

查找 PlatformIO 安装位置


Exit code 1


PlatformIO is not installed in this environment. The user will need to compile it themselves. Let me just present what was done and ask them to compile.


编译环境不在当前 shell 里，代码全部写完了。你在 VSCode 里点编译或者 `pio run` 就行。

---

### BLE 配网 — 改动汇总

| 文件 | 改动 |
|------|------|
| [wifi_manager.h:22-25](include/wifi_manager.h#L22-L25) | 新增 BLE 头文件 |
| [wifi_manager.h:58](include/wifi_manager.h#L58) | `handleBLEProvision()` — public 回调 |
| [wifi_manager.h:79-81](include/wifi_manager.h#L79-L81) | `_startBLE()` / `_stopBLE()` |
| [wifi_manager.h:105-108](include/wifi_manager.h#L105-L108) | BLE 成员变量 |
| [wifi_manager.cpp:8-18](src/wifi_manager.cpp#L8-L18) | `BLEProvisionCB` 回调类 |
| [wifi_manager.cpp:236](src/wifi_manager.cpp#L236) | `_startPortal()` 末尾启动 BLE |
| [wifi_manager.cpp:240](src/wifi_manager.cpp#L240) | `_stopPortal()` 开头停止 BLE |
| [wifi_manager.cpp:369-428](src/wifi_manager.cpp#L369-L428) | `_startBLE()` / `_stopBLE()` / `handleBLEProvision()` 三个方法 |

### 运行流程

```
配网启动
    ├── WiFi AP: HealthMonitor-Setup (192.168.4.1)
    └── BLE   : HealthMonitor (UUID: 4fafc201-...)
                  │
                  └── Char 4fafc202 (WRITE + NOTIFY)
                        │
        📱 小程序写 JSON ─┘
                        │
              handleBLEProvision(json)
                        │
              ┌─ 解析 ssid/pass/device_id/env_type/mqtt...
              ├─ 保存 LittleFS
              ├─ 回复 "OK" (BLE notify)
              └─ portalStopFlag → 关 AP + 关 BLE → 连 WiFi
```

### 小程序端写 BLE 数据格式

```json
{
  "s": "MyWiFi名称",
  "p": "WiFi密码",
  "device_id": "10100001001",
  "env_note": "主卧",
  "env_type": "bedroom",
  "mqtt_server": "202.110.234.102",
  "mqtt_port": "8000",
  "mqtt_user": "test",
  "mqtt_pass": "testtest"
}
```

### 设备名 & UUID 供小程序扫描

| 项目 | 值 |
|------|-----|
| 蓝牙名 | `HealthMonitor` |
| Service UUID | `4fafc201-1fb5-459e-8fcc-c5c9c331914b` |
| Char UUID | `4fafc202-1fb5-459e-8fcc-c5c9c331914b` |
| 写入后回复 | `"OK"` / `"ERR_JSON"` / `"ERR_NO_SSID"` |

编译试试，看看有没有报错。


Connection error.

The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the second argument to fetch()


Connection error.

Unable to connect to API (UNKNOWN_CERTIFICATE_VERIFICATION_ERROR)

unknown certificate verification error


Connection error.


Connection error.

unknown certificate verification error


<ide_opened_file>The user opened the file d:\PlatformIO1\Projects\Healthv2.0\src\wifi_manager.cpp in the IDE. This may or may not be related to the current task.</ide_opened_file>

蓝牙名	HealthMonitor
Service UUID	4fafc201-1fb5-459e-8fcc-c5c9c331914b
Char UUID	4fafc202-1fb5-459e-8fcc-c5c9c331914b
写入后回复	"OK" / "ERR_JSON" / "ERR_NO_SSID"
编译试试，看看有没有报错。啥意思


The user is asking "啥意思" (what does this mean) — they're confused about the BLE UUIDs and the table I showed. Let me explain in plain terms what these BLE concepts mean in the context of their device and the WeChat mini-program.

The user seems to not be familiar with BLE concepts. Let me explain simply.

UUID = 唯一标识符，相当于 BLE 通信的"地址"
Service UUID = 蓝牙服务的唯一编号，小程序用它来找到设备提供的配网服务
Characteristic UUID = 服务下面具体的数据通道编号，小程序往这里写入 WiFi 密码
"OK" / "ERR_JSON" / "ERR_NO_SSID" = 设备收到数据后给小程序回的确认消息


简单说就是：

- **蓝牙名 `HealthMonitor`** — 小程序扫描蓝牙时会看到这个名字，点它连接
- **Service UUID 和 Char UUID** — 小程序找到设备后，往这个"数据通道"里写 JSON 配网信息
- **回复 `OK`** — 设备收到配网数据后告诉小程序"收到了"

小程序端做的事：

```
1. 扫描蓝牙 → 找到 "HealthMonitor" → 连接
2. 用户填完 WiFi 密码点确定
3. 小程序把 JSON 通过 UUID 通道写入设备
4. 设备回 "OK" → 配网完成
```

UUID 那一长串你不用管，是程序之间约定的通信地址，烧录进去就行。先用 `pio run` 编译试试？


这个每一个设备都有uuid


The user seems concerned that every device has the same UUID. They're asking if the UUID is unique per device. Let me clarify - the UUIDs I used are hardcoded service/characteristic UUIDs that are the same across all devices. This is fine for BLE provisioning because:

1. BLE Service UUIDs are meant to identify the type of service, not individual devices. All devices running the same firmware should have the same service UUID so the app knows what service to connect to.
2. Each device is uniquely identified by its MAC address (hardware), not by UUID.
3. This is the standard BLE design pattern - same service UUID across all devices of the same type.

The user might be confused and thinking this is like a unique device ID. Let me clarify.


没关系，UUID 是**服务类型标识**，不是设备唯一 ID。所有同型号设备 **UUID 必须一样**，这是正确的设计。

类比：

| | 蓝牙配网 | 现实世界 |
|------|---------|------|
| UUID | ="加油站" 招牌 | 所有加油站招牌一样 |
| 设备区分 |= **MAC 地址**（硬件唯一） | 每个加油站的地址不同 |

- 小程序扫描到 3 个 `HealthMonitor` → 通过 **MAC 地址**区分哪个是你家哪个是邻居家的
- 所有设备用相同的 Service UUID → 小程序才知道"这是个配网服务"
- 设备唯一标识：`device_id`（你配置的 `10100001001`），存在 JSON 里写进去的

所以 UUID 不用改，硬件 MAC 地址保证唯一性。


吗


The user is asking "吗" which seems like they might have been cut off mid-sentence, or they're asking for confirmation — like "对吗?" meaning "right?" They just want confirmation that what I said is correct.

Let me just confirm simply.


对，放心烧录。同一个 UUID 所有设备共用，MAC 地址自动区分。


不用我每一个设备都去告诉吧