<ide_opened_file>The user opened the file d:\PlatformIO1\Projects\Healthv2.0\include\config.h 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".
- 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
- review: Review a GitHub pull request; for your working diff use /code-review
- security-review: Complete a security review of the pending changes on the current branch

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


The user is asking if I have memory — essentially asking whether I can remember things from previous conversations. Let me check what memories exist.


是的，我有记忆功能。让我查看一下当前的记忆文件。


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 the memory directory exists and if there are any memory files.


No files found


The memory directory is empty — no memories have been saved yet. This is the first session for this project.

Let me also quickly look at what this project is about so I can offer to create some initial memories.


目前还没有保存任何记忆。这是我在这个项目（Healthv2.0）中的第一次会话，所以记忆目录是空的。

如果你希望我记住某些事情，比如：

- **关于你的信息**（角色、偏好、技术栈偏好）
- **项目上下文**（架构决策、设计约束、正在进行的任务）
- **反馈/偏好**（你喜欢的代码风格、命名规范等）

直接告诉我"记住 XXX"就行，我会把它保存下来，下次会话时自动加载。

要我先看看项目代码，了解一下这个 Healthv2.0 项目是做什么的吗？


Connection error.

Unable to connect to API (UNKNOWN_CERTIFICATE_VERIFICATION_ERROR)

unknown certificate verification error


现在该系统，按钮老是卡住，


The user is saying that the system's buttons keep getting stuck/frozen. Let me first look at the project structure and the opened file (config.h) to understand what this project is about.


让我先看看项目结构和当前的代码，了解一下问题所在。


1	/**
2	 * @file config.h
3	 * @brief 全局配置文件 - 五劳无感康养监测系统
4	 *
5	 * 协议版本: V0.08 (2026-07-17)
6	 * 包含: 硬件引脚定义、WiFi/MQTT 默认参数、
7	 *        雷达协议常量、告警阈值、任务配置等
8	 */
9
10	#ifndef CONFIG_H
11	#define CONFIG_H
12
13	#include <Arduino.h>
14
15	// ============================================================
16	// 1. 硬件引脚定义 (ESP32-S3)
17	// ============================================================
18
19	// --- R60ABD1 毫米波雷达 ---
20	#define RADAR_UART_PORT       Serial2
21	#define RADAR_RX_PIN          17
22	#define RADAR_TX_PIN          1             // UART 发送 (从18挪到1)
23	#define RADAR_BAUD_RATE       115200
24
25	// --- 雷达备用 GPIO ---
26	#define RADAR_GPIO1_PIN       2             // 活跃/静止指示 (从10挪到2)
27	#define RADAR_GPIO2_PIN       11            // 有人/无人指示
28
29	// --- DHT11 温湿度传感器 ---
30	#define DHT11_PIN             4             // 温湿度传感器
31
32	// --- 水浸传感器 ---
33	#define WATER_SENSOR_PIN      45            // 数字输入, 低电平=有水
34
35	// --- 蜂鸣器 (PWM) ---
36	#define BUZZER_PIN            21
37	#define BUZZER_PWM_CHANNEL    0
38	#define BUZZER_PWM_FREQ       2000
39	#define BUZZER_PWM_RESOLUTION 8
40
41	// --- 配网按钮 ---
42	#define CONFIG_BUTTON_PIN     0             // 长按 3 秒复位 WiFi
43	#define BUTTON_LONG_PRESS_MS  3000
44
45	// --- 状态指示灯 ---
46	#define LED_WIFI_PIN          8             // 蓝色 - WiFi 状态
47	#define LED_ALARM_PIN         9             // 红色 - 报警指示
48
49	// --- OLED 显示屏 (SSD1306 128x64 I2C) ---
50	#define OLED_SDA_PIN          15            // I2C 数据线
51	#define OLED_SCL_PIN          16            // I2C 时钟线
52
53	// --- 交互按钮 ---
54	#define BTN_PROVISION_PIN     14            // 擦除WiFi配网按钮 (长按3秒)
55	#define BTN_ALARM_DISMISS_PIN 13            // 解除报警按钮
56	#define BTN_PAGE_SWITCH_PIN   46            // 翻页按钮
57	#define BTN_DEBOUNCE_MS       50            // 去抖时间
58	#define BTN_LONG_PRESS_MS     1500          // 长按判定
59
60	// ============================================================
61	// 2. 设备标识 (协议规范 §2)
62	// ============================================================
63
64	#define SALEID_DEFAULT        "10100001"    // 站点/系统编号
65	#define GATEID_DEFAULT        "10100001001" // 网关唯一编号
66	#define METER_ID_DEFAULT      "ENV001"      // 感知节点ID (卧室=ENV001, 卫生间=ENV002, 客厅=ENV003)
67	#define METER_NAME_DEFAULT    "RadarDHT11"  // 设备型号名称
68	#define FW_VERSION            "V1.0.0"
69
70	// ============================================================
71	// 3. WiFi 默认配置 (配网使用)
72	// ============================================================
73
74	#define WIFI_AP_SSID          "HealthMonitor-Setup"
75	#define WIFI_AP_PASSWORD      "12345678"
76	#define WIFI_CONFIG_TIMEOUT   300
77	#define WIFI_CONNECT_TIMEOUT  15
78	#define WIFI_RETRY_INTERVAL   10000
79
80	// ============================================================
81	// 4. MQTT 默认配置 (协议规范 §2)
82	// ============================================================
83
84	#define MQTT_DEFAULT_SERVER   "202.110.234.102"
85	#define MQTT_DEFAULT_PORT     8000
86	#define MQTT_DEFAULT_PORT_STR "8000"         // WiFiManager 参数用字符串
87	#define MQTT_DEFAULT_USER     "test"
88	#define MQTT_DEFAULT_PASS     "testtest"
89	#define MQTT_DEFAULT_DEVICE   "10100001001"  // 默认网关ID = gateid
90	#define MQTT_DEFAULT_ENV      "主卧"           // 默认环境备注
91	#undef MQTT_KEEPALIVE
92	#define MQTT_KEEPALIVE        30              // 30秒 (协议规范)
93	#define MQTT_RECONNECT_MIN    1000
94	#define MQTT_RECONNECT_MAX    60000
95
96	// ============================================================
97	// 5. MQTT 主题定义 (协议规范 §3)
98	// ============================================================
99
100	// --- 上行主题 (设备 → 服务端) ---
101	#define MQTT_TOPIC_REPORT_ALLPOINTS   "report/allpoints"                // 全量遥测上报 QoS0
102	#define MQTT_TOPIC_STATUS_ONLINE      "status/%s/online"                // 上线通知 QoS1
103	#define MQTT_TOPIC_STATUS_OFFLINE     "status/%s/offline"               // 离线遗嘱 QoS1
104	#define MQTT_TOPIC_STATUS_PROVISION   "status/%s/provision"             // 配网状态 QoS1
105	#define MQTT_TOPIC_RESPONSE_CMD       "response/%s/cmd"                 // 指令响应 QoS1
106	#define MQTT_TOPIC_RESPONSE_PROVISION "response/%s/provision"           // 配网响应 QoS1
107
108	// --- 下行主题 (服务端 → 设备) ---
109	#define MQTT_TOPIC_CMD_ALARM          "cmd/%s/alarm"                    // 告警指令 QoS1
110	#define MQTT_TOPIC_CMD_ALARM_RESTORE  "cmd/%s/alarm/restore"            // 告警恢复 QoS1
111	#define MQTT_TOPIC_CMD_CONTROL        "cmd/%s/control"                  // 控制指令 QoS1
112	#define MQTT_TOPIC_CMD_PROVISION_WIFI "cmd/%s/provision/wifi"           // WiFi配网 QoS1
113
114	// ============================================================
115	// 6. 上报配置 (协议规范 §4.1, §7.2)
116	// ============================================================
117
118	#define REPORT_INTERVAL_MS    1000                // 遥测上报周期 1秒
119	#define REPORT_QOS            0                   // 遥测 QoS 0
120	#define STATUS_QOS            1                   // 状态/响应 QoS 1
121
122	// ============================================================
123	// 7. 报警阈值 (协议规范 §6, 服务端判定用)
124	// ============================================================
125
126	#define HEART_RATE_MIN        50
127	#define HEART_RATE_MAX        120
128	#define BREATH_RATE_MIN       8
129	#define BREATH_RATE_MAX       24
130	#define TEMP_LOW              5.0f
131	#define TEMP_HIGH             45.0f
132	#define HUMI_LOW              15.0f
133	#define HUMI_HIGH             90.0f
134	#define WATER_DEBOUNCE_MS     50
135
136	// ============================================================
137	// 8. 任务配置
138	// ============================================================
139
140	#define RADAR_TASK_STACK      4096
141	#define RADAR_TASK_PRIORITY   5
142	#define RADAR_TASK_CORE       0
143
144	#define MQTT_TASK_STACK       8192
145	#define MQTT_TASK_PRIORITY    4
146	#define MQTT_TASK_CORE        1
147
148	// ============================================================
149	// 9. NTP 时间同步
150	// ============================================================
151
152	#define NTP_SERVER            "ntp.aliyun.com"
153	#define NTP_GMT_OFFSET_SEC    28800             // UTC+8 (北京时间)
154	#define NTP_DAYLIGHT_OFFSET   0
155
156	// ============================================================
157	// 10. LittleFS 配置文件名
158	// ============================================================
159
160	#define CONFIG_FILE_MQTT      "/mqtt_config.json"
161	#define CONFIG_FILE_WIFI      "/wifi_config.json"
162	#define CONFIG_FILE_DEVICE    "/device_config.json"
163
164	// ============================================================
165	// 11. 结构体: 全量遥测数据 (协议规范 §4.1)
166	// ============================================================
167
168	struct TelemetryData {
169	    // --- 设备标识 ---
170	    char     saleid[12];          // 站点编号 "10100001"
171	    char     gateid[16];          // 网关编号 "10100001001"
172	    char     meterId[8];          // 节点ID "ENV001"
173	    char     meterName[16];       // 设备型号 "RadarDHT11"
174
175	    // --- 环境数据 (DHT11) ---
176	    float    temp;                // 温度 ℃
177	    float    humi;                // 湿度 %RH
178	    bool     dhtValid;            // 温湿度有效性
179
180	    // --- 雷达: 存在/运动 ---
181	    bool     presence;            // 有人=1 无人=0
182	    bool     motion;              // 有运动=1 无运动=0
183	    bool     fall;                // 跌倒检测 1=触发
184	    bool     resident;            // 驻留状态 1=驻留
185
186	    // --- 雷达: 生命体征 ---
187	    uint8_t  heartRate;           // 心率 bpm (无效=0)
188	    uint8_t  breathRate;          // 呼吸 次/分 (无效=0)
189	    uint8_t  body;                // 人体能量/体征值
190	    bool     radarValid;          // 雷达数据有效性
191	    bool     vitalValid;          // 心率呼吸有效性
192
193	    // --- 水浸 ---
194	    bool     rainDO;              // 1=正常 0=水浸触发
195	    bool     rainValid;           // 水浸传感器有效性
196
197	    // --- 设备状态 ---
198	    bool     wifiReady;           // WiFi/MQTT 连接状态
199	    uint32_t uptimeSec;           // 设备累计运行秒数
200	    bool     buzzerState;         // 蜂鸣器状态 1=开启
201
202	    // --- 时间戳 ---
203	    uint32_t lastUpdate;          // 最后更新 ms
204	};
205
206	// 结构体: 下行报警指令 (协议规范 §5.1, §5.2)
207	struct AlarmCommand {
208	    char     cmdId[32];           // 指令唯一ID
209	    char     type[20];            // alarm / alarm_restore
210	    char     alarmType[24];       // fall/water_leak/heart_abnormal/breath_abnormal
211	    char     alarmLevel[8];       // low/medium/high
212	    char     alarmDesc[64];       // 告警描述
213	    char     meterId[8];          // 触发节点ID
214	    char     relatedCmdId[32];    // 关联原始告警ID (恢复用)
215
216	    // 联动动作
217	    bool     buzzerOn;            // 蜂鸣器开关
218	    uint16_t buzzerDuration;      // 蜂鸣持续秒数 (0=持续到恢复)
219	    bool     nightLight;          // 夜灯
220
221	    uint32_t timestamp;
222	};
223
224	// 结构体: 下行控制指令 (协议规范 §5.3)
225	struct ControlCommand {
226	    char     cmdId[32];
227	    char     type[20];            // control
228	    char     controlType[24];     // buzzer_test/device_restart/report_interval_set/factory_reset
229	    uint32_t params_duration;     // 参数: 持续秒数
230	    uint32_t params_interval;     // 参数: 上报间隔
231	    uint32_t timestamp;
232	};
233
234	// 结构体: 下行配网指令 (协议规范 §5.4)
235	struct ProvisionCommand {
236	    char     cmdId[32];
237	    char     wifi_ssid[33];
238	    char     wifi_password[65];
239	    char     wifi_encrypt[8];     // OPEN/WPA2/WPA3
240	    char     mqtt_broker[64];
241	    uint16_t mqtt_port;
242	    bool     mqtt_useTls;
243	    uint32_t timestamp;
244	};
245
246	#endif // CONFIG_H
247

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

#ifndef CONFIG_H
#define CONFIG_H

#include <Arduino.h>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    uint32_t timestamp;
};

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

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

#endif // CONFIG_H


1	; PlatformIO Project Configuration File
2	; ESP32-S3 + R60ABD1 60G 毫米波雷达 健康监测系统
3
4	[env:esp32-s3-devkitc-1]
5	platform = espressif32
6	board = esp32-s3-devkitc-1
7	framework = arduino
8
9	; 串口监视器
10	monitor_speed = 115200
11	monitor_filters = direct
12
13	; Flash 配置
14	board_build.flash_mode = qio
15	board_build.filesystem = littlefs
16	board_build.partitions = default_8MB.csv
17	board_build.arduino.memory_type = qio_opi
18
19	; 编译选项 - 优化速度
20	build_flags =
21	    -Os
22	    -DCORE_DEBUG_LEVEL=0
23	    -DARDUINO_USB_CDC_ON_BOOT=1
24	    -DBOARD_HAS_PSRAM
25
26	; 库依赖
27	lib_deps =
28	    ; WiFi 配网 (自带 captive portal)
29	    tzapu/WiFiManager@^2.0.17
30
31	    ; MQTT 客户端
32	    knolleary/PubSubClient@^2.8
33
34	    ; JSON 解析 (更省内存)
35	    bblanchon/ArduinoJson@^7.1.0
36
37	    ; DHT11 温湿度传感器
38	    adafruit/DHT sensor library@^1.4.6
39	    adafruit/Adafruit Unified Sensor@^1.1.14
40
41	    ; OLED 显示屏 (SSD1306 128x64 I2C)
42	    olikraus/U8g2@^2.35.30
43
44	; 上传配置
45	upload_speed = 921600
46	upload_port = COM16
47

; PlatformIO Project Configuration File
; ESP32-S3 + R60ABD1 60G 毫米波雷达 健康监测系统

[env:esp32-s3-devkitc-1]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino

; 串口监视器
monitor_speed = 115200
monitor_filters = direct

; Flash 配置
board_build.flash_mode = qio
board_build.filesystem = littlefs
board_build.partitions = default_8MB.csv
board_build.arduino.memory_type = qio_opi

; 编译选项 - 优化速度
build_flags =
    -Os
    -DCORE_DEBUG_LEVEL=0
    -DARDUINO_USB_CDC_ON_BOOT=1
    -DBOARD_HAS_PSRAM

; 库依赖
lib_deps =
    ; WiFi 配网 (自带 captive portal)
    tzapu/WiFiManager@^2.0.17

    ; MQTT 客户端
    knolleary/PubSubClient@^2.8

    ; JSON 解析 (更省内存)
    bblanchon/ArduinoJson@^7.1.0

    ; DHT11 温湿度传感器
    adafruit/DHT sensor library@^1.4.6
    adafruit/Adafruit Unified Sensor@^1.1.14

    ; OLED 显示屏 (SSD1306 128x64 I2C)
    olikraus/U8g2@^2.35.30

; 上传配置
upload_speed = 921600
upload_port = COM16


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


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


The user is saying that buttons keep getting stuck. Let me look at the button_manager.cpp and button_manager.h files since those are the most relevant. I should also check main.cpp to understand how buttons are used in the main loop.


让我看看按钮管理模块和主程序的实现。


1	/**
2	 * @file button_manager.h
3	 * @brief 三按钮管理器 - 去抖 + 长按/短按识别
4	 *
5	 * 按钮1 (GPIO14): 长按3秒 → 配网
6	 * 按钮2 (GPIO15): 短按 → 解除报警
7	 * 按钮3 (GPIO16): 短按 → 翻页
8	 */
9
10	#ifndef BUTTON_MANAGER_H
11	#define BUTTON_MANAGER_H
12
13	#include <Arduino.h>
14	#include "config.h"
15
16	typedef void (*ButtonCallback)(void);
17
18	class ButtonManager {
19	public:
20	    ButtonManager();
21
22	    /** 初始化三个按钮引脚 */
23	    void begin();
24
25	    /** 主循环轮询 (每 10ms 调用) */
26	    void loop();
27
28	    /** 注册回调 */
29	    void onProvisionLongPress(ButtonCallback cb)   { _cbProvision = cb; }
30	    void onAlarmDismiss(ButtonCallback cb)          { _cbDismiss  = cb; }
31	    void onPageSwitch(ButtonCallback cb)            { _cbPage     = cb; }
32
33	private:
34	    struct ButtonState {
35	        uint8_t  pin;
36	        bool     pressed;        // 当前是否在按下状态
37	        uint32_t pressTime;      // 按下时刻 (ms)
38	        bool     longTriggered;  // 防止长按时重复触发短按
39	    };
40
41	    /** 处理单个按钮的状态机 */
42	    void _processButton(ButtonState& btn, bool hasShort, bool hasLong);
43	    bool _readPin(uint8_t pin);
44
45	    ButtonState _btnProvision;
46	    ButtonState _btnDismiss;
47	    ButtonState _btnPage;
48
49	    ButtonCallback _cbProvision;
50	    ButtonCallback _cbDismiss;
51	    ButtonCallback _cbPage;
52	};
53
54	#endif // BUTTON_MANAGER_H
55

/**
 * @file button_manager.h
 * @brief 三按钮管理器 - 去抖 + 长按/短按识别
 *
 * 按钮1 (GPIO14): 长按3秒 → 配网
 * 按钮2 (GPIO15): 短按 → 解除报警
 * 按钮3 (GPIO16): 短按 → 翻页
 */

#ifndef BUTTON_MANAGER_H
#define BUTTON_MANAGER_H

#include <Arduino.h>
#include "config.h"

typedef void (*ButtonCallback)(void);

class ButtonManager {
public:
    ButtonManager();

    /** 初始化三个按钮引脚 */
    void begin();

    /** 主循环轮询 (每 10ms 调用) */
    void loop();

    /** 注册回调 */
    void onProvisionLongPress(ButtonCallback cb)   { _cbProvision = cb; }
    void onAlarmDismiss(ButtonCallback cb)          { _cbDismiss  = cb; }
    void onPageSwitch(ButtonCallback cb)            { _cbPage     = cb; }

private:
    struct ButtonState {
        uint8_t  pin;
        bool     pressed;        // 当前是否在按下状态
        uint32_t pressTime;      // 按下时刻 (ms)
        bool     longTriggered;  // 防止长按时重复触发短按
    };

    /** 处理单个按钮的状态机 */
    void _processButton(ButtonState& btn, bool hasShort, bool hasLong);
    bool _readPin(uint8_t pin);

    ButtonState _btnProvision;
    ButtonState _btnDismiss;
    ButtonState _btnPage;

    ButtonCallback _cbProvision;
    ButtonCallback _cbDismiss;
    ButtonCallback _cbPage;
};

#endif // BUTTON_MANAGER_H


1	/**
2	 * @file button_manager.cpp
3	 * @brief 三按钮管理器实现 - 去抖 + 长按/短按
4	 */
5
6	#include "button_manager.h"
7
8	// ============================================================
9	// 构造
10	// ============================================================
11
12	ButtonManager::ButtonManager()
13	    : _cbProvision(nullptr)
14	    , _cbDismiss(nullptr)
15	    , _cbPage(nullptr)
16	{
17	    _btnProvision = { BTN_PROVISION_PIN,     false, 0, false };
18	    _btnDismiss   = { BTN_ALARM_DISMISS_PIN, false, 0, false };
19	    _btnPage      = { BTN_PAGE_SWITCH_PIN,   false, 0, false };
20	}
21
22	// ============================================================
23	// 初始化
24	// ============================================================
25
26	void ButtonManager::begin() {
27	    pinMode(BTN_PROVISION_PIN,     INPUT_PULLUP);
28	    pinMode(BTN_ALARM_DISMISS_PIN, INPUT_PULLUP);
29	    pinMode(BTN_PAGE_SWITCH_PIN,   INPUT_PULLUP);
30
31	    Serial.println(F("[按钮] ✅ 三按钮初始化完成"));
32	    Serial.printf("  配网: GPIO%d | 解除报警: GPIO%d | 翻页: GPIO%d\n",
33	                  BTN_PROVISION_PIN, BTN_ALARM_DISMISS_PIN, BTN_PAGE_SWITCH_PIN);
34	}
35
36	// ============================================================
37	// 主循环轮询
38	// ============================================================
39
40	void ButtonManager::loop() {
41	    // 按钮1: 配网 (仅长按)
42	    _processButton(_btnProvision, false, true);
43
44	    // 按钮2: 解除报警 (仅短按)
45	    _processButton(_btnDismiss, true, false);
46
47	    // 按钮3: 翻页 (仅短按)
48	    _processButton(_btnPage, true, false);
49	}
50
51	// ============================================================
52	// 单按钮状态机
53	// ============================================================
54
55	void ButtonManager::_processButton(ButtonState& btn, bool hasShort, bool hasLong) {
56	    bool raw = _readPin(btn.pin);  // LOW = 按下
57
58	    if (raw && !btn.pressed) {
59	        // 按下沿
60	        btn.pressed = true;
61	        btn.pressTime = millis();
62	        btn.longTriggered = false;
63	    }
64	    else if (!raw && btn.pressed) {
65	        // 松开沿
66	        btn.pressed = false;
67	        uint32_t holdTime = millis() - btn.pressTime;
68
69	        if (holdTime < BTN_DEBOUNCE_MS) {
70	            return;  // 抖动脉冲，忽略
71	        }
72
73	        if (hasLong && holdTime >= BTN_LONG_PRESS_MS && !btn.longTriggered) {
74	            // 长按触发
75	            btn.longTriggered = true;
76	            if (_cbProvision) _cbProvision();
77	        }
78	        else if (hasShort && holdTime >= BTN_DEBOUNCE_MS && holdTime < BTN_LONG_PRESS_MS) {
79	            // 短按触发
80	            if (btn.pin == BTN_ALARM_DISMISS_PIN && _cbDismiss) _cbDismiss();
81	            if (btn.pin == BTN_PAGE_SWITCH_PIN   && _cbPage)    _cbPage();
82	        }
83	    }
84	}
85
86	bool ButtonManager::_readPin(uint8_t pin) {
87	    return digitalRead(pin) == LOW;  // INPUT_PULLUP: LOW = 按下
88	}
89

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

#include "button_manager.h"

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

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

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

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

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

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

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

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

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

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

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

    if (raw && !btn.pressed) {
        // 按下沿
        btn.pressed = true;
        btn.pressTime = millis();
        btn.longTriggered = false;
    }
    else if (!raw && btn.pressed) {
        // 松开沿
        btn.pressed = false;
        uint32_t holdTime = millis() - btn.pressTime;

        if (holdTime < BTN_DEBOUNCE_MS) {
            return;  // 抖动脉冲，忽略
        }

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return t;
}

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

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

    // --- 跌倒检测 (本地紧急报警, 立即触发) ---
    if (radar.fall && radar.radarValid && !g_localFallActive) {
        g_localFallActive = true;
        g_alarm.triggerLocal(AlarmManager::ALARM_FALL);
    } else if (!radar.fall && g_localFallActive) {
        g_localFallActive = false;
        g_alarm.clearLocal(AlarmManager::ALARM_FALL);
    }

    // --- 水浸检测 ---
    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);
    }

    // --- 心率异常 (离线本地判定, 阈值预留后续调整) ---
    if (radar.vitalValid && radar.heartRate > 0) {
        bool heartAbn = (radar.heartRate < HEART_RATE_MIN || radar.heartRate > HEART_RATE_MAX);
        if (heartAbn && !g_localHeartAbnActive) {
            g_localHeartAbnActive = true;
            g_alarm.triggerLocal(AlarmManager::ALARM_HEART_ABNORMAL);
        } else if (!heartAbn && g_localHeartAbnActive) {
            g_localHeartAbnActive = false;
            g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
        }
    }

    // --- 呼吸异常 ---
    if (radar.vitalValid && radar.breathRate > 0) {
        bool breathAbn = (radar.breathRate < BREATH_RATE_MIN || radar.breathRate > BREATH_RATE_MAX);
        if (breathAbn && !g_localBreathAbnActive) {
            g_localBreathAbnActive = true;
            g_alarm.triggerLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
        } else if (!breathAbn && g_localBreathAbnActive) {
            g_localBreathAbnActive = false;
            g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
        }
    }
}

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

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_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
    if (g_wifi.isConnected()) {
        g_mqtt.connect();
    }

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

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

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

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

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

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

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

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

        // WiFi 刚连上但 MQTT 还没连
        static bool wasDisconnected = true;
        if (g_mqtt.isConnected()) {
            if (wasDisconnected) {
                // 刚恢复连接, 同步时间
                syncTime();
                wasDisconnected = false;
            }
        } else {
            wasDisconnected = true;
        }
    }

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

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

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

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

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

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

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

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

    delay(10);
}


The user is saying "现在该系统，按钮老是卡住" - the buttons keep getting stuck/frozen. Let me analyze the button code carefully.

Looking at the `_processButton` function in [button_manager.cpp](src/button_manager.cpp#L55-L83):

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

    if (raw && !btn.pressed) {
        // 按下沿
        btn.pressed = true;
        btn.pressTime = millis();
        btn.longTriggered = false;
    }
    else if (!raw && btn.pressed) {
        // 松开沿
        btn.pressed = false;
        uint32_t holdTime = millis() - btn.pressTime;

        if (holdTime < BTN_DEBOUNCE_MS) {
            return;  // 抖动脉冲，忽略
        }

        if (hasLong && holdTime >= BTN_LONG_PRESS_MS && !btn.longTriggered) {
            // 长按触发
            btn.longTriggered = true;
            if (_cbProvision) _cbProvision();
        }
        else if (hasShort && holdTime >= BTN_DEBOUNCE_MS && holdTime < BTN_LONG_PRESS_MS) {
            // 短按触发
            if (btn.pin == BTN_ALARM_DISMISS_PIN && _cbDismiss) _cbDismiss();
            if (btn.pin == BTN_PAGE_SWITCH_PIN   && _cbPage)    _cbPage();
        }
    }
}
```

I see several issues:

1. **The long press check only happens on release (松开沿)**. In the `loop()`, the button manager is only called every ~10ms (through the `delay(10)` at the end of main loop). However, there's a radar task running at high priority on core 0 that could starve the main loop on core 1.

2. **The main problem**: Looking at `main.cpp`, the `delay(10)` at line 462 means the main loop runs at roughly 100Hz. But the radar task on core 0 runs at 10ms intervals too. The bigger issue is the main loop does a lot of work - `g_sensor.read()` (DHT11 is slow!), `checkLocalAlarms()`, `g_alarm.loop()`, and the telemetry reporting every second. If any of these operations blocks for more than a few milliseconds, the button state machine misses samples.

3. **Critical bug - polling frequency**: The button manager's `loop()` is called once per main loop iteration, which has `delay(10)`. But the main loop has `g_sensor.read()` which for DHT11 can take up to 20-25ms! When DHT11 is being read, it blocks the entire loop, meaning buttons are not polled during that time. If a button is pressed and released within that blocking period, the state machine misses it entirely.

4. **Another critical issue**: Looking at the `_processButton` logic more carefully:
   - The long press callback is only triggered on the release edge (松开沿). That's actually correct for some UX patterns, but the `longTriggered` flag is set to `true` when the long press fires on release, but it's never checked DURING the hold. The problem is that `hasLong && holdTime >= BTN_LONG_PRESS_MS` check only happens on release. But `longTriggered` being set to true at line 75 doesn't prevent re-triggering because `longTriggered` is reset to `false` on the next press.

   Actually wait, let me re-read the code more carefully.

   The `_processButton` for the provision button is called with `hasShort=false, hasLong=true`.

   When button is pressed: `raw=true, !btn.pressed` → sets `pressed=true, pressTime=now, longTriggered=false`.

   When button is released: `!raw && btn.pressed` → sets `pressed=false`, calculates `holdTime`.
   - If `holdTime >= BTN_LONG_PRESS_MS (1500)` and `!btn.longTriggered` → fires long press callback, sets `longTriggered=true`.
   - Since `hasShort=false`, the short press branch is skipped.

   This means the long press ONLY fires on release, not while holding. This is fine, but the user never gets feedback while holding. More importantly, long press works correctly.

   For the dismiss and page buttons: `hasShort=true, hasLong=false`.
   - On release: if `holdTime >= BTN_DEBOUNCE_MS (50)` and `holdTime < BTN_LONG_PRESS_MS (1500)`, fires the callback.

5. **The actual "stuck" problem**: I think there are several potential causes:

   a. **DHT11 blocking**: `g_sensor.read()` likely blocks for ~25ms reading the DHT11. During this time, button state changes are missed.

   b. **MQTT operations blocking**: `g_mqtt.loop()` might block for significant time during WiFi/MQTT operations.

   c. **The `delay(10)` at the end of loop**: Combined with blocking operations, the effective button polling rate could be very irregular (10ms to 50ms+).

   d. **Debounce logic issue**: Actually, the debounce logic looks mostly correct. The issue is more likely the irregular polling.

   e. **Race condition with radar task**: The radar task runs at priority 5 on core 0, while the main loop runs on core 1. This shouldn't cause issues on a dual-core ESP32-S3.

6. **The most probable cause of "stuck" buttons**: Let me think about this differently. The user says buttons "老是卡住" (keep getting stuck). This likely means:
   - The button state machine gets into a state where it can't recover
   - Or the button press is detected but the callback never fires
   - Or the button remains in "pressed" state even after physical release

   Looking at the state machine: if `btn.pressed` is `true` and the button is physically released, but `_readPin` returns `LOW` (pressed) due to noise or the pin floating, the button would be stuck in "pressed" state forever.

   Actually, GPIO0 (CONFIG_BUTTON_PIN) on ESP32 is special - it's a strapping pin. GPIO46 (BTN_PAGE_SWITCH_PIN) is also a special pin on ESP32-S3 - it's a strapping pin!

   Let me check: GPIO0 is a boot strapping pin. GPIO46 on ESP32-S3 is also a strapping pin (it controls the voltage of VDD_SPI).

   Wait, for the interactive buttons:
   - BTN_PROVISION_PIN = 14
   - BTN_ALARM_DISMISS_PIN = 13
   - BTN_PAGE_SWITCH_PIN = 46

   GPIO46 is a strapping pin on ESP32-S3! This is problematic. But this would cause boot issues, not button sticking issues.

7. **The real problem might be simpler**: Let me look at the `_readPin` function:

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

This reads the raw pin state. With `INPUT_PULLUP`, a pressed button reads LOW. But without proper hardware debouncing (capacitor), the mechanical switch bounce could cause:
- Multiple rapid press/release detections
- The state machine getting confused

But the debounce logic should handle this... unless the bounce causes `raw` to toggle very rapidly and the state machine gets into an inconsistent state.

Actually, I think I see a more fundamental issue now:

**The state machine only transitions on edges.** If for any reason the `raw` reading doesn't match the physical button state (noise, timing issue, etc.), the state machine gets stuck:

- If `btn.pressed=true` but the button is physically released AND `_readPin` returns LOW (noise/bounce), the state stays in "pressed" forever.
- If `btn.pressed=false` but the button is physically pressed AND `_readPin` returns HIGH (pullup works but noise), the press is never detected.

The fix should be:
1. **Use a more robust debounce approach** - sample the pin multiple times or use a time-based debounce
2. **Add a timeout/safety mechanism** - if a button is "pressed" for an unreasonably long time (e.g., 10 seconds), reset it
3. **Consider using interrupts** instead of polling
4. **Reduce blocking in the main loop** - especially DHT11 reads

Let me also check what `g_sensor.read()` does since DHT11 is notoriously slow.

Let me also look at the sensor_manager.cpp to understand the blocking.

Actually, let me just present my analysis to the user and propose a fix. The main issues I see:

1. **The debounce only works on the release edge but not on the press edge.** If noise on the press causes a false trigger, `pressed` becomes true and stays true until a clean release is detected. But if more noise comes, it might never see a clean release.

2. **No timeout/stuck detection.** If the button state gets stuck (pressed=true but physically released), there's no recovery.

3. **Blocking main loop.** DHT11 reading and MQTT operations can block for tens of milliseconds, causing button state transitions to be missed.

4. **The debounce on press:** The press transition (`raw && !btn.pressed`) has NO debounce check! Any noise spike sets `pressed=true` immediately. The debounce only happens on release. This means a noise spike on the pin could set the button to "pressed" state, and if the pin then stays LOW (for whatever reason - GPIO strapping, noise, floating), the button is stuck forever.

This is likely the root cause! The press edge has no debounce. A noise spike sets `pressed=true`. If the next readings are clean (HIGH, not pressed), the state machine sees `!raw && btn.pressed` and fires the release logic. But if there's any issue (pin stays low due to capacitance, noise, etc.), the button is stuck.

Let me propose a comprehensive fix.


找到问题了。分析完 [button_manager.cpp](src/button_manager.cpp)，按钮卡住的根因有几个：

**核心 Bug：按下沿（press edge）完全没有去抖！**

看第 58 行 `if (raw && !btn.pressed)` — 任何一次噪声尖峰都会立即将 `pressed` 设为 `true`，之后如果没有干净的释放信号，按钮就永久卡死在"按下"状态。

其他问题：
1. 主循环 `delay(10)` + DHT11 读取（阻塞 ~25ms）+ MQTT 操作 → 轮询间隔极不规律，可能丢失按钮状态变化
2. 没有"超时强制复位"机制 — 一旦卡住就永远无法恢复

我来重写按钮管理器，加入完整的双向去抖和超时保护：


1	/**
2	 * @file sensor_manager.cpp
3	 * @brief 传感器管理器实现 - DHT11 + 水浸
4	 */
5
6	#include "sensor_manager.h"
7
8	SensorManager::SensorManager()
9	    : _dht(nullptr)
10	    , _temp(0)
11	    , _humi(0)
12	    , _dhtValid(false)
13	    , _dhtErrorCnt(0)
14	    , _rainDO(true)       // 1=正常
15	    , _rainValid(false)
16	    , _lastRainRaw(true)
17	    , _rainChangeTime(0)
18	    , _callback(nullptr)
19	    , _lastReadTime(0)
20	{
21	}
22
23	SensorManager::~SensorManager() {
24	    if (_dht) delete _dht;
25	}
26
27	// ============================================================
28	// 初始化
29	// ============================================================
30
31	bool SensorManager::begin() {
32	    // DHT11 — 允许失败，不影响水浸
33	    _dht = new DHT(DHT11_PIN, DHT_TYPE);
34	    _dht->begin();
35	    _dhtValid = false;
36	    _dhtErrorCnt = 0;
37
38	    // 水浸传感器 — 独立初始化
39	    pinMode(WATER_SENSOR_PIN, INPUT_PULLUP);
40	    _lastRainRaw = (digitalRead(WATER_SENSOR_PIN) == HIGH);  // HIGH=正常
41	    _rainDO = _lastRainRaw;
42	    _rainValid = true;
43
44	    // 首次读取 (DHT11 可能失败)
45	    read();
46
47	    Serial.println(F("[传感器] 初始化完成"));
48	    Serial.printf("  DHT11: %s\n",
49	                  _dhtValid ? "✅ 正常" : "⚠ 未检测到 (不影响系统运行)");
50	    Serial.printf("  水浸:  %s\n",
51	                  _rainValid ? "✅ 正常" : "⚠ 未检测到");
52	    return true;  // 始终成功，不阻塞系统启动
53	}
54
55	// ============================================================
56	// 读取传感器
57	// ============================================================
58
59	void SensorManager::read() {
60	    // --- DHT11 读取 (独立，失败不影响水浸) ---
61	    if (millis() - _lastReadTime >= 2000) {
62	        _lastReadTime = millis();
63
64	        float t = _dht->readTemperature();
65	        float h = _dht->readHumidity();
66
67	        if (!isnan(t) && !isnan(h)) {
68	            if (t > -20 && t < 80 && h >= 0 && h <= 100) {
69	                _temp = t;
70	                _humi = h;
71	                _dhtValid = true;
72	                _dhtErrorCnt = 0;  // 恢复正常，清零计数
73	            } else {
74	                _dhtValid = false;
75	                _dhtErrorCnt++;
76	            }
77	        } else {
78	            _dhtValid = false;
79	            _dhtErrorCnt++;
80	        }
81
82	        // 连续失败 10 次 (~20秒) 后只报一次，避免刷屏
83	        if (_dhtErrorCnt == 10) {
84	            Serial.println(F("[传感器] ⚠ DHT11 持续读取失败 (不影响其他传感器)"));
85	        }
86	    }
87
88	    // 水浸传感器读取 (防抖)
89	    bool raw = (digitalRead(WATER_SENSOR_PIN) == HIGH);  // HIGH=正常
90	    if (raw != _lastRainRaw) {
91	        _rainChangeTime = millis();
92	        _lastRainRaw = raw;
93	    }
94
95	    if (millis() - _rainChangeTime >= WATER_DEBOUNCE_MS) {
96	        bool newState = _lastRainRaw;
97	        if (newState != _rainDO) {
98	            _rainDO = newState;
99	            _rainValid = true;
100
101	            if (!_rainDO) {
102	                Serial.println(F("[传感器] ⚠ 检测到水浸!"));
103	            } else {
104	                Serial.println(F("[传感器] ✅ 水浸解除"));
105	            }
106
107	            if (_callback) _callback();
108	        }
109	    }
110	}
111
112	// ============================================================
113	// 回调
114	// ============================================================
115
116	void SensorManager::onChange(SensorCallback cb) {
117	    _callback = cb;
118	}
119

/**
 * @file sensor_manager.cpp
 * @brief 传感器管理器实现 - DHT11 + 水浸
 */

#include "sensor_manager.h"

SensorManager::SensorManager()
    : _dht(nullptr)
    , _temp(0)
    , _humi(0)
    , _dhtValid(false)
    , _dhtErrorCnt(0)
    , _rainDO(true)       // 1=正常
    , _rainValid(false)
    , _lastRainRaw(true)
    , _rainChangeTime(0)
    , _callback(nullptr)
    , _lastReadTime(0)
{
}

SensorManager::~SensorManager() {
    if (_dht) delete _dht;
}

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

bool SensorManager::begin() {
    // DHT11 — 允许失败，不影响水浸
    _dht = new DHT(DHT11_PIN, DHT_TYPE);
    _dht->begin();
    _dhtValid = false;
    _dhtErrorCnt = 0;

    // 水浸传感器 — 独立初始化
    pinMode(WATER_SENSOR_PIN, INPUT_PULLUP);
    _lastRainRaw = (digitalRead(WATER_SENSOR_PIN) == HIGH);  // HIGH=正常
    _rainDO = _lastRainRaw;
    _rainValid = true;

    // 首次读取 (DHT11 可能失败)
    read();

    Serial.println(F("[传感器] 初始化完成"));
    Serial.printf("  DHT11: %s\n",
                  _dhtValid ? "✅ 正常" : "⚠ 未检测到 (不影响系统运行)");
    Serial.printf("  水浸:  %s\n",
                  _rainValid ? "✅ 正常" : "⚠ 未检测到");
    return true;  // 始终成功，不阻塞系统启动
}

// ============================================================
// 读取传感器
// ============================================================

void SensorManager::read() {
    // --- DHT11 读取 (独立，失败不影响水浸) ---
    if (millis() - _lastReadTime >= 2000) {
        _lastReadTime = millis();

        float t = _dht->readTemperature();
        float h = _dht->readHumidity();

        if (!isnan(t) && !isnan(h)) {
            if (t > -20 && t < 80 && h >= 0 && h <= 100) {
                _temp = t;
                _humi = h;
                _dhtValid = true;
                _dhtErrorCnt = 0;  // 恢复正常，清零计数
            } else {
                _dhtValid = false;
                _dhtErrorCnt++;
            }
        } else {
            _dhtValid = false;
            _dhtErrorCnt++;
        }

        // 连续失败 10 次 (~20秒) 后只报一次，避免刷屏
        if (_dhtErrorCnt == 10) {
            Serial.println(F("[传感器] ⚠ DHT11 持续读取失败 (不影响其他传感器)"));
        }
    }

    // 水浸传感器读取 (防抖)
    bool raw = (digitalRead(WATER_SENSOR_PIN) == HIGH);  // HIGH=正常
    if (raw != _lastRainRaw) {
        _rainChangeTime = millis();
        _lastRainRaw = raw;
    }

    if (millis() - _rainChangeTime >= WATER_DEBOUNCE_MS) {
        bool newState = _lastRainRaw;
        if (newState != _rainDO) {
            _rainDO = newState;
            _rainValid = true;

            if (!_rainDO) {
                Serial.println(F("[传感器] ⚠ 检测到水浸!"));
            } else {
                Serial.println(F("[传感器] ✅ 水浸解除"));
            }

            if (_callback) _callback();
        }
    }
}

// ============================================================
// 回调
// ============================================================

void SensorManager::onChange(SensorCallback cb) {
    _callback = cb;
}


Now I have a complete picture. The DHT11 reads every 2 seconds, and each read takes ~25ms (blocking). But this alone isn't the main issue for buttons getting stuck.

The primary issue is in the button state machine - the press edge has NO debounce. Let me fix this properly.

Here's my plan for the fix:
1. Add debounce on the press edge too (X consecutive stable readings before accepting a state change)
2. Add a timeout safety - if a button is "pressed" for more than 10 seconds, force-reset it
3. Use a more robust debounce that requires stable readings

Actually, let me think about the best approach for an embedded system:

The standard approach is:
1. Sample the pin
2. If the state is different from the last stable state, start a debounce timer
3. If the state remains stable for the debounce period, accept the state change
4. Track press duration for short/long press detection AFTER debounce confirms the press

This is fundamentally different from the current approach which:
- Instantly accepts the press edge
- Only debounces on the release edge

Let me rewrite the button manager with this proper approach.

Also, I need to address the fact that the button press/release is only checked when `loop()` is called, which could have irregular intervals. But with proper debounce timing based on `millis()`, this should be okay as long as the polling interval is less than the debounce time (50ms). With `delay(10)` and DHT11 blocking ~25ms every 2 seconds, the worst case is about 35ms between polls, which is fine for 50ms debounce.

Key changes:
1. Replace edge-triggered state machine with a time-based debounce state machine
2. Both press and release require stable readings for BTN_DEBOUNCE_MS
3. Add stuck-button timeout (10 seconds) to auto-reset
4. Use `BTN_LONG_PRESS_MS` for long press detection (trigger on hold, not just release)

Let me write this now.


好的，我现在完全理解了。让我总结诊断结果并修复：

**根本原因：** [button_manager.cpp:58](src/button_manager.cpp#L58) — 按钮的**按下沿没有任何去抖**，噪声尖峰会立即让状态机进入 `pressed=true`，如果之后读不到干净的释放信号，按钮就永久卡死。再加上 DHT11 每 2 秒阻塞主循环 ~25ms，加剧了状态丢失。

现在重写按钮管理器，加入完整的双向去抖 + 超时保护：


Edit

/**
 * @file button_manager.h
 * @brief 三按钮管理器 - 去抖 + 长按/短按识别
 *
 * 按钮1 (GPIO14): 长按3秒 → 配网
 * 按钮2 (GPIO15): 短按 → 解除报警
 * 按钮3 (GPIO16): 短按 → 翻页
 */

#ifndef BUTTON_MANAGER_H
#define BUTTON_MANAGER_H

#include <Arduino.h>
#include "config.h"

typedef void (*ButtonCallback)(void);

class ButtonManager {
public:
    ButtonManager();

    /** 初始化三个按钮引脚 */
    void begin();

    /** 主循环轮询 (每 10ms 调用) */
    void loop();

    /** 注册回调 */
    void onProvisionLongPress(ButtonCallback cb)   { _cbProvision = cb; }
    void onAlarmDismiss(ButtonCallback cb)          { _cbDismiss  = cb; }
    void onPageSwitch(ButtonCallback cb)            { _cbPage     = cb; }

private:
    struct ButtonState {
        uint8_t  pin;
        bool     pressed;        // 当前是否在按下状态
        uint32_t pressTime;      // 按下时刻 (ms)
        bool     longTriggered;  // 防止长按时重复触发短按
    };

    /** 处理单个按钮的状态机 */
    void _processButton(ButtonState& btn, bool hasShort, bool hasLong);
    bool _readPin(uint8_t pin);

    ButtonState _btnProvision;
    ButtonState _btnDismiss;
    ButtonState _btnPage;

    ButtonCallback _cbProvision;
    ButtonCallback _cbDismiss;
    ButtonCallback _cbPage;
};

#endif // BUTTON_MANAGER_H

/**
 * @file button_manager.h
 * @brief 三按钮管理器 - 双向去抖 + 长按/短按识别 + 超时自愈
 *
 * 按钮1 (GPIO14): 长按1.5秒 → 配网
 * 按钮2 (GPIO13): 短按 → 解除报警
 * 按钮3 (GPIO46): 短按 → 翻页
 *
 * 去抖策略: 状态变化需持续稳定 BTN_DEBOUNCE_MS 才确认
 * 自愈策略: 按钮按下超过 10 秒强制复位，防止卡死
 */

#ifndef BUTTON_MANAGER_H
#define BUTTON_MANAGER_H

#include <Arduino.h>
#include "config.h"

#define BTN_STUCK_TIMEOUT_MS  10000   // 按钮卡死超时 (10秒强制复位)

typedef void (*ButtonCallback)(void);

class ButtonManager {
public:
    ButtonManager();

    /** 初始化三个按钮引脚 */
    void begin();

    /** 主循环轮询 (每 10ms 调用) */
    void loop();

    /** 注册回调 */
    void onProvisionLongPress(ButtonCallback cb)   { _cbProvision = cb; }
    void onAlarmDismiss(ButtonCallback cb)          { _cbDismiss  = cb; }
    void onPageSwitch(ButtonCallback cb)            { _cbPage     = cb; }

private:
    /**
     * 按钮状态机 (基于稳定读数而非边沿)
     *
     * 状态转换:
     *   IDLE ──[raw稳定LOW×BTN_DEBOUNCE_MS]──→ PRESSED (记录pressTime)
     *   PRESSED ──[hold≥BTN_LONG_PRESS_MS]──→ 触发长按回调 (仅限hasLong)
     *   PRESSED ──[raw稳定HIGH×BTN_DEBOUNCE_MS]──→ 检查hold时长触发短按 → IDLE
     *   PRESSED ──[hold≥BTN_STUCK_TIMEOUT_MS]──→ 强制复位 → IDLE (防卡死)
     */
    struct ButtonState {
        uint8_t  pin;
        bool     stableState;    // 当前确认的稳定状态 (true=按下)
        bool     rawState;       // 上一次 raw 读值
        uint32_t changeTime;     // raw 状态变化时刻
        uint32_t pressTime;      // 稳定按下时刻 (ms)
        bool     longTriggered;  // 长按已触发
        bool     shortPending;   // 等待释放以判定短按
    };

    /** 处理单个按钮 (基于稳定状态而非瞬时边沿) */
    void _processButton(ButtonState& btn, bool hasShort, bool hasLong);
    bool _readPin(uint8_t pin);

    /** 强制复位单个按钮 */
    void _resetButton(ButtonState& btn);

    ButtonState _btnProvision;
    ButtonState _btnDismiss;
    ButtonState _btnPage;

    ButtonCallback _cbProvision;
    ButtonCallback _cbDismiss;
    ButtonCallback _cbPage;
};

#endif // BUTTON_MANAGER_H


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

/**
 * @file button_manager.h
 * @brief 三按钮管理器 - 去抖 + 长按/短按识别
 *
 * 按钮1 (GPIO14): 长按3秒 → 配网
 * 按钮2 (GPIO15): 短按 → 解除报警
 * 按钮3 (GPIO16): 短按 → 翻页
 */

#ifndef BUTTON_MANAGER_H
#define BUTTON_MANAGER_H

#include <Arduino.h>
#include "config.h"

typedef void (*ButtonCallback)(void);

class ButtonManager {
public:
    ButtonManager();

    /** 初始化三个按钮引脚 */
    void begin();

    /** 主循环轮询 (每 10ms 调用) */
    void loop();

    /** 注册回调 */
    void onProvisionLongPress(ButtonCallback cb)   { _cbProvision = cb; }
    void onAlarmDismiss(ButtonCallback cb)          { _cbDismiss  = cb; }
    void onPageSwitch(ButtonCallback cb)            { _cbPage     = cb; }

private:
    struct ButtonState {
        uint8_t  pin;
        bool     pressed;        // 当前是否在按下状态
        uint32_t pressTime;      // 按下时刻 (ms)
        bool     longTriggered;  // 防止长按时重复触发短按
    };

    /** 处理单个按钮的状态机 */
    void _processButton(ButtonState& btn, bool hasShort, bool hasLong);
    bool _readPin(uint8_t pin);

    ButtonState _btnProvision;
    ButtonState _btnDismiss;
    ButtonState _btnPage;

    ButtonCallback _cbProvision;
    ButtonCallback _cbDismiss;
    ButtonCallback _cbPage;
};

#endif // BUTTON_MANAGER_H

/**
 * @file button_manager.h
 * @brief 三按钮管理器 - 双向去抖 + 长按/短按识别 + 超时自愈
 *
 * 按钮1 (GPIO14): 长按1.5秒 → 配网
 * 按钮2 (GPIO13): 短按 → 解除报警
 * 按钮3 (GPIO46): 短按 → 翻页
 *
 * 去抖策略: 状态变化需持续稳定 BTN_DEBOUNCE_MS 才确认
 * 自愈策略: 按钮按下超过 10 秒强制复位，防止卡死
 */

#ifndef BUTTON_MANAGER_H
#define BUTTON_MANAGER_H

#include <Arduino.h>
#include "config.h"

#define BTN_STUCK_TIMEOUT_MS  10000   // 按钮卡死超时 (10秒强制复位)

typedef void (*ButtonCallback)(void);

class ButtonManager {
public:
    ButtonManager();

    /** 初始化三个按钮引脚 */
    void begin();

    /** 主循环轮询 (每 10ms 调用) */
    void loop();

    /** 注册回调 */
    void onProvisionLongPress(ButtonCallback cb)   { _cbProvision = cb; }
    void onAlarmDismiss(ButtonCallback cb)          { _cbDismiss  = cb; }
    void onPageSwitch(ButtonCallback cb)            { _cbPage     = cb; }

private:
    /**
     * 按钮状态机 (基于稳定读数而非边沿)
     *
     * 状态转换:
     *   IDLE ──[raw稳定LOW×BTN_DEBOUNCE_MS]──→ PRESSED (记录pressTime)
     *   PRESSED ──[hold≥BTN_LONG_PRESS_MS]──→ 触发长按回调 (仅限hasLong)
     *   PRESSED ──[raw稳定HIGH×BTN_DEBOUNCE_MS]──→ 检查hold时长触发短按 → IDLE
     *   PRESSED ──[hold≥BTN_STUCK_TIMEOUT_MS]──→ 强制复位 → IDLE (防卡死)
     */
    struct ButtonState {
        uint8_t  pin;
        bool     stableState;    // 当前确认的稳定状态 (true=按下)
        bool     rawState;       // 上一次 raw 读值
        uint32_t changeTime;     // raw 状态变化时刻
        uint32_t pressTime;      // 稳定按下时刻 (ms)
        bool     longTriggered;  // 长按已触发
        bool     shortPending;   // 等待释放以判定短按
    };

    /** 处理单个按钮 (基于稳定状态而非瞬时边沿) */
    void _processButton(ButtonState& btn, bool hasShort, bool hasLong);
    bool _readPin(uint8_t pin);

    /** 强制复位单个按钮 */
    void _resetButton(ButtonState& btn);

    ButtonState _btnProvision;
    ButtonState _btnDismiss;
    ButtonState _btnPage;

    ButtonCallback _cbProvision;
    ButtonCallback _cbDismiss;
    ButtonCallback _cbPage;
};

#endif // BUTTON_MANAGER_H

- * @brief 三按钮管理器 - 去抖 + 长按/短按识别

+ * @brief 三按钮管理器 - 双向去抖 + 长按/短按识别 + 超时自愈

+ * 自愈策略: 按钮按下超过 10 秒强制复位，防止卡死

+#define BTN_STUCK_TIMEOUT_MS  10000   // 按钮卡死超时 (10秒强制复位)

+     * 按钮状态机 (基于稳定读数而非边沿)

+     * 状态转换:

+     *   IDLE ──[raw稳定LOW×BTN_DEBOUNCE_MS]──→ PRESSED (记录pressTime)

+     *   PRESSED ──[hold≥BTN_LONG_PRESS_MS]──→ 触发长按回调 (仅限hasLong)

+     *   PRESSED ──[raw稳定HIGH×BTN_DEBOUNCE_MS]──→ 检查hold时长触发短按 → IDLE

+     *   PRESSED ──[hold≥BTN_STUCK_TIMEOUT_MS]──→ 强制复位 → IDLE (防卡死)

-        bool     pressed;        // 当前是否在按下状态

-        uint32_t pressTime;      // 按下时刻 (ms)

-        bool     longTriggered;  // 防止长按时重复触发短按

+        bool     stableState;    // 当前确认的稳定状态 (true=按下)

+        bool     rawState;       // 上一次 raw 读值

+        uint32_t changeTime;     // raw 状态变化时刻

+        uint32_t pressTime;      // 稳定按下时刻 (ms)

+        bool     longTriggered;  // 长按已触发

+        bool     shortPending;   // 等待释放以判定短按

-    /** 处理单个按钮的状态机 */

+    /** 处理单个按钮 (基于稳定状态而非瞬时边沿) */

+    /** 强制复位单个按钮 */

+    void _resetButton(ButtonState& btn);


现在该系统，按钮老是卡住，


Now rewrite the implementation file with the robust debounce logic.


现在重写实现文件：


Write

/**
 * @file button_manager.cpp
 * @brief 三按钮管理器实现 - 双向去抖 + 长按/短按 + 超时自愈
 *
 * 核心改进 (v2):
 *   1. 按下沿和释放沿都经过 BTN_DEBOUNCE_MS 稳定确认
 *   2. 长按期间持续检测 (不再等到松手才触发)
 *   3. 按下超过 BTN_STUCK_TIMEOUT_MS 自动复位, 防止状态机卡死
 *   4. 用 stableState 替代瞬时 raw 驱动状态, 消除噪声误触发
 */

#include "button_manager.h"

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

ButtonManager::ButtonManager()
    : _cbProvision(nullptr)
    , _cbDismiss(nullptr)
    , _cbPage(nullptr)
{
    auto initBtn = [](ButtonState& s, uint8_t pin) {
        s.pin           = pin;
        s.stableState   = false;
        s.rawState      = false;
        s.changeTime    = 0;
        s.pressTime     = 0;
        s.longTriggered = false;
        s.shortPending  = false;
    };

    initBtn(_btnProvision, BTN_PROVISION_PIN);
    initBtn(_btnDismiss,   BTN_ALARM_DISMISS_PIN);
    initBtn(_btnPage,      BTN_PAGE_SWITCH_PIN);
}

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

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

    // 读取初始状态, 避免启动时的虚假边沿
    _btnProvision.rawState   = _readPin(BTN_PROVISION_PIN);
    _btnProvision.stableState = _btnProvision.rawState;
    _btnDismiss.rawState     = _readPin(BTN_ALARM_DISMISS_PIN);
    _btnDismiss.stableState   = _btnDismiss.rawState;
    _btnPage.rawState        = _readPin(BTN_PAGE_SWITCH_PIN);
    _btnPage.stableState      = _btnPage.rawState;

    Serial.println(F("[按钮] ✅ 三按钮初始化完成 (双向去抖+超时自愈)"));
    Serial.printf("  配网: GPIO%d | 解除报警: GPIO%d | 翻页: GPIO%d\n",
                  BTN_PROVISION_PIN, BTN_ALARM_DISMISS_PIN, BTN_PAGE_SWITCH_PIN);
    Serial.printf("  去抖:%dms | 长按:%dms | 卡死超时:%dms\n",
                  BTN_DEBOUNCE_MS, BTN_LONG_PRESS_MS, BTN_STUCK_TIMEOUT_MS);
}

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

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

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

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

// ============================================================
// 单按钮状态机 (基于稳定状态, 非瞬时边沿)
// ============================================================

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

    // ── 第1步: raw 状态变化时启动去抖计时 ──
    if (raw != btn.rawState) {
        btn.rawState = raw;
        btn.changeTime = now;
        // 不立即改变 stableState — 等稳定确认
    }

    // ── 第2步: 去抖确认 — raw 保持稳定超过 BTN_DEBOUNCE_MS 才生效 ──
    bool debounced = (now - btn.changeTime >= BTN_DEBOUNCE_MS);
    bool stateChanged = (btn.rawState != btn.stableState);

    if (debounced && stateChanged) {
        // 状态正式切换
        btn.stableState = btn.rawState;

        if (btn.stableState) {
            // ── 确认按下 ──
            btn.pressTime     = now;
            btn.longTriggered = false;
            btn.shortPending  = hasShort;  // 只有支持短按的按钮才标记待判定
        } else {
            // ── 确认释放 ──
            uint32_t holdTime = now - btn.pressTime;

            // 短按判定: 按下时长在去抖和长按阈值之间
            if (btn.shortPending && holdTime >= BTN_DEBOUNCE_MS
                && holdTime < BTN_LONG_PRESS_MS && !btn.longTriggered) {
                // 根据引脚分发短按回调
                if (btn.pin == BTN_ALARM_DISMISS_PIN && _cbDismiss) {
                    _cbDismiss();
                }
                if (btn.pin == BTN_PAGE_SWITCH_PIN && _cbPage) {
                    _cbPage();
                }
            }

            btn.shortPending  = false;
            btn.longTriggered = false;
        }
    }

    // ── 第3步: 长按检测 (在按下稳定期间持续检查) ──
    if (hasLong && btn.stableState && !btn.longTriggered) {
        uint32_t holdTime = now - btn.pressTime;
        if (holdTime >= BTN_LONG_PRESS_MS) {
            btn.longTriggered = true;
            btn.shortPending  = false;  // 长按已触发, 不再判定短按
            if (_cbProvision) {
                _cbProvision();
            }
        }
    }

    // ── 第4步: 卡死自愈 — 按下超时强制复位 ──
    if (btn.stableState && (now - btn.pressTime >= BTN_STUCK_TIMEOUT_MS)) {
        Serial.printf("[按钮] ⚠ GPIO%d 按下超时 %lums, 强制复位\n",
                      btn.pin, now - btn.pressTime);
        _resetButton(btn);
    }
}

// ============================================================
// 强制复位
// ============================================================

void ButtonManager::_resetButton(ButtonState& btn) {
    btn.stableState   = false;
    btn.rawState      = _readPin(btn.pin);  // 重新同步真实硬件状态
    btn.changeTime    = 0;
    btn.pressTime     = 0;
    btn.longTriggered = false;
    btn.shortPending  = false;
}

// ============================================================
// 读引脚 (INPUT_PULLUP → LOW = 按下)
// ============================================================

bool ButtonManager::_readPin(uint8_t pin) {
    return digitalRead(pin) == LOW;
}