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

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

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

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

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

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

// 本地报警跟踪 (防重复触发)
bool    g_localFallActive       = false;
bool    g_localWaterActive      = false;
bool    g_localHeartAbnActive   = false;
bool    g_localBreathAbnActive  = false;
bool    g_localTempAbnActive    = false;
bool    g_localHumiAbnActive    = false;
bool    g_localRadarFaultActive = false;
bool    g_localDhtFaultActive   = false;

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

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

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

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

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

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

void handleAlarmCommand(const AlarmCommand& cmd) {
    if (strcmp(cmd.type, "alarm_restore") == 0) {
        // 告警恢复 — 远程指令与手动按钮同等优先级
        g_alarm.executeRestore(cmd);

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

        // 防抖计数器也复位
        g_tempAbnCnt      = 0;
        g_humiAbnCnt      = 0;
        g_radarTimeoutCnt = 0;
        g_heartAbnCnt     = 0;
        g_breathAbnCnt    = 0;
        g_fallDetectMs     = 0;
        g_fallClearMs      = 0;
        g_fallDismissed    = false;
    } else {
        // 告警触发
        g_alarm.executeAlarm(cmd);
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    // 环境类型: 从配网配置读取 (默认 bedroom)
    DeviceConfig cfg = g_wifi.getConfig();
    strncpy(t.deviceType, cfg.env_type[0] ? cfg.env_type : "bedroom", sizeof(t.deviceType)-1);

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

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

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

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

    return t;
}

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

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

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

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

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

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

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

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

    // ─── ★★★ 环境安全: 水浸 (仅浴室) ───
    if (enWater) {
    bool waterLeak = !g_sensor.getRainDO() && g_sensor.isRainValid();
    if (waterLeak && !g_localWaterActive) {
        g_localWaterActive = true;
        g_alarm.triggerLocal(AlarmManager::ALARM_WATER_LEAK, "水浸传感器触发");
    } else if (!waterLeak && g_localWaterActive) {
        g_localWaterActive = false;
        g_alarm.clearLocal(AlarmManager::ALARM_WATER_LEAK);
    }
    }  // enWater

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

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

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

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

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

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

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

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

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

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

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

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

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

    g_bootTime = millis();

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

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

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

    // 4. 按钮
    g_buttons.begin();
    g_buttons.onPageSwitch([]() {
        g_display.nextPage();
    });
    g_buttons.onAlarmDismiss([]() {
        g_alarm.clearAllLocal();
        // 同步清除所有本地状态标记
        g_localFallActive       = false;
        g_localWaterActive      = false;
        g_localHeartAbnActive   = false;
        g_localBreathAbnActive  = false;
        g_localTempAbnActive    = false;
        g_localHumiAbnActive    = false;
        g_localRadarFaultActive = false;
        g_localDhtFaultActive   = false;
        g_tempAbnCnt            = 0;
        g_humiAbnCnt            = 0;
        g_radarTimeoutCnt       = 0;
        g_fallClearMs           = 0;
        g_fallDismissed         = true;   // 抑制重复报警, 等跌倒信号消了再恢复
    });
    g_buttons.onProvisionLongPress([]() {
        g_wifi.startConfigPortal();
    });

    // 5. WiFi (WiFiManager 配网)
    g_wifi.begin();

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

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

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

    // 注册本地报警回调 → MQTT 上报
    g_alarm.onAlarmChange(onLocalAlarmChange);

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

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

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

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

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

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

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

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

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

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

        // WiFi 刚连上但 MQTT 还没连
        static bool wasDisconnected = true;
        if (g_mqtt.isConnected()) {
            if (wasDisconnected) {
                // 刚恢复连接, 同步时间
                syncTime();
                wasDisconnected = false;
            }
            // 配网成功后更新 MQTT 配置 (如果有改动) 并上报
            if (g_wifi.justProvisioned()) {
                DeviceConfig cfg = g_wifi.getConfig();
                g_mqtt.setServer(cfg.mqtt_server, atoi(cfg.mqtt_port),
                                 cfg.mqtt_user, cfg.mqtt_pass);
                g_mqtt.publishProvisionStatus("success", "");
            }
        } else {
            wasDisconnected = true;
        }
    }

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

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

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

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

    // --- 6. 遥测上报 (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(),
                         g_alarm.hasActiveAlarm(), g_alarm.getActiveAlarmStr());
    }

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

    delay(10);
}


Now let me look at the mqtt_manager.cpp to see the `publishAllPoints` function to understand exactly what JSON is sent when no person is detected.


再来看 `publishAllPoints` 具体怎么构建报文的：


1	/**
2	 * @file mqtt_manager.cpp
3	 * @brief MQTT 通信管理器实现 (协议规范 V0.08)
4	 */
5
6	#include "mqtt_manager.h"
7
8	MQTTManager* MQTTManager::_instance = nullptr;
9
10	// ============================================================
11	// 构造函数 & 析构函数
12	// ============================================================
13
14	MQTTManager::MQTTManager()
15	    : _mqtt(_wifiClient)
16	    , _port(1883)
17	    , _reconnectInterval(MQTT_RECONNECT_MIN)
18	    , _lastReconnectAttempt(0)
19	    , _connected(false)
20	    , _alarmCb(nullptr)
21	    , _controlCb(nullptr)
22	    , _provisionCb(nullptr)
23	{
24	    memset(_gateid, 0, sizeof(_gateid));
25	    memset(_server, 0, sizeof(_server));
26	    memset(_user,   0, sizeof(_user));
27	    memset(_pass,   0, sizeof(_pass));
28	    _instance = this;
29	}
30
31	MQTTManager::~MQTTManager() {
32	    disconnect();
33	    _instance = nullptr;
34	}
35
36	// ============================================================
37	// 初始化
38	// ============================================================
39
40	bool MQTTManager::begin(const char* gateid) {
41	    strncpy(_gateid, gateid, sizeof(_gateid) - 1);
42
43	    Serial.println(F("[MQTT] 初始化 (协议 V0.08)"));
44	    Serial.printf("  网关ID: %s\n", _gateid);
45	    Serial.printf("  服务器: %s:%d\n", _server, _port);
46
47	    _mqtt.setServer(_server, _port);
48	    _mqtt.setCallback(_mqttCallback);
49	    _mqtt.setKeepAlive(MQTT_KEEPALIVE);
50	    _mqtt.setBufferSize(1024);  // JSON 报文较大
51
52	    return true;
53	}
54
55	// ============================================================
56	// 设置服务器
57	// ============================================================
58
59	void MQTTManager::setServer(const char* host, uint16_t port,
60	                             const char* user, const char* pass) {
61	    strncpy(_server, host, sizeof(_server) - 1);
62	    _port = port;
63	    strncpy(_user, user, sizeof(_user) - 1);
64	    strncpy(_pass, pass, sizeof(_pass) - 1);
65	    _mqtt.setServer(_server, _port);
66	}
67
68	// ============================================================
69	// 连接 & 断开
70	// ============================================================
71
72	bool MQTTManager::connect() {
73	    if (_connected && _mqtt.connected()) return true;
74	    if (!WiFi.isConnected()) return false;
75
76	    Serial.printf("[MQTT] 连接 %s:%d ...\n", _server, _port);
77
78	    // Client ID = gateid
79	    String clientId = _gateid;
80
81	    // 遗嘱消息 (§4.3): status/{gateid}/offline
82	    String willTopic = _buildTopic(MQTT_TOPIC_STATUS_OFFLINE);
83	    JsonDocument willDoc;
84	    willDoc["gateid"] = _gateid;
85	    willDoc["type"]   = "offline";
86	    willDoc["time"]   = "";  // Broker 不会填充时间
87	    String willPayload;
88	    serializeJson(willDoc, willPayload);
89
90	    bool hasAuth = (strlen(_user) > 0);
91	    bool ok = false;
92
93	    if (hasAuth) {
94	        ok = _mqtt.connect(clientId.c_str(),
95	                          _user, _pass,
96	                          willTopic.c_str(), 1, true, willPayload.c_str());
97	    } else {
98	        ok = _mqtt.connect(clientId.c_str(),
99	                          willTopic.c_str(), 1, true, willPayload.c_str());
100	    }
101
102	    if (ok) {
103	        _connected = true;
104	        Serial.println(F("[MQTT] ✅ 已连接!"));
105
106	        // 订阅下行主题 (§3.2)
107	        String subAlarm    = _buildTopic(MQTT_TOPIC_CMD_ALARM);
108	        String subRestore  = _buildTopic(MQTT_TOPIC_CMD_ALARM_RESTORE);
109	        String subControl  = _buildTopic(MQTT_TOPIC_CMD_CONTROL);
110	        String subProv     = _buildTopic(MQTT_TOPIC_CMD_PROVISION_WIFI);
111
112	        _mqtt.subscribe(subAlarm.c_str(), 1);
113	        _mqtt.subscribe(subRestore.c_str(), 1);
114	        _mqtt.subscribe(subControl.c_str(), 1);
115	        _mqtt.subscribe(subProv.c_str(), 1);
116
117	        Serial.printf("  +订阅: %s\n", subAlarm.c_str());
118	        Serial.printf("  +订阅: %s\n", subRestore.c_str());
119	        Serial.printf("  +订阅: %s\n", subControl.c_str());
120	        Serial.printf("  +订阅: %s\n", subProv.c_str());
121	    } else {
122	        _connected = false;
123	        Serial.printf("[MQTT] ❌ 连接失败, rc=%d\n", _mqtt.state());
124	    }
125
126	    return _connected;
127	}
128
129	void MQTTManager::disconnect() {
130	    if (_connected) {
131	        // 主动发布离线
132	        publishOffline();
133	        delay(100);
134	        _mqtt.disconnect();
135	        _connected = false;
136	    }
137	}
138
139	bool MQTTManager::isConnected() {
140	    return _connected && _mqtt.connected();
141	}
142
143	// ============================================================
144	// 主循环
145	// ============================================================
146
147	void MQTTManager::loop() {
148	    if (!_connected) {
149	        uint32_t now = millis();
150	        if (now - _lastReconnectAttempt >= _reconnectInterval) {
151	            _lastReconnectAttempt = now;
152	            if (connect()) {
153	                _reconnectInterval = MQTT_RECONNECT_MIN;
154	                publishOnline();  // 上线通知
155	            } else {
156	                _reconnectInterval = min(_reconnectInterval * 2,
157	                                        (uint32_t)MQTT_RECONNECT_MAX);
158	            }
159	        }
160	    } else {
161	        _mqtt.loop();
162	    }
163	}
164
165	// ============================================================
166	// 上行: 全量遥测上报 (§4.1)
167	// ============================================================
168
169	void MQTTManager::publishAllPoints(const TelemetryData& d, uint32_t sequence) {
170	    if (!isConnected()) return;
171
172	    // 构建时间字符串
173	    struct tm timeinfo;
174	    char timeStr[24];
175	    if (getLocalTime(&timeinfo)) {
176	        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
177	    } else {
178	        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
179	    }
180
181	    // 构建 JSON (协议规范 V0.08)
182	    JsonDocument doc;
183	    doc["gateid"]   = d.gateid;
184	    doc["type"]     = "report";
185	    doc["time"]     = timeStr;
186	    doc["sequence"] = String(sequence);
187	    doc["source"]   = "da";
188
189	    // meter.id = gateid + 后缀 (如 10100001001 → 10100001001001)
190	    String meterId = String(d.gateid) + "001";
191
192	    JsonArray meter = doc["meter"].to<JsonArray>();
193	    JsonObject m    = meter.add<JsonObject>();
194	    m["id"]     = meterId;
195	    m["status"] = "1";
196	    m["name"]   = d.meterName;
197	    m["type"]   = d.deviceType;   // 设备类型枚举
198
199	    JsonObject v = m["values"].to<JsonObject>();
200	    // 环境数据
201	    v["Temp"]    = String(d.temp, 1);
202	    v["Humi"]    = String(d.humi, 1);
203	    v["DhtValid"] = d.dhtValid ? "1" : "0";
204	    // 雷达存在/运动
205	    v["Presence"]  = d.presence ? "1" : "0";
206	    v["Motion"]    = d.motion ? "1" : "0";
207	    v["Fall"]      = d.fall ? "1" : "0";
208	    v["Resident"]  = d.resident ? "1" : "0";
209	    // 生命体征
210	    v["Body"]       = String(d.body);
211	    v["SleepState"] = String(d.sleepState);
212	    v["SleepScore"] = String(d.sleepScore);
213	    v["HeartRate"]  = String(d.heartRate);
214	    v["BreathRate"] = String(d.breathRate);
215	    v["RadarValid"] = d.radarValid ? "1" : "0";
216	    v["VitalValid"] = d.vitalValid ? "1" : "0";
217	    // 水浸
218	    v["RainDO"]    = d.rainDO ? "1" : "0";
219	    v["RainValid"] = d.rainValid ? "1" : "0";
220	    // 设备状态
221	    v["WifiReady"]   = d.wifiReady ? "1" : "0";
222	    v["UptimeSec"]   = String(d.uptimeSec);
223	    v["BuzzerState"] = d.buzzerState ? "1" : "0";
224
225	    // 序列化
226	    String payload;
227	    serializeJson(doc, payload);
228
229	    // 发布到 report/allpoints (QoS 0)
230	    _mqtt.publish("report/allpoints", payload.c_str());
231	}
232
233	// ============================================================
234	// 上行: 上线通知 (§4.2)
235	// ============================================================
236
237	void MQTTManager::publishOnline() {
238	    if (!isConnected()) return;
239
240	    struct tm timeinfo;
241	    char timeStr[24];
242	    if (getLocalTime(&timeinfo)) {
243	        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
244	    } else {
245	        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
246	    }
247
248	    JsonDocument doc;
249	    doc["gateid"]          = _gateid;
250	    doc["type"]            = "online";
251	    doc["time"]            = timeStr;
252	    doc["firmwareVersion"] = FW_VERSION;
253	    doc["wifiSignal"]      = String(WiFi.RSSI());
254
255	    JsonArray devList = doc["deviceList"].to<JsonArray>();
256	    devList.add(METER_ID_DEFAULT);
257
258	    String payload;
259	    serializeJson(doc, payload);
260
261	    _publish(MQTT_TOPIC_STATUS_ONLINE, payload.c_str(), STATUS_QOS);
262	}
263
264	// ============================================================
265	// 上行: 离线通知 (§4.3)
266	// ============================================================
267
268	void MQTTManager::publishOffline() {
269	    struct tm timeinfo;
270	    char timeStr[24];
271	    if (getLocalTime(&timeinfo)) {
272	        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
273	    } else {
274	        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
275	    }
276
277	    JsonDocument doc;
278	    doc["gateid"] = _gateid;
279	    doc["type"]   = "offline";
280	    doc["time"]   = timeStr;
281
282	    String payload;
283	    serializeJson(doc, payload);
284
285	    _publish(MQTT_TOPIC_STATUS_OFFLINE, payload.c_str(), STATUS_QOS);
286	}
287
288	// ============================================================
289	// 上行: 配网状态通知 (§4.4)
290	// ============================================================
291
292	void MQTTManager::publishProvisionStatus(const char* state, const char* bleName) {
293	    struct tm timeinfo;
294	    char timeStr[24];
295	    if (getLocalTime(&timeinfo)) {
296	        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
297	    } else {
298	        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
299	    }
300
301	    JsonDocument doc;
302	    doc["gateid"]  = _gateid;
303	    doc["type"]    = "provision_status";
304	    doc["time"]    = timeStr;
305	    doc["state"]   = state;
306	    doc["bleName"] = bleName ? bleName : "";
307
308	    String payload;
309	    serializeJson(doc, payload);
310
311	    _publish(MQTT_TOPIC_STATUS_PROVISION, payload.c_str(), STATUS_QOS);
312	}
313
314	// ============================================================
315	// 上行: 指令执行响应 (§4.5)
316	// ============================================================
317
318	void MQTTManager::publishCmdResponse(const char* cmdId, const char* cmdType,
319	                                      const char* alarmType, bool success,
320	                                      const char* errorCode, const char* errorMsg) {
321	    struct tm timeinfo;
322	    char timeStr[24];
323	    if (getLocalTime(&timeinfo)) {
324	        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
325	    } else {
326	        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
327	    }
328
329	    JsonDocument doc;
330	    doc["gateid"]    = _gateid;
331	    doc["type"]      = "cmd_response";
332	    doc["time"]      = timeStr;
333	    doc["cmdId"]     = cmdId;
334	    doc["cmdType"]   = cmdType;     // alarm / alarm_restore / control
335	    doc["alarmType"] = alarmType ? alarmType : "";
336	    doc["result"]    = success ? "success" : "fail";
337	    doc["errorCode"] = errorCode;
338	    doc["errorMsg"]  = errorMsg;
339
340	    String payload;
341	    serializeJson(doc, payload);
342
343	    _publish(MQTT_TOPIC_RESPONSE_CMD, payload.c_str(), STATUS_QOS);
344	}
345
346	// ============================================================
347	// 上行: 配网指令响应 (§4.6)
348	// ============================================================
349
350	void MQTTManager::publishProvisionResponse(const char* cmdId, bool success,
351	                                            const char* errorCode, const char* errorMsg,
352	                                            const char* wifiSignal) {
353	    struct tm timeinfo;
354	    char timeStr[24];
355	    if (getLocalTime(&timeinfo)) {
356	        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
357	    } else {
358	        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
359	    }
360
361	    JsonDocument doc;
362	    doc["gateid"]    = _gateid;
363	    doc["type"]      = "provision_response";
364	    doc["time"]      = timeStr;
365	    doc["cmdId"]     = cmdId;
366	    doc["result"]    = success ? "success" : "fail";
367	    doc["errorCode"] = errorCode;
368	    doc["errorMsg"]  = errorMsg;
369	    if (success && wifiSignal && strlen(wifiSignal) > 0) {
370	        doc["wifiSignal"] = wifiSignal;
371	    }
372
373	    String payload;
374	    serializeJson(doc, payload);
375
376	    _publish(MQTT_TOPIC_RESPONSE_PROVISION, payload.c_str(), STATUS_QOS);
377	}
378
379	// ============================================================
380	// 上行: 本地报警上报 (§4.7 扩展)
381	// 设备本地判定报警后立即上报，格式兼容服务端告警协议
382	// ============================================================
383
384	void MQTTManager::publishLocalAlarm(uint8_t alarmType, bool triggered, const char* desc) {
385	    if (!isConnected()) return;
386
387	    struct tm timeinfo;
388	    char timeStr[24];
389	    if (getLocalTime(&timeinfo)) {
390	        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
391	    } else {
392	        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
393	    }
394
395	    // 类型映射
396	    const char* typeStr = "unknown";
397	    switch (alarmType) {
398	    case 1:  typeStr = "fall";             break;  // ALARM_FALL
399	    case 2:  typeStr = "water_leak";       break;  // ALARM_WATER_LEAK
400	    case 3:  typeStr = "heart_abnormal";   break;  // ALARM_HEART_ABNORMAL
401	    case 4:  typeStr = "breath_abnormal";  break;  // ALARM_BREATH_ABNORMAL
402	    case 5:  typeStr = "temp_abnormal";    break;  // ALARM_TEMP_ABNORMAL
403	    case 6:  typeStr = "humi_abnormal";    break;  // ALARM_HUMI_ABNORMAL
404	    case 7:  typeStr = "sensor_fault";     break;  // ALARM_SENSOR_FAULT
405	    default: typeStr = "unknown";          break;
406	    }
407
408	    JsonDocument doc;
409	    doc["gateid"]  = _gateid;
410	    doc["type"]    = triggered ? "alarm" : "alarm_restore";
411	    doc["time"]    = timeStr;
412	    doc["alarmType"] = typeStr;
413	    doc["source"]  = "local";   // 标识本地判定
414	    doc["desc"]    = desc ? desc : "";
415
416	    String payload;
417	    serializeJson(doc, payload);
418
419	    // 通过 status 通道上报本地报警
420	    String topic = _buildTopic("status/%s/alarm");
421	    _mqtt.publish(topic.c_str(), payload.c_str());
422	}
423
424	// ============================================================
425	// 回调注册
426	// ============================================================
427
428	void MQTTManager::onAlarmCommand(MQTTAlarmCallback cb)     { _alarmCb = cb; }
429	void MQTTManager::onControlCommand(MQTTControlCallback cb)  { _controlCb = cb; }
430	void MQTTManager::onProvisionCommand(MQTTProvisionCallback cb) { _provisionCb = cb; }
431
432	// ============================================================
433	// 静态 MQTT 回调
434	// ============================================================
435
436	void MQTTManager::_mqttCallback(char* topic, byte* payload, unsigned int length) {
437	    char msg[512];
438	    unsigned int l = min(length, (unsigned int)(sizeof(msg) - 1));
439	    memcpy(msg, payload, l);
440	    msg[l] = '\0';
441
442	    Serial.printf("[MQTT] 📩 [%s]\n", topic);
443	    // Serial.println(msg);  // 完整打印在 debug 时打开
444
445	    if (_instance) {
446	        _instance->_handleMessage(topic, msg);
447	    }
448	}
449
450	// ============================================================
451	// 下行报文路由
452	// ============================================================
453
454	void MQTTManager::_handleMessage(const char* topic, const char* payload) {
455	    // 判断 topic 属于哪种指令
456	    String t(topic);
457
458	    if (t.indexOf("/alarm/restore") > 0) {
459	        _parseAlarm(payload, true);
460	    } else if (t.indexOf("/alarm") > 0) {
461	        _parseAlarm(payload, false);
462	    } else if (t.indexOf("/control") > 0) {
463	        _parseControl(payload);
464	    } else if (t.indexOf("/provision/wifi") > 0) {
465	        _parseProvision(payload);
466	    } else {
467	        Serial.printf("[MQTT] ⚠ 未识别的主题: %s\n", topic);
468	    }
469	}
470
471	// ============================================================
472	// 解析告警指令 (§5.1, §5.2)
473	// ============================================================
474
475	void MQTTManager::_parseAlarm(const char* payload, bool isRestore) {
476	    JsonDocument doc;
477	    DeserializationError err = deserializeJson(doc, payload);
478	    if (err) {
479	        Serial.printf("[MQTT] 告警指令 JSON 解析失败: %s\n", err.c_str());
480	        return;
481	    }
482
483	    AlarmCommand cmd;
484	    memset(&cmd, 0, sizeof(cmd));
485
486	    strncpy(cmd.cmdId,   doc["cmdId"]   | "", sizeof(cmd.cmdId) - 1);
487	    strncpy(cmd.type,    doc["type"]    | (isRestore ? "alarm_restore" : "alarm"), sizeof(cmd.type) - 1);
488	    strncpy(cmd.alarmType, doc["alarmType"] | "", sizeof(cmd.alarmType) - 1);
489	    strncpy(cmd.alarmLevel, doc["alarmLevel"] | "medium", sizeof(cmd.alarmLevel) - 1);
490	    strncpy(cmd.alarmDesc, doc["alarmDesc"] | "", sizeof(cmd.alarmDesc) - 1);
491	    strncpy(cmd.meterId, doc["meterId"] | "", sizeof(cmd.meterId) - 1);
492	    strncpy(cmd.relatedCmdId, doc["relatedCmdId"] | "", sizeof(cmd.relatedCmdId) - 1);
493
494	    JsonObject action = doc["action"];
495	    if (!action.isNull()) {
496	        cmd.buzzerOn       = action["buzzer"] | false;
497	        cmd.buzzerDuration = action["buzzerDuration"] | 0;
498	        cmd.nightLight     = action["nightLight"] | false;
499	    }
500	    cmd.timestamp = millis();
501
502	    Serial.printf("[MQTT] 🚨 告警指令: type=%s alarmType=%s buzzer=%d\n",
503	                  cmd.type, cmd.alarmType, cmd.buzzerOn);
504
505	    if (_alarmCb) {
506	        _alarmCb(cmd);
507	    }
508	}
509
510	// ============================================================
511	// 解析控制指令 (§5.3)
512	// ============================================================
513
514	void MQTTManager::_parseControl(const char* payload) {
515	    JsonDocument doc;
516	    DeserializationError err = deserializeJson(doc, payload);
517	    if (err) {
518	        Serial.printf("[MQTT] 控制指令 JSON 解析失败: %s\n", err.c_str());
519	        return;
520	    }
521
522	    ControlCommand cmd;
523	    memset(&cmd, 0, sizeof(cmd));
524
525	    strncpy(cmd.cmdId,        doc["cmdId"]        | "", sizeof(cmd.cmdId) - 1);
526	    strncpy(cmd.type,         doc["type"]         | "control", sizeof(cmd.type) - 1);
527	    strncpy(cmd.controlType,  doc["controlType"]  | "", sizeof(cmd.controlType) - 1);
528
529	    JsonObject params = doc["params"];
530	    if (!params.isNull()) {
531	        cmd.params_duration = params["duration"] | 0;
532	        cmd.params_interval = params["interval"] | 0;
533	    }
534	    cmd.timestamp = millis();
535
536	    Serial.printf("[MQTT] 🎛 控制指令: %s\n", cmd.controlType);
537
538	    if (_controlCb) {
539	        _controlCb(cmd);
540	    }
541	}
542
543	// ============================================================
544	// 解析配网指令 (§5.4)
545	// ============================================================
546
547	void MQTTManager::_parseProvision(const char* payload) {
548	    JsonDocument doc;
549	    DeserializationError err = deserializeJson(doc, payload);
550	    if (err) {
551	        Serial.printf("[MQTT] 配网指令 JSON 解析失败: %s\n", err.c_str());
552	        return;
553	    }
554
555	    ProvisionCommand cmd;
556	    memset(&cmd, 0, sizeof(cmd));
557
558	    strncpy(cmd.cmdId, doc["cmdId"] | "", sizeof(cmd.cmdId) - 1);
559
560	    JsonObject wifi = doc["wifi"];
561	    if (!wifi.isNull()) {
562	        strncpy(cmd.wifi_ssid,     wifi["ssid"]       | "", sizeof(cmd.wifi_ssid) - 1);
563	        strncpy(cmd.wifi_password, wifi["password"]   | "", sizeof(cmd.wifi_password) - 1);
564	        strncpy(cmd.wifi_encrypt,  wifi["encryptType"] | "WPA2", sizeof(cmd.wifi_encrypt) - 1);
565	    }
566
567	    JsonObject mqtt = doc["mqtt"];
568	    if (!mqtt.isNull()) {
569	        strncpy(cmd.mqtt_broker, mqtt["broker"] | "", sizeof(cmd.mqtt_broker) - 1);
570	        cmd.mqtt_port  = mqtt["port"]   | 1883;
571	        cmd.mqtt_useTls = mqtt["useTls"] | false;
572	    }
573	    cmd.timestamp = millis();
574
575	    Serial.printf("[MQTT] 📶 配网指令: SSID=%s Broker=%s:%d\n",
576	                  cmd.wifi_ssid, cmd.mqtt_broker, cmd.mqtt_port);
577
578	    if (_provisionCb) {
579	        _provisionCb(cmd);
580	    }
581	}
582
583	// ============================================================
584	// 辅助方法
585	// ============================================================
586
587	String MQTTManager::_buildTopic(const char* tmpl) const {
588	    char buf[128];
589	    snprintf(buf, sizeof(buf), tmpl, _gateid);
590	    return String(buf);
591	}
592
593	bool MQTTManager::_publish(const char* topicTmpl, const char* payload,
594	                            uint8_t qos, bool retain) {
595	    if (!isConnected()) return false;
596	    String topic = _buildTopic(topicTmpl);
597	    bool ok = _mqtt.publish(topic.c_str(), payload, retain);
598	    if (!ok) {
599	        Serial.printf("[MQTT] ⚠ 发布失败: %s\n", topic.c_str());
600	    }
601	    return ok;
602	}
603

/**
 * @file mqtt_manager.cpp
 * @brief MQTT 通信管理器实现 (协议规范 V0.08)
 */

#include "mqtt_manager.h"

MQTTManager* MQTTManager::_instance = nullptr;

// ============================================================
// 构造函数 & 析构函数
// ============================================================

MQTTManager::MQTTManager()
    : _mqtt(_wifiClient)
    , _port(1883)
    , _reconnectInterval(MQTT_RECONNECT_MIN)
    , _lastReconnectAttempt(0)
    , _connected(false)
    , _alarmCb(nullptr)
    , _controlCb(nullptr)
    , _provisionCb(nullptr)
{
    memset(_gateid, 0, sizeof(_gateid));
    memset(_server, 0, sizeof(_server));
    memset(_user,   0, sizeof(_user));
    memset(_pass,   0, sizeof(_pass));
    _instance = this;
}

MQTTManager::~MQTTManager() {
    disconnect();
    _instance = nullptr;
}

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

bool MQTTManager::begin(const char* gateid) {
    strncpy(_gateid, gateid, sizeof(_gateid) - 1);

    Serial.println(F("[MQTT] 初始化 (协议 V0.08)"));
    Serial.printf("  网关ID: %s\n", _gateid);
    Serial.printf("  服务器: %s:%d\n", _server, _port);

    _mqtt.setServer(_server, _port);
    _mqtt.setCallback(_mqttCallback);
    _mqtt.setKeepAlive(MQTT_KEEPALIVE);
    _mqtt.setBufferSize(1024);  // JSON 报文较大

    return true;
}

// ============================================================
// 设置服务器
// ============================================================

void MQTTManager::setServer(const char* host, uint16_t port,
                             const char* user, const char* pass) {
    strncpy(_server, host, sizeof(_server) - 1);
    _port = port;
    strncpy(_user, user, sizeof(_user) - 1);
    strncpy(_pass, pass, sizeof(_pass) - 1);
    _mqtt.setServer(_server, _port);
}

// ============================================================
// 连接 & 断开
// ============================================================

bool MQTTManager::connect() {
    if (_connected && _mqtt.connected()) return true;
    if (!WiFi.isConnected()) return false;

    Serial.printf("[MQTT] 连接 %s:%d ...\n", _server, _port);

    // Client ID = gateid
    String clientId = _gateid;

    // 遗嘱消息 (§4.3): status/{gateid}/offline
    String willTopic = _buildTopic(MQTT_TOPIC_STATUS_OFFLINE);
    JsonDocument willDoc;
    willDoc["gateid"] = _gateid;
    willDoc["type"]   = "offline";
    willDoc["time"]   = "";  // Broker 不会填充时间
    String willPayload;
    serializeJson(willDoc, willPayload);

    bool hasAuth = (strlen(_user) > 0);
    bool ok = false;

    if (hasAuth) {
        ok = _mqtt.connect(clientId.c_str(),
                          _user, _pass,
                          willTopic.c_str(), 1, true, willPayload.c_str());
    } else {
        ok = _mqtt.connect(clientId.c_str(),
                          willTopic.c_str(), 1, true, willPayload.c_str());
    }

    if (ok) {
        _connected = true;
        Serial.println(F("[MQTT] ✅ 已连接!"));

        // 订阅下行主题 (§3.2)
        String subAlarm    = _buildTopic(MQTT_TOPIC_CMD_ALARM);
        String subRestore  = _buildTopic(MQTT_TOPIC_CMD_ALARM_RESTORE);
        String subControl  = _buildTopic(MQTT_TOPIC_CMD_CONTROL);
        String subProv     = _buildTopic(MQTT_TOPIC_CMD_PROVISION_WIFI);

        _mqtt.subscribe(subAlarm.c_str(), 1);
        _mqtt.subscribe(subRestore.c_str(), 1);
        _mqtt.subscribe(subControl.c_str(), 1);
        _mqtt.subscribe(subProv.c_str(), 1);

        Serial.printf("  +订阅: %s\n", subAlarm.c_str());
        Serial.printf("  +订阅: %s\n", subRestore.c_str());
        Serial.printf("  +订阅: %s\n", subControl.c_str());
        Serial.printf("  +订阅: %s\n", subProv.c_str());
    } else {
        _connected = false;
        Serial.printf("[MQTT] ❌ 连接失败, rc=%d\n", _mqtt.state());
    }

    return _connected;
}

void MQTTManager::disconnect() {
    if (_connected) {
        // 主动发布离线
        publishOffline();
        delay(100);
        _mqtt.disconnect();
        _connected = false;
    }
}

bool MQTTManager::isConnected() {
    return _connected && _mqtt.connected();
}

// ============================================================
// 主循环
// ============================================================

void MQTTManager::loop() {
    if (!_connected) {
        uint32_t now = millis();
        if (now - _lastReconnectAttempt >= _reconnectInterval) {
            _lastReconnectAttempt = now;
            if (connect()) {
                _reconnectInterval = MQTT_RECONNECT_MIN;
                publishOnline();  // 上线通知
            } else {
                _reconnectInterval = min(_reconnectInterval * 2,
                                        (uint32_t)MQTT_RECONNECT_MAX);
            }
        }
    } else {
        _mqtt.loop();
    }
}

// ============================================================
// 上行: 全量遥测上报 (§4.1)
// ============================================================

void MQTTManager::publishAllPoints(const TelemetryData& d, uint32_t sequence) {
    if (!isConnected()) return;

    // 构建时间字符串
    struct tm timeinfo;
    char timeStr[24];
    if (getLocalTime(&timeinfo)) {
        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
    } else {
        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
    }

    // 构建 JSON (协议规范 V0.08)
    JsonDocument doc;
    doc["gateid"]   = d.gateid;
    doc["type"]     = "report";
    doc["time"]     = timeStr;
    doc["sequence"] = String(sequence);
    doc["source"]   = "da";

    // meter.id = gateid + 后缀 (如 10100001001 → 10100001001001)
    String meterId = String(d.gateid) + "001";

    JsonArray meter = doc["meter"].to<JsonArray>();
    JsonObject m    = meter.add<JsonObject>();
    m["id"]     = meterId;
    m["status"] = "1";
    m["name"]   = d.meterName;
    m["type"]   = d.deviceType;   // 设备类型枚举

    JsonObject v = m["values"].to<JsonObject>();
    // 环境数据
    v["Temp"]    = String(d.temp, 1);
    v["Humi"]    = String(d.humi, 1);
    v["DhtValid"] = d.dhtValid ? "1" : "0";
    // 雷达存在/运动
    v["Presence"]  = d.presence ? "1" : "0";
    v["Motion"]    = d.motion ? "1" : "0";
    v["Fall"]      = d.fall ? "1" : "0";
    v["Resident"]  = d.resident ? "1" : "0";
    // 生命体征
    v["Body"]       = String(d.body);
    v["SleepState"] = String(d.sleepState);
    v["SleepScore"] = String(d.sleepScore);
    v["HeartRate"]  = String(d.heartRate);
    v["BreathRate"] = String(d.breathRate);
    v["RadarValid"] = d.radarValid ? "1" : "0";
    v["VitalValid"] = d.vitalValid ? "1" : "0";
    // 水浸
    v["RainDO"]    = d.rainDO ? "1" : "0";
    v["RainValid"] = d.rainValid ? "1" : "0";
    // 设备状态
    v["WifiReady"]   = d.wifiReady ? "1" : "0";
    v["UptimeSec"]   = String(d.uptimeSec);
    v["BuzzerState"] = d.buzzerState ? "1" : "0";

    // 序列化
    String payload;
    serializeJson(doc, payload);

    // 发布到 report/allpoints (QoS 0)
    _mqtt.publish("report/allpoints", payload.c_str());
}

// ============================================================
// 上行: 上线通知 (§4.2)
// ============================================================

void MQTTManager::publishOnline() {
    if (!isConnected()) return;

    struct tm timeinfo;
    char timeStr[24];
    if (getLocalTime(&timeinfo)) {
        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
    } else {
        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
    }

    JsonDocument doc;
    doc["gateid"]          = _gateid;
    doc["type"]            = "online";
    doc["time"]            = timeStr;
    doc["firmwareVersion"] = FW_VERSION;
    doc["wifiSignal"]      = String(WiFi.RSSI());

    JsonArray devList = doc["deviceList"].to<JsonArray>();
    devList.add(METER_ID_DEFAULT);

    String payload;
    serializeJson(doc, payload);

    _publish(MQTT_TOPIC_STATUS_ONLINE, payload.c_str(), STATUS_QOS);
}

// ============================================================
// 上行: 离线通知 (§4.3)
// ============================================================

void MQTTManager::publishOffline() {
    struct tm timeinfo;
    char timeStr[24];
    if (getLocalTime(&timeinfo)) {
        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
    } else {
        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
    }

    JsonDocument doc;
    doc["gateid"] = _gateid;
    doc["type"]   = "offline";
    doc["time"]   = timeStr;

    String payload;
    serializeJson(doc, payload);

    _publish(MQTT_TOPIC_STATUS_OFFLINE, payload.c_str(), STATUS_QOS);
}

// ============================================================
// 上行: 配网状态通知 (§4.4)
// ============================================================

void MQTTManager::publishProvisionStatus(const char* state, const char* bleName) {
    struct tm timeinfo;
    char timeStr[24];
    if (getLocalTime(&timeinfo)) {
        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
    } else {
        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
    }

    JsonDocument doc;
    doc["gateid"]  = _gateid;
    doc["type"]    = "provision_status";
    doc["time"]    = timeStr;
    doc["state"]   = state;
    doc["bleName"] = bleName ? bleName : "";

    String payload;
    serializeJson(doc, payload);

    _publish(MQTT_TOPIC_STATUS_PROVISION, payload.c_str(), STATUS_QOS);
}

// ============================================================
// 上行: 指令执行响应 (§4.5)
// ============================================================

void MQTTManager::publishCmdResponse(const char* cmdId, const char* cmdType,
                                      const char* alarmType, bool success,
                                      const char* errorCode, const char* errorMsg) {
    struct tm timeinfo;
    char timeStr[24];
    if (getLocalTime(&timeinfo)) {
        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
    } else {
        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
    }

    JsonDocument doc;
    doc["gateid"]    = _gateid;
    doc["type"]      = "cmd_response";
    doc["time"]      = timeStr;
    doc["cmdId"]     = cmdId;
    doc["cmdType"]   = cmdType;     // alarm / alarm_restore / control
    doc["alarmType"] = alarmType ? alarmType : "";
    doc["result"]    = success ? "success" : "fail";
    doc["errorCode"] = errorCode;
    doc["errorMsg"]  = errorMsg;

    String payload;
    serializeJson(doc, payload);

    _publish(MQTT_TOPIC_RESPONSE_CMD, payload.c_str(), STATUS_QOS);
}

// ============================================================
// 上行: 配网指令响应 (§4.6)
// ============================================================

void MQTTManager::publishProvisionResponse(const char* cmdId, bool success,
                                            const char* errorCode, const char* errorMsg,
                                            const char* wifiSignal) {
    struct tm timeinfo;
    char timeStr[24];
    if (getLocalTime(&timeinfo)) {
        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
    } else {
        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
    }

    JsonDocument doc;
    doc["gateid"]    = _gateid;
    doc["type"]      = "provision_response";
    doc["time"]      = timeStr;
    doc["cmdId"]     = cmdId;
    doc["result"]    = success ? "success" : "fail";
    doc["errorCode"] = errorCode;
    doc["errorMsg"]  = errorMsg;
    if (success && wifiSignal && strlen(wifiSignal) > 0) {
        doc["wifiSignal"] = wifiSignal;
    }

    String payload;
    serializeJson(doc, payload);

    _publish(MQTT_TOPIC_RESPONSE_PROVISION, payload.c_str(), STATUS_QOS);
}

// ============================================================
// 上行: 本地报警上报 (§4.7 扩展)
// 设备本地判定报警后立即上报，格式兼容服务端告警协议
// ============================================================

void MQTTManager::publishLocalAlarm(uint8_t alarmType, bool triggered, const char* desc) {
    if (!isConnected()) return;

    struct tm timeinfo;
    char timeStr[24];
    if (getLocalTime(&timeinfo)) {
        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
    } else {
        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
    }

    // 类型映射
    const char* typeStr = "unknown";
    switch (alarmType) {
    case 1:  typeStr = "fall";             break;  // ALARM_FALL
    case 2:  typeStr = "water_leak";       break;  // ALARM_WATER_LEAK
    case 3:  typeStr = "heart_abnormal";   break;  // ALARM_HEART_ABNORMAL
    case 4:  typeStr = "breath_abnormal";  break;  // ALARM_BREATH_ABNORMAL
    case 5:  typeStr = "temp_abnormal";    break;  // ALARM_TEMP_ABNORMAL
    case 6:  typeStr = "humi_abnormal";    break;  // ALARM_HUMI_ABNORMAL
    case 7:  typeStr = "sensor_fault";     break;  // ALARM_SENSOR_FAULT
    default: typeStr = "unknown";          break;
    }

    JsonDocument doc;
    doc["gateid"]  = _gateid;
    doc["type"]    = triggered ? "alarm" : "alarm_restore";
    doc["time"]    = timeStr;
    doc["alarmType"] = typeStr;
    doc["source"]  = "local";   // 标识本地判定
    doc["desc"]    = desc ? desc : "";

    String payload;
    serializeJson(doc, payload);

    // 通过 status 通道上报本地报警
    String topic = _buildTopic("status/%s/alarm");
    _mqtt.publish(topic.c_str(), payload.c_str());
}

// ============================================================
// 回调注册
// ============================================================

void MQTTManager::onAlarmCommand(MQTTAlarmCallback cb)     { _alarmCb = cb; }
void MQTTManager::onControlCommand(MQTTControlCallback cb)  { _controlCb = cb; }
void MQTTManager::onProvisionCommand(MQTTProvisionCallback cb) { _provisionCb = cb; }

// ============================================================
// 静态 MQTT 回调
// ============================================================

void MQTTManager::_mqttCallback(char* topic, byte* payload, unsigned int length) {
    char msg[512];
    unsigned int l = min(length, (unsigned int)(sizeof(msg) - 1));
    memcpy(msg, payload, l);
    msg[l] = '\0';

    Serial.printf("[MQTT] 📩 [%s]\n", topic);
    // Serial.println(msg);  // 完整打印在 debug 时打开

    if (_instance) {
        _instance->_handleMessage(topic, msg);
    }
}

// ============================================================
// 下行报文路由
// ============================================================

void MQTTManager::_handleMessage(const char* topic, const char* payload) {
    // 判断 topic 属于哪种指令
    String t(topic);

    if (t.indexOf("/alarm/restore") > 0) {
        _parseAlarm(payload, true);
    } else if (t.indexOf("/alarm") > 0) {
        _parseAlarm(payload, false);
    } else if (t.indexOf("/control") > 0) {
        _parseControl(payload);
    } else if (t.indexOf("/provision/wifi") > 0) {
        _parseProvision(payload);
    } else {
        Serial.printf("[MQTT] ⚠ 未识别的主题: %s\n", topic);
    }
}

// ============================================================
// 解析告警指令 (§5.1, §5.2)
// ============================================================

void MQTTManager::_parseAlarm(const char* payload, bool isRestore) {
    JsonDocument doc;
    DeserializationError err = deserializeJson(doc, payload);
    if (err) {
        Serial.printf("[MQTT] 告警指令 JSON 解析失败: %s\n", err.c_str());
        return;
    }

    AlarmCommand cmd;
    memset(&cmd, 0, sizeof(cmd));

    strncpy(cmd.cmdId,   doc["cmdId"]   | "", sizeof(cmd.cmdId) - 1);
    strncpy(cmd.type,    doc["type"]    | (isRestore ? "alarm_restore" : "alarm"), sizeof(cmd.type) - 1);
    strncpy(cmd.alarmType, doc["alarmType"] | "", sizeof(cmd.alarmType) - 1);
    strncpy(cmd.alarmLevel, doc["alarmLevel"] | "medium", sizeof(cmd.alarmLevel) - 1);
    strncpy(cmd.alarmDesc, doc["alarmDesc"] | "", sizeof(cmd.alarmDesc) - 1);
    strncpy(cmd.meterId, doc["meterId"] | "", sizeof(cmd.meterId) - 1);
    strncpy(cmd.relatedCmdId, doc["relatedCmdId"] | "", sizeof(cmd.relatedCmdId) - 1);

    JsonObject action = doc["action"];
    if (!action.isNull()) {
        cmd.buzzerOn       = action["buzzer"] | false;
        cmd.buzzerDuration = action["buzzerDuration"] | 0;
        cmd.nightLight     = action["nightLight"] | false;
    }
    cmd.timestamp = millis();

    Serial.printf("[MQTT] 🚨 告警指令: type=%s alarmType=%s buzzer=%d\n",
                  cmd.type, cmd.alarmType, cmd.buzzerOn);

    if (_alarmCb) {
        _alarmCb(cmd);
    }
}

// ============================================================
// 解析控制指令 (§5.3)
// ============================================================

void MQTTManager::_parseControl(const char* payload) {
    JsonDocument doc;
    DeserializationError err = deserializeJson(doc, payload);
    if (err) {
        Serial.printf("[MQTT] 控制指令 JSON 解析失败: %s\n", err.c_str());
        return;
    }

    ControlCommand cmd;
    memset(&cmd, 0, sizeof(cmd));

    strncpy(cmd.cmdId,        doc["cmdId"]        | "", sizeof(cmd.cmdId) - 1);
    strncpy(cmd.type,         doc["type"]         | "control", sizeof(cmd.type) - 1);
    strncpy(cmd.controlType,  doc["controlType"]  | "", sizeof(cmd.controlType) - 1);

    JsonObject params = doc["params"];
    if (!params.isNull()) {
        cmd.params_duration = params["duration"] | 0;
        cmd.params_interval = params["interval"] | 0;
    }
    cmd.timestamp = millis();

    Serial.printf("[MQTT] 🎛 控制指令: %s\n", cmd.controlType);

    if (_controlCb) {
        _controlCb(cmd);
    }
}

// ============================================================
// 解析配网指令 (§5.4)
// ============================================================

void MQTTManager::_parseProvision(const char* payload) {
    JsonDocument doc;
    DeserializationError err = deserializeJson(doc, payload);
    if (err) {
        Serial.printf("[MQTT] 配网指令 JSON 解析失败: %s\n", err.c_str());
        return;
    }

    ProvisionCommand cmd;
    memset(&cmd, 0, sizeof(cmd));

    strncpy(cmd.cmdId, doc["cmdId"] | "", sizeof(cmd.cmdId) - 1);

    JsonObject wifi = doc["wifi"];
    if (!wifi.isNull()) {
        strncpy(cmd.wifi_ssid,     wifi["ssid"]       | "", sizeof(cmd.wifi_ssid) - 1);
        strncpy(cmd.wifi_password, wifi["password"]   | "", sizeof(cmd.wifi_password) - 1);
        strncpy(cmd.wifi_encrypt,  wifi["encryptType"] | "WPA2", sizeof(cmd.wifi_encrypt) - 1);
    }

    JsonObject mqtt = doc["mqtt"];
    if (!mqtt.isNull()) {
        strncpy(cmd.mqtt_broker, mqtt["broker"] | "", sizeof(cmd.mqtt_broker) - 1);
        cmd.mqtt_port  = mqtt["port"]   | 1883;
        cmd.mqtt_useTls = mqtt["useTls"] | false;
    }
    cmd.timestamp = millis();

    Serial.printf("[MQTT] 📶 配网指令: SSID=%s Broker=%s:%d\n",
                  cmd.wifi_ssid, cmd.mqtt_broker, cmd.mqtt_port);

    if (_provisionCb) {
        _provisionCb(cmd);
    }
}

// ============================================================
// 辅助方法
// ============================================================

String MQTTManager::_buildTopic(const char* tmpl) const {
    char buf[128];
    snprintf(buf, sizeof(buf), tmpl, _gateid);
    return String(buf);
}

bool MQTTManager::_publish(const char* topicTmpl, const char* payload,
                            uint8_t qos, bool retain) {
    if (!isConnected()) return false;
    String topic = _buildTopic(topicTmpl);
    bool ok = _mqtt.publish(topic.c_str(), payload, retain);
    if (!ok) {
        Serial.printf("[MQTT] ⚠ 发布失败: %s\n", topic.c_str());
    }
    return ok;
}


Now I have a clear picture. The user is asking "现在检测到无人，是发啥" — what is sent when no person is detected.

Looking at the code:

1. **Radar side** (`radar_manager.cpp`): When presence changes to 0 (after PRESENCE_HYSTERESIS=8 consecutive frames), `_data.presence` becomes `false`. After 10 seconds of no heart/breath data, `vitalValid` goes false and heart/breath rate are cleared to 0.

2. **buildTelemetry** (`main.cpp`): Just copies `radar.presence` directly to `t.presence`, and copies all other radar fields as-is.

3. **publishAllPoints** (`mqtt_manager.cpp`): Builds the JSON values. When presence=0:
   - `"Presence": "0"`
   - Heart/breath rate become 0 after 10 seconds, vitalValid becomes "0"
   - Other fields remain as-is (motion, fall, resident, etc.)

So when no person is detected, the device still sends `report/allpoints` every 1 second, with:
```json
{
  "gateid": "10100001001",
  "type": "report",
  "time": "2026-08-07 ...",
  "sequence": "N",
  "source": "da",
  "meter": [{
    "id": "10100001001001",
    "status": "1",
    "name": "RadarDHT11",
    "type": "bedroom",
    "values": {
      "Temp": "25.0",
      "Humi": "60.0",
      "DhtValid": "1",
      "Presence": "0",       ← 无人
      "Motion": "0",         ← 无运动
      "Fall": "0",
      "Resident": "0",
      "Body": "0",
      "SleepState": "0",
      "SleepScore": "0",
      "HeartRate": "0",      ← 10秒后归零
      "BreathRate": "0",     ← 10秒后归零
      "RadarValid": "1",
      "VitalValid": "0",     ← 体征失效
      "RainDO": "1",
      "RainValid": "1",
      "WifiReady": "1",
      "UptimeSec": "120",
      "BuzzerState": "0"
    }
  }]
}
```

There's no separate "无人" event or alarm — it's just a value change in the regular 1-second periodic telemetry. The summary says the user was asking this before, so let me answer concisely.


检测到无人时，**没有单独的告警事件**，只是在每秒上报的 `report/allpoints` 报文中把相关字段清零：

**核心变化：**

| 字段 | 有人时 | 无人时 |
|------|--------|--------|
| `Presence` | `"1"` | `"0"` |
| `Motion` | `"1"` 或 `"0"` | `"0"` |
| `HeartRate` | 实际值 | **10秒后归零** |
| `BreathRate` | 实际值 | **10秒后归零** |
| `VitalValid` | `"1"` | **10秒后变 `"0"`** |
| `RadarValid` | `"1"` | `"1"` (雷达本身还在工作) |

**关键逻辑链：**

1. **雷达防抖** ([radar_manager.cpp:317-327](src/radar_manager.cpp#L317-L327))：连续 `PRESENCE_HYSTERESIS=8` 帧都报告无人，`presence` 才从 `1` 翻成 `0`
2. **体征过期** ([radar_manager.cpp:80-87](src/radar_manager.cpp#L80-L87))：心率/呼吸超过 10 秒无更新 → `vitalValid=false`，`heartRate/breathRate` 清零
3. **报警自动解除** ([main.cpp:343-347](src/main.cpp#L343-L347))：心率/呼吸异常报警在 `!humanPresent` 时自动清除

**实际报文示例（无人时）：**
```json
{
  "gateid": "10100001001",
  "type": "report",
  "meter": [{
    "values": {
      "Presence": "0",
      "Motion": "0",
      "Fall": "0",
      "HeartRate": "0",
      "BreathRate": "0",
      "VitalValid": "0",
      ...
    }
  }]
}
```

总结：**无人 = 正常遥测继续上报，Presence 变 `"0"`，体征数据 10 秒后过期清零，不会产生任何告警。**


还有就是，睡觉的评分，是只会是整数吗，有小数吗


The user is asking whether the sleep score from the R60ABD1 radar is only integers or can have decimals.

Let me look at the code to check how sleepScore is extracted and stored.

From what I've already read:

1. In `radar_manager.h` line 25: `uint8_t sleepScore;` — it's defined as a `uint8_t` (unsigned 8-bit integer), so it can only hold 0-255 as an integer.

2. In `radar_manager.cpp` line 383: `_data.sleepScore = frame.data[2];` — it's read directly from a single byte of the radar frame, so it's inherently an integer.

3. In `config.h` line 198: `uint8_t sleepScore;` — also uint8_t in the TelemetryData struct.

4. In `mqtt_manager.cpp` line 212: `v["SleepScore"] = String(d.sleepScore);` — it's sent as a string of the integer value.

So the answer is: the sleep score is always an integer (0-100), coming from a single byte in the radar protocol. There are no decimals.

Let me verify by checking the radar protocol header too.


从代码来看，睡眠评分**只有整数，没有小数**。

证据在三个地方：

1. **雷达协议层** — 数据来自雷达帧的单个字节 ([radar_manager.cpp:383](src/radar_manager.cpp#L383))：
   ```cpp
   _data.sleepScore = frame.data[2];   // 0~100
   ```
   一个字节只能是整数 0~255。

2. **数据结构定义** — 两处都是 `uint8_t`（无符号 8 位整数）：
   - [radar_manager.h:25](include/radar_manager.h#L25)：`uint8_t sleepScore;`
   - [config.h:198](include/config.h#L198)：`uint8_t sleepScore;`

3. **MQTT 上报** ([mqtt_manager.cpp:212](src/mqtt_manager.cpp#L212))：
   ```cpp
   v["SleepScore"] = String(d.sleepScore);
   ```
   直接转整数为字符串，不会出现小数点。

所以评分范围是 **0~100 的整数**，R60ABD1 雷达协议本身就不输出小数。


现在能不能改成在检测到无人的时候，不去在发送报文，只发有人无人，有人后再去发完整信息。后期增加，4g模块，要考虑流量问题，我只是构想，你就说好弄吗


The user is asking about a power-saving/流量优化 approach:

1. When no person is detected, only send a minimal message (presence=0), not the full telemetry
2. When a person is detected, resume sending the full telemetry
3. This is for future 4G module integration to save data usage

Let me think about how easy this would be to implement with the current code structure.

Looking at the current code:
- `publishAllPoints()` in mqtt_manager.cpp is called every 1 second in main.cpp's loop()
- The telemetry is built in `buildTelemetry()` which reads radar data

The approach would be:
1. Track presence state changes (transition events)
2. When presence changes to 0: send a lightweight "presence=0" message, stop sending full telemetry
3. When presence changes to 1: send a lightweight "presence=1" message, resume full telemetry
4. Keep sending heartbeats (e.g., every 30-60 seconds) even when nobody is present

This is actually quite easy to implement because:
- The presence state is already tracked with hysteresis in radar_manager
- We just need to add a boolean flag `g_sendFullTelemetry` in main.cpp
- When presence goes from 1→0, switch to minimal mode
- When presence goes from 0→1, switch back to full mode
- We could also add a periodic heartbeat (e.g., every 60s) even in minimal mode

Let me also think about what message to send for presence-only. The current format has all the fields, but we could either:
- Send a separate minimal topic like `report/presence`
- Or send the same `report/allpoints` but with most fields as empty/0

The simplest approach matches the existing protocol structure - just a flag check before `publishAllPoints`.

Let me just answer this simply - it's easy to do.


**非常好改**，当前架构做这个改动很简单。大概几分钟的事。

现在的逻辑是每秒无脑发全量报文，改后变成：

```
检测到无人 → 只发 "Presence:0" 的极简报文（几十字节）
检测到有人 → 恢复发完整报文（几百字节）
```

需要的改动就一个地方 — 在 [main.cpp:643-648](src/main.cpp#L643-L648) 的上报逻辑处加个状态判断：

```cpp
// 现在：每秒必发
if (now - g_lastReportTime >= REPORT_INTERVAL_MS) {
    TelemetryData tele = buildTelemetry(radar);
    g_mqtt.publishAllPoints(tele, g_sequence);  // ← 完整报文
}

// 改成：
static bool lastPresence = false;
if (now - g_lastReportTime >= REPORT_INTERVAL_MS) {
    if (radar.presence) {
        // 有人 → 完整报文
        TelemetryData tele = buildTelemetry(radar);
        g_mqtt.publishAllPoints(tele, g_sequence);
    } else if (lastPresence != radar.presence) {
        // 刚变无人 → 只发一次精简报文（Presence=0）
        g_mqtt.publishPresenceOnly(radar, g_sequence);
    }
    // 持续无人 → 不发（省流量）
    lastPresence = radar.presence;
}
```

再加一个**心跳兜底**：持续无人超过 60 秒，发一次精简报文让服务端知道设备还活着。

等你要上 4G 模块的时候跟我说一声，几分钟就改好。现阶段 WiFi 流量不用省，先保持每秒全量上报方便调试。


可以你改下，我现在烧录到，r60abd1上。


The user wants me to implement the change now - modify the code so that when no person is detected, it only sends a minimal "Presence:0" message, and when a person is detected, it sends the full telemetry. This is to save data for future 4G module usage.

Let me plan the changes:

1. In `main.cpp` - modify the telemetry reporting logic to:
   - When presence changes from 1→0: send a minimal "presence only" message once
   - When presence=0 continuously: don't send anything (except periodic heartbeat every 60s)
   - When presence=1: send full telemetry every second as before

2. In `mqtt_manager.h` - add declaration for `publishPresenceOnly()`

3. In `mqtt_manager.cpp` - implement `publishPresenceOnly()` - a minimal JSON with just presence=0

Let me also make sure the RADAR_TYPE is set correctly. The user says they're using R60ABD1 now.

Let me check config.h - currently `RADAR_TYPE RADAR_TYPE_AFD1`. I need to change it to `RADAR_TYPE_ABD1`.

Actually wait, the user said "我现在烧录到，r60abd1上" - they want to flash to R60ABD1. So I need to:
1. Change RADAR_TYPE to ABD1 in config.h
2. Implement the presence-based reporting optimization

Let me read the mqtt_manager.h to see the current declarations.


Let me first check the current mqtt header, then make all the changes.