    _wm.setTitle("🏥 五劳无感康养监测 - 设备配网");  // 配网页面的中文标题
    _wm.setCustomHeadElement(
        "<style>"
        "body { font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif; }"
        ".msg .info { font-size: 14px; }"
        "</style>"
    );
    WiFi.setAutoReconnect(true);  // ESP32 自动重连

    // 注册保存回调 (必须使用静态函数, 不能使用 lambda)
    _wm.setSaveConfigCallback(_saveParamsCallback);

    // 创建自定义参数
    _param_mqtt_server = new WiFiManagerParameter(
        "mqtt_server", "MQTT 服务器地址",
        _config.mqtt_server, 40);
    _param_mqtt_port = new WiFiManagerParameter(
        "mqtt_port", "MQTT 端口",
        _config.mqtt_port, 6);
    _param_mqtt_user = new WiFiManagerParameter(
        "mqtt_user", "MQTT 用户名",
        _config.mqtt_user, 20);
    _param_mqtt_pass = new WiFiManagerParameter(
        "mqtt_pass", "MQTT 密码",
        _config.mqtt_pass, 20);
    _param_device_id = new WiFiManagerParameter(
        "device_id", "设备名称",
        _config.device_id, 20);
    _param_env_note = new WiFiManagerParameter(
        "env_note", "🏠 环境备注 (如: 主卧/客厅/老人房)",
        _config.env_note, 20);

    // 注册自定义参数
    _wm.addParameter(_param_mqtt_server);
    _wm.addParameter(_param_mqtt_port);
    _wm.addParameter(_param_mqtt_user);
    _wm.addParameter(_param_mqtt_pass);
    _wm.addParameter(_param_device_id);
    _wm.addParameter(_param_env_note);

    _wm.setTitle("观照-五劳无感 · 设备配网");
    _wm.setCustomHeadElement(
        "<meta name='theme-color' content='#12a889'>"
        "<style>"
        ":root{--color-primary:#12a889;--color-primary-light:#35c9aa;--color-primary-dark:#08745f;--color-success:#00b42a;--color-warning:#ff7d00;--color-error:#f53f3f;--color-bg-page:#f3faf8;--color-bg-card:#ffffff;--color-bg-hover:#eef8f5;--color-card-green:#e9fff8;--color-text-primary:#15201d;--color-text-secondary:#4e6660;--color-text-tertiary:#8aa09a;--color-border:#dcebe7;--color-divider:#edf5f2;}"
        "*{box-sizing:border-box;margin:0;padding:0}"
        "body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'PingFang SC','Hiragino Sans GB','Microsoft YaHei',sans-serif;background:var(--color-bg-page);min-height:100vh;padding:12px 16px 24px;color:var(--color-text-primary);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;}"
        ".wrap{max-width:420px;margin:0 auto;}"
        "h1{font-size:20px;font-weight:700;color:var(--color-text-primary);letter-spacing:0.3px;}"
        "h3{font-size:12px;font-weight:400;color:var(--color-text-secondary);margin-top:4px;}"
        ".card{background:var(--color-bg-card);border-radius:16px;padding:20px;margin-bottom:16px;box-shadow:0 2px 12px rgba(0,0,0,0.06);}"
        "label{display:block;font-size:14px;font-weight:500;color:var(--color-text-secondary);margin:14px 0 8px;}"
        "input,select{flex:1;width:100%;padding:12px 14px;font-size:14px;border:1.5px solid #dfe6e9;border-radius:10px;background:#fafafa;color:var(--color-text-primary);outline:none;font-family:inherit;transition:border-color 0.2s;}"
        "input:focus,select:focus{border-color:var(--color-primary);background:#fff;box-shadow:0 0 0 3px rgba(18,168,137,0.1);}"
        "input::placeholder{color:var(--color-text-tertiary);}"
        "button{width:100%;height:48px;border:none;border-radius:24px;font-size:15px;font-weight:600;color:#fff;background:var(--color-primary);box-shadow:0 2px 6px rgba(18,168,137,0.2);cursor:pointer;transition:transform 0.15s,opacity 0.15s;font-family:inherit;margin-top:8px;}"
        "button:active{transform:scale(0.98);opacity:0.92;}"
        "button.D{background:#fff;color:var(--color-error);border:1px solid #ffd6d6;box-shadow:none;}"
        ".msg{padding:16px;margin:12px 0;border-radius:12px;font-size:14px;line-height:1.6;}"
        ".msg.S{background:#e9fff8;color:#08745f;border-left:4px solid var(--color-primary);}"
        ".msg.D{background:#fff0f0;color:#c0392b;border-left:4px solid var(--color-error);}"
        ".q{display:inline-block;vertical-align:middle;}"
        "a{color:var(--color-primary);font-weight:600;text-decoration:none;}"
        "a:hover{text-decoration:underline;}"
        ".footer{text-align:center;font-size:11px;color:var(--color-text-tertiary);padding:20px 0 6px;}"
        "@media(prefers-color-scheme:dark){body{background:#0f1f1c;color:#e6f0ee;}.card{background:#182b27;box-shadow:0 2px 12px rgba(0,0,0,0.4);}label{color:#a8bdb8;}h1{color:#e6f0ee;}input,select{background:#1a1a2e;border-color:#444;color:#eee;}input:focus,select:focus{background:#243d38;border-color:var(--color-primary-light);}button.D{background:#2a1a1a;color:#ff8a8a;border-color:#5a2828;}.msg.S{background:#143530;color:#35c9aa;}.msg.D{background:#2a1a1a;color:#ff8a8a;}}"
        "</style>"
    );
    WiFi.setAutoReconnect(true);
    _wm.setSaveConfigCallback(_saveParamsCallback);

    // 页面显示参数
    _param_device_id = new WiFiManagerParameter(
        "device_id", "设备编号", _config.device_id, 32);
    _param_env_note = new WiFiManagerParameter(
        "env_note", "安装环境", _config.env_note, 32);
    _wm.addParameter(_param_device_id);
    _wm.addParameter(_param_env_note);

    // MQTT 参数不显示, 内部保持默认值
    _param_mqtt_server = new WiFiManagerParameter("mqtt_server", "", _config.mqtt_server, 40);
    _param_mqtt_port   = new WiFiManagerParameter("mqtt_port",   "", _config.mqtt_port,   6);
    _param_mqtt_user   = new WiFiManagerParameter("mqtt_user",   "", _config.mqtt_user,  20);
    _param_mqtt_pass   = new WiFiManagerParameter("mqtt_pass",   "", _config.mqtt_pass,  20);


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)

    _wm.setTitle("🏥 五劳无感康养监测 - 设备配网");  // 配网页面的中文标题
    _wm.setCustomHeadElement(
        "<style>"
        "body { font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif; }"
        ".msg .info { font-size: 14px; }"
        "</style>"
    );
    WiFi.setAutoReconnect(true);  // ESP32 自动重连

    // 注册保存回调 (必须使用静态函数, 不能使用 lambda)
    _wm.setSaveConfigCallback(_saveParamsCallback);

    // 创建自定义参数
    _param_mqtt_server = new WiFiManagerParameter(
        "mqtt_server", "MQTT 服务器地址",
        _config.mqtt_server, 40);
    _param_mqtt_port = new WiFiManagerParameter(
        "mqtt_port", "MQTT 端口",
        _config.mqtt_port, 6);
    _param_mqtt_user = new WiFiManagerParameter(
        "mqtt_user", "MQTT 用户名",
        _config.mqtt_user, 20);
    _param_mqtt_pass = new WiFiManagerParameter(
        "mqtt_pass", "MQTT 密码",
        _config.mqtt_pass, 20);
    _param_device_id = new WiFiManagerParameter(
        "device_id", "设备名称",
        _config.device_id, 20);
    _param_env_note = new WiFiManagerParameter(
        "env_note", "🏠 环境备注 (如: 主卧/客厅/老人房)",
        _config.env_note, 20);

    // 注册自定义参数
    _wm.addParameter(_param_mqtt_server);
    _wm.addParameter(_param_mqtt_port);
    _wm.addParameter(_param_mqtt_user);
    _wm.addParameter(_param_mqtt_pass);
    _wm.addParameter(_param_device_id);
    _wm.addParameter(_param_env_note);

    _wm.setTitle("观照-五劳无感 · 设备配网");
    _wm.setCustomHeadElement(
        "<meta name='theme-color' content='#12a889'>"
        "<style>"
        ":root{--color-primary:#12a889;--color-primary-light:#35c9aa;--color-primary-dark:#08745f;--color-success:#00b42a;--color-warning:#ff7d00;--color-error:#f53f3f;--color-bg-page:#f3faf8;--color-bg-card:#ffffff;--color-bg-hover:#eef8f5;--color-card-green:#e9fff8;--color-text-primary:#15201d;--color-text-secondary:#4e6660;--color-text-tertiary:#8aa09a;--color-border:#dcebe7;--color-divider:#edf5f2;}"
        "*{box-sizing:border-box;margin:0;padding:0}"
        "body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'PingFang SC','Hiragino Sans GB','Microsoft YaHei',sans-serif;background:var(--color-bg-page);min-height:100vh;padding:12px 16px 24px;color:var(--color-text-primary);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;}"
        ".wrap{max-width:420px;margin:0 auto;}"
        "h1{font-size:20px;font-weight:700;color:var(--color-text-primary);letter-spacing:0.3px;}"
        "h3{font-size:12px;font-weight:400;color:var(--color-text-secondary);margin-top:4px;}"
        ".card{background:var(--color-bg-card);border-radius:16px;padding:20px;margin-bottom:16px;box-shadow:0 2px 12px rgba(0,0,0,0.06);}"
        "label{display:block;font-size:14px;font-weight:500;color:var(--color-text-secondary);margin:14px 0 8px;}"
        "input,select{flex:1;width:100%;padding:12px 14px;font-size:14px;border:1.5px solid #dfe6e9;border-radius:10px;background:#fafafa;color:var(--color-text-primary);outline:none;font-family:inherit;transition:border-color 0.2s;}"
        "input:focus,select:focus{border-color:var(--color-primary);background:#fff;box-shadow:0 0 0 3px rgba(18,168,137,0.1);}"
        "input::placeholder{color:var(--color-text-tertiary);}"
        "button{width:100%;height:48px;border:none;border-radius:24px;font-size:15px;font-weight:600;color:#fff;background:var(--color-primary);box-shadow:0 2px 6px rgba(18,168,137,0.2);cursor:pointer;transition:transform 0.15s,opacity 0.15s;font-family:inherit;margin-top:8px;}"
        "button:active{transform:scale(0.98);opacity:0.92;}"
        "button.D{background:#fff;color:var(--color-error);border:1px solid #ffd6d6;box-shadow:none;}"
        ".msg{padding:16px;margin:12px 0;border-radius:12px;font-size:14px;line-height:1.6;}"
        ".msg.S{background:#e9fff8;color:#08745f;border-left:4px solid var(--color-primary);}"
        ".msg.D{background:#fff0f0;color:#c0392b;border-left:4px solid var(--color-error);}"
        ".q{display:inline-block;vertical-align:middle;}"
        "a{color:var(--color-primary);font-weight:600;text-decoration:none;}"
        "a:hover{text-decoration:underline;}"
        ".footer{text-align:center;font-size:11px;color:var(--color-text-tertiary);padding:20px 0 6px;}"
        "@media(prefers-color-scheme:dark){body{background:#0f1f1c;color:#e6f0ee;}.card{background:#182b27;box-shadow:0 2px 12px rgba(0,0,0,0.4);}label{color:#a8bdb8;}h1{color:#e6f0ee;}input,select{background:#1a1a2e;border-color:#444;color:#eee;}input:focus,select:focus{background:#243d38;border-color:var(--color-primary-light);}button.D{background:#2a1a1a;color:#ff8a8a;border-color:#5a2828;}.msg.S{background:#143530;color:#35c9aa;}.msg.D{background:#2a1a1a;color:#ff8a8a;}}"
        "</style>"
    );
    WiFi.setAutoReconnect(true);
    _wm.setSaveConfigCallback(_saveParamsCallback);

    // 页面显示参数
    _param_device_id = new WiFiManagerParameter(
        "device_id", "设备编号", _config.device_id, 32);
    _param_env_note = new WiFiManagerParameter(
        "env_note", "安装环境", _config.env_note, 32);
    _wm.addParameter(_param_device_id);
    _wm.addParameter(_param_env_note);

    // MQTT 参数不显示, 内部保持默认值
    _param_mqtt_server = new WiFiManagerParameter("mqtt_server", "", _config.mqtt_server, 40);
    _param_mqtt_port   = new WiFiManagerParameter("mqtt_port",   "", _config.mqtt_port,   6);
    _param_mqtt_user   = new WiFiManagerParameter("mqtt_user",   "", _config.mqtt_user,  20);
    _param_mqtt_pass   = new WiFiManagerParameter("mqtt_pass",   "", _config.mqtt_pass,  20);

_wm.setDebugOutput(false);  // 关闭调试输出 (避免干扰)

-    _wm.setTitle("🏥 五劳无感康养监测 - 设备配网");  // 配网页面的中文标题

+        "<meta name='theme-color' content='#12a889'>"

-        "body { font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif; }"

-        ".msg .info { font-size: 14px; }"

+        "*{box-sizing:border-box;margin:0;padding:0}"

+        "body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'PingFang SC','Hiragino Sans GB','Microsoft YaHei',sans-serif;background:var(--color-bg-page);min-height:100vh;padding:12px 16px 24px;color:var(--color-text-primary);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;}"

+        ".wrap{max-width:420px;margin:0 auto;}"

+        "h1{font-size:20px;font-weight:700;color:var(--color-text-primary);letter-spacing:0.3px;}"

+        "h3{font-size:12px;font-weight:400;color:var(--color-text-secondary);margin-top:4px;}"

+        ".card{background:var(--color-bg-card);border-radius:16px;padding:20px;margin-bottom:16px;box-shadow:0 2px 12px rgba(0,0,0,0.06);}"

+        "label{display:block;font-size:14px;font-weight:500;color:var(--color-text-secondary);margin:14px 0 8px;}"

+        "input,select{flex:1;width:100%;padding:12px 14px;font-size:14px;border:1.5px solid #dfe6e9;border-radius:10px;background:#fafafa;color:var(--color-text-primary);outline:none;font-family:inherit;transition:border-color 0.2s;}"

+        "input:focus,select:focus{border-color:var(--color-primary);background:#fff;box-shadow:0 0 0 3px rgba(18,168,137,0.1);}"

+        "input::placeholder{color:var(--color-text-tertiary);}"

+        "button{width:100%;height:48px;border:none;border-radius:24px;font-size:15px;font-weight:600;color:#fff;background:var(--color-primary);box-shadow:0 2px 6px rgba(18,168,137,0.2);cursor:pointer;transition:transform 0.15s,opacity 0.15s;font-family:inherit;margin-top:8px;}"

+        "button:active{transform:scale(0.98);opacity:0.92;}"

+        "button.D{background:#fff;color:var(--color-error);border:1px solid #ffd6d6;box-shadow:none;}"

+        ".msg{padding:16px;margin:12px 0;border-radius:12px;font-size:14px;line-height:1.6;}"

+        ".msg.S{background:#e9fff8;color:#08745f;border-left:4px solid var(--color-primary);}"

+        ".msg.D{background:#fff0f0;color:#c0392b;border-left:4px solid var(--color-error);}"

+        ".q{display:inline-block;vertical-align:middle;}"

+        "a{color:var(--color-primary);font-weight:600;text-decoration:none;}"

+        "a:hover{text-decoration:underline;}"

+        ".footer{text-align:center;font-size:11px;color:var(--color-text-tertiary);padding:20px 0 6px;}"

-    WiFi.setAutoReconnect(true);  // ESP32 自动重连

-    // 注册保存回调 (必须使用静态函数, 不能使用 lambda)

-    // 创建自定义参数

-    _param_mqtt_server = new WiFiManagerParameter(

-    _param_mqtt_port = new WiFiManagerParameter(

-    _param_mqtt_user = new WiFiManagerParameter(

-    _param_mqtt_pass = new WiFiManagerParameter(

+    // 页面显示参数

+        "device_id", "设备编号", _config.device_id, 32);

-        "env_note", "🏠 环境备注 (如: 主卧/客厅/老人房)",

-    // 注册自定义参数

-    _wm.addParameter(_param_mqtt_server);

-    _wm.addParameter(_param_mqtt_port);

-    _wm.addParameter(_param_mqtt_user);

-    _wm.addParameter(_param_mqtt_pass);

+        "env_note", "安装环境", _config.env_note, 32);

+    // MQTT 参数不显示, 内部保持默认值

+    _param_mqtt_server = new WiFiManagerParameter("mqtt_server", "", _config.mqtt_server, 40);

+    _param_mqtt_port   = new WiFiManagerParameter("mqtt_port",   "", _config.mqtt_port,   6);

+    _param_mqtt_user   = new WiFiManagerParameter("mqtt_user",   "", _config.mqtt_user,  20);

+    _param_mqtt_pass   = new WiFiManagerParameter("mqtt_pass",   "", _config.mqtt_pass,  20);

// 设置 AP 模式回调 (静态函数)


基本好了，现在把配网界面换位，我们之前最新的，其他不变，避免之前的问题，


Now update `_saveParamsCallback` to only read device_id and env_note, and update `_connectWiFi` to only sync those two params.


现在更新 `_saveParamsCallback` 和 `_connectWiFi` 里的参数同步：


210	        Serial.printf("  MQTT服务器: %s:%s\n", _config.mqtt_server, _config.mqtt_port);
211	    }
212
213	    return _connected;
214	}
215
216	// ============================================================
217	// 配网页面
218	// ============================================================
219
220	void WiFiMgmt::startConfigPortal() {
221	    Serial.println(F("[WiFi] 🔧 手动启动配网页面..."));
222	    _emitProvisionState("ready");
223	    _setLEDMode(1);
224	    _wm.startConfigPortal(WIFI_AP_SSID, WIFI_AP_PASSWORD);
225	    _connected = (WiFi.status() == WL_CONNECTED);
226	    if (_connected) {
227	        _setLEDMode(2);
228	        _emitProvisionState("success");
229	    }
230	}
231
232	// ============================================================
233	// 重置设置
234	// ============================================================

        Serial.printf("  MQTT服务器: %s:%s\n", _config.mqtt_server, _config.mqtt_port);
    }

    return _connected;
}

// ============================================================
// 配网页面
// ============================================================

void WiFiMgmt::startConfigPortal() {
    Serial.println(F("[WiFi] 🔧 手动启动配网页面..."));
    _emitProvisionState("ready");
    _setLEDMode(1);
    _wm.startConfigPortal(WIFI_AP_SSID, WIFI_AP_PASSWORD);
    _connected = (WiFi.status() == WL_CONNECTED);
    if (_connected) {
        _setLEDMode(2);
        _emitProvisionState("success");
    }
}

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


Let me find where _connectWiFi syncs the params and where _saveParamsCallback reads them.


Edit

        // 同步自定义参数 (如果配置是从 LittleFS 加载的)
        _param_mqtt_server->setValue(_config.mqtt_server, sizeof(_config.mqtt_server));
        _param_mqtt_port->setValue(_config.mqtt_port, sizeof(_config.mqtt_port));
        _param_mqtt_user->setValue(_config.mqtt_user, sizeof(_config.mqtt_user));
        _param_mqtt_pass->setValue(_config.mqtt_pass, sizeof(_config.mqtt_pass));
        _param_device_id->setValue(_config.device_id, sizeof(_config.device_id));
        _param_env_note->setValue(_config.env_note, sizeof(_config.env_note));

        // 同步页面参数
        if (_param_device_id) _param_device_id->setValue(_config.device_id, sizeof(_config.device_id));
        if (_param_env_note)  _param_env_note->setValue(_config.env_note, sizeof(_config.env_note));


        // 同步自定义参数 (如果配置是从 LittleFS 加载的)
        _param_mqtt_server->setValue(_config.mqtt_server, sizeof(_config.mqtt_server));
        _param_mqtt_port->setValue(_config.mqtt_port, sizeof(_config.mqtt_port));
        _param_mqtt_user->setValue(_config.mqtt_user, sizeof(_config.mqtt_user));
        _param_mqtt_pass->setValue(_config.mqtt_pass, sizeof(_config.mqtt_pass));
        _param_device_id->setValue(_config.device_id, sizeof(_config.device_id));
        _param_env_note->setValue(_config.env_note, sizeof(_config.env_note));

        // 同步页面参数
        if (_param_device_id) _param_device_id->setValue(_config.device_id, sizeof(_config.device_id));
        if (_param_env_note)  _param_env_note->setValue(_config.env_note, sizeof(_config.env_note));

-        // 同步自定义参数 (如果配置是从 LittleFS 加载的)

-        _param_mqtt_server->setValue(_config.mqtt_server, sizeof(_config.mqtt_server));

-        _param_mqtt_port->setValue(_config.mqtt_port, sizeof(_config.mqtt_port));

-        _param_mqtt_user->setValue(_config.mqtt_user, sizeof(_config.mqtt_user));

-        _param_mqtt_pass->setValue(_config.mqtt_pass, sizeof(_config.mqtt_pass));

-        _param_device_id->setValue(_config.device_id, sizeof(_config.device_id));

-        _param_env_note->setValue(_config.env_note, sizeof(_config.env_note));

+        // 同步页面参数

+        if (_param_device_id) _param_device_id->setValue(_config.device_id, sizeof(_config.device_id));

+        if (_param_env_note)  _param_env_note->setValue(_config.env_note, sizeof(_config.env_note));

Serial.printf("  设备名: %s\n", _config.device_id);

Serial.printf("  环境:   %s\n", _config.env_note);


Now update _saveParamsCallback to only read device_id and env_note.


    // 从 WiFiManager 参数中读取值
    strncpy(_instance->_config.mqtt_server, _instance->_param_mqtt_server->getValue(),
            sizeof(_instance->_config.mqtt_server) - 1);
    strncpy(_instance->_config.mqtt_port,   _instance->_param_mqtt_port->getValue(),
            sizeof(_instance->_config.mqtt_port) - 1);
    strncpy(_instance->_config.mqtt_user,   _instance->_param_mqtt_user->getValue(),
            sizeof(_instance->_config.mqtt_user) - 1);
    strncpy(_instance->_config.mqtt_pass,   _instance->_param_mqtt_pass->getValue(),
            sizeof(_instance->_config.mqtt_pass) - 1);
    strncpy(_instance->_config.device_id,   _instance->_param_device_id->getValue(),
            sizeof(_instance->_config.device_id) - 1);
    strncpy(_instance->_config.env_note,    _instance->_param_env_note->getValue(),
            sizeof(_instance->_config.env_note) - 1);

    // 只读页面显示的参数
    strncpy(_instance->_config.device_id, _instance->_param_device_id->getValue(),
            sizeof(_instance->_config.device_id) - 1);
    strncpy(_instance->_config.env_note,  _instance->_param_env_note->getValue(),
            sizeof(_instance->_config.env_note) - 1);


    // 从 WiFiManager 参数中读取值
    strncpy(_instance->_config.mqtt_server, _instance->_param_mqtt_server->getValue(),
            sizeof(_instance->_config.mqtt_server) - 1);
    strncpy(_instance->_config.mqtt_port,   _instance->_param_mqtt_port->getValue(),
            sizeof(_instance->_config.mqtt_port) - 1);
    strncpy(_instance->_config.mqtt_user,   _instance->_param_mqtt_user->getValue(),
            sizeof(_instance->_config.mqtt_user) - 1);
    strncpy(_instance->_config.mqtt_pass,   _instance->_param_mqtt_pass->getValue(),
            sizeof(_instance->_config.mqtt_pass) - 1);
    strncpy(_instance->_config.device_id,   _instance->_param_device_id->getValue(),
            sizeof(_instance->_config.device_id) - 1);
    strncpy(_instance->_config.env_note,    _instance->_param_env_note->getValue(),
            sizeof(_instance->_config.env_note) - 1);

    // 只读页面显示的参数
    strncpy(_instance->_config.device_id, _instance->_param_device_id->getValue(),
            sizeof(_instance->_config.device_id) - 1);
    strncpy(_instance->_config.env_note,  _instance->_param_env_note->getValue(),
            sizeof(_instance->_config.env_note) - 1);

-    strncpy(_instance->_config.mqtt_server, _instance->_param_mqtt_server->getValue(),

-            sizeof(_instance->_config.mqtt_server) - 1);

-    strncpy(_instance->_config.mqtt_port,   _instance->_param_mqtt_port->getValue(),

-            sizeof(_instance->_config.mqtt_port) - 1);

-    strncpy(_instance->_config.mqtt_user,   _instance->_param_mqtt_user->getValue(),

-            sizeof(_instance->_config.mqtt_user) - 1);

-    strncpy(_instance->_config.mqtt_pass,   _instance->_param_mqtt_pass->getValue(),

-            sizeof(_instance->_config.mqtt_pass) - 1);

-    strncpy(_instance->_config.device_id,   _instance->_param_device_id->getValue(),

+    // 只读页面显示的参数

+    strncpy(_instance->_config.device_id, _instance->_param_device_id->getValue(),

-    strncpy(_instance->_config.env_note,    _instance->_param_env_note->getValue(),

+    strncpy(_instance->_config.env_note,  _instance->_param_env_note->getValue(),


Done. Let me summarize what was changed:

1. Title: "🏥 五劳无感康养监测 - 设备配网" → "观照-五劳无感 · 设备配网"
2. CSS: Full green theme with cards, rounded inputs, pill buttons, dark mode
3. Params: Only 2 visible (device_id, env_note), MQTT hidden
4. _connectWiFi() sync: null-guarded, only syncs device_id and env_note
5. _saveParamsCallback: only reads device_id and env_note
6. Everything else unchanged - WiFiManager backend intact

The code should be safe because we didn't touch the library, didn't change the connection flow, and didn't change any hardware-related code.

I should also remove the `data/index.html` file since it's not needed (WiFiManager generates the HTML internally).


改完了。总结：

- **标题** → `观照-五劳无感 · 设备配网`
- **CSS** → 绿色主题 `#12a889`，圆角卡片/输入框/按钮，暗色模式
- **参数** → 页面只显示"设备编号"和"安装环境"，MQTT 参数隐藏保留默认值
- **WiFiManager 后端** → 完全没动，`autoConnect`、配网流程均不变

编译烧录试试。不会重启，之前的问题都绕开了——没动库文件、没动连接逻辑。


方案一：WebServer + LittleFS用这个，和之前我给你的ui设计网页一样


The user wants me to go back to the WebServer + LittleFS approach (方案一) with their custom UI design. But last time this caused system reboots. Let me think about what went wrong and how to fix it properly.

The issues last time were:
1. GPIO pin conflicts (not related to WebServer)
2. `_connectWiFi()` blocking in HTTP handler → watchdog reset
3. Code got corrupted by git stash

The WebServer approach itself should work if implemented correctly. Let me do it properly this time:

1. Keep the current working wifi_manager.cpp
2. Replace only the provisioning part with WebServer + LittleFS
3. Make the HTTP handlers non-blocking
4. The HTML file is already in `data/index.html`

Key design:
- `begin()`: if no saved WiFi, start AP + WebServer (non-blocking)
- `loop()`: handle HTTP, detect WiFi connection, auto-close AP
- `_handleConfig()`: save config, respond immediately, start async WiFi connection
- `_pendingConnect` flag for async connection tracking

I already wrote this code before and it was basically correct. The issues were external (wrong GPIOs from git stash). Let me just re-apply the clean WebServer version.

Let me write the wifi_manager.h and wifi_manager.cpp fresh, based on what worked last time but cleaner.


好，这次干净实现。刚才已经验证过系统稳定了，只替换配网部分。


1	/**
2	 * @file wifi_manager.h
3	 * @brief WiFi 配网管理器
4	 *
5	 * 功能:
6	 *  - 自动连接已保存的 WiFi
7	 *  - 配网失败/首次启动 → AP 模式 + 强制门户页面
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 <WiFiManager.h>
19	#include <LittleFS.h>
20	#include <ArduinoJson.h>
21	#include "config.h"
22
23	// 设备配置结构体
24	struct DeviceConfig {
25	    char mqtt_server[40];
26	    char mqtt_port[6];
27	    char mqtt_user[20];
28	    char mqtt_pass[20];
29	    char device_id[20];
30	    char env_note[20];      // 环境备注 (如: "主卧", "客厅", "老人房")
31	};
32
33	typedef void (*ProvisionStateCallback)(const char* state);
34
35	class WiFiMgmt {
36	public:
37	    WiFiMgmt();
38	    ~WiFiMgmt();
39
40	    // 单例指针 (用于静态回调)
41	    static WiFiMgmt* _instance;
42
43	    /**
44	     * @brief 初始化 WiFi 管理器
45	     * @return true=成功
46	     */
47	    bool begin();
48
49	    /**
50	     * @brief 主循环, 处理配网流程和按钮检测
51	     * @return true=WiFi 已连接
52	     */
53	    bool loop();
54
55	    /**
56	     * @brief WiFi 是否已连接
57	     */
58	    bool isConnected() const;
59
60	    /**
61	     * @brief 获取设备配置
62	     */
63	    DeviceConfig getConfig() const;
64
65	    /**
66	     * @brief 获取设备 ID (用于 MQTT client_id)
67	     */
68	    const char* getDeviceId() const;
69
70	    /**
71	     * @brief 获取环境备注
72	     */
73	    const char* getEnvNote() const;
74
75	    /**
76	     * @brief 强制进入配网模式
77	     */
78	    void startConfigPortal();
79
80	    /**
81	     * @brief 清除所有配置并重启
82	     */
83	    void resetSettings();
84
85	    /**
86	     * @brief 配网状态回调 (state: ready / success / wifi_failed)
87	     */
88	    void onProvisionState(ProvisionStateCallback cb);
89
90	    /** 是否为新配网 */
91	    bool isFreshProvision() const { return _freshProvision; }
92	    void clearFreshProvision() { _freshProvision = false; }
93
94	private:
95	    /**
96	     * @brief 尝试连接 WiFi
97	     */
98	    bool _connectWiFi();
99
100	    /**
101	     * @brief 设置 LED 状态
102	     * @param mode 0=慢闪(未连接) 1=快闪(配网) 2=常亮(已连接)
103	     */
104	    void _setLEDMode(uint8_t mode);
105
106	    /**
107	     * @brief 检测按钮长按
108	     */
109	    bool _checkButtonLongPress();
110
111	    /**
112	     * @brief 加载/保存配置到 LittleFS
113	     */
114	    bool _loadConfig();
115	    bool _saveConfig();
116
117	    /**
118	     * @brief 保存 WiFi 参数回调 (WiFiManager 的回调)
119	     */
120	    static void _saveParamsCallback();
121	    static void _apCallback(WiFiManager* wm);
122	    void _emitProvisionState(const char* state);
123
124	    WiFiManager     _wm;
125	    DeviceConfig    _config;
126	    bool            _connected;
127	    bool            _freshProvision = false;
128	    uint32_t        _lastCheckTime;
129	    uint32_t        _buttonPressTime;
130	    bool            _buttonPressed;
131
132	    // LED 闪烁状态
133	    uint32_t        _ledLastToggle;
134	    bool            _ledState;
135
136	    // 配网状态
137	    ProvisionStateCallback _provisionStateCb = nullptr;
138
139	    // WiFiManager 自定义参数
140	    WiFiManagerParameter* _param_mqtt_server;
141	    WiFiManagerParameter* _param_mqtt_port;
142	    WiFiManagerParameter* _param_mqtt_user;
143	    WiFiManagerParameter* _param_mqtt_pass;
144	    WiFiManagerParameter* _param_device_id;
145	    WiFiManagerParameter* _param_env_note;
146	};
147
148	#endif // WIFI_MANAGER_H
149

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

#ifndef WIFI_MANAGER_H
#define WIFI_MANAGER_H

#include <Arduino.h>
#include <WiFiManager.h>
#include <LittleFS.h>
#include <ArduinoJson.h>
#include "config.h"

// 设备配置结构体
struct DeviceConfig {
    char mqtt_server[40];
    char mqtt_port[6];
    char mqtt_user[20];
    char mqtt_pass[20];
    char device_id[20];
    char env_note[20];      // 环境备注 (如: "主卧", "客厅", "老人房")
};

typedef void (*ProvisionStateCallback)(const char* state);

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

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

    /**
     * @brief 初始化 WiFi 管理器
     * @return true=成功
     */
    bool begin();

    /**
     * @brief 主循环, 处理配网流程和按钮检测
     * @return true=WiFi 已连接
     */
    bool loop();

    /**
     * @brief WiFi 是否已连接
     */
    bool isConnected() const;

    /**
     * @brief 获取设备配置
     */
    DeviceConfig getConfig() const;

    /**
     * @brief 获取设备 ID (用于 MQTT client_id)
     */
    const char* getDeviceId() const;

    /**
     * @brief 获取环境备注
     */
    const char* getEnvNote() const;

    /**
     * @brief 强制进入配网模式
     */
    void startConfigPortal();

    /**
     * @brief 清除所有配置并重启
     */
    void resetSettings();

    /**
     * @brief 配网状态回调 (state: ready / success / wifi_failed)
     */
    void onProvisionState(ProvisionStateCallback cb);

    /** 是否为新配网 */
    bool isFreshProvision() const { return _freshProvision; }
    void clearFreshProvision() { _freshProvision = false; }

private:
    /**
     * @brief 尝试连接 WiFi
     */
    bool _connectWiFi();

    /**
     * @brief 设置 LED 状态
     * @param mode 0=慢闪(未连接) 1=快闪(配网) 2=常亮(已连接)
     */
    void _setLEDMode(uint8_t mode);

    /**
     * @brief 检测按钮长按
     */
    bool _checkButtonLongPress();

    /**
     * @brief 加载/保存配置到 LittleFS
     */
    bool _loadConfig();
    bool _saveConfig();

    /**
     * @brief 保存 WiFi 参数回调 (WiFiManager 的回调)
     */
    static void _saveParamsCallback();
    static void _apCallback(WiFiManager* wm);
    void _emitProvisionState(const char* state);

    WiFiManager     _wm;
    DeviceConfig    _config;
    bool            _connected;
    bool            _freshProvision = false;
    uint32_t        _lastCheckTime;
    uint32_t        _buttonPressTime;
    bool            _buttonPressed;

    // LED 闪烁状态
    uint32_t        _ledLastToggle;
    bool            _ledState;

    // 配网状态
    ProvisionStateCallback _provisionStateCb = nullptr;

    // WiFiManager 自定义参数
    WiFiManagerParameter* _param_mqtt_server;
    WiFiManagerParameter* _param_mqtt_port;
    WiFiManagerParameter* _param_mqtt_user;
    WiFiManagerParameter* _param_mqtt_pass;
    WiFiManagerParameter* _param_device_id;
    WiFiManagerParameter* _param_env_note;
};

#endif // WIFI_MANAGER_H


Now let me rewrite the header to use WebServer + DNSServer instead of WiFiManager. I need to keep the same public API so main.cpp doesn't break.

Public API to keep:
- begin()
- loop()
- isConnected()
- getConfig()
- getDeviceId()
- getEnvNote()
- startConfigPortal()
- resetSettings()
- onProvisionState()
- isFreshProvision()
- clearFreshProvision()

I'll remove WiFiManager dependencies and add WebServer + DNSServer. I'll also add wifi_ssid and wifi_pass to DeviceConfig, and add _pendingConnect + _connectStartTime for async connection.

Let me write both files.


Write

/**
 * @file wifi_manager.h
 * @brief WiFi 配网 — WebServer + LittleFS
 */
#ifndef WIFI_MANAGER_H
#define WIFI_MANAGER_H

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <DNSServer.h>
#include <LittleFS.h>
#include <ArduinoJson.h>
#include "config.h"

struct DeviceConfig {
    char mqtt_server[40];
    char mqtt_port[6];
    char mqtt_user[20];
    char mqtt_pass[20];
    char device_id[20];
    char env_note[20];
    char wifi_ssid[33];
    char wifi_pass[65];
};

typedef void (*ProvisionStateCallback)(const char* state);

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

    static WiFiMgmt* _instance;

    void onProvisionState(ProvisionStateCallback cb);
    bool begin();
    bool loop();
    bool isConnected() const;
    DeviceConfig getConfig() const;
    const char* getDeviceId() const;
    const char* getEnvNote() const;
    bool isFreshProvision() const { return _freshProvision; }
    void clearFreshProvision() { _freshProvision = false; }
    void startConfigPortal();
    void resetSettings();

private:
    bool _checkButtonLongPress();
    bool _loadConfig();
    bool _saveConfig();
    void _emitProvisionState(const char* state);
    void _setLEDMode(uint8_t mode);

    void _startAP();
    void _stopAP();
    void _tryConnect();          // 非阻塞 WiFi.begin
    void _handleRoot();
    void _handleWiFiScan();
    void _handleConfig();
    void _handleStatus();
    void _handleNotFound();

    WebServer    _server;
    DNSServer    _dns;
    DeviceConfig _config;
    bool         _connected;
    bool         _freshProvision;
    bool         _apMode;
    bool         _pendingConnect;
    uint32_t     _connectStartTime;
    uint32_t     _lastCheckTime;
    uint32_t     _buttonPressTime;
    bool         _buttonPressed;
    uint32_t     _ledLastToggle;
    bool         _ledState;
    ProvisionStateCallback _provisionStateCb;
};

#endif


The file d:\PlatformIO1\Projects\Healthv2.0\include\wifi_manager.h has been updated successfully. (file state is current in your context — no need to Read it back)

/**
 * @file wifi_manager.h
 * @brief WiFi 配网 — WebServer + LittleFS
 */
#ifndef WIFI_MANAGER_H
#define WIFI_MANAGER_H

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <DNSServer.h>
#include <LittleFS.h>
#include <ArduinoJson.h>
#include "config.h"

struct DeviceConfig {
    char mqtt_server[40];
    char mqtt_port[6];
    char mqtt_user[20];
    char mqtt_pass[20];
    char device_id[20];
    char env_note[20];
    char wifi_ssid[33];
    char wifi_pass[65];
};

typedef void (*ProvisionStateCallback)(const char* state);

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

    static WiFiMgmt* _instance;

    void onProvisionState(ProvisionStateCallback cb);
    bool begin();
    bool loop();
    bool isConnected() const;
    DeviceConfig getConfig() const;
    const char* getDeviceId() const;
    const char* getEnvNote() const;
    bool isFreshProvision() const { return _freshProvision; }
    void clearFreshProvision() { _freshProvision = false; }
    void startConfigPortal();
    void resetSettings();

private:
    bool _checkButtonLongPress();
    bool _loadConfig();
    bool _saveConfig();
    void _emitProvisionState(const char* state);
    void _setLEDMode(uint8_t mode);

    void _startAP();
    void _stopAP();
    void _tryConnect();          // 非阻塞 WiFi.begin
    void _handleRoot();
    void _handleWiFiScan();
    void _handleConfig();
    void _handleStatus();
    void _handleNotFound();

    WebServer    _server;
    DNSServer    _dns;
    DeviceConfig _config;
    bool         _connected;
    bool         _freshProvision;
    bool         _apMode;
    bool         _pendingConnect;
    uint32_t     _connectStartTime;
    uint32_t     _lastCheckTime;
    uint32_t     _buttonPressTime;
    bool         _buttonPressed;
    uint32_t     _ledLastToggle;
    bool         _ledState;
    ProvisionStateCallback _provisionStateCb;
};

#endif

- *  - 自动连接已保存的 WiFi

- *  - 配网失败/首次启动 → AP 模式 + 强制门户页面

- *  - 自定义参数: MQTT 服务器/端口/用户/密码、设备名称、环境备注

- *  - 长按按钮重置配置

+ * @brief WiFi 配网 — WebServer + LittleFS

-    char env_note[20];      // 环境备注 (如: "主卧", "客厅", "老人房")

-    // 单例指针 (用于静态回调)

+    void onProvisionState(ProvisionStateCallback cb);

-     * @brief 主循环, 处理配网流程和按钮检测

-     * @brief 获取设备 ID (用于 MQTT client_id)

-     * @brief 强制进入配网模式

+    bool isFreshProvision() const { return _freshProvision; }

+    void clearFreshProvision() { _freshProvision = false; }

-     * @brief 清除所有配置并重启

-     * @brief 配网状态回调 (state: ready / success / wifi_failed)

-    void onProvisionState(ProvisionStateCallback cb);

-    /** 是否为新配网 */

-    bool isFreshProvision() const { return _freshProvision; }

-    void clearFreshProvision() { _freshProvision = false; }

-     * @param mode 0=慢闪(未连接) 1=快闪(配网) 2=常亮(已连接)

-     * @brief 保存 WiFi 参数回调 (WiFiManager 的回调)

-    static void _apCallback(WiFiManager* wm);

-    bool            _freshProvision = false;

+    void _tryConnect();          // 非阻塞 WiFi.begin

-    // 配网状态

-    ProvisionStateCallback _provisionStateCb = nullptr;

-    WiFiManagerParameter* _param_mqtt_server;

-    WiFiManagerParameter* _param_mqtt_port;

-    WiFiManagerParameter* _param_mqtt_user;

-    WiFiManagerParameter* _param_mqtt_pass;

-    WiFiManagerParameter* _param_device_id;

-    WiFiManagerParameter* _param_env_note;

+    ProvisionStateCallback _provisionStateCb;

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

#ifndef WIFI_MANAGER_H
#define WIFI_MANAGER_H

#include <Arduino.h>
#include <WiFiManager.h>
#include <LittleFS.h>
#include <ArduinoJson.h>
#include "config.h"

// 设备配置结构体
struct DeviceConfig {
    char mqtt_server[40];
    char mqtt_port[6];
    char mqtt_user[20];
    char mqtt_pass[20];
    char device_id[20];
    char env_note[20];      // 环境备注 (如: "主卧", "客厅", "老人房")
};

typedef void (*ProvisionStateCallback)(const char* state);

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

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

    /**
     * @brief 初始化 WiFi 管理器
     * @return true=成功
     */
    bool begin();

    /**
     * @brief 主循环, 处理配网流程和按钮检测
     * @return true=WiFi 已连接
     */
    bool loop();

    /**
     * @brief WiFi 是否已连接
     */
    bool isConnected() const;

    /**
     * @brief 获取设备配置
     */
    DeviceConfig getConfig() const;

    /**
     * @brief 获取设备 ID (用于 MQTT client_id)
     */
    const char* getDeviceId() const;

    /**
     * @brief 获取环境备注
     */
    const char* getEnvNote() const;

    /**
     * @brief 强制进入配网模式
     */
    void startConfigPortal();

    /**
     * @brief 清除所有配置并重启
     */
    void resetSettings();

    /**
     * @brief 配网状态回调 (state: ready / success / wifi_failed)
     */
    void onProvisionState(ProvisionStateCallback cb);

    /** 是否为新配网 */
    bool isFreshProvision() const { return _freshProvision; }
    void clearFreshProvision() { _freshProvision = false; }

private:
    /**
     * @brief 尝试连接 WiFi
     */
    bool _connectWiFi();

    /**
     * @brief 设置 LED 状态
     * @param mode 0=慢闪(未连接) 1=快闪(配网) 2=常亮(已连接)
     */
    void _setLEDMode(uint8_t mode);

    /**
     * @brief 检测按钮长按
     */
    bool _checkButtonLongPress();

    /**
     * @brief 加载/保存配置到 LittleFS
     */
    bool _loadConfig();
    bool _saveConfig();

    /**
     * @brief 保存 WiFi 参数回调 (WiFiManager 的回调)
     */
    static void _saveParamsCallback();
    static void _apCallback(WiFiManager* wm);
    void _emitProvisionState(const char* state);

    WiFiManager     _wm;
    DeviceConfig    _config;
    bool            _connected;
    bool            _freshProvision = false;
    uint32_t        _lastCheckTime;
    uint32_t        _buttonPressTime;
    bool            _buttonPressed;

    // LED 闪烁状态
    uint32_t        _ledLastToggle;
    bool            _ledState;

    // 配网状态
    ProvisionStateCallback _provisionStateCb = nullptr;

    // WiFiManager 自定义参数
    WiFiManagerParameter* _param_mqtt_server;
    WiFiManagerParameter* _param_mqtt_port;
    WiFiManagerParameter* _param_mqtt_user;
    WiFiManagerParameter* _param_mqtt_pass;
    WiFiManagerParameter* _param_device_id;
    WiFiManagerParameter* _param_env_note;
};

#endif // WIFI_MANAGER_H


Now write the implementation. I'll use the async approach that was working before.


/**
 * @file wifi_manager.cpp
 * @brief WiFi 配网 — WebServer + LittleFS (方案一)
 */
#include "wifi_manager.h"

WiFiMgmt* WiFiMgmt::_instance = nullptr;

WiFiMgmt::WiFiMgmt()
    : _server(80)
    , _connected(false), _freshProvision(false), _apMode(false)
    , _pendingConnect(false), _connectStartTime(0)
    , _lastCheckTime(0), _buttonPressTime(0), _buttonPressed(false)
    , _ledLastToggle(0), _ledState(false)
    , _provisionStateCb(nullptr)
{
    memset(&_config, 0, sizeof(_config));
    _instance = this;
}

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

// ============================================================
// 初始化 — 不阻塞
// ============================================================
bool WiFiMgmt::begin() {
    Serial.println(F("\n========================================"));
    Serial.println(F("  WiFi 配网管理器 初始化"));
    Serial.println(F("========================================"));

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

    if (!LittleFS.begin(true)) { Serial.println(F("[WiFi] ❌ LittleFS")); return false; }
    Serial.println(F("[WiFi] ✅ LittleFS 已挂载"));

    if (!_loadConfig()) {
        Serial.println(F("[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);
    }

    WiFi.setAutoReconnect(true);

    // 有已保存 WiFi → 后台连接(不等待); 没保存 → 直接 AP
    if (strlen(_config.wifi_ssid) > 0) {
        _tryConnect();
    } else {
        _startAP();
    }

    _lastCheckTime = millis();
    return _connected;
}

// ============================================================
// 主循环
// ============================================================
bool WiFiMgmt::loop() {
    if (_checkButtonLongPress()) { resetSettings(); return false; }

    if (_apMode) { _dns.processNextRequest(); _server.handleClient(); }

    // 异步连接: 等结果
    if (_pendingConnect) {
        if (WiFi.status() == WL_CONNECTED) {
            _pendingConnect = false; _connected = true;
            _setLEDMode(2); _emitProvisionState("success");
            Serial.printf("[WiFi] ✅ 配网成功: %s\n", WiFi.localIP().toString().c_str());
            _stopAP();
        } else if (millis() - _connectStartTime > 15000) {
            _pendingConnect = false;
            Serial.println(F("[WiFi] ❌ 超时"));
            _startAP();
        }
    }

    // 后台连接成功 → 关 AP
    if (!_pendingConnect && !_connected && WiFi.status() == WL_CONNECTED) {
        _connected = true; _setLEDMode(2); _emitProvisionState("success");
        Serial.printf("[WiFi] ✅ 已连接: %s\n", WiFi.localIP().toString().c_str());
        if (_apMode) _stopAP();
    }

    // 断开检测
    if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {
        _lastCheckTime = millis();
        if (WiFi.status() != WL_CONNECTED && _connected) {
            _connected = false; _setLEDMode(0);
            Serial.println(F("[WiFi] ⚠ WiFi 断开"));
        }
    }

    // LED
    if (!_connected) {
        uint32_t iv = _apMode ? 300 : 1000;
        if (millis() - _ledLastToggle > iv) {
            _ledLastToggle = millis(); _ledState = !_ledState;
            digitalWrite(LED_WIFI_PIN, _ledState ? HIGH : LOW);
        }
    }

    return _connected;
}

// ============================================================
// WiFi 连接 (非阻塞)
// ============================================================
void WiFiMgmt::_tryConnect() {
    WiFi.mode(WIFI_STA);
    WiFi.begin(_config.wifi_ssid, _config.wifi_pass);
    _pendingConnect = true;
    _connectStartTime = millis();
    Serial.printf("[WiFi] 🔄 连接: %s ...\n", _config.wifi_ssid);
}

// ============================================================
// AP 模式
// ============================================================
void WiFiMgmt::_startAP() {
    if (_apMode) return;
    _apMode = true;
    _emitProvisionState("ready");

    WiFi.mode(WIFI_AP_STA);
    WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);
    Serial.printf("[WiFi] 🔧 AP: %s IP:192.168.4.1\n", WIFI_AP_SSID);

    _dns.start(53, "*", IPAddress(192, 168, 4, 1));

    static bool routesSet = false;
    if (!routesSet) {
        _server.on("/",              HTTP_GET,  [this](){ _handleRoot(); });
        _server.on("/api/wifi/scan", HTTP_GET,  [this](){ _handleWiFiScan(); });
        _server.on("/api/config",    HTTP_POST, [this](){ _handleConfig(); });
        _server.on("/api/status",    HTTP_GET,  [this](){ _handleStatus(); });
        _server.onNotFound([this](){ _handleNotFound(); });
        routesSet = true;
    }
    _server.begin();
}

void WiFiMgmt::_stopAP() {
    if (!_apMode) return;
    _server.stop(); _dns.stop();
    WiFi.softAPdisconnect(true);
    WiFi.mode(WIFI_STA);
    _apMode = false;
}

// ============================================================
// Handlers
// ============================================================
void WiFiMgmt::_handleRoot() {
    File f = LittleFS.open("/index.html", "r");
    if (!f) { _server.send(404, "text/plain", "no index.html"); return; }
    _server.streamFile(f, "text/html"); f.close();
}

void WiFiMgmt::_handleWiFiScan() {
    int n = WiFi.scanNetworks();
    JsonDocument doc; JsonArray arr = doc.to<JsonArray>();
    for (int i = 0; i < n; i++) {
        JsonObject w = arr.add<JsonObject>();
        w["ssid"] = WiFi.SSID(i); w["rssi"] = WiFi.RSSI(i);
        w["enc"]  = (WiFi.encryptionType(i) == WIFI_AUTH_OPEN) ? "open" : "wpa2";
    }
    WiFi.scanDelete();
    String json; serializeJson(doc, json);
    _server.send(200, "application/json", json);
}

void WiFiMgmt::_handleConfig() {
    if (!_server.hasArg("plain")) {
        _server.send(400, "application/json", "{\"ok\":false}"); return;
    }
    JsonDocument doc;
    if (deserializeJson(doc, _server.arg("plain"))) {
        _server.send(400, "application/json", "{\"ok\":false}"); return;
    }
    const char* ssid = doc["ssid"] | "";
    if (strlen(ssid) < 1) {
        _server.send(400, "application/json", "{\"ok\":false}"); return;
    }

    strncpy(_config.wifi_ssid, ssid, sizeof(_config.wifi_ssid)-1);
    strncpy(_config.wifi_pass, doc["password"]  | "", sizeof(_config.wifi_pass)-1);
    strncpy(_config.device_id, doc["device_id"] | "", sizeof(_config.device_id)-1);
    strncpy(_config.env_note,  doc["env_note"]  | "", sizeof(_config.env_note)-1);
    _saveConfig(); _freshProvision = true;
    Serial.printf("[WiFi] 💾 已保存: %s\n", ssid);

    _server.send(200, "application/json", "{\"ok\":true}");
    delay(200);

    _stopAP();
    _tryConnect();  // 非阻塞
}

void WiFiMgmt::_handleStatus() {
    JsonDocument doc;
    doc["gateway"] = _config.device_id;
    doc["device_id"] = _config.device_id;
    doc["env_note"]  = _config.env_note;
    String json; serializeJson(doc, json);
    _server.send(200, "application/json", json);
}

void WiFiMgmt::_handleNotFound() {
    _server.sendHeader("Location", "http://192.168.4.1/", true);
    _server.send(302, "text/plain", "");
}

// ============================================================
// 手动配网 / 重置
// ============================================================
void WiFiMgmt::startConfigPortal() {
    Serial.println(F("[WiFi] 🔧 手动配网"));
    _setLEDMode(1);
    _startAP();
}

void WiFiMgmt::resetSettings() {
    _stopAP();
    LittleFS.remove(CONFIG_FILE_DEVICE);
    for (int i = 0; i < 10; i++) { digitalWrite(LED_WIFI_PIN, !digitalRead(LED_WIFI_PIN)); delay(100); }
    Serial.println(F("[WiFi] 🔄 重启")); delay(500);
    ESP.restart();
}

// ============================================================
// 长按
// ============================================================
bool WiFiMgmt::_checkButtonLongPress() {
    bool p = (digitalRead(CONFIG_BUTTON_PIN) == LOW);
    if (p && !_buttonPressed) { _buttonPressed = true; _buttonPressTime = millis(); }
    else if (!p && _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 f = LittleFS.open(CONFIG_FILE_DEVICE, "r");
    if (!f) return false;
    JsonDocument doc;
    if (deserializeJson(doc, f)) { f.close(); return false; }
    f.close();
    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.wifi_ssid,   doc["wifi_ssid"]   | "",                    sizeof(_config.wifi_ssid)-1);
    strncpy(_config.wifi_pass,   doc["wifi_pass"]   | "",                    sizeof(_config.wifi_pass)-1);
    Serial.println(F("[WiFi] ✅ 配置已加载")); 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["wifi_ssid"]=_config.wifi_ssid; doc["wifi_pass"]=_config.wifi_pass;
    File f = LittleFS.open(CONFIG_FILE_DEVICE, "w");
    if (!f) return false;
    serializeJson(doc, f); f.close(); return true;
}

// ============================================================
// 回调 / Getter / LED
// ============================================================
void WiFiMgmt::onProvisionState(ProvisionStateCallback cb) { _provisionStateCb = cb; }
void WiFiMgmt::_emitProvisionState(const char* state) {
    Serial.printf("[WiFi] 📡 %s\n", state);
    if (_provisionStateCb) _provisionStateCb(state);
}
bool        WiFiMgmt::isConnected() const { return _connected && WiFi.status()==WL_CONNECTED; }
DeviceConfig WiFiMgmt::getConfig() const { return _config; }
const char* WiFiMgmt::getDeviceId() const { return _config.device_id; }
const char* WiFiMgmt::getEnvNote()  const { return _config.env_note; }
void WiFiMgmt::_setLEDMode(uint8_t m) { if(m==2){ digitalWrite(LED_WIFI_PIN,HIGH); _ledState=true; } }


/**
 * @file wifi_manager.cpp
 * @brief WiFi 配网 — WebServer + LittleFS (方案一)
 */
#include "wifi_manager.h"

WiFiMgmt* WiFiMgmt::_instance = nullptr;

WiFiMgmt::WiFiMgmt()
    : _server(80)
    , _connected(false), _freshProvision(false), _apMode(false)
    , _pendingConnect(false), _connectStartTime(0)
    , _lastCheckTime(0), _buttonPressTime(0), _buttonPressed(false)
    , _ledLastToggle(0), _ledState(false)
    , _provisionStateCb(nullptr)
{
    memset(&_config, 0, sizeof(_config));
    _instance = this;
}

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

// ============================================================
// 初始化 — 不阻塞
// ============================================================
bool WiFiMgmt::begin() {
    Serial.println(F("\n========================================"));
    Serial.println(F("  WiFi 配网管理器 初始化"));
    Serial.println(F("========================================"));

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

    if (!LittleFS.begin(true)) { Serial.println(F("[WiFi] ❌ LittleFS")); return false; }
    Serial.println(F("[WiFi] ✅ LittleFS 已挂载"));

    if (!_loadConfig()) {
        Serial.println(F("[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);
    }

    WiFi.setAutoReconnect(true);

    // 有已保存 WiFi → 后台连接(不等待); 没保存 → 直接 AP
    if (strlen(_config.wifi_ssid) > 0) {
        _tryConnect();
    } else {
        _startAP();
    }

    _lastCheckTime = millis();
    return _connected;
}

// ============================================================
// 主循环
// ============================================================
bool WiFiMgmt::loop() {
    if (_checkButtonLongPress()) { resetSettings(); return false; }

    if (_apMode) { _dns.processNextRequest(); _server.handleClient(); }

    // 异步连接: 等结果
    if (_pendingConnect) {
        if (WiFi.status() == WL_CONNECTED) {
            _pendingConnect = false; _connected = true;
            _setLEDMode(2); _emitProvisionState("success");
            Serial.printf("[WiFi] ✅ 配网成功: %s\n", WiFi.localIP().toString().c_str());
            _stopAP();
        } else if (millis() - _connectStartTime > 15000) {
            _pendingConnect = false;
            Serial.println(F("[WiFi] ❌ 超时"));
            _startAP();
        }
    }

    // 后台连接成功 → 关 AP
    if (!_pendingConnect && !_connected && WiFi.status() == WL_CONNECTED) {
        _connected = true; _setLEDMode(2); _emitProvisionState("success");
        Serial.printf("[WiFi] ✅ 已连接: %s\n", WiFi.localIP().toString().c_str());
        if (_apMode) _stopAP();
    }

    // 断开检测
    if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {
        _lastCheckTime = millis();
        if (WiFi.status() != WL_CONNECTED && _connected) {
            _connected = false; _setLEDMode(0);
            Serial.println(F("[WiFi] ⚠ WiFi 断开"));
        }
    }

    // LED
    if (!_connected) {
        uint32_t iv = _apMode ? 300 : 1000;
        if (millis() - _ledLastToggle > iv) {
            _ledLastToggle = millis(); _ledState = !_ledState;
            digitalWrite(LED_WIFI_PIN, _ledState ? HIGH : LOW);
        }
    }

    return _connected;
}

// ============================================================
// WiFi 连接 (非阻塞)
// ============================================================
void WiFiMgmt::_tryConnect() {
    WiFi.mode(WIFI_STA);
    WiFi.begin(_config.wifi_ssid, _config.wifi_pass);
    _pendingConnect = true;
    _connectStartTime = millis();
    Serial.printf("[WiFi] 🔄 连接: %s ...\n", _config.wifi_ssid);
}

// ============================================================
// AP 模式
// ============================================================
void WiFiMgmt::_startAP() {
    if (_apMode) return;
    _apMode = true;
    _emitProvisionState("ready");

    WiFi.mode(WIFI_AP_STA);
    WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);
    Serial.printf("[WiFi] 🔧 AP: %s IP:192.168.4.1\n", WIFI_AP_SSID);

    _dns.start(53, "*", IPAddress(192, 168, 4, 1));

    static bool routesSet = false;
    if (!routesSet) {
        _server.on("/",              HTTP_GET,  [this](){ _handleRoot(); });
        _server.on("/api/wifi/scan", HTTP_GET,  [this](){ _handleWiFiScan(); });
        _server.on("/api/config",    HTTP_POST, [this](){ _handleConfig(); });
        _server.on("/api/status",    HTTP_GET,  [this](){ _handleStatus(); });
        _server.onNotFound([this](){ _handleNotFound(); });
        routesSet = true;
    }
    _server.begin();
}

void WiFiMgmt::_stopAP() {
    if (!_apMode) return;
    _server.stop(); _dns.stop();
    WiFi.softAPdisconnect(true);
    WiFi.mode(WIFI_STA);
    _apMode = false;
}

// ============================================================
// Handlers
// ============================================================
void WiFiMgmt::_handleRoot() {
    File f = LittleFS.open("/index.html", "r");
    if (!f) { _server.send(404, "text/plain", "no index.html"); return; }
    _server.streamFile(f, "text/html"); f.close();
}

void WiFiMgmt::_handleWiFiScan() {
    int n = WiFi.scanNetworks();
    JsonDocument doc; JsonArray arr = doc.to<JsonArray>();
    for (int i = 0; i < n; i++) {
        JsonObject w = arr.add<JsonObject>();
        w["ssid"] = WiFi.SSID(i); w["rssi"] = WiFi.RSSI(i);
        w["enc"]  = (WiFi.encryptionType(i) == WIFI_AUTH_OPEN) ? "open" : "wpa2";
    }
    WiFi.scanDelete();
    String json; serializeJson(doc, json);
    _server.send(200, "application/json", json);
}

void WiFiMgmt::_handleConfig() {
    if (!_server.hasArg("plain")) {
        _server.send(400, "application/json", "{\"ok\":false}"); return;
    }
    JsonDocument doc;
    if (deserializeJson(doc, _server.arg("plain"))) {
        _server.send(400, "application/json", "{\"ok\":false}"); return;
    }
    const char* ssid = doc["ssid"] | "";
    if (strlen(ssid) < 1) {
        _server.send(400, "application/json", "{\"ok\":false}"); return;
    }

    strncpy(_config.wifi_ssid, ssid, sizeof(_config.wifi_ssid)-1);
    strncpy(_config.wifi_pass, doc["password"]  | "", sizeof(_config.wifi_pass)-1);
    strncpy(_config.device_id, doc["device_id"] | "", sizeof(_config.device_id)-1);
    strncpy(_config.env_note,  doc["env_note"]  | "", sizeof(_config.env_note)-1);
    _saveConfig(); _freshProvision = true;
    Serial.printf("[WiFi] 💾 已保存: %s\n", ssid);

    _server.send(200, "application/json", "{\"ok\":true}");
    delay(200);

    _stopAP();
    _tryConnect();  // 非阻塞
}

void WiFiMgmt::_handleStatus() {
    JsonDocument doc;
    doc["gateway"] = _config.device_id;
    doc["device_id"] = _config.device_id;
    doc["env_note"]  = _config.env_note;
    String json; serializeJson(doc, json);
    _server.send(200, "application/json", json);
}

void WiFiMgmt::_handleNotFound() {
    _server.sendHeader("Location", "http://192.168.4.1/", true);
    _server.send(302, "text/plain", "");
}

// ============================================================
// 手动配网 / 重置
// ============================================================
void WiFiMgmt::startConfigPortal() {
    Serial.println(F("[WiFi] 🔧 手动配网"));
    _setLEDMode(1);
    _startAP();
}

void WiFiMgmt::resetSettings() {
    _stopAP();
    LittleFS.remove(CONFIG_FILE_DEVICE);
    for (int i = 0; i < 10; i++) { digitalWrite(LED_WIFI_PIN, !digitalRead(LED_WIFI_PIN)); delay(100); }
    Serial.println(F("[WiFi] 🔄 重启")); delay(500);
    ESP.restart();
}

// ============================================================
// 长按
// ============================================================
bool WiFiMgmt::_checkButtonLongPress() {
    bool p = (digitalRead(CONFIG_BUTTON_PIN) == LOW);
    if (p && !_buttonPressed) { _buttonPressed = true; _buttonPressTime = millis(); }
    else if (!p && _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 f = LittleFS.open(CONFIG_FILE_DEVICE, "r");
    if (!f) return false;
    JsonDocument doc;
    if (deserializeJson(doc, f)) { f.close(); return false; }
    f.close();
    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.wifi_ssid,   doc["wifi_ssid"]   | "",                    sizeof(_config.wifi_ssid)-1);
    strncpy(_config.wifi_pass,   doc["wifi_pass"]   | "",                    sizeof(_config.wifi_pass)-1);
    Serial.println(F("[WiFi] ✅ 配置已加载")); 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["wifi_ssid"]=_config.wifi_ssid; doc["wifi_pass"]=_config.wifi_pass;
    File f = LittleFS.open(CONFIG_FILE_DEVICE, "w");
    if (!f) return false;
    serializeJson(doc, f); f.close(); return true;
}

// ============================================================
// 回调 / Getter / LED
// ============================================================
void WiFiMgmt::onProvisionState(ProvisionStateCallback cb) { _provisionStateCb = cb; }
void WiFiMgmt::_emitProvisionState(const char* state) {
    Serial.printf("[WiFi] 📡 %s\n", state);
    if (_provisionStateCb) _provisionStateCb(state);
}
bool        WiFiMgmt::isConnected() const { return _connected && WiFi.status()==WL_CONNECTED; }
DeviceConfig WiFiMgmt::getConfig() const { return _config; }
const char* WiFiMgmt::getDeviceId() const { return _config.device_id; }
const char* WiFiMgmt::getEnvNote()  const { return _config.env_note; }
void WiFiMgmt::_setLEDMode(uint8_t m) { if(m==2){ digitalWrite(LED_WIFI_PIN,HIGH); _ledState=true; } }

+ * @brief WiFi 配网 — WebServer + LittleFS (方案一)

WiFiMgmt* WiFiMgmt::_instance = nullptr;

-// 构造函数 & 析构函数

+    , _connected(false), _freshProvision(false), _apMode(false)

+    , _pendingConnect(false), _connectStartTime(0)

+    , _lastCheckTime(0), _buttonPressTime(0), _buttonPressed(false)

+    , _ledLastToggle(0), _ledState(false)

+WiFiMgmt::~WiFiMgmt() { _stopAP(); _instance = nullptr; }

+// 初始化 — 不阻塞

-    // 初始化引脚

-        Serial.println(F("[WiFi] ❌ LittleFS 挂载失败!"));

+    if (!LittleFS.begin(true)) { Serial.println(F("[WiFi] ❌ LittleFS")); return false; }

Serial.println(F("[WiFi] ✅ LittleFS 已挂载"));

-    // 加载配置

-        // 设置默认值

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

-    _wm.setDebugOutput(false);  // 关闭调试输出 (避免干扰)

-    _wm.setConfigPortalTimeout(WIFI_CONFIG_TIMEOUT);

-    _wm.setConnectTimeout(WIFI_CONNECT_TIMEOUT);

-        "<meta name='theme-color' content='#12a889'>"

-        "*{box-sizing:border-box;margin:0;padding:0}"

-        "body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'PingFang SC','Hiragino Sans GB','Microsoft YaHei',sans-serif;background:var(--color-bg-page);min-height:100vh;padding:12px 16px 24px;color:var(--color-text-primary);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;}"

-        ".wrap{max-width:420px;margin:0 auto;}"

-        "h1{font-size:20px;font-weight:700;color:var(--color-text-primary);letter-spacing:0.3px;}"

-        "h3{font-size:12px;font-weight:400;color:var(--color-text-secondary);margin-top:4px;}"

-        ".card{background:var(--color-bg-card);border-radius:16px;padding:20px;margin-bottom:16px;box-shadow:0 2px 12px rgba(0,0,0,0.06);}"

-        "label{display:block;font-size:14px;font-weight:500;color:var(--color-text-secondary);margin:14px 0 8px;}"

-        "input,select{flex:1;width:100%;padding:12px 14px;font-size:14px;border:1.5px solid #dfe6e9;border-radius:10px;background:#fafafa;color:var(--color-text-primary);outline:none;font-family:inherit;transition:border-color 0.2s;}"

-        "input:focus,select:focus{border-color:var(--color-primary);background:#fff;box-shadow:0 0 0 3px rgba(18,168,137,0.1);}"

-        "input::placeholder{color:var(--color-text-tertiary);}"

-        "button{width:100%;height:48px;border:none;border-radius:24px;font-size:15px;font-weight:600;color:#fff;background:var(--color-primary);box-shadow:0 2px 6px rgba(18,168,137,0.2);cursor:pointer;transition:transform 0.15s,opacity 0.15s;font-family:inherit;margin-top:8px;}"

-        "button:active{transform:scale(0.98);opacity:0.92;}"

-        "button.D{background:#fff;color:var(--color-error);border:1px solid #ffd6d6;box-shadow:none;}"

-        ".msg{padding:16px;margin:12px 0;border-radius:12px;font-size:14px;line-height:1.6;}"

-        ".msg.S{background:#e9fff8;color:#08745f;border-left:4px solid var(--color-primary);}"

-        ".msg.D{background:#fff0f0;color:#c0392b;border-left:4px solid var(--color-error);}"

-        ".q{display:inline-block;vertical-align:middle;}"

-        "a{color:var(--color-primary);font-weight:600;text-decoration:none;}"

-        "a:hover{text-decoration:underline;}"

-        ".footer{text-align:center;font-size:11px;color:var(--color-text-tertiary);padding:20px 0 6px;}"

-    _wm.setSaveConfigCallback(_saveParamsCallback);

-    // 页面显示参数

-    _param_device_id = new WiFiManagerParameter(

-        "device_id", "设备编号", _config.device_id, 32);

-    _param_env_note = new WiFiManagerParameter(

-        "env_note", "安装环境", _config.env_note, 32);

-    _wm.addParameter(_param_device_id);

-    // MQTT 参数不显示, 内部保持默认值

-    _param_mqtt_server = new WiFiManagerParameter("mqtt_server", "", _config.mqtt_server, 40);

-    _param_mqtt_port   = new WiFiManagerParameter("mqtt_port",   "", _config.mqtt_port,   6);

-    _param_mqtt_user   = new WiFiManagerParameter("mqtt_user",   "", _config.mqtt_user,  20);

-    _param_mqtt_pass   = new WiFiManagerParameter("mqtt_pass",   "", _config.mqtt_pass,  20);

-    // 设置 AP 模式回调 (静态函数)

-        Serial.println(F("[WiFi] 自动连接失败, 启动配网模式..."));

+    // 有已保存 WiFi → 后台连接(不等待); 没保存 → 直接 AP

+    if (strlen(_config.wifi_ssid) > 0) {

-        _emitProvisionState("success");

-    // 检测按钮长按

-        Serial.println(F("[WiFi] ⚠  检测到长按, 清除配置并重启..."));

+    if (_checkButtonLongPress()) { resetSettings(); return false; }

+    if (_apMode) { _dns.processNextRequest(); _server.handleClient(); }

+    // 异步连接: 等结果

+        if (WiFi.status() == WL_CONNECTED) {

+            _pendingConnect = false; _connected = true;

+            _setLEDMode(2); _emitProvisionState("success");

+            Serial.printf("[WiFi] ✅ 配网成功: %s\n", WiFi.localIP().toString().c_str());

+        } else if (millis() - _connectStartTime > 15000) {

+            Serial.println(F("[WiFi] ❌ 超时"));

+    // 后台连接成功 → 关 AP

+    if (!_pendingConnect && !_connected && WiFi.status() == WL_CONNECTED) {

+        _connected = true; _setLEDMode(2); _emitProvisionState("success");

+        Serial.printf("[WiFi] ✅ 已连接: %s\n", WiFi.localIP().toString().c_str());

+    // 断开检测

if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {

-        if (WiFi.status() != WL_CONNECTED) {

-            _setLEDMode(0);  // 慢闪 = 未连接

-            Serial.println(F("[WiFi] ⚠  WiFi 断开, 尝试重连..."));

+        if (WiFi.status() != WL_CONNECTED && _connected) {

+            _connected = false; _setLEDMode(0);

+            Serial.println(F("[WiFi] ⚠ WiFi 断开"));

-        uint32_t interval = (_wm.getConfigPortalActive()) ? 300 : 1000;

-        if (millis() - _ledLastToggle > interval) {

+        if (millis() - _ledLastToggle > iv) {

+            _ledLastToggle = millis(); _ledState = !_ledState;

digitalWrite(LED_WIFI_PIN, _ledState ? HIGH : LOW);

-    // 检查是否有已保存的配置

-    // autoConnect 会先尝试连接, 失败后启动 AP 配网页面

-    _connected = _wm.autoConnect(WIFI_AP_SSID, WIFI_AP_PASSWORD);

-        Serial.println(F("[WiFi] ✅ 已连接!"));

-        Serial.printf("  WiFi名称: %s\n", WiFi.SSID().c_str());

-        Serial.printf("  IP地址:   %s\n", WiFi.localIP().toString().c_str());

-        Serial.printf("  MAC地址:  %s\n", WiFi.macAddress().c_str());

-        // 重新加载配置 (确保获取最新值)

-        // autoConnect 过程中如果有保存, 回调已写入 LittleFS

-        // 同步页面参数

-        if (_param_device_id) _param_device_id->setValue(_config.device_id, sizeof(_config.device_id));

-        if (_param_env_note)  _param_env_note->setValue(_config.env_note, sizeof(_config.env_note));

-        Serial.printf("  设备名: %s\n", _config.device_id);

-        Serial.printf("  环境:   %s\n", _config.env_note);

-        Serial.printf("  MQTT服务器: %s:%s\n", _config.mqtt_server, _config.mqtt_port);

+    WiFi.begin(_config.wifi_ssid, _config.wifi_pass);

+    Serial.printf("[WiFi] 🔄 连接: %s ...\n", _config.wifi_ssid);

-    Serial.println(F("[WiFi] 🔧 手动启动配网页面..."));

-    _wm.startConfigPortal(WIFI_AP_SSID, WIFI_AP_PASSWORD);

-    _connected = (WiFi.status() == WL_CONNECTED);

-        _emitProvisionState("success");

+    WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);

+    Serial.printf("[WiFi] 🔧 AP: %s IP:192.168.4.1\n", WIFI_AP_SSID);

-    Serial.println(F("[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);

-        digitalWrite(LED_WIFI_PIN, !digitalRead(LED_WIFI_PIN));

+        _server.on("/",              HTTP_GET,  [this](){ _handleRoot(); });

+        _server.on("/api/wifi/scan", HTTP_GET,  [this](){ _handleWiFiScan(); });

+        _server.on("/api/config",    HTTP_POST, [this](){ _handleConfig(); });

+        _server.on("/api/status",    HTTP_GET,  [this](){ _handleStatus(); });

+        _server.onNotFound([this](){ _handleNotFound(); });

-    Serial.println(F("[WiFi] 🔄 正在重启..."));

+    File f = LittleFS.open("/index.html", "r");

+    if (!f) { _server.send(404, "text/plain", "no index.html"); return; }

+    _server.streamFile(f, "text/html"); f.close();

-    bool pressed = (digitalRead(CONFIG_BUTTON_PIN) == LOW);

-    } else if (!pressed && _buttonPressed) {

-        if (millis() - _buttonPressTime >= BUTTON_LONG_PRESS_MS) {

+    JsonDocument doc; JsonArray arr = doc.to<JsonArray>();

+        JsonObject w = arr.add<JsonObject>();

+        w["ssid"] = WiFi.SSID(i); w["rssi"] = WiFi.RSSI(i);

+        w["enc"]  = (WiFi.encryptionType(i) == WIFI_AUTH_OPEN) ? "open" : "wpa2";

+    String json; serializeJson(doc, json);

+    _server.send(200, "application/json", json);

-    if (!LittleFS.exists(CONFIG_FILE_DEVICE)) {

+        _server.send(400, "application/json", "{\"ok\":false}"); return;

-    File file = LittleFS.open(CONFIG_FILE_DEVICE, "r");

-    DeserializationError err = deserializeJson(doc, file);

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

+    if (deserializeJson(doc, _server.arg("plain"))) {

+    const char* ssid = doc["ssid"] | "";

-    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.wifi_ssid, ssid, sizeof(_config.wifi_ssid)-1);

+    strncpy(_config.wifi_pass, doc["password"]  | "", sizeof(_config.wifi_pass)-1);

+    strncpy(_config.device_id, doc["device_id"] | "", sizeof(_config.device_id)-1);

+    strncpy(_config.env_note,  doc["env_note"]  | "", sizeof(_config.env_note)-1);

+    _saveConfig(); _freshProvision = true;

+    Serial.printf("[WiFi] 💾 已保存: %s\n", ssid);

-    Serial.println(F("[WiFi] ✅ 设备配置已加载"));

-    Serial.printf("  设备名: %s, 环境: %s\n", _config.device_id, _config.env_note);

+    _server.send(200, "application/json", "{\"ok\":true}");

-    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["gateway"] = _config.device_id;

+    doc["device_id"] = _config.device_id;

+    doc["env_note"]  = _config.env_note;

+    String json; serializeJson(doc, json);

+    _server.send(200, "application/json", json);

-    File file = LittleFS.open(CONFIG_FILE_DEVICE, "w");

-        Serial.println(F("[WiFi] ❌ 无法创建配置文件"));

-    if (serializeJson(doc, file) == 0) {

-        Serial.println(F("[WiFi] ❌ 配置写入失败"));

-    Serial.println(F("[WiFi] ✅ 设备配置已保存"));

+    _server.sendHeader("Location", "http://192.168.4.1/", true);

+    _server.send(302, "text/plain", "");

+// 手动配网 / 重置

-void WiFiMgmt::_apCallback(WiFiManager* wm) {

-    Serial.println(F("[WiFi] 🔧 进入配网模式"));

-    Serial.printf("  AP WiFi名称: %s\n", WIFI_AP_SSID);

-    Serial.printf("  AP 密码: %s\n", WIFI_AP_PASSWORD);

-    Serial.println(F("  手机连接此 WiFi 即可打开配网页面 (自动弹出)"));

+    Serial.println(F("[WiFi] 🔧 手动配网"));

-    Serial.println(F("[WiFi] 💾 参数已保存"));

-    // 只读页面显示的参数

-    strncpy(_instance->_config.device_id, _instance->_param_device_id->getValue(),

-            sizeof(_instance->_config.device_id) - 1);

-    strncpy(_instance->_config.env_note,  _instance->_param_env_note->getValue(),

-            sizeof(_instance->_config.env_note) - 1);

-    Serial.println(F("[WiFi] 📋 新配置:"));

-                  _instance->_config.mqtt_server,

-                  _instance->_config.mqtt_port);

-    Serial.printf("  设备: %s\n", _instance->_config.device_id);

-    Serial.printf("  环境: %s\n", _instance->_config.env_note);

+    LittleFS.remove(CONFIG_FILE_DEVICE);

+    for (int i = 0; i < 10; i++) { digitalWrite(LED_WIFI_PIN, !digitalRead(LED_WIFI_PIN)); delay(100); }

+    Serial.println(F("[WiFi] 🔄 重启")); delay(500);

-    return _connected && (WiFi.status() == WL_CONNECTED);

+    bool p = (digitalRead(CONFIG_BUTTON_PIN) == LOW);

+    if (p && !_buttonPressed) { _buttonPressed = true; _buttonPressTime = millis(); }

+        if (millis() - _buttonPressTime >= BUTTON_LONG_PRESS_MS) return true;

-DeviceConfig WiFiMgmt::getConfig() const {

-const char* WiFiMgmt::getDeviceId() const {

-const char* WiFiMgmt::getEnvNote() const {

+    if (!LittleFS.exists(CONFIG_FILE_DEVICE)) return false;

+    File f = LittleFS.open(CONFIG_FILE_DEVICE, "r");

+    if (deserializeJson(doc, f)) { f.close(); 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.wifi_ssid,   doc["wifi_ssid"]   | "",                    sizeof(_config.wifi_ssid)-1);

+    strncpy(_config.wifi_pass,   doc["wifi_pass"]   | "",                    sizeof(_config.wifi_pass)-1);

+    Serial.println(F("[WiFi] ✅ 配置已加载")); return true;

-void WiFiMgmt::_setLEDMode(uint8_t mode) {

-        digitalWrite(LED_WIFI_PIN, HIGH);

+    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["wifi_ssid"]=_config.wifi_ssid; doc["wifi_pass"]=_config.wifi_pass;

+    File f = LittleFS.open(CONFIG_FILE_DEVICE, "w");

+    serializeJson(doc, f); f.close(); return true;

-void WiFiMgmt::onProvisionState(ProvisionStateCallback cb) {

+void WiFiMgmt::onProvisionState(ProvisionStateCallback cb) { _provisionStateCb = cb; }

void WiFiMgmt::_emitProvisionState(const char* state) {

-    Serial.printf("[WiFi] 📡 配网状态: %s\n", state);

+    Serial.printf("[WiFi] 📡 %s\n", state);

+bool        WiFiMgmt::isConnected() const { return _connected && WiFi.status()==WL_CONNECTED; }

+DeviceConfig WiFiMgmt::getConfig() const { return _config; }

+const char* WiFiMgmt::getDeviceId() const { return _config.device_id; }

+const char* WiFiMgmt::getEnvNote()  const { return _config.env_note; }

+void WiFiMgmt::_setLEDMode(uint8_t m) { if(m==2){ digitalWrite(LED_WIFI_PIN,HIGH); _ledState=true; } }


Done. The code is clean and minimal. Key points:
1. No WiFiManager dependency - uses ESP32 native WebServer + DNSServer
2. `begin()` - non-blocking, starts AP if no saved WiFi
3. `_tryConnect()` - non-blocking WiFi.begin()
4. `_handleConfig()` - saves config, responds immediately, async connection
5. `loop()` - monitors async connection, auto-closes AP when STA connects
6. HTML served from LittleFS (`/index.html`)
7. Same public API as before (main.cpp compatible)

The user needs to:
1. Upload the HTML to LittleFS first: `pio run --target uploadfs`
2. Then flash the firmware: `pio run --target upload`

Let me remind them.


写完。`wifi_manager.h` 和 `.cpp` 两个文件全部重写，~200行。

**核心设计：**
- 无 WiFiManager 依赖，用 ESP32 原生 WebServer + DNSServer
- `begin()` 不阻塞，有已存 WiFi 后台连，没有直接开 AP
- 配网提交 → 保存配置 → 立即回客户端 → 异步 `WiFi.begin()`（不触发看门狗）
- `loop()` 监控连接状态，连上自动关 AP

**使用：**
```
先: pio run --target uploadfs   (上传 data/index.html 到 LittleFS)
后: pio run --target upload     (烧录固件)
```

配网页面 `data/index.html` 上次已经写好了，不用动。


现在还是之前的问题，配网成功后，我需要，按下重新配网按钮，然他进入重新配网


The user says after successful provisioning, they press the re-provision button but it doesn't enter provisioning mode. Let me trace the flow:

1. System boots, WiFi connects (has saved credentials)
2. User presses GPIO14 for 3 seconds and releases
3. Button manager detects long press → calls `_cbProvision` → `g_wifi.startConfigPortal()`
4. `startConfigPortal()` calls `_startAP()`

In `_startAP()`:
```cpp
WiFi.mode(WIFI_AP_STA);
WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);
```

But if WiFi is already connected in STA mode, switching to `WIFI_AP_STA` should keep the STA connection and add AP. But `WiFi.softAP()` might fail if the STA is already active and the WiFi hardware doesn't support simultaneous AP+STA, or there could be an issue.

Actually, wait. After provisioning succeeds, WiFi connects to the user's router. The `_pendingConnect` flag becomes false, `_connected` becomes true, and `_stopAP()` is called. So the AP is OFF.

Now the user presses the button → `startConfigPortal()` → `_startAP()`. In `_startAP()`:
- `if (_apMode) return;` — _apMode is false, so it continues
- `_apMode = true`
- `WiFi.mode(WIFI_AP_STA)` — switches to AP+STA mode
- `WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD)` — starts AP
- Starts DNS and WebServer

But wait - there's an issue with `WiFi.mode(WIFI_AP_STA)`. If WiFi is currently in STA mode and connected, switching to AP_STA might briefly disconnect and reconnect. This shouldn't cause a crash, but it might take a moment.

The user says "还是之前的问题" - "still the same problem as before". So the button still doesn't trigger re-provisioning.

Let me add debug output. But actually, I just realized: maybe the issue is that `startConfigPortal()` IS called but the AP doesn't actually start, or the AP starts but the user can't see it.

Actually, let me re-read the button code. The button manager processes the long press and calls `_cbProvision`. But `_cbProvision` is set in main.cpp:

```cpp
g_buttons.onProvisionLongPress([]() {
    g_wifi.startConfigPortal();
});
```

This should call `startConfigPortal()`. Let me add a debug print in `startConfigPortal()` to confirm it's called.

Wait, I already have: `Serial.println(F("[WiFi] 🔧 手动配网"));`

If the user doesn't see this in the serial output, then the button is not triggering. This could be because:
1. The button pin is wrong
2. The button is not being held long enough
3. The button callback is not registered

But all of these should work since the button manager was working before.

Actually, let me check if maybe the system is crashing when `startConfigPortal()` is called. After `_startAP()`, the system tries to start the AP and WebServer. If there's a crash in `_startAP()`, we'd see a reboot.

Maybe the issue is that `_server.begin()` fails or crashes when called a second time (after `_server.stop()` was called). Let me check if the ESP32 WebServer library supports stop/begin cycles.

Actually, looking at my code:
```cpp
void WiFiMgmt::_stopAP() {
    if (!_apMode) return;
    _server.stop(); _dns.stop();
    WiFi.softAPdisconnect(true);
    WiFi.mode(WIFI_STA);
    _apMode = false;
}

void WiFiMgmt::_startAP() {
    if (_apMode) return;
    _apMode = true;
    ...
    _server.begin();
}
```

The issue might be that after `_server.stop()`, calling `_server.begin()` again doesn't properly restart the server. The ESP32 WebServer's `stop()` method closes the socket, and `begin()` opens a new one. But there might be issues with re-binding to port 80.

Let me add a Serial.println in `_startAP()` to see if it's being called and if it completes.

Actually wait, let me just add debug prints and test.


加调试确认按钮有没有触发：


Edit

void WiFiMgmt::startConfigPortal() {
    Serial.println(F("[WiFi] 🔧 手动配网"));
    _setLEDMode(1);
    _startAP();
}

void WiFiMgmt::startConfigPortal() {
    Serial.println(F("[WiFi] 🔧 手动配网按钮触发!"));
    _setLEDMode(1);
    _startAP();
}


void WiFiMgmt::startConfigPortal() {
    Serial.println(F("[WiFi] 🔧 手动配网"));
    _setLEDMode(1);
    _startAP();
}

void WiFiMgmt::startConfigPortal() {
    Serial.println(F("[WiFi] 🔧 手动配网按钮触发!"));
    _setLEDMode(1);
    _startAP();
}

// 手动配网 / 重置

-    Serial.println(F("[WiFi] 🔧 手动配网"));

+    Serial.println(F("[WiFi] 🔧 手动配网按钮触发!"));


Actually, let me also check if there's a problem with `_server.stop()` and `_server.begin()` cycle. The ESP32 WebServer might not support stop/begin well. Let me try a different approach: don't call `_server.stop()`. Instead, just keep the server running. When WiFi connects, don't stop the server — just keep it available. This way re-provisioning always works.

Actually, the issue might be even simpler. After WiFi connects and `_stopAP()` is called, the server and DNS are stopped. When `startConfigPortal()` calls `_startAP()`, it calls `_server.begin()` again. But `_server.begin()` might fail silently because:

1. The WiFi stack was in STA mode and is now switching to AP_STA
2. The socket might not bind correctly

Let me try a different approach: don't stop the server at all. Keep AP always available (or at least the server running). When WiFi is connected, AP mode is "dormant" but the server is still ready. Then when the user presses the button, we just need to make sure the AP is broadcasting.

Actually, the cleanest fix: make `_startAP()` work correctly even after `_stopAP()`. The issue might be that after `_server.stop()`, the server can't `begin()` again. Let me try creating a new WiFiServer/WebServer each time instead.

Wait, let me look at this from a different angle. Instead of stopping and restarting the server, let me just NOT stop it. Keep the server running all the time. When WiFi connects, just stop the DNS captive portal (so normal internet works), but keep the web server on port 80. This way `192.168.4.1` is always accessible.

But then port 80 would conflict with the STA interface... Actually, the web server listens on the AP interface (192.168.4.1), not the STA interface. So it should be fine.

Actually, the simplest fix: just don't call `_stopAP()` when WiFi connects. Keep AP+STA running. The user can access the web interface on 192.168.4.1 at any time.

But the user might want the AP to turn off to save power/spectrum. Let me give them the option.

Actually, for now, let me just fix the core issue: make re-provisioning work. The simplest way: keep the AP alive but stop the DNS server after connection.


问题可能是 `_server.stop()` 后再 `_server.begin()` 绑不上端口。改成 AP 常驻，WiFi 连上后只关 DNS，WebServer 保持运行：


void WiFiMgmt::_stopAP() {
    if (!_apMode) return;
    _server.stop(); _dns.stop();
    WiFi.softAPdisconnect(true);
    WiFi.mode(WIFI_STA);
    _apMode = false;
}

void WiFiMgmt::_stopAP() {
    if (!_apMode) return;
    _dns.stop();  // 只关 DNS，WebServer 留着
    _apMode = false;
    Serial.println(F("[WiFi]    AP 休眠 (WebServer 常驻)"));
}


void WiFiMgmt::_stopAP() {
    if (!_apMode) return;
    _server.stop(); _dns.stop();
    WiFi.softAPdisconnect(true);
    WiFi.mode(WIFI_STA);
    _apMode = false;
}

void WiFiMgmt::_stopAP() {
    if (!_apMode) return;
    _dns.stop();  // 只关 DNS，WebServer 留着
    _apMode = false;
    Serial.println(F("[WiFi]    AP 休眠 (WebServer 常驻)"));
}

+    _dns.stop();  // 只关 DNS，WebServer 留着

+    Serial.println(F("[WiFi]    AP 休眠 (WebServer 常驻)"));


Similarly, `_startAP()` needs to be updated so it doesn't try to start the server again (it's already running). It should just re-enable the AP and DNS.


void WiFiMgmt::_startAP() {
    if (_apMode) return;
    _apMode = true;
    _emitProvisionState("ready");

    WiFi.mode(WIFI_AP_STA);
    WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);
    Serial.printf("[WiFi] 🔧 AP: %s IP:192.168.4.1\n", WIFI_AP_SSID);

    _dns.start(53, "*", IPAddress(192, 168, 4, 1));

    static bool routesSet = false;
    if (!routesSet) {
        _server.on("/",              HTTP_GET,  [this](){ _handleRoot(); });
        _server.on("/api/wifi/scan", HTTP_GET,  [this](){ _handleWiFiScan(); });
        _server.on("/api/config",    HTTP_POST, [this](){ _handleConfig(); });
        _server.on("/api/status",    HTTP_GET,  [this](){ _handleStatus(); });
        _server.onNotFound([this](){ _handleNotFound(); });
        routesSet = true;
    }
    _server.begin();
}

void WiFiMgmt::_startAP() {
    if (_apMode) return;
    _apMode = true;
    _emitProvisionState("ready");

    WiFi.mode(WIFI_AP_STA);
    WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);
    Serial.printf("[WiFi] 🔧 AP: %s IP:192.168.4.1\n", WIFI_AP_SSID);

    _dns.start(53, "*", IPAddress(192, 168, 4, 1));

    static bool routesSet = false;
    if (!routesSet) {
        _server.on("/",              HTTP_GET,  [this](){ _handleRoot(); });
        _server.on("/api/wifi/scan", HTTP_GET,  [this](){ _handleWiFiScan(); });
        _server.on("/api/config",    HTTP_POST, [this](){ _handleConfig(); });
        _server.on("/api/status",    HTTP_GET,  [this](){ _handleStatus(); });
        _server.onNotFound([this](){ _handleNotFound(); });
        _server.begin();
        routesSet = true;
        Serial.println(F("[WiFi]    WebServer 已启动"));
    }
}


void WiFiMgmt::_startAP() {
    if (_apMode) return;
    _apMode = true;
    _emitProvisionState("ready");

    WiFi.mode(WIFI_AP_STA);
    WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);
    Serial.printf("[WiFi] 🔧 AP: %s IP:192.168.4.1\n", WIFI_AP_SSID);

    _dns.start(53, "*", IPAddress(192, 168, 4, 1));

    static bool routesSet = false;
    if (!routesSet) {
        _server.on("/",              HTTP_GET,  [this](){ _handleRoot(); });
        _server.on("/api/wifi/scan", HTTP_GET,  [this](){ _handleWiFiScan(); });
        _server.on("/api/config",    HTTP_POST, [this](){ _handleConfig(); });
        _server.on("/api/status",    HTTP_GET,  [this](){ _handleStatus(); });
        _server.onNotFound([this](){ _handleNotFound(); });
        routesSet = true;
    }
    _server.begin();
}

void WiFiMgmt::_startAP() {
    if (_apMode) return;
    _apMode = true;
    _emitProvisionState("ready");

    WiFi.mode(WIFI_AP_STA);
    WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);
    Serial.printf("[WiFi] 🔧 AP: %s IP:192.168.4.1\n", WIFI_AP_SSID);

    _dns.start(53, "*", IPAddress(192, 168, 4, 1));

    static bool routesSet = false;
    if (!routesSet) {
        _server.on("/",              HTTP_GET,  [this](){ _handleRoot(); });
        _server.on("/api/wifi/scan", HTTP_GET,  [this](){ _handleWiFiScan(); });
        _server.on("/api/config",    HTTP_POST, [this](){ _handleConfig(); });
        _server.on("/api/status",    HTTP_GET,  [this](){ _handleStatus(); });
        _server.onNotFound([this](){ _handleNotFound(); });
        _server.begin();
        routesSet = true;
        Serial.println(F("[WiFi]    WebServer 已启动"));
    }
}

+        Serial.println(F("[WiFi]    WebServer 已启动"));


Now also need to update `loop()` — `_apMode` now only controls DNS processing, the server is always handled. Actually no, looking at the loop code:

```cpp
if (_apMode) { _dns.processNextRequest(); _server.handleClient(); }
```

Wait, the `_server.handleClient()` is inside `if (_apMode)`. If _apMode is false after _stopAP(), the server won't process requests. But the server is still running! So the user could still access the web interface at 192.168.4.1 after provisioning.

I should move `_server.handleClient()` outside the `if (_apMode)` check, so it's always processed. The DNS should only be processed in AP mode.


`_server.handleClient()` 也需要在 AP 关掉后继续跑：


if (_apMode) { _dns.processNextRequest(); _server.handleClient(); }

    if (_apMode) { _dns.processNextRequest(); }
    _server.handleClient();  // WebServer 常驻，随时可访问配网页面


    if (_apMode) { _dns.processNextRequest(); }
    _server.handleClient();  // WebServer 常驻，随时可访问配网页面

if (_checkButtonLongPress()) { resetSettings(); return false; }

-    if (_apMode) { _dns.processNextRequest(); _server.handleClient(); }

+    if (_apMode) { _dns.processNextRequest(); }

+    _server.handleClient();  // WebServer 常驻，随时可访问配网页面

// 异步连接: 等结果


Also, `_handleConfig()` calls `_stopAP()` which now doesn't stop the server. Then it calls `_tryConnect()`. Good.

But wait, `_handleConfig()` also calls `_stopAP()` and then `_tryConnect()`. `_tryConnect()` does `WiFi.mode(WIFI_STA)` which switches away from AP mode. This would stop the softAP. The server would still be technically "running" but unreachable since the AP is gone.

For re-provisioning, when the user presses the button, `startConfigPortal()` → `_startAP()` → `WiFi.mode(WIFI_AP_STA)` → `WiFi.softAP(...)`. This should re-enable the AP. And since the server is still running, it should be reachable.

This should work. Let me compile and test.

Actually, I should also make sure the `begin()` function also calls `_server.begin()` on first start. Let me check: in `begin()`, if there's no saved WiFi, it calls `_startAP()` which sets up routes and calls `_server.begin()`. If there IS saved WiFi, it calls `_tryConnect()` but does NOT start the server. So the server isn't running in that case.

If the system boots with saved WiFi and connects successfully, the server is never started. Then when the user presses the button, `startConfigPortal()` → `_startAP()` → routes are already set (static bool), `_server.begin()` is NOT called again (it's inside the `if (!routesSet)` block). So the server runs but was never `begin()`ed.

Wait, on the FIRST call to `_startAP()`, `routesSet` is false, so it sets up routes AND calls `_server.begin()`. On subsequent calls to `_startAP()`, `routesSet` is true, so it skips both routes AND `_server.begin()`. The server should still be running from the first begin.

But in the case where the system boots with saved WiFi (no AP needed), `_startAP()` is never called, so `_server.begin()` is never called. Then when the user presses the button, `_startAP()` is called but routesSet is false (first time), so it sets up routes and calls `_server.begin()`. This should work.

In the case where the system boots WITHOUT saved WiFi, `_startAP()` is called immediately, routes set, server begins. Then user provisions, `_stopAP()` called (only stops DNS), `_tryConnect()` called, WiFi connects. Later, user presses button, `_startAP()` called again. This time `routesSet` is true, so it just starts AP and DNS. Server is already running. Should work.

OK, this looks correct. Let me tell the user to compile and test.


现在 WebServer 初始化后一直在后台跑，配网成功后不会关。重按配网按钮只是把 AP 热点重新广播出来，页面秒开。编译烧录试试。

记得先 `pio run --target uploadfs` 上传 `data/index.html`。


要求现在按下配网按钮，3s后，配网灯闪烁，进入重新配网，可行


The user wants: when pressing the provision button (GPIO14) for 3 seconds, the WiFi LED should blink to indicate provisioning mode, and the system should enter re-provisioning.

Let me think about what's happening currently:

1. Button press → `startConfigPortal()` → `_startAP()` → WiFi LED should blink (we set `_setLEDMode(1)` which for mode 1 does nothing - it relies on loop's LED logic)

Wait, looking at the LED code:
```cpp
void WiFiMgmt::_setLEDMode(uint8_t m) {
    if(m==2){ digitalWrite(LED_WIFI_PIN,HIGH); _ledState=true; }
}
```

For mode 0 and 1, it does nothing! The LED blinking is handled in `loop()`:
```cpp
if (!_connected) {
    uint32_t iv = _apMode ? 300 : 1000;
    ...
}
```

When `startConfigPortal()` is called while WiFi is connected:
1. `_setLEDMode(1)` — does nothing (only mode 2 does something)
2. `_startAP()` — starts AP

But `_connected` is still true (WiFi is connected). So in the LED code: `if (!_connected)` is false, and the LED doesn't blink!

Fix: when AP mode is active, the LED should blink regardless of connection status. Let me update the LED logic.

Also, I need to make sure:
1. Button press (3s) → `_setLEDMode(1)` makes LED blink fast
2. AP starts, user can connect to `192.168.4.1`
3. After re-provisioning, LED goes back to solid (mode 2)

Let me fix the LED logic:
- If `_apMode` is true → fast blink (300ms)
- Else if not connected → slow blink (1000ms)
- Else → solid (HIGH)


    // LED
    if (!_connected) {
        uint32_t iv = _apMode ? 300 : 1000;
        if (millis() - _ledLastToggle > iv) {
            _ledLastToggle = millis(); _ledState = !_ledState;
            digitalWrite(LED_WIFI_PIN, _ledState ? HIGH : LOW);
        }
    }

    // LED: 配网模式快闪, 未连接慢闪, 已连接常亮
    if (_apMode) {
        if (millis() - _ledLastToggle > 300) {
            _ledLastToggle = millis(); _ledState = !_ledState;
            digitalWrite(LED_WIFI_PIN, _ledState ? HIGH : LOW);
        }
    } else if (!_connected) {
        if (millis() - _ledLastToggle > 1000) {
            _ledLastToggle = millis(); _ledState = !_ledState;
            digitalWrite(LED_WIFI_PIN, _ledState ? HIGH : LOW);
        }
    } else {
        digitalWrite(LED_WIFI_PIN, HIGH);
    }


    // LED
    if (!_connected) {
        uint32_t iv = _apMode ? 300 : 1000;
        if (millis() - _ledLastToggle > iv) {
            _ledLastToggle = millis(); _ledState = !_ledState;
            digitalWrite(LED_WIFI_PIN, _ledState ? HIGH : LOW);
        }
    }

    // LED: 配网模式快闪, 未连接慢闪, 已连接常亮
    if (_apMode) {
        if (millis() - _ledLastToggle > 300) {
            _ledLastToggle = millis(); _ledState = !_ledState;
            digitalWrite(LED_WIFI_PIN, _ledState ? HIGH : LOW);
        }
    } else if (!_connected) {
        if (millis() - _ledLastToggle > 1000) {
            _ledLastToggle = millis(); _ledState = !_ledState;
            digitalWrite(LED_WIFI_PIN, _ledState ? HIGH : LOW);
        }
    } else {
        digitalWrite(LED_WIFI_PIN, HIGH);
    }

-        if (millis() - _ledLastToggle > iv) {

+    // LED: 配网模式快闪, 未连接慢闪, 已连接常亮

+        if (millis() - _ledLastToggle > 300) {

+        if (millis() - _ledLastToggle > 1000) {

+            _ledLastToggle = millis(); _ledState = !_ledState;

+            digitalWrite(LED_WIFI_PIN, _ledState ? HIGH : LOW);

+        digitalWrite(LED_WIFI_PIN, HIGH);


Also simplify `_setLEDMode` since LED state is now managed entirely in `loop()`.


void WiFiMgmt::_setLEDMode(uint8_t m) { if(m==2){ digitalWrite(LED_WIFI_PIN,HIGH); _ledState=true; } }

void WiFiMgmt::_setLEDMode(uint8_t m) { /* LED 由 loop() 统一管理 */ }


DeviceConfig WiFiMgmt::getConfig() const { return _config; }

const char* WiFiMgmt::getDeviceId() const { return _config.device_id; }

const char* WiFiMgmt::getEnvNote()  const { return _config.env_note; }

-void WiFiMgmt::_setLEDMode(uint8_t m) { if(m==2){ digitalWrite(LED_WIFI_PIN,HIGH); _ledState=true; } }

+void WiFiMgmt::_setLEDMode(uint8_t m) { /* LED 由 loop() 统一管理 */ }


Done. Now:
- `_apMode = true` → LED fast blink (300ms) regardless of WiFi connection status
- `_apMode = false` and WiFi disconnected → LED slow blink (1000ms)
- `_apMode = false` and WiFi connected → LED solid HIGH

When the user presses the button:
1. `startConfigPortal()` → `_setLEDMode(1)` → `_startAP()` → `_apMode = true`
2. LED starts fast blinking (300ms)
3. AP starts, user can connect to 192.168.4.1
4. User configures WiFi → `_handleConfig()` → `_stopAP()` → `_apMode = false` → `_tryConnect()`
5. When WiFi connects → `_connected = true`, `_apMode = false` → LED solid

This should work. Tell the user to compile and test.


改好了。现在的行为：

- 按住 GPIO14 **3 秒松手** → LED 快闪（300ms）→ AP 热点开启 → 手机连 `HealthMonitor-Setup` → `192.168.4.1` 配网
- 配网成功 → LED 常亮
- 平常未连接 → LED 慢闪（1 秒）

编译烧录试试。


Conversation compacted