This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.

Summary:
1. Primary Request and Intent:
   - Debug why the alarm/buzzer was triggering (found: breath rate 25 exceeding BREATH_RATE_MAX of 24)
   - Recommend ferrite bead for radar 5V power line (suggested 60Ω @ 100MHz Murata BLM18KG601SN1)
   - Replace the WiFi provisioning interface with the software team's custom design (green theme `#12a889`, card-based layout, modal guide, only device_id and env_note params visible)
   - When provisioning succeeds, send provision status + online MQTT message together
   - Optimize OLED display refresh (5000ms → 1000ms, I2C speed, static buffers)
   - Implement PIR sensor cross-validation for fall detection
   - Provide GPIO mapping and PCB enclosure cutout specifications from PickAndPlace file
   - Fix system rebooting issues caused by code changes
   - Make re-provisioning button (GPIO14, 3-second long press) work with LED blinking indication

2. Key Technical Concepts:
   - ESP32-S3 + R60ABD1 60GHz mmWave radar health monitoring system
   - WiFi provisioning: WiFiManager library vs WebServer + DNSServer + LittleFS (方案一 vs 方案二)
   - Captive portal via DNS hijacking (DNSServer)
   - Non-blocking WiFi connection to avoid ESP32 task watchdog resets
   - `git stash` + `git stash drop` causing permanent data loss; recovery via `git fsck --unreachable`
   - PROGMEM string templates in WiFiManager's wm_strings_zh.h (cannot be safely modified)
   - ArduinoJson for config persistence in LittleFS
   - `WiFi.mode(WIFI_AP_STA)` to run AP and STA simultaneously
   - GPIO pin conflicts (OLED SDA/SCL vs provision button)

3. Files and Code Sections:
   - **src/wifi_manager.cpp** (FINAL VERSION - WebServer + LittleFS approach):
     - Complete rewrite replacing WiFiManager with ESP32 WebServer + DNSServer
     - Public API maintained: `begin()`, `loop()`, `isConnected()`, `getConfig()`, `startConfigPortal()`, `resetSettings()`, `onProvisionState()`, `isFreshProvision()`, `clearFreshProvision()`
     - Non-blocking `_tryConnect()` for async WiFi connection
     - `_handleConfig()` saves config, responds immediately, then async connects
     - LED logic: AP mode fast blink (300ms), unconnected slow blink (1000ms), connected solid
     - Key functions:
```cpp
void WiFiMgmt::_startAP() {
    if (_apMode) return;
    _apMode = true;
    _emitProvisionState("ready");
    WiFi.mode(WIFI_AP_STA);
    WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);
    _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;
    }
}
void WiFiMgmt::_stopAP() {
    if (!_apMode) return;
    _dns.stop();  // 只关 DNS，WebServer 留着
    _apMode = false;
}
```

   - **include/wifi_manager.h** (FINAL VERSION):
     - Removed WiFiManager dependency, added WebServer, DNSServer
     - Added `wifi_ssid[33]`, `wifi_pass[65]` to DeviceConfig struct
     - Added `_pendingConnect`, `_connectStartTime`, `_apMode` members

   - **data/index.html**:
     - Custom provisioning page with green theme CSS, card layout, modal guide
     - JS API calls: GET `/api/wifi/scan`, POST `/api/config`, GET `/api/status`
     - Software team's design: "观照-五劳无感 · 设备配网"

   - **include/config.h** (recovered from stash, then corrected):
     - Final GPIO: OLED_SDA=15, OLED_SCL=16, BTN_PROVISION=14, BTN_ALARM_DISMISS=13, BTN_PAGE_SWITCH=46, WATER=45, BUZZER=21, DHT11=4
     - Was corrupted to OLED_SDA=14 (conflicting with BTN_PROVISION) by linter after git checkout

   - **src/button_manager.cpp**: Added debug print for long press detection
```cpp
Serial.printf("[按钮] GPIO%d 长按触发! hold=%dms\n", btn.pin, (int)holdTime);
```

   - **src/main.cpp**: Uses `isFreshProvision()`, `clearFreshProvision()`, `onProvisionState()` for provision+online MQTT logic
   - **src/display_manager.cpp**: Had refresh interval changed to 1000ms, `_fmtFloat`/`_fmtUptime` changed to static buffers (may have been reverted)

4. Errors and fixes:
   - **System reboot loop after WiFi changes**: Caused by `_connectWiFi()` blocking 15 seconds in HTTP handler context → watchdog reset. Fixed by making WiFi connection async (`_pendingConnect` flag, `_tryConnect()` non-blocking).
   - **Git stash drop lost all user code**: `git stash` + `git stash drop` deleted all working modifications. Recovered via `git fsck --unreachable` finding commit `93de300` and `git checkout 93de300 -- .`
   - **OLED not working, wrong GPIOs**: After git stash drop, config.h was corrupted by linter. OLED_SDA=14 conflicted with BTN_PROVISION=14. Fixed by restoring correct pins (OLED=15/16, BTN_PROVISION=14).
   - **WiFiManager wm_strings_zh.h modification caused crashes**: Library PROGMEM template modifications caused system instability. Abandoned this approach.
   - **`setCustomMenuHTML` only affects root page `/`**: The actual WiFi form is on `/wifi` page which uses library's hardcoded HTML. CSS injection works but HTML structure can't be changed.
   - **`_server.stop()` then `_server.begin()` fails**: WebServer can't reliably restart. Fixed by keeping server always running (only DNS stops), AP just re-broadcasts when needed.
   - **LED not blinking in AP mode when WiFi connected**: LED code only blinked when `!_connected`, but during re-provisioning WiFi is still connected. Fixed by separating AP mode blinking from connection status.

5. Problem Solving:
   - Diagnosed alarm trigger as breath rate exceeding threshold (25 > 24)
   - Provided ferrite bead recommendation for radar power filtering
   - Analyzed WiFiManager architecture limitations for UI customization
   - Recovered lost code from git unreachable objects
   - Successfully replaced WiFiManager with native WebServer approach
   - Fixed multiple GPIO conflicts from corrupted config.h

6. All user messages:
   - "看看是为啥报警" (why is the alarm going off)
   - "在雷达接入5v的磁珠，需要多少om" (ferrite bead question)
   - "配网界面的代码在哪里" (where is the provisioning UI code)
   - "HTML是烧录在单片机里了吗" (is HTML flashed into MCU)
   - "我现在需要把配网界面优化。把代码给软件组" (optimize UI for software team)
   - "代码给就行，软件组优化" (just give code, software team optimizes)
   - "主要的html配网界面在哪里。给我" (where is the main HTML UI)
   - "把什么给他们" (what to give them)
   - "每次配网成功，的提示要和online在一起发送" (provision success and online sent together)
   - "该雷达的检测跌倒准吗" (is fall detection accurate)
   - "40 块的红外人体传感器并联做交叉验证比怎么整" (PIR cross-validation)
   - "现在跟新了新的配网界面，我给你看看，你把它改过去" (new UI design to integrate)
   - New UI code provided (hardware配网.txt with R"HTML(...)" raw strings)
   - "不行" repeated multiple times about rebooting
   - "方案一你写也得一天吗" (will option 1 take you a day)
   - "行你写吧，ui界面，用我之前给你发的" (write it, use the UI I sent)
   - "现在无论是重启，啥的oled屏幕显示的有点慢" (OLED slow)
   - "系统，一直重新启动" (system keeps rebooting)
   - "不对啊，原本就是，改14配网，15，和16是ic" (GPIO14 is provision, 15/16 are I2C)
   - "咋办" (what to do)
   - "怎么 好多gpio口都变了" (why did many GPIOs change)
   - "你给我完整的gpio" (give me complete GPIO)
   - "到底执行了啥" (what exactly did you execute)
   - "git stash drop啥意思" (what does git stash drop mean)
   - "不清楚" (not sure)
   - "还能找回吗" (can it be recovered)
   - "基本好了，现在把配网界面换位，我们之前最新的，其他不变，避免之前的问题" (basically working, now swap to latest UI)
   - "方案一：WebServer + LittleFS用这个，和之前我给你的ui设计网页一样" (use option 1)
   - "现在还是之前的问题，配网成功后，我需要，按下重新配网按钮，然他进入重新配网" (re-provision button issue)
   - "要求现在按下配网按钮，3s后，配网灯闪烁，进入重新配网，可行" (button 3s press, LED blink, enter re-provisioning)

7. Pending Tasks:
   - Compile and flash the final WebServer + LittleFS code
   - Upload `data/index.html` to LittleFS via `pio run --target uploadfs`
   - Test re-provisioning button (GPIO14, 3-second hold) with LED blinking
   - Verify the provisioning page displays correctly at 192.168.4.1
   - Verify WiFi connection succeeds after form submission
   - Verify re-provisioning works after successful connection

8. Current Work:
   Just completed LED logic fix so that when re-provisioning button is pressed:
   - LED fast blinks (300ms) in AP mode regardless of WiFi connection status
   - WebServer stays running (only DNS stops/restarts)
   - `startConfigPortal()` → `_startAP()` re-enables softAP broadcast

   Final state of wifi_manager.cpp has:
   - LED logic: `_apMode` → fast blink, `!_connected` → slow blink, else → solid HIGH
   - `_startAP()`: sets up routes and begins server only once (static bool routesSet)
   - `_stopAP()`: only stops DNS, keeps server alive
   - `loop()`: `_server.handleClient()` runs unconditionally, DNS only in AP mode

9. Optional Next Step:
   User needs to: (1) `pio run --target uploadfs` to upload data/index.html, (2) `pio run --target upload` to flash firmware, (3) test GPIO14 3-second hold for LED blink + re-provisioning.

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

If you need specific details from before compaction (like exact code snippets, error messages, or content you generated), read the full transcript at: C:\Users\杨志佳\.claude\projects\d--PlatformIO1-Projects-Healthv2-0\d07c3716-a856-4ff7-b6ef-cd0a3519488e.jsonl
Continue the conversation from where it left off without asking the user any further questions. Resume directly — do not acknowledge the summary, do not recap what was happening, do not preface with "I'll continue" or similar. Pick up the last task as if the break never happened.


<local-command-caveat>Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to.</local-command-caveat>


<command-name>/compact</command-name>
            <command-message>compact</command-message>
            <command-args></command-args>


<local-command-stdout>Compacted </local-command-stdout>


/**
 * @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();  // WebServer 常驻，随时可访问配网页面

    // 异步连接: 等结果
    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 (_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);
    }

    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(); });
        _server.begin();
        routesSet = true;
        Serial.println(F("[WiFi]    WebServer 已启动"));
    }
}

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

// ============================================================
// 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) { /* LED 由 loop() 统一管理 */ }


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


/**
 * @file button_manager.cpp
 * @brief 三按钮管理器实现 - 去抖 + 长按/短按
 */

#include "button_manager.h"

// ============================================================
// 构造
// ============================================================

ButtonManager::ButtonManager()
    : _cbProvision(nullptr)
    , _cbDismiss(nullptr)
    , _cbPage(nullptr)
{
    _btnProvision = { BTN_PROVISION_PIN,     false, 0, false };
    _btnDismiss   = { BTN_ALARM_DISMISS_PIN, false, 0, false };
    _btnPage      = { BTN_PAGE_SWITCH_PIN,   false, 0, false };
}

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

void ButtonManager::begin() {
    pinMode(BTN_PROVISION_PIN,     INPUT_PULLUP);
    pinMode(BTN_ALARM_DISMISS_PIN, INPUT_PULLUP);
    pinMode(BTN_PAGE_SWITCH_PIN,   INPUT_PULLUP);

    Serial.println(F("[按钮] ✅ 三按钮初始化完成"));
    Serial.printf("  配网: GPIO%d | 解除报警: GPIO%d | 翻页: GPIO%d\n",
                  BTN_PROVISION_PIN, BTN_ALARM_DISMISS_PIN, BTN_PAGE_SWITCH_PIN);
}

// ============================================================
// 主循环轮询
// ============================================================

void ButtonManager::loop() {
    // 按钮1: 配网 (仅长按)
    _processButton(_btnProvision, false, true);

    // 按钮2: 解除报警 (仅短按)
    _processButton(_btnDismiss, true, false);

    // 按钮3: 翻页 (仅短按)
    _processButton(_btnPage, true, false);
}

// ============================================================
// 单按钮状态机
// ============================================================

void ButtonManager::_processButton(ButtonState& btn, bool hasShort, bool hasLong) {
    bool raw = _readPin(btn.pin);  // LOW = 按下

    if (raw && !btn.pressed) {
        btn.pressed = true;
        btn.pressTime = millis();
        btn.longTriggered = false;
    }
    else if (!raw && btn.pressed) {
        btn.pressed = false;
        uint32_t holdTime = millis() - btn.pressTime;

        if (holdTime < BTN_DEBOUNCE_MS) {
            return;
        }

        if (hasLong && holdTime >= BTN_LONG_PRESS_MS && !btn.longTriggered) {
            btn.longTriggered = true;
            if (_cbProvision) _cbProvision();
        }
        else if (hasShort && holdTime >= BTN_DEBOUNCE_MS && holdTime < BTN_LONG_PRESS_MS) {
            if (btn.pin == BTN_ALARM_DISMISS_PIN && _cbDismiss) _cbDismiss();
            if (btn.pin == BTN_PAGE_SWITCH_PIN   && _cbPage)    _cbPage();
        }
    }
}

bool ButtonManager::_readPin(uint8_t pin) {
    return digitalRead(pin) == LOW;  // INPUT_PULLUP: LOW = 按下
}


/**
 * @file config.h
 * @brief 全局配置文件 - 五劳无感康养监测系统
 *
 * 协议版本: V0.08 (2026-07-17)
 * 包含: 硬件引脚定义、WiFi/MQTT 默认参数、
 *        雷达协议常量、告警阈值、任务配置等
 */

#ifndef CONFIG_H
#define CONFIG_H

#include <Arduino.h>

// ============================================================
// 1. 硬件引脚定义 (ESP32-S3)
// ============================================================

// --- R60ABD1 毫米波雷达 ---
#define RADAR_UART_PORT       Serial2
#define RADAR_RX_PIN          17
#define RADAR_TX_PIN          1             // UART 发送 (从18挪到1)
#define RADAR_BAUD_RATE       115200

// --- 雷达备用 GPIO ---
#define RADAR_GPIO1_PIN       2             // 活跃/静止指示 (从10挪到2)
#define RADAR_GPIO2_PIN       11            // 有人/无人指示

// --- DHT11 温湿度传感器 ---
#define DHT11_PIN             4             // 温湿度传感器

// --- 水浸传感器 ---
#define WATER_SENSOR_PIN      45            // 数字输入, 低电平=有水

// --- 蜂鸣器 (PWM) ---
#define BUZZER_PIN            21
#define BUZZER_PWM_CHANNEL    0
#define BUZZER_PWM_FREQ       2000
#define BUZZER_PWM_RESOLUTION 8

// --- 配网按钮 ---
#define CONFIG_BUTTON_PIN     0             // 长按 3 秒复位 WiFi
#define BUTTON_LONG_PRESS_MS  3000

// --- 状态指示灯 ---
#define LED_WIFI_PIN          8             // 蓝色 - WiFi 状态
#define LED_ALARM_PIN         9             // 红色 - 报警指示

// --- OLED 显示屏 (SSD1306 128x64 I2C) ---
#define OLED_SDA_PIN          15            // I2C 数据线
#define OLED_SCL_PIN          16            // I2C 时钟线

// --- 交互按钮 ---
#define BTN_PROVISION_PIN     14            // 擦除WiFi配网按钮 (长按3秒)
#define BTN_ALARM_DISMISS_PIN 13            // 解除报警按钮
#define BTN_PAGE_SWITCH_PIN   46            // 翻页按钮
#define BTN_DEBOUNCE_MS       50            // 去抖时间
#define BTN_LONG_PRESS_MS     1500          // 长按判定 (1.5秒)

// ============================================================
// 2. 设备标识 (协议规范 §2)
// ============================================================

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

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

#define WIFI_AP_SSID          "HealthMonitor-Setup"
#define WIFI_AP_PASSWORD      "12345678"
#define WIFI_CONFIG_TIMEOUT   60
#define WIFI_CONNECT_TIMEOUT  15
#define WIFI_RETRY_INTERVAL   10000

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

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

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

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

// --- 下行主题 (服务端 → 设备) ---
#define MQTT_TOPIC_CMD_ALARM          "cmd/%s/alarm"                    // 告警指令 QoS1
#define MQTT_TOPIC_CMD_ALARM_RESTORE  "cmd/%s/alarm/restore"            // 告警恢复 QoS1
#define MQTT_TOPIC_CMD_CONTROL        "cmd/%s/control"                  // 控制指令 QoS1
#define MQTT_TOPIC_CMD_PROVISION_WIFI "cmd/%s/provision/wifi"           // WiFi配网 QoS1

// ============================================================
// 6. 上报配置 (协议规范 §4.1, §7.2)
// ============================================================

#define REPORT_INTERVAL_MS    1000                // 遥测上报周期 1秒
#define REPORT_QOS            0                   // 遥测 QoS 0
#define STATUS_QOS            1                   // 状态/响应 QoS 1

// ============================================================
// 7. 报警阈值 (协议规范 §6, 服务端判定用)
// ============================================================

#define HEART_RATE_MIN        50
#define HEART_RATE_MAX        120
#define BREATH_RATE_MIN       8
#define BREATH_RATE_MAX       24
#define TEMP_LOW              5.0f
#define TEMP_HIGH             45.0f
#define HUMI_LOW              15.0f
#define HUMI_HIGH             90.0f
#define WATER_DEBOUNCE_MS     50

// ============================================================
// 8. 任务配置
// ============================================================

#define RADAR_TASK_STACK      4096
#define RADAR_TASK_PRIORITY   5
#define RADAR_TASK_CORE       0

#define MQTT_TASK_STACK       8192
#define MQTT_TASK_PRIORITY    4
#define MQTT_TASK_CORE        1

// ============================================================
// 9. NTP 时间同步
// ============================================================

#define NTP_SERVER            "ntp.aliyun.com"
#define NTP_GMT_OFFSET_SEC    28800             // UTC+8 (北京时间)
#define NTP_DAYLIGHT_OFFSET   0

// ============================================================
// 10. LittleFS 配置文件名
// ============================================================

#define CONFIG_FILE_MQTT      "/mqtt_config.json"
#define CONFIG_FILE_WIFI      "/wifi_config.json"
#define CONFIG_FILE_DEVICE    "/device_config.json"

// ============================================================
// 11. 结构体: 全量遥测数据 (协议规范 §4.1)
// ============================================================

struct TelemetryData {
    // --- 设备标识 ---
    char     saleid[12];          // 站点编号 "10100001"
    char     gateid[16];          // 网关编号 "10100001001"
    char     meterId[8];          // 节点ID "ENV001"
    char     meterName[16];       // 设备型号 "RadarDHT11"

    // --- 环境数据 (DHT11) ---
    float    temp;                // 温度 ℃
    float    humi;                // 湿度 %RH
    bool     dhtValid;            // 温湿度有效性

    // --- 雷达: 存在/运动 ---
    bool     presence;            // 有人=1 无人=0
    bool     motion;              // 有运动=1 无运动=0
    bool     fall;                // 跌倒检测 1=触发
    bool     resident;            // 驻留状态 1=驻留

    // --- 雷达: 生命体征 ---
    uint8_t  heartRate;           // 心率 bpm (无效=0)
    uint8_t  breathRate;          // 呼吸 次/分 (无效=0)
    uint8_t  body;                // 人体能量/体征值
    bool     radarValid;          // 雷达数据有效性
    bool     vitalValid;          // 心率呼吸有效性

    // --- 水浸 ---
    bool     rainDO;              // 1=正常 0=水浸触发
    bool     rainValid;           // 水浸传感器有效性

    // --- 设备状态 ---
    bool     wifiReady;           // WiFi/MQTT 连接状态
    uint32_t uptimeSec;           // 设备累计运行秒数
    bool     buzzerState;         // 蜂鸣器状态 1=开启

    // --- 时间戳 ---
    uint32_t lastUpdate;          // 最后更新 ms
};

// 结构体: 下行报警指令 (协议规范 §5.1, §5.2)
struct AlarmCommand {
    char     cmdId[32];           // 指令唯一ID
    char     type[20];            // alarm / alarm_restore
    char     alarmType[24];       // fall/water_leak/heart_abnormal/breath_abnormal
    char     alarmLevel[8];       // low/medium/high
    char     alarmDesc[64];       // 告警描述
    char     meterId[8];          // 触发节点ID
    char     relatedCmdId[32];    // 关联原始告警ID (恢复用)

    // 联动动作
    bool     buzzerOn;            // 蜂鸣器开关
    uint16_t buzzerDuration;      // 蜂鸣持续秒数 (0=持续到恢复)
    bool     nightLight;          // 夜灯

    uint32_t timestamp;
};

// 结构体: 下行控制指令 (协议规范 §5.3)
struct ControlCommand {
    char     cmdId[32];
    char     type[20];            // control
    char     controlType[24];     // buzzer_test/device_restart/report_interval_set/factory_reset
    uint32_t params_duration;     // 参数: 持续秒数
    uint32_t params_interval;     // 参数: 上报间隔
    uint32_t timestamp;
};

// 结构体: 下行配网指令 (协议规范 §5.4)
struct ProvisionCommand {
    char     cmdId[32];
    char     wifi_ssid[33];
    char     wifi_password[65];
    char     wifi_encrypt[8];     // OPEN/WPA2/WPA3
    char     mqtt_broker[64];
    uint16_t mqtt_port;
    bool     mqtt_useTls;
    uint32_t timestamp;
};

#endif // CONFIG_H


claude claude-code-guide Explore general-purpose Plan statusline-setup

- claude: Catch-all for any task that doesn't fit a more specific agent. FleetView's default when no agent name is typed. (Tools: *)

- claude-code-guide: Use this agent when the user asks questions ("Can Claude...", "Does Claude...", "How do I...") about: (1) Claude Code (the CLI tool) - features, hooks, slash commands, MCP servers, settings, IDE integrations, keyboard shortcuts; (2) Claude Agent SDK - building custom agents; (3) Claude API (formerly Anthropic API) - Messages API for directly passing messages to Claude, Tool Runner (`client.beta.messages.tool_runner`) for running an agentic loop over your own tools, manual tool-use loops, Managed Agents for server-hosted agents with a managed sandbox, prompt caching, and general Anthropic SDK usage; (4) Claude Tag (Claude in Slack) - what it is, setting it up for a Slack workspace, `/install-slack-app`. **IMPORTANT:** Before spawning a new agent, check if there is already a running or recently completed claude-code-guide agent that you can continue via SendMessage. (Tools: Glob, Grep, Read, WebFetch, WebSearch)

- Explore: Read-only search agent for broad fan-out searches — when answering means sweeping many files, directories, or naming conventions and you only need the conclusion, not the file dumps. It reads excerpts rather than whole files, so it locates code; it doesn't review or audit it. Specify search breadth: "medium" for moderate exploration, "very thorough" for multiple locations and naming conventions. (Tools: All tools except Agent, Artifact, ExitPlanMode, Edit, Write, NotebookEdit)

- general-purpose: General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you. (Tools: *)

- Plan: Software architect agent for designing implementation plans. Use this when you need to plan the implementation strategy for a task. Returns step-by-step plans, identifies critical files, and considers architectural trade-offs. (Tools: All tools except Agent, Artifact, ExitPlanMode, Edit, Write, NotebookEdit)

- statusline-setup: Use this agent to configure the user's Claude Code status line setting. (Tools: Read, Edit)


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


按下配网按钮后，闪烁出闪烁很慢，


The user is saying that after pressing the provision button, the LED blinks very slowly. Let me analyze the code to understand the issue.

Looking at the flow:

1. Button long press detected in `button_manager.cpp` → calls `_cbProvision()` callback
2. This callback is likely connected to `WiFiMgmt::startConfigPortal()` in `main.cpp`
3. `startConfigPortal()` calls `_startAP()` which sets `_apMode = true`

The problem is in the LED timing in `loop()`:

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

The issue is that `_ledLastToggle` is shared across all modes. When the device is already connected (LED steady HIGH, no toggling happening), `_ledLastToggle` is stale - it's some old value. When `_apMode` is set to true and loop runs, `millis() - _ledLastToggle` may be very large, but that should cause an immediate toggle.

Wait, actually the issue might be different. When connected and LED is HIGH (steady), the `_ledLastToggle` value is old. When entering AP mode, the first check `millis() - _ledLastToggle > 300` would be true (since it's been a long time since last toggle), so it should toggle immediately. That seems fine.

But wait - there's a subtlety. The `_ledLastToggle` is only updated in the non-steady branches. When connected (steady HIGH), `_ledLastToggle` is never updated. So when switching to AP mode, `millis() - _ledLastToggle` will be very large, causing an immediate toggle. That's fine.

Hmm, let me think about this differently. The user says the LED blinks "很慢" (very slowly) after pressing the button.

Wait - I need to check what happens when `startConfigPortal()` is called when WiFi is already connected. The flow:
1. `startConfigPortal()` → `_startAP()` → sets `_apMode = true`
2. In `loop()`, the `_apMode` branch runs, LED blinks fast (300ms)

But there's another consideration. `startConfigPortal()` calls `_setLEDMode(1)` which does nothing (the function body is empty: `void WiFiMgmt::_setLEDMode(uint8_t m) { /* LED 由 loop() 统一管理 */ }`).

So the LED blinking should follow the `_apMode` branch with 300ms. That should be fast...

Unless... The button press detection in `button_manager.cpp` uses `BTN_PROVISION_PIN` (GPIO14), but in `config.h` there's also `CONFIG_BUTTON_PIN` which is GPIO 0. And in `wifi_manager.cpp` line 245, `_checkButtonLongPress()` reads `CONFIG_BUTTON_PIN` which is GPIO 0, not GPIO14.

Wait, there are TWO separate button detection mechanisms:
1. `WiFiMgmt::_checkButtonLongPress()` - reads `CONFIG_BUTTON_PIN` (GPIO 0), triggers `resetSettings()` (full reset)
2. `ButtonManager::loop()` - reads `BTN_PROVISION_PIN` (GPIO 14), triggers `_cbProvision()` callback → `startConfigPortal()`

So the external `ButtonManager` handles GPIO14 long press → `startConfigPortal()`. And `WiFiMgmt` also has its own `_checkButtonLongPress()` on GPIO 0 which does `resetSettings()`.

The user is pressing GPIO14 (BTN_PROVISION_PIN). The `ButtonManager` detects long press and calls the callback, which should be `startConfigPortal()`.

Let me check what `startConfigPortal()` does:
```cpp
void WiFiMgmt::startConfigPortal() {
    Serial.println(F("[WiFi] 🔧 手动配网按钮触发!"));
    _setLEDMode(1);  // This is a no-op
    _startAP();
}
```

`_startAP()` sets `_apMode = true`.

Then in `loop()`:
```cpp
if (_apMode) {
    if (millis() - _ledLastToggle > 300) {
        _ledLastToggle = millis(); _ledState = !_ledState;
        digitalWrite(LED_WIFI_PIN, _ledState ? HIGH : LOW);
    }
}
```

The issue might be that `_ledLastToggle` is a stale value from before. When WiFi was connected, LED was steady HIGH, and `_ledLastToggle` hadn't been updated in a while. So the first check `millis() - _ledLastToggle > 300` would be true, causing an immediate toggle. That's fine for the first toggle.

But then after the first toggle, `_ledLastToggle` is updated to current `millis()`, and the next check 300ms later should toggle again. This should work correctly.

Hmm, wait. I think the user might be saying that the LED blinking is slow in general - maybe they're comparing to what they expect. 300ms blink period = 600ms full cycle (on+off). That might be perceived as "slow" if they expect a very rapid blink.

Or... maybe the problem is that `loop()` isn't being called frequently enough. If `loop()` is blocked elsewhere, the LED toggle would be delayed.

Actually, let me re-read the user's message: "按下配网按钮后，闪烁出闪烁很慢" - After pressing the provision button, the blinking is very slow.

The issue might be simpler than I think. Let me look at where `_ledLastToggle` gets initialized. In the constructor:
```cpp
, _ledLastToggle(0), _ledState(false)
```

When `startConfigPortal()` is called and `_apMode` becomes true, the first time `loop()` runs:
- `millis() - _ledLastToggle` = `millis() - 0` = a large number > 300
- So LED toggles immediately to HIGH (since `_ledState` starts as false, `!_ledState` = true)

Then 300ms later it toggles to LOW, etc. This seems correct at 300ms rate.

But wait - maybe the issue is that after `startConfigPortal()` is called, the system also enters `_tryConnect()` or some other state that changes `_apMode` back to false? Let me check...

`startConfigPortal()` only calls `_startAP()`, it doesn't call `_tryConnect()`. So `_apMode` should stay true.

But there's `_checkButtonLongPress()` inside `WiFiMgmt::loop()` that reads GPIO 0 (CONFIG_BUTTON_PIN). If GPIO 0 happens to be LOW (maybe floating or pulled low), it could trigger `resetSettings()`.

Actually, I think I need to look at the actual main.cpp to see how the button callback is wired. Let me also check if there's an issue with the LED speed.

Actually wait - I just realized something. The user might be seeing the LED blink at 1000ms (slow blink, not the AP fast blink at 300ms). This would happen if `_apMode` is false but `_connected` is also false.

Let me trace through what happens:
1. User presses GPIO14 for 3 seconds
2. `ButtonManager` detects long press, calls callback
3. Callback calls `WiFiMgmt::startConfigPortal()`
4. `startConfigPortal()` calls `_startAP()`, sets `_apMode = true`
5. LED should now blink at 300ms

But what if the callback isn't properly connected? What if `startConfigPortal()` is not being called at all?

Actually, re-reading the code more carefully... I see that `WiFiMgmt::_checkButtonLongPress()` checks `CONFIG_BUTTON_PIN` (GPIO 0) and calls `resetSettings()`. This is a SEPARATE mechanism from the `ButtonManager`.

The `ButtonManager` on GPIO14 calls `_cbProvision()` which should call `startConfigPortal()`.

But wait - in `main.cpp`, the callback needs to be set up. Let me check if there's a potential issue with the callback.

Actually, I think the most likely issue is what the user is describing. Let me consider: what if when already connected and the user presses the button, `startConfigPortal()` runs but then something immediately calls `_stopAP()` because WiFi is still connected?

Look at `loop()`:
```cpp
// 后台连接成功 → 关 AP
if (!_pendingConnect && !_connected && WiFi.status() == WL_CONNECTED) {
    _connected = true; _setLEDMode(2); _emitProvisionState("success");
    ...
    if (_apMode) _stopAP();
}
```

This won't fire because `_connected` is already true (WiFi was connected before).

But what about this:
```cpp
if (_pendingConnect) {
    if (WiFi.status() == WL_CONNECTED) {
        _pendingConnect = false; _connected = true;
        ...
        _stopAP();
    }
}
```
This won't fire because `_pendingConnect` is false.

Hmm, I don't see an obvious code path that would immediately exit AP mode. But let me think about the LED timing issue more carefully.

OH WAIT. I think I see the problem now. Look at how `_ledLastToggle` is used:

When in connected mode (steady HIGH), `_ledLastToggle` is not updated. But `_ledState` might be left as `true` or `false` from the last toggle before entering connected mode.

When entering AP mode, the first toggle happens immediately (since `millis() - _ledLastToggle` is large). After that, toggles happen every 300ms.

But the user says it's slow. Could the issue be that `loop()` isn't running frequently enough? If `loop()` takes 500ms to run (blocked by other operations), the effective blink rate would be much slower.

Let me check if there are blocking operations in the main loop. I should look at main.cpp.

Actually, let me also consider another angle. The user says "闪烁出闪烁很慢" which could mean the LED is blinking slowly. With the current code, the slow blink is 1000ms. Is it possible that `_apMode` is not being set correctly?

Let me check: `_startAP()` has a guard:
```cpp
void WiFiMgmt::_startAP() {
    if (_apMode) return;
    ...
}
```

What if `_apMode` is already true when `startConfigPortal()` is called? That could happen if AP mode was already started during `begin()` (when there's no saved WiFi config).

But in the typical scenario where WiFi is connected and user re-provisions, `_apMode` should be false.

Let me focus on the most likely issue: maybe the LED still blinks slow (1000ms) despite `_apMode` being true. This could happen if there's a timing issue with `_ledLastToggle`.

Actually, I just realized something. The `_ledLastToggle` is shared between the fast blink (300ms) and slow blink (1000ms) states. When transitioning from slow blink to fast blink:

1. Slow blink: `_ledLastToggle` is updated every 1000ms
2. Transition to AP mode: `_apMode = true`
3. Now checking `millis() - _ledLastToggle > 300`

If `_ledLastToggle` was updated recently (say 100ms ago), then `millis() - _ledLastToggle = 100`, which is NOT > 300, so no toggle. The next check would be whenever `loop()` runs again. If `loop()` runs frequently (every few ms), then we'd need to wait until `millis() - _ledLastToggle > 300`, which is about 200ms from now. That seems fine.

But what if `loop()` has blocking sections? The 300ms check would be delayed.

I think the most practical fix is to reset `_ledLastToggle` to 0 when entering AP mode, so the LED toggles immediately. And also make sure the LED is properly managed.

Actually, let me reconsider. Looking at the `_setLEDMode` function - it's a no-op! But it's called in several places:
- `_setLEDMode(2)` when connected
- `_setLEDMode(0)` when disconnected
- `_setLEDMode(1)` in `startConfigPortal()`

Since `_setLEDMode` does nothing, the LED state is entirely managed by `loop()`. The `_setLEDMode` calls are decorative.

OK let me focus on what would actually make the LED blink slowly after pressing the button. The most probable cause:

When WiFi is already connected and the user presses the re-provision button:
1. `startConfigPortal()` is called
2. `_startAP()` sets `_apMode = true`
3. In `loop()`, the `_apMode` branch runs with 300ms toggle

But `_ledLastToggle` might have a recent value from the slow-blink era (if WiFi disconnected briefly) or a very old value (if WiFi was connected for a long time). Either way, the first toggle should happen quickly.

I wonder if the issue is actually about the BUTTON'S own LED indication during the 3-second press, not the WiFi LED after AP mode starts. The user might be saying the LED feedback during the button hold is slow.

Hmm, but the code doesn't have LED feedback during the button hold - the ButtonManager just detects the long press and fires the callback.

Let me just look at main.cpp to understand the full picture, and also check if there's an obvious issue.

Let me also consider: maybe the problem is that when the button is pressed while WiFi is connected, the LED stays on (connected = HIGH), and only after 3 seconds when `startConfigPortal()` runs does the LED start blinking. But the user wants LED feedback DURING the 3-second hold. Currently there's no LED feedback during the hold.

But the user's message is "按下配网按钮后，闪烁出闪烁很慢" - "After pressing the provision button, the blinking is very slow." This suggests the blinking does happen, but is slow.

I think the most likely issue is that `_apMode` is being set but then something else is interfering. Or perhaps `_connected` is remaining true while `_apMode` is also true, and some interaction between states causes issues.

Wait, actually - I just realized there could be a race condition. When `_startAP()` is called:
```cpp
WiFi.mode(WIFI_AP_STA);
```

This changes WiFi mode. If WiFi was already connected in STA mode, changing to AP_STA might briefly disconnect and reconnect WiFi. During this transition, `WiFi.status()` might not be `WL_CONNECTED`, causing the disconnect detection to set `_connected = false`. Then since `_apMode` is true, the LED fast blinks. That should be correct...

Unless the WiFi quickly reconnects and then `_stopAP()` is called by some code path...

Look at this code in `loop()`:
```cpp
// 后台连接成功 → 关 AP
if (!_pendingConnect && !_connected && WiFi.status() == WL_CONNECTED) {
    _connected = true; _setLEDMode(2); _emitProvisionState("success");
    ...
    if (_apMode) _stopAP();
}
```

If `_connected` was set to false during the WiFi mode change, and then WiFi reconnects, this code would call `_stopAP()`! That would set `_apMode = false`, and then the LED would go to slow blink (if `_connected` becomes true, it'd go steady).

But wait, this code runs in `loop()`. The sequence would be:
1. `startConfigPortal()` → `_startAP()` → `_apMode = true`
2. Same `loop()` iteration: later checks might see WiFi temporarily disconnected
3. Next `loop()` iteration: if WiFi reconnected, `_stopAP()` is called, killing AP mode

This IS the bug! When re-provisioning while connected, `WiFi.mode(WIFI_AP_STA)` might briefly cause WiFi to appear disconnected, then reconnected, triggering `_stopAP()`.

The fix should be: when manually entering AP mode (via `startConfigPortal()`), we should prevent automatic AP stop until the user completes provisioning or a timeout occurs.

Let me add a flag like `_manualAPMode` to distinguish manual AP start from automatic AP start.

Or, simpler: reset `_ledLastToggle` to 0 when entering AP mode, and add a guard in the reconnect logic to not stop AP if it was manually started.

Actually, looking more carefully at the code flow:

```cpp
void WiFiMgmt::_startAP() {
    if (_apMode) return;
    _apMode = true;
    ...
    WiFi.mode(WIFI_AP_STA);
    WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);
    ...
}
```

After `WiFi.mode(WIFI_AP_STA)`, the STA might briefly disconnect. Then in `loop()`:

```cpp
// 断开检测
if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {  // 10000ms!
    _lastCheckTime = millis();
    if (WiFi.status() != WL_CONNECTED && _connected) {
        _connected = false;
        ...
    }
}
```

The disconnect check only runs every 10 seconds (`WIFI_RETRY_INTERVAL`). So this shouldn't trigger immediately.

And the reconnect:
```cpp
// 后台连接成功 → 关 AP
if (!_pendingConnect && !_connected && WiFi.status() == WL_CONNECTED) {
    _connected = true;
    ...
    if (_apMode) _stopAP();
}
```

This runs every loop iteration. But `_connected` was only set to false after 10 seconds. So this shouldn't fire right away either.

Hmm, so maybe my theory is wrong. Let me think about this differently.

Maybe the issue is simpler. The user says the LED blinks slowly after pressing the button. Let me check if the LED pin is correct. `LED_WIFI_PIN` is GPIO 8. `CONFIG_BUTTON_PIN` is GPIO 0. These shouldn't conflict.

Actually, wait. Let me re-read the WiFiMgmt::loop() more carefully:

```cpp
if (_apMode) {
    if (millis() - _ledLastToggle > 300) {
        _ledLastToggle = millis(); _ledState = !_ledState;
        digitalWrite(LED_WIFI_PIN, _ledState ? HIGH : LOW);
    }
} else if (!_connected) {
    if (millis() - _ledLastToggle > 1000) {
        ...
    }
} else {
    digitalWrite(LED_WIFI_PIN, HIGH);
}
```

The problem could be: when `_apMode` is true but WiFi is also connected, the LED fast blinks. But what if `_apMode` gets set back to false soon after?

Let me look at `_stopAP()`:
```cpp
void WiFiMgmt::_stopAP() {
    if (!_apMode) return;
    _dns.stop();
    _apMode = false;
}
```

Who calls `_stopAP()`?
1. `_handleConfig()` line 208: after successful config POST
2. `loop()` line 88: when WiFi reconnects and `_apMode` is true
3. `resetSettings()` line 236

For case 2, this happens when `!_pendingConnect && !_connected && WiFi.status() == WL_CONNECTED`.

When `_startAP()` calls `WiFi.mode(WIFI_AP_STA)`, does this cause a brief WiFi disconnection? If so, WiFi.status() might briefly not be WL_CONNECTED, and then recover. But `_connected` wouldn't have been set to false yet (that takes 10 seconds).

Actually wait, let me look at the WiFi.mode() behavior. When you call `WiFi.mode(WIFI_AP_STA)` when already in `WIFI_STA` mode, ESP32 should add AP without disrupting STA. So there shouldn't be a disconnect.

OK, I'm going in circles. Let me just look at main.cpp to see how the callback is wired, and then suggest a fix.

Actually, I think the most practical approach is:
1. Add `_ledLastToggle = 0` to `startConfigPortal()` to force an immediate LED toggle
2. Ensure the LED blinks correctly

But I also want to understand why it's "slow". Let me look at the code one more time...

Oh! I think I found it. Look at the `_setLEDMode` function:

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

It's a NO-OP. But `startConfigPortal()` calls `_setLEDMode(1)` before `_startAP()`. This has no effect.

In `loop()`, the LED states are:
- `_apMode` → 300ms blink
- `!_connected` → 1000ms blink
- else → HIGH

The question is: does `_apMode` actually get set to true?

`startConfigPortal()` → `_startAP()` → `if (_apMode) return;` (false, so continues) → `_apMode = true;`

This should work. Unless... `_startAP()` was already called during `begin()` (when there was no saved WiFi) and `_apMode` is already true. Then the `if (_apMode) return;` guard would prevent re-execution.

But if WiFi is connected (normal operation), `_apMode` should be false.

Hmm, let me consider: when the user says "很慢", maybe they mean the 300ms blink is still too slow for their preference? Maybe they want something like 150ms or 200ms?

Or maybe the issue is that `loop()` doesn't run frequently enough, so the 300ms becomes effectively 500ms or more.

I think the best approach is to:
1. Speed up the AP mode blink to 200ms or even 150ms
2. Reset `_ledLastToggle = 0` in `startConfigPortal()` to force immediate blink start
3. Maybe add a `_manualAP` flag to prevent auto-stop of AP during manual provisioning

Let me also check main.cpp to see the callback setup.


Let me check the callback wiring and understand the full flow.


1	/**
2	 * @file main.cpp
3	 * @brief 五劳无感康养监测系统 - 主程序 (协议 V0.08)
4	 *
5	 * ESP32-S3 + R60ABD1 60G 毫米波雷达 + DHT11 + 水浸
6	 * 本地紧急报警 (离线可用) + 服务端告警联动
7	 */
8
9	#include <Arduino.h>
10	#include <WiFi.h>
11	#include "config.h"
12	#include "radar_protocol.h"
13	#include "radar_manager.h"
14	#include "wifi_manager.h"
15	#include "mqtt_manager.h"
16	#include "sensor_manager.h"
17	#include "alarm_manager.h"
18	#include "display_manager.h"
19	#include "button_manager.h"
20
21	// ============================================================
22	// 全局对象
23	// ============================================================
24
25	RadarManager   g_radar;
26	WiFiMgmt       g_wifi;
27	MQTTManager    g_mqtt;
28	SensorManager  g_sensor;
29	AlarmManager   g_alarm;
30	DisplayManager g_display;
31	ButtonManager  g_buttons;
32
33	// 上报控制
34	uint32_t g_lastReportTime  = 0;
35	uint32_t g_sequence        = 1;       // 上报序号
36	uint32_t g_bootTime        = 0;       // 启动时刻
37
38	// 本地报警跟踪 (防重复触发)
39	bool g_localFallActive       = false;
40	bool g_localWaterActive      = false;
41	bool g_localHeartAbnActive   = false;
42	bool g_localBreathAbnActive  = false;
43	uint32_t g_presenceStartTime = 0;       // 首次检测到人的时刻
44	bool     g_vitalAlarmReady   = false;   // 心率呼吸报警就绪 (有人30秒后)
45	uint32_t g_fallStartTime     = 0;       // 跌倒开始时刻 (防误判)
46
47	// ============================================================
48	// 时间同步
49	// ============================================================
50
51	void syncTime() {
52	    Serial.println(F("[时间] 正在同步时间..."));
53	    configTime(NTP_GMT_OFFSET_SEC, NTP_DAYLIGHT_OFFSET, NTP_SERVER, "pool.ntp.org");
54
55	    struct tm timeinfo;
56	    int retry = 0;
57	    while (!getLocalTime(&timeinfo) && retry < 20) {
58	        delay(500);
59	        Serial.print(".");
60	        retry++;
61	    }
62
63	    if (retry < 20) {
64	        Serial.println();
65	        Serial.printf("[时间] ✅ 时间同步成功: %04d-%02d-%02d %02d:%02d:%02d\n",
66	                      timeinfo.tm_year + 1900, timeinfo.tm_mon + 1,
67	                      timeinfo.tm_mday, timeinfo.tm_hour,
68	                      timeinfo.tm_min, timeinfo.tm_sec);
69	    } else {
70	        Serial.println(F("\n[时间] ⚠ 时间同步失败, 使用默认时间"));
71	    }
72	}
73
74	// ============================================================
75	// MQTT 告警指令回调 (协议规范 §5.1, §5.2)
76	// ============================================================
77
78	void handleAlarmCommand(const AlarmCommand& cmd) {
79	    if (strcmp(cmd.type, "alarm_restore") == 0) {
80	        // 告警恢复
81	        g_alarm.executeRestore(cmd);
82
83	        // 清除对应本地告警状态
84	        if (strcmp(cmd.alarmType, "fall") == 0)           g_localFallActive = false;
85	        else if (strcmp(cmd.alarmType, "water_leak") == 0) g_localWaterActive = false;
86	        else if (strcmp(cmd.alarmType, "heart_abnormal") == 0)  g_localHeartAbnActive = false;
87	        else if (strcmp(cmd.alarmType, "breath_abnormal") == 0) g_localBreathAbnActive = false;
88	    } else {
89	        // 告警触发
90	        g_alarm.executeAlarm(cmd);
91	    }
92
93	    // 回复响应 (§4.5)
94	    g_mqtt.publishCmdResponse(cmd.cmdId, cmd.type, cmd.alarmType,
95	                              true, "0", "");
96	}
97
98	// ============================================================
99	// MQTT 控制指令回调 (§5.3)
100	// ============================================================
101
102	void handleControlCommand(const ControlCommand& cmd) {
103	    Serial.printf("[控制] 收到控制指令: %s\n", cmd.controlType);
104
105	    bool success = true;
106	    const char* errCode = "0";
107	    const char* errMsg  = "";
108
109	    if (strcmp(cmd.controlType, "buzzer_test") == 0) {
110	        uint32_t dur = cmd.params_duration > 0 ? cmd.params_duration : 3;
111	        g_alarm.buzzerTest(dur);
112	    }
113	    else if (strcmp(cmd.controlType, "device_restart") == 0) {
114	        Serial.println(F("[控制] 🔄 远程重启..."));
115	        g_mqtt.publishCmdResponse(cmd.cmdId, "control", "", true, "0", "");
116	        g_mqtt.publishOffline();
117	        delay(500);
118	        ESP.restart();
119	    }
120	    else if (strcmp(cmd.controlType, "factory_reset") == 0) {
121	        Serial.println(F("[控制] 🔄 恢复出厂设置..."));
122	        g_wifi.resetSettings();
123	    }
124	    else if (strcmp(cmd.controlType, "enter_provision") == 0) {
125	        Serial.println(F("[控制] 📶 进入配网模式"));
126	        g_wifi.startConfigPortal();
127	    }
128	    else {
129	        success = false;
130	        errCode = "2001";
131	        errMsg  = "不支持的指令类型";
132	    }
133
134	    g_mqtt.publishCmdResponse(cmd.cmdId, "control", "",
135	                              success, errCode, errMsg);
136	}
137
138	// ============================================================
139	// MQTT 配网指令回调 (§5.4)
140	// ============================================================
141
142	void handleProvisionCommand(const ProvisionCommand& cmd) {
143	    Serial.printf("[配网] 📶 收到配网指令: SSID=%s\n", cmd.wifi_ssid);
144
145	    if (strlen(cmd.wifi_ssid) == 0) {
146	        g_mqtt.publishProvisionResponse(cmd.cmdId, false,
147	                                        "1002", "未提供WiFi SSID");
148	        return;
149	    }
150
151	    // 连接新 WiFi
152	    g_mqtt.publishProvisionStatus("wifi_connecting", "");
153
154	    WiFi.begin(cmd.wifi_ssid, cmd.wifi_password);
155	    int retry = 0;
156	    while (WiFi.status() != WL_CONNECTED && retry < 30) {
157	        delay(500);
158	        Serial.print(".");
159	        retry++;
160	    }
161
162	    if (WiFi.status() == WL_CONNECTED) {
163	        Serial.printf("\n[配网] ✅ WiFi 已连接, IP=%s\n",
164	                      WiFi.localIP().toString().c_str());
165
166	        // 更新 MQTT 服务器配置
167	        if (strlen(cmd.mqtt_broker) > 0) {
168	            g_mqtt.setServer(cmd.mqtt_broker, cmd.mqtt_port,
169	                           GATEID_DEFAULT, GATEID_DEFAULT); // 正式账号 = gateid
170	        }
171
172	        char signal[8];
173	        snprintf(signal, sizeof(signal), "%d", WiFi.RSSI());
174	        g_mqtt.publishProvisionResponse(cmd.cmdId, true, "0", "", signal);
175
176	        // 重连 MQTT
177	        g_mqtt.disconnect();
178	        g_mqtt.connect();
179	        g_mqtt.publishOnline();
180	        g_mqtt.publishProvisionStatus("success", "");
181	    } else {
182	        Serial.println(F("\n[配网] ❌ WiFi 连接失败"));
183	        g_mqtt.publishProvisionStatus("wifi_failed", "");
184	        g_mqtt.publishProvisionResponse(cmd.cmdId, false,
185	                                        "1003", "WiFi连接超时");
186	    }
187	}
188
189	// ============================================================
190	// 构建遥测数据
191	// ============================================================
192
193	TelemetryData buildTelemetry(const RadarRawData& radar) {
194	    TelemetryData t;
195	    memset(&t, 0, sizeof(t));
196
197	    // 设备标识
198	    strncpy(t.saleid,    SALEID_DEFAULT,   sizeof(t.saleid) - 1);
199	    strncpy(t.gateid,    g_mqtt.getGateId(), sizeof(t.gateid) - 1);
200	    strncpy(t.meterId,   METER_ID_DEFAULT, sizeof(t.meterId) - 1);
201	    strncpy(t.meterName, METER_NAME_DEFAULT, sizeof(t.meterName) - 1);
202
203	    // DHT11
204	    t.temp     = g_sensor.getTemp();
205	    t.humi     = g_sensor.getHumi();
206	    t.dhtValid = g_sensor.isDhtValid();
207
208	    // 雷达
209	    t.presence   = radar.presence;
210	    t.motion     = radar.motion;
211	    t.fall       = radar.fall;
212	    t.resident   = radar.resident;
213	    t.heartRate  = radar.heartRate;
214	    t.breathRate = radar.breathRate;
215	    t.body       = radar.body;
216	    t.radarValid = radar.radarValid;
217	    t.vitalValid = radar.vitalValid;
218
219	    // 水浸
220	    t.rainDO    = g_sensor.getRainDO();
221	    t.rainValid = g_sensor.isRainValid();
222
223	    // 设备状态
224	    t.wifiReady  = g_wifi.isConnected() && g_mqtt.isConnected();
225	    t.uptimeSec  = (millis() - g_bootTime) / 1000;
226	    t.buzzerState = g_alarm.isBuzzerOn();
227	    t.lastUpdate = millis();
228
229	    return t;
230	}
231
232	// ============================================================
233	// 本地报警检查 (离线可用)
234	// ============================================================
235
236	void checkLocalAlarms(const RadarRawData& radar) {
237	    uint32_t now = millis();
238
239	    // --- 跌倒检测 (持续1秒才触发，防坐下误判) ---
240	    if (radar.fall && radar.radarValid && !g_localFallActive) {
241	        if (g_fallStartTime == 0) g_fallStartTime = now;
242	        if (now - g_fallStartTime >= 1000) {  // 跌倒持续1秒才报警
243	            g_localFallActive = true;
244	            g_alarm.triggerLocal(AlarmManager::ALARM_FALL);
245	        }
246	    } else if (!radar.fall && g_localFallActive) {
247	        g_localFallActive = false;
248	        g_fallStartTime = 0;
249	        g_alarm.clearLocal(AlarmManager::ALARM_FALL);
250	    }
251	    if (!radar.fall) g_fallStartTime = 0;  // 没有跌倒时重置计时
252
253	    // --- 水浸检测 ---
254	    bool waterLeak = !g_sensor.getRainDO() && g_sensor.isRainValid();
255	    if (waterLeak && !g_localWaterActive) {
256	        g_localWaterActive = true;
257	        g_alarm.triggerLocal(AlarmManager::ALARM_WATER_LEAK);
258	    } else if (!waterLeak && g_localWaterActive) {
259	        g_localWaterActive = false;
260	        g_alarm.clearLocal(AlarmManager::ALARM_WATER_LEAK);
261	    }
262
263	    // --- 有心率呼吸报警延迟: 检测到人后等 30 秒数据稳定 ---
264	    #define VITAL_ALARM_DELAY_MS  30000
265	    if (radar.presence && !g_vitalAlarmReady) {
266	        if (g_presenceStartTime == 0) g_presenceStartTime = now;
267	        if (now - g_presenceStartTime >= VITAL_ALARM_DELAY_MS) {
268	            g_vitalAlarmReady = true;
269	            Serial.println(F("[报警] 心率呼吸监测就绪 (30秒稳定期结束)"));
270	        }
271	    } else if (!radar.presence) {
272	        g_presenceStartTime = 0;
273	        g_vitalAlarmReady   = false;
274	    }
275
276	    // --- 心率异常 (仅有人+就绪时判定) ---
277	    if (g_vitalAlarmReady && radar.vitalValid && radar.heartRate > 0) {
278	        bool heartAbn = (radar.heartRate < HEART_RATE_MIN || radar.heartRate > HEART_RATE_MAX);
279	        if (heartAbn && !g_localHeartAbnActive) {
280	            g_localHeartAbnActive = true;
281	            g_alarm.triggerLocal(AlarmManager::ALARM_HEART_ABNORMAL);
282	        } else if (!heartAbn && g_localHeartAbnActive) {
283	            g_localHeartAbnActive = false;
284	            g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
285	        }
286	    }
287	    // 无人时清除心率异常报警
288	    else if (!radar.presence && g_localHeartAbnActive) {
289	        g_localHeartAbnActive = false;
290	        g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
291	    }
292
293	    // --- 呼吸异常 (仅有人+就绪时判定) ---
294	    if (g_vitalAlarmReady && radar.vitalValid && radar.breathRate > 0) {
295	        bool breathAbn = (radar.breathRate < BREATH_RATE_MIN || radar.breathRate > BREATH_RATE_MAX);
296	        if (breathAbn && !g_localBreathAbnActive) {
297	            g_localBreathAbnActive = true;
298	            g_alarm.triggerLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
299	        } else if (!breathAbn && g_localBreathAbnActive) {
300	            g_localBreathAbnActive = false;
301	            g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
302	        }
303	    }
304	    // 无人时清除呼吸异常报警
305	    else if (!radar.presence && g_localBreathAbnActive) {
306	        g_localBreathAbnActive = false;
307	        g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
308	    }
309	}
310
311	// ============================================================
312	// 调试输出
313	// ============================================================
314
315	void printDebug(const TelemetryData& t) {
316	    static uint32_t lastPrint = 0;
317	    if (millis() - lastPrint < 5000) return;
318	    lastPrint = millis();
319
320	    Serial.println(F("─── 遥测数据 ───"));
321	    Serial.printf("  环境: T=%.1f°C H=%.1f%% (温湿度:%d)\n",
322	                  t.temp, t.humi, t.dhtValid);
323	    Serial.printf("  存在:%d 运动:%d 跌倒:%d 驻留:%d (雷达:%d)\n",
324	                  t.presence, t.motion, t.fall, t.resident, t.radarValid);
325	    Serial.printf("  心率:%d 呼吸:%d (体征:%d)\n",
326	                  t.heartRate, t.breathRate, t.vitalValid);
327	    Serial.printf("  水浸:%d (水浸有效:%d) | WiFi:%d 蜂鸣器:%d\n",
328	                  t.rainDO, t.rainValid, t.wifiReady, t.buzzerState);
329	    Serial.printf("  运行:%ds 序号:%d\n", t.uptimeSec, g_sequence);
330	    Serial.println(F("─────────────────"));
331	}
332
333	// ============================================================
334	// 雷达任务 - 核心0 专用
335	// ============================================================
336
337	void radarTask(void* param) {
338	    Serial.println(F("[任务] 📡 雷达任务启动 (核心0)"));
339	    TickType_t lastWake = xTaskGetTickCount();
340	    const TickType_t freq = pdMS_TO_TICKS(10);
341
342	    while (true) {
343	        g_radar.loop();
344	        vTaskDelayUntil(&lastWake, freq);
345	    }
346	}
347
348	// ============================================================
349	// Setup
350	// ============================================================
351
352	void setup() {
353	    Serial.begin(115200);
354	    delay(500);
355
356	    Serial.println(F("\n"));
357	    Serial.println(F("╔══════════════════════════════════════════╗"));
358	    Serial.println(F("║  🏥 五劳无感康养监测系统 v1.0.0       ║"));
359	    Serial.println(F("║  ESP32-S3 + R60ABD1 + DHT11 + 水浸     ║"));
360	    Serial.println(F("║  MQTT 协议 V0.08                        ║"));
361	    Serial.println(F("╚══════════════════════════════════════════╝"));
362
363	    g_bootTime = millis();
364
365	    // 1. 报警模块 (最先, 引脚安全)
366	    g_alarm.begin();
367
368	    // 2. 雷达
369	    g_radar.begin();
370
371	    // 3. OLED 显示
372	    g_display.begin();
373
374	    // 4. 按钮
375	    g_buttons.begin();
376	    g_buttons.onPageSwitch([]() {
377	        g_display.nextPage();
378	    });
379	    g_buttons.onAlarmDismiss([]() {
380	        g_alarm.clearAllLocal();
381	        g_radar.clearFall();  // 数据也归零
382	    });
383	    g_buttons.onProvisionLongPress([]() {
384	        g_wifi.startConfigPortal();
385	        // 配网后 MQTT 配置可能已更新, 重新设置并重连
386	        DeviceConfig cfg = g_wifi.getConfig();
387	        g_mqtt.setServer(cfg.mqtt_server, atoi(cfg.mqtt_port),
388	                         cfg.mqtt_user, cfg.mqtt_pass);
389	        g_mqtt.begin(cfg.device_id);
390	        g_mqtt.connect();
391	        g_mqtt.publishOnline();
392	        if (g_wifi.isFreshProvision()) {
393	            g_mqtt.publishProvisionStatus("success", "");
394	            g_wifi.clearFreshProvision();
395	        }
396	    });
397
398	    // 5. WiFi (WiFiManager 配网)
399	    // 先注册配网状态回调, 再 begin(), 这样配网过程中的 ready/success 都不会丢失
400	    g_wifi.onProvisionState([](const char* state) {
401	        g_mqtt.publishProvisionStatus(state, "");
402	    });
403	    g_wifi.begin();
404
405	    // 6. 时间同步
406	    if (g_wifi.isConnected()) {
407	        syncTime();
408	    }
409
410	    // 7. MQTT
411	    DeviceConfig cfg = g_wifi.getConfig();
412	    g_mqtt.setServer(cfg.mqtt_server, atoi(cfg.mqtt_port),
413	                     cfg.mqtt_user, cfg.mqtt_pass);
414	    g_mqtt.begin(cfg.device_id);
415
416	    // 注册 MQTT 回调
417	    g_mqtt.onAlarmCommand(handleAlarmCommand);
418	    g_mqtt.onControlCommand(handleControlCommand);
419	    g_mqtt.onProvisionCommand(handleProvisionCommand);
420
421	    // 连接 MQTT
422	    if (g_wifi.isConnected()) {
423	        g_mqtt.connect();
424	        // 首次连接: 发送 online 通知
425	        g_mqtt.publishOnline();
426	        // 新配网成功: provision success 与 online 一起发送
427	        if (g_wifi.isFreshProvision()) {
428	            g_mqtt.publishProvisionStatus("success", "");
429	            g_wifi.clearFreshProvision();
430	        }
431	    }
432
433	    // 8. 传感器
434	    g_sensor.begin();
435
436	    // 9. 雷达任务 (核心0)
437	    xTaskCreatePinnedToCore(
438	        radarTask, "RadarTask",
439	        RADAR_TASK_STACK, nullptr,
440	        RADAR_TASK_PRIORITY, nullptr,
441	        RADAR_TASK_CORE
442	    );
443
444	    Serial.println(F("[系统] ✅ 系统就绪"));
445	    Serial.println(F("══════════════════════════════════════════\n"));
446	}
447
448	// ============================================================
449	// 主循环 (核心1)
450	// ============================================================
451
452	void loop() {
453	    uint32_t now = millis();
454
455	    // --- 0. 按钮轮询 ---
456	    g_buttons.loop();
457
458	    // --- 1. WiFi ---
459	    bool wifiOk = g_wifi.loop();
460
461	    // --- 2. MQTT (含自动重连) ---
462	    if (wifiOk) {
463	        g_mqtt.loop();
464
465	        // WiFi 刚连上但 MQTT 还没连
466	        static bool wasDisconnected = true;
467	        if (g_mqtt.isConnected()) {
468	            if (wasDisconnected) {
469	                // 刚恢复连接, 同步时间
470	                syncTime();
471	                // 重连成功也发送 online
472	                g_mqtt.publishOnline();
473	                // 新配网成功: provision success 与 online 一起发送
474	                if (g_wifi.isFreshProvision()) {
475	                    g_mqtt.publishProvisionStatus("success", "");
476	                    g_wifi.clearFreshProvision();
477	                }
478	                wasDisconnected = false;
479	            }
480	        } else {
481	            wasDisconnected = true;
482	        }
483	    }
484
485	    // --- 3. 传感器 (独立，任何传感器故障不影响其他) ---
486	    g_sensor.read();
487
488	    // --- 4. 本地报警检查 (离线也工作) ---
489	    RadarRawData radar = g_radar.getData();
490	    checkLocalAlarms(radar);
491
492	    // --- 传感器故障检测 (每30秒检查一次，避免刷屏) ---
493	    static uint32_t lastFaultCheck = 0;
494	    if (now - lastFaultCheck > 30000) {
495	        lastFaultCheck = now;
496	        if (!g_sensor.isDhtValid() && millis() > 60000) {
497	            Serial.println(F("[系统] ⚠ DHT11 未检测到 (不影响运行)"));
498	        }
499	        if (g_radar.isTimeout()) {
500	            Serial.println(F("[系统] ⚠ 雷达超时无数据 (不影响运行)"));
501	        }
502	    }
503
504	    // --- 5. 报警模块更新 (定时器/闪烁) ---
505	    g_alarm.loop();
506
507	    // --- 6. 遥测上报 (1秒周期) ---
508	    if (now - g_lastReportTime >= REPORT_INTERVAL_MS) {
509	        g_lastReportTime = now;
510
511	        TelemetryData tele = buildTelemetry(radar);
512	        g_mqtt.publishAllPoints(tele, g_sequence);
513	        g_sequence++;
514
515	        // 调试输出 + OLED 显示
516	        printDebug(tele);
517	        g_display.update(tele, wifiOk, g_alarm.isBuzzerOn());
518	    }
519
520	    // --- 7. MQTT 配网状态 (WiFiManager 触发时) ---
521	    // WiFiManager 的 captive portal 启动/关闭时更新状态
522	    // 此逻辑已集成在 wifi_manager 中
523
524	    delay(10);
525	}
526

/**
 * @file main.cpp
 * @brief 五劳无感康养监测系统 - 主程序 (协议 V0.08)
 *
 * ESP32-S3 + R60ABD1 60G 毫米波雷达 + DHT11 + 水浸
 * 本地紧急报警 (离线可用) + 服务端告警联动
 */

#include <Arduino.h>
#include <WiFi.h>
#include "config.h"
#include "radar_protocol.h"
#include "radar_manager.h"
#include "wifi_manager.h"
#include "mqtt_manager.h"
#include "sensor_manager.h"
#include "alarm_manager.h"
#include "display_manager.h"
#include "button_manager.h"

// ============================================================
// 全局对象
// ============================================================

RadarManager   g_radar;
WiFiMgmt       g_wifi;
MQTTManager    g_mqtt;
SensorManager  g_sensor;
AlarmManager   g_alarm;
DisplayManager g_display;
ButtonManager  g_buttons;

// 上报控制
uint32_t g_lastReportTime  = 0;
uint32_t g_sequence        = 1;       // 上报序号
uint32_t g_bootTime        = 0;       // 启动时刻

// 本地报警跟踪 (防重复触发)
bool g_localFallActive       = false;
bool g_localWaterActive      = false;
bool g_localHeartAbnActive   = false;
bool g_localBreathAbnActive  = false;
uint32_t g_presenceStartTime = 0;       // 首次检测到人的时刻
bool     g_vitalAlarmReady   = false;   // 心率呼吸报警就绪 (有人30秒后)
uint32_t g_fallStartTime     = 0;       // 跌倒开始时刻 (防误判)

// ============================================================
// 时间同步
// ============================================================

void syncTime() {
    Serial.println(F("[时间] 正在同步时间..."));
    configTime(NTP_GMT_OFFSET_SEC, NTP_DAYLIGHT_OFFSET, NTP_SERVER, "pool.ntp.org");

    struct tm timeinfo;
    int retry = 0;
    while (!getLocalTime(&timeinfo) && retry < 20) {
        delay(500);
        Serial.print(".");
        retry++;
    }

    if (retry < 20) {
        Serial.println();
        Serial.printf("[时间] ✅ 时间同步成功: %04d-%02d-%02d %02d:%02d:%02d\n",
                      timeinfo.tm_year + 1900, timeinfo.tm_mon + 1,
                      timeinfo.tm_mday, timeinfo.tm_hour,
                      timeinfo.tm_min, timeinfo.tm_sec);
    } else {
        Serial.println(F("\n[时间] ⚠ 时间同步失败, 使用默认时间"));
    }
}

// ============================================================
// MQTT 告警指令回调 (协议规范 §5.1, §5.2)
// ============================================================

void handleAlarmCommand(const AlarmCommand& cmd) {
    if (strcmp(cmd.type, "alarm_restore") == 0) {
        // 告警恢复
        g_alarm.executeRestore(cmd);

        // 清除对应本地告警状态
        if (strcmp(cmd.alarmType, "fall") == 0)           g_localFallActive = false;
        else if (strcmp(cmd.alarmType, "water_leak") == 0) g_localWaterActive = false;
        else if (strcmp(cmd.alarmType, "heart_abnormal") == 0)  g_localHeartAbnActive = false;
        else if (strcmp(cmd.alarmType, "breath_abnormal") == 0) g_localBreathAbnActive = false;
    } else {
        // 告警触发
        g_alarm.executeAlarm(cmd);
    }

    // 回复响应 (§4.5)
    g_mqtt.publishCmdResponse(cmd.cmdId, cmd.type, cmd.alarmType,
                              true, "0", "");
}

// ============================================================
// MQTT 控制指令回调 (§5.3)
// ============================================================

void handleControlCommand(const ControlCommand& cmd) {
    Serial.printf("[控制] 收到控制指令: %s\n", cmd.controlType);

    bool success = true;
    const char* errCode = "0";
    const char* errMsg  = "";

    if (strcmp(cmd.controlType, "buzzer_test") == 0) {
        uint32_t dur = cmd.params_duration > 0 ? cmd.params_duration : 3;
        g_alarm.buzzerTest(dur);
    }
    else if (strcmp(cmd.controlType, "device_restart") == 0) {
        Serial.println(F("[控制] 🔄 远程重启..."));
        g_mqtt.publishCmdResponse(cmd.cmdId, "control", "", true, "0", "");
        g_mqtt.publishOffline();
        delay(500);
        ESP.restart();
    }
    else if (strcmp(cmd.controlType, "factory_reset") == 0) {
        Serial.println(F("[控制] 🔄 恢复出厂设置..."));
        g_wifi.resetSettings();
    }
    else if (strcmp(cmd.controlType, "enter_provision") == 0) {
        Serial.println(F("[控制] 📶 进入配网模式"));
        g_wifi.startConfigPortal();
    }
    else {
        success = false;
        errCode = "2001";
        errMsg  = "不支持的指令类型";
    }

    g_mqtt.publishCmdResponse(cmd.cmdId, "control", "",
                              success, errCode, errMsg);
}

// ============================================================
// MQTT 配网指令回调 (§5.4)
// ============================================================

void handleProvisionCommand(const ProvisionCommand& cmd) {
    Serial.printf("[配网] 📶 收到配网指令: SSID=%s\n", cmd.wifi_ssid);

    if (strlen(cmd.wifi_ssid) == 0) {
        g_mqtt.publishProvisionResponse(cmd.cmdId, false,
                                        "1002", "未提供WiFi SSID");
        return;
    }

    // 连接新 WiFi
    g_mqtt.publishProvisionStatus("wifi_connecting", "");

    WiFi.begin(cmd.wifi_ssid, cmd.wifi_password);
    int retry = 0;
    while (WiFi.status() != WL_CONNECTED && retry < 30) {
        delay(500);
        Serial.print(".");
        retry++;
    }

    if (WiFi.status() == WL_CONNECTED) {
        Serial.printf("\n[配网] ✅ WiFi 已连接, IP=%s\n",
                      WiFi.localIP().toString().c_str());

        // 更新 MQTT 服务器配置
        if (strlen(cmd.mqtt_broker) > 0) {
            g_mqtt.setServer(cmd.mqtt_broker, cmd.mqtt_port,
                           GATEID_DEFAULT, GATEID_DEFAULT); // 正式账号 = gateid
        }

        char signal[8];
        snprintf(signal, sizeof(signal), "%d", WiFi.RSSI());
        g_mqtt.publishProvisionResponse(cmd.cmdId, true, "0", "", signal);

        // 重连 MQTT
        g_mqtt.disconnect();
        g_mqtt.connect();
        g_mqtt.publishOnline();
        g_mqtt.publishProvisionStatus("success", "");
    } else {
        Serial.println(F("\n[配网] ❌ WiFi 连接失败"));
        g_mqtt.publishProvisionStatus("wifi_failed", "");
        g_mqtt.publishProvisionResponse(cmd.cmdId, false,
                                        "1003", "WiFi连接超时");
    }
}

// ============================================================
// 构建遥测数据
// ============================================================

TelemetryData buildTelemetry(const RadarRawData& radar) {
    TelemetryData t;
    memset(&t, 0, sizeof(t));

    // 设备标识
    strncpy(t.saleid,    SALEID_DEFAULT,   sizeof(t.saleid) - 1);
    strncpy(t.gateid,    g_mqtt.getGateId(), sizeof(t.gateid) - 1);
    strncpy(t.meterId,   METER_ID_DEFAULT, sizeof(t.meterId) - 1);
    strncpy(t.meterName, METER_NAME_DEFAULT, sizeof(t.meterName) - 1);

    // DHT11
    t.temp     = g_sensor.getTemp();
    t.humi     = g_sensor.getHumi();
    t.dhtValid = g_sensor.isDhtValid();

    // 雷达
    t.presence   = radar.presence;
    t.motion     = radar.motion;
    t.fall       = radar.fall;
    t.resident   = radar.resident;
    t.heartRate  = radar.heartRate;
    t.breathRate = radar.breathRate;
    t.body       = radar.body;
    t.radarValid = radar.radarValid;
    t.vitalValid = radar.vitalValid;

    // 水浸
    t.rainDO    = g_sensor.getRainDO();
    t.rainValid = g_sensor.isRainValid();

    // 设备状态
    t.wifiReady  = g_wifi.isConnected() && g_mqtt.isConnected();
    t.uptimeSec  = (millis() - g_bootTime) / 1000;
    t.buzzerState = g_alarm.isBuzzerOn();
    t.lastUpdate = millis();

    return t;
}

// ============================================================
// 本地报警检查 (离线可用)
// ============================================================

void checkLocalAlarms(const RadarRawData& radar) {
    uint32_t now = millis();

    // --- 跌倒检测 (持续1秒才触发，防坐下误判) ---
    if (radar.fall && radar.radarValid && !g_localFallActive) {
        if (g_fallStartTime == 0) g_fallStartTime = now;
        if (now - g_fallStartTime >= 1000) {  // 跌倒持续1秒才报警
            g_localFallActive = true;
            g_alarm.triggerLocal(AlarmManager::ALARM_FALL);
        }
    } else if (!radar.fall && g_localFallActive) {
        g_localFallActive = false;
        g_fallStartTime = 0;
        g_alarm.clearLocal(AlarmManager::ALARM_FALL);
    }
    if (!radar.fall) g_fallStartTime = 0;  // 没有跌倒时重置计时

    // --- 水浸检测 ---
    bool waterLeak = !g_sensor.getRainDO() && g_sensor.isRainValid();
    if (waterLeak && !g_localWaterActive) {
        g_localWaterActive = true;
        g_alarm.triggerLocal(AlarmManager::ALARM_WATER_LEAK);
    } else if (!waterLeak && g_localWaterActive) {
        g_localWaterActive = false;
        g_alarm.clearLocal(AlarmManager::ALARM_WATER_LEAK);
    }

    // --- 有心率呼吸报警延迟: 检测到人后等 30 秒数据稳定 ---
    #define VITAL_ALARM_DELAY_MS  30000
    if (radar.presence && !g_vitalAlarmReady) {
        if (g_presenceStartTime == 0) g_presenceStartTime = now;
        if (now - g_presenceStartTime >= VITAL_ALARM_DELAY_MS) {
            g_vitalAlarmReady = true;
            Serial.println(F("[报警] 心率呼吸监测就绪 (30秒稳定期结束)"));
        }
    } else if (!radar.presence) {
        g_presenceStartTime = 0;
        g_vitalAlarmReady   = false;
    }

    // --- 心率异常 (仅有人+就绪时判定) ---
    if (g_vitalAlarmReady && radar.vitalValid && radar.heartRate > 0) {
        bool heartAbn = (radar.heartRate < HEART_RATE_MIN || radar.heartRate > HEART_RATE_MAX);
        if (heartAbn && !g_localHeartAbnActive) {
            g_localHeartAbnActive = true;
            g_alarm.triggerLocal(AlarmManager::ALARM_HEART_ABNORMAL);
        } else if (!heartAbn && g_localHeartAbnActive) {
            g_localHeartAbnActive = false;
            g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
        }
    }
    // 无人时清除心率异常报警
    else if (!radar.presence && g_localHeartAbnActive) {
        g_localHeartAbnActive = false;
        g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
    }

    // --- 呼吸异常 (仅有人+就绪时判定) ---
    if (g_vitalAlarmReady && radar.vitalValid && radar.breathRate > 0) {
        bool breathAbn = (radar.breathRate < BREATH_RATE_MIN || radar.breathRate > BREATH_RATE_MAX);
        if (breathAbn && !g_localBreathAbnActive) {
            g_localBreathAbnActive = true;
            g_alarm.triggerLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
        } else if (!breathAbn && g_localBreathAbnActive) {
            g_localBreathAbnActive = false;
            g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
        }
    }
    // 无人时清除呼吸异常报警
    else if (!radar.presence && g_localBreathAbnActive) {
        g_localBreathAbnActive = false;
        g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
    }
}

// ============================================================
// 调试输出
// ============================================================

void printDebug(const TelemetryData& t) {
    static uint32_t lastPrint = 0;
    if (millis() - lastPrint < 5000) return;
    lastPrint = millis();

    Serial.println(F("─── 遥测数据 ───"));
    Serial.printf("  环境: T=%.1f°C H=%.1f%% (温湿度:%d)\n",
                  t.temp, t.humi, t.dhtValid);
    Serial.printf("  存在:%d 运动:%d 跌倒:%d 驻留:%d (雷达:%d)\n",
                  t.presence, t.motion, t.fall, t.resident, t.radarValid);
    Serial.printf("  心率:%d 呼吸:%d (体征:%d)\n",
                  t.heartRate, t.breathRate, t.vitalValid);
    Serial.printf("  水浸:%d (水浸有效:%d) | WiFi:%d 蜂鸣器:%d\n",
                  t.rainDO, t.rainValid, t.wifiReady, t.buzzerState);
    Serial.printf("  运行:%ds 序号:%d\n", t.uptimeSec, g_sequence);
    Serial.println(F("─────────────────"));
}

// ============================================================
// 雷达任务 - 核心0 专用
// ============================================================

void radarTask(void* param) {
    Serial.println(F("[任务] 📡 雷达任务启动 (核心0)"));
    TickType_t lastWake = xTaskGetTickCount();
    const TickType_t freq = pdMS_TO_TICKS(10);

    while (true) {
        g_radar.loop();
        vTaskDelayUntil(&lastWake, freq);
    }
}

// ============================================================
// Setup
// ============================================================

void setup() {
    Serial.begin(115200);
    delay(500);

    Serial.println(F("\n"));
    Serial.println(F("╔══════════════════════════════════════════╗"));
    Serial.println(F("║  🏥 五劳无感康养监测系统 v1.0.0       ║"));
    Serial.println(F("║  ESP32-S3 + R60ABD1 + DHT11 + 水浸     ║"));
    Serial.println(F("║  MQTT 协议 V0.08                        ║"));
    Serial.println(F("╚══════════════════════════════════════════╝"));

    g_bootTime = millis();

    // 1. 报警模块 (最先, 引脚安全)
    g_alarm.begin();

    // 2. 雷达
    g_radar.begin();

    // 3. OLED 显示
    g_display.begin();

    // 4. 按钮
    g_buttons.begin();
    g_buttons.onPageSwitch([]() {
        g_display.nextPage();
    });
    g_buttons.onAlarmDismiss([]() {
        g_alarm.clearAllLocal();
        g_radar.clearFall();  // 数据也归零
    });
    g_buttons.onProvisionLongPress([]() {
        g_wifi.startConfigPortal();
        // 配网后 MQTT 配置可能已更新, 重新设置并重连
        DeviceConfig cfg = g_wifi.getConfig();
        g_mqtt.setServer(cfg.mqtt_server, atoi(cfg.mqtt_port),
                         cfg.mqtt_user, cfg.mqtt_pass);
        g_mqtt.begin(cfg.device_id);
        g_mqtt.connect();
        g_mqtt.publishOnline();
        if (g_wifi.isFreshProvision()) {
            g_mqtt.publishProvisionStatus("success", "");
            g_wifi.clearFreshProvision();
        }
    });

    // 5. WiFi (WiFiManager 配网)
    // 先注册配网状态回调, 再 begin(), 这样配网过程中的 ready/success 都不会丢失
    g_wifi.onProvisionState([](const char* state) {
        g_mqtt.publishProvisionStatus(state, "");
    });
    g_wifi.begin();

    // 6. 时间同步
    if (g_wifi.isConnected()) {
        syncTime();
    }

    // 7. MQTT
    DeviceConfig cfg = g_wifi.getConfig();
    g_mqtt.setServer(cfg.mqtt_server, atoi(cfg.mqtt_port),
                     cfg.mqtt_user, cfg.mqtt_pass);
    g_mqtt.begin(cfg.device_id);

    // 注册 MQTT 回调
    g_mqtt.onAlarmCommand(handleAlarmCommand);
    g_mqtt.onControlCommand(handleControlCommand);
    g_mqtt.onProvisionCommand(handleProvisionCommand);

    // 连接 MQTT
    if (g_wifi.isConnected()) {
        g_mqtt.connect();
        // 首次连接: 发送 online 通知
        g_mqtt.publishOnline();
        // 新配网成功: provision success 与 online 一起发送
        if (g_wifi.isFreshProvision()) {
            g_mqtt.publishProvisionStatus("success", "");
            g_wifi.clearFreshProvision();
        }
    }

    // 8. 传感器
    g_sensor.begin();

    // 9. 雷达任务 (核心0)
    xTaskCreatePinnedToCore(
        radarTask, "RadarTask",
        RADAR_TASK_STACK, nullptr,
        RADAR_TASK_PRIORITY, nullptr,
        RADAR_TASK_CORE
    );

    Serial.println(F("[系统] ✅ 系统就绪"));
    Serial.println(F("══════════════════════════════════════════\n"));
}

// ============================================================
// 主循环 (核心1)
// ============================================================

void loop() {
    uint32_t now = millis();

    // --- 0. 按钮轮询 ---
    g_buttons.loop();

    // --- 1. WiFi ---
    bool wifiOk = g_wifi.loop();

    // --- 2. MQTT (含自动重连) ---
    if (wifiOk) {
        g_mqtt.loop();

        // WiFi 刚连上但 MQTT 还没连
        static bool wasDisconnected = true;
        if (g_mqtt.isConnected()) {
            if (wasDisconnected) {
                // 刚恢复连接, 同步时间
                syncTime();
                // 重连成功也发送 online
                g_mqtt.publishOnline();
                // 新配网成功: provision success 与 online 一起发送
                if (g_wifi.isFreshProvision()) {
                    g_mqtt.publishProvisionStatus("success", "");
                    g_wifi.clearFreshProvision();
                }
                wasDisconnected = false;
            }
        } else {
            wasDisconnected = true;
        }
    }

    // --- 3. 传感器 (独立，任何传感器故障不影响其他) ---
    g_sensor.read();

    // --- 4. 本地报警检查 (离线也工作) ---
    RadarRawData radar = g_radar.getData();
    checkLocalAlarms(radar);

    // --- 传感器故障检测 (每30秒检查一次，避免刷屏) ---
    static uint32_t lastFaultCheck = 0;
    if (now - lastFaultCheck > 30000) {
        lastFaultCheck = now;
        if (!g_sensor.isDhtValid() && millis() > 60000) {
            Serial.println(F("[系统] ⚠ DHT11 未检测到 (不影响运行)"));
        }
        if (g_radar.isTimeout()) {
            Serial.println(F("[系统] ⚠ 雷达超时无数据 (不影响运行)"));
        }
    }

    // --- 5. 报警模块更新 (定时器/闪烁) ---
    g_alarm.loop();

    // --- 6. 遥测上报 (1秒周期) ---
    if (now - g_lastReportTime >= REPORT_INTERVAL_MS) {
        g_lastReportTime = now;

        TelemetryData tele = buildTelemetry(radar);
        g_mqtt.publishAllPoints(tele, g_sequence);
        g_sequence++;

        // 调试输出 + OLED 显示
        printDebug(tele);
        g_display.update(tele, wifiOk, g_alarm.isBuzzerOn());
    }

    // --- 7. MQTT 配网状态 (WiFiManager 触发时) ---
    // WiFiManager 的 captive portal 启动/关闭时更新状态
    // 此逻辑已集成在 wifi_manager 中

    delay(10);
}


网络信号也是