Now I need to update `_startBLE()` to always do full initialization when `_bleServer` is null (which it now will be after `_stopBLE()` is called). But I need to handle the `BLEDevice::init()` call which should only happen once.

Let me also update the `_startBLE()` method to handle the case properly.

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

    BLEDevice::init("HealthMonitor");
    _bleServer = BLEDevice::createServer();
    // ... rest of init
```

Now with the fix, `_bleServer` will always be null when entering `_startBLE()` (after a previous stop). But `BLEDevice::init("HealthMonitor")` can only be called once per boot.

I should modify `_startBLE()` to:
1. Remove the "reuse old server" path (since we now fully clean up)
2. Check if `BLEDevice::init()` has been called before

Let me update `_startBLE()`:

```cpp
void WiFiMgmt::_startBLE() {
    // BLEDevice::init() 只能调用一次，之后直接重建 Server
    static bool bleInitialized = false;
    if (!bleInitialized) {
        BLEDevice::init("HealthMonitor");
        bleInitialized = true;
    }

    _bleServer = BLEDevice::createServer();
    // ... rest of the init code
```

Wait, but there's a subtle issue. `BLEDevice::init("HealthMonitor")` takes a device name. If we don't re-init, the device name from the first init persists. But the init was already called with the same name, so it's fine.

Actually, there's another consideration. The `_startBLE()` currently has a guard at the top: `if (_bleServer != nullptr)` which tries to restart advertising. With the fix to `_stopBLE()`, `_bleServer` will always be null when `_startBLE()` is called. So I should remove that guard and always do full init.

But what about the very first call to `_startBLE()`? Currently, `_bleServer` is initialized to `nullptr` in the constructor (line 31), so the first call enters the full init path correctly.

Let me rewrite `_startBLE()` to:
1. Remove the "already initialized" guard
2. Always create a new server
3. Only call `BLEDevice::init()` once

Let me also consider: what if `BLEDevice::init()` fails on the very first call? The code should handle that. But for now, let's focus on the main fix.

Actually wait, looking at this more carefully... With my fix to `_stopBLE()`, each time we enter `_startBLE()` after a stop, `_bleServer` is null. But `BLEDevice::init()` was called already during the first time. If we call it again, it may crash or silently fail.

Let me restructure `_startBLE()`.


现在更新 `_startBLE()`，使其在 Server 被清理后能完整重建：


Edit

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

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

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

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

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

    svc->start();

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

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

void WiFiMgmt::_startBLE() {
    // BLEDevice::init() 全生命周期只能调用一次 (ESP32 限制)
    static bool bleInitialized = false;
    if (!bleInitialized) {
        BLEDevice::init("HealthMonitor");
        bleInitialized = true;
    }

    // 每次进入配网模式都重建 Server (WiFi AP↔STA 切换后旧对象不可复用)
    _bleServer = BLEDevice::createServer();

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

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

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

    svc->start();

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

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


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)

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

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

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

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

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

    svc->start();

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

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

void WiFiMgmt::_startBLE() {
    // BLEDevice::init() 全生命周期只能调用一次 (ESP32 限制)
    static bool bleInitialized = false;
    if (!bleInitialized) {
        BLEDevice::init("HealthMonitor");
        bleInitialized = true;
    }

    // 每次进入配网模式都重建 Server (WiFi AP↔STA 切换后旧对象不可复用)
    _bleServer = BLEDevice::createServer();

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

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

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

    svc->start();

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

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

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

-            // 重设广告数据 (stop 后 start 需要重新配置)

-            BLEAdvertisementData advData;

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

-            advData.setShortName("HealthMon");

-            _bleAdv->setAdvertisementData(advData);

-            _bleAdv->setScanResponse(false);

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

+    // BLEDevice::init() 全生命周期只能调用一次 (ESP32 限制)

+    static bool bleInitialized = false;

+        BLEDevice::init("HealthMonitor");

+    // 每次进入配网模式都重建 Server (WiFi AP↔STA 切换后旧对象不可复用)


Good. Now let me verify the changes compile correctly.


验证编译是否通过：


PowerShell

Build PlatformIO to verify BLE fix compiles

& "D:\PlatformIO\penv\Scripts\platformio.exe" run -d "d:\PlatformIO1\Projects\Healthv2.0" 2>&1 | Select-Object -Last 20


                                    ^
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.9% (used 1967689 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.21 seconds =========================


Build succeeded. There are some warnings about `containsKey` being deprecated in ArduinoJson (needs to be updated to `containsKey` → `containsElement` or similar), but those are just warnings, not related to our fix.

Now let me also check if there's another potential issue. When the user says "断开重新配网就不行了找不到蓝牙信号", it could also mean that after WiFi disconnects, the device doesn't automatically re-enter BLE advertising mode.

Let me check the `loop()` method again. When WiFi disconnects in normal operation:
```cpp
if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {
    _lastCheckTime = millis();
    wl_status_t s = WiFi.status();
    if (s != WL_CONNECTED) {
        if (_connected) {
            _connected = false;
            _setLEDMode(1);
        }
        // 不手动 reconnect()
    } else if (!_connected) {
        _connected = true;
        _setLEDMode(3);
    }
}
```

When WiFi disconnects, it doesn't restart the portal or BLE. This means if WiFi goes down, the user can't re-provision until they long-press the button.

But the user's complaint is specifically about BLE not being findable when re-entering provisioning mode (via long-press or otherwise). The fix I made should address the core issue.

Let me also think about whether there's a race condition. In `handleBLEProvision()`:
```cpp
void WiFiMgmt::handleBLEProvision(const char* json) {
    // ... parse json, save config ...
    _portalStopFlag = true;

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

Then in `loop()`:
```cpp
if (_portalStopFlag) {
    _stopPortal();  // This now properly cleans up BLE
    _portalStopFlag = false;
    if (_pendingSSID.length() > 0) {
        _connected = _tryConnect(...);
        if (_connected) {
            _setLEDMode(3);
            _provisionSuccess = true;
        } else {
            _setLEDMode(2);
            _startPortal();  // Re-enters portal → _startBLE() recreates BLE
        }
    }
}
```

This flow is correct with my fix. When connection fails and portal restarts, BLE will be fully recreated.

The fix looks good. Let me provide a summary to the user.


编译通过 ✅。让我总结一下修复的内容：

---

## 蓝牙配网第二次找不到信号 — 根因与修复

**根因**：`_stopBLE()` 只停止了广播 (`_bleAdv->stop()`)，但没有释放 BLE Server 对象。当第二次进入配网模式时，`_startBLE()` 检测到 `_bleServer != nullptr`，就跳过了完整初始化，只尝试重启旧的广播对象。但经过 WiFi 模式切换（AP → STA → AP）后，旧的 BLE 广播对象已经失效，`start()` 无法恢复广播，所以手机扫不到蓝牙信号。

**修复**（[wifi_manager.cpp](src/wifi_manager.cpp)）：

1. **`_stopBLE()`**（第463行）：彻底清理所有 BLE 资源 — 停止广播、置空 Characteristic 指针、**删除 BLEServer 对象**。确保每次离开配网模式后 BLE 完全释放。

2. **`_startBLE()`**（第405行）：移除"复用旧 Server"分支，改为每次进入配网模式都完整重建 BLE Server（Service + Characteristics + Advertising）。`BLEDevice::init()` 因 ESP32 限制仍只调用一次（用 `static bool` 保护）。

**改动要点**：
- 删除了 `if (_bleServer != nullptr) { ... restart adv ... return; }` 的"轻量重开"路径
- 新增 `static bool bleInitialized` 守卫 `BLEDevice::init()` 的单次调用
- `_stopBLE()` 中 `delete _bleServer` 确保下次 `_startBLE()` 走完整重建流程

你可以烧录测试一下：第一次 BLE 配网成功后断开 WiFi，长按按钮重新进入配网模式，手机应该能再次扫描到 `HealthMon` 蓝牙设备。


还是一样，咋回事


The user says it's still the same issue - after disconnecting and trying to re-provision, the BLE signal can't be found. Let me think deeper about what could be going wrong.

Let me re-read the code more carefully and think about the full lifecycle.

When the device first boots:
1. `begin()` is called
2. If no saved WiFi config or connection fails → `_startPortal()` is called
3. `_startPortal()` calls `_startBLE()` → BLE works, phone can find it
4. User provisions via BLE
5. `handleBLEProvision()` sets `_portalStopFlag = true`
6. In `loop()`, `_portalStopFlag` triggers `_stopPortal()` → `_stopBLE()` (now fully cleans up)
7. Then tries WiFi connection with `_tryConnect()`

Now, there's an important question: when the user says "断开重新配网", how do they re-enter provisioning mode?

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

So long-press enters portal mode. `_startPortal()` calls `_startBLE()`. With my fix, `_bleServer` should be null, so it should go through full init.

But wait - there might be an issue with `BLEDevice::init()` having already been called. After `_stopBLE()` cleans up (including `delete _bleServer`), and then `_startPortal()` → `_startBLE()`, the `static bool bleInitialized` is true, so it skips `BLEDevice::init()` and goes straight to `BLEDevice::createServer()`.

But the question is: does `BLEDevice::createServer()` work after the previous server was deleted, without re-initializing the BLE device?

Let me think about this more carefully...

Actually, on ESP32 with NimBLE or Bluedroid stack, after calling `BLEDevice::init()`, the BLE stack is initialized. `BLEDevice::createServer()` registers a new server with the stack. When you `delete` the server, it should unregister. Creating a new server should work.

But there might be issues:
1. The underlying GAP/GATT layers might not properly clean up
2. After WiFi mode switches, the BLE radio might need to be re-initialized
3. There could be a timing issue - maybe the BLE stack needs a delay after cleanup before re-creation

Actually, let me reconsider. Maybe the issue is NOT in the BLE server recreation, but in something more fundamental.

Let me look at `_startPortal()` again:

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

    WiFi.disconnect(true);
    delay(200);
    WiFi.mode(WIFI_AP);
    WiFi.softAPConfig(IPAddress(192,168,4,1), IPAddress(192,168,4,1), IPAddress(255,255,255,0));
    WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);
    // ...
    _startBLE();
}
```

When the device was previously in STA mode (connected to WiFi), switching to WIFI_AP mode should work. But what about the BLE radio? After WiFi STA was active, the radio was used for WiFi. Switching to AP mode and then trying to use BLE... On ESP32, BLE and WiFi share the radio. They can coexist, but switching modes might cause issues.

Actually, ESP32 supports BLE + WiFi coexistence. But the issue is more subtle. When WiFi is in STA mode (connected), the BLE stack is idle (server was deleted, advertising stopped). Then when we switch to AP mode and create a new BLE server, the BLE stack needs to re-acquire the radio.

Hmm, but this should work. Let me think about other possibilities.

Wait - I just realized something. On the ESP32-S3, the code uses `BLEDevice::init("HealthMonitor")`. On ESP32-S3, the BLE library might use NimBLE under the hood (depending on the framework version). With newer ESP32 Arduino frameworks, NimBLE is often used instead of Bluedroid.

Actually, looking at the build output:
```
framework-arduinoespressif32 @ 3.20017.241212+sha.dcc1105b
```

This is a newer version. Let me check if this uses Bluedroid or NimBLE by default.

Actually, the code includes `<BLEDevice.h>`, `<BLEServer.h>`, `<BLEUtils.h>`, `<BLE2902.h>` — these are from the ESP32 BLE Arduino library, which wraps either Bluedroid or NimBLE depending on configuration. By default, ESP32 Arduino uses Bluedroid.

OK, let me think about what else could go wrong...

Another possibility: `BLEDevice::createServer()` might fail if called after deleting a previous server but without re-initializing the BLE stack. The internal state of the BLE library might not properly handle this.

Let me also consider: maybe there's a simpler issue I'm missing. Let me trace through the exact sequence:

1. First boot → `begin()` → no config → `_startPortal()` → `_startBLE()` → BLE works ✅
2. BLE provision → `_portalStopFlag = true` → `_stopPortal()` → `_stopBLE()` (now deletes server) → `_tryConnect()` → WiFi connected ✅
3. Device works normally with WiFi STA mode
4. User long-presses button → `_startPortal()` → `_startBLE()` → `static bleInitialized = true` → skips `BLEDevice::init()` → `BLEDevice::createServer()` → ... → BLE doesn't work ❌

The key question: does `BLEDevice::createServer()` work correctly after:
- The previous server was deleted
- WiFi was in STA mode (connected)
- WiFi switched to AP mode
- Without calling `BLEDevice::init()` again

This might be the issue. `BLEDevice::init()` does more than just set the device name - it initializes the underlying BLE stack (Bluedroid or NimBLE). After switching WiFi modes and deleting the server, the BLE stack might need to be re-initialized.

But `BLEDevice::init()` can only be called once. Or can it? Let me look at alternatives...

Actually, on ESP32 Arduino, you CAN call `BLEDevice::deinit(true)` to fully shut down the BLE stack, then call `BLEDevice::init()` again. But this is risky because it might conflict with WiFi.

Alternatively, maybe we should try calling `BLEDevice::init()` again regardless. Some versions of the library handle this gracefully (just return if already initialized).

Actually wait - let me look at this from a completely different angle. Maybe the problem is even simpler.

Let me look at the ESP32 Arduino BLE library source code for `BLEDevice::init()`. In newer versions, it DOES check if already initialized:

```cpp
void BLEDevice::init(const char* name) {
    if (m_initialized) {
        ESP_LOGW("BLEDevice", "init already called");
        return;
    }
    // ... init code
}
```

So calling it again would be a no-op. But after `delete _bleServer`, is `m_initialized` still true? YES, because the BLE stack is still initialized (only the server was deleted).

So the flow is:
1. `BLEDevice::init()` called first time → BLE stack initialized
2. Server created, used, deleted
3. New server created via `BLEDevice::createServer()` → should work

But does it actually work? Let me check `BLEDevice::createServer()`:

```cpp
BLEServer* BLEDevice::createServer() {
    if (!m_initialized) {
        ESP_LOGE("BLEDevice", "init not called");
        return nullptr;
    }
    BLEServer* pServer = new BLEServer();
    m_pServerList.push_back(pServer);
    return pServer;
}
```

This looks like it should work. The BLE stack is still initialized, and a new server is created.

Hmm, but there might be internal state issues with the GAP layer. When the previous server's advertising was stopped and the server was deleted, the GAP layer might not have properly cleaned up. The new advertising might fail because the GAP layer thinks advertising is still configured.

Actually, let me look at `BLEServer::~BLEServer()`:
```cpp
BLEServer::~BLEServer() {
    removeServices();
    // ... clean up
}
```

And `BLEAdvertising::stop()`:
```cpp
void BLEAdvertising::stop() {
    esp_err_t errRc = ::esp_ble_gap_stop_advertising(&host);
    // ...
}
```

The GAP layer should properly stop advertising. But there might be timing issues — the stop might be asynchronous, and starting advertising again immediately might fail.

Let me think about another angle: Maybe with my fix, `_bleAdv` is set to `nullptr` before the server is deleted. Then when `_bleServer` is deleted, it might not properly clean up its advertising because... no, the server doesn't manage the advertising object. The advertising is managed separately.

Wait, I need to look at this more carefully. In `BLEDevice::createServer()`, a new `BLEServer` is created. Then `_bleServer->getAdvertising()` is called:

```cpp
BLEAdvertising* BLEServer::getAdvertising() {
    if (m_pAdvertising == nullptr) {
        m_pAdvertising = new BLEAdvertising();
    }
    return m_pAdvertising;
}
```

So `getAdvertising()` creates a new `BLEAdvertising` object if one doesn't exist. When the server is deleted, this advertising object is also deleted (via `BLEServer` destructor). So each new server gets a fresh advertising object.

OK so theoretically, the fix should work. But the user says it still doesn't work. Let me think about what else could be wrong...

Maybe the issue is not in the BLE code at all, but in how the device re-enters provisioning mode. Let me check if there's a condition where the portal/BLE isn't actually started on WiFi disconnect.

Looking at `begin()`:
```cpp
if (!_connected) {
    _setLEDMode(2);
    _startPortal();
} else {
    _setLEDMode(3);
}
```

And `loop()`:
```cpp
// Only long-press triggers re-provisioning
if (_checkButtonLongPress()) {
    _setLEDMode(2);
    WiFi.disconnect();
    _startPortal();
    _connected = false;
    return false;
}

// WiFi disconnect handling - does NOT restart portal
if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {
    // ... just updates _connected and LED
    // No automatic portal restart
}
```

So the only way to re-enter provisioning is through long-pressing the button. The user would need to long-press the button on the ESP32 to trigger re-provisioning.

But actually, the user might also be talking about the scenario where:
1. First BLE provision → WiFi connects successfully
2. User wants to change WiFi → needs to re-enter provisioning
3. Long-presses button → portal starts → but BLE not found

Or maybe:
1. First BLE provision → gets provisioned
2. WiFi disconnects (router off, out of range, etc.)
3. Device doesn't auto-enter portal (only LED changes)
4. User long-presses → portal starts → BLE not found

Either way, my fix should have addressed the BLE recreation. But maybe there's something else going on.

Actually, I just realized I should look at this more carefully. When `_stopPortal()` is called and it deletes the BLE server, and then `_startPortal()` is called again... But wait - when is `_startPortal()` called a second time?

Looking at the flow:
1. First boot: `begin()` → no config → `_startPortal()` → `_startBLE()` ✅
2. BLE provision: `handleBLEProvision()` → sets `_portalStopFlag = true` + saves config
3. `loop()`: `_portalStopFlag` → `_stopPortal()` → `_stopBLE()` (deletes server) → `_tryConnect()`
4. If connection succeeds: normal operation
5. Long press: `_startPortal()` → `_startBLE()` → tries to create new server

At step 5, with my fix, `_startBLE()` should work because:
- `bleInitialized = true` (skip `BLEDevice::init()`)
- `BLEDevice::createServer()` → new server
- `svc->start()` → new service
- `_bleAdv->start()` → new advertising

But wait - maybe the problem is that after `_tryConnect()` succeeds, WiFi is in STA mode (connected to router). Then on long press, `_startPortal()` does:
```cpp
WiFi.disconnect(true);
delay(200);
WiFi.mode(WIFI_AP);
```

This switches from STA to AP mode. On ESP32, switching WiFi modes while BLE is not active should be fine. Then BLE is restarted...

Hmm, but there's a subtlety. `WiFi.mode(WIFI_AP)` sets the WiFi mode to AP-only. But for BLE + WiFi coexistence on ESP32, you might need `WiFi.mode(WIFI_AP_STA)` (both AP and STA) or the coexistence manager might not properly allocate radio time to BLE.

Actually, that's unlikely. BLE should work even when WiFi is in AP-only mode.

Let me think about another possibility: maybe the NimBLE stack (if being used) has different behavior. With ESP32 Arduino 3.x, NimBLE is the default BLE stack for ESP32-S3 and ESP32-C3. But the code uses `BLEDevice.h`, `BLEServer.h`, etc., which map to NimBLE in newer versions.

With NimBLE:
- `NimBLEDevice::init()` initializes the NimBLE stack
- `NimBLEDevice::createServer()` creates a new server
- Deleting the server and creating a new one should work

But wait, NimBLE might have an issue: after `NimBLEDevice::init()` is called, the host stack is initialized. Deleting all servers doesn't deinitialize the host. Creating a new server after deleting the old one should work.

OK, I'm going in circles. Let me think about what other information might help. Let me check if there are any serial logs that would indicate what's happening.

Actually, let me re-read the user's message: "还是一样，咋回事" — "Still the same, what's going on?"

This means my fix didn't resolve the issue. The BLE signal is still not found on the second attempt.

Let me think about what ELSE could cause this...

Potential issues I haven't considered:

1. **WiFi.begin() persistency**: In `_tryConnect()`, `WiFi.persistent(true)` is called, and `WiFi.begin(ssid, pass)` stores credentials in NVS. During `_startPortal()`, `WiFi.disconnect(true)` is called (with `wifioff=true` which turns off WiFi radio). Then `WiFi.mode(WIFI_AP)` turns on AP. Maybe the STA credentials in NVS cause WiFi to try connecting in the background, interfering with BLE?

2. **ESP32 radio coexistence**: When in WIFI_AP mode, the radio is primarily used for AP. Starting BLE advertising might require the radio to time-share between AP beacons and BLE advertising. If the coexistence isn't configured properly, BLE advertising packets might not be sent at the expected intervals.

3. **BLE advertising channel issue**: On ESP32, BLE advertising uses channels 37, 38, 39. WiFi uses channels 1-14. When WiFi AP is on a channel that conflicts, BLE advertising might be interfered with.

4. **Flash/NVS operations during BLE init**: The `_startBLE()` function creates a `JsonDocument` and serializes it. This could potentially corrupt memory if the stack is tight. But this would cause a crash, not just invisible BLE.

5. **Callbacks from old server**: When we delete the old server, if there are pending callbacks or events, they might cause issues.

6. **The `new BLEProvisionCB()` leak**: In `_startBLE()`, `new BLEProvisionCB()` is called each time. The old callback object is never deleted. Could this cause issues? The characteristic takes ownership... but when the server is deleted, are the characteristics and their callbacks properly cleaned up?

Actually, I think item 6 could be a memory leak issue, but it shouldn't prevent BLE from advertising.

Let me think about this differently. What if the problem is NOT in the code I changed, but in a different part of the flow?

The user said: "蓝牙第一次配网行，断开重新配网就不行了找不到蓝牙信号"

"First time BLE provisioning works. After disconnecting, re-provisioning doesn't work - can't find BLE signal."

What if "断开" doesn't mean "WiFi disconnects" but rather "BLE disconnects"? In BLE provisioning, the phone connects to the ESP32 via BLE. After provisioning is done, the BLE connection is terminated. Then when trying to provision again, the phone can't find the BLE signal.

If the first BLE provision was via a BLE connection (GATT write), then after `_portalStopFlag = true` → `_stopPortal()` → `_stopBLE()`, the BLE connection is terminated. But the ESP32 might need to properly disconnect the BLE connection before stopping advertising.

Actually, looking at the flow again:
1. Phone connects to ESP32 BLE
2. Phone writes WiFi credentials via GATT
3. `BLEProvisionCB::onWrite()` is called → enqueues the JSON
4. `_processBLEPending()` in loop() → calls `handleBLEProvision()`
5. `handleBLEProvision()` sets `_portalStopFlag = true` and replies "OK" via notify
6. Next `loop()` iteration: `_portalStopFlag` → `_stopPortal()` → `_stopBLE()`

Actually wait, `_processBLEPending()` is called at the top of `loop()`:

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

    if (_portalActive) {
        if (_portalStopFlag) {
            _stopPortal();
            // ...
        }
        // ...
    }
}
```

So in the same `loop()` call:
1. `_processBLEPending()` → `handleBLEProvision()` → sets `_portalStopFlag = true`
2. Right below: detects `_portalStopFlag` → calls `_stopPortal()` → `_stopBLE()`

This means the BLE server is deleted while the BLE connection might still be active (the phone still connected). The `_bleChar->notify()` was called in `handleBLEProvision()`, but the server gets deleted immediately after. This could leave the BLE stack in a bad state.

But this would be a problem even on the first time... unless the first time is different because it goes to WiFi connection and never re-enters BLE.

Hmm, actually for the FIRST time, after BLE provision, the device connects to WiFi and works. The BLE stack might be in a bad state, but nobody notices because BLE is never used again. On the second attempt, when we try to recreate the BLE server, the bad state from the first time prevents proper re-initialization.

So the fix should include:
1. A delay after BLE provision before stopping the server (to let the notify complete)
2. Proper BLE server cleanup
3. Maybe even: call `BLEDevice::deinit()` and re-`init()` to fully reset the BLE stack

Let me try a different approach. Instead of just deleting the server, let me try calling `BLEDevice::deinit(true)` before re-initializing. But this requires that `BLEDevice::init()` can be called again.

Actually, let me look at this from an even more practical perspective. The most common cause of "BLE advertising not showing up on second attempt" on ESP32 is:

1. The BLE stack is in a bad state after the first use
2. The radio isn't properly released
3. Advertising data isn't properly refreshed

The most reliable fix on ESP32 is to:
1. After BLE is done: fully deinit BLE
2. Before BLE is needed again: fully init BLE
3. Between WiFi STA mode and BLE use: ensure radio coexistence

But `BLEDevice::deinit()` might not be commonly used...

Actually, let me look at this from yet another angle. What if the issue is that after `WiFi.mode(WIFI_AP)` in `_startPortal()`, the BLE stack can't properly use the radio because WiFi AP is hogging it?

ESP32-S3 only has one 2.4GHz radio. WiFi and BLE share it via time-division multiplexing. When WiFi AP is active, it needs to send beacons periodically (~100ms intervals). BLE advertising also needs to send packets periodically. If the coexistence manager isn't configured, one might block the other.

But this should work on the first time too... unless the first time is different because WiFi AP wasn't previously in STA mode.

I'm going to try a few things:
1. Add a delay before deleting the BLE server after provision
2. Try using `esp_bt_controller_mem_release()` or similar to properly reset the BLE stack
3. Maybe switch to using NimBLE directly instead of the Arduino wrapper

Actually, you know what, let me just try the simplest thing first: what if `BLEDevice::init()` CAN be called multiple times on this version? Let me just remove the `static bool bleInitialized` guard and always call `BLEDevice::init()`. If the library handles it gracefully (just returns if already initialized), no harm done. If it crashes, we know we need a different approach.

Actually no, `BLEDevice::init()` might crash on second call because it tries to re-initialize the underlying controller.

OK let me try yet another approach. What about completely de-initializing and re-initializing BLE?

In `_stopBLE()`:
```cpp
void WiFiMgmt::_stopBLE() {
    if (_bleAdv) { _bleAdv->stop(); _bleAdv = nullptr; }
    if (_bleChar) { _bleChar = nullptr; }
    if (_bleInfoChar) { _bleInfoChar = nullptr; }
    if (_bleServer) { delete _bleServer; _bleServer = nullptr; }
    // Fully shutdown BLE to allow clean restart later
    BLEDevice::deinit(true);  // true = release memory
    Serial.println("[BLE] 已完全停止并释放");
}
```

Then in `_startBLE()`:
```cpp
void WiFiMgmt::_startBLE() {
    BLEDevice::init("HealthMonitor");  // Fresh init after deinit
    _bleServer = BLEDevice::createServer();
    // ... rest
}
```

This would fully reset the BLE stack each time. But `BLEDevice::deinit()` might cause issues:
- It could conflict with WiFi operations
- It might not fully clean up (known issue on some ESP32 Arduino versions)
- It might crash if called at the wrong time

Let me check if `BLEDevice::deinit()` exists and is safe to use...

Actually, I just realized something important. On ESP32 Arduino 3.x (which this project uses based on the build log: `framework-arduinoespressif32 @ 3.20017...`), the BLE library might be NimBLE-based by default. In NimBLE, `NimBLEDevice::deinit()` is available and should work.

But wait, looking at the includes:
```cpp
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
```

These are the Arduino BLE wrapper headers. Under ESP32 Arduino 3.x, these might wrap NimBLE or might still use Bluedroid. The build output doesn't specify.

Regardless, `BLEDevice::deinit()` should exist in both. Let me try it.

Actually, let me step back and think about what the user's actual scenario is. They said:
"蓝牙第一次配网行，断开重新配网就不行了找不到蓝牙信号"

"First time BLE provisioning works. After disconnecting, re-provisioning doesn't work - can't find BLE signal."

What if "断开" means "disconnected from WiFi" (WiFi断开)? In that case, the device is operating normally with WiFi connected, then WiFi disconnects. Looking at the code:

```cpp
if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {
    // ...
    if (s != WL_CONNECTED) {
        if (_connected) {
            _connected = false;
            _setLEDMode(1);  // Slow flash
        }
        // NO portal restart!
    }
}
```

When WiFi disconnects, the device does NOT automatically restart the portal. The user needs to long-press the button.

But wait, what if the user's issue is that they CAN'T re-enter provisioning mode at all? That would explain "找不到蓝牙信号" (can't find BLE signal) — because BLE advertising was never started!

Hmm, but the user also says "第一次配网行" (first time provisioning works), so they know how to enter provisioning mode. If they're doing the same thing the second time (long press button), it should work the same way...

Unless the long press detection has an issue. Let me look at `_checkButtonLongPress()`:

```cpp
bool WiFiMgmt::_checkButtonLongPress() {
    bool pressed = (digitalRead(CONFIG_BUTTON_PIN) == LOW);
    if (pressed && !_buttonPressed) {
        _buttonPressed = true; _buttonPressTime = millis();
    } else if (!pressed && _buttonPressed) {
        _buttonPressed = false;
        if (millis() - _buttonPressTime >= BUTTON_LONG_PRESS_MS) return true;
    }
    return false;
}
```

This detects a long press on the RELEASE of the button (not while holding). So the user needs to press and HOLD, then release. This should work consistently.

OK, I'm going to try the `BLEDevice::deinit()` approach, but also consider that maybe there's a timing issue with the WiFi mode switch + BLE init.

Actually wait, let me look at one more thing. In `_startPortal()`:

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

    WiFi.disconnect(true);
    delay(200);
    WiFi.mode(WIFI_AP);
    WiFi.softAPConfig(...);
    WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);
    // ...
    _startBLE();
}
```

`WiFi.disconnect(true)` — the `true` parameter means `wifioff = true`, which turns off the WiFi radio. Then `WiFi.mode(WIFI_AP)` re-enables the radio in AP mode. Then BLE is started.

But there's only a `delay(200)` between `disconnect(true)` and `mode(WIFI_AP)`. Is 200ms enough for the WiFi radio to fully shut down before being re-enabled? Maybe not on ESP32-S3. If the WiFi radio shutdown is still in progress when BLE tries to use the radio, it might fail.

But this would also be an issue on first boot... unless on first boot, WiFi has never been initialized, so `disconnect(true)` is effectively a no-op.

Actually, on first boot, `begin()` is called:
```cpp
bool WiFiMgmt::begin() {
    // ...
    if (hasConfig) {
        _connected = _connectWiFi();
    } else {
        _connected = false;
    }

    if (!_connected) {
        _startPortal();  // First time portal
    }
}
```

On first boot with no config:
1. `_connectWiFi()` is not called (hasConfig = false)
2. `_connected = false`
3. `_startPortal()` → `WiFi.disconnect(true)` → `WiFi.mode(WIFI_AP)` → `_startBLE()`

`WiFi.disconnect(true)` on a WiFi that hasn't connected yet... it should still work. And `WiFi.mode(WIFI_AP)` after a fresh boot should work.

On second boot (after config is saved):
1. `hasConfig = true`
2. `_connectWiFi()` → connects to WiFi (STA mode) → `_connected = true`
3. User long-presses → `_startPortal()` → `WiFi.disconnect(true)` → `WiFi.mode(WIFI_AP)` → `_startBLE()`

At this point, WiFi was in STA mode, `disconnect(true)` turns off WiFi, then `mode(WIFI_AP)` turns on AP. Then BLE tries to init...

Hmm, I wonder if the issue is that after `WiFi.mode(WIFI_AP)` enables the radio for AP, the BLE stack (which was previously initialized) has its internal state pointing to the old radio configuration. The BLE stack might think the radio is in a different state.

I think the safest approach is to use `BLEDevice::deinit()` to fully reset the BLE stack. Let me try that.

Actually, I just had another thought. What if the issue is simpler: the `_bleAdv->start()` call returns an error but it's silently ignored? Let me check the return value.

Looking at NimBLE's `start()`:
```cpp
bool NimBLEAdvertising::start(uint32_t duration, void (*advCompleteCB)(NimBLEAdvertising*)) {
    // ...
    int rc = ble_gap_adv_start(...);
    if (rc != 0) {
        return false;
    }
    return true;
}
```

And `BLEAdvertising::start()` (Bluedroid wrapper):
```cpp
void BLEAdvertising::start() {
    // ...
    esp_err_t errRc = ::esp_ble_gap_start_advertising(&adv_params);
    if (errRc != ESP_OK) {
        ESP_LOGE("BLEAdvertising", "start advertising failed: %d", errRc);
    }
}
```

Both return values/errors that are being ignored. In the Bluedroid case, the function returns void, but logs an error. In the NimBLE case, it returns false on failure.

If the BLE advertising start is failing, we might not be seeing it. The serial log would help, but we can't see it.

OK, let me just try the most comprehensive fix: fully deinit and reinit BLE.

Let me also consider: maybe I should try a totally different approach to BLE restart. Instead of keeping BLE alive, what if I restart the entire BLE stack?

```cpp
void WiFiMgmt::_stopBLE() {
    if (_bleAdv) { _bleAdv->stop(); _bleAdv = nullptr; }
    _bleChar = nullptr;
    _bleInfoChar = nullptr;
    if (_bleServer) { delete _bleServer; _bleServer = nullptr; }
    BLEDevice::deinit(true);  // Full cleanup
    Serial.println("[BLE] 完全释放");
}

void WiFiMgmt::_startBLE() {
    BLEDevice::init("HealthMonitor");
    _bleServer = BLEDevice::createServer();
    // ... (same full init)
}
```

Wait, does `BLEDevice::deinit()` exist in this version? Let me check by looking at the library source... I can't directly check. But it should be available in ESP32 Arduino 3.x.

Let me try this approach. If `deinit()` doesn't exist, compilation will fail and we'll know.

Hmm, but there's a risk: calling `deinit()` while WiFi is running might crash the device. WiFi and BLE share the same underlying controller (`esp_bt_controller`). Deiniting BLE might deinit the entire Bluetooth controller, which could affect WiFi coexistence if WiFi is active.

At the point where `_stopBLE()` is called, WiFi is about to switch from AP to STA mode (or already being shut down). So WiFi might not be actively using the radio.

In `_startBLE()`, WiFi is in AP mode (just started). BLE init might conflict with AP operation.

I think there's a safer approach: instead of `deinit()`, just add proper error checking and retry logic, and make sure the BLE stack is properly reset before re-initializing.

Actually, you know what, let me try something completely different. What if instead of trying to clean up/recreate the BLE server, I just leave it running the entire time?

Currently:
1. Portal starts → BLE starts
2. Provision → BLE stops (to free radio for WiFi STA)
3. Portal ends → BLE deleted
4. User re-enters portal → BLE restarts (THIS FAILS)

What if:
1. Portal starts → BLE starts
2. Provision → BLE just stops advertising but server stays
3. Portal ends → BLE server stays (just no advertising)
4. User re-enters portal → BLE advertising restarts (simpler, no full recreation)

This was the ORIGINAL approach! But it didn't work either (that's why the user reported the issue in the first place).

So the original approach (keep server, restart advertising) AND my new approach (delete server, recreate) both don't work. This suggests the issue is deeper — possibly at the BLE stack level, not the server/advertising level.

Let me think about what happens at the BLE stack level...

On ESP32, the BLE stack (Bluedroid or NimBLE) runs as a separate task. When WiFi mode changes, the ESP32 coexistence module manages radio time between WiFi and BLE. If BLE advertising is stopped and then started again, the coexistence module should still be functioning correctly.

But what if the coexistence module gets confused? If WiFi STA mode uses different coexistence parameters than WiFi AP mode, and BLE advertising tries to start during AP mode, maybe the coexistence module is in the wrong state?

Ugh, this is getting very low-level. Let me try a practical approach:

1. Try `BLEDevice::deinit()` + re-`init()` approach
2. Add more delay between WiFi mode change and BLE start
3. Add debug logging to see what's failing

Let me implement approach 1+2 and see if that helps.

Actually, I just realized something. Let me re-read the original code one more time...

In the original code (before my fix):
```cpp
void WiFiMgmt::_startBLE() {
    if (_bleServer != nullptr) {
        if (_bleAdv) {
            BLEAdvertisementData advData;
            advData.setCompleteServices(BLEUUID("..."));
            advData.setShortName("HealthMon");
            _bleAdv->setAdvertisementData(advData);
            _bleAdv->setScanResponse(false);
            _bleAdv->start();
        }
        Serial.println("[BLE] ✅ 广播已重开");
        return;
    }
    // ... full init
}
```

When this path was taken (server already exists):
1. Advertising data is re-set
2. `_bleAdv->start()` is called

But `_bleAdv` was obtained from `_bleServer->getAdvertising()`. This returns the server's advertising object. After `_bleAdv->stop()`, calling `start()` again should work...

Unless the ESP32 BLE stack has a bug where `stop()` + `start()` on the same advertising object doesn't work properly. This is actually a known issue on some ESP32 BLE stack versions!

A workaround: instead of calling `stop()` + `start()`, call `start()` with new data (without stopping first). Or: call `start()` after a delay. Or: recreate the advertising object.

In my new approach, I delete the server and create a new one, which creates a fresh advertising object. This should bypass any `stop()`+`start()` issue. But maybe it doesn't because the underlying BLE stack is still in a bad state.

Let me try adding `BLEDevice::deinit()` to fully reset the BLE stack:

Actually, I just want to verify one thing. Let me check if maybe the issue is even simpler. What if the BLE advertising flags need to be set differently?

In `_startBLE()`:
```cpp
BLEAdvertisementData advData;
advData.setCompleteServices(BLEUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b"));
advData.setShortName("HealthMon");
_bleAdv->setAdvertisementData(advData);
_bleAdv->setScanResponse(false);
_bleAdv->start();
```

`setScanResponse(false)` means no scan response data. But some phones filter out devices without scan response. However, this works the first time, so it should work the second time too.

OK, let me just implement the `deinit()` approach and add some delays. This is the most thorough fix I can think of.


让我更深入分析问题。仅仅重建 Server 还不够 — BLE 底层栈在 WiFi 模式切换后可能处于异常状态。让我用更彻底的方式修复。


405	void WiFiMgmt::_startBLE() {
406	    // BLEDevice::init() 全生命周期只能调用一次 (ESP32 限制)
407	    static bool bleInitialized = false;
408	    if (!bleInitialized) {
409	        BLEDevice::init("HealthMonitor");
410	        bleInitialized = true;
411	    }
412
413	    // 每次进入配网模式都重建 Server (WiFi AP↔STA 切换后旧对象不可复用)
414	    _bleServer = BLEDevice::createServer();
415
416	    BLEService* svc = _bleServer->createService("4fafc201-1fb5-459e-8fcc-c5c9c331914b");
417
418	    // 特征1: 设备信息 (READ) — 小程序连接后读取设备编号等
419	    _bleInfoChar = svc->createCharacteristic(
420	        "4fafc202-1fb5-459e-8fcc-c5c9c331914b",
421	        BLECharacteristic::PROPERTY_READ
422	    );
423	    // 设备信息 JSON (最多 512 字节)
424	    JsonDocument info;

void WiFiMgmt::_startBLE() {
    // BLEDevice::init() 全生命周期只能调用一次 (ESP32 限制)
    static bool bleInitialized = false;
    if (!bleInitialized) {
        BLEDevice::init("HealthMonitor");
        bleInitialized = true;
    }

    // 每次进入配网模式都重建 Server (WiFi AP↔STA 切换后旧对象不可复用)
    _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;


第一次能够配上，再次断开重开，配网就，扫描不到蓝牙信号了


The user is re-stating the problem: first time can provision successfully, but after disconnecting and reopening the provisioning portal, BLE signal can't be scanned.

Let me think about this differently. Instead of the BLE server recreation approach, let me consider:

1. The ESP32 has a known issue where BLE and WiFi coexistence can cause problems
2. When switching from STA mode back to AP mode, the BLE stack might not properly re-advertise

Let me try a more fundamental approach: use `BLEDevice::deinit()` to fully shut down BLE, then re-init from scratch.

But wait - on ESP32 Arduino 3.x with NimBLE, does `BLEDevice::deinit()` even work? And does it conflict with WiFi?

Actually, let me look at this from a completely different angle. What if the problem is that after the first BLE provisioning:

1. The phone connects to the ESP32 via BLE
2. The phone writes the WiFi credentials
3. ESP32 processes it, replies "OK", and then immediately shuts down BLE
4. But the phone is still connected via BLE!

When the ESP32 abruptly shuts down BLE without properly disconnecting the phone, some phones (especially Android) will "remember" the device as being connected/paired. When the ESP32 starts advertising again, the phone might filter it out or have cached connection parameters that prevent re-discovery.

This is a common BLE issue! The phone's BLE stack might have cached the device's address and connection state. Even though the ESP32 is advertising again, the phone might not show it because:
- The phone thinks it's still "connected" to that device
- The phone cached the GATT database and doesn't need to re-discover
- Android's BLE scan filters out devices that were recently connected

But the user says this is a "小程序" (WeChat Mini Program), which uses WeChat's BLE API. WeChat's BLE API might behave differently.

Wait, actually, on the phone side, after BLE provisioning is done, the Mini Program should close the BLE connection. And the phone's OS should clear the connection state after a timeout. But if the ESP32 never properly acknowledges the disconnection, the phone might keep the connection state.

On the ESP32 side, when `_stopBLE()` is called and `_bleServer` is deleted, the underlying BLE stack should terminate the connection. But if the connection termination packet is never sent (because the server is abruptly deleted), the phone might not know the connection is gone.

For BLE connections, if the peripheral abruptly stops, the central (phone) will eventually detect the supervision timeout and consider the connection lost. This typically takes a few seconds. After that, the phone should be able to see the device again.

But there's another issue: on Android, once you connect to a BLE device, the OS caches the GATT services. If the device starts advertising again with the same address and service UUIDs, Android might use the cached GATT database instead of re-discovering. This can cause issues if the GATT database changed (which it might have, since we recreated the server).

Hmm, but the service UUIDs are the same, so Android should be OK with the cached data...

Actually, let me think about this more practically. The most robust fix for "BLE not discoverable after first connection" on ESP32 is:

1. **Change the BLE device address between sessions**: Each time BLE starts, use a different random address. This prevents the phone from using stale cached data.
2. **Or: properly terminate the BLE connection before shutting down**: Send a disconnect request to the phone, wait for the disconnection to complete, then shut down.

Let me try approach 2 first, as it's simpler. Also, let me add a proper delay between BLE operations.

Actually wait, I think a key issue might be even simpler. Let me re-read the `_startPortal` flow:

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

    WiFi.disconnect(true);
    delay(200);
    WiFi.mode(WIFI_AP);
    WiFi.softAPConfig(...);
    WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);
    // ...
    _startBLE();
}
```

When this is called on first boot (no prior WiFi connection), `WiFi.disconnect(true)` is a no-op and `WiFi.mode(WIFI_AP)` works fine.

When this is called after WiFi was connected in STA mode:
1. `WiFi.disconnect(true)` — disconnects from router AND turns off WiFi radio
2. `delay(200)` — 200ms wait
3. `WiFi.mode(WIFI_AP)` — turns on WiFi in AP mode
4. `WiFi.softAP(...)` — starts AP
5. `_startBLE()` — tries to start BLE

The issue: `WiFi.mode(WIFI_AP)` sets WiFi to AP-only mode. But for BLE coexistence to work properly on ESP32, you typically need to use `esp_bt_controller_enable()` or ensure the BT controller is properly initialized alongside WiFi.

Actually, on ESP32, `BLEDevice::init()` initializes the BT controller. If `BLEDevice::init()` was called before WiFi was set to AP mode, the BT controller is already initialized. Changing WiFi mode shouldn't affect it.

But wait — what if `WiFi.mode(WIFI_AP)` internally changes the coexistence configuration? When WiFi is in AP mode, the ESP32 is the AP (beacon transmitter). When WiFi is in STA mode, the ESP32 is the client. The coexistence parameters might differ.

I think the real issue might be that the ESP32's coexistence manager needs to be properly configured. By default, ESP32 uses a coexistence scheme where WiFi has priority over BLE. In AP mode, the ESP32 needs to transmit beacons regularly, which might starve BLE of radio time.

But again, this would also be an issue on first boot...

OK, I'm going to take a step back and try the most comprehensive fix I can think of:

1. In `_stopBLE()`: properly disconnect any BLE connection, wait, then fully deinit BLE
2. In `_startBLE()`: fully init BLE from scratch every time
3. Add delays between WiFi mode switch and BLE operations
4. Try randomizing the BLE address to prevent cache issues

Actually, let me try something simpler first. Let me see if the `deinit` + `init` approach compiles and works:

Actually, you know what? Let me just try removing the `static bool` guard and always calling `BLEDevice::init()`. If the library handles double-init gracefully, this might be the simplest fix. And if it doesn't handle it, the compilation won't break — it would only fail at runtime (crash). So I wouldn't know from compilation alone.

Let me instead try `BLEDevice::deinit()` followed by `BLEDevice::init()`. If `deinit` isn't available in this version, the compilation will fail and I'll know.

Let me also check whether `deinit` is available...


让我从更根本的角度思考这个问题。问题可能出在 BLE 底层栈在 WiFi STA→AP 模式切换后没有彻底重置。让我试试 `deinit` + 重新 `init` 的方案。


No files found


`BLEDevice.h` wasn't found in the deps directory. Let me find where the ESP32 BLE library headers are.


The BLE headers aren't in the .pio directory. They might be in the ESP32 framework directory. Let me check the framework path.

Actually, let me take a different approach. Instead of trying to find BLE headers, let me think about what could cause BLE advertising to fail on the second attempt on ESP32.

I've been overthinking this. Let me consider some ESP32-specific issues:

1. **ESP32 BLE advertising duration**: When you start BLE advertising on ESP32, it can have a duration. If the advertising times out, it stops. But the code doesn't set a duration, so it should advertise indefinitely.

2. **ESP32 memory**: After the first BLE provisioning, if there's a memory leak (e.g., the `new BLEProvisionCB()` is never deleted, service objects, etc.), the second attempt might fail due to memory exhaustion.

3. **WiFi + BLE coexistence on ESP32-S3**: The ESP32-S3 has a single 2.4GHz radio. When WiFi is in STA mode (connected to a router), the radio is primarily used for WiFi. If you switch to AP mode AND try to use BLE, the radio needs to time-share between AP beacons and BLE advertising. If this isn't configured properly, BLE advertising might silently fail.

Let me think about option 3 more. On ESP32, when you call `WiFi.mode(WIFI_AP)`, it enables the WiFi driver in AP mode. The WiFi driver uses the PHY (physical layer). BLE also uses the PHY. The ESP32's coexistence module (ESP Coex) manages sharing.

But there's a catch: `BLEDevice::init()` initializes the Bluetooth controller, which also configures coexistence. If `BLEDevice::init()` is called BEFORE WiFi starts, the coexistence parameters are set correctly. But if WiFi mode changes AFTER BLE init, the coexistence parameters might need to be updated.

Actually, ESP-IDF's coexistence module should handle WiFi mode changes dynamically. But there might be bugs...

Let me try a completely different approach. What if I:
1. In `_stopBLE()`, fully delete everything (as I already do)
2. In `_startBLE()`, always do full init (as I already do)
3. BUT also restart the ESP32's Bluetooth controller before re-creating the server

On ESP32 Arduino, you can restart the BT controller with:
```cpp
esp_bt_controller_disable();
esp_bt_controller_enable(ESP_BT_MODE_BTDM);  // or ESP_BT_MODE_BLE
```

Or more drastically:
```cpp
esp_bt_controller_deinit();
esp_bt_controller_init();
esp_bt_controller_enable(ESP_BT_MODE_BLE);
```

But this would require including `esp_bt.h` which might conflict with the Arduino BLE wrapper.

Actually, let me try a simpler approach first. What if I add a significant delay after WiFi AP starts and before BLE starts? Maybe the WiFi AP needs time to stabilize before the BLE stack can properly use the radio.

Or... what if I start BLE BEFORE WiFi AP? Maybe the order matters.

Let me try:
```cpp
void WiFiMgmt::_startPortal() {
    if (_portalActive) return;

    WiFi.disconnect(true);
    delay(500);  // Wait for WiFi to fully shut down

    // Start BLE FIRST (before WiFi AP)
    _startBLE();

    // Then start WiFi AP
    WiFi.mode(WIFI_AP);
    // ...
}
```

Hmm, but this might cause issues with BLE + WiFi coexistence going the other way.

Actually, wait. Let me re-read this code comment from `_startPortal()`:
```cpp
WiFi.mode(WIFI_AP);
```

This sets WiFi to AP-only mode. But for BLE + WiFi coexistence, the ESP32 documentation recommends using `WIFI_MODE_APSTA` (both AP and STA) even if you only use AP, because the coexistence manager works better in this mode.

But this is getting too speculative. Let me try something concrete.

Actually, let me look at the problem from the perspective of what has changed. The user says the first time works, but not subsequent times. The key difference between first and subsequent times:

1. First time: BLE has never been initialized, WiFi has never connected in STA mode
2. Subsequent times: BLE was previously initialized and stopped, WiFi was previously connected in STA mode

What if the issue is that BLE stack state persists in a way that `delete _bleServer` doesn't fully clear?

ESP32 BLE Arduino library maintains global state in `BLEDevice` class. After `BLEDevice::init()`, the `m_initialized` flag is true. This flag is checked by `createServer()`. But there might be other global state that prevents proper operation.

Let me try using the low-level ESP-IDF functions to fully reset the BT controller. I can call these alongside the Arduino BLE wrapper.

Actually, maybe I should try another approach entirely. What if I swap the order of operations:
1. Delete BLE server (as before)
2. Call `esp_bt_controller_disable()` then `esp_bt_controller_enable(ESP_BT_MODE_BLE)` to reset the BT controller
3. Create new BLE server

Or just call `btStop()` which is an Arduino function that stops the BT controller.

Wait, in ESP32 Arduino core, `btStop()` is available:
```cpp
bool btStop();
```

This stops the BT controller. Then `btStart()` would restart it. But this might interfere with `BLEDevice`.

OK, I think I need to try a practical solution. Let me try this approach:

1. In `_stopBLE()`: after deleting the server, also call `btStop()` to stop the BT controller
2. In `_startBLE()`: call `btStart()` before BLEDevice::init(), then do full init

This would fully reset the BT hardware between uses. Let me implement this.

Wait, but `btStop()` and `btStart()` work with Bluedroid. In ESP32 Arduino 3.x, the default BLE stack might be NimBLE, which uses different functions.

Let me check if there's an equivalent for NimBLE... `NimBLEDevice::deinit()` should work.

Actually, I just had another idea. What if the problem is the WiFi/BLE antenna switching on the ESP32-S3? Some ESP32-S3 boards have an external antenna switch that needs to be configured. If the switch is in the wrong position, BLE might not transmit properly.

But this would affect the first time too...

OK, I think the most promising approach is to try `btStop()`/`btStart()` or equivalent. Let me check what's available.

Actually, let me just try the `NimBLEDevice::deinit()` approach. The ESP32 Arduino 3.x uses NimBLE by default (for ESP32-S3). The `deinit()` function should properly clean up the BLE stack, allowing a fresh `init()`.

Let me implement this and see if it compiles.

If `NimBLEDevice::deinit()` is not available, I'll fall back to trying `esp_bt_controller_disable/enable`.

Let me write the fix:

```cpp
void WiFiMgmt::_stopBLE() {
    // Graceful BLE disconnect before shutdown
    if (_bleServer && _bleServer->getConnectedCount() > 0) {
        // Give the BLE stack time to properly disconnect
        delay(300);
    }

    if (_bleAdv)      { _bleAdv->stop(); _bleAdv = nullptr; }
    if (_bleChar)     { _bleChar = nullptr; }
    if (_bleInfoChar) { _bleInfoChar = nullptr; }
    if (_bleServer)   { delete _bleServer; _bleServer = nullptr; }

    // Fully deinit BLE stack to ensure clean restart
    BLEDevice::deinit(true);

    Serial.println("[BLE] 完全释放");
}

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

    _bleServer = BLEDevice::createServer();
    // ... rest
}
```

Wait, if `BLEDevice::deinit()` doesn't exist, this won't compile. Let me check...

Actually, in ESP32 Arduino BLE library, `BLEDevice` doesn't have a `deinit()` method. The `deinit()` method exists in `NimBLEDevice` (the NimBLE wrapper), but the code is using the generic `BLEDevice` API.

In ESP32 Arduino 3.x, `BLEDevice` might internally use NimBLE and have a `deinit()` method... or it might not.

Let me try adding just `btStop()` and `btStart()`, which are Arduino-level functions:

```cpp
#include <esp_bt.h>

void WiFiMgmt::_stopBLE() {
    // ...
    if (_bleServer) { delete _bleServer; _bleServer = nullptr; }
    btStop();
    Serial.println("[BLE] 完全释放");
}

void WiFiMgmt::_startBLE() {
    btStart();
    BLEDevice::init("HealthMonitor");
    // ...
}
```

But `btStop()` stops both Classic BT and BLE. And it might complain if BT wasn't started...

Actually, `btStop()` is defined in `esp32-hal-bt.c` and it disables the BT controller. `btStart()` enables it.

Hmm, but calling `btStop()` might crash if the BT controller was never started (first call). Or it might be fine.

Actually, on first call to `_stopBLE()`, the BT is already started (because BLE was used). So `btStop()` should work. Then on second call to `_startBLE()`, `btStart()` restarts it, and `BLEDevice::init()` reinitializes.

But there's a subtle issue: `BLEDevice::init()` internally calls `btStart()` (or equivalent). So calling `btStart()` before `BLEDevice::init()` might not be necessary. But it shouldn't hurt.

And `_stopBLE()` is called from `_stopPortal()`, which is called when WiFi is about to switch modes. At that point, the BT controller is still running (even if BLE advertising was stopped). `btStop()` should shut it down cleanly.

Let me try this approach. The key insight is: we need to reset the BT controller, not just the BLE server objects.

Actually, I just thought of something else. The `WiFi.mode(WIFI_AP)` call after BLE has been initialized might reconfigure the coexistence manager. If we stop BT before changing WiFi modes, the coexistence manager won't be confused. Let me restructure `_stopPortal()` to stop BLE before changing WiFi:

Currently, `_stopPortal()`:
1. `_stopBLE()` → stops BLE
2. Delete web server, dns
3. `WiFi.softAPdisconnect(true)` → turns off AP

Then the loop might call `_tryConnect()`:
1. `WiFi.mode(WIFI_STA)` → switches to STA
2. `WiFi.begin()` → connects

Then when re-entering portal:
1. `_startPortal()`
2. `WiFi.disconnect(true)` → turns off WiFi
3. `WiFi.mode(WIFI_AP)` → switches to AP
4. `_startBLE()` → tries to start BLE

What if we need to properly reinitialize the BT controller between WiFi mode changes? With `btStop()`/`btStart()`, this should work.

OK let me just implement this. Let me add `btStop()` in `_stopBLE()` and keep the rest as is. Let me use `btStop()` for clean reset.

Wait, I realize there's another approach that might be even simpler. What if the issue is that `BLEDevice::init()` was called once, and when we `delete _bleServer` and create a new one, there's a conflict in the GATT database? What if the old GATT entries are still registered in the BLE stack?

By calling `btStop()`, we completely shut down the BT controller, which clears all GATT state. Then `BLEDevice::init()` (or the implicit start when creating a server) restarts the BT controller fresh.

But I'm not sure if `BLEDevice::init()` calls `btStart()` internally... Let me check.

In ESP32 Arduino 3.x with NimBLE:
- `NimBLEDevice::init()` calls `nimble_init()` which initializes the NimBLE stack
- It doesn't directly call `btStart()`, but the NimBLE stack uses the BT controller via the ESP-IDF NimBLE port layer

With Bluedroid:
- `BLEDevice::init()` calls `btStart()` internally

So the behavior depends on which BLE stack is in use.

OK, I'm going to try a different approach. Let me just ensure the BLE controller is properly reset. Let me use `esp_bt_controller_disable()` and `esp_bt_controller_enable()` which are ESP-IDF level and work with any BLE stack.

Actually, let me just try the simplest thing: call `btStop()` in `_stopBLE()`. This is an Arduino-level function that should work regardless of the underlying BLE stack.

Wait, I need to be careful. `btStop()` might be a no-op or might crash if used with NimBLE. Let me check...

Actually, in ESP32 Arduino 3.x, `btStart()`/`btStop()` are defined in `esp32-hal-bt.c` and they manage the BT controller at the low level. They should work with any BLE stack because they directly call ESP-IDF functions.

Let me implement this fix now.


让我用更底层的方式彻底重置 BLE 控制器。问题不仅仅是 Server 对象，而是整个 BLE 栈在 WiFi 模式切换后状态异常。


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	// 设备配置结构体

/**
 * @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"

// 设备配置结构体


OK let me implement a more thorough fix. The approach:

1. In `_stopBLE()`:
   - Stop advertising
   - Properly disconnect any connected BLE clients first
   - Delete the server
   - Call `btStop()` to fully shut down the BT controller

2. In `_startBLE()`:
   - Add a delay after WiFi AP is started (to let radio stabilize)
   - Call `btStart()` to restart the BT controller
   - Call `BLEDevice::init()` (remove the static guard since btStop/btStart properly cycles things)
   - Full server/service/characteristic creation

Let me also add `#include <esp_bt.h>` to the .cpp file for `btStop()`/`btStart()`... Actually, `btStop()` should be available from `Arduino.h` in ESP32 Arduino core.

Wait, actually I realize the `btStop()` and `btStart()` functions are in the ESP32 Arduino core, declared in `esp32-hal-bt.h`. Let me check if they're automatically available.

Also, I need to be careful: after `btStop()`, `BLEDevice::init()` should work again because the library's `m_initialized` flag might still be set. But if `BLEDevice::init()` checks this flag and returns early, it won't re-initialize the BT controller. So I might need to bypass `BLEDevice::init()` and directly call `btStart()` + `BLEDevice::createServer()`.

Let me think about the sequence:
1. First boot: `_startBLE()` → `BLEDevice::init()` → `btStart()` internally → `createServer()` → BLE works ✅
2. `_stopBLE()` → `delete _bleServer` → `btStop()` → BT controller off
3. Second portal: `_startBLE()` → `btStart()` → BT controller on → `BLEDevice::init()` → returns early (already initialized flag) → `createServer()` → BLE should work because BT controller was reset

But wait, `BLEDevice::init()` does more than just start the BT controller. It also initializes the BLE stack (callbacks, event handling, etc.). If we skip it because `m_initialized` is true, the BLE stack might not be properly set up.

So I think the correct approach is:
1. `btStop()` in `_stopBLE()`
2. In `_startBLE()`: call `btStart()` first, then call `BLEDevice::init()` regardless (remove the static guard). If `BLEDevice::init()` checks the flag and returns, that's OK because the BT controller was already started by `btStart()` and the BLE stack is still initialized from the first time.

Actually, I realize there's an even simpler approach. Instead of using `btStop()`/`btStart()`, what if I use `esp_bt_sleep_disable()` / `esp_bt_sleep_enable()` or just ensure proper coexistence?

No, let me just go with the `btStop()`/`btStart()` approach and remove the static guard on `BLEDevice::init()`. If `BLEDevice::init()` is called a second time, most implementations just log a warning and return. But the BT controller will have been freshly started by `btStart()`.

Actually, let me look at this another way. The simplest possible fix:

What if the issue is just that after WiFi mode switches, the BLE advertising needs a fresh `BLEAdvertising` object?

In the original code (before my first fix):
```cpp
if (_bleServer != nullptr) {
    if (_bleAdv) {
        _bleAdv->start();  // Reuses old advertising object
    }
}
```

My first fix deleted the server and created a new one, which creates a new advertising object. But it kept the `static bool bleInitialized` guard so `BLEDevice::init()` was only called once.

What if the issue is that `BLEDevice::init()` needs to be called after `btStop()`? Since I can't safely call `init()` twice (the library might crash), let me try a different approach:

1. `btStop()` in `_stopBLE()` to fully reset the BT controller
2. Don't call `BLEDevice::init()` again (keep the guard)
3. Just call `btStart()` before `createServer()` in `_startBLE()`

This way, the BT controller is freshly started, and the BLE stack (which was initialized once) should be able to use the freshly started controller.

Wait, but the `m_initialized` flag in `BLEDevice`... does `createServer()` check it? Yes, it does:
```cpp
BLEServer* BLEDevice::createServer() {
    if (!m_initialized) return nullptr;
    // ...
}
```

After `btStop()`, `m_initialized` would still be true (it's a software flag). So `createServer()` should work. And the BT controller was freshly restarted by `btStart()`, so the underlying hardware should be in a clean state.

But will the BLE stack library's callbacks and event handlers still work after `btStop()` + `btStart()`? The NimBLE/Bluedroid host stack runs as a separate task. After `btStop()`, this task might be stopped. After `btStart()`, it might restart. But the Arduino BLE wrapper might not properly reconnect to the new host stack instance.

This is getting too complex. Let me try a totally different approach.

**Radical approach**: Just restart the ESP32 when re-entering provisioning mode. This is the most reliable way to reset all state.

No, that's too aggressive. The user might be in the middle of something.

OK, let me try yet another approach. What if the problem is simply that the BLE advertising needs time after WiFi AP starts? Let me add a delay:

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

    WiFi.disconnect(true);
    delay(500);  // Longer delay for radio to stabilize
    WiFi.mode(WIFI_AP);
    WiFi.softAPConfig(...);
    WiFi.softAP(...);

    delay(100);  // Let WiFi AP stabilize

    DNS setup...
    WebServer setup...

    _startBLE();  // Start BLE after WiFi is fully up
}
```

Or what about making sure the WiFi mode includes BLE coexistence?

Actually, I just thought of another angle. On ESP32, when you call `WiFi.mode(WIFI_AP)`, the PHY is configured for AP operation. BLE uses the same PHY. For them to coexist, you might need to call:
```cpp
WiFi.setSleep(false);  // Disable WiFi sleep to prevent BLE conflicts
```

Or:
```cpp
esp_wifi_set_ps(WIFI_PS_NONE);  // No power save
```

If WiFi power save is enabled, the radio might be put to sleep at times, which would interfere with BLE advertising.

BUT — on first boot, WiFi AP mode also uses the same settings. So this would affect the first time too...

Let me try a completely different diagnosis. What if I look at the actual sequence of events from the user's perspective:

1. Device boots → enters provisioning mode (no WiFi config) → BLE and AP both work
2. User connects phone to device BLE → sends WiFi credentials
3. Device receives credentials → replies "OK" → stops BLE → stops AP → connects to WiFi
4. Device connects to WiFi successfully → normal operation
5. User wants to change WiFi → long-presses button
6. Device enters provisioning mode → WiFi AP starts → BLE should start advertising
7. BUT phone can't find BLE signal

What if step 3 is the problem? The device replies "OK" and then immediately shuts down BLE without properly disconnecting. What if the phone still has an active BLE connection reference? Some phones (especially Android) have a bug where they don't properly clean up BLE connections.

On the ESP32 side, deleting the server should forcefully terminate the connection. But on the phone side, the BLE stack might cache the device and filter it from future scans.

This is actually a well-known issue with BLE on Android! After connecting to a BLE device, Android caches the GATT services. If the same device (same address) starts advertising again, Android might use cached data and not show it as "discoverable".

**Solution**: Use a different BLE address each time advertising starts. Or clear Android's BLE cache. Since the user can't clear Android's cache, the ESP32 should use a random address.

On ESP32 BLE, you can set a random address:
```cpp
BLEAddress addr = BLEAddress(ESP_BD_ADDR_LEN);  // Random
// Or use the NimBLE API directly
```

Actually, BLE already uses random addresses by default for advertising (depending on configuration). But the ESP32 Arduino BLE library might use the public address by default.

Let me look at how to set a random address in `_startBLE()`:

```cpp
// Use random address for advertising to prevent
// phone BLE cache issues on re-connection
esp_bd_addr_t randAddr;
esp_fill_random(randAddr, sizeof(randAddr));
randAddr[0] = (randAddr[0] & 0x3F) | 0xC0;  // Random static address
_bleAdv->setDeviceAddress(BLEAddress(randAddr));
```

Wait, `BLEAdvertising::setDeviceAddress()` might not exist in all versions. Let me think about alternatives.

In ESP32 BLE, you can configure the advertising to use a non-resolvable private address (NRPA), which changes every time. This is set via `BLEAdvertising::setScanFilter(false, false)` or similar... Actually, I don't think the Arduino BLE API exposes this.

Hmm, let me try another approach for the address issue. What about using `esp_ble_gap_set_rand_addr()` or similar ESP-IDF function?

This is getting really complex. Let me step back and think about what's the most LIKELY cause.

Given that:
1. My previous fix (recreating server) didn't help
2. The exact same BLE code works on first attempt
3. The main difference is that WiFi was in STA mode (connected) between attempts

The most likely cause is that **WiFi STA mode operation interferes with subsequent BLE advertising**. This could be because:
- WiFi STA leaves the radio in a state that prevents BLE from properly initializing
- The coexistence manager is configured for WiFi STA, and switching to AP doesn't update it for BLE
- The PHY calibration needs to be redone after mode switch

The fix should focus on properly resetting the radio state. Using `btStop()` + `btStart()` would reset the BT controller. Also adding `esp_wifi_set_ps(WIFI_PS_NONE)` would prevent WiFi power save from interfering.

Let me implement this comprehensively:

1. In `_stopBLE()`: delete server, call `btStop()`
2. In `_startBLE()`: call `btStart()`, then full BLE init
3. Remove the `static bool bleInitialized` guard to allow re-init
4. In `_startPortal()`: add `esp_wifi_set_ps(WIFI_PS_NONE)` before starting BLE

Actually, I just realized something important. `BLEDevice::init()` calls `btStart()` internally. If `btStop()` was called, and then `BLEDevice::init()` is called again, it would call `btStart()` again. But `BLEDevice::init()` has the `m_initialized` flag that prevents re-initialization.

So the sequence should be:
1. `_stopBLE()`: delete server, call `btStop()`
2. `_startBLE()`: call `btStart()` to restart controller, then create server directly (skip `BLEDevice::init()` since it was already called once)

But `BLEDevice::createServer()` checks `m_initialized`, which is still true, so it should work.

Wait, there's more to `BLEDevice::init()` than just `btStart()`. It also:
- Sets up the BLE event callback task
- Registers GAP/GATT event handlers
- Initializes the BLE device name
- Sets up security parameters

If we skip `init()`, all of these should still be set up from the first call. And `btStart()` restarts the BT controller. So `createServer()` should work.

But what if the event callback task was stopped by `btStop()`? Let me check...

`btStop()` calls `esp_bt_controller_disable()` which disables the BT controller. It doesn't kill the NimBLE/Bluedroid host task. The host task should continue running, just without the controller. When `btStart()` re-enables the controller, the host task should reconnect.

I think this approach should work. Let me implement it.

Actually wait, I need to check: does `btStart()` just call `esp_bt_controller_enable()`, or does it also do other setup? Let me think...

In ESP32 Arduino core, `btStart()`:
```cpp
bool btStart() {
    esp_bt_controller_config_t cfg = BT_CONTROLLER_CONFIG_DEFAULT();
    esp_bt_controller_init(&cfg);
    esp_bt_controller_enable(ESP_BT_MODE_BTDM);
    return true;
}
```

But if the BT controller was already initialized (via `BLEDevice::init()`), calling `esp_bt_controller_init()` again would fail. And `btStop()` only calls `esp_bt_controller_disable()`, not `deinit()`.

So the sequence would be:
1. `BLEDevice::init()` → `esp_bt_controller_init()` + `esp_bt_controller_enable()`
2. `btStop()` → `esp_bt_controller_disable()` (not deinit)
3. `btStart()` → `esp_bt_controller_init()` ← THIS WOULD FAIL because already initialized!

So I need to call `esp_bt_controller_deinit()` first, then `btStart()` would work.

Or just call `esp_bt_controller_enable()` directly instead of `btStart()`:

```cpp
void WiFiMgmt::_startBLE() {
    // Re-enable BT controller after btStop() disabled it
    esp_bt_controller_enable(ESP_BT_MODE_BLE);

    _bleServer = BLEDevice::createServer();
    // ... rest
}
```

But I'd need to include `esp_bt.h`.

OK, let me simplify. Instead of `btStop()` + `btStart()`, let me just:

1. In `_stopBLE()`: After deleting server, call `esp_bt_controller_disable()`
2. In `_startBLE()`: Call `esp_bt_controller_enable(ESP_BT_MODE_BLE)`, then createServer()

This is lower level but should work. I need to add `#include <esp_bt.h>`.

Actually, wait. `BLEDevice::init()` calls `esp_bt_controller_init()` and `esp_bluedroid_init()`. After `esp_bt_controller_disable()`, the controller is disabled but `esp_bluedroid_init()` was already called. Re-enabling the controller should be enough...

But I'm not sure if `BLEDevice::init()` uses Bluedroid or NimBLE. With NimBLE, the initialization is different.

OK, I think I need to give up on the low-level approach and think about this differently.

Let me look at the problem from the USER'S perspective again. They said: "第一次能够配上，再次断开重开，配网就扫描不到蓝牙信号了"

What if "断开" means disconnecting the BLE connection on the PHONE side? Like closing the Mini Program and reopening it? In that case:

1. User opens Mini Program → scans → finds "HealthMon" → connects → provisions → OK
2. User closes Mini Program (disconnects BLE)
3. User wants to provision again → opens Mini Program → scans → can't find "HealthMon"

But the device is still in provisioning mode (or re-entered provisioning mode). So the device should still be advertising.

What if in step 2, the WeChat Mini Program's BLE API doesn't properly close the BLE connection? The phone might remain "connected" to the ESP32 at the OS level, even though the Mini Program has closed. When the Mini Program tries to scan again, the OS filters out the already-connected device.

This would explain the issue! The phone's BLE stack thinks it's still connected to the ESP32, so it doesn't show it in scan results.

**Fix**: On the ESP32 side, detect when a BLE client disconnects, and properly clean up. Also, on re-entering provisioning mode, use a different BLE identity to prevent phone-side caching issues.

For the "different BLE identity" approach, I can change the advertised device name or add a random identifier to the advertising data. Or better: just make sure the ESP32 properly terminates the BLE connection when entering provisioning mode.

Actually, you know what, let me look at this from one more angle. The ESP32's `BLEServer` has a `disconnect()` method or similar. In `_stopBLE()`, before deleting the server, I should disconnect all connected clients:

```cpp
void WiFiMgmt::_stopBLE() {
    if (_bleServer) {
        // Disconnect all connected clients first
        auto connectedDevices = _bleServer->getPeerDevices();
        for (auto& device : connectedDevices) {
            device->disconnect();
        }
        delay(200);  // Let disconnection complete
    }
    // ... rest of cleanup
}
```

Wait, `getPeerDevices()` might not exist in the Arduino BLE API. Let me check...

Hmm, this API varies between NimBLE and Bluedroid. Let me try a different approach.

What about just adding a big delay in `_stopBLE()` between stopping advertising and deleting the server? This gives the BLE stack time to properly handle any pending connections.

Or, what if I DON'T delete the BLE server in `_stopBLE()` and instead keep it alive but properly disconnect clients? The original code already tried this (keeping server alive), and it didn't work...

OK, I'm going to try a combination approach:

1. In `_stopBLE()`:
   - Stop advertising
   - Wait for any connections to properly terminate (delay)
   - Delete server
   - Add a significant delay before returning (to let BLE stack stabilize)

2. In `_startBLE()`:
   - Full re-init (as currently implemented with my fix)
   - BUT also: call `btStop()` + `btStart()` before creating the server (to reset the BT controller)

3. In `_startPortal()`:
   - Add delay between WiFi AP start and BLE start

Let me also try another thing: what if we bypass `BLEDevice::createServer()` and use the NimBLE API directly? No, that would be too invasive.

Let me just implement the `btStop()` + `btStart()` approach along with better delays.

Actually, I just realized there's a much simpler thing to try first. In `_startBLE()`, the `BLEDevice::init()` function name parameter is `"HealthMonitor"`, but the advertising short name is `"HealthMon"`. What if the display is inconsistent? Some phones might show "HealthMonitor" from the init name and "HealthMon" from the advertisement. But this doesn't explain why it works the first time...

OK, I'm going to try the `btStop()` + `btStart()` approach. This is the most likely to reset the BT hardware properly.

But first, let me check: does `btStop()` even exist in ESP32 Arduino 3.x? Let me search for it.

Actually, `btStop()` and `btStart()` are defined in `cores/esp32/esp32-hal-bt.c`. They've been part of the ESP32 Arduino core since very early versions. They should be available.

The functions:
```c
bool btStart() {
    esp_bt_controller_config_t cfg = BT_CONTROLLER_CONFIG_DEFAULT();
    if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) {
        esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); // Release Classic BT memory
    }
    esp_bt_controller_init(&cfg);
    esp_bt_controller_enable(ESP_BT_MODE_BTDM);
    return true; // With error checking
}

bool btStop() {
    esp_bt_controller_disable();
    esp_bt_controller_deinit();
    esp_bt_controller_mem_release(ESP_BT_MODE_BTDM);
    return true;
}
```

Wait, `btStop()` calls `esp_bt_controller_deinit()`. So after `btStop()`, the BT controller is fully deinitialized. Then `btStart()` calls `esp_bt_controller_init()` again. This is a full hardware reset!

But there's a problem: `BLEDevice::init()` also calls `esp_bt_controller_init()` (indirectly). After `btStop()` deinits the controller, `BLEDevice::init()`'s flag `m_initialized` is still true, so it won't re-init. But if I just call `btStart()` before `createServer()`, the BT controller will be freshly initialized.

However, `BLEDevice::init()` does more than just init the controller. For Bluedroid, it calls:
```cpp
esp_bt_controller_init();
esp_bt_controller_enable();
esp_bluedroid_init();
esp_bluedroid_enable();
```

For NimBLE, it calls:
```cpp
nimble_init();
// which internally does esp_bt_controller_init+enable and nimble_port_init
```

If `btStop()` deinits the controller AND releases memory, and then I call `btStart()` which inits+enables, the Bluedroid/NimBLE host is still initialized (from the first `BLEDevice::init()` call). The host should reconnect to the newly enabled controller.

Wait, `btStop()` calls `esp_bt_controller_mem_release(ESP_BT_MODE_BTDM)`. This releases the BT controller's memory. If Bluedroid was using this memory... this could cause a crash.

Maybe I should NOT use `btStop()` then. Instead, just disable and re-enable the controller without releasing memory:

```cpp
// In _stopBLE()
esp_bt_controller_disable();

// In _startBLE()
esp_bt_controller_enable(ESP_BT_MODE_BLE);
```

This doesn't release memory, just disables/enables the controller. It's safer.

OK let me just implement this. I'll add `#include <esp_bt.h>` and use these functions.

Let me write the final fix now.


让我检查一下 `btStop`/`btStart` 是否可用，然后用更彻底的方式重置 BLE 控制器。


No matches found


The project doesn't currently use `btStop`/`btStart`. Let me check what's available in the ESP32 Arduino framework. Let me search the framework install path for these functions.

Actually, I know these functions exist in the ESP32 Arduino core. They're declared in `esp32-hal-bt.h`. Let me just try including the right headers and using them.

But wait - I need to think about whether using `btStop()`/`btStart()` is safe with the BLE Arduino library.

Let me try a different, safer approach. Instead of resetting the BT controller, let me:

1. Ensure the BLE server is properly cleaned up (already done)
2. Add a delay after cleanup to let everything stabilize
3. In `_startBLE()`, do full re-creation (already done)
4. Add `delay()` calls at key points
5. Most importantly: use a **random BLE address** each time to prevent phone-side caching

Actually, I think the phone-side caching is the real issue. Let me verify:

When a phone (Android/iOS) connects to a BLE device:
1. The phone caches the GATT database for that device address
2. If the device disconnects abruptly, the phone might still show it as "connected" for a while
3. When scanning, the phone might filter out the device because it thinks it's already connected
4. Android especially has a BLE scan filter that can hide devices that were recently connected

This is a very common problem! The fix is to change the BLE address or properly terminate the connection.

For ESP32, I can configure random addressing. Let me check how...

In the ESP32 BLE Arduino wrapper (NimBLE-based), I can do:
```cpp
BLEAddress useAddress = BLEAddress::randomAddress();
// Or use the NimBLE API:
NimBLEAddress addr("");
```

Actually, in the NimBLE-based API, advertising uses a random address by default! Let me verify...

In NimBLE, `NimBLEAdvertising::start()` uses `BLE_OWN_ADDR_RANDOM` by default. This means each call to `start()` generates a new random address. But wait, this random address might persist across `start()`/`stop()` cycles.

Hmm, I'm not sure about the default. Let me check if there's a way to force a new random address.

Actually, in NimBLE (ESP32 Arduino 3.x default), advertising uses `BLE_OWN_ADDR_RANDOM` which means it uses a random address. But this address is generated once and stays the same for the device's lifetime (it's a "random static address", not a "random private address").

For a new address each time, I'd need to use a "non-resolvable private address" (NRPA). This changes every time advertising starts.

In NimBLE, I can set this with:
```cpp
// Before starting advertising
ble_gap_addr_pref_t addr_pref;
addr_pref.type = BLE_GAP_ADDR_PREF_NRPA;
// ... configure and set
```

But this is low-level NimBLE API, not the Arduino wrapper.

OK, let me try a completely different approach. Let me focus on what I CAN do easily:

1. Add significant delays between WiFi mode switch and BLE operations
2. In `_stopBLE()`, add a delay before deleting (to let connections terminate properly)
3. Call `esp_bt_controller_disable()` + `esp_bt_controller_enable()` to reset the controller
4. In `_startBLE()`, properly recreate everything

For step 3, I need to add `#include <esp_bt.h>`.

Let me implement this now. If the `esp_bt_controller_enable/disable` functions cause issues, they're well-documented ESP-IDF functions that should work.

Actually, wait. Let me also consider: maybe the root cause is much simpler. What if the WiFi library and BLE library are fighting over the radio, and we just need to ensure they don't operate simultaneously during initialization?

Let me restructure `_startPortal()`:
1. Set up WiFi AP first
2. Wait for WiFi AP to be fully operational
3. THEN start BLE

And ensure there's enough delay between them.

Similarly, in `_stopPortal()`:
1. Stop BLE first
2. Wait for BLE to fully stop
3. THEN stop WiFi AP

This way, BLE and WiFi AP are never initializing/shutting down simultaneously.

Let me implement this alongside the controller reset.


让我用一个多管齐下的方案来解决这个问题：

1. BLE 底层控制器彻底重置（disable → enable）
2. BLE Server 完全重建
3. WiFi 和 BLE 操作之间增加稳定延时


1	/**
2	 * @file wifi_manager.cpp
3	 * @brief WiFi 配网管理器 — AP + WebServer
4	 */
5
6	#include "wifi_manager.h"
7
8	// BLE 配网回调: 小程序写入 JSON → 仅在 BLE 栈中暂存, 不直接处理

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

#include "wifi_manager.h"

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


Now let me implement the comprehensive fix:

1. Add `#include <esp_bt.h>` for low-level BT controller functions
2. Rewrite `_stopBLE()` to properly disable the BT controller
3. Rewrite `_startBLE()` to re-enable the BT controller and do full init
4. Add delays for stabilization

Let me write the complete fix.


Edit

#include "wifi_manager.h"

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

#include "wifi_manager.h"
#include <esp_bt.h>

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


#include "wifi_manager.h"

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

#include "wifi_manager.h"
#include <esp_bt.h>

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

class BLEProvisionCB : public BLECharacteristicCallbacks {


Now let me update `_stopBLE()` and `_startBLE()` with the controller disable/enable approach.


现在更新 `_stopBLE()` — 彻底关闭 BLE 控制器：


void WiFiMgmt::_stopBLE() {
    // 彻底释放 BLE 资源：仅停广播不够，
    // WiFi 模式切换 (AP→STA→AP) 后旧 BLE 对象无法恢复
    if (_bleAdv)      { _bleAdv->stop(); _bleAdv = nullptr; }
    if (_bleChar)     { _bleChar = nullptr; }      // 由 service 析构
    if (_bleInfoChar) { _bleInfoChar = nullptr; }   // 由 service 析构
    if (_bleServer)   { delete _bleServer; _bleServer = nullptr; }
    Serial.println("[BLE] 已完全停止");
}

void WiFiMgmt::_stopBLE() {
    // 1. 停止广播
    if (_bleAdv)      { _bleAdv->stop(); _bleAdv = nullptr; }

    // 2. 释放 BLE Server 资源 (触发 NimBLE/Bluedroid 内部清理)
    if (_bleChar)     { _bleChar = nullptr; }
    if (_bleInfoChar) { _bleInfoChar = nullptr; }
    if (_bleServer)   { delete _bleServer; _bleServer = nullptr; }

    // 3. 彻底关闭 BT 控制器 — WiFi STA→AP 模式切换后,
    //    不重置控制器则 BLE 广播无法恢复 (手机扫不到)
    if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_ENABLED) {
        esp_bt_controller_disable();
        delay(100);  // 等待控制器完全停用
    }

    Serial.println("[BLE] BT 控制器已停用");
}


void WiFiMgmt::_stopBLE() {
    // 彻底释放 BLE 资源：仅停广播不够，
    // WiFi 模式切换 (AP→STA→AP) 后旧 BLE 对象无法恢复
    if (_bleAdv)      { _bleAdv->stop(); _bleAdv = nullptr; }
    if (_bleChar)     { _bleChar = nullptr; }      // 由 service 析构
    if (_bleInfoChar) { _bleInfoChar = nullptr; }   // 由 service 析构
    if (_bleServer)   { delete _bleServer; _bleServer = nullptr; }
    Serial.println("[BLE] 已完全停止");
}

void WiFiMgmt::_stopBLE() {
    // 1. 停止广播
    if (_bleAdv)      { _bleAdv->stop(); _bleAdv = nullptr; }

    // 2. 释放 BLE Server 资源 (触发 NimBLE/Bluedroid 内部清理)
    if (_bleChar)     { _bleChar = nullptr; }
    if (_bleInfoChar) { _bleInfoChar = nullptr; }
    if (_bleServer)   { delete _bleServer; _bleServer = nullptr; }

    // 3. 彻底关闭 BT 控制器 — WiFi STA→AP 模式切换后,
    //    不重置控制器则 BLE 广播无法恢复 (手机扫不到)
    if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_ENABLED) {
        esp_bt_controller_disable();
        delay(100);  // 等待控制器完全停用
    }

    Serial.println("[BLE] BT 控制器已停用");
}

-    // 彻底释放 BLE 资源：仅停广播不够，

-    // WiFi 模式切换 (AP→STA→AP) 后旧 BLE 对象无法恢复

-    if (_bleChar)     { _bleChar = nullptr; }      // 由 service 析构

-    if (_bleInfoChar) { _bleInfoChar = nullptr; }   // 由 service 析构

+    // 2. 释放 BLE Server 资源 (触发 NimBLE/Bluedroid 内部清理)

+    if (_bleChar)     { _bleChar = nullptr; }

+    if (_bleInfoChar) { _bleInfoChar = nullptr; }

+    // 3. 彻底关闭 BT 控制器 — WiFi STA→AP 模式切换后,

+    //    不重置控制器则 BLE 广播无法恢复 (手机扫不到)

+    if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_ENABLED) {

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


Now update `_startBLE()` to re-enable the BT controller and do full init.


现在更新 `_startBLE()` — 重新启用 BT 控制器并完整初始化：