<ide_opened_file>The user opened the file d:\PlatformIO1\Projects\Healthv2.0\platformio.ini in the IDE. This may or may not be related to the current task.</ide_opened_file>

继续上次的聊天


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)


- dataviz: Use this skill whenever you are about to create ANY chart, graph, plot, dashboard, or data visualization, in ANY output medium — an HTML or React artifact, inline SVG, plotting code in any library (matplotlib, plotly, d3, Recharts, …), an image/PNG you will render and upload, or a chart shared into Slack. Read it BEFORE writing the first line of chart code, choosing chart colors, building a stat tile / meter / KPI row, or laying out a dashboard. Produces visualizations that read as one system — elegant, accessible, consistent in light and dark — using a brand-neutral placeholder palette you swap for your own. Teaches a design-system-agnostic method: a form heuristic, a color formula with a runnable validator, mark specs, and interaction rules. A validated default palette is documented in `references/palette.md` — swap that file's values for your brand's. Triggers on: "chart", "graph", "plot", "data viz", "visualization", "dashboard", "analytics", "visualize data", "categorical colors", "sequential / diverging palette", "stat tile", "sparkline", "heatmap", "legend", "axis", "tooltip", "chart colors", "color by series".
- artifact-design: Design guidance and fundamentals for Artifacts.
- artifact-diagramming: Diagramming know-how for Artifacts — when a picture earns its place, how to draw one that shows the real mechanism, and the inline-SVG mechanics that keep it legible in both themes.
- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors ("from now on when X", "each time X", "whenever X", "before/after X") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions ("allow X", "add permission", "move permission to"), env vars ("set X=Y"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: "allow npm commands", "add bq permission to global settings", "move permission to user settings", "set DEBUG=true", "when claude stops show X". For simple settings like theme/model, suggest the /config command.
- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: "rebind ctrl+s", "add a chord shortcut", "change the submit key", "customize keybindings".
- simplify: Review the changed code for reuse, simplification, efficiency, and altitude cleanups, then apply the fixes. Quality only — it does not hunt for bugs; use /code-review for that.
- fewer-permission-prompts: Scan your transcripts for common read-only Bash and MCP tool calls, then add a prioritized allowlist to project .claude/settings.json to reduce permission prompts.
- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. "check the deploy every 5 minutes", "keep running /babysit-prs"). Do NOT invoke for one-off tasks.
- claude-api: Reference for the Claude API / Anthropic SDK — model ids, pricing, params, streaming, tool use, MCP, agents, caching, token counting, model migration.
TRIGGER — read BEFORE opening the target file; don't skip because it "looks like a one-liner" — whenever: the prompt names Claude/Anthropic in any form (Claude, Anthropic, Fable, Opus, Sonnet, Haiku, `anthropic`, `@anthropic-ai`, `claude-*`, `us.anthropic.*`, `[1m]`); the user asks about an LLM (pricing/model choice/limits/caching) — never answer from memory; OR the task is LLM-shaped with provider unstated (agent/MCP/tool-definition/multi-agent/RAG/LLM-judge/computer-use; generate/summarize/extract/classify/rewrite/converse over NL; debugging refusals/cutoffs/streaming/tool-calls/tokens).
SKIP only when another provider is being worked on (overrides all triggers): OpenAI/GPT/Gemini/Llama/Mistral/Cohere/Ollama named in the query; OR `grep -rE 'openai|langchain_openai|google.generativeai|genai|mistralai|cohere|ollama'` over the project hits (run this grep FIRST if no provider named — don't Read the file).
- run: Launch and drive this project's app to see a change working. Use when asked to run, start, or screenshot the app, or to confirm a change works in the real app (not just tests). First looks for a project skill that already covers launching the app; otherwise falls back to built-in patterns per project type (CLI, server, TUI, Electron, browser-driven, library).
- init: Initialize a new CLAUDE.md file with codebase documentation
- security-review: Complete a security review of the pending changes on the current branch

dataviz artifact-design artifact-diagramming update-config keybindings-help simplify fewer-permission-prompts loop claude-api run init security-review


The user is asking to continue our previous chat. Let me check the memory files and recent context to understand what we were working on. Let me look at the memory directory and any available context.


Let me check the memory and recent context to pick up where we left off.


Error: File does not exist. Note: your current working directory is d:\PlatformIO1\Projects\Healthv2.0.


No files found


The memory directory is empty - there's no MEMORY.md or any memory files. Let me check the git status and the recently modified files to understand what the project is about and what was being worked on. Let me also check the `ai/` directory since it's untracked.


No previous memory found. Let me check the current project state and the untracked `ai/` directory for context.


Bash

Show recent git commits


Show working tree changes summary


2ce4ac0 版本1.2
69e927c feat: 按钮去抖+配网界面美化+水浸离线+LED独立任务+报警全拓展(人身优先)+有人才检测
49e3abb feat: init


warning: in the working copy of 'include/config.h', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'include/mqtt_manager.h', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'include/radar_protocol.h', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'include/wifi_manager.h', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'src/wifi_manager.cpp', LF will be replaced by CRLF the next time Git touches it
 include/config.h         |  14 ++-
 include/mqtt_manager.h   |   3 +
 include/radar_manager.h  |   2 +
 include/radar_protocol.h |   1 +
 include/wifi_manager.h   |  22 +++-
 include/wm_strings_zh.h  | 262 -----------------------------------------------
 src/main.cpp             |  74 +++++++++----
 src/mqtt_manager.cpp     |  52 +++++++++-
 src/radar_manager.cpp    |  17 ++-
 src/wifi_manager.cpp     | 117 +++++++++++++++++++++
 10 files changed, 263 insertions(+), 301 deletions(-)


Looking at the git status and recent changes, this is an ESP32 PlatformIO project for a health monitoring system. The current working state has uncommitted changes across multiple files. Let me look at the key files to understand what was being worked on. The changes touch:

1. `include/config.h` - configuration
2. `include/mqtt_manager.h` - MQTT manager header
3. `include/radar_manager.h` - Radar manager header
4. `include/radar_protocol.h` - Radar protocol header
5. `include/wifi_manager.h` - WiFi manager header
6. `include/wm_strings_zh.h` - Deleted (Chinese strings)
7. `src/main.cpp` - Main source
8. `src/mqtt_manager.cpp` - MQTT manager source
9. `src/radar_manager.cpp` - Radar manager source
10. `src/wifi_manager.cpp` - WiFi manager source

Let me look at the current diff to understand what was being worked on.


Let me look at the current changes to understand what was being worked on.


Show unstaged changes in detail


<persisted-output>
Output too large (106.7KB). Full output saved to: C:\Users\杨志佳\.claude\projects\d--PlatformIO1-Projects-Healthv2-0\861a1013-414b-44f3-b2a2-ee22ec67de60\tool-results\bdihd8suq.txt

Preview (first 2KB):
warning: in the working copy of 'include/config.h', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'include/mqtt_manager.h', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'include/radar_protocol.h', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'include/wifi_manager.h', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'src/wifi_manager.cpp', LF will be replaced by CRLF the next time Git touches it
diff --git a/include/config.h b/include/config.h
index a7207c1..6d2b281 100644
--- a/include/config.h
+++ b/include/config.h
@@ -16,10 +16,15 @@
 // 1. 硬件引脚定义 (ESP32-S3)
 // ============================================================

-// --- R60ABD1 毫米波雷达 ---
+// --- 雷达型号选择 ---
+#define RADAR_TYPE_ABD1       0   // R60ABD1: 心率/呼吸精准, 跌倒一般
+#define RADAR_TYPE_AFD1       1   // R60AFD1: 跌倒/姿态精准, 无心率呼吸
+#define RADAR_TYPE            RADAR_TYPE_ABD1  // ABD1 (心率/呼吸/睡眠)
+
+// --- 毫米波雷达 ---
 #define RADAR_UART_PORT       Serial2
 #define RADAR_RX_PIN          17
-#define RADAR_TX_PIN          1             // UART 发送 (从18挪到1)
+#define RADAR_TX_PIN          1
 #define RADAR_BAUD_RATE       115200

 // --- 雷达备用 GPIO ---
@@ -72,8 +77,8 @@
 // ============================================================

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

@@ -115,8 +120,9 @@
 // 6. 上报配置 (协议规范 §4.1, §7.2)
 // ============================================================

-#define REPORT_INTERVAL_MS    1000                // 遥测上报周期 1秒
+#define REPORT_INTERVAL_MS    1000                // 遥测上报周期 1秒 (有人时)
 #define REPORT_QOS            0                   // 遥测 QoS 0
...
</persisted-output>

warning: in the working copy of 'include/config.h', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'include/mqtt_manager.h', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'include/radar_protocol.h', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'include/wifi_manager.h', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'src/wifi_manager.cpp', LF will be replaced by CRLF the next time Git touches it
diff --git a/include/config.h b/include/config.h
index a7207c1..6d2b281 100644
--- a/include/config.h
+++ b/include/config.h
@@ -16,10 +16,15 @@
 // 1. 硬件引脚定义 (ESP32-S3)
 // ============================================================

-// --- R60ABD1 毫米波雷达 ---
+// --- 雷达型号选择 ---
+#define RADAR_TYPE_ABD1       0   // R60ABD1: 心率/呼吸精准, 跌倒一般
+#define RADAR_TYPE_AFD1       1   // R60AFD1: 跌倒/姿态精准, 无心率呼吸
+#define RADAR_TYPE            RADAR_TYPE_ABD1  // ABD1 (心率/呼吸/睡眠)
+
+// --- 毫米波雷达 ---
 #define RADAR_UART_PORT       Serial2
 #define RADAR_RX_PIN          17
-#define RADAR_TX_PIN          1             // UART 发送 (从18挪到1)
+#define RADAR_TX_PIN          1
 #define RADAR_BAUD_RATE       115200

 // --- 雷达备用 GPIO ---
@@ -72,8 +77,8 @@
 // ============================================================

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

@@ -115,8 +120,9 @@
 // 6. 上报配置 (协议规范 §4.1, §7.2)
 // ============================================================

-#define REPORT_INTERVAL_MS    1000                // 遥测上报周期 1秒
+#define REPORT_INTERVAL_MS    1000                // 遥测上报周期 1秒 (有人时)
 #define REPORT_QOS            0                   // 遥测 QoS 0
+#define PRESENCE_HEARTBEAT_MS 60000               // 无人心跳间隔 60秒 (4G省流量)
 #define STATUS_QOS            1                   // 状态/响应 QoS 1

 // ============================================================
@@ -130,6 +136,7 @@
 #define TEMP_LOW              5.0f
 #define TEMP_HIGH             45.0f
 #define HUMI_LOW              15.0f
+#define PRESENCE_HYSTERESIS   8   // 有人无人防抖 (PCB信号抖动时过滤)
 #define HUMI_HIGH             90.0f
 #define WATER_DEBOUNCE_MS     50

@@ -167,10 +174,11 @@

 struct TelemetryData {
     // --- 设备标识 ---
-    char     saleid[12];          // 站点编号 "10100001"
-    char     gateid[16];          // 网关编号 "10100001001"
-    char     meterId[8];          // 节点ID "ENV001"
+    char     saleid[12];          // 站点编号 (已弃用)
+    char     gateid[16];          // 网关编号
+    char     meterId[8];          // 节点ID (已弃用, 新用 gateid+后缀)
     char     meterName[16];       // 设备型号 "RadarDHT11"
+    char     deviceType[16];      // 设备类型枚举 bedroom/bathroom/...

     // --- 环境数据 (DHT11) ---
     float    temp;                // 温度 ℃
@@ -187,6 +195,8 @@ struct TelemetryData {
     uint8_t  heartRate;           // 心率 bpm (无效=0)
     uint8_t  breathRate;          // 呼吸 次/分 (无效=0)
     uint8_t  body;                // 人体能量/体征值
+    uint8_t  sleepState;          // 睡眠状态 0=清醒 1=浅睡 2=深睡
+    uint8_t  sleepScore;          // 睡眠评分 0~100
     bool     radarValid;          // 雷达数据有效性
     bool     vitalValid;          // 心率呼吸有效性

diff --git a/include/mqtt_manager.h b/include/mqtt_manager.h
index e830043..fb51036 100644
--- a/include/mqtt_manager.h
+++ b/include/mqtt_manager.h
@@ -83,6 +83,9 @@ public:
     /** 本地报警上报 status/{gateid}/alarm (§4.7 扩展) */
     void publishLocalAlarm(uint8_t alarmType, bool triggered, const char* desc);

+    /** 无人极简报文 (仅 Presence+RadarValid, 省4G流量) */
+    void publishPresenceOnly(bool presence, bool radarValid, uint32_t sequence);
+
     // ============================================================
     // 回调注册 (协议规范 §5)
     // ============================================================
diff --git a/include/radar_manager.h b/include/radar_manager.h
index 30935fd..adbdb08 100644
--- a/include/radar_manager.h
+++ b/include/radar_manager.h
@@ -21,6 +21,8 @@ struct RadarRawData {
     uint8_t  breathRate;     // 呼吸
     uint8_t  body;           // 体动/能量值
     uint16_t distance;       // 距离 cm
+    uint8_t  sleepState;     // 睡眠状态: 0=清醒 1=浅睡 2=深睡
+    uint8_t  sleepScore;     // 睡眠评分: 0~100

     bool     radarValid;     // 雷达有效性
     bool     vitalValid;     // 生命体征有效性
@@ -68,6 +70,7 @@ private:
     void _extractPresence(const RadarFrame& frame);
     void _extractBreathing(const RadarFrame& frame);
     void _extractHeartRate(const RadarFrame& frame);
+    void _extractFall(const RadarFrame& frame);      // AFD1 跌倒
     void _extractSleep(const RadarFrame& frame);
     void _extractWorkStatus(const RadarFrame& frame);
     void _extractDetectRange(const RadarFrame& frame);
@@ -85,6 +88,13 @@ private:
     RadarCallback   _callback;
     uint32_t        _frameCount;
     uint32_t        _errorCount;
+    uint32_t        _lastHeartBeat;   // 上次心率数据时间
+    uint32_t        _lastBreathBeat;  // 上次呼吸数据时间
+
+    // 有人无人防抖 (PCB 信号不稳定时过滤跳变)
+    bool            _stablePresence;  // 防抖后的稳定有人/无人
+    uint8_t         _presenceCnt;     // 连续一致计数
+    bool            _lastRawPresence; // 上一次原始值
 };

 #endif // RADAR_MANAGER_H
diff --git a/include/radar_protocol.h b/include/radar_protocol.h
index 205fc8e..302d625 100644
--- a/include/radar_protocol.h
+++ b/include/radar_protocol.h
@@ -33,9 +33,10 @@ enum RadarControl : uint8_t {
     CTRL_WORK_STATUS    = 0x05,   // 工作状态
     CTRL_DETECT_RANGE   = 0x07,   // 探测范围
     CTRL_PRESENCE       = 0x80,   // 人体存在
-    CTRL_BREATHING      = 0x81,   // 呼吸检测
-    CTRL_SLEEP          = 0x84,   // 睡眠监测
-    CTRL_HEART_RATE     = 0x85,   // 心率监测
+    CTRL_BREATHING      = 0x81,   // 呼吸检测 (仅 ABD1)
+    CTRL_FALL           = 0x83,   // 跌倒检测 (仅 AFD1)
+    CTRL_SLEEP          = 0x84,   // 睡眠监测 (仅 ABD1)
+    CTRL_HEART_RATE     = 0x85,   // 心率监测 (仅 ABD1)
 };

 // ============================================================
@@ -73,6 +74,7 @@ enum SleepStatus : uint8_t {
     SLEEP_AWAKE         = 0,      // 清醒
     SLEEP_LIGHT         = 1,      // 浅睡
     SLEEP_DEEP          = 2,      // 深睡
+    SLEEP_ABSENT        = 3,      // 离床/无人/无效
 };

 // ============================================================
diff --git a/include/wifi_manager.h b/include/wifi_manager.h
index 3e3a904..b6c327e 100644
--- a/include/wifi_manager.h
+++ b/include/wifi_manager.h
@@ -1,10 +1,10 @@
 /**
  * @file wifi_manager.h
- * @brief WiFi 配网管理器
+ * @brief WiFi 配网管理器 (AP + WebServer)
  *
  * 功能:
  *  - 自动连接已保存的 WiFi
- *  - 配网失败/首次启动 → AP 模式 + 强制门户页面
+ *  - 配网失败/首次启动 → AP 模式 + Captive Portal
  *  - 自定义参数: MQTT 服务器/端口/用户/密码、设备名称、环境备注
  *  - 长按按钮重置配置
  *  - LED 状态指示
@@ -15,8 +15,14 @@
 #define WIFI_MANAGER_H

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

@@ -27,7 +33,10 @@ struct DeviceConfig {
     char mqtt_user[20];
     char mqtt_pass[20];
     char device_id[20];
-    char env_note[20];      // 环境备注 (如: "主卧", "客厅", "老人房")
+    char env_note[20];      // 环境备注 (房间名)
+    char env_type[16];      // 环境类型 bedroom/bathroom/...
+    char wifi_ssid[33];     // WiFi SSID
+    char wifi_pass[65];     // WiFi 密码
 };

 class WiFiMgmt {
@@ -38,100 +47,66 @@ public:
     // 单例指针 (用于静态回调)
     static WiFiMgmt* _instance;

-    /**
-     * @brief 初始化 WiFi 管理器
-     * @return true=成功
-     */
     bool begin();
-
-    /**
-     * @brief 主循环, 处理配网流程和按钮检测
-     * @return true=WiFi 已连接
-     */
     bool loop();
-
-    /**
-     * @brief WiFi 是否已连接
-     */
     bool isConnected() const;
-
-    /**
-     * @brief 获取设备配置
-     */
+    bool justProvisioned();     // 刚配网成功 (读取后自动清除)
     DeviceConfig getConfig() const;
-
-    /**
-     * @brief 获取设备 ID (用于 MQTT client_id)
-     */
     const char* getDeviceId() const;
-
-    /**
-     * @brief 获取环境备注
-     */
     const char* getEnvNote() const;
-
-    /**
-     * @brief 强制进入配网模式
-     */
     void startConfigPortal();
-
-    /**
-     * @brief 清除所有配置并重启
-     */
+    void handleBLEProvision(const char* json);  // BLE 配网回调
     void resetSettings();

 private:
-    /**
-     * @brief 尝试连接 WiFi
-     */
-    bool _connectWiFi();
-
-    /**
-     * @brief 设置 LED 模式 (线程安全, 无阻塞)
-     * @param mode 0=灭 1=慢闪(断连1s) 2=快闪(配网300ms) 3=常亮(已连接)
-     */
+    bool _connectWiFi();          // 仅尝试已保存凭据
+    bool _tryConnect(const char* ssid, const char* pass);  // 尝试连接指定 WiFi
+    bool _loadConfig();
+    bool _saveConfig();
     void _setLEDMode(uint8_t mode);
-
-    /**
-     * @brief LED 闪烁独立任务 (FreeRTOS, 不受主循环阻塞影响)
-     */
     static void _ledTaskFunc(void* param);
-
-    /**
-     * @brief 检测按钮长按
-     */
     bool _checkButtonLongPress();

-    /**
-     * @brief 加载/保存配置到 LittleFS
-     */
-    bool _loadConfig();
-    bool _saveConfig();
-
-    /**
-     * @brief 保存 WiFi 参数回调 (WiFiManager 的回调)
-     */
-    static void _saveParamsCallback();
-    static void _apCallback(WiFiManager* wm);
-
-    WiFiManager     _wm;
-    DeviceConfig    _config;
-    bool            _connected;
-    uint32_t        _lastCheckTime;
-    uint32_t        _buttonPressTime;
-    bool            _buttonPressed;
-
-    // LED 独立任务 (不被 startConfigPortal 阻塞影响)
+    // Web 服务器
+    void _startPortal();
+    void _stopPortal();
+    void _handleRoot();
+    void _handleSave();
+    void _handleReset();
+    void _updateWifiScan();    // WiFi 扫描缓存
+    String _htmlPage();        // 返回配网 HTML 页面
+
+    // BLE 蓝牙配网
+    void _startBLE();
+    void _stopBLE();
+
+    WebServer*     _server;
+    DNSServer*     _dns;
+    DeviceConfig   _config;
+    bool           _connected;
+    bool           _portalActive;
+    bool           _portalStopFlag;     // 延迟关闭标记
+    uint32_t       _portalStartTime;
+    String         _pendingSSID;
+    String         _pendingPass;
+    bool           _provisionSuccess;  // 刚配网成功标记
+    uint32_t       _lastCheckTime;
+    uint32_t       _buttonPressTime;
+    bool           _buttonPressed;
+
+    // WiFi 扫描缓存 (避免每次打开页面都扫描3-8秒)
+    String         _wifiScanCache;
+    uint32_t       _lastScanTime;
+
+    // LED 独立任务
     TaskHandle_t    _ledTaskHandle;
     volatile uint8_t _ledMode;  // 0=灭 1=慢闪 2=快闪 3=常亮

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

 #endif // WIFI_MANAGER_H
diff --git a/include/wm_strings_zh.h b/include/wm_strings_zh.h
deleted file mode 100644
index e3398b2..0000000
--- a/include/wm_strings_zh.h
+++ /dev/null
@@ -1,262 +0,0 @@
-/**
- * wm_strings_zh.h
- * 中文语言包 for WiFiManager
- * 基于 wm_strings_en.h 翻译
- *
- * WiFiManager, a library for the ESP8266/Arduino platform
- * for configuration of WiFi credentials using a Captive Portal
- *
- * @author Creator tzapu
- * @author tablatronix
- * @version 0.0.0
- * @license MIT
- */
-
-#ifndef _WM_STRINGS_ZH_H_
-#define _WM_STRINGS_ZH_H_
-
-// strings files must include a consts file!
-#include "wm_consts_en.h" // include constants, tokens, routes
-
-const char WM_LANGUAGE[] PROGMEM = "zh-CN"; // i18n lang code
-
-const char HTTP_HEAD_START[]       PROGMEM = "<!DOCTYPE html>"
-""
-"<meta name='format-detection' content='telephone=no'>"
-"<meta charset='UTF-8'>"
-"<meta  name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'/>"
-"<title>{v}</title>";
-
-const char HTTP_SCRIPT[]           PROGMEM = "";
-
-const char HTTP_HEAD_END[]         PROGMEM = "
"; // {c} = _bodyclass
-
-const char HTTP_ROOT_MAIN[]        PROGMEM = "
{t}

{v}
";
-
-const char * const HTTP_PORTAL_MENU[] PROGMEM = {
-"<form action='/wifi'    method='get'><button>配置 WiFi</button></form>
\n",     // MENU_WIFI
-"<form action='/0wifi'   method='get'><button>配置 WiFi (不扫描)</button></form>
\n", // MENU_WIFINOSCAN
-"<form action='/info'    method='get'><button>系统信息</button></form>
\n",       // MENU_INFO
-"<form action='/param'   method='get'><button>参数设置</button></form>
\n",       // MENU_PARAM
-"<form action='/close'   method='get'><button>关闭页面</button></form>
\n",       // MENU_CLOSE
-"<form action='/restart' method='get'><button>重启设备</button></form>
\n",       // MENU_RESTART
-"<form action='/exit'    method='get'><button>退出配网</button></form>
\n",       // MENU_EXIT
-"<form action='/erase'   method='get'><button class='D'>清除配置</button></form>
\n", // MENU_ERASE
-"<form action='/update'  method='get'><button>固件升级</button></form>
\n",       // MENU_UPDATE
-"

" // MENU_SEP
-};
-
-const char HTTP_PORTAL_OPTIONS[]   PROGMEM = "";
-const char HTTP_ITEM_QI[]          PROGMEM = "

";
-const char HTTP_ITEM_QP[]          PROGMEM = "
{r}%
";
-const char HTTP_ITEM[]             PROGMEM = "
{v}{qi}{qp}
";
-
-const char HTTP_FORM_START[]       PROGMEM = "<form method='POST' action='{v}'>";
-const char HTTP_FORM_WIFI[]        PROGMEM = "WiFi 名称 (SSID)<input id='s' name='s' maxlength='32' autocorrect='off' autocapitalize='none' placeholder='{v}'>
WiFi 密码<input id='p' name='p' maxlength='64' type='password' placeholder='{p}'><input type='checkbox' id='showpass' onclick='f()'> 显示密码
";
-const char HTTP_FORM_WIFI_END[]    PROGMEM = "";
-const char HTTP_FORM_STATIC_HEAD[] PROGMEM = "

";
-const char HTTP_FORM_END[]         PROGMEM = "

<button type='submit'>保存配置</button></form>";
-const char HTTP_FORM_LABEL[]       PROGMEM = "{t}";
-const char HTTP_FORM_PARAM_HEAD[]  PROGMEM = "

";
-const char HTTP_FORM_PARAM[]       PROGMEM = "
<input id='{i}' name='{n}' maxlength='{l}' value='{v}' {c}>\n";
-
-const char HTTP_SCAN_LINK[]        PROGMEM = "
<form action='/wifi?refresh=1' method='POST'><button name='refresh' value='1'>重新扫描</button></form>";
-const char HTTP_SAVED[]            PROGMEM = "
正在保存配置
尝试连接 WiFi...
如果连接失败，请重新连接 AP 再试一次
";
-const char HTTP_PARAMSAVED[]       PROGMEM = "
配置已保存

";
-const char HTTP_END[]              PROGMEM = "
";
-const char HTTP_ERASEBTN[]         PROGMEM = "
<form action='/erase' method='get'><button class='D'>清除 WiFi 配置</button></form>";
-const char HTTP_UPDATEBTN[]        PROGMEM = "
<form action='/update' method='get'><button>固件升级</button></form>";
-const char HTTP_BACKBTN[]          PROGMEM = "

<form action='/' method='get'><button>返回首页</button></form>";
-
-const char HTTP_STATUS_ON[]        PROGMEM = "
已连接 {v}
IP 地址: {i}
";
-const char HTTP_STATUS_OFF[]       PROGMEM = "
未连接 {v}{r}
";
-const char HTTP_STATUS_OFFPW[]     PROGMEM = "
WiFi 密码错误";
-const char HTTP_STATUS_OFFNOAP[]   PROGMEM = "
未找到该 WiFi";
-const char HTTP_STATUS_OFFFAIL[]   PROGMEM = "
无法连接";
-const char HTTP_STATUS_NONE[]      PROGMEM = "
未设置 WiFi
";
-const char HTTP_BR[]               PROGMEM = "
";
-
-const char HTTP_STYLE[]            PROGMEM = "";
-
-#ifndef WM_NOHELP
-const char HTTP_HELP[]             PROGMEM =
- "

可用页面

"
- "
"
- "

页面	功能

"
- "
/	"
- "主菜单页面
"
- "
/wifi	"
- "显示 WiFi 扫描结果并配置连接 (/0wifi 不扫描)
"
- "
/wifisave	"
- "保存 WiFi 配置并连接设备
"
- "
/param	"
- "参数设置页面
"
- "
/info	"
- "系统信息页面
"
- "
/u	"
- "固件 OTA 升级
"
- "
/close	"
- "关闭弹出窗口，配网服务保持运行
"
- "
/exit	"
- "退出配网模式，配网服务将关闭
"
- "
/restart	"
- "重启设备
"
- "
/erase	"
- "清除 WiFi 配置并重启。清除后设备不会自动连接，需重新配网
"
- "
"
- "
GitHub https://github.com/tzapu/WiFiManager.";
-#else
-const char HTTP_HELP[]             PROGMEM = "";
-#endif
-
-const char HTTP_UPDATE[] PROGMEM = "上传新固件
<form method='POST' action='u' enctype='multipart/form-data' onchange=\"(function(el){document.getElementById('uploadbin').style.display = el.value=='' ? 'none' : 'initial';})(this)\"><input type='file' name='update' accept='.bin,application/octet-stream'><button id='uploadbin' type='submit' class='h D'>开始升级</button></form>* 如果在配网页面内无法使用，请在浏览器中打开 http://192.168.4.1";
-const char HTTP_UPDATE_FAIL[] PROGMEM = "
升级失败!
请重启设备后重试
";
-const char HTTP_UPDATE_SUCCESS[] PROGMEM = "
升级成功!
设备正在重启...
";
-
-#ifdef WM_JSTEST
-const char HTTP_JS[] PROGMEM =
-"";
-#endif
-
-// Info html
-#ifdef ESP32
-	const char HTTP_INFO_esphead[]    PROGMEM = "
ESP32 信息

";
-	const char HTTP_INFO_chiprev[]    PROGMEM = "
芯片版本

{1}
";
-  	const char HTTP_INFO_lastreset[]  PROGMEM = "
上次复位原因

CPU0: {1}
CPU1: {2}
";
-  	const char HTTP_INFO_aphost[]     PROGMEM = "
AP 主机名

{1}
";
-    const char HTTP_INFO_psrsize[]    PROGMEM = "
PSRAM 大小

{1} 字节
";
-	const char HTTP_INFO_temp[]       PROGMEM = "
芯片温度

{1}°C / {2}°F
";
-    const char HTTP_INFO_hall[]       PROGMEM = "
霍尔传感器

{1}
";
-#else
-	const char HTTP_INFO_esphead[]    PROGMEM = "
ESP8266 信息

";
-	const char HTTP_INFO_fchipid[]    PROGMEM = "
Flash 芯片 ID

{1}
";
-	const char HTTP_INFO_corever[]    PROGMEM = "
Core 版本

{1}
";
-	const char HTTP_INFO_bootver[]    PROGMEM = "
Boot 版本

{1}
";
-	const char HTTP_INFO_lastreset[]  PROGMEM = "
上次复位原因

{1}
";
-	const char HTTP_INFO_flashsize[]  PROGMEM = "
实际 Flash 大小

{1} 字节
";
-#endif
-
-const char HTTP_INFO_memsmeter[]  PROGMEM = "
<progress value='{1}' max='{2}'></progress>
";
-const char HTTP_INFO_memsketch[]  PROGMEM = "
内存 - 程序大小

已用 / 总计
{1} / {2} 字节";
-const char HTTP_INFO_freeheap[]   PROGMEM = "
内存 - 可用堆

{1} 字节
";
-const char HTTP_INFO_wifihead[]   PROGMEM = "

WiFi 信息

";
-const char HTTP_INFO_uptime[]     PROGMEM = "
运行时间

{1} 分 {2} 秒
";
-const char HTTP_INFO_chipid[]     PROGMEM = "
芯片 ID

{1}
";
-const char HTTP_INFO_idesize[]    PROGMEM = "
Flash 大小

{1} 字节
";
-const char HTTP_INFO_sdkver[]     PROGMEM = "
SDK 版本

{1}
";
-const char HTTP_INFO_cpufreq[]    PROGMEM = "
CPU 频率

{1}MHz
";
-const char HTTP_INFO_apip[]       PROGMEM = "
AP IP 地址

{1}
";
-const char HTTP_INFO_apmac[]      PROGMEM = "
AP MAC 地址

{1}
";
-const char HTTP_INFO_apssid[]     PROGMEM = "
AP SSID

{1}
";
-const char HTTP_INFO_apbssid[]    PROGMEM = "
BSSID

{1}
";
-const char HTTP_INFO_stassid[]    PROGMEM = "
Station SSID

{1}
";
-const char HTTP_INFO_staip[]      PROGMEM = "
Station IP

{1}
";
-const char HTTP_INFO_stagw[]      PROGMEM = "
网关地址

{1}
";
-const char HTTP_INFO_stasub[]     PROGMEM = "
子网掩码

{1}
";
-const char HTTP_INFO_dnss[]       PROGMEM = "
DNS 服务器

{1}
";
-const char HTTP_INFO_host[]       PROGMEM = "
主机名

{1}
";
-const char HTTP_INFO_stamac[]     PROGMEM = "
Station MAC

{1}
";
-const char HTTP_INFO_conx[]       PROGMEM = "
连接状态

{1}
";
-const char HTTP_INFO_autoconx[]   PROGMEM = "
自动连接

{1}
";
-
-const char HTTP_INFO_aboutver[]     PROGMEM = "
WiFiManager 版本

{1}
";
-const char HTTP_INFO_aboutarduino[] PROGMEM = "
Arduino 版本

{1}
";
-const char HTTP_INFO_aboutsdk[]     PROGMEM = "
ESP-SDK/IDF 版本

{1}
";
-const char HTTP_INFO_aboutdate[]    PROGMEM = "
构建日期

{1}
";
-
-// ============================================================
-// 文本常量
-// ============================================================
-const char S_brand[]              PROGMEM = "健康监测配网";
-const char S_debugPrefix[]        PROGMEM = "*wm:";
-const char S_y[]                  PROGMEM = "是";
-const char S_n[]                  PROGMEM = "否";
-const char S_enable[]             PROGMEM = "已启用";
-const char S_disable[]            PROGMEM = "已禁用";
-const char S_GET[]                PROGMEM = "GET";
-const char S_POST[]               PROGMEM = "POST";
-const char S_NA[]                 PROGMEM = "未知";
-const char S_passph[]             PROGMEM = "********";
-const char S_titlewifisaved[]     PROGMEM = "WiFi 已保存";
-const char S_titlewifisettings[]  PROGMEM = "设置已保存";
-const char S_titlewifi[]          PROGMEM = "配置 WiFi";
-const char S_titleinfo[]          PROGMEM = "系统信息";
-const char S_titleparam[]         PROGMEM = "参数设置";
-const char S_titleparamsaved[]    PROGMEM = "设置已保存";
-const char S_titleexit[]          PROGMEM = "退出配网";
-const char S_titlereset[]         PROGMEM = "重置";
-const char S_titleerase[]         PROGMEM = "清除配置";
-const char S_titleclose[]         PROGMEM = "关闭页面";
-const char S_options[]            PROGMEM = "选项";
-const char S_nonetworks[]         PROGMEM = "未发现 WiFi 网络，请点击刷新重新扫描。";
-const char S_staticip[]           PROGMEM = "静态 IP";
-const char S_staticgw[]           PROGMEM = "静态网关";
-const char S_staticdns[]          PROGMEM = "静态 DNS";
-const char S_subnet[]             PROGMEM = "子网掩码";
-const char S_exiting[]            PROGMEM = "正在退出配网模式";
-const char S_resetting[]          PROGMEM = "设备将在几秒后重启";
-const char S_closing[]            PROGMEM = "可以关闭此页面，配网服务将继续运行";
-const char S_error[]              PROGMEM = "发生错误";
-const char S_notfound[]           PROGMEM = "页面未找到\n\n";
-const char S_uri[]                PROGMEM = "URI: ";
-const char S_method[]             PROGMEM = "\n请求方法: ";
-const char S_args[]               PROGMEM = "\n参数: "


The diff is quite large (106KB). Let me read more of it to understand the context. But I can see the project is an ESP32-based health monitoring system using a millimeter wave radar (R60ABD1 or R60AFD1), with WiFi, MQTT, and various sensors. Let me look at the main files to understand the project better, and also check if there were any recent AI session transcripts that might give context on what was being discussed.


Let me read the key modified files and check the most recent session transcript for context.


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	bool    g_localTempAbnActive    = false;
44	bool    g_localHumiAbnActive    = false;
45	bool    g_localRadarFaultActive = false;
46	bool    g_localDhtFaultActive   = false;
47
48	// 报警防抖计数器 (连续N次确认才触发)
49	uint8_t g_tempAbnCnt       = 0;
50	uint8_t g_humiAbnCnt       = 0;
51	uint8_t g_radarTimeoutCnt  = 0;
52	uint8_t g_heartAbnCnt      = 0;      // 心率异常防抖
53	uint8_t g_breathAbnCnt     = 0;      // 呼吸异常防抖
54	uint32_t g_fallDetectMs     = 0;      // 跌倒检测计时 (ms)
55	uint32_t g_fallClearMs      = 0;      // 跌倒清除计时 (ms)
56	bool     g_fallDismissed     = false;  // 已手动解除, 不重复报警
57	#define ALARM_HYSTERESIS      3       // 连续3次确认才触发
58	#define FALL_DETECT_MS        2000    // 跌倒持续2秒才触发 (房顶安装, 过滤坐下)
59	#define FALL_CLEAR_MS         5000    // 跌倒消失5秒后才清除 (防误消)
60
61	// ============================================================
62	// 时间同步
63	// ============================================================
64
65	void syncTime() {
66	    Serial.println(F("[时间] 正在同步时间..."));
67	    configTime(NTP_GMT_OFFSET_SEC, NTP_DAYLIGHT_OFFSET, NTP_SERVER, "pool.ntp.org");
68
69	    struct tm timeinfo;
70	    int retry = 0;
71	    while (!getLocalTime(&timeinfo) && retry < 20) {
72	        delay(500);
73	        Serial.print(".");
74	        retry++;
75	    }
76
77	    if (retry < 20) {
78	        Serial.println();
79	        Serial.printf("[时间] ✅ 时间同步成功: %04d-%02d-%02d %02d:%02d:%02d\n",
80	                      timeinfo.tm_year + 1900, timeinfo.tm_mon + 1,
81	                      timeinfo.tm_mday, timeinfo.tm_hour,
82	                      timeinfo.tm_min, timeinfo.tm_sec);
83	    } else {
84	        Serial.println(F("\n[时间] ⚠ 时间同步失败, 使用默认时间"));
85	    }
86	}
87
88	// ============================================================
89	// MQTT 告警指令回调 (协议规范 §5.1, §5.2)
90	// ============================================================
91
92	void handleAlarmCommand(const AlarmCommand& cmd) {
93	    if (strcmp(cmd.type, "alarm_restore") == 0) {
94	        // 告警恢复 — 远程指令与手动按钮同等优先级
95	        g_alarm.executeRestore(cmd);
96
97	        // 清除全部本地告警状态 (远程恢复=手动解除)
98	        g_localFallActive       = false;
99	        g_localWaterActive      = false;
100	        g_localHeartAbnActive   = false;
101	        g_localBreathAbnActive  = false;
102	        g_localTempAbnActive    = false;
103	        g_localHumiAbnActive    = false;
104	        g_localRadarFaultActive = false;
105	        g_localDhtFaultActive   = false;
106
107	        // 防抖计数器也复位
108	        g_tempAbnCnt      = 0;
109	        g_humiAbnCnt      = 0;
110	        g_radarTimeoutCnt = 0;
111	        g_heartAbnCnt     = 0;
112	        g_breathAbnCnt    = 0;
113	        g_fallDetectMs     = 0;
114	        g_fallClearMs      = 0;
115	        g_fallDismissed    = false;
116	    } else {
117	        // 告警触发
118	        g_alarm.executeAlarm(cmd);
119	    }
120
121	    // 回复响应 (§4.5)
122	    g_mqtt.publishCmdResponse(cmd.cmdId, cmd.type, cmd.alarmType,
123	                              true, "0", "");
124	}
125
126	// ============================================================
127	// MQTT 控制指令回调 (§5.3)
128	// ============================================================
129
130	void handleControlCommand(const ControlCommand& cmd) {
131	    Serial.printf("[控制] 收到控制指令: %s\n", cmd.controlType);
132
133	    bool success = true;
134	    const char* errCode = "0";
135	    const char* errMsg  = "";
136
137	    if (strcmp(cmd.controlType, "buzzer_test") == 0) {
138	        uint32_t dur = cmd.params_duration > 0 ? cmd.params_duration : 3;
139	        g_alarm.buzzerTest(dur);
140	    }
141	    else if (strcmp(cmd.controlType, "device_restart") == 0) {
142	        Serial.println(F("[控制] 🔄 远程重启..."));
143	        g_mqtt.publishCmdResponse(cmd.cmdId, "control", "", true, "0", "");
144	        g_mqtt.publishOffline();
145	        delay(500);
146	        ESP.restart();
147	    }
148	    else if (strcmp(cmd.controlType, "factory_reset") == 0) {
149	        Serial.println(F("[控制] 🔄 恢复出厂设置..."));
150	        g_wifi.resetSettings();
151	    }
152	    else if (strcmp(cmd.controlType, "enter_provision") == 0) {
153	        Serial.println(F("[控制] 📶 进入配网模式"));
154	        g_wifi.startConfigPortal();
155	    }
156	    else {
157	        success = false;
158	        errCode = "2001";
159	        errMsg  = "不支持的指令类型";
160	    }
161
162	    g_mqtt.publishCmdResponse(cmd.cmdId, "control", "",
163	                              success, errCode, errMsg);
164	}
165
166	// ============================================================
167	// MQTT 配网指令回调 (§5.4)
168	// ============================================================
169
170	void handleProvisionCommand(const ProvisionCommand& cmd) {
171	    Serial.printf("[配网] 📶 收到配网指令: SSID=%s\n", cmd.wifi_ssid);
172
173	    if (strlen(cmd.wifi_ssid) == 0) {
174	        g_mqtt.publishProvisionResponse(cmd.cmdId, false,
175	                                        "1002", "未提供WiFi SSID");
176	        return;
177	    }
178
179	    // 连接新 WiFi
180	    g_mqtt.publishProvisionStatus("wifi_connecting", "");
181
182	    WiFi.begin(cmd.wifi_ssid, cmd.wifi_password);
183	    int retry = 0;
184	    while (WiFi.status() != WL_CONNECTED && retry < 30) {
185	        delay(500);
186	        Serial.print(".");
187	        retry++;
188	    }
189
190	    if (WiFi.status() == WL_CONNECTED) {
191	        Serial.printf("\n[配网] ✅ WiFi 已连接, IP=%s\n",
192	                      WiFi.localIP().toString().c_str());
193
194	        // 更新 MQTT 服务器配置
195	        if (strlen(cmd.mqtt_broker) > 0) {
196	            g_mqtt.setServer(cmd.mqtt_broker, cmd.mqtt_port,
197	                           GATEID_DEFAULT, GATEID_DEFAULT); // 正式账号 = gateid
198	        }
199
200	        char signal[8];
201	        snprintf(signal, sizeof(signal), "%d", WiFi.RSSI());
202	        g_mqtt.publishProvisionResponse(cmd.cmdId, true, "0", "", signal);
203
204	        // 重连 MQTT
205	        g_mqtt.disconnect();
206	        g_mqtt.connect();
207	        g_mqtt.publishOnline();
208	    } else {
209	        Serial.println(F("\n[配网] ❌ WiFi 连接失败"));
210	        g_mqtt.publishProvisionStatus("wifi_failed", "");
211	        g_mqtt.publishProvisionResponse(cmd.cmdId, false,
212	                                        "1003", "WiFi连接超时");
213	    }
214	}
215
216	// ============================================================
217	// 构建遥测数据
218	// ============================================================
219
220	TelemetryData buildTelemetry(const RadarRawData& radar) {
221	    TelemetryData t;
222	    memset(&t, 0, sizeof(t));
223
224	    // 设备标识
225	    strncpy(t.gateid,    g_mqtt.getGateId(), sizeof(t.gateid) - 1);
226	    strncpy(t.meterName, METER_NAME_DEFAULT, sizeof(t.meterName) - 1);
227
228	    // 环境类型: 从配网配置读取 (默认 bedroom)
229	    DeviceConfig cfg = g_wifi.getConfig();
230	    strncpy(t.deviceType, cfg.env_type[0] ? cfg.env_type : "bedroom", sizeof(t.deviceType)-1);
231
232	    // DHT11
233	    t.temp     = g_sensor.getTemp();
234	    t.humi     = g_sensor.getHumi();
235	    t.dhtValid = g_sensor.isDhtValid();
236
237	    // 雷达
238	    t.presence   = radar.presence;
239	    t.motion     = radar.motion;
240	    t.fall       = radar.fall;
241	    t.resident   = radar.resident;
242	    t.heartRate  = radar.heartRate;
243	    t.breathRate = radar.breathRate;
244	    t.body       = radar.body;
245	    t.sleepState = radar.sleepState;
246	    t.sleepScore = radar.sleepScore;
247	    t.radarValid = radar.radarValid;
248	    t.vitalValid = radar.vitalValid;
249
250	    // 水浸
251	    t.rainDO    = g_sensor.getRainDO();
252	    t.rainValid = g_sensor.isRainValid();
253
254	    // 设备状态
255	    t.wifiReady  = g_wifi.isConnected() && g_mqtt.isConnected();
256	    t.uptimeSec  = (millis() - g_bootTime) / 1000;
257	    t.buzzerState = g_alarm.isBuzzerOn();
258	    t.lastUpdate = millis();
259
260	    return t;
261	}
262
263	// ============================================================
264	// 本地报警回调 → MQTT 上报 (连接时)
265	// ============================================================
266
267	void onLocalAlarmChange(uint8_t type, bool triggered, const char* desc) {
268	    if (g_mqtt.isConnected()) {
269	        g_mqtt.publishLocalAlarm(type, triggered, desc);
270	    }
271	}
272
273	// ============================================================
274	// 本地报警检查 (离线可用) — 人身安全 > 环境 > 设备
275	// ============================================================
276
277	void checkLocalAlarms(const RadarRawData& radar) {
278	    // 环境类型 → 报警开关
279	    const char* env = g_wifi.getConfig().env_type;
280	    bool enFall  = (strcmp(env, "bathroom") == 0 || strcmp(env, "hallway") == 0 || strcmp(env, "livingroom") == 0);
281	    bool enVital = (strcmp(env, "bedroom")  == 0);
282	    bool enWater = (strcmp(env, "bathroom") == 0);
283	    bool enEnv   = (strcmp(env, "bedroom")  == 0 || strcmp(env, "balcony") == 0 || strcmp(env, "kitchen") == 0);
284
285	    // ─── 前置条件: 有人才检测人体生命体征 ───
286	    bool humanPresent = radar.presence && radar.radarValid;
287
288	    // ─── ★★★★★ 人身安全: 跌倒 (仅浴室/走廊/客厅) ───
289	    if (enFall && radar.fall && humanPresent && !g_fallDismissed && millis() > 30000) {
290	        if (!g_localFallActive) {
291	            // 持续检测中: 必须满 FALL_DETECT_MS 才报警
292	            if (g_fallDetectMs == 0) {
293	                g_fallDetectMs = millis();
294	            } else if (millis() - g_fallDetectMs >= FALL_DETECT_MS) {
295	                g_localFallActive = true;
296	                g_fallDetectMs = 0;
297	                g_fallClearMs = 0;
298	                g_alarm.triggerLocal(AlarmManager::ALARM_FALL, "雷达检测到跌倒");
299	            }
300	        } else {
301	            g_fallClearMs = 0;  // 还在跌倒, 重置清除计时
302	        }
303	    } else {
304	        g_fallDetectMs = 0;
305	        // 解除抑制需要 fall 持续消失 FALL_CLEAR_MS (防 AFD1 频繁跳变)
306	        if (g_fallDismissed && g_fallClearMs == 0) {
307	            g_fallClearMs = millis();
308	        }
309	        if (!g_fallDismissed || millis() - g_fallClearMs >= FALL_CLEAR_MS) {
310	            g_fallDismissed = false;
311	            g_fallClearMs = 0;
312	        }
313	        if (g_localFallActive) {
314	            // 跌倒已消失 → 延迟清除
315	            if (g_fallClearMs == 0) {
316	                g_fallClearMs = millis();
317	            } else if (millis() - g_fallClearMs >= FALL_CLEAR_MS) {
318	                g_localFallActive = false;
319	                g_fallClearMs = 0;
320	                g_alarm.clearLocal(AlarmManager::ALARM_FALL);
321	            }
322	        }
323	    }
324
325	    // ─── ★★★★★ 人身安全: 心率异常 (仅卧室+ABD1, 防抖3次, 30s) ───
326	#if RADAR_TYPE != RADAR_TYPE_AFD1
327	    if (enVital && millis() > 30000 && humanPresent && radar.vitalValid && radar.heartRate > 0) {
328	        bool heartAbn = (radar.heartRate < HEART_RATE_MIN || radar.heartRate > HEART_RATE_MAX);
329	        if (heartAbn) {
330	            if (++g_heartAbnCnt >= ALARM_HYSTERESIS && !g_localHeartAbnActive) {
331	                g_localHeartAbnActive = true;
332	                char buf[32];
333	                snprintf(buf, sizeof(buf), "心率异常: %d bpm", radar.heartRate);
334	                g_alarm.triggerLocal(AlarmManager::ALARM_HEART_ABNORMAL, buf);
335	            }
336	        } else {
337	            g_heartAbnCnt = 0;
338	            if (g_localHeartAbnActive) {
339	                g_localHeartAbnActive = false;
340	                g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
341	            }
342	        }
343	    } else if (!humanPresent && g_localHeartAbnActive) {
344	        g_localHeartAbnActive = false;
345	        g_heartAbnCnt = 0;
346	        g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
347	    }
348	#endif  // RADAR_TYPE != AFD1
349
350	    // ─── ★★★★★ 人身安全: 呼吸异常 (仅卧室+ABD1, 防抖3次, 30秒后生效) ───
351	#if RADAR_TYPE != RADAR_TYPE_AFD1
352	    if (enVital && millis() > 30000 && humanPresent && radar.vitalValid && radar.breathRate > 0) {
353	        bool breathAbn = (radar.breathRate < BREATH_RATE_MIN || radar.breathRate > BREATH_RATE_MAX);
354	        if (breathAbn) {
355	            if (++g_breathAbnCnt >= ALARM_HYSTERESIS && !g_localBreathAbnActive) {
356	                g_localBreathAbnActive = true;
357	                char buf[32];
358	                snprintf(buf, sizeof(buf), "呼吸异常: %d 次/分", radar.breathRate);
359	                g_alarm.triggerLocal(AlarmManager::ALARM_BREATH_ABNORMAL, buf);
360	            }
361	        } else {
362	            g_breathAbnCnt = 0;
363	            if (g_localBreathAbnActive) {
364	                g_localBreathAbnActive = false;
365	                g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
366	            }
367	        }
368	    } else if (!humanPresent && g_localBreathAbnActive) {
369	        g_localBreathAbnActive = false;
370	        g_breathAbnCnt = 0;
371	        g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
372	    }
373	#endif  // RADAR_TYPE != AFD1
374
375	    // ─── ★★★ 环境安全: 水浸 (仅浴室) ───
376	    if (enWater) {
377	    bool waterLeak = !g_sensor.getRainDO() && g_sensor.isRainValid();
378	    if (waterLeak && !g_localWaterActive) {
379	        g_localWaterActive = true;
380	        g_alarm.triggerLocal(AlarmManager::ALARM_WATER_LEAK, "水浸传感器触发");
381	    } else if (!waterLeak && g_localWaterActive) {
382	        g_localWaterActive = false;
383	        g_alarm.clearLocal(AlarmManager::ALARM_WATER_LEAK);
384	    }
385	    }  // enWater
386
387	    // ─── ★★ 环境舒适: 温湿度异常 (仅卧室/阳台/厨房, 防抖3次, 30s) ───
388	    if (enEnv && millis() > 30000 && g_sensor.isDhtValid()) {
389	        float temp = g_sensor.getTemp();
390	        bool tempAbn = (temp < TEMP_LOW || temp > TEMP_HIGH);
391	        if (tempAbn) {
392	            if (++g_tempAbnCnt >= ALARM_HYSTERESIS && !g_localTempAbnActive) {
393	                g_localTempAbnActive = true;
394	                char buf[32];
395	                snprintf(buf, sizeof(buf), "温度%.1f°C 超出范围", temp);
396	                g_alarm.triggerLocal(AlarmManager::ALARM_TEMP_ABNORMAL, buf);
397	            }
398	        } else {
399	            g_tempAbnCnt = 0;
400	            if (g_localTempAbnActive) {
401	                g_localTempAbnActive = false;
402	                g_alarm.clearLocal(AlarmManager::ALARM_TEMP_ABNORMAL);
403	            }
404	        }
405
406	        // ─── ★★ 环境舒适: 湿度异常 (防抖3次) ───
407	        float humi = g_sensor.getHumi();
408	        bool humiAbn = (humi < HUMI_LOW || humi > HUMI_HIGH);
409	        if (humiAbn) {
410	            if (++g_humiAbnCnt >= ALARM_HYSTERESIS && !g_localHumiAbnActive) {
411	                g_localHumiAbnActive = true;
412	                char buf[32];
413	                snprintf(buf, sizeof(buf), "湿度%.1f%% 超出范围", humi);
414	                g_alarm.triggerLocal(AlarmManager::ALARM_HUMI_ABNORMAL, buf);
415	            }
416	        } else {
417	            g_humiAbnCnt = 0;
418	            if (g_localHumiAbnActive) {
419	                g_localHumiAbnActive = false;
420	                g_alarm.clearLocal(AlarmManager::ALARM_HUMI_ABNORMAL);
421	            }
422	        }
423	    }
424
425	    // ─── 设备：雷达超时 (连续5秒无数据, 仅标记, 不报警) ───
426	    if (g_radar.isTimeout()) {
427	        if (++g_radarTimeoutCnt >= 5 && !g_localRadarFaultActive) {
428	            g_localRadarFaultActive = true;
429	            Serial.println(F("[系统] ⚠ 雷达超时无数据"));
430	        }
431	    } else {
432	        g_radarTimeoutCnt = 0;
433	        if (g_localRadarFaultActive) {
434	            g_localRadarFaultActive = false;
435	            Serial.println(F("[系统] ✅ 雷达数据恢复"));
436	        }
437	    }
438
439	    // ─── 设备：DHT 持续故障 (仅标记, 不报警) ───
440	    if (g_sensor.isDhtFault() && !g_localDhtFaultActive && millis() > 60000) {
441	        g_localDhtFaultActive = true;
442	        Serial.println(F("[系统] ⚠ DHT11 温湿度传感器故障"));
443	    } else if (!g_sensor.isDhtFault() && g_localDhtFaultActive) {
444	        g_localDhtFaultActive = false;
445	        Serial.println(F("[系统] ✅ DHT11 传感器恢复"));
446	    }
447	}
448
449	// ============================================================
450	// 调试输出
451	// ============================================================
452
453	void printDebug(const TelemetryData& t) {
454	    static uint32_t lastPrint = 0;
455	    if (millis() - lastPrint < 5000) return;
456	    lastPrint = millis();
457
458	    Serial.println(F("─── 遥测数据 ───"));
459	    Serial.printf("  环境: T=%.1f°C H=%.1f%% (温湿度:%d)\n",
460	                  t.temp, t.humi, t.dhtValid);
461	    Serial.printf("  存在:%d 运动:%d 跌倒:%d 驻留:%d (雷达:%d)\n",
462	                  t.presence, t.motion, t.fall, t.resident, t.radarValid);
463	    Serial.printf("  心率:%d 呼吸:%d (体征:%d)\n",
464	                  t.heartRate, t.breathRate, t.vitalValid);
465	    Serial.printf("  水浸:%d (水浸有效:%d) | WiFi:%d 蜂鸣器:%d\n",
466	                  t.rainDO, t.rainValid, t.wifiReady, t.buzzerState);
467	    Serial.printf("  运行:%ds 序号:%d\n", t.uptimeSec, g_sequence);
468	    Serial.println(F("─────────────────"));
469	}
470
471	// ============================================================
472	// 雷达任务 - 核心0 专用
473	// ============================================================
474
475	void radarTask(void* param) {
476	    Serial.println(F("[任务] 📡 雷达任务启动 (核心0)"));
477	    TickType_t lastWake = xTaskGetTickCount();
478	    const TickType_t freq = pdMS_TO_TICKS(10);
479
480	    while (true) {
481	        g_radar.loop();
482	        vTaskDelayUntil(&lastWake, freq);
483	    }
484	}
485
486	// ============================================================
487	// Setup
488	// ============================================================
489
490	void setup() {
491	    Serial.begin(115200);
492	    delay(500);
493
494	    Serial.println(F("\n"));
495	    Serial.println(F("╔══════════════════════════════════════════╗"));
496	    Serial.println(F("║  🏥 五劳无感康养监测系统 v1.0.0       ║"));
497	    Serial.println(F("║  ESP32-S3 + R60ABD1 + DHT11 + 水浸     ║"));
498	    Serial.println(F("║  MQTT 协议 V0.08                        ║"));
499	    Serial.println(F("╚══════════════════════════════════════════╝"));
500
501	    g_bootTime = millis();
502
503	    // 1. 报警模块 (最先, 引脚安全)
504	    g_alarm.begin();
505
506	    // 2. 雷达
507	    g_radar.begin();
508
509	    // 3. OLED 显示
510	    g_display.begin();
511
512	    // 4. 按钮
513	    g_buttons.begin();
514	    g_buttons.onPageSwitch([]() {
515	        g_display.nextPage();
516	    });
517	    g_buttons.onAlarmDismiss([]() {
518	        g_alarm.clearAllLocal();
519	        // 同步清除所有本地状态标记
520	        g_localFallActive       = false;
521	        g_localWaterActive      = false;
522	        g_localHeartAbnActive   = false;
523	        g_localBreathAbnActive  = false;
524	        g_localTempAbnActive    = false;
525	        g_localHumiAbnActive    = false;
526	        g_localRadarFaultActive = false;
527	        g_localDhtFaultActive   = false;
528	        g_tempAbnCnt            = 0;
529	        g_humiAbnCnt            = 0;
530	        g_radarTimeoutCnt       = 0;
531	        g_fallClearMs           = 0;
532	        g_fallDismissed         = true;   // 抑制重复报警, 等跌倒信号消了再恢复
533	    });
534	    g_buttons.onProvisionLongPress([]() {
535	        g_wifi.startConfigPortal();
536	    });
537
538	    // 5. WiFi (WiFiManager 配网)
539	    g_wifi.begin();
540
541	    // 6. 时间同步
542	    if (g_wifi.isConnected()) {
543	        syncTime();
544	    }
545
546	    // 7. MQTT
547	    DeviceConfig cfg = g_wifi.getConfig();
548	    g_mqtt.setServer(cfg.mqtt_server, atoi(cfg.mqtt_port),
549	                     cfg.mqtt_user, cfg.mqtt_pass);
550	    g_mqtt.begin(cfg.device_id);
551
552	    // 注册 MQTT 回调
553	    g_mqtt.onAlarmCommand(handleAlarmCommand);
554	    g_mqtt.onControlCommand(handleControlCommand);
555	    g_mqtt.onProvisionCommand(handleProvisionCommand);
556
557	    // 注册本地报警回调 → MQTT 上报
558	    g_alarm.onAlarmChange(onLocalAlarmChange);
559
560	    // 连接 MQTT
561	    if (g_wifi.isConnected()) {
562	        g_mqtt.connect();
563	    }
564
565	    // 8. 传感器
566	    g_sensor.begin();
567
568	    // 8. 等雷达 + 电源稳定
569	    delay(8000);
570
571	    // 9. 雷达任务 (核心0)
572	    xTaskCreatePinnedToCore(
573	        radarTask, "RadarTask",
574	        RADAR_TASK_STACK, nullptr,
575	        RADAR_TASK_PRIORITY, nullptr,
576	        RADAR_TASK_CORE
577	    );
578
579	    Serial.println(F("[系统] ✅ 系统就绪"));
580	    Serial.println(F("══════════════════════════════════════════\n"));
581	}
582
583	// ============================================================
584	// 主循环 (核心1)
585	// ============================================================
586
587	void loop() {
588	    uint32_t now = millis();
589
590	    // --- 0. 按钮轮询 ---
591	    g_buttons.loop();
592
593	    // --- 1. WiFi ---
594	    bool wifiOk = g_wifi.loop();
595
596	    // --- 2. MQTT (含自动重连) ---
597	    if (wifiOk) {
598	        g_mqtt.loop();
599
600	        // WiFi 刚连上但 MQTT 还没连
601	        static bool wasDisconnected = true;
602	        if (g_mqtt.isConnected()) {
603	            if (wasDisconnected) {
604	                // 刚恢复连接, 同步时间
605	                syncTime();
606	                wasDisconnected = false;
607	            }
608	            // 配网成功后更新 MQTT 配置 (如果有改动) 并上报
609	            if (g_wifi.justProvisioned()) {
610	                DeviceConfig cfg = g_wifi.getConfig();
611	                g_mqtt.setServer(cfg.mqtt_server, atoi(cfg.mqtt_port),
612	                                 cfg.mqtt_user, cfg.mqtt_pass);
613	                g_mqtt.publishProvisionStatus("success", "");
614	            }
615	        } else {
616	            wasDisconnected = true;
617	        }
618	    }
619
620	    // --- 3. 传感器 (独立，任何传感器故障不影响其他) ---
621	    g_sensor.read();
622
623	    // --- 4. 本地报警检查 (离线也工作) ---
624	    RadarRawData radar = g_radar.getData();
625	    checkLocalAlarms(radar);
626
627	    // --- 传感器状态日志 (仅记录, 不报警) ---
628	    static uint32_t lastFaultLog = 0;
629	    if (now - lastFaultLog > 60000) {
630	        lastFaultLog = now;
631	        if (!g_sensor.isDhtValid() && millis() > 60000) {
632	            Serial.println(F("[系统] ⚠ DHT11 读数失效"));
633	        }
634	        if (g_radar.isTimeout()) {
635	            Serial.println(F("[系统] ⚠ 雷达数据超时"));
636	        }
637	    }
638
639	    // --- 5. 报警模块更新 (定时器/闪烁) ---
640	    g_alarm.loop();
641
642	    // --- 6. 遥测上报 ---
643	    //     有人时: 每秒全量上报
644	    //     无人时: 仅状态变化时发极简报文 + 60秒心跳 (省4G流量)
645	    static bool lastPresence = false;
646	    static uint32_t lastHeartbeat = 0;  // 无人心跳计时
647
648	    if (radar.presence) {
649	        // ── 有人: 每秒完整上报 ──
650	        if (now - g_lastReportTime >= REPORT_INTERVAL_MS) {
651	            g_lastReportTime = now;
652	            TelemetryData tele = buildTelemetry(radar);
653	            g_mqtt.publishAllPoints(tele, g_sequence);
654	            g_sequence++;
655	            lastHeartbeat = now;  // 有人时重置心跳计时
656
657	            printDebug(tele);
658	            g_display.update(tele, wifiOk, g_alarm.isBuzzerOn(),
659	                             g_alarm.hasActiveAlarm(), g_alarm.getActiveAlarmStr());
660	        }
661	    } else {
662	        // ── 无人: 状态翻转发一次极简报文, 之后每60秒心跳 ──
663	        if (lastPresence != radar.presence || now - lastHeartbeat >= PRESENCE_HEARTBEAT_MS) {
664	            g_mqtt.publishPresenceOnly(radar.presence, radar.radarValid, g_sequence);
665	            g_sequence++;
666	            lastHeartbeat = now;
667
668	            // 调试输出
669	            Serial.printf("[上报] 无人极简报文 (Presence=0) 序号:%d\n", g_sequence - 1);
670	        }
671
672	        // OLED 还是照常刷新 (显示无人状态)
673	        TelemetryData tele = buildTelemetry(radar);
674	        g_display.update(tele, wifiOk, g_alarm.isBuzzerOn(),
675	                         g_alarm.hasActiveAlarm(), g_alarm.getActiveAlarmStr());
676	    }
677	    lastPresence = radar.presence;
678
679	    // --- 7. MQTT 配网状态 (WiFiManager 触发时) ---
680	    // WiFiManager 的 captive portal 启动/关闭时更新状态
681	    // 此逻辑已集成在 wifi_manager 中
682
683	    delay(10);
684	}
685

/**
 * @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;
bool    g_localTempAbnActive    = false;
bool    g_localHumiAbnActive    = false;
bool    g_localRadarFaultActive = false;
bool    g_localDhtFaultActive   = false;

// 报警防抖计数器 (连续N次确认才触发)
uint8_t g_tempAbnCnt       = 0;
uint8_t g_humiAbnCnt       = 0;
uint8_t g_radarTimeoutCnt  = 0;
uint8_t g_heartAbnCnt      = 0;      // 心率异常防抖
uint8_t g_breathAbnCnt     = 0;      // 呼吸异常防抖
uint32_t g_fallDetectMs     = 0;      // 跌倒检测计时 (ms)
uint32_t g_fallClearMs      = 0;      // 跌倒清除计时 (ms)
bool     g_fallDismissed     = false;  // 已手动解除, 不重复报警
#define ALARM_HYSTERESIS      3       // 连续3次确认才触发
#define FALL_DETECT_MS        2000    // 跌倒持续2秒才触发 (房顶安装, 过滤坐下)
#define FALL_CLEAR_MS         5000    // 跌倒消失5秒后才清除 (防误消)

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

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

        // 清除全部本地告警状态 (远程恢复=手动解除)
        g_localFallActive       = false;
        g_localWaterActive      = false;
        g_localHeartAbnActive   = false;
        g_localBreathAbnActive  = false;
        g_localTempAbnActive    = false;
        g_localHumiAbnActive    = false;
        g_localRadarFaultActive = false;
        g_localDhtFaultActive   = false;

        // 防抖计数器也复位
        g_tempAbnCnt      = 0;
        g_humiAbnCnt      = 0;
        g_radarTimeoutCnt = 0;
        g_heartAbnCnt     = 0;
        g_breathAbnCnt    = 0;
        g_fallDetectMs     = 0;
        g_fallClearMs      = 0;
        g_fallDismissed    = 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();
    } 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.gateid,    g_mqtt.getGateId(), sizeof(t.gateid) - 1);
    strncpy(t.meterName, METER_NAME_DEFAULT, sizeof(t.meterName) - 1);

    // 环境类型: 从配网配置读取 (默认 bedroom)
    DeviceConfig cfg = g_wifi.getConfig();
    strncpy(t.deviceType, cfg.env_type[0] ? cfg.env_type : "bedroom", sizeof(t.deviceType)-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.sleepState = radar.sleepState;
    t.sleepScore = radar.sleepScore;
    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;
}

// ============================================================
// 本地报警回调 → MQTT 上报 (连接时)
// ============================================================

void onLocalAlarmChange(uint8_t type, bool triggered, const char* desc) {
    if (g_mqtt.isConnected()) {
        g_mqtt.publishLocalAlarm(type, triggered, desc);
    }
}

// ============================================================
// 本地报警检查 (离线可用) — 人身安全 > 环境 > 设备
// ============================================================

void checkLocalAlarms(const RadarRawData& radar) {
    // 环境类型 → 报警开关
    const char* env = g_wifi.getConfig().env_type;
    bool enFall  = (strcmp(env, "bathroom") == 0 || strcmp(env, "hallway") == 0 || strcmp(env, "livingroom") == 0);
    bool enVital = (strcmp(env, "bedroom")  == 0);
    bool enWater = (strcmp(env, "bathroom") == 0);
    bool enEnv   = (strcmp(env, "bedroom")  == 0 || strcmp(env, "balcony") == 0 || strcmp(env, "kitchen") == 0);

    // ─── 前置条件: 有人才检测人体生命体征 ───
    bool humanPresent = radar.presence && radar.radarValid;

    // ─── ★★★★★ 人身安全: 跌倒 (仅浴室/走廊/客厅) ───
    if (enFall && radar.fall && humanPresent && !g_fallDismissed && millis() > 30000) {
        if (!g_localFallActive) {
            // 持续检测中: 必须满 FALL_DETECT_MS 才报警
            if (g_fallDetectMs == 0) {
                g_fallDetectMs = millis();
            } else if (millis() - g_fallDetectMs >= FALL_DETECT_MS) {
                g_localFallActive = true;
                g_fallDetectMs = 0;
                g_fallClearMs = 0;
                g_alarm.triggerLocal(AlarmManager::ALARM_FALL, "雷达检测到跌倒");
            }
        } else {
            g_fallClearMs = 0;  // 还在跌倒, 重置清除计时
        }
    } else {
        g_fallDetectMs = 0;
        // 解除抑制需要 fall 持续消失 FALL_CLEAR_MS (防 AFD1 频繁跳变)
        if (g_fallDismissed && g_fallClearMs == 0) {
            g_fallClearMs = millis();
        }
        if (!g_fallDismissed || millis() - g_fallClearMs >= FALL_CLEAR_MS) {
            g_fallDismissed = false;
            g_fallClearMs = 0;
        }
        if (g_localFallActive) {
            // 跌倒已消失 → 延迟清除
            if (g_fallClearMs == 0) {
                g_fallClearMs = millis();
            } else if (millis() - g_fallClearMs >= FALL_CLEAR_MS) {
                g_localFallActive = false;
                g_fallClearMs = 0;
                g_alarm.clearLocal(AlarmManager::ALARM_FALL);
            }
        }
    }

    // ─── ★★★★★ 人身安全: 心率异常 (仅卧室+ABD1, 防抖3次, 30s) ───
#if RADAR_TYPE != RADAR_TYPE_AFD1
    if (enVital && millis() > 30000 && humanPresent && radar.vitalValid && radar.heartRate > 0) {
        bool heartAbn = (radar.heartRate < HEART_RATE_MIN || radar.heartRate > HEART_RATE_MAX);
        if (heartAbn) {
            if (++g_heartAbnCnt >= ALARM_HYSTERESIS && !g_localHeartAbnActive) {
                g_localHeartAbnActive = true;
                char buf[32];
                snprintf(buf, sizeof(buf), "心率异常: %d bpm", radar.heartRate);
                g_alarm.triggerLocal(AlarmManager::ALARM_HEART_ABNORMAL, buf);
            }
        } else {
            g_heartAbnCnt = 0;
            if (g_localHeartAbnActive) {
                g_localHeartAbnActive = false;
                g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
            }
        }
    } else if (!humanPresent && g_localHeartAbnActive) {
        g_localHeartAbnActive = false;
        g_heartAbnCnt = 0;
        g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
    }
#endif  // RADAR_TYPE != AFD1

    // ─── ★★★★★ 人身安全: 呼吸异常 (仅卧室+ABD1, 防抖3次, 30秒后生效) ───
#if RADAR_TYPE != RADAR_TYPE_AFD1
    if (enVital && millis() > 30000 && humanPresent && radar.vitalValid && radar.breathRate > 0) {
        bool breathAbn = (radar.breathRate < BREATH_RATE_MIN || radar.breathRate > BREATH_RATE_MAX);
        if (breathAbn) {
            if (++g_breathAbnCnt >= ALARM_HYSTERESIS && !g_localBreathAbnActive) {
                g_localBreathAbnActive = true;
                char buf[32];
                snprintf(buf, sizeof(buf), "呼吸异常: %d 次/分", radar.breathRate);
                g_alarm.triggerLocal(AlarmManager::ALARM_BREATH_ABNORMAL, buf);
            }
        } else {
            g_breathAbnCnt = 0;
            if (g_localBreathAbnActive) {
                g_localBreathAbnActive = false;
                g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
            }
        }
    } else if (!humanPresent && g_localBreathAbnActive) {
        g_localBreathAbnActive = false;
        g_breathAbnCnt = 0;
        g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
    }
#endif  // RADAR_TYPE != AFD1

    // ─── ★★★ 环境安全: 水浸 (仅浴室) ───
    if (enWater) {
    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);
    }
    }  // enWater

    // ─── ★★ 环境舒适: 温湿度异常 (仅卧室/阳台/厨房, 防抖3次, 30s) ───
    if (enEnv && millis() > 30000 && g_sensor.isDhtValid()) {
        float temp = g_sensor.getTemp();
        bool tempAbn = (temp < TEMP_LOW || temp > TEMP_HIGH);
        if (tempAbn) {
            if (++g_tempAbnCnt >= ALARM_HYSTERESIS && !g_localTempAbnActive) {
                g_localTempAbnActive = true;
                char buf[32];
                snprintf(buf, sizeof(buf), "温度%.1f°C 超出范围", temp);
                g_alarm.triggerLocal(AlarmManager::ALARM_TEMP_ABNORMAL, buf);
            }
        } else {
            g_tempAbnCnt = 0;
            if (g_localTempAbnActive) {
                g_localTempAbnActive = false;
                g_alarm.clearLocal(AlarmManager::ALARM_TEMP_ABNORMAL);
            }
        }

        // ─── ★★ 环境舒适: 湿度异常 (防抖3次) ───
        float humi = g_sensor.getHumi();
        bool humiAbn = (humi < HUMI_LOW || humi > HUMI_HIGH);
        if (humiAbn) {
            if (++g_humiAbnCnt >= ALARM_HYSTERESIS && !g_localHumiAbnActive) {
                g_localHumiAbnActive = true;
                char buf[32];
                snprintf(buf, sizeof(buf), "湿度%.1f%% 超出范围", humi);
                g_alarm.triggerLocal(AlarmManager::ALARM_HUMI_ABNORMAL, buf);
            }
        } else {
            g_humiAbnCnt = 0;
            if (g_localHumiAbnActive) {
                g_localHumiAbnActive = false;
                g_alarm.clearLocal(AlarmManager::ALARM_HUMI_ABNORMAL);
            }
        }
    }

    // ─── 设备：雷达超时 (连续5秒无数据, 仅标记, 不报警) ───
    if (g_radar.isTimeout()) {
        if (++g_radarTimeoutCnt >= 5 && !g_localRadarFaultActive) {
            g_localRadarFaultActive = true;
            Serial.println(F("[系统] ⚠ 雷达超时无数据"));
        }
    } else {
        g_radarTimeoutCnt = 0;
        if (g_localRadarFaultActive) {
            g_localRadarFaultActive = false;
            Serial.println(F("[系统] ✅ 雷达数据恢复"));
        }
    }

    // ─── 设备：DHT 持续故障 (仅标记, 不报警) ───
    if (g_sensor.isDhtFault() && !g_localDhtFaultActive && millis() > 60000) {
        g_localDhtFaultActive = true;
        Serial.println(F("[系统] ⚠ DHT11 温湿度传感器故障"));
    } else if (!g_sensor.isDhtFault() && g_localDhtFaultActive) {
        g_localDhtFaultActive = false;
        Serial.println(F("[系统] ✅ DHT11 传感器恢复"));
    }
}

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

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_localFallActive       = false;
        g_localWaterActive      = false;
        g_localHeartAbnActive   = false;
        g_localBreathAbnActive  = false;
        g_localTempAbnActive    = false;
        g_localHumiAbnActive    = false;
        g_localRadarFaultActive = false;
        g_localDhtFaultActive   = false;
        g_tempAbnCnt            = 0;
        g_humiAbnCnt            = 0;
        g_radarTimeoutCnt       = 0;
        g_fallClearMs           = 0;
        g_fallDismissed         = true;   // 抑制重复报警, 等跌倒信号消了再恢复
    });
    g_buttons.onProvisionLongPress([]() {
        g_wifi.startConfigPortal();
    });

    // 5. WiFi (WiFiManager 配网)
    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 上报
    g_alarm.onAlarmChange(onLocalAlarmChange);

    // 连接 MQTT
    if (g_wifi.isConnected()) {
        g_mqtt.connect();
    }

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

    // 8. 等雷达 + 电源稳定
    delay(8000);

    // 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();
                wasDisconnected = false;
            }
            // 配网成功后更新 MQTT 配置 (如果有改动) 并上报
            if (g_wifi.justProvisioned()) {
                DeviceConfig cfg = g_wifi.getConfig();
                g_mqtt.setServer(cfg.mqtt_server, atoi(cfg.mqtt_port),
                                 cfg.mqtt_user, cfg.mqtt_pass);
                g_mqtt.publishProvisionStatus("success", "");
            }
        } else {
            wasDisconnected = true;
        }
    }

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

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

    // --- 传感器状态日志 (仅记录, 不报警) ---
    static uint32_t lastFaultLog = 0;
    if (now - lastFaultLog > 60000) {
        lastFaultLog = now;
        if (!g_sensor.isDhtValid() && millis() > 60000) {
            Serial.println(F("[系统] ⚠ DHT11 读数失效"));
        }
        if (g_radar.isTimeout()) {
            Serial.println(F("[系统] ⚠ 雷达数据超时"));
        }
    }

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

    // --- 6. 遥测上报 ---
    //     有人时: 每秒全量上报
    //     无人时: 仅状态变化时发极简报文 + 60秒心跳 (省4G流量)
    static bool lastPresence = false;
    static uint32_t lastHeartbeat = 0;  // 无人心跳计时

    if (radar.presence) {
        // ── 有人: 每秒完整上报 ──
        if (now - g_lastReportTime >= REPORT_INTERVAL_MS) {
            g_lastReportTime = now;
            TelemetryData tele = buildTelemetry(radar);
            g_mqtt.publishAllPoints(tele, g_sequence);
            g_sequence++;
            lastHeartbeat = now;  // 有人时重置心跳计时

            printDebug(tele);
            g_display.update(tele, wifiOk, g_alarm.isBuzzerOn(),
                             g_alarm.hasActiveAlarm(), g_alarm.getActiveAlarmStr());
        }
    } else {
        // ── 无人: 状态翻转发一次极简报文, 之后每60秒心跳 ──
        if (lastPresence != radar.presence || now - lastHeartbeat >= PRESENCE_HEARTBEAT_MS) {
            g_mqtt.publishPresenceOnly(radar.presence, radar.radarValid, g_sequence);
            g_sequence++;
            lastHeartbeat = now;

            // 调试输出
            Serial.printf("[上报] 无人极简报文 (Presence=0) 序号:%d\n", g_sequence - 1);
        }

        // OLED 还是照常刷新 (显示无人状态)
        TelemetryData tele = buildTelemetry(radar);
        g_display.update(tele, wifiOk, g_alarm.isBuzzerOn(),
                         g_alarm.hasActiveAlarm(), g_alarm.getActiveAlarmStr());
    }
    lastPresence = radar.presence;

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

    delay(10);
}