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*   _bleInfoChar;   // READ: 设备信息
108	    BLECharacteristic*   _bleChar;       // WRITE+NOTIFY: 配网
109	    BLEAdvertising*      _bleAdv;
110	};
111
112	#endif // WIFI_MANAGER_H
113

/**
 * @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*   _bleInfoChar;   // READ: 设备信息
    BLECharacteristic*   _bleChar;       // WRITE+NOTIFY: 配网
    BLEAdvertising*      _bleAdv;
};

#endif // WIFI_MANAGER_H


蓝牙配网后[BLE] 收到配网数据 (57 字节)Guru Meditation Error: Core  0 panic'ed (Unhandled debug exception). Debug exception reason: Stack canary watchpoint triggered (BTC_TASK) Core  0 register dump:PC      : 0x403…


Now let me also check the `loop()` function to see where to add the BLE pending processing.


103:bool WiFiMgmt::loop() {
104-    // --- Portal 模式 ---
105-    if (_portalActive) {
106-        if (_portalStopFlag) {
107-            _stopPortal();
108-            _portalStopFlag = false;
109-            if (_pendingSSID.length() > 0) {
110-                Serial.printf("[WiFi] 尝试连接: %s\n", _pendingSSID.c_str());
111-                _connected = _tryConnect(_pendingSSID.c_str(), _pendingPass.c_str());
112-                _pendingSSID = ""; _pendingPass = "";
113-                if (_connected) {
114-                    _setLEDMode(3);
115-                    _provisionSuccess = true;  // 标记配网成功
116-                } else {
117-                    _setLEDMode(2);
118-                    _startPortal();
119-                }
120-            }
121-            return _connected;
122-        }
123-
124-        _dns->processNextRequest();
125-        _server->handleClient();
126-        delay(10);
127-        return _connected;
128-    }
129-
130-    // --- 正常运行 ---
131-    if (_checkButtonLongPress()) {
132-        Serial.println(("[WiFi] 长按, 进配网模式"));
133-        _setLEDMode(2);
134-        WiFi.disconnect();
135-        _startPortal();
136-        _connected = false;
137-        return false;
138-    }
139-
140-    if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {
141-        _lastCheckTime = millis();
142-        wl_status_t s = WiFi.status();
143-        if (s != WL_CONNECTED) {
144-            if (_connected) {
145-                _connected = false;
146-                _setLEDMode(1);
147-                Serial.printf("[WiFi] 断开! status=%d RSSI=%d\n", s, WiFi.RSSI());
148-            }
149-            // 不手动 reconnect(), 让 WiFi.setAutoReconnect(true) 自动处理
150-        } else if (!_connected) {
151-            _connected = true;
152-            _setLEDMode(3);
153-            Serial.printf("[WiFi] 已连接 RSSI=%d\n", WiFi.RSSI());


Now I have a clear picture. I need to:

1. Add `_blePendingJson` (String) and `_blePendingFlag` (bool) to the header
2. Change the BLE callback to only save data and set flag
3. In `loop()`, check the flag and process in the main context (not BLE task)

Let me make the changes.


三步修复：① header 加 pending 标记、② 回调只存不处理、③ loop 里处理。


Edit

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

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

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


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)

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

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

/**
 * @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*   _bleInfoChar;   // READ: 设备信息
    BLECharacteristic*   _bleChar;       // WRITE+NOTIFY: 配网
    BLEAdvertising*      _bleAdv;
};

#endif // WIFI_MANAGER_H

+    // BLE 回调保护: 不在 BTC_TASK 栈里做 Flash 写操作

+    String        _blePendingJson;      // 待处理的 BLE 配网 JSON

+    bool          _blePendingFlag;      // 有未处理的 BLE 数据

+    void _processBLEPending();          // 在 loop() 上下文处理


Now let me update the BLE callback to only save the data:


// 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 → 仅在 BLE 栈中暂存, 不直接处理
class BLEProvisionCB : public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic* ch) override {
        std::string val = ch->getValue();
        if (val.length() > 0 && WiFiMgmt::_instance) {
            Serial.printf("[BLE] 收到配网数据 (%d 字节)\n", val.length());
            // 仅暂存, 不在这里处理 — BTC_TASK 栈太小, 写 Flash 会栈溢出
            WiFiMgmt::_instance->_blePendingJson = val.c_str();
            WiFiMgmt::_instance->_blePendingFlag = true;
        }
    }
};


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)

// 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 → 仅在 BLE 栈中暂存, 不直接处理
class BLEProvisionCB : public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic* ch) override {
        std::string val = ch->getValue();
        if (val.length() > 0 && WiFiMgmt::_instance) {
            Serial.printf("[BLE] 收到配网数据 (%d 字节)\n", val.length());
            // 仅暂存, 不在这里处理 — BTC_TASK 栈太小, 写 Flash 会栈溢出
            WiFiMgmt::_instance->_blePendingJson = val.c_str();
            WiFiMgmt::_instance->_blePendingFlag = true;
        }
    }
};

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

+// BLE 配网回调: 小程序写入 JSON → 仅在 BLE 栈中暂存, 不直接处理

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

+            // 仅暂存, 不在这里处理 — BTC_TASK 栈太小, 写 Flash 会栈溢出

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

+            WiFiMgmt::_instance->_blePendingFlag = true;


配网页 HTML 添加 env_type 下拉选择

添加环境类型选择器到配网页

_handleSave 保存 env_type 字段

编译验证


Now I need to:
1. Initialize `_blePendingFlag = false` in the constructor
2. Add `_processBLEPending()` implementation
3. Call `_processBLEPending()` in `loop()`

Let me update the constructor first.


Now initialize the flag in the constructor and add the processing in `loop()`:


    , _bleServer(nullptr), _bleInfoChar(nullptr), _bleChar(nullptr), _bleAdv(nullptr)
{

    , _bleServer(nullptr), _bleInfoChar(nullptr), _bleChar(nullptr), _bleAdv(nullptr)
    , _blePendingFlag(false)
{


    , _bleServer(nullptr), _bleInfoChar(nullptr), _bleChar(nullptr), _bleAdv(nullptr)
{

    , _bleServer(nullptr), _bleInfoChar(nullptr), _bleChar(nullptr), _bleAdv(nullptr)
    , _blePendingFlag(false)
{


<ide_diagnostics>[
  {
    "filePath": "/d:/PlatformIO1/Projects/Healthv2.0/src/wifi_manager.cpp",
    "line": 15,
    "column": 34,
    "message": "成员 \"WiFiMgmt::_blePendingJson\" (已声明 所在行数:112，所属文件:\"D:\\PlatformIO1\\Projects\\Healthv2.0\\include\\wifi_manager.h\") 不可访问",
    "code": "265",
    "severity": "Error"
  },
  {
    "filePath": "/d:/PlatformIO1/Projects/Healthv2.0/src/wifi_manager.cpp",
    "line": 16,
    "column": 34,
    "message": "成员 \"WiFiMgmt::_blePendingFlag\" (已声明 所在行数:113，所属文件:\"D:\\PlatformIO1\\Projects\\Healthv2.0\\include\\wifi_manager.h\") 不可访问",
    "code": "265",
    "severity": "Error"
  }
]</ide_diagnostics>


The _blePendingJson and _blePendingFlag are private members, but BLEProvisionCB is trying to access them. I need to make BLEProvisionCB a friend class, or make those members public, or move the logic differently.

Actually, looking at the existing code, `_instance` is already public (line 48 of the header). Let me just make `_blePendingJson` and `_blePendingFlag` public too, or better yet, add `friend class BLEProvisionCB` to the class.

The cleanest solution: make `_blePendingJson` and `_blePendingFlag` public (they're in the "private" section). Or add a friend declaration.

Actually the simplest fix: make those two members public. Or even better, add a small public method `enqueueBLEJson(const char* json)` that sets those.

Let me go with the public method approach - it's clean.


`_blePendingJson` 和 `_blePendingFlag` 是 private 成员，需要改成 public 或加个公开方法。加个 `enqueueBLEJson`：


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

    void handleBLEProvision(const char* json);  // BLE 配网回调
    void enqueueBLEJson(const char* json);       // BLE 线程安全入队 (仅暂存)


    void handleBLEProvision(const char* json);  // BLE 配网回调
    void enqueueBLEJson(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 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*   _bleInfoChar;   // READ: 设备信息
    BLECharacteristic*   _bleChar;       // WRITE+NOTIFY: 配网
    BLEAdvertising*      _bleAdv;

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

#endif // WIFI_MANAGER_H

+    void enqueueBLEJson(const char* json);       // BLE 线程安全入队 (仅暂存)


Wait, but `_blePendingJson` and `_blePendingFlag` are still private members. The `BLEProvisionCB` class needs to call `enqueueBLEJson` which is now public. Let me update the callback to use the public method.

Actually wait - `_blePendingJson` and `_blePendingFlag` are in the private section still, and `_processBLEPending()` is also private. `enqueueBLEJson` is now public. The BLEProvisionCB only needs to call `enqueueBLEJson` which is public now. Good.

Let me add the `enqueueBLEJson` method and `_processBLEPending` in the .cpp file.


Now add the `enqueueBLEJson` method and update the callback:


// 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());
            // 仅暂存, 不在这里处理 — BTC_TASK 栈太小, 写 Flash 会栈溢出
            WiFiMgmt::_instance->_blePendingJson = val.c_str();
            WiFiMgmt::_instance->_blePendingFlag = true;
        }
    }
};

// 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->enqueueBLEJson(val.c_str());
        }
    }
};


<tool_use_error>String to replace not found in file.
String: // 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());
            // 仅暂存, 不在这里处理 — BTC_TASK 栈太小, 写 Flash 会栈溢出
            WiFiMgmt::_instance->_blePendingJson = val.c_str();
            WiFiMgmt::_instance->_blePendingFlag = true;
        }
    }
};
(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 配网回调: 小程序写入 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());
            // 仅暂存, 不在这里处理 — BTC_TASK 栈太小, 写 Flash 会栈溢出
            WiFiMgmt::_instance->_blePendingJson = val.c_str();
            WiFiMgmt::_instance->_blePendingFlag = true;
        }
    }
};
(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.)


Let me re-read the file to see the exact current state.


1	/**
2	 * @file wifi_manager.cpp
3	 * @brief WiFi 配网管理器 — AP + WebServer
4	 */
5
6	#include "wifi_manager.h"
7
8	// BLE 配网回调: 小程序写入 JSON → 仅在 BLE 栈中暂存, 不直接处理
9	class BLEProvisionCB : public BLECharacteristicCallbacks {
10	    void onWrite(BLECharacteristic* ch) override {
11	        std::string val = ch->getValue();
12	        if (val.length() > 0 && WiFiMgmt::_instance) {
13	            Serial.printf("[BLE] 收到配网数据 (%d 字节)\n", val.length());
14	            // 仅暂存, 不在这里处理 — BTC_TASK 栈太小, 写 Flash 会栈溢出
15	            WiFiMgmt::_instance->_blePendingJson = val.c_str();
16	            WiFiMgmt::_instance->_blePendingFlag = true;
17	        }
18	    }
19	};
20

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

#include "wifi_manager.h"

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


The file already has my previous edit. Now I need to change lines 15-16 to use the public `enqueueBLEJson` method instead of accessing private members directly. But actually, looking at the header, I already added `enqueueBLEJson` as a public method. Let me make the callback use it.

The current callback code references `_blePendingJson` and `_blePendingFlag` directly. But these are private members. I need to either:
1. Change the callback to call `enqueueBLEJson`
2. Or make the members public

I already added `enqueueBLEJson` public method. Let me update the callback to use it.


            // 仅暂存, 不在这里处理 — BTC_TASK 栈太小, 写 Flash 会栈溢出
            WiFiMgmt::_instance->_blePendingJson = val.c_str();
            WiFiMgmt::_instance->_blePendingFlag = true;

            // 仅暂存 → 在 loop() 里处理 (BTC_TASK 栈太小, 写 Flash 会栈溢出)
            WiFiMgmt::_instance->enqueueBLEJson(val.c_str());


            // 仅暂存, 不在这里处理 — BTC_TASK 栈太小, 写 Flash 会栈溢出
            WiFiMgmt::_instance->_blePendingJson = val.c_str();
            WiFiMgmt::_instance->_blePendingFlag = true;

            // 仅暂存 → 在 loop() 里处理 (BTC_TASK 栈太小, 写 Flash 会栈溢出)
            WiFiMgmt::_instance->enqueueBLEJson(val.c_str());

-            // 仅暂存, 不在这里处理 — BTC_TASK 栈太小, 写 Flash 会栈溢出

-            WiFiMgmt::_instance->_blePendingJson = val.c_str();

-            WiFiMgmt::_instance->_blePendingFlag = true;

+            // 仅暂存 → 在 loop() 里处理 (BTC_TASK 栈太小, 写 Flash 会栈溢出)

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


<ide_diagnostics>[
  {
    "filePath": "/d:/PlatformIO1/Projects/Healthv2.0/src/wifi_manager.cpp",
    "line": 15,
    "column": 34,
    "message": "成员 \"WiFiMgmt::_blePendingJson\" (已声明 所在行数:113，所属文件:\"D:\\PlatformIO1\\Projects\\Healthv2.0\\include\\wifi_manager.h\") 不可访问",
    "code": "265",
    "severity": "Error"
  },
  {
    "filePath": "/d:/PlatformIO1/Projects/Healthv2.0/src/wifi_manager.cpp",
    "line": 16,
    "column": 34,
    "message": "成员 \"WiFiMgmt::_blePendingFlag\" (已声明 所在行数:114，所属文件:\"D:\\PlatformIO1\\Projects\\Healthv2.0\\include\\wifi_manager.h\") 不可访问",
    "code": "265",
    "severity": "Error"
  }
]</ide_diagnostics>


The IDE diagnostics are stale - they're still showing errors from the previous version. Let me verify the current state of the file - I already changed line 15 to use `enqueueBLEJson`. The diagnostics should update after saving. Let me continue and add the `enqueueBLEJson` and `_processBLEPending` methods, then add the loop call.

Let me now find where to add `enqueueBLEJson` and `_processBLEPending`. I'll add them near `handleBLEProvision`.


Now add the `enqueueBLEJson` and `_processBLEPending` methods. Let me find `handleBLEProvision`:


450	    _bleChar = nullptr;
451	    Serial.println("[BLE] 已停止");
452	}
453
454	void WiFiMgmt::handleBLEProvision(const char* json) {
455	    JsonDocument doc;
456	    DeserializationError err = deserializeJson(doc, json);
457	    if (err) {
458	        Serial.printf("[BLE] JSON 解析失败: %s\n", err.c_str());
459	        if (_bleChar) { _bleChar->setValue("ERR_JSON"); _bleChar->notify(); }
460	        return;
461	    }
462
463	    // 提取 WiFi 凭证
464	    const char* ssid = doc["s"] | "";
465	    const char* pass = doc["p"] | "";
466
467	    if (strlen(ssid) == 0) {
468	        Serial.println("[BLE] 未提供 WiFi SSID");
469	        if (_bleChar) { _bleChar->setValue("ERR_NO_SSID"); _bleChar->notify(); }
470	        return;
471	    }
472
473	    _pendingSSID = ssid;
474	    _pendingPass = pass;
475
476	    // 保存到 LittleFS (和 Web 配网完全相同的逻辑)
477	    strncpy(_config.wifi_ssid, ssid, sizeof(_config.wifi_ssid) - 1);
478	    strncpy(_config.wifi_pass, pass, sizeof(_config.wifi_pass) - 1);
479
480	    if (doc.containsKey("device_id"))   strncpy(_config.device_id,   doc["device_id"]   | "", sizeof(_config.device_id) - 1);
481	    if (doc.containsKey("env_note"))    strncpy(_config.env_note,    doc["env_note"]    | "", sizeof(_config.env_note) - 1);
482	    if (doc.containsKey("env_type"))    strncpy(_config.env_type,    doc["env_type"]    | "", sizeof(_config.env_type) - 1);
483	    if (doc.containsKey("mqtt_server")) strncpy(_config.mqtt_server, doc["mqtt_server"] | "", sizeof(_config.mqtt_server) - 1);
484	    if (doc.containsKey("mqtt_port"))   strncpy(_config.mqtt_port,   doc["mqtt_port"]   | "", sizeof(_config.mqtt_port) - 1);
485	    if (doc.containsKey("mqtt_user"))   strncpy(_config.mqtt_user,   doc["mqtt_user"]   | "", sizeof(_config.mqtt_user) - 1);
486	    if (doc.containsKey("mqtt_pass"))   strncpy(_config.mqtt_pass,   doc["mqtt_pass"]   | "", sizeof(_config.mqtt_pass) - 1);
487
488	    _saveConfig();
489	    _portalStopFlag = true;  // 触发延迟关闭 AP+BLE → 连接 WiFi
490
491	    Serial.printf("[BLE] ✅ 配网完成, SSID=%s\n", ssid);
492
493	    // 回复 OK
494	    if (_bleChar) { _bleChar->setValue("OK"); _bleChar->notify(); }
495	    delay(100);
496	}
497
498	// ============================================================
499	// 重置
500	// ============================================================
501
502	void WiFiMgmt::resetSettings() {
503	    Serial.println(("[WiFi] 清除所有配置..."));
504	    if (LittleFS.exists(CONFIG_FILE_WIFI))   LittleFS.remove(CONFIG_FILE_WIFI);

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

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

void WiFiMgmt::resetSettings() {
    Serial.println(("[WiFi] 清除所有配置..."));
    if (LittleFS.exists(CONFIG_FILE_WIFI))   LittleFS.remove(CONFIG_FILE_WIFI);


配网页 HTML 添加 env_type 下拉选择

添加环境类型选择器到配网页

_handleSave 保存 env_type 字段

编译验证


Now I need to:
1. Add `enqueueBLEJson` method implementation
2. Add `_processBLEPending` method (which calls `handleBLEProvision` from loop context)
3. Add the call to `_processBLEPending()` in the `loop()` function

Let me add the implementations right before `handleBLEProvision`.


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

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

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

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


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

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

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

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

+// 在 loop() 上下文中处理 BLE 配网数据 (安全 — 不在 BTC_TASK 栈中)

+    Serial.printf("[BLE] loop 处理配网: %s\n", _blePendingJson.c_str());

+    handleBLEProvision(_blePendingJson.c_str());

DeserializationError err = deserializeJson(doc, json);


Now I need to add the `_processBLEPending()` call in the `loop()` function. Let me find the right place - it should be early in the loop, before portal handling since `_portalActive` must be true for the processing to make sense (BLE is only active during portal mode). Actually, let me add it right at the beginning of `loop()`, before the portal check. That way it works whether the portal is active or not.


Now add the call in `loop()`:


bool WiFiMgmt::loop() {
    // --- Portal 模式 ---

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

    // --- Portal 模式 ---


bool WiFiMgmt::loop() {
    // --- Portal 模式 ---

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

    // --- Portal 模式 ---

+    // --- BLE 待处理数据 (从 loop 上下文安全处理, 不占 BTC_TASK 栈) ---


Good. Now let me compile and check for errors.


编译验证：


PowerShell

& "D:\PlatformIO\penv\Scripts\platformio.exe" run 2>&1

Compile after BLE stack overflow fix


Processing esp32-s3-devkitc-1 (platform: espressif32; board: esp32-s3-devkitc-1; framework: arduino)
--------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
PLATFORM: Espressif 32 (6.13.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES:
 - framework-arduinoespressif32 @ 3.20017.241212+sha.dcc1105b
 - tool-esptoolpy @ 2.41100.0 (4.11.0)
 - toolchain-riscv32-esp @ 8.4.0+2021r2-patch5
 - toolchain-xtensa-esp32s3 @ 8.4.0+2021r2-patch5
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 38 compatible libraries
Scanning dependencies...
Dependency Graph
|-- PubSubClient @ 2.8.0
|-- ArduinoJson @ 7.4.3
|-- DHT sensor library @ 1.4.7
|-- Adafruit Unified Sensor @ 1.1.15
|-- U8g2 @ 2.36.18
|-- WiFi @ 2.0.0
|-- ESP32 BLE Arduino @ 2.0.0
|-- DNSServer @ 2.0.0
|-- LittleFS @ 2.0.0
|-- WebServer @ 2.0.0
Building in release mode
Compiling .pio\build\esp32-s3-devkitc-1\src\main.cpp.o
Compiling .pio\build\esp32-s3-devkitc-1\src\wifi_manager.cpp.o
platformio.exe : src/wifi_manager.cpp: In member function 'void WiFiMgmt::handleBLEProvision(const char*)':
At line:1 char:302
+ ...  catch {} } }; & "D:\PlatformIO\penv\Scripts\platformio.exe" run 2>&1
+                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (src/wifi_manage...(const char*)'::String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError

src/wifi_manager.cpp:498:36: warning: 'bool ArduinoJson::V743PB22::JsonDocument::containsKey(TChar*) const [with TChar
= const char]' is deprecated: use doc["key"].is<T>() instead [-Wdeprecated-declarations]
     if (doc.containsKey("device_id"))   strncpy(_config.device_id,   doc["device_id"]   | "", sizeof(_config.device_id
) - 1);
                                    ^
In file included from .pio/libdeps/esp32-s3-devkitc-1/ArduinoJson/src/ArduinoJson.hpp:42,
                 from .pio/libdeps/esp32-s3-devkitc-1/ArduinoJson/src/ArduinoJson.h:9,
                 from include/wifi_manager.h:26,
                 from src/wifi_manager.cpp:6:
.pio/libdeps/esp32-s3-devkitc-1/ArduinoJson/src/ArduinoJson/Document/JsonDocument.hpp:167:8: note: declared here
   bool containsKey(TChar* key) const {
        ^~~~~~~~~~~
src/wifi_manager.cpp:499:35: warning: 'bool ArduinoJson::V743PB22::JsonDocument::containsKey(TChar*) const [with TChar
= const char]' is deprecated: use doc["key"].is<T>() instead [-Wdeprecated-declarations]
     if (doc.containsKey("env_note"))    strncpy(_config.env_note,    doc["env_note"]    | "", sizeof(_config.env_note)
 - 1);
                                   ^
In file included from .pio/libdeps/esp32-s3-devkitc-1/ArduinoJson/src/ArduinoJson.hpp:42,
                 from .pio/libdeps/esp32-s3-devkitc-1/ArduinoJson/src/ArduinoJson.h:9,
                 from include/wifi_manager.h:26,
                 from src/wifi_manager.cpp:6:
.pio/libdeps/esp32-s3-devkitc-1/ArduinoJson/src/ArduinoJson/Document/JsonDocument.hpp:167:8: note: declared here
   bool containsKey(TChar* key) const {
        ^~~~~~~~~~~
src/wifi_manager.cpp:500:35: warning: 'bool ArduinoJson::V743PB22::JsonDocument::containsKey(TChar*) const [with TChar
= const char]' is deprecated: use doc["key"].is<T>() instead [-Wdeprecated-declarations]
     if (doc.containsKey("env_type"))    strncpy(_config.env_type,    doc["env_type"]    | "", sizeof(_config.env_type)
 - 1);
                                   ^
In file included from .pio/libdeps/esp32-s3-devkitc-1/ArduinoJson/src/ArduinoJson.hpp:42,
                 from .pio/libdeps/esp32-s3-devkitc-1/ArduinoJson/src/ArduinoJson.h:9,
                 from include/wifi_manager.h:26,
                 from src/wifi_manager.cpp:6:
.pio/libdeps/esp32-s3-devkitc-1/ArduinoJson/src/ArduinoJson/Document/JsonDocument.hpp:167:8: note: declared here
   bool containsKey(TChar* key) const {
        ^~~~~~~~~~~
src/wifi_manager.cpp:501:38: warning: 'bool ArduinoJson::V743PB22::JsonDocument::containsKey(TChar*) const [with TChar
= const char]' is deprecated: use doc["key"].is<T>() instead [-Wdeprecated-declarations]
     if (doc.containsKey("mqtt_server")) strncpy(_config.mqtt_server, doc["mqtt_server"] | "", sizeof(_config.mqtt_serv
er) - 1);
                                      ^
In file included from .pio/libdeps/esp32-s3-devkitc-1/ArduinoJson/src/ArduinoJson.hpp:42,
                 from .pio/libdeps/esp32-s3-devkitc-1/ArduinoJson/src/ArduinoJson.h:9,
                 from include/wifi_manager.h:26,
                 from src/wifi_manager.cpp:6:
.pio/libdeps/esp32-s3-devkitc-1/ArduinoJson/src/ArduinoJson/Document/JsonDocument.hpp:167:8: note: declared here
   bool containsKey(TChar* key) const {
        ^~~~~~~~~~~
src/wifi_manager.cpp:502:36: warning: 'bool ArduinoJson::V743PB22::JsonDocument::containsKey(TChar*) const [with TChar
= const char]' is deprecated: use doc["key"].is<T>() instead [-Wdeprecated-declarations]
     if (doc.containsKey("mqtt_port"))   strncpy(_config.mqtt_port,   doc["mqtt_port"]   | "", sizeof(_config.mqtt_port
) - 1);
                                    ^
In file included from .pio/libdeps/esp32-s3-devkitc-1/ArduinoJson/src/ArduinoJson.hpp:42,
                 from .pio/libdeps/esp32-s3-devkitc-1/ArduinoJson/src/ArduinoJson.h:9,
                 from include/wifi_manager.h:26,
                 from src/wifi_manager.cpp:6:
.pio/libdeps/esp32-s3-devkitc-1/ArduinoJson/src/ArduinoJson/Document/JsonDocument.hpp:167:8: note: declared here
   bool containsKey(TChar* key) const {
        ^~~~~~~~~~~
src/wifi_manager.cpp:503:36: warning: 'bool ArduinoJson::V743PB22::JsonDocument::containsKey(TChar*) const [with TChar
= const char]' is deprecated: use doc["key"].is<T>() instead [-Wdeprecated-declarations]
     if (doc.containsKey("mqtt_user"))   strncpy(_config.mqtt_user,   doc["mqtt_user"]   | "", sizeof(_config.mqtt_user
) - 1);
                                    ^
In file included from .pio/libdeps/esp32-s3-devkitc-1/ArduinoJson/src/ArduinoJson.hpp:42,
                 from .pio/libdeps/esp32-s3-devkitc-1/ArduinoJson/src/ArduinoJson.h:9,
                 from include/wifi_manager.h:26,
                 from src/wifi_manager.cpp:6:
.pio/libdeps/esp32-s3-devkitc-1/ArduinoJson/src/ArduinoJson/Document/JsonDocument.hpp:167:8: note: declared here
   bool containsKey(TChar* key) const {
        ^~~~~~~~~~~
src/wifi_manager.cpp:504:36: warning: 'bool ArduinoJson::V743PB22::JsonDocument::containsKey(TChar*) const [with TChar
= const char]' is deprecated: use doc["key"].is<T>() instead [-Wdeprecated-declarations]
     if (doc.containsKey("mqtt_pass"))   strncpy(_config.mqtt_pass,   doc["mqtt_pass"]   | "", sizeof(_config.mqtt_pass
) - 1);
                                    ^
In file included from .pio/libdeps/esp32-s3-devkitc-1/ArduinoJson/src/ArduinoJson.hpp:42,
                 from .pio/libdeps/esp32-s3-devkitc-1/ArduinoJson/src/ArduinoJson.h:9,
                 from include/wifi_manager.h:26,
                 from src/wifi_manager.cpp:6:
.pio/libdeps/esp32-s3-devkitc-1/ArduinoJson/src/ArduinoJson/Document/JsonDocument.hpp:167:8: note: declared here
   bool containsKey(TChar* key) const {
        ^~~~~~~~~~~
Linking .pio\build\esp32-s3-devkitc-1\firmware.elf
Retrieving maximum program size .pio\build\esp32-s3-devkitc-1\firmware.elf
Checking size .pio\build\esp32-s3-devkitc-1\firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM:   [==        ]  21.1% (used 69188 bytes from 327680 bytes)
Flash: [======    ]  58.8% (used 1966105 bytes from 3342336 bytes)
Building .pio\build\esp32-s3-devkitc-1\firmware.bin
esptool.py v4.11.0
Creating esp32s3 image...
Merged 2 ELF sections
Successfully created esp32s3 image.
========================= [SUCCESS] Took 46.12 seconds =========================


Compilation successful. Now let me upload.


编译通过 ✅，烧录：


& "D:\PlatformIO\penv\Scripts\platformio.exe" run --target upload 2>&1

Upload fixed firmware to bathroom board


Error: Exit code 1
Processing esp32-s3-devkitc-1 (platform: espressif32; board: esp32-s3-devkitc-1; framework: arduino)
--------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
PLATFORM: Espressif 32 (6.13.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES:
 - framework-arduinoespressif32 @ 3.20017.241212+sha.dcc1105b
 - tool-esptoolpy @ 2.41100.0 (4.11.0)
 - tool-mkfatfs @ 2.0.1
 - tool-mklittlefs @ 1.203.210628 (2.3)
 - tool-mkspiffs @ 2.230.0 (2.30)
 - toolchain-riscv32-esp @ 8.4.0+2021r2-patch5
 - toolchain-xtensa-esp32s3 @ 8.4.0+2021r2-patch5
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 38 compatible libraries
Scanning dependencies...
Dependency Graph
|-- PubSubClient @ 2.8.0
|-- ArduinoJson @ 7.4.3
|-- DHT sensor library @ 1.4.7
|-- Adafruit Unified Sensor @ 1.1.15
|-- U8g2 @ 2.36.18
|-- WiFi @ 2.0.0
|-- ESP32 BLE Arduino @ 2.0.0
|-- DNSServer @ 2.0.0
|-- LittleFS @ 2.0.0
|-- WebServer @ 2.0.0
Building in release mode
Retrieving maximum program size .pio\build\esp32-s3-devkitc-1\firmware.elf
Checking size .pio\build\esp32-s3-devkitc-1\firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM:   [==        ]  21.1% (used 69188 bytes from 327680 bytes)
Flash: [======    ]  58.8% (used 1966105 bytes from 3342336 bytes)
Configuring upload protocol...
AVAILABLE: cmsis-dap, esp-bridge, esp-builtin, esp-prog, espota, esptool, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa
CURRENT: upload_protocol = esptool
Looking for upload port...
Using manually specified: COM19
Uploading .pio\build\esp32-s3-devkitc-1\firmware.bin
esptool.py v4.11.0
Serial port COM19

A fatal error occurred: Could not open COM19, the port is busy or doesn't exist.
(could not open port 'COM19': PermissionError(13, '拒绝访问。', None, 5))

platformio.exe : *** [upload] Error 2
At line:1 char:302
+ ... tch {} } }; & "D:\PlatformIO\penv\Scripts\platformio.exe" run --targe ...
+                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (*** [upload] Error 2:String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError

========================= [FAILED] Took 27.28 seconds =========================


COM19 is busy - probably the serial monitor is still open or the board is being accessed by another program. The user needs to close the serial monitor or whatever is using COM19. Let me check what's using it, or just tell the user to close the serial monitor.

Actually, the board might have restarted from the crash and the serial port might be in a weird state. Let me wait a moment and retry. Or check available ports.


COM19 被占用了，串口监视器关掉再试：


& "D:\PlatformIO\penv\Scripts\platformio.exe" run --target upload 2>&1

Retry upload to COM19


Error: Exit code 1
Processing esp32-s3-devkitc-1 (platform: espressif32; board: esp32-s3-devkitc-1; framework: arduino)
--------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
PLATFORM: Espressif 32 (6.13.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES:
 - framework-arduinoespressif32 @ 3.20017.241212+sha.dcc1105b
 - tool-esptoolpy @ 2.41100.0 (4.11.0)
 - tool-mkfatfs @ 2.0.1
 - tool-mklittlefs @ 1.203.210628 (2.3)
 - tool-mkspiffs @ 2.230.0 (2.30)
 - toolchain-riscv32-esp @ 8.4.0+2021r2-patch5
 - toolchain-xtensa-esp32s3 @ 8.4.0+2021r2-patch5
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 38 compatible libraries
Scanning dependencies...
Dependency Graph
|-- PubSubClient @ 2.8.0
|-- ArduinoJson @ 7.4.3
|-- DHT sensor library @ 1.4.7
|-- Adafruit Unified Sensor @ 1.1.15
|-- U8g2 @ 2.36.18
|-- WiFi @ 2.0.0
|-- ESP32 BLE Arduino @ 2.0.0
|-- DNSServer @ 2.0.0
|-- LittleFS @ 2.0.0
|-- WebServer @ 2.0.0
Building in release mode
Retrieving maximum program size .pio\build\esp32-s3-devkitc-1\firmware.elf
Checking size .pio\build\esp32-s3-devkitc-1\firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM:   [==        ]  21.1% (used 69188 bytes from 327680 bytes)
Flash: [======    ]  58.8% (used 1966105 bytes from 3342336 bytes)
Configuring upload protocol...
AVAILABLE: cmsis-dap, esp-bridge, esp-builtin, esp-prog, espota, esptool, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa
CURRENT: upload_protocol = esptool
Looking for upload port...
Using manually specified: COM19
Uploading .pio\build\esp32-s3-devkitc-1\firmware.bin
esptool.py v4.11.0
Serial port COM19

A fatal error occurred: Could not open COM19, the port is busy or doesn't exist.
(could not open port 'COM19': PermissionError(13, '拒绝访问。', None, 5))

platformio.exe : *** [upload] Error 2
At line:1 char:302
+ ... tch {} } }; & "D:\PlatformIO\penv\Scripts\platformio.exe" run --targe ...
+                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (*** [upload] Error 2:String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError

========================= [FAILED] Took 27.93 seconds =========================


COM19 is still busy. The board is probably in a crash-reboot loop and the serial port is stuck. The user should:
1. Unplug and replug the USB cable
2. Or press the reset button on the board
3. Close any serial monitor that might be open
4. Then try again

Let me tell the user.


COM19 一直被占用——可能是上次崩溃后板子卡在 boot loop 里占着串口。拔掉 USB 重新插上，或者按一下板子上的 **RST 键**，等 2 秒我再烧：


Write-Host "等待 3 秒..."; Start-Sleep 3; & "D:\PlatformIO\penv\Scripts\platformio.exe" run --target upload 2>&1

Retry upload after port reset


等待 3 秒...
Processing esp32-s3-devkitc-1 (platform: espressif32; board: esp32-s3-devkitc-1; framework: arduino)
--------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
PLATFORM: Espressif 32 (6.13.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES:
 - framework-arduinoespressif32 @ 3.20017.241212+sha.dcc1105b
 - tool-esptoolpy @ 2.41100.0 (4.11.0)
 - tool-mkfatfs @ 2.0.1
 - tool-mklittlefs @ 1.203.210628 (2.3)
 - tool-mkspiffs @ 2.230.0 (2.30)
 - toolchain-riscv32-esp @ 8.4.0+2021r2-patch5
 - toolchain-xtensa-esp32s3 @ 8.4.0+2021r2-patch5
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 38 compatible libraries
Scanning dependencies...
Dependency Graph
|-- PubSubClient @ 2.8.0
|-- ArduinoJson @ 7.4.3
|-- DHT sensor library @ 1.4.7
|-- Adafruit Unified Sensor @ 1.1.15
|-- U8g2 @ 2.36.18
|-- WiFi @ 2.0.0
|-- ESP32 BLE Arduino @ 2.0.0
|-- DNSServer @ 2.0.0
|-- LittleFS @ 2.0.0
|-- WebServer @ 2.0.0
Building in release mode
Retrieving maximum program size .pio\build\esp32-s3-devkitc-1\firmware.elf
Checking size .pio\build\esp32-s3-devkitc-1\firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM:   [==        ]  21.1% (used 69188 bytes from 327680 bytes)
Flash: [======    ]  58.8% (used 1966105 bytes from 3342336 bytes)
Configuring upload protocol...
AVAILABLE: cmsis-dap, esp-bridge, esp-builtin, esp-prog, espota, esptool, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa
CURRENT: upload_protocol = esptool
Looking for upload port...
Using manually specified: COM19
Uploading .pio\build\esp32-s3-devkitc-1\firmware.bin
esptool.py v4.11.0
Serial port COM19
Connecting...
Chip is ESP32-S3 (QFN56) (revision v0.2)
Features: WiFi, BLE, Embedded PSRAM 8MB (AP_3v3)
Crystal is 40MHz
USB mode: USB-Serial/JTAG
MAC: 94:a9:90:f1:1a:d4
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 921600
Changed.
Configuring flash size...
Flash will be erased from 0x00000000 to 0x00003fff...
Flash will be erased from 0x00008000 to 0x00008fff...
Flash will be erased from 0x0000e000 to 0x0000ffff...
Flash will be erased from 0x00010000 to 0x001f0fff...
SHA digest in image updated
Compressed 15104 bytes to 10430...
Writing at 0x00000000... (100 %)
Wrote 15104 bytes (10430 compressed) at 0x00000000 in 0.2 seconds (effective 633.9 kbit/s)...
Hash of data verified.
Compressed 3072 bytes to 146...
Writing at 0x00008000... (100 %)
Wrote 3072 bytes (146 compressed) at 0x00008000 in 0.1 seconds (effective 362.1 kbit/s)...
Hash of data verified.
Compressed 8192 bytes to 47...
Writing at 0x0000e000... (100 %)
Wrote 8192 bytes (47 compressed) at 0x0000e000 in 0.1 seconds (effective 742.7 kbit/s)...
Hash of data verified.
Compressed 1966464 bytes to 1309243...
Writing at 0x00010000... (1 %)
Writing at 0x00017308... (2 %)
Writing at 0x0001c75f... (3 %)
Writing at 0x00021e19... (5 %)
Writing at 0x000275d8... (6 %)
Writing at 0x0002cde6... (7 %)
Writing at 0x00032503... (8 %)
Writing at 0x00037d7d... (10 %)
Writing at 0x0003d07d... (11 %)
Writing at 0x00042889... (12 %)
Writing at 0x00047dd2... (13 %)
Writing at 0x0004d917... (15 %)
Writing at 0x00052b71... (16 %)
Writing at 0x00058098... (17 %)
Writing at 0x0005d5d7... (18 %)
Writing at 0x00062646... (20 %)
Writing at 0x00067ce2... (21 %)
Writing at 0x0006d450... (22 %)
Writing at 0x00072d2f... (23 %)
Writing at 0x000786be... (25 %)
Writing at 0x0007db44... (26 %)
Writing at 0x00083320... (27 %)
Writing at 0x00088a6c... (28 %)
Writing at 0x0008e188... (30 %)
Writing at 0x00094e8f... (31 %)
Writing at 0x000a0915... (32 %)
Writing at 0x000ae46f... (33 %)
Writing at 0x000bb07a... (35 %)
Writing at 0x000c684f... (36 %)
Writing at 0x000d3673... (37 %)
Writing at 0x000d8f04... (38 %)
Writing at 0x000dea68... (40 %)
Writing at 0x000e4650... (41 %)
Writing at 0x000ea87b... (42 %)
Writing at 0x000f053b... (43 %)
Writing at 0x000f5b4e... (45 %)
Writing at 0x000fb432... (46 %)
Writing at 0x00100663... (47 %)
Writing at 0x0010544f... (48 %)
Writing at 0x0010a3e7... (50 %)
Writing at 0x0010f38d... (51 %)
Writing at 0x00114d8f... (52 %)
Writing at 0x0011a093... (53 %)
Writing at 0x0011fd03... (55 %)
Writing at 0x001250bf... (56 %)
Writing at 0x0012a57c... (57 %)
Writing at 0x0012f584... (58 %)
Writing at 0x00134937... (60 %)
Writing at 0x0013a857... (61 %)
Writing at 0x0013faac... (62 %)
Writing at 0x001454ed... (63 %)
Writing at 0x0014afd4... (65 %)
Writing at 0x00150815... (66 %)
Writing at 0x00156442... (67 %)
Writing at 0x0015c694... (68 %)
Writing at 0x00162bc7... (70 %)
Writing at 0x00168494... (71 %)
Writing at 0x0016d5ed... (72 %)
Writing at 0x001729c5... (73 %)
Writing at 0x0017806f... (75 %)
Writing at 0x0017d65e... (76 %)
Writing at 0x001826cc... (77 %)
Writing at 0x00188048... (78 %)
Writing at 0x0018d5eb... (80 %)
Writing at 0x00192490... (81 %)
Writing at 0x001975fa... (82 %)
Writing at 0x0019c55e... (83 %)
Writing at 0x001a1970... (85 %)
Writing at 0x001a6e28... (86 %)
Writing at 0x001ac832... (87 %)
Writing at 0x001b1ec5... (88 %)
Writing at 0x001b7889... (90 %)
Writing at 0x001bcff3... (91 %)
Writing at 0x001c3972... (92 %)
Writing at 0x001cbfcf... (93 %)
Writing at 0x001d40a5... (95 %)
Writing at 0x001d9396... (96 %)
Writing at 0x001dfc37... (97 %)
Writing at 0x001e535b... (98 %)
Writing at 0x001eaa43... (100 %)
Wrote 1966464 bytes (1309243 compressed) at 0x00010000 in 11.7 seconds (effective 1346.5 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...
========================= [SUCCESS] Took 46.76 seconds =========================


配网页 HTML 添加 env_type 下拉选择

添加环境类型选择器到配网页

_handleSave 保存 env_type 字段

编译验证


The upload succeeded. Now the fix is in place:

1. BLE callback now only stores the JSON string and sets a flag (`enqueueBLEJson`)
2. The actual processing happens in `_processBLEPending()` which is called from `loop()` (core 1, main task stack, not BTC_TASK)
3. This prevents the stack overflow that happened when `handleBLEProvision` was called directly from the BLE callback context

Let me summarize for the user. The key changes:
- `BLEProvisionCB::onWrite` → only calls `enqueueBLEJson()` (safe, no heavy ops)
- `_processBLEPending()` → called in `loop()`, handles the actual JSON parsing + `_saveConfig()` + `notify()`
- No more stack overflow in BTC_TASK

The user can now retry BLE provisioning with the same steps as before.


烧录成功 ✅ 修复内容：

```
之前: BLE回调(onWrite) → handleBLEProvision → _saveConfig(Flash写) → notify()
       ↑ 全在 BTC_TASK 栈里, 栈溢出 → crash

现在: BLE回调(onWrite) → enqueueBLEJson (仅暂存)
      loop() → _processBLEPending → handleBLEProvision → _saveConfig → notify()
       ↑ 在 core 1 主循环栈里跑, 安全
```

现在重新用 BLE 配网，不会再 panic 了。步骤不变：

```
连接 "HealthMonitor" → 订阅 4fafc203 Notify → 写 JSON → 收到 "OK"
```

```json
{"s":"WiFi名","p":"密码","device_id":"10100001003","env_type":"bathroom","env_note":"卫生间"}


蓝牙在配网时广播多久


The user wants to know how long the BLE broadcast lasts during provisioning. Let me check the code to see what controls the BLE advertising and portal timeout.


26:    , _portalStartTime(0)
39:    _stopPortal();
92:        _startPortal();  // 只开 AP
112:            _stopPortal();
123:                    _startPortal();
140:        _startPortal();
212:    _startPortal();
215:void WiFiMgmt::_startPortal() {
238:    _portalStartTime = millis();
242:    _startBLE();  // 同时启动 BLE 配网广播
245:void WiFiMgmt::_stopPortal() {
247:    _stopBLE();
305:    _stopPortal();
406:void WiFiMgmt::_startBLE() {
448:void WiFiMgmt::_stopBLE() {


Let me read the _startPortal and _stopPortal functions to understand the timeout logic, and also the loop() to see how the portal is managed.


215	void WiFiMgmt::_startPortal() {
216	    if (_portalActive) return;
217
218	    WiFi.disconnect(true);
219	    WiFi.mode(WIFI_OFF);
220	    delay(200);
221	    WiFi.mode(WIFI_AP);
222	    WiFi.softAPConfig(IPAddress(192,168,4,1), IPAddress(192,168,4,1), IPAddress(255,255,255,0));
223	    WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);
224	    Serial.printf("[WiFi] AP: %s  IP: 192.168.4.1\n", WIFI_AP_SSID);
225
226	    // DNS 劫持: 所有域名 → 192.168.4.1 → 触发手机强制门户弹窗
227	    _dns = new DNSServer();
228	    _dns->start(53, "*", WiFi.softAPIP());
229
230	    _server = new WebServer(80);
231	    _server->on("/",      HTTP_GET,  [this]() { _handleRoot(); });
232	    _server->on("/save",  HTTP_POST, [this]() { _handleSave(); });
233	    _server->on("/reset", HTTP_GET,  [this]() { _handleReset(); });
234	    _server->onNotFound(  [this]() { _handleRoot(); });
235	    _server->begin();
236
237	    _portalActive = true;
238	    _portalStartTime = millis();
239
240	    Serial.println(("[WiFi] 手机连 AP 后浏览器打开 http://192.168.4.1"));
241
242	    _startBLE();  // 同时启动 BLE 配网广播
243	}
244
245	void WiFiMgmt::_stopPortal() {
246	    if (!_portalActive) return;
247	    _stopBLE();
248	    if (_server) { _server->stop(); delete _server; _server = nullptr; }
249	    if (_dns)    { _dns->stop();    delete _dns;    _dns = nullptr; }
250	    WiFi.softAPdisconnect(true);
251	    _portalActive = false;
252	}
253
254	// ============================================================
255	// Web 处理器
256	// ============================================================
257
258	void WiFiMgmt::_handleRoot() {
259	    _server->send(200, "text/html; charset=utf-8", _htmlPage());
260	}
261
262	void WiFiMgmt::_handleSave() {
263	    if (_server->hasArg("s")) {
264	        _pendingSSID = _server->arg("s");
265	        _pendingPass = _server->arg("p");
266	        // 持久化 WiFi 密码到 LittleFS
267	        strncpy(_config.wifi_ssid, _pendingSSID.c_str(), sizeof(_config.wifi_ssid)-1);
268	        strncpy(_config.wifi_pass, _pendingPass.c_str(), sizeof(_config.wifi_pass)-1);
269	        if (_server->hasArg("device_id"))  strncpy(_config.device_id, _server->arg("device_id").c_str(), sizeof(_config.device_id)-1);

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

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

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

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

    _portalActive = true;
    _portalStartTime = millis();

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

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

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

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

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

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


Looking at the code, the portal (AP + BLE) stays active indefinitely. There's no timeout check in `loop()`. Let me verify by looking at the loop function to see if there's any timeout logic.

The `WIFI_CONFIG_TIMEOUT` is set to `0` in config.h (meaning no timeout, always wait for config). Let me check the loop function again to confirm there's no timeout handling.

Looking at the loop (lines 104-155), when `_portalActive` is true:
- It checks `_portalStopFlag` (set on save)
- It runs `_dns->processNextRequest()` and `_server->handleClient()`
- There's no timeout check

So the answer is: **BLE broadcast lasts indefinitely until the user saves the config OR presses the reset button**. Since `WIFI_CONFIG_TIMEOUT = 0`, there's no timeout.

Let me also quickly check the `_startBLE` function for any advertising timeout parameter.


406	void WiFiMgmt::_startBLE() {
407	    if (_bleServer != nullptr) return;  // 已启动
408
409	    BLEDevice::init("HealthMonitor");
410	    _bleServer = BLEDevice::createServer();
411
412	    BLEService* svc = _bleServer->createService("4fafc201-1fb5-459e-8fcc-c5c9c331914b");
413
414	    // 特征1: 设备信息 (READ) — 小程序连接后读取设备编号等
415	    _bleInfoChar = svc->createCharacteristic(
416	        "4fafc202-1fb5-459e-8fcc-c5c9c331914b",
417	        BLECharacteristic::PROPERTY_READ
418	    );
419	    // 设备信息 JSON (最多 512 字节)
420	    JsonDocument info;
421	    info["device_id"]   = _config.device_id;
422	    info["mqtt_server"] = _config.mqtt_server;
423	    info["mqtt_port"]   = _config.mqtt_port;
424	    info["fw_version"]  = FW_VERSION;
425	    String infoJson;
426	    serializeJson(info, infoJson);
427	    _bleInfoChar->setValue(infoJson.c_str());
428
429	    // 特征2: 配网写入 (WRITE + NOTIFY) — 小程序发配网数据, 设备回复
430	    _bleChar = svc->createCharacteristic(
431	        "4fafc203-1fb5-459e-8fcc-c5c9c331914b",
432	        BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY
433	    );
434	    _bleChar->setCallbacks(new BLEProvisionCB());
435	    _bleChar->addDescriptor(new BLE2902());  // 启用 notify
436	    _bleChar->setValue("");  // 初始为空
437
438	    svc->start();
439
440	    _bleAdv = _bleServer->getAdvertising();
441	    _bleAdv->addServiceUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b");
442	    _bleAdv->setScanResponse(true);
443	    _bleAdv->start();
444
445	    Serial.printf("[BLE] ✅ 配网广播: HealthMonitor  device_id=%s\n", _config.device_id);
446	}
447
448	void WiFiMgmt::_stopBLE() {
449	    if (!_bleServer) return;
450	    if (_bleAdv) { _bleAdv->stop(); _bleAdv = nullptr; }
451	    BLEDevice::deinit(false);
452	    _bleServer = nullptr;
453	    _bleChar = nullptr;
454	    Serial.println("[BLE] 已停止");
455	}

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

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

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

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

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

    svc->start();

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

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

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


Confirmed:

1. `_bleAdv->start()` on line 443 starts advertising with NO timeout parameter — it broadcasts indefinitely.
2. The `WIFI_CONFIG_TIMEOUT` in config.h is set to `0`, which means "no timeout, always wait for config."
3. There's no timeout check in `loop()` that would stop the portal/ble after a certain time.
4. BLE stops only when:
   - User writes valid JSON via BLE → `_portalStopFlag = true` → `_stopPortal()` → `_stopBLE()`
   - User presses reset button → system reboots
   - User long-presses the config button → doesn't stop portal, it starts it again

So the answer: **BLE 广播无限持续，直到配网成功或被手动中断。**


**无限广播**，不会超时。

```
_startBLE() → _bleAdv->start()   // 无超时参数
```

代码里 `WIFI_CONFIG_TIMEOUT = 0`（不超时），BLE 广播只在以下情况停止：

- ✅ 收到配网 JSON → 回复 OK → `_stopBLE()`
- ✅ 长按配网键重置
- ❌ 不会自动停止

所以小程序随时连都行，不用担心连接窗口过期。配完一个板子后，下一个板子上电又能立即搜到。


为啥蓝牙在手机中能搜索到，在小程序扫描不到


The user is asking why the BLE device "HealthMonitor" can be found by the phone's system Bluetooth settings but not by the WeChat mini program's BLE scan.

This is a common issue with WeChat mini programs. Let me think about the possible causes:

1. **WeChat mini program BLE restrictions**: WeChat mini programs have some limitations on BLE scanning:
   - `wx.startBluetoothDevicesDiscovery` needs to be called with the correct `services` filter
   - Without the services filter, it might not find all devices
   - Some Android/WeChat versions have issues with BLE scanning

2. **Service UUID filtering**: The mini program might be using a service UUID filter that doesn't match. The device advertises the service UUID `4fafc201-1fb5-459e-8fcc-c5c9c331914b`, but WeChat's BLE scan might need this to be included in the scan request.

3. **Platform differences**: iOS vs Android behavior differs:
   - iOS: `wx.startBluetoothDevicesDiscovery` with `services` parameter filters properly
   - Android: Some versions don't filter well or need specific configurations

4. **Advertising data format**: WeChat mini programs might have stricter requirements for:
   - The advertising data must include the service UUID in a specific way
   - `setScanResponse(true)` might put the UUID in the scan response instead of the main advertising packet
   - If the UUID is only in the scan response, WeChat's "passive scan" might not pick it up

5. **`allowDuplicatesKey`**: If not set to `true`, WeChat might deduplicate the device.

The most likely cause is #4: `setScanResponse(true)` puts the service UUID in the scan response data rather than the main advertising packet. WeChat mini programs on some platforms might not request scan response data, or they might only look at the main advertising packet.

Let me suggest fixes:
1. Change `setScanResponse(false)` or remove it, so the UUID goes in the main advertising data
2. Or explicitly set the advertising data with `setAdvertisementData` and `setScanResponseData`

Actually, looking at the code:
```cpp
_bleAdv->setScanResponse(true);
```

This means: the device advertises its name/address in the main advertising packet, and the service UUID goes in the scan response. Some WeChat mini program implementations might do passive scanning (only receiving advertising packets, not requesting scan responses).

The fix would be to either:
1. Not use scan response - just put everything in the main advertising data
2. Or explicitly set the advertising data to include the service UUID in the main packet

Also, another common WeChat issue: the `services` parameter in `wx.startBluetoothDevicesDiscovery` must match exactly. If the mini program is filtering by a different UUID, it won't find the device.

Let me provide a clear answer with solutions.


大概率是广告数据结构的问题。系统蓝牙扫描看所有设备，但微信小程序 `wx.startBluetoothDevicesDiscovery` 默认用**被动扫描**，不主动请求 Scan Response——而你的 UUID 刚好放在了 Scan Response 里。

```cpp
_bleAdv->setScanResponse(true);  // ← UUID 进了 Scan Response, 小程序看不到
```

改成把 UUID 放到**主广告包**里：


Edit

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

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


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)

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

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

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

+    advData.setCompleteServices(BLEUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b"));

+    _bleAdv->setAdvertisementData(advData);

+    _bleAdv->setScanResponse(false);  // 不需要 scan response, 全在主包

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


Good. Now let me explain the issue and also check if the mini program side might need changes. But first, let me also note that there might be an issue with the mini program code itself - specifically the `services` parameter in `startBluetoothDevicesDiscovery`.

Let me give the user both sides:
1. Firmware fix (done) - UUID now in main advertising packet
2. Mini program checklist


编译验证一下：


PowerShell

& "D:\PlatformIO\penv\Scripts\platformio.exe" run 2>&1 | Select-Object -Last 10

Quick compile check


Checking size .pio\build\esp32-s3-devkitc-1\firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM:   [==        ]  21.1% (used 69188 bytes from 327680 bytes)
Flash: [======    ]  58.9% (used 1968425 bytes from 3342336 bytes)
Building .pio\build\esp32-s3-devkitc-1\firmware.bin
esptool.py v4.11.0
Creating esp32s3 image...
Merged 2 ELF sections
Successfully created esp32s3 image.
========================= [SUCCESS] Took 43.92 seconds =========================


Good, compilation succeeded. Let me summarize the fix and also remind about the mini program side.


编译通过 ✅ 同时检查小程序端是否正确：

## 固件端（已修复）

| 之前 | 现在 |
|------|------|
| UUID 在 Scan Response | UUID 在主广告包 |
| 被动扫描看不到 | 被动/主动扫描都能看到 |

## 小程序端自查

```js
// 1. 不要过滤 service UUID（最容易漏掉）
wx.startBluetoothDevicesDiscovery({
  // services: ['4fafc201-...'],  ← 注释掉！不指定则不过滤
  allowDuplicatesKey: true,       // 同名设备也保留
  interval: 0,                    // 上报间隔
  success: () => {
    // 2. 监听发现
    wx.onBluetoothDeviceFound(res => {
      res.devices.forEach(d => {
        // 3. 模糊匹配，不依赖 UUID
        if (d.name && d.name.includes('Health')) {
          console.log('找到了:', d)
        }
      })
    })
  }
})
```

如果之前小程序里 `startBluetoothDevicesDiscovery` 传了 `services` 参数指定 UUID，可能会因为 UUID 在广告包中的位置不对而过滤掉——现在改成主包后这个问题也一并解决了。

要烧录吗？


能配上了，但是连接上以后，再次按下配网的话，就没有蓝牙信号了


配网页 HTML 添加 env_type 下拉选择

添加环境类型选择器到配网页

_handleSave 保存 env_type 字段

编译验证


The user is saying that after a successful provision (BLE配网成功), if they try to re-enter provisioning mode (by long-pressing the button or calling `startConfigPortal()`), the BLE signal doesn't appear anymore.

Let me trace through the code to understand what happens:

1. First boot (no WiFi saved) → `_startPortal()` → AP + BLE starts → user provisions → `_portalStopFlag = true` → `_stopPortal()` → `_stopBLE()` is called

2. After successful connection, user wants to re-provision:
   - They might be pressing the long-press button → `startConfigPortal()` → `_startPortal()` → `_startBLE()`

The issue: `_startBLE()` has a guard:
```cpp
void WiFiMgmt::_startBLE() {
    if (_bleServer != nullptr) return;  // 已启动
```

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

After `BLEDevice::deinit(false)`, the BLE stack is deinitialized. Then when trying to reinitialize, `_bleServer` should be `nullptr`, so the `_startBLE()` check should pass...

Wait, let me check the sequence again. After the BLE provision is done:
1. `_processBLEPending()` → `handleBLEProvision()` → `_portalStopFlag = true`
2. In `loop()`, `_portalStopFlag` is true → `_stopPortal()` → `_stopBLE()` → `BLEDevice::deinit(false)`
3. After WiFi connects, everything works

Then user long-presses button:
1. In `loop()`: `_checkButtonLongPress()` → `_startPortal()` → `_startBLE()`
2. `_startBLE()` checks `_bleServer != nullptr` → should be `nullptr` since `_stopBLE()` set it to `nullptr`

But wait - there's a problem! After `BLEDevice::deinit(false)`, re-initializing BLE with `BLEDevice::init("HealthMonitor")` might fail because the BLE stack isn't fully shut down. The `false` parameter in `deinit(false)` means "don't release memory". This could cause issues on re-init.

Actually, the more likely issue is that `_bleAdv` was set to `nullptr` after `_stopBLE()`, but `BLEDevice::deinit(false)` might not fully clean up the BLE stack. Re-initializing BLE after deinit is problematic on ESP32.

But there's another issue. Let me look at `_startPortal()` more carefully:

```cpp
void WiFiMgmt::_startPortal() {
    if (_portalActive) return;
    // ...
    _startBLE();
}
```

And `_stopPortal()`:
```cpp
void WiFiMgmt::_stopPortal() {
    if (!_portalActive) return;
    _stopBLE();
    // ...
    WiFi.softAPdisconnect(true);
    _portalActive = false;
}
```

This looks correct. After stop, `_portalActive` is false, so a subsequent `_startPortal()` should work.

Wait, let me re-read the user's message: "连接上以后，再次按下配网的话，就没有蓝牙信号了" — After connecting, pressing the provision button again, no BLE signal.

Let me check `_checkButtonLongPress()`:
```cpp
if (_checkButtonLongPress()) {
    Serial.println(("[WiFi] 长按, 进配网模式"));
    _setLEDMode(2);
    WiFi.disconnect();
    _startPortal();
    _connected = false;
    return false;
}
```

So the flow is:
1. WiFi is connected, then long press
2. `WiFi.disconnect()`
3. `_startPortal()` → checks `!_portalActive` → passes → creates AP + calls `_startBLE()`
4. `_startBLE()` → checks `_bleServer != nullptr`

Hmm, `_bleServer` was set to `nullptr` in the first `_stopBLE()`. So the check should pass.

But the real issue might be: **`BLEDevice::deinit(false)` followed by `BLEDevice::init("HealthMonitor")` doesn't work properly on ESP32 Arduino**. Once you deinit BLE, you can't re-init it again without a full reboot.

This is a known ESP32 issue. The BLE stack doesn't properly support re-initialization after deinit.

So the options are:
1. Don't deinit BLE - just stop advertising and re-start it
2. Reboot the ESP32 before re-entering provisioning
3. Keep BLE running always (not ideal for power)

Let me go with option 1: In `_stopBLE()`, instead of deiniting the whole BLE stack, just stop advertising and keep the server. Then in `_startBLE()`, just restart advertising.

Let me modify the approach:
- `_stopBLE()`: Only stop advertising, don't deinit BLE
- `_startBLE()`: If server exists, just re-start advertising; if not, create everything


`BLEDevice::deinit()` 后无法重新 `init()`——这是 ESP32 BLE 栈的已知限制，只能完整重启。

修复：`_stopBLE()` 不停 BLE 栈，只停广播，下次 `_startBLE()` 时重新打广告即可。


448	    Serial.printf("[BLE] ✅ 配网广播: HealthMonitor  device_id=%s\n", _config.device_id);
449	}
450
451	void WiFiMgmt::_stopBLE() {
452	    if (!_bleServer) return;
453	    if (_bleAdv) { _bleAdv->stop(); _bleAdv = nullptr; }
454	    BLEDevice::deinit(false);
455	    _bleServer = nullptr;
456	    _bleChar = nullptr;
457	    Serial.println("[BLE] 已停止");
458	}
459
460	// BLE 线程安全入队 (仅暂存, 不在 BTC_TASK 回调里处理)
461	void WiFiMgmt::enqueueBLEJson(const char* json) {
462	    _blePendingJson  = json;

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

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

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


Edit

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

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

void WiFiMgmt::_startBLE() {
    // 如果 BLE 栈已在运行, 仅重启广播 (避免 deinit→init 失败)
    if (_bleServer != nullptr) {
        if (_bleAdv) { _bleAdv->start(); }
        Serial.println("[BLE] ✅ 广播已重开");
        return;
    }

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


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

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

void WiFiMgmt::_startBLE() {
    // 如果 BLE 栈已在运行, 仅重启广播 (避免 deinit→init 失败)
    if (_bleServer != nullptr) {
        if (_bleAdv) { _bleAdv->start(); }
        Serial.println("[BLE] ✅ 广播已重开");
        return;
    }

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

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

+    // 如果 BLE 栈已在运行, 仅重启广播 (避免 deinit→init 失败)

+        if (_bleAdv) { _bleAdv->start(); }

+        Serial.println("[BLE] ✅ 广播已重开");


Now update `_stopBLE()` to NOT deinit the BLE stack, just stop the advertising:


Now fix `_stopBLE()` — stop advertising only, keep BLE stack alive:


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

void WiFiMgmt::_stopBLE() {
    if (!_bleServer) return;
    if (_bleAdv) { _bleAdv->stop(); }
    Serial.println("[BLE] 广播已暂停");
}


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

void WiFiMgmt::_stopBLE() {
    if (!_bleServer) return;
    if (_bleAdv) { _bleAdv->stop(); }
    Serial.println("[BLE] 广播已暂停");
}

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

// BLE 线程安全入队 (仅暂存, 不在 BTC_TASK 回调里处理)


Now I also need to update `_bleAdv` to not be nulled out in `_stopBLE`. Actually, the `_startBLE()` now checks `_bleServer != nullptr` and if it exists, just calls `_bleAdv->start()`. Since `_stopBLE` only calls `_bleAdv->stop()` and doesn't null it, `_bleAdv` is still valid. Good.

Let me compile and verify.


编译验证：


PowerShell

& "D:\PlatformIO\penv\Scripts\platformio.exe" run 2>&1 | Select-Object -Last 8

Quick compile check


RAM:   [==        ]  21.1% (used 69188 bytes from 327680 bytes)
Flash: [======    ]  58.9% (used 1967569 bytes from 3342336 bytes)
Building .pio\build\esp32-s3-devkitc-1\firmware.bin
esptool.py v4.11.0
Creating esp32s3 image...
Merged 2 ELF sections
Successfully created esp32s3 image.
========================= [SUCCESS] Took 44.03 seconds =========================


编译成功了。总结一下修复的内容。


编译通过 ✅

```
之前: _stopBLE() → BLEDevice::deinit() → BLE 栈彻底关掉
      _startBLE() → BLEDevice::init()   → ❌ ESP32 不支持重新 init

现在: _stopBLE() → 只停广播, BLE 栈常驻
      _startBLE() → 广播重开             → ✅ 秒恢复
```

要烧录吗？


现在又找不到蓝牙信号了


The user is saying they can't find the BLE signal now. Let me think about what might be going on...

The fix I made should keep the BLE stack alive, but wait - there's still a problem. Let me trace through the exact flow once more after a successful provision:

1. BLE callback enqueues data → `loop()` calls `_processBLEPending()` → `handleBLEProvision()` → `_portalStopFlag = true`
2. Next `loop()` iteration, `_portalStopFlag` is true → calls `_stopPortal()`
3. `_stopPortal()` calls `_stopBLE()` → stops advertising
4. Then `_stopPortal()` does `WiFi.softAPdisconnect(true)`
5. After WiFi connects, the device is online

Now user long-presses to re-enter provisioning:
1. `_checkButtonLongPress()` → `WiFi.disconnect()` → `_startPortal()`
2. `_startPortal()` checks `if (_portalActive) return;` → should be false
3. Creates WiFi AP again
4. Calls `_startBLE()`
5. `_startBLE()` checks `if (_bleServer != nullptr)` → should be true (we didn't null it)
6. Calls `_bleAdv->start()` to restart advertising

Wait, the issue might be that `_startPortal()` has `if (_portalActive) return;` at the top. After provisioning, `_portalActive` is set to false in `_stopPortal()`. But let me double check...

Actually, there could be another path. Let me re-read the flow more carefully.

When the user long-presses and calls `startConfigPortal()`, what does it do? Let me check.

Actually, I don't have a `startConfigPortal()` function - wait, yes, it calls `_startPortal()`. Let me re-read the button press handler in loop:

```cpp
if (_checkButtonLongPress()) {
    Serial.println(("[WiFi] 长按, 进配网模式"));
    _setLEDMode(2);
    WiFi.disconnect();
    _startPortal();
    _connected = false;
    return false;
}
```

This calls `_startPortal()`. `_startPortal` checks `if (_portalActive) return;`. Since `_stopPortal()` set it false, this should work.

Hmm, but wait. Could the problem be that the user hasn't flashed the new firmware yet? I just compiled but didn't upload. Let me re-read the conversation.

I said "要烧录吗？" (want to flash?) and the user replied "现在又找不到蓝牙信号了" (now can't find BLE signal again).

So either:
1. The user is still on the OLD firmware where BLE deinit was killing the stack
2. Or the user flashed and it still doesn't work

If it's #1, I just need to upload.
If it's #2, there might be another issue.

But actually, re-reading the conversation flow:
- First issue: BLE crash on provision → I fixed and uploaded
- Second issue: After re-provisioning, no BLE → I fixed _stopBLE/_startBLE but didn't upload yet
- User says "现在又找不到蓝牙信号了" → they likely haven't flashed the new fix yet

Let me just upload the new firmware.

But wait, actually there might also be an issue even with the fix. Let me think about another possible problem: the `_startPortal()` function does:

```cpp
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
delay(200);
WiFi.mode(WIFI_AP);
```

When WiFi mode changes to WIFI_OFF and then to WIFI_AP, this can affect the BLE stack because WiFi and BLE share the same radio on ESP32. Setting WiFi mode explicitly might interfere with the BLE that's already initialized.

Actually, this shouldn't be a problem because BLE was stopped (only advertising stopped, stack still alive) and then WiFi toggles. The BLE stack should handle this.

Let me just upload and see.

Actually wait, there's another thing I should double check. Let me look at `_stopPortal()` again:

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

And `_startPortal()`:
```cpp
void WiFiMgmt::_startPortal() {
    if (_portalActive) return;

    WiFi.disconnect(true);
    WiFi.mode(WIFI_OFF);
    delay(200);
    WiFi.mode(WIFI_AP);
    ...
    _startBLE();
}
```

OK this should work. Let me just upload.

But actually, I just realized there could be another issue. When `_startPortal()` is called while WiFi is connected (during a long-press re-provision), the sequence is:
1. `WiFi.disconnect()` in loop before `_startPortal()`
2. Then `_startPortal()` does `WiFi.disconnect(true)` again and `WiFi.mode(WIFI_OFF)`

This should be fine. Let me upload.


还没烧新固件吧？刚才只编译了没上传。现在烧：


& "D:\PlatformIO\penv\Scripts\platformio.exe" run --target upload 2>&1

Upload fixed firmware with BLE re-connect support


Processing esp32-s3-devkitc-1 (platform: espressif32; board: esp32-s3-devkitc-1; framework: arduino)
--------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
PLATFORM: Espressif 32 (6.13.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES:
 - framework-arduinoespressif32 @ 3.20017.241212+sha.dcc1105b
 - tool-esptoolpy @ 2.41100.0 (4.11.0)
 - tool-mkfatfs @ 2.0.1
 - tool-mklittlefs @ 1.203.210628 (2.3)
 - tool-mkspiffs @ 2.230.0 (2.30)
 - toolchain-riscv32-esp @ 8.4.0+2021r2-patch5
 - toolchain-xtensa-esp32s3 @ 8.4.0+2021r2-patch5
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 38 compatible libraries
Scanning dependencies...
Dependency Graph
|-- PubSubClient @ 2.8.0
|-- ArduinoJson @ 7.4.3
|-- DHT sensor library @ 1.4.7
|-- Adafruit Unified Sensor @ 1.1.15
|-- U8g2 @ 2.36.18
|-- WiFi @ 2.0.0
|-- ESP32 BLE Arduino @ 2.0.0
|-- DNSServer @ 2.0.0
|-- LittleFS @ 2.0.0
|-- WebServer @ 2.0.0
Building in release mode
Retrieving maximum program size .pio\build\esp32-s3-devkitc-1\firmware.elf
Checking size .pio\build\esp32-s3-devkitc-1\firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM:   [==        ]  21.1% (used 69188 bytes from 327680 bytes)
Flash: [======    ]  58.9% (used 1967569 bytes from 3342336 bytes)
Configuring upload protocol...
AVAILABLE: cmsis-dap, esp-bridge, esp-builtin, esp-prog, espota, esptool, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa
CURRENT: upload_protocol = esptool
Looking for upload port...
Using manually specified: COM19
Uploading .pio\build\esp32-s3-devkitc-1\firmware.bin
esptool.py v4.11.0
Serial port COM19
Connecting...
Chip is ESP32-S3 (QFN56) (revision v0.2)
Features: WiFi, BLE, Embedded PSRAM 8MB (AP_3v3)
Crystal is 40MHz
USB mode: USB-Serial/JTAG
MAC: 94:a9:90:f1:1a:d4
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 921600
Changed.
Configuring flash size...
Flash will be erased from 0x00000000 to 0x00003fff...
Flash will be erased from 0x00008000 to 0x00008fff...
Flash will be erased from 0x0000e000 to 0x0000ffff...
Flash will be erased from 0x00010000 to 0x001f0fff...
SHA digest in image updated
Compressed 15104 bytes to 10430...
Writing at 0x00000000... (100 %)
Wrote 15104 bytes (10430 compressed) at 0x00000000 in 0.2 seconds (effective 623.5 kbit/s)...
Hash of data verified.
Compressed 3072 bytes to 146...
Writing at 0x00008000... (100 %)
Wrote 3072 bytes (146 compressed) at 0x00008000 in 0.1 seconds (effective 360.1 kbit/s)...
Hash of data verified.
Compressed 8192 bytes to 47...
Writing at 0x0000e000... (100 %)
Wrote 8192 bytes (47 compressed) at 0x0000e000 in 0.1 seconds (effective 753.6 kbit/s)...
Hash of data verified.
Compressed 1967936 bytes to 1310327...
Writing at 0x00010000... (1 %)
Writing at 0x0001732b... (2 %)
Writing at 0x0001c784... (3 %)
Writing at 0x00021e3b... (5 %)
Writing at 0x000275fd... (6 %)
Writing at 0x0002ce08... (7 %)
Writing at 0x00032523... (8 %)
Writing at 0x00037d97... (10 %)
Writing at 0x0003d0a0... (11 %)
Writing at 0x000428af... (12 %)
Writing at 0x00047df8... (13 %)
Writing at 0x0004d93b... (15 %)
Writing at 0x00052b93... (16 %)
Writing at 0x000580bd... (17 %)
Writing at 0x0005d5fe... (18 %)
Writing at 0x0006266c... (20 %)
Writing at 0x00067d03... (21 %)
Writing at 0x0006d472... (22 %)
Writing at 0x00072d4b... (23 %)
Writing at 0x000786e0... (25 %)
Writing at 0x0007db68... (26 %)
Writing at 0x00083346... (27 %)
Writing at 0x00088a91... (28 %)
Writing at 0x0008e1ad... (30 %)
Writing at 0x00094eb1... (31 %)
Writing at 0x000a0890... (32 %)
Writing at 0x000ae474... (33 %)
Writing at 0x000bafeb... (35 %)
Writing at 0x000c65cd... (36 %)
Writing at 0x000d3562... (37 %)
Writing at 0x000d8dd4... (38 %)
Writing at 0x000de984... (40 %)
Writing at 0x000e455a... (41 %)
Writing at 0x000ea7ef... (42 %)
Writing at 0x000f0499... (43 %)
Writing at 0x000f5ab8... (45 %)
Writing at 0x000fb374... (46 %)
Writing at 0x001006a1... (47 %)
Writing at 0x00105488... (48 %)
Writing at 0x0010a422... (50 %)
Writing at 0x0010f3ac... (51 %)
Writing at 0x00114daa... (52 %)
Writing at 0x00119f54... (53 %)
Writing at 0x0011fb23... (55 %)
Writing at 0x00125051... (56 %)
Writing at 0x0012a4d2... (57 %)
Writing at 0x0012f503... (58 %)
Writing at 0x001348d0... (60 %)
Writing at 0x0013a5d0... (61 %)
Writing at 0x0013f9f5... (62 %)
Writing at 0x001453b1... (63 %)
Writing at 0x0014af37... (65 %)
Writing at 0x001506cc... (66 %)
Writing at 0x001562ea... (67 %)
Writing at 0x0015c4b8... (68 %)
Writing at 0x00162a21... (70 %)
Writing at 0x001683a8... (71 %)
Writing at 0x0016d58d... (72 %)
Writing at 0x001728ff... (73 %)
Writing at 0x00177fe8... (75 %)
Writing at 0x0017d5a2... (76 %)
Writing at 0x0018264a... (77 %)
Writing at 0x00187fc7... (78 %)
Writing at 0x0018d599... (80 %)
Writing at 0x0019242d... (81 %)
Writing at 0x0019756a... (82 %)
Writing at 0x0019c468... (83 %)
Writing at 0x001a18fe... (85 %)
Writing at 0x001a6d89... (86 %)
Writing at 0x001ac7e9... (87 %)
Writing at 0x001b1e41... (88 %)
Writing at 0x001b77b2... (90 %)
Writing at 0x001bcfc6... (91 %)
Writing at 0x001c3855... (92 %)
Writing at 0x001cbf75... (93 %)
Writing at 0x001d3ec5... (95 %)
Writing at 0x001d922a... (96 %)
Writing at 0x001dfb9b... (97 %)
Writing at 0x001e517b... (98 %)
Writing at 0x001eaa01... (100 %)
Wrote 1967936 bytes (1310327 compressed) at 0x00010000 in 11.7 seconds (effective 1347.1 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...
========================= [SUCCESS] Took 49.27 seconds =========================


配网页 HTML 添加 env_type 下拉选择

添加环境类型选择器到配网页

_handleSave 保存 env_type 字段

编译验证


Conversation compacted