<ide_opened_file>The user opened the file d:\PlatformIO1\Projects\Healthv2.0\src\button_manager.cpp 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 simply saying "你好" (Hello in Chinese). This is just a greeting. I should respond in kind and be welcoming.


你好！👋

我是 Claude，在 VS Code 扩展环境中运行。我可以帮你处理软件工程任务——编写代码、调试问题、重构项目、搜索代码库等。

我看到你当前打开了 `button_manager.cpp`，这是一个 PlatformIO 的嵌入式项目（Healthv2.0）。有什么我可以帮你的吗？


你还记得上回说的吗


The user is asking if I remember what we talked about last time. Let me check if there are any memory files or context from previous sessions that might help.


让我查看一下有没有之前的记忆记录。


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


The MEMORY.md file doesn't exist yet. Let me check if there are any memory files in the directory.


No files found


No memory files exist for this project. I don't have any record of previous conversations about this project. I should let the user know honestly.


抱歉，我没有找到关于这个项目的任何之前的记忆记录——这是我第一次在这个会话中与你交流，该项目的 memory 目录也是空的。

不过我可以看到你当前项目的概况：
- 这是一个 **PlatformIO 嵌入式项目**（Healthv2.0），基于 ESP 平台
- 涉及的功能模块包括：WiFi 管理、MQTT 通信、雷达传感器、报警管理、按钮管理等
- 当前分支是 `main`，有一些未提交的修改

你能提醒我一下上次我们讨论了什么吗？或者你遇到了什么问题？我会尽力帮你继续。


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

现在是，蓝牙配网成功后。蓝牙不直接发送报文，重启后才行


The user is describing a bug: after Bluetooth provisioning (配网) succeeds, the Bluetooth doesn't send messages immediately. It only works after a restart. Let me look at the relevant code to understand the issue.

Let me first check the project structure and then look at the Bluetooth/provisioning related code.


明白了，蓝牙配网成功后需要重启才能正常工作。让我先看看相关代码来定位问题。


src\sensor_manager.cpp
src\display_manager.cpp
src\alarm_manager.cpp
src\button_manager.cpp
src\radar_manager.cpp
src\mqtt_manager.cpp
src\wifi_manager.cpp
src\main.cpp


include\sensor_manager.h
include\button_manager.h
include\alarm_manager.h
include\display_manager.h
include\radar_manager.h
include\mqtt_manager.h
include\radar_protocol.h
include\config.h
include\wifi_manager.h


Let me read the main files to understand the Bluetooth provisioning flow and the WiFi manager.


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	                syncTime();
605	                wasDisconnected = false;
606	            }
607	        } else {
608	            wasDisconnected = true;
609	        }
610
611	        // 配网成功后: 先更新 MQTT 配置, 再强制重连
612	        if (g_wifi.justProvisioned()) {
613	            DeviceConfig cfg = g_wifi.getConfig();
614	            g_mqtt.disconnect();
615	            g_mqtt.setServer(cfg.mqtt_server, atoi(cfg.mqtt_port),
616	                             cfg.mqtt_user, cfg.mqtt_pass);
617	            g_mqtt.connect();
618	            g_mqtt.publishProvisionStatus("success", "");
619	        }
620	    }
621
622	    // --- 3. 传感器 (独立，任何传感器故障不影响其他) ---
623	    g_sensor.read();
624
625	    // --- 4. 本地报警检查 (离线也工作) ---
626	    RadarRawData radar = g_radar.getData();
627	    checkLocalAlarms(radar);
628
629	    // --- 传感器状态日志 (仅记录, 不报警) ---
630	    static uint32_t lastFaultLog = 0;
631	    if (now - lastFaultLog > 60000) {
632	        lastFaultLog = now;
633	        if (!g_sensor.isDhtValid() && millis() > 60000) {
634	            Serial.println(F("[系统] ⚠ DHT11 读数失效"));
635	        }
636	        if (g_radar.isTimeout()) {
637	            Serial.println(F("[系统] ⚠ 雷达数据超时"));
638	        }
639	    }
640
641	    // --- 5. 报警模块更新 (定时器/闪烁) ---
642	    g_alarm.loop();
643
644	    // --- 6. 遥测上报 ---
645	    //     有人时: 每秒全量上报
646	    //     无人时: 仅状态变化时发极简报文 + 60秒心跳 (省4G流量)
647	    static bool lastPresence = false;
648	    static uint32_t lastHeartbeat = 0;  // 无人心跳计时
649
650	    if (radar.presence) {
651	        // ── 有人: 每秒完整上报 ──
652	        if (now - g_lastReportTime >= REPORT_INTERVAL_MS) {
653	            g_lastReportTime = now;
654	            TelemetryData tele = buildTelemetry(radar);
655	            g_mqtt.publishAllPoints(tele, g_sequence);
656	            g_sequence++;
657	            lastHeartbeat = now;  // 有人时重置心跳计时
658
659	            printDebug(tele);
660	            g_display.update(tele, wifiOk, g_alarm.isBuzzerOn(),
661	                             g_alarm.hasActiveAlarm(), g_alarm.getActiveAlarmStr());
662	        }
663	    } else {
664	        // ── 无人: 状态翻转发一次极简报文, 之后每60秒心跳 ──
665	        if (lastPresence != radar.presence || now - lastHeartbeat >= PRESENCE_HEARTBEAT_MS) {
666	            g_mqtt.publishPresenceOnly(radar.presence, radar.radarValid, g_sequence);
667	            g_sequence++;
668	            lastHeartbeat = now;
669
670	            // 调试输出
671	            Serial.printf("[上报] 无人极简报文 (Presence=0) 序号:%d\n", g_sequence - 1);
672	        }
673
674	        // OLED 还是照常刷新 (显示无人状态)
675	        TelemetryData tele = buildTelemetry(radar);
676	        g_display.update(tele, wifiOk, g_alarm.isBuzzerOn(),
677	                         g_alarm.hasActiveAlarm(), g_alarm.getActiveAlarmStr());
678	    }
679	    lastPresence = radar.presence;
680
681	    // --- 7. MQTT 配网状态 (WiFiManager 触发时) ---
682	    // WiFiManager 的 captive portal 启动/关闭时更新状态
683	    // 此逻辑已集成在 wifi_manager 中
684
685	    delay(10);
686	}
687

/**
 * @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;
            }
        } else {
            wasDisconnected = true;
        }

        // 配网成功后: 先更新 MQTT 配置, 再强制重连
        if (g_wifi.justProvisioned()) {
            DeviceConfig cfg = g_wifi.getConfig();
            g_mqtt.disconnect();
            g_mqtt.setServer(cfg.mqtt_server, atoi(cfg.mqtt_port),
                             cfg.mqtt_user, cfg.mqtt_pass);
            g_mqtt.connect();
            g_mqtt.publishProvisionStatus("success", "");
        }
    }

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