Let me understand the context. The last session (July 21, 2026) was working on fixing compilation errors in this ESP32-S3 project. The session fixed:

1. `last_update` → `lastUpdate` in radar_manager.cpp (member name mismatch)
2. Added `queryDetectRange()` declaration to radar_manager.h
3. Was working on MQTT compilation errors (MQTT_KEEPALIVE redefinition and PubSubClient::connected() const issue)

Now the user says "继续上会的话题" (continue the previous topic). Let me check the current state of the files to see what still needs fixing. Looking at the git status:
- Many files are modified (M) or deleted (D)
- `include/mqtt_manager.h`, `include/radar_manager.h`, `include/radar_protocol.h`, `include/wifi_manager.h` are staged
- `include/wm_strings_zh.h` is deleted
- `src/mqtt_manager.cpp`, `src/radar_manager.cpp`, etc. are modified

The last session ended mid-fix on the MQTT compilation issue. The errors were:
1. `MQTT_KEEPALIVE` redefinition (warning)
2. `_mqtt.connected()` - const method issue in `isConnected()`

Let me read the current state of the modified files to understand what's changed and what still needs to be fixed.


让我看看当前代码状态，特别是上次未完成的 MQTT 编译问题。


133	        delay(100);
134	        _mqtt.disconnect();
135	        _connected = false;
136	    }
137	}
138
139	bool MQTTManager::isConnected() {
140	    return _connected && _mqtt.connected();
141	}
142
143	// ============================================================
144	// 主循环
145	// ============================================================
146
147	void MQTTManager::loop() {

        delay(100);
        _mqtt.disconnect();
        _connected = false;
    }
}

bool MQTTManager::isConnected() {
    return _connected && _mqtt.connected();
}

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

void MQTTManager::loop() {


1	/**
2	 * @file mqtt_manager.h
3	 * @brief MQTT 通信管理器 (协议规范 V0.08)
4	 *
5	 * 上行: report/allpoints, status/{gateid}/online, status/{gateid}/offline,
6	 *       status/{gateid}/provision, response/{gateid}/cmd, response/{gateid}/provision
7	 * 下行: cmd/{gateid}/alarm, cmd/{gateid}/alarm/restore,
8	 *       cmd/{gateid}/control, cmd/{gateid}/provision/wifi
9	 */
10
11	#ifndef MQTT_MANAGER_H
12	#define MQTT_MANAGER_H
13
14	#include <Arduino.h>
15	#include <WiFi.h>
16	#include <PubSubClient.h>
17	#include <ArduinoJson.h>
18	#include "config.h"
19
20	// ============================================================
21	// 回调函数类型
22	// ============================================================
23
24	// 告警指令回调 (alarm / alarm_restore)
25	typedef void (*MQTTAlarmCallback)(const AlarmCommand& cmd);
26
27	// 控制指令回调
28	typedef void (*MQTTControlCallback)(const ControlCommand& cmd);
29
30	// 配网指令回调 (WiFi 凭证下发)
31	typedef void (*MQTTProvisionCallback)(const ProvisionCommand& cmd);
32
33	// ============================================================
34	// MQTTManager 类
35	// ============================================================
36
37	class MQTTManager {
38	public:
39	    MQTTManager();
40	    ~MQTTManager();
41
42	    /** 初始化 */
43	    bool begin(const char* gateid);
44
45	    /** 主循环 */
46	    void loop();
47
48	    /** 连接/断开 */
49	    bool connect();
50	    void disconnect();
51	    bool isConnected();
52
53	    /** 设置服务器 */
54	    void setServer(const char* host, uint16_t port,
55	                   const char* user, const char* pass);
56
57	    // ============================================================
58	    // 上行: 发布方法 (协议规范 §4)
59	    // ============================================================
60
61	    /** 全量遥测上报 report/allpoints (§4.1) */
62	    void publishAllPoints(const TelemetryData& data, uint32_t sequence);
63
64	    /** 设备上线通知 status/{gateid}/online (§4.2) */
65	    void publishOnline();
66
67	    /** 设备离线通知 status/{gateid}/offline (§4.3) — 主动调用 */
68	    void publishOffline();
69
70	    /** 配网状态通知 status/{gateid}/provision (§4.4) */
71	    void publishProvisionStatus(const char* state, const char* bleName);
72
73	    /** 指令执行响应 response/{gateid}/cmd (§4.5) */
74	    void publishCmdResponse(const char* cmdId, const char* cmdType,
75	                            const char* alarmType, bool success,
76	                            const char* errorCode, const char* errorMsg);
77
78	    /** 配网指令响应 response/{gateid}/provision (§4.6) */
79	    void publishProvisionResponse(const char* cmdId, bool success,
80	                                  const char* errorCode, const char* errorMsg,
81	                                  const char* wifiSignal = "");
82
83	    /** 本地报警上报 status/{gateid}/alarm (§4.7 扩展) */
84	    void publishLocalAlarm(uint8_t alarmType, bool triggered, const char* desc);
85
86	    /** 无人极简报文 (仅 Presence+RadarValid, 省4G流量) */
87	    void publishPresenceOnly(bool presence, bool radarValid, uint32_t sequence);
88
89	    // ============================================================
90	    // 回调注册 (协议规范 §5)
91	    // ============================================================
92
93	    void onAlarmCommand(MQTTAlarmCallback cb);
94	    void onControlCommand(MQTTControlCallback cb);
95	    void onProvisionCommand(MQTTProvisionCallback cb);
96
97	    /** 获取 gateid */
98	    const char* getGateId() const { return _gateid; }
99
100	private:
101	    static void _mqttCallback(char* topic, byte* payload, unsigned int length);
102	    static MQTTManager* _instance;
103
104	    // 主题构建
105	    String _buildTopic(const char* tmpl) const;
106
107	    // 下行报文解析
108	    void _handleMessage(const char* topic, const char* payload);
109	    void _parseAlarm(const char* payload, bool isRestore);
110	    void _parseControl(const char* payload);
111	    void _parseProvision(const char* payload);
112
113	    // 发布辅助
114	    bool _publish(const char* topicTmpl, const char* payload, uint8_t qos, bool retain = false);
115
116	    WiFiClient      _wifiClient;
117	    PubSubClient    _mqtt;
118	    char            _gateid[16];
119
120	    // 服务器配置
121	    char     _server[64];
122	    uint16_t _port;
123	    char     _user[32];
124	    char     _pass[32];
125
126	    // 重连
127	    uint32_t _reconnectInterval;
128	    uint32_t _lastReconnectAttempt;
129	    bool     _connected;
130
131	    // 回调
132	    MQTTAlarmCallback     _alarmCb;
133	    MQTTControlCallback   _controlCb;
134	    MQTTProvisionCallback _provisionCb;
135	};
136
137	#endif // MQTT_MANAGER_H
138

/**
 * @file mqtt_manager.h
 * @brief MQTT 通信管理器 (协议规范 V0.08)
 *
 * 上行: report/allpoints, status/{gateid}/online, status/{gateid}/offline,
 *       status/{gateid}/provision, response/{gateid}/cmd, response/{gateid}/provision
 * 下行: cmd/{gateid}/alarm, cmd/{gateid}/alarm/restore,
 *       cmd/{gateid}/control, cmd/{gateid}/provision/wifi
 */

#ifndef MQTT_MANAGER_H
#define MQTT_MANAGER_H

#include <Arduino.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include "config.h"

// ============================================================
// 回调函数类型
// ============================================================

// 告警指令回调 (alarm / alarm_restore)
typedef void (*MQTTAlarmCallback)(const AlarmCommand& cmd);

// 控制指令回调
typedef void (*MQTTControlCallback)(const ControlCommand& cmd);

// 配网指令回调 (WiFi 凭证下发)
typedef void (*MQTTProvisionCallback)(const ProvisionCommand& cmd);

// ============================================================
// MQTTManager 类
// ============================================================

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

    /** 初始化 */
    bool begin(const char* gateid);

    /** 主循环 */
    void loop();

    /** 连接/断开 */
    bool connect();
    void disconnect();
    bool isConnected();

    /** 设置服务器 */
    void setServer(const char* host, uint16_t port,
                   const char* user, const char* pass);

    // ============================================================
    // 上行: 发布方法 (协议规范 §4)
    // ============================================================

    /** 全量遥测上报 report/allpoints (§4.1) */
    void publishAllPoints(const TelemetryData& data, uint32_t sequence);

    /** 设备上线通知 status/{gateid}/online (§4.2) */
    void publishOnline();

    /** 设备离线通知 status/{gateid}/offline (§4.3) — 主动调用 */
    void publishOffline();

    /** 配网状态通知 status/{gateid}/provision (§4.4) */
    void publishProvisionStatus(const char* state, const char* bleName);

    /** 指令执行响应 response/{gateid}/cmd (§4.5) */
    void publishCmdResponse(const char* cmdId, const char* cmdType,
                            const char* alarmType, bool success,
                            const char* errorCode, const char* errorMsg);

    /** 配网指令响应 response/{gateid}/provision (§4.6) */
    void publishProvisionResponse(const char* cmdId, bool success,
                                  const char* errorCode, const char* errorMsg,
                                  const char* wifiSignal = "");

    /** 本地报警上报 status/{gateid}/alarm (§4.7 扩展) */
    void publishLocalAlarm(uint8_t alarmType, bool triggered, const char* desc);

    /** 无人极简报文 (仅 Presence+RadarValid, 省4G流量) */
    void publishPresenceOnly(bool presence, bool radarValid, uint32_t sequence);

    // ============================================================
    // 回调注册 (协议规范 §5)
    // ============================================================

    void onAlarmCommand(MQTTAlarmCallback cb);
    void onControlCommand(MQTTControlCallback cb);
    void onProvisionCommand(MQTTProvisionCallback cb);

    /** 获取 gateid */
    const char* getGateId() const { return _gateid; }

private:
    static void _mqttCallback(char* topic, byte* payload, unsigned int length);
    static MQTTManager* _instance;

    // 主题构建
    String _buildTopic(const char* tmpl) const;

    // 下行报文解析
    void _handleMessage(const char* topic, const char* payload);
    void _parseAlarm(const char* payload, bool isRestore);
    void _parseControl(const char* payload);
    void _parseProvision(const char* payload);

    // 发布辅助
    bool _publish(const char* topicTmpl, const char* payload, uint8_t qos, bool retain = false);

    WiFiClient      _wifiClient;
    PubSubClient    _mqtt;
    char            _gateid[16];

    // 服务器配置
    char     _server[64];
    uint16_t _port;
    char     _user[32];
    char     _pass[32];

    // 重连
    uint32_t _reconnectInterval;
    uint32_t _lastReconnectAttempt;
    bool     _connected;

    // 回调
    MQTTAlarmCallback     _alarmCb;
    MQTTControlCallback   _controlCb;
    MQTTProvisionCallback _provisionCb;
};

#endif // MQTT_MANAGER_H


70	#define GATEID_DEFAULT        "10100001001" // 网关唯一编号
71	#define METER_ID_DEFAULT      "ENV001"      // 感知节点ID (卧室=ENV001, 卫生间=ENV002, 客厅=ENV003)
72	#define METER_NAME_DEFAULT    "RadarDHT11"  // 设备型号名称
73	#define FW_VERSION            "V1.0.0"
74
75	// ============================================================
76	// 3. WiFi 默认配置 (配网使用)
77	// ============================================================
78
79	#define WIFI_AP_SSID          "HealthMonitor-Setup"
80	#define WIFI_AP_PASSWORD      "1234567890"
81	#define WIFI_CONFIG_TIMEOUT   0   // 0=不超时, 一直等待配置
82	#define WIFI_CONNECT_TIMEOUT  15
83	#define WIFI_RETRY_INTERVAL   10000
84
85	// ============================================================
86	// 4. MQTT 默认配置 (协议规范 §2)
87	// ============================================================
88
89	#define MQTT_DEFAULT_SERVER   "202.110.234.102"

#define GATEID_DEFAULT        "10100001001" // 网关唯一编号
#define METER_ID_DEFAULT      "ENV001"      // 感知节点ID (卧室=ENV001, 卫生间=ENV002, 客厅=ENV003)
#define METER_NAME_DEFAULT    "RadarDHT11"  // 设备型号名称
#define FW_VERSION            "V1.0.0"

// ============================================================
// 3. WiFi 默认配置 (配网使用)
// ============================================================

#define WIFI_AP_SSID          "HealthMonitor-Setup"
#define WIFI_AP_PASSWORD      "1234567890"
#define WIFI_CONFIG_TIMEOUT   0   // 0=不超时, 一直等待配置
#define WIFI_CONNECT_TIMEOUT  15
#define WIFI_RETRY_INTERVAL   10000

// ============================================================
// 4. MQTT 默认配置 (协议规范 §2)
// ============================================================

#define MQTT_DEFAULT_SERVER   "202.110.234.102"


Let me analyze the current state:

1. The previous session fixed two issues:
   - `last_update` → `lastUpdate` in radar_manager.cpp (line 45) ✅
   - Added `queryDetectRange()` declaration to radar_manager.h ✅

2. The session was mid-fix on a third issue - MQTT compilation errors:
   - `MQTT_KEEPALIVE` redefinition warning
   - `isConnected() const` calling non-const `_mqtt.connected()`

Looking at the current code:
- `mqtt_manager.cpp:139` - `isConnected()` is now non-const: `bool MQTTManager::isConnected() {` — this seems already fixed (the `const` was removed)
- `mqtt_manager.h:51` - declaration is also non-const: `bool isConnected();`

Wait, but the git status shows these files are modified but not committed. Let me check if the MQTT_KEEPALIVE redefinition warning is still there.

Looking at config.h, the MQTT_KEEPALIVE is likely defined later in the file. Let me also check if PubSubClient.h defines it as 15.

Let me also look at the other modified files to understand the full scope of work. The user wants to continue the previous topic - fixing compilation errors.

Let me check the current compilation status by looking at the main issues. Let me also check what other structural changes were made (like deleted files, new files in ai/, etc.).

Looking at the git status more carefully:
- `M .claude/settings.local.json` - modified
- `MM include/config.h` - staged + working tree changes
- ` M include/mqtt_manager.h` - working tree changes
- `MM include/radar_manager.h` - staged + working tree changes
- `MM include/radar_protocol.h` - staged + working tree changes
- `MM include/wifi_manager.h` - staged + working tree changes
- ` D include/wm_strings_zh.h` - deleted (staged)
- `MM platformio.ini` - staged + working tree changes
- `M  src/alarm_manager.cpp` - staged
- `M  src/button_manager.cpp` - staged
- `MM src/main.cpp` - staged + working tree changes
- ` M src/mqtt_manager.cpp` - working tree changes
- `MM src/radar_manager.cpp` - staged + working tree changes
- `MM src/wifi_manager.cpp` - staged + working tree changes
- `?? ai/` - untracked

So there are significant changes across the project. The previous conversation was about fixing compilation errors. Let me try to build and see what errors remain, or let me look at the code to understand the current state.

Actually, the user just says "继续上会的话题" (continue the previous topic). The previous topic was fixing compilation errors. The last error was about MQTT:
1. MQTT_KEEPALIVE redefinition - warning
2. PubSubClient::connected() const issue - error

Looking at the current code, `isConnected()` already has the `const` removed. So it seems the fix was partially applied. But the MQTT_KEEPALIVE warning might still be there.

Let me try to build the project to see the current compilation status, or let me check the key files to understand what's been changed and what still needs work.

Actually, let me first check the memory directory for what was saved about this project.


让我看看 MQTT_KEEPALIVE 相关代码和当前编译状态。


86	// 4. MQTT 默认配置 (协议规范 §2)
87	// ============================================================
88
89	#define MQTT_DEFAULT_SERVER   "202.110.234.102"
90	#define MQTT_DEFAULT_PORT     8000
91	#define MQTT_DEFAULT_PORT_STR "8000"         // WiFiManager 参数用字符串
92	#define MQTT_DEFAULT_USER     "test"
93	#define MQTT_DEFAULT_PASS     "testtest"
94	#define MQTT_DEFAULT_DEVICE   "10100001001"  // 默认网关ID = gateid
95	#define MQTT_DEFAULT_ENV      "主卧"           // 默认环境备注
96	#undef MQTT_KEEPALIVE
97	#define MQTT_KEEPALIVE        30              // 30秒 (协议规范)
98	#define MQTT_RECONNECT_MIN    1000
99	#define MQTT_RECONNECT_MAX    60000
100
101	// ============================================================
102	// 5. MQTT 主题定义 (协议规范 §3)
103	// ============================================================
104
105	// --- 上行主题 (设备 → 服务端) ---
106	#define MQTT_TOPIC_REPORT_ALLPOINTS   "report/allpoints"                // 全量遥测上报 QoS0
107	#define MQTT_TOPIC_STATUS_ONLINE      "status/%s/online"                // 上线通知 QoS1
108	#define MQTT_TOPIC_STATUS_OFFLINE     "status/%s/offline"               // 离线遗嘱 QoS1
109	#define MQTT_TOPIC_STATUS_PROVISION   "status/%s/provision"             // 配网状态 QoS1
110	#define MQTT_TOPIC_RESPONSE_CMD       "response/%s/cmd"                 // 指令响应 QoS1
111	#define MQTT_TOPIC_RESPONSE_PROVISION "response/%s/provision"           // 配网响应 QoS1
112
113	// --- 下行主题 (服务端 → 设备) ---
114	#define MQTT_TOPIC_CMD_ALARM          "cmd/%s/alarm"                    // 告警指令 QoS1
115	#define MQTT_TOPIC_CMD_ALARM_RESTORE  "cmd/%s/alarm/restore"            // 告警恢复 QoS1

// 4. MQTT 默认配置 (协议规范 §2)
// ============================================================

#define MQTT_DEFAULT_SERVER   "202.110.234.102"
#define MQTT_DEFAULT_PORT     8000
#define MQTT_DEFAULT_PORT_STR "8000"         // WiFiManager 参数用字符串
#define MQTT_DEFAULT_USER     "test"
#define MQTT_DEFAULT_PASS     "testtest"
#define MQTT_DEFAULT_DEVICE   "10100001001"  // 默认网关ID = gateid
#define MQTT_DEFAULT_ENV      "主卧"           // 默认环境备注
#undef MQTT_KEEPALIVE
#define MQTT_KEEPALIVE        30              // 30秒 (协议规范)
#define MQTT_RECONNECT_MIN    1000
#define MQTT_RECONNECT_MAX    60000

// ============================================================
// 5. MQTT 主题定义 (协议规范 §3)
// ============================================================

// --- 上行主题 (设备 → 服务端) ---
#define MQTT_TOPIC_REPORT_ALLPOINTS   "report/allpoints"                // 全量遥测上报 QoS0
#define MQTT_TOPIC_STATUS_ONLINE      "status/%s/online"                // 上线通知 QoS1
#define MQTT_TOPIC_STATUS_OFFLINE     "status/%s/offline"               // 离线遗嘱 QoS1
#define MQTT_TOPIC_STATUS_PROVISION   "status/%s/provision"             // 配网状态 QoS1
#define MQTT_TOPIC_RESPONSE_CMD       "response/%s/cmd"                 // 指令响应 QoS1
#define MQTT_TOPIC_RESPONSE_PROVISION "response/%s/provision"           // 配网响应 QoS1

// --- 下行主题 (服务端 → 设备) ---
#define MQTT_TOPIC_CMD_ALARM          "cmd/%s/alarm"                    // 告警指令 QoS1
#define MQTT_TOPIC_CMD_ALARM_RESTORE  "cmd/%s/alarm/restore"            // 告警恢复 QoS1


1	/**
2	 * @file mqtt_manager.cpp
3	 * @brief MQTT 通信管理器实现 (协议规范 V0.08)
4	 */
5
6	#include "mqtt_manager.h"
7
8	MQTTManager* MQTTManager::_instance = nullptr;
9
10	// ============================================================
11	// 构造函数 & 析构函数
12	// ============================================================
13
14	MQTTManager::MQTTManager()
15	    : _mqtt(_wifiClient)
16	    , _port(1883)
17	    , _reconnectInterval(MQTT_RECONNECT_MIN)
18	    , _lastReconnectAttempt(0)
19	    , _connected(false)
20	    , _alarmCb(nullptr)
21	    , _controlCb(nullptr)
22	    , _provisionCb(nullptr)
23	{
24	    memset(_gateid, 0, sizeof(_gateid));
25	    memset(_server, 0, sizeof(_server));
26	    memset(_user,   0, sizeof(_user));
27	    memset(_pass,   0, sizeof(_pass));
28	    _instance = this;
29	}
30

/**
 * @file mqtt_manager.cpp
 * @brief MQTT 通信管理器实现 (协议规范 V0.08)
 */

#include "mqtt_manager.h"

MQTTManager* MQTTManager::_instance = nullptr;

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

MQTTManager::MQTTManager()
    : _mqtt(_wifiClient)
    , _port(1883)
    , _reconnectInterval(MQTT_RECONNECT_MIN)
    , _lastReconnectAttempt(0)
    , _connected(false)
    , _alarmCb(nullptr)
    , _controlCb(nullptr)
    , _provisionCb(nullptr)
{
    memset(_gateid, 0, sizeof(_gateid));
    memset(_server, 0, sizeof(_server));
    memset(_user,   0, sizeof(_user));
    memset(_pass,   0, sizeof(_pass));
    _instance = this;
}


Good. I can see that the `MQTT_KEEPALIVE` issue has been addressed with `#undef MQTT_KEEPALIVE` before redefining it. And the `isConnected()` const issue was fixed by removing `const`.

Now let me try to build the project to see if there are any remaining compilation errors. The user wants to continue from the previous session where compilation errors were being fixed. Let me compile and see what we get.

Wait, first let me also check the `radar_protocol.h` and `radar_manager.h` since they're shown as modified in git status.


看起来上次的两个修复（`MQTT_KEEPALIVE` 的 `#undef` + 重定义，以及 `isConnected()` 去掉 `const`）已经应用了。让我尝试编译，看还有没有残留的错误。


Bash

cd "d:\PlatformIO1\Projects\Healthv2.0" && pio run 2>&1 | tail -80

Build PlatformIO project to check compilation errors


/usr/bin/bash: line 1: pio: command not found


The `pio` command is not available in Git Bash. Let me try using PowerShell since this is a Windows project, or find where PlatformIO is installed.


PowerShell

Build PlatformIO project to check compilation errors

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


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


不是，现在是蓝牙配网的问题


现在是蓝牙第一次配网行，断开重新配网就不行了找不到蓝牙信号


The user is clarifying that the current issue is about Bluetooth (BLE) provisioning - specifically:

1. First-time Bluetooth provisioning works fine
2. After disconnecting and trying to re-provision, the Bluetooth signal can't be found

This is a BLE (Bluetooth Low Energy) provisioning issue on an ESP32-S3 device. Let me look at the relevant code to understand the BLE provisioning flow.

Let me check:
- `wifi_manager.cpp` and `wifi_manager.h` - these likely contain the BLE/WiFi provisioning logic
- `main.cpp` - for the overall flow
- `wm_strings_zh.h` was deleted - this might be related to WiFi Manager strings

The issue is that after first successful BLE provisioning and disconnection, the device's BLE advertising doesn't restart properly for a second provisioning attempt.


明白了！编译已经通过了。问题是蓝牙配网：第一次可以配网，但断开后重新配网就找不到蓝牙信号了。让我看看相关的 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	// 设备配置结构体
30	struct DeviceConfig {
31	    char mqtt_server[40];
32	    char mqtt_port[6];
33	    char mqtt_user[20];
34	    char mqtt_pass[20];
35	    char device_id[20];
36	    char env_note[20];      // 环境备注 (房间名)
37	    char env_type[16];      // 环境类型 bedroom/bathroom/...
38	    char wifi_ssid[33];     // WiFi SSID
39	    char wifi_pass[65];     // WiFi 密码
40	};
41
42	class WiFiMgmt {
43	public:
44	    WiFiMgmt();
45	    ~WiFiMgmt();
46
47	    // 单例指针 (用于静态回调)
48	    static WiFiMgmt* _instance;
49
50	    bool begin();
51	    bool loop();
52	    bool isConnected() const;
53	    bool justProvisioned();     // 刚配网成功 (读取后自动清除)
54	    DeviceConfig getConfig() const;
55	    const char* getDeviceId() const;
56	    const char* getEnvNote() const;
57	    void startConfigPortal();
58	    void handleBLEProvision(const char* json);  // BLE 配网回调
59	    void enqueueBLEJson(const char* json);       // BLE 线程安全入队 (仅暂存)
60	    void resetSettings();
61
62	private:
63	    bool _connectWiFi();          // 仅尝试已保存凭据
64	    bool _tryConnect(const char* ssid, const char* pass);  // 尝试连接指定 WiFi
65	    bool _loadConfig();
66	    bool _saveConfig();
67	    void _setLEDMode(uint8_t mode);
68	    static void _ledTaskFunc(void* param);
69	    bool _checkButtonLongPress();
70
71	    // Web 服务器
72	    void _startPortal();
73	    void _stopPortal();
74	    void _handleRoot();
75	    void _handleSave();
76	    void _handleReset();
77	    void _updateWifiScan();    // WiFi 扫描缓存
78	    String _htmlPage();        // 返回配网 HTML 页面
79
80	    // BLE 蓝牙配网
81	    void _startBLE();
82	    void _stopBLE();
83
84	    WebServer*     _server;
85	    DNSServer*     _dns;
86	    DeviceConfig   _config;
87	    bool           _connected;
88	    bool           _portalActive;
89	    bool           _portalStopFlag;     // 延迟关闭标记
90	    uint32_t       _portalStartTime;
91	    String         _pendingSSID;
92	    String         _pendingPass;
93	    bool           _provisionSuccess;  // 刚配网成功标记
94	    uint32_t       _lastCheckTime;
95	    uint32_t       _buttonPressTime;
96	    bool           _buttonPressed;
97
98	    // WiFi 扫描缓存 (避免每次打开页面都扫描3-8秒)
99	    String         _wifiScanCache;
100	    uint32_t       _lastScanTime;
101
102	    // LED 独立任务
103	    TaskHandle_t    _ledTaskHandle;
104	    volatile uint8_t _ledMode;  // 0=灭 1=慢闪 2=快闪 3=常亮
105
106	    // BLE
107	    BLEServer*           _bleServer;
108	    BLECharacteristic*   _bleInfoChar;   // READ: 设备信息
109	    BLECharacteristic*   _bleChar;       // WRITE+NOTIFY: 配网
110	    BLEAdvertising*      _bleAdv;
111
112	    // BLE 回调保护: 不在 BTC_TASK 栈里做 Flash 写操作
113	    String        _blePendingJson;      // 待处理的 BLE 配网 JSON
114	    bool          _blePendingFlag;      // 有未处理的 BLE 数据
115	    void _processBLEPending();          // 在 loop() 上下文处理
116	};
117
118	#endif // WIFI_MANAGER_H
119

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

#ifndef WIFI_MANAGER_H
#define WIFI_MANAGER_H

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

// 设备配置结构体
struct DeviceConfig {
    char mqtt_server[40];
    char mqtt_port[6];
    char mqtt_user[20];
    char mqtt_pass[20];
    char device_id[20];
    char env_note[20];      // 环境备注 (房间名)
    char env_type[16];      // 环境类型 bedroom/bathroom/...
    char wifi_ssid[33];     // WiFi SSID
    char wifi_pass[65];     // WiFi 密码
};

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

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

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

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

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

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

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

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

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

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

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

#endif // WIFI_MANAGER_H


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

⏳
"
284	            "
配置已保存

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

配置已清除

设备正在重启...

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

📡

观照-五劳无感 设备配网

WiFi 联网与参数配置

";
353	    html += "
💡 也可用 ESP-Touch App 一键配网
";
354
355	    html += "<form method='POST' action='/save'>";
356	    html += "

📶

无线网络

选择 WiFi 或使用 SmartConfig

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

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

";
361
362	    html += "

⚙

设备基础信息

唯一标识与安装位置

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

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

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

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

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

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

#include "wifi_manager.h"

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

WiFiMgmt* WiFiMgmt::_instance = nullptr;

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

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

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

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

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

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

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

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

    WiFi.setAutoReconnect(true);

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

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

    _lastCheckTime = millis();
    return _connected;
}

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

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

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

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

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

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

    return _connected;
}

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

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

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

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

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

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

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

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

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

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

    _portalActive = true;
    _portalStartTime = millis();

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

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

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

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

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

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

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

⏳
"
            "
配置已保存

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

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

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

配置已清除

设备正在重启...

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

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

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

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

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

    // Header + 提示
    html += "

📡

观照-五劳无感 设备配网

WiFi 联网与参数配置

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

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

📶

无线网络

选择 WiFi 或使用 SmartConfig

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

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

";

    html += "

⚙

设备基础信息

唯一标识与安装位置

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

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

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

";

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

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

";

    return html;
}

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

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

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

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

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

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

    svc->start();

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

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

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

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

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

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

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

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

    _pendingSSID = ssid;
    _pendingPass = pass;

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

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

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

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

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

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

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

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

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

// ============================================================
// LittleFS
// ============================================================

bool WiFiMgmt::_loadConfig() {
    if (!LittleFS.exists(CONFIG_FILE_DEVICE)) return false;
    File file = LittleFS.open(CONFIG_FILE_DEVICE, "r");
    if (!file) return false;
    JsonDocument doc;
    DeserializationError err = deserializeJson(doc, file);
    file.close();
    if (err) return false;
    strncpy(_config.mqtt_server, doc["mqtt_server"] | MQTT_DEFAULT_SERVER, sizeof(_config.mqtt_server)-1);
    strncpy(_config.mqtt_port, doc["mqtt_port"] | MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port)-1);
    strncpy(_config.mqtt_user, doc["mqtt_user"] | MQTT_DEFAULT_USER, sizeof(_config.mqtt_user)-1);
    strncpy(_config.mqtt_pass, doc["mqtt_pass"] | MQTT_DEFAULT_PASS, sizeof(_config.mqtt_pass)-1);
    strncpy(_config.device_id, doc["device_id"] | MQTT_DEFAULT_DEVICE, sizeof(_config.device_id)-1);
    strncpy(_config.env_note, doc["env_note"] | MQTT_DEFAULT_ENV, sizeof(_config.env_note)-1);
    strncpy(_config.env_type, doc["env_type"] | "bedroom", sizeof(_config.env_type)-1);
    strncpy(_config.wifi_ssid, doc["wifi_ssid"] | "", sizeof(_config.wifi_ssid)-1);
    strncpy(_config.wifi_pass, doc["wifi_pass"] | "", sizeof(_config.wifi_pass)-1);
    Serial.printf("[WiFi] 配置加载, 已存WiFi: %s\n", _config.wifi_ssid[0] ? _config.wifi_ssid : "(空)");
    return true;
}

bool WiFiMgmt::_saveConfig() {
    JsonDocument doc;
    doc["mqtt_server"] = _config.mqtt_server;
    doc["mqtt_port"]   = _config.mqtt_port;
    doc["mqtt_user"]   = _config.mqtt_user;
    doc["mqtt_pass"]   = _config.mqtt_pass;
    doc["device_id"]   = _config.device_id;
    doc["env_note"]    = _config.env_note;
    doc["env_type"]    = _config.env_type;
    doc["wifi_ssid"]   = _config.wifi_ssid;
    doc["wifi_pass"]   = _config.wifi_pass;
    File file = LittleFS.open(CONFIG_FILE_DEVICE, "w");
    if (!file) return false;
    if (serializeJson(doc, file) == 0) { file.close(); return false; }
    file.close();
    return true;
}

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

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

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

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

void WiFiMgmt::_ledTaskFunc(void* param) {
    const TickType_t fastMs = pdMS_TO_TICKS(300);
    const TickType_t slowMs = pdMS_TO_TICKS(1000);
    while (true) {
        if (!_instance) { vTaskDelay(slowMs); continue; }
        uint8_t mode = _instance->_ledMode;
        switch (mode) {
        case 0: digitalWrite(LED_WIFI_PIN, LOW); vTaskDelay(slowMs); break;
        case 1: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(slowMs);
                digitalWrite(LED_WIFI_PIN, LOW);  vTaskDelay(slowMs); break;
        case 2: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(fastMs);
                digitalWrite(LED_WIFI_PIN, LOW);  vTaskDelay(fastMs); break;
        case 3: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(slowMs); break;
        }
    }
}


Now I understand the BLE provisioning issue. Let me analyze the flow:

1. **First-time provisioning works**: `_startBLE()` is called from `_startPortal()`. It initializes BLE with `BLEDevice::init("HealthMonitor")`, creates the server, service, characteristics, and starts advertising.

2. **After provisioning**: When BLE provisioning completes (`handleBLEProvision`), it sets `_portalStopFlag = true`, which triggers `_stopPortal()` → `_stopBLE()`. The `_stopBLE()` method stops advertising but doesn't fully de-initialize BLE.

3. **Second-time provisioning fails**: When the user tries to re-provision (e.g., long press button → `_startPortal()` → `_startBLE()`), the `_startBLE()` method checks if `_bleServer != nullptr`. If the server still exists, it tries to restart advertising. BUT... this is the problem!

Looking at `_startBLE()`:
```cpp
void WiFiMgmt::_startBLE() {
    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;
    }
    // ... full init
}
```

The issue is that `_stopBLE()` only stops advertising:
```cpp
void WiFiMgmt::_stopBLE() {
    if (!_bleServer) return;
    if (_bleAdv) { _bleAdv->stop(); }
    Serial.println("[BLE] 广播已暂停");
}
```

But after `_stopPortal()` is called, the BLE server is still in memory (`_bleServer != nullptr`). When `_startBLE()` is called again, it takes the "restart advertising" path...

However, there's a subtle issue. When `_stopPortal()` is called:
1. It calls `_stopBLE()` — stops advertising
2. Deletes `_server` and `_dns`
3. Calls `WiFi.softAPdisconnect(true)` — kills soft AP
4. Sets `_portalActive = false`

Then WiFi mode switches to STA and connection starts.

When re-entering portal mode:
1. `_startPortal()` switches to WIFI_AP mode
2. Calls `_startBLE()` — but `_bleServer` is not null...

The issue might be more nuanced. Let me think about what happens after first BLE provisioning:

1. BLE data received → `handleBLEProvision()` sets `_portalStopFlag = true`
2. In loop(): `_portalStopFlag` triggers `_stopPortal()` → `_stopBLE()` — just stops advertising
3. Then tries to connect WiFi with `_tryConnect()`
4. If successful: `_connected = true`, LED mode 3
5. If fails: re-enters portal mode → `_startPortal()` → `_startBLE()`

In the success case, the BLE server object is still allocated but advertising is stopped. WiFi is connected. The device is working.

Later, when user long-presses button:
1. `_checkButtonLongPress()` returns true
2. `WiFi.disconnect()`
3. `_startPortal()` → switches to WIFI_AP mode → calls `_startBLE()`
4. `_startBLE()` sees `_bleServer != nullptr`, only restarts advertising

Wait — there's a critical problem! After WiFi is connected and we switch BACK to AP mode:
- In `_startPortal()`: `WiFi.mode(WIFI_AP)` is called — this changes from STA mode (or STA+AP) to AP-only mode
- But WiFi might have been in STA mode (connected). Switching to AP-only kills the STA connection, OK.

The real BLE issue: After `_stopPortal()` is called, `_bleServer` remains non-null. Then when `_startBLE()` is called again, it enters the "already initialized" branch. BUT...

Actually, when WiFi is in STA mode and connected, the BLE server object should still be valid. The issue might be with the BLE stack state after mode switching.

Wait, let me re-read the problem description more carefully: "蓝牙第一次配网行，断开重新配网就不行了找不到蓝牙信号" — First time BLE provisioning works. After disconnecting, re-provisioning doesn't work — can't find the BLE signal.

So the sequence is:
1. First time: BLE advertising works → phone finds it → provisions successfully
2. Device connects to WiFi, operates normally
3. User wants to re-provision (maybe long-press button, or WiFi disconnects)
4. Device tries to start BLE advertising again
5. Phone can't find the BLE signal

Now let me think about what could cause this...

The `_startBLE()` method has a code path for when `_bleServer != nullptr`:
```cpp
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;
}
```

This is trying to re-use the existing BLE server. But there might be issues:
1. The BLE server's state might be stale after WiFi mode switches
2. `BLEDevice::init()` is called only once per boot on ESP32 — calling it again would fail, so the code is correctly avoiding re-init
3. But the advertising data set via `_bleAdv->setAdvertisementData()` might not be properly refreshed

Actually, the more likely issue is that after `_stopBLE()` is called (`_bleAdv->stop()`), and then WiFi switches to STA mode and back to AP mode, the BLE hardware might need a full reset.

But wait, there's another issue: In `_stopBLE()`, `_bleAdv->stop()` is called but the `_bleServer` object is NOT deleted. So when `_startBLE()` is called again, it tries to reuse the same server. This could be problematic because:

1. On ESP32, BLE and WiFi share the same radio. Switching between modes can cause BLE state issues.
2. The BLE server might not properly restart advertising after being stopped, especially after WiFi mode changes.

The fix should be to properly de-initialize and re-initialize BLE when restarting. But `BLEDevice::init()` can only be called once per boot on ESP32...

Actually, let me look at this from a different angle. The real problem might be simpler:

When `_stopPortal()` is called:
```cpp
void WiFiMgmt::_stopPortal() {
    if (!_portalActive) return;
    _stopBLE();   // stops advertising
    if (_server) { _server->stop(); delete _server; _server = nullptr; }
    if (_dns)    { _dns->stop();    delete _dns;    _dns = nullptr; }
    WiFi.softAPdisconnect(true);  // kills AP
    _portalActive = false;
}
```

`_stopBLE()` only stops the advertisement. But when the WiFi mode changes from AP to STA (for connection), and then back to AP (for re-provisioning), the BLE stack might need a complete reset.

Actually wait, I just noticed something even more fundamental. When `_startPortal()` is called again, it does:
```cpp
WiFi.mode(WIFI_AP);
```

This switches to AP-only mode. In AP-only mode, BLE should still work because ESP32 supports BLE + WiFi AP coexistence. But when the device was previously in STA mode (connected to WiFi), the BLE was still stopped. Switching to AP mode and then calling `_startBLE()` should work...

Let me think about this differently. Maybe the issue is that `_bleAdv->setAdvertisementData()` is being called but it doesn't take effect because the BLE server/GAP state needs to be properly reconfigured.

Or... maybe the problem is that `_bleAdv->start()` is failing silently. After stopping advertising and switching WiFi modes, the BLE stack might need a small delay or the advertising object might need to be re-created.

Actually, I think the most reliable fix would be to completely clean up and recreate the BLE server when re-entering portal mode. The concern about `BLEDevice::init()` being called only once is valid, but `BLEDevice::deinit()` can be called and then `BLEDevice::init()` can be called again. However, this is also risky because it can conflict with the WiFi stack.

Let me look at this more practically. The current approach is:
1. Keep `_bleServer` alive across portal sessions
2. Just restart advertising

But the problem might be that after `_bleAdv->stop()`, the advertising data is cleared or the advertising object is in a bad state. The fix should be:

**Option A**: In `_stopBLE()`, properly delete and null all BLE objects, then in `_startBLE()` reinitialize everything (but skip `BLEDevice::init()` if already called, use `BLEDevice::deinit()` + re-`init()` properly)

**Option B**: In `_startBLE()`, when `_bleServer != nullptr`, do a more thorough restart: `_bleAdv->stop()`, recreate the advertising data, maybe even recreate the service/characteristics.

Actually, I think the most robust approach is:

In `_stopBLE()`, fully clean up:
```cpp
void WiFiMgmt::_stopBLE() {
    if (_bleAdv) { _bleAdv->stop(); _bleAdv = nullptr; }
    if (_bleServer) {
        // Remove service etc.
        delete _bleServer;
        _bleServer = nullptr;
    }
    _bleInfoChar = nullptr;
    _bleChar = nullptr;
}
```

Then in `_startBLE()`, always do full initialization. But we can't call `BLEDevice::init()` again...

Hmm, actually on ESP32 Arduino BLE, `BLEDevice::init()` can only be called once. Calling it again will fail (or crash). But `BLEDevice::deinit()` was added in some versions to allow re-initialization.

Let me think about what the best approach is for this project.

Actually, the simplest and most reliable fix might be to just restart the ESP32 when re-entering provisioning mode, or to do a more thorough cleanup of BLE resources. But restarting is too heavy-handed.

Let me re-read the code flow more carefully.

When entering portal mode for the SECOND time (after a successful first provision):

The user can enter portal mode in two ways:
1. WiFi disconnected → `_startPortal()` is called from `begin()` or `loop()`
2. Long press button → `_startPortal()` is called

In `loop()`:
```cpp
if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {
    // ... checks WiFi status ...
    // does NOT automatically restart portal on disconnect
}
```

Wait, looking more carefully at `loop()`, when WiFi disconnects:
- `_connected` becomes false
- LED mode becomes 1 (slow flash)
- But portal is NOT restarted!

So to re-enter portal mode, the user must long-press the button. Let me trace that flow:

1. Long press detected
2. `WiFi.disconnect()`
3. `_startPortal()` called
4. `_startPortal()`:
   - `WiFi.disconnect(true)`
   - `WiFi.mode(WIFI_AP)`
   - `WiFi.softAP(...)`
   - Creates DNS, WebServer
   - `_startBLE()` ← THIS IS WHERE IT FAILS

In `_startBLE()`:
- `_bleServer != nullptr` (it was never deleted)
- So it enters the "restart advertising" branch
- Tries `_bleAdv->start()`

The issue: After the BLE advertisement was previously stopped AND WiFi switched from AP→STA→AP, the BLE advertising might not restart properly. The old `_bleAdv` object might be in a bad state, or the BLE stack needs a more thorough refresh.

**My proposed fix:**

In `_stopBLE()`, do a more thorough cleanup by also deleting the BLE server and nulling pointers:

```cpp
void WiFiMgmt::_stopBLE() {
    if (_bleAdv) { _bleAdv->stop(); }
    if (_bleServer) {
        // Get the service to clean up
        // Actually, deleting the server should clean up everything
        delete _bleServer;
        _bleServer = nullptr;
    }
    _bleAdv = nullptr;
    _bleInfoChar = nullptr;
    _bleChar = nullptr;
    Serial.println("[BLE] 广播已停止");
}
```

But then `_startBLE()` needs to do a full init. However, `BLEDevice::init()` can only be called once...

Let me check: does ESP32 Arduino BLE support calling `init()` multiple times?

Actually, looking at ESP32 BLE Arduino library source code, `BLEDevice::init()` checks if it's already initialized and returns if so. But that means after we delete the server, the BLE device is still "initialized" in the library's static state. We should be able to create a new `BLEServer` via `BLEDevice::createServer()` without re-initializing.

But wait, there might be a problem: once `BLEServer` is deleted, can we create a new one? Let me check...

In the ESP32 BLE library, `BLEDevice::createServer()` creates a new `BLEServer` object and registers it. If the old one was deleted, a new one should be creatable. The concern is whether the underlying NimBLE/Bluedroid stack properly cleans up.

Actually, I think the issue might be simpler. Let me re-examine `_startBLE()`:

```cpp
void WiFiMgmt::_startBLE() {
    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;
    }
    // ... full init
}
```

The problem: When `_bleAdv->start()` is called, it might fail because the underlying stack needs the advertising to be properly re-initialized. After `stop()`, calling `setAdvertisementData()` and `start()` might not be enough on the ESP32 — especially after WiFi mode has changed.

Also, there's another potential issue: `_bleAdv` is obtained from `_bleServer->getAdvertising()`. If the server has been stopped, the advertising object might not be in a valid state to restart.

I think the best approach is:

1. In `_stopBLE()`, also delete the server (not just stop advertising)
2. In `_startBLE()`, always do full initialization (create new server, service, characteristics)
3. `BLEDevice::init()` only needs to be called once — we can call `BLEDevice::createServer()` multiple times

Let me write the fix:

```cpp
void WiFiMgmt::_stopBLE() {
    if (_bleAdv) { _bleAdv->stop(); _bleAdv = nullptr; }
    if (_bleChar) { _bleChar = nullptr; }
    if (_bleInfoChar) { _bleInfoChar = nullptr; }
    if (_bleServer) { delete _bleServer; _bleServer = nullptr; }
    Serial.println("[BLE] 已完全停止");
}

void WiFiMgmt::_startBLE() {
    // Always do full init if server doesn't exist
    if (_bleServer == nullptr) {
        // ... full init including BLEDevice::init("HealthMonitor") ...
    } else {
        // Just restart advertising (this path should now only happen if server was never stopped)
        // Or better: force full re-creation
    }
}
```

Wait, but `BLEDevice::init("HealthMonitor")` can only be called once. If we always go through the full init path, we'll call it again on the second entry to `_startBLE()`.

Let me check if there's a way to call `BLEDevice::init()` multiple times. On ESP32 Arduino BLE, `BLEDevice::init()` stores the device name and calls `esp_bt_controller_init()` + `esp_bluedroid_init()` etc. These can only be called once (calling again would fail).

So the approach should be:
1. Only call `BLEDevice::init()` once (keep track with a flag or check if it's already been called)
2. Always create a new `BLEServer` in `_startBLE()`

Let me revise:

```cpp
// Add a static flag
static bool _bleInitialized = false;

void WiFiMgmt::_startBLE() {
    if (!_bleInitialized) {
        BLEDevice::init("HealthMonitor");
        _bleInitialized = true;
    }

    _bleServer = BLEDevice::createServer();
    // ... create service, characteristics, start advertising ...
}

void WiFiMgmt::_stopBLE() {
    if (_bleAdv) { _bleAdv->stop(); _bleAdv = nullptr; }
    if (_bleChar) { _bleChar = nullptr; }
    if (_bleInfoChar) { _bleInfoChar = nullptr; }
    if (_bleServer) { delete _bleServer; _bleServer = nullptr; }
    Serial.println("[BLE] 已完全停止");
}
```

Wait, but `delete _bleServer` might have issues. Let me think...

In ESP32 BLE Arduino, `BLEServer` destructor calls `BLEDevice::removeServer()` which should handle cleanup. Then creating a new one should work. But the BLE callbacks are attached to characteristics, and deleting the characteristics might cause issues if callbacks are still referenced.

Actually, let me look at this from a simpler angle. Maybe the issue is even more basic.

When `_startBLE()` is called for the second time:
```cpp
if (_bleServer != nullptr) {
    if (_bleAdv) {
        _bleAdv->start();
    }
}
```

This calls `_bleAdv->start()` — but `_bleAdv` is the SAME advertising object from the first time. After `_bleAdv->stop()`, the advertising might not restart properly.

The simplest fix: get a fresh advertising object from the server:
```cpp
if (_bleServer != nullptr) {
    _bleAdv = _bleServer->getAdvertising();  // Get fresh advertising object
    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;
}
```

Hmm, but `getAdvertising()` might return the same object...

Actually, I think the most reliable approach is to fully recreate the BLE server. Let me implement this.

Let me also check what happens in `_stopPortal()` in the loop flow. After `_stopPortal()` is called (via `_portalStopFlag = true`):

```cpp
if (_portalStopFlag) {
    _stopPortal();
    _portalStopFlag = false;
    if (_pendingSSID.length() > 0) {
        _connected = _tryConnect(...);
        if (_connected) {
            _setLEDMode(3);
            _provisionSuccess = true;
        } else {
            _setLEDMode(2);
            _startPortal();  // ← This re-enters portal mode if connection fails!
        }
    }
    return _connected;
}
```

So if WiFi connection fails, it calls `_startPortal()` again, which calls `_startBLE()`. At this point, the BLE server was just stopped (via `_stopPortal()` → `_stopBLE()`), so `_bleServer` is still non-null. Then `_startBLE()` tries to restart advertising on the old server.

OK so here's my final analysis and fix plan:

**Root Cause**: When `_stopBLE()` is called, it only stops advertising but doesn't clean up the BLE server. When `_startBLE()` is called again, it tries to restart advertising on the stale BLE server object, which fails because:
1. The BLE stack may have been affected by WiFi mode changes (AP → STA → AP)
2. The advertising object is in a stopped state and may not restart properly

**Fix**:
1. In `_stopBLE()`, fully clean up BLE objects (server, characteristics, advertising)
2. In `_startBLE()`, always recreate the BLE server (but only call `BLEDevice::init()` once)

Let me implement this now.

Actually, wait. Let me also consider: does calling `BLEDevice::init()` a second time actually crash or just silently return? If it silently returns, we can just call it every time in `_startBLE()` and it would be safe.

Looking at the ESP32 BLE Arduino library code for `BLEDevice::init()` — I believe it calls the underlying init functions which will fail if already initialized, but the library might handle this gracefully. However, I'm not 100% sure, so it's safer to use a flag.

Actually, a much simpler approach: call `BLEDevice::deinit(true)` before re-initializing. This function exists in newer versions of the ESP32 BLE library and properly shuts down the BLE stack. Then `BLEDevice::init()` can be called again.

But `deinit` might also cause issues if WiFi is using the radio...

OK, I think the safest and most practical fix is:

1. In `_stopBLE()`, properly clean up by deleting the server and nulling all BLE pointers
2. In `_startBLE()`, if `_bleServer` is null, do full init. If it's not null (should no longer happen after the fix), still recreate it.
3. Add a static flag or check to track whether `BLEDevice::init()` has been called.

Let me write the code.


现在我看到了 BLE 配网的代码。让我分析问题的根因。

**问题分析：**

在 `_stopBLE()` 中（第463-467行），只停止了广播但没有清理 `_bleServer` 对象。当第二次进入配网模式时，`_startBLE()` 检测到 `_bleServer != nullptr`（第407行），就跳过完整初始化，仅重启旧的 `_bleAdv`。但在 WiFi 模式经过 AP→STA→AP 切换后，旧的 BLE 广播对象已处于不健康状态，`start()` 无法正常工作。

**修复方案：** `_stopBLE()` 彻底清理 BLE 对象，`_startBLE()` 每次重新创建 Server。


Edit

void WiFiMgmt::_stopBLE() {
    if (!_bleServer) return;
    if (_bleAdv) { _bleAdv->stop(); }
    Serial.println("[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] 已完全停止");
}


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::_stopBLE() {
    if (!_bleServer) return;
    if (_bleAdv) { _bleAdv->stop(); }
    Serial.println("[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] 已完全停止");
}

+    // 彻底释放 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; }

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