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
75	    if (!WiFi.isConnected()) {
76	        Serial.println(F("[MQTT] ⚠ 未连接WiFi, 跳过MQTT连接"));
77	        return false;
78	    }
79
80	    Serial.printf("[MQTT] ── 开始连接 ──\n");
81	    Serial.printf("[MQTT]   服务器: %s:%d\n", _server, _port);
82	    Serial.printf("[MQTT]   客户端ID: %s\n", _gateid);
83	    Serial.printf("[MQTT]   认证: %s\n", (strlen(_user) > 0) ? "有用户名/密码" : "无认证");
84
85	    // Client ID = gateid
86	    String clientId = _gateid;
87
88	    // 遗嘱消息 (§4.3): status/{gateid}/offline
89	    String willTopic = _buildTopic(MQTT_TOPIC_STATUS_OFFLINE);
90	    JsonDocument willDoc;
91	    willDoc["gateid"] = _gateid;
92	    willDoc["type"]   = "offline";
93	    willDoc["time"]   = "";  // Broker 不会填充时间
94	    String willPayload;
95	    serializeJson(willDoc, willPayload);
96
97	    Serial.printf("[MQTT]   遗嘱主题: %s\n", willTopic.c_str());
98
99	    bool hasAuth = (strlen(_user) > 0);
100	    bool ok = false;
101
102	    if (hasAuth) {
103	        ok = _mqtt.connect(clientId.c_str(),
104	                          _user, _pass,
105	                          willTopic.c_str(), 1, true, willPayload.c_str());
106	    } else {
107	        ok = _mqtt.connect(clientId.c_str(),
108	                          willTopic.c_str(), 1, true, willPayload.c_str());
109	    }
110
111	    if (ok) {
112	        _connected = true;
113	        _reconnectInterval = MQTT_RECONNECT_MIN;
114	        Serial.println(F("[MQTT] ✅ 连接成功!"));
115
116	        // 订阅下行主题 (§3.2)
117	        String subAlarm    = _buildTopic(MQTT_TOPIC_CMD_ALARM);
118	        String subRestore  = _buildTopic(MQTT_TOPIC_CMD_ALARM_RESTORE);
119	        String subControl  = _buildTopic(MQTT_TOPIC_CMD_CONTROL);
120	        String subProv     = _buildTopic(MQTT_TOPIC_CMD_PROVISION_WIFI);
121
122	        _mqtt.subscribe(subAlarm.c_str(), 1);
123	        _mqtt.subscribe(subRestore.c_str(), 1);
124	        _mqtt.subscribe(subControl.c_str(), 1);
125	        _mqtt.subscribe(subProv.c_str(), 1);
126
127	        Serial.printf("[MQTT]   已订阅: %s\n", subAlarm.c_str());
128	        Serial.printf("[MQTT]   已订阅: %s\n", subRestore.c_str());
129	        Serial.printf("[MQTT]   已订阅: %s\n", subControl.c_str());
130	        Serial.printf("[MQTT]   已订阅: %s\n", subProv.c_str());
131	    } else {
132	        _connected = false;
133	        int rc = _mqtt.state();
134	        Serial.printf("[MQTT] ❌ 连接失败! 返回码=%d", rc);
135	        switch (rc) {
136	        case -4: Serial.println(F(" (连接超时, 检查服务器地址和端口)")); break;
137	        case -2: Serial.println(F(" (网络不可达, 检查WiFi和网络)")); break;
138	        case -1: Serial.println(F(" (连接断开)")); break;
139	        case 1:  Serial.println(F(" (协议版本不支持)")); break;
140	        case 2:  Serial.println(F(" (ClientID被拒绝)")); break;
141	        case 3:  Serial.println(F(" (服务器不可用)")); break;
142	        case 4:  Serial.println(F(" (用户名密码错误)")); break;
143	        case 5:  Serial.println(F(" (未授权)")); break;
144	        default: Serial.println(F(" (未知错误)")); break;
145	        }
146	    }
147
148	    return _connected;
149	}
150
151	void MQTTManager::disconnect() {
152	    if (_connected) {
153	        // 主动发布离线
154	        publishOffline();
155	        delay(100);
156	        _mqtt.disconnect();
157	        _connected = false;
158	    }
159	}
160
161	bool MQTTManager::isConnected() {
162	    return _connected && _mqtt.connected();
163	}
164
165	// ============================================================
166	// 主循环
167	// ============================================================
168
169	void MQTTManager::loop() {
170	    static uint32_t lastStatusLog = 0;
171
172	    if (!_connected) {
173	        uint32_t now = millis();
174	        if (now - _lastReconnectAttempt >= _reconnectInterval) {
175	            _lastReconnectAttempt = now;
176	            Serial.printf("[MQTT] 🔄 尝试重连 (已等%ds)\n",
177	                          _reconnectInterval / 1000);
178	            if (connect()) {
179	                publishOnline();  // 上线通知
180	                Serial.println(F("[MQTT] 🔄 重连成功!"));
181	            }
182	        }
183	    } else {
184	        _mqtt.loop();
185
186	        // 检测意外断开
187	        if (!_mqtt.connected()) {
188	            _connected = false;
189	            Serial.println(F("[MQTT] ⚠ 连接意外断开!"));
190	            _lastReconnectAttempt = millis();
191	            _reconnectInterval = MQTT_RECONNECT_MIN;
192	        }
193
194	        // 每30秒打印一次连接状态
195	        if (millis() - lastStatusLog > 30000) {
196	            lastStatusLog = millis();
197	            Serial.printf("[MQTT] 📊 连接正常 | WiFi RSSI=%d | 服务器=%s:%d\n",
198	                          WiFi.RSSI(), _server, _port);
199	        }
200	    }
201	}
202
203	// ============================================================
204	// 上行: 全量遥测上报 (§4.1)
205	// ============================================================
206
207	void MQTTManager::publishAllPoints(const TelemetryData& d, uint32_t sequence) {
208	    if (!isConnected()) return;
209
210	    // 构建时间字符串
211	    struct tm timeinfo;
212	    char timeStr[24];
213	    if (getLocalTime(&timeinfo)) {
214	        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
215	    } else {
216	        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
217	    }
218
219	    // 构建 JSON (协议规范 §4.1)
220	    JsonDocument doc;
221	    doc["saleid"]   = d.saleid;
222	    doc["gateid"]   = d.gateid;
223	    doc["type"]     = "report";
224	    doc["time"]     = timeStr;
225	    doc["sequence"] = String(sequence);
226	    doc["source"]   = "da";
227
228	    JsonArray meter = doc["meter"].to<JsonArray>();
229	    JsonObject m    = meter.add<JsonObject>();
230	    m["id"]     = d.meterId;
231	    m["status"] = "1";
232	    m["name"]   = d.meterName;
233
234	    JsonObject v = m["values"].to<JsonObject>();
235	    // 环境数据
236	    v["Temp"]    = String(d.temp, 1);
237	    v["Humi"]    = String(d.humi, 1);
238	    v["DhtValid"] = d.dhtValid ? "1" : "0";
239	    // 雷达存在/运动
240	    v["Presence"]  = d.presence ? "1" : "0";
241	    v["Motion"]    = d.motion ? "1" : "0";
242	    v["Fall"]      = d.fall ? "1" : "0";
243	    v["Resident"]  = d.resident ? "1" : "0";
244	    // 生命体征
245	    v["Body"]       = String(d.body);
246	    v["HeartRate"]  = String(d.heartRate);
247	    v["BreathRate"] = String(d.breathRate);
248	    v["RadarValid"] = d.radarValid ? "1" : "0";
249	    v["VitalValid"] = d.vitalValid ? "1" : "0";
250	    // 水浸
251	    // 水浸: 0=有水 1=正常 2=无效(未接/故障)
252	    if (!d.rainValid)      v["RainDO"] = "2";
253	    else if (!d.rainDO)    v["RainDO"] = "0";
254	    else                   v["RainDO"] = "1";
255	    // 设备状态
256	    v["WifiReady"]   = d.wifiReady ? "1" : "0";
257	    v["UptimeSec"]   = String(d.uptimeSec);
258	    v["BuzzerState"] = d.buzzerState ? "1" : "0";
259
260	    // 序列化
261	    String payload;
262	    serializeJson(doc, payload);
263
264	    // 发布到 report/allpoints (QoS 0)
265	    bool sent = _mqtt.publish("report/allpoints", payload.c_str());
266	    static uint32_t lastPubLog = 0;
267	    static uint32_t pubCount  = 0;
268	    static uint32_t failCount = 0;
269	    pubCount++;
270	    if (!sent) failCount++;
271	    if (millis() - lastPubLog > 30000) {
272	        lastPubLog = millis();
273	        Serial.printf("[MQTT] 📡 发布统计: 总%d次, 成功%d, 失败%d\n",
274	                      pubCount, pubCount - failCount, failCount);
275	    }
276	}
277
278	// ============================================================
279	// 上行: 上线通知 (§4.2)
280	// ============================================================
281
282	void MQTTManager::publishOnline() {
283	    if (!isConnected()) return;
284
285	    struct tm timeinfo;
286	    char timeStr[24];
287	    if (getLocalTime(&timeinfo)) {
288	        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
289	    } else {
290	        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
291	    }
292
293	    JsonDocument doc;
294	    doc["gateid"]          = _gateid;
295	    doc["type"]            = "online";
296	    doc["time"]            = timeStr;
297	    doc["firmwareVersion"] = FW_VERSION;
298	    doc["wifiSignal"]      = String(WiFi.RSSI());
299
300	    JsonArray devList = doc["deviceList"].to<JsonArray>();
301	    devList.add(METER_ID_DEFAULT);
302
303	    String payload;
304	    serializeJson(doc, payload);
305
306	    _publish(MQTT_TOPIC_STATUS_ONLINE, payload.c_str(), STATUS_QOS);
307	}
308
309	// ============================================================
310	// 上行: 离线通知 (§4.3)
311	// ============================================================
312
313	void MQTTManager::publishOffline() {
314	    struct tm timeinfo;
315	    char timeStr[24];
316	    if (getLocalTime(&timeinfo)) {
317	        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
318	    } else {
319	        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
320	    }
321
322	    JsonDocument doc;
323	    doc["gateid"] = _gateid;
324	    doc["type"]   = "offline";
325	    doc["time"]   = timeStr;
326
327	    String payload;
328	    serializeJson(doc, payload);
329
330	    _publish(MQTT_TOPIC_STATUS_OFFLINE, payload.c_str(), STATUS_QOS);
331	}
332
333	// ============================================================
334	// 上行: 配网状态通知 (§4.4)
335	// ============================================================
336
337	void MQTTManager::publishProvisionStatus(const char* state, const char* bleName) {
338	    struct tm timeinfo;
339	    char timeStr[24];
340	    if (getLocalTime(&timeinfo)) {
341	        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
342	    } else {
343	        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
344	    }
345
346	    JsonDocument doc;
347	    doc["gateid"]  = _gateid;
348	    doc["type"]    = "provision_status";
349	    doc["time"]    = timeStr;
350	    doc["state"]   = state;
351	    doc["bleName"] = bleName ? bleName : "";
352
353	    String payload;
354	    serializeJson(doc, payload);
355
356	    _publish(MQTT_TOPIC_STATUS_PROVISION, payload.c_str(), STATUS_QOS);
357	}
358
359	// ============================================================
360	// 上行: 指令执行响应 (§4.5)
361	// ============================================================
362
363	void MQTTManager::publishCmdResponse(const char* cmdId, const char* cmdType,
364	                                      const char* alarmType, bool success,
365	                                      const char* errorCode, const char* errorMsg) {
366	    struct tm timeinfo;
367	    char timeStr[24];
368	    if (getLocalTime(&timeinfo)) {
369	        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
370	    } else {
371	        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
372	    }
373
374	    JsonDocument doc;
375	    doc["gateid"]    = _gateid;
376	    doc["type"]      = "cmd_response";
377	    doc["time"]      = timeStr;
378	    doc["cmdId"]     = cmdId;
379	    doc["cmdType"]   = cmdType;     // alarm / alarm_restore / control
380	    doc["alarmType"] = alarmType ? alarmType : "";
381	    doc["result"]    = success ? "success" : "fail";
382	    doc["errorCode"] = errorCode;
383	    doc["errorMsg"]  = errorMsg;
384
385	    String payload;
386	    serializeJson(doc, payload);
387
388	    _publish(MQTT_TOPIC_RESPONSE_CMD, payload.c_str(), STATUS_QOS);
389	}
390
391	// ============================================================
392	// 上行: 配网指令响应 (§4.6)
393	// ============================================================
394
395	void MQTTManager::publishProvisionResponse(const char* cmdId, bool success,
396	                                            const char* errorCode, const char* errorMsg,
397	                                            const char* wifiSignal) {
398	    struct tm timeinfo;
399	    char timeStr[24];
400	    if (getLocalTime(&timeinfo)) {
401	        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
402	    } else {
403	        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
404	    }
405
406	    JsonDocument doc;
407	    doc["gateid"]    = _gateid;
408	    doc["type"]      = "provision_response";
409	    doc["time"]      = timeStr;
410	    doc["cmdId"]     = cmdId;
411	    doc["result"]    = success ? "success" : "fail";
412	    doc["errorCode"] = errorCode;
413	    doc["errorMsg"]  = errorMsg;
414	    if (success && wifiSignal && strlen(wifiSignal) > 0) {
415	        doc["wifiSignal"] = wifiSignal;
416	    }
417
418	    String payload;
419	    serializeJson(doc, payload);
420
421	    _publish(MQTT_TOPIC_RESPONSE_PROVISION, payload.c_str(), STATUS_QOS);
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()) {
        Serial.println(F("[MQTT] ⚠ 未连接WiFi, 跳过MQTT连接"));
        return false;
    }

    Serial.printf("[MQTT] ── 开始连接 ──\n");
    Serial.printf("[MQTT]   服务器: %s:%d\n", _server, _port);
    Serial.printf("[MQTT]   客户端ID: %s\n", _gateid);
    Serial.printf("[MQTT]   认证: %s\n", (strlen(_user) > 0) ? "有用户名/密码" : "无认证");

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

    Serial.printf("[MQTT]   遗嘱主题: %s\n", willTopic.c_str());

    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;
        _reconnectInterval = MQTT_RECONNECT_MIN;
        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("[MQTT]   已订阅: %s\n", subAlarm.c_str());
        Serial.printf("[MQTT]   已订阅: %s\n", subRestore.c_str());
        Serial.printf("[MQTT]   已订阅: %s\n", subControl.c_str());
        Serial.printf("[MQTT]   已订阅: %s\n", subProv.c_str());
    } else {
        _connected = false;
        int rc = _mqtt.state();
        Serial.printf("[MQTT] ❌ 连接失败! 返回码=%d", rc);
        switch (rc) {
        case -4: Serial.println(F(" (连接超时, 检查服务器地址和端口)")); break;
        case -2: Serial.println(F(" (网络不可达, 检查WiFi和网络)")); break;
        case -1: Serial.println(F(" (连接断开)")); break;
        case 1:  Serial.println(F(" (协议版本不支持)")); break;
        case 2:  Serial.println(F(" (ClientID被拒绝)")); break;
        case 3:  Serial.println(F(" (服务器不可用)")); break;
        case 4:  Serial.println(F(" (用户名密码错误)")); break;
        case 5:  Serial.println(F(" (未授权)")); break;
        default: Serial.println(F(" (未知错误)")); break;
        }
    }

    return _connected;
}

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

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

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

void MQTTManager::loop() {
    static uint32_t lastStatusLog = 0;

    if (!_connected) {
        uint32_t now = millis();
        if (now - _lastReconnectAttempt >= _reconnectInterval) {
            _lastReconnectAttempt = now;
            Serial.printf("[MQTT] 🔄 尝试重连 (已等%ds)\n",
                          _reconnectInterval / 1000);
            if (connect()) {
                publishOnline();  // 上线通知
                Serial.println(F("[MQTT] 🔄 重连成功!"));
            }
        }
    } else {
        _mqtt.loop();

        // 检测意外断开
        if (!_mqtt.connected()) {
            _connected = false;
            Serial.println(F("[MQTT] ⚠ 连接意外断开!"));
            _lastReconnectAttempt = millis();
            _reconnectInterval = MQTT_RECONNECT_MIN;
        }

        // 每30秒打印一次连接状态
        if (millis() - lastStatusLog > 30000) {
            lastStatusLog = millis();
            Serial.printf("[MQTT] 📊 连接正常 | WiFi RSSI=%d | 服务器=%s:%d\n",
                          WiFi.RSSI(), _server, _port);
        }
    }
}

// ============================================================
// 上行: 全量遥测上报 (§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 (协议规范 §4.1)
    JsonDocument doc;
    doc["saleid"]   = d.saleid;
    doc["gateid"]   = d.gateid;
    doc["type"]     = "report";
    doc["time"]     = timeStr;
    doc["sequence"] = String(sequence);
    doc["source"]   = "da";

    JsonArray meter = doc["meter"].to<JsonArray>();
    JsonObject m    = meter.add<JsonObject>();
    m["id"]     = d.meterId;
    m["status"] = "1";
    m["name"]   = d.meterName;

    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["HeartRate"]  = String(d.heartRate);
    v["BreathRate"] = String(d.breathRate);
    v["RadarValid"] = d.radarValid ? "1" : "0";
    v["VitalValid"] = d.vitalValid ? "1" : "0";
    // 水浸
    // 水浸: 0=有水 1=正常 2=无效(未接/故障)
    if (!d.rainValid)      v["RainDO"] = "2";
    else if (!d.rainDO)    v["RainDO"] = "0";
    else                   v["RainDO"] = "1";
    // 设备状态
    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)
    bool sent = _mqtt.publish("report/allpoints", payload.c_str());
    static uint32_t lastPubLog = 0;
    static uint32_t pubCount  = 0;
    static uint32_t failCount = 0;
    pubCount++;
    if (!sent) failCount++;
    if (millis() - lastPubLog > 30000) {
        lastPubLog = millis();
        Serial.printf("[MQTT] 📡 发布统计: 总%d次, 成功%d, 失败%d\n",
                      pubCount, pubCount - failCount, failCount);
    }
}

// ============================================================
// 上行: 上线通知 (§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);
}

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

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


1	/**
2	 * @file wifi_manager.cpp
3	 * @brief WiFi 配网管理器实现
4	 */
5
6	#include "wifi_manager.h"
7
8	// 单例指针 + LED 任务
9	WiFiMgmt* WiFiMgmt::_instance = nullptr;
10	bool WiFiMgmt::_ledTaskRunning = false;
11	uint32_t WiFiMgmt::_ledTaskInterval = 300;
12
13	// ============================================================
14	// 构造函数 & 析构函数
15	// ============================================================
16
17	WiFiMgmt::WiFiMgmt()
18	    : _connected(false)
19	    , _lastCheckTime(0)
20	    , _buttonPressTime(0)
21	    , _buttonPressed(false)
22	    , _ledLastToggle(0)
23	    , _ledState(false)
24	    , _param_mqtt_server(nullptr)
25	    , _param_mqtt_port(nullptr)
26	    , _param_mqtt_user(nullptr)
27	    , _param_mqtt_pass(nullptr)
28	    , _param_device_id(nullptr)
29	    , _param_env_note(nullptr)
30	    , _provisionStateCb(nullptr)
31	{
32	    memset(&_config, 0, sizeof(_config));
33	    _instance = this;  // 设置单例
34	}
35
36	WiFiMgmt::~WiFiMgmt() {
37	    _instance = nullptr;
38	}
39
40	// ============================================================
41	// 初始化
42	// ============================================================
43
44	bool WiFiMgmt::begin() {
45	    Serial.println(F("\n========================================"));
46	    Serial.println(F("  WiFi 配网管理器 初始化"));
47	    Serial.println(F("========================================"));
48
49	    // 初始化引脚
50	    pinMode(CONFIG_BUTTON_PIN, INPUT_PULLUP);
51	    pinMode(LED_WIFI_PIN, OUTPUT);
52	    digitalWrite(LED_WIFI_PIN, LOW);
53
54	    // 初始化 LittleFS
55	    if (!LittleFS.begin(true)) {
56	        Serial.println(F("[WiFi] ❌ LittleFS 挂载失败!"));
57	        return false;
58	    }
59	    Serial.println(F("[WiFi] ✅ LittleFS 已挂载"));
60
61	    // 加载配置
62	    if (!_loadConfig()) {
63	        Serial.println(F("[WiFi] 使用默认配置"));
64	        // 设置默认值
65	        strncpy(_config.mqtt_server, MQTT_DEFAULT_SERVER,   sizeof(_config.mqtt_server) - 1);
66	        strncpy(_config.mqtt_port,   MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port) - 1);
67	        strncpy(_config.mqtt_user,   MQTT_DEFAULT_USER,     sizeof(_config.mqtt_user) - 1);
68	        strncpy(_config.mqtt_pass,   MQTT_DEFAULT_PASS,     sizeof(_config.mqtt_pass) - 1);
69	        strncpy(_config.device_id,   MQTT_DEFAULT_DEVICE,   sizeof(_config.device_id) - 1);
70	        strncpy(_config.env_note,    MQTT_DEFAULT_ENV,      sizeof(_config.env_note) - 1);
71	    }
72
73	    // 配置 WiFiManager
74	    _wm.setDebugOutput(false);  // 关闭调试输出 (避免干扰)
75	    _wm.setConfigPortalTimeout(WIFI_CONFIG_TIMEOUT);
76	    _wm.setConnectTimeout(WIFI_CONNECT_TIMEOUT);
77	    _wm.setTitle("🏥 五劳无感康养监测 - 设备配网");  // 配网页面的中文标题
78	    _wm.setCustomHeadElement(
79	        "<style>"
80	        "body { font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif; }"
81	        ".msg .info { font-size: 14px; }"
82	        "</style>"
83	    );
84	    WiFi.setAutoReconnect(true);  // ESP32 自动重连
85
86	    // 注册保存回调 (必须使用静态函数, 不能使用 lambda)
87	    _wm.setSaveConfigCallback(_saveParamsCallback);
88
89	    // 创建自定义参数
90	    _param_mqtt_server = new WiFiManagerParameter(
91	        "mqtt_server", "MQTT 服务器地址",
92	        _config.mqtt_server, 40);
93	    _param_mqtt_port = new WiFiManagerParameter(
94	        "mqtt_port", "MQTT 端口",
95	        _config.mqtt_port, 6);
96	    _param_mqtt_user = new WiFiManagerParameter(
97	        "mqtt_user", "MQTT 用户名",
98	        _config.mqtt_user, 20);
99	    _param_mqtt_pass = new WiFiManagerParameter(
100	        "mqtt_pass", "MQTT 密码",
101	        _config.mqtt_pass, 20);
102	    _param_device_id = new WiFiManagerParameter(
103	        "device_id", "设备名称",
104	        _config.device_id, 20);
105	    _param_env_note = new WiFiManagerParameter(
106	        "env_note", "🏠 环境备注 (如: 主卧/客厅/老人房)",
107	        _config.env_note, 20);
108
109	    // 注册自定义参数
110	    _wm.addParameter(_param_mqtt_server);
111	    _wm.addParameter(_param_mqtt_port);
112	    _wm.addParameter(_param_mqtt_user);
113	    _wm.addParameter(_param_mqtt_pass);
114	    _wm.addParameter(_param_device_id);
115	    _wm.addParameter(_param_env_note);
116
117	    // 设置 AP 模式回调 (静态函数)
118	    _wm.setAPCallback(_apCallback);
119
120	    // 尝试连接 WiFi
121	    _connected = _connectWiFi();
122
123	    if (!_connected) {
124	        // 如果自动连接失败, 启动配网页面
125	        Serial.println(F("[WiFi] 自动连接失败, 启动配网模式..."));
126	        _emitProvisionState("ready");  // 协议 §4.4: 进入配网模式
127	        _setLEDMode(1);  // 快闪 = 配网模式
128	        // 配网期间 loop() 阻塞，LED 靠另一个小任务单独闪 (见 _startLEDTask)
129	        _startLEDTask(300);  // 300ms 快闪
130	        _connected = _connectWiFi();  // 内部会启动配网页面 (阻塞!)
131	        _stopLEDTask();
132	    }
133
134	    if (_connected) {
135	        _setLEDMode(2);  // 常亮 = 已连接
136	        _emitProvisionState("success");  // 协议 §4.4: 配网成功
137	    }
138
139	    _lastCheckTime = millis();
140	    return _connected;
141	}
142
143	// ============================================================
144	// 主循环
145	// ============================================================
146
147	bool WiFiMgmt::loop() {
148	    // 检测按钮长按
149	    if (_checkButtonLongPress()) {
150	        Serial.println(F("[WiFi] ⚠  检测到长按, 清除配置并重启..."));
151	        resetSettings();
152	        return false;
153	    }
154
155	    // 检查 WiFi 状态
156	    if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {
157	        _lastCheckTime = millis();
158
159	        if (WiFi.status() != WL_CONNECTED) {
160	            _connected = false;
161	            _setLEDMode(0);  // 慢闪 = 未连接
162	            Serial.println(F("[WiFi] ⚠  WiFi 断开, 尝试重连..."));
163	            _connectWiFi();
164	        } else {
165	            _connected = true;
166	            _setLEDMode(2);  // 常亮
167	        }
168	    }
169
170	    // LED 闪烁处理
171	    if (!_connected) {
172	        uint32_t interval = (_wm.getConfigPortalActive()) ? 300 : 1000;
173	        if (millis() - _ledLastToggle > interval) {
174	            _ledLastToggle = millis();
175	            _ledState = !_ledState;
176	            digitalWrite(LED_WIFI_PIN, _ledState ? HIGH : LOW);
177	        }
178	    }
179
180	    return _connected;
181	}
182
183	// ============================================================
184	// WiFi 连接
185	// ============================================================
186
187	bool WiFiMgmt::_connectWiFi() {
188	    // 检查是否有已保存的配置
189	    // autoConnect 会先尝试连接, 失败后启动 AP 配网页面
190	    _connected = _wm.autoConnect(WIFI_AP_SSID, WIFI_AP_PASSWORD);
191
192	    if (_connected) {
193	        Serial.println(F("[WiFi] ✅ 已连接!"));
194	        Serial.printf("  WiFi名称: %s\n", WiFi.SSID().c_str());
195	        Serial.printf("  IP地址:   %s\n", WiFi.localIP().toString().c_str());
196	        Serial.printf("  MAC地址:  %s\n", WiFi.macAddress().c_str());
197
198	        // 重新加载配置 (确保获取最新值)
199	        // autoConnect 过程中如果有保存, 回调已写入 LittleFS
200	        _loadConfig();
201
202	        // 同步自定义参数 (如果配置是从 LittleFS 加载的)
203	        _param_mqtt_server->setValue(_config.mqtt_server, sizeof(_config.mqtt_server));
204	        _param_mqtt_port->setValue(_config.mqtt_port, sizeof(_config.mqtt_port));
205	        _param_mqtt_user->setValue(_config.mqtt_user, sizeof(_config.mqtt_user));
206	        _param_mqtt_pass->setValue(_config.mqtt_pass, sizeof(_config.mqtt_pass));
207	        _param_device_id->setValue(_config.device_id, sizeof(_config.device_id));
208	        _param_env_note->setValue(_config.env_note, sizeof(_config.env_note));
209
210	        Serial.printf("  设备名: %s\n", _config.device_id);
211	        Serial.printf("  环境:   %s\n", _config.env_note);
212	        Serial.printf("  MQTT服务器: %s:%s\n", _config.mqtt_server, _config.mqtt_port);
213
214	        _emitProvisionState("success");  // 协议 §4.4: 配网成功
215	    } else {
216	        _emitProvisionState("wifi_failed");  // 协议 §4.4: WiFi 连接失败
217	    }
218
219	    return _connected;
220	}
221
222	// ============================================================
223	// 配网状态回调注册
224	// ============================================================
225
226	void WiFiMgmt::onProvisionState(ProvisionStateCallback cb) {
227	    _provisionStateCb = cb;
228	}
229
230	void WiFiMgmt::_emitProvisionState(const char* state) {
231	    Serial.printf("[WiFi] 📡 配网状态: %s\n", state);
232	    if (_provisionStateCb) {
233	        _provisionStateCb(state);
234	    }
235	}
236
237	// ============================================================
238	// 配网页面
239	// ============================================================
240
241	void WiFiMgmt::startConfigPortal() {
242	    Serial.println(F("[WiFi] 🔧 手动启动配网页面..."));
243	    _emitProvisionState("ready");  // 协议 §4.4: 进入配网模式
244	    _setLEDMode(1);
245	    _startLEDTask(300);  // 快闪
246	    _wm.startConfigPortal(WIFI_AP_SSID, WIFI_AP_PASSWORD);
247	    _stopLEDTask();
248	    _connected = (WiFi.status() == WL_CONNECTED);
249	    if (_connected) {
250	        _setLEDMode(2);
251	        _emitProvisionState("success");  // 协议 §4.4: 配网成功
252	    }
253	}
254
255	// ============================================================
256	// 重置设置
257	// ============================================================
258
259	void WiFiMgmt::resetSettings() {
260	    Serial.println(F("[WiFi] 清除所有配置..."));
261
262	    // 删除 LittleFS 中的配置文件
263	    if (LittleFS.exists(CONFIG_FILE_WIFI)) LittleFS.remove(CONFIG_FILE_WIFI);
264	    if (LittleFS.exists(CONFIG_FILE_MQTT)) LittleFS.remove(CONFIG_FILE_MQTT);
265	    if (LittleFS.exists(CONFIG_FILE_DEVICE)) LittleFS.remove(CONFIG_FILE_DEVICE);
266
267	    // 清除 WiFiManager 的配置
268	    _wm.resetSettings();
269
270	    // LED 快闪提示
271	    for (int i = 0; i < 10; i++) {
272	        digitalWrite(LED_WIFI_PIN, !digitalRead(LED_WIFI_PIN));
273	        delay(100);
274	    }
275
276	    Serial.println(F("[WiFi] 🔄 正在重启..."));
277	    delay(500);
278	    ESP.restart();
279	}
280
281	// ============================================================
282	// 按钮长按检测
283	// ============================================================
284
285	bool WiFiMgmt::_checkButtonLongPress() {
286	    bool pressed = (digitalRead(CONFIG_BUTTON_PIN) == LOW);
287
288	    if (pressed && !_buttonPressed) {
289	        // 按下
290	        _buttonPressed = true;
291	        _buttonPressTime = millis();
292	    } else if (!pressed && _buttonPressed) {
293	        // 松开
294	        _buttonPressed = false;
295	        if (millis() - _buttonPressTime >= BUTTON_LONG_PRESS_MS) {
296	            return true;  // 长按确认
297	        }
298	    }
299
300	    return false;
301	}
302
303	// ============================================================
304	// LittleFS 配置持久化
305	// ============================================================
306
307	bool WiFiMgmt::_loadConfig() {
308	    if (!LittleFS.exists(CONFIG_FILE_DEVICE)) {
309	        return false;
310	    }
311
312	    File file = LittleFS.open(CONFIG_FILE_DEVICE, "r");
313	    if (!file) return false;
314
315	    JsonDocument doc;
316	    DeserializationError err = deserializeJson(doc, file);
317	    file.close();
318
319	    if (err) {
320	        Serial.printf("[WiFi] JSON 解析失败: %s\n", err.c_str());
321	        return false;
322	    }
323
324	    strncpy(_config.mqtt_server, doc["mqtt_server"] | MQTT_DEFAULT_SERVER, sizeof(_config.mqtt_server) - 1);
325	    strncpy(_config.mqtt_port,   doc["mqtt_port"]   | MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port) - 1);
326	    strncpy(_config.mqtt_user,   doc["mqtt_user"]   | MQTT_DEFAULT_USER,     sizeof(_config.mqtt_user) - 1);
327	    strncpy(_config.mqtt_pass,   doc["mqtt_pass"]   | MQTT_DEFAULT_PASS,     sizeof(_config.mqtt_pass) - 1);
328	    strncpy(_config.device_id,   doc["device_id"]   | MQTT_DEFAULT_DEVICE,   sizeof(_config.device_id) - 1);
329	    strncpy(_config.env_note,    doc["env_note"]    | MQTT_DEFAULT_ENV,      sizeof(_config.env_note) - 1);
330
331	    Serial.println(F("[WiFi] ✅ 设备配置已加载"));
332	    Serial.printf("  设备名: %s, 环境: %s\n", _config.device_id, _config.env_note);
333	    return true;
334	}
335
336	bool WiFiMgmt::_saveConfig() {
337	    JsonDocument doc;
338	    doc["mqtt_server"] = _config.mqtt_server;
339	    doc["mqtt_port"]   = _config.mqtt_port;
340	    doc["mqtt_user"]   = _config.mqtt_user;
341	    doc["mqtt_pass"]   = _config.mqtt_pass;
342	    doc["device_id"]   = _config.device_id;
343	    doc["env_note"]    = _config.env_note;
344
345	    File file = LittleFS.open(CONFIG_FILE_DEVICE, "w");
346	    if (!file) {
347	        Serial.println(F("[WiFi] ❌ 无法创建配置文件"));
348	        return false;
349	    }
350
351	    if (serializeJson(doc, file) == 0) {
352	        Serial.println(F("[WiFi] ❌ 配置写入失败"));
353	        file.close();
354	        return false;
355	    }
356
357	    file.close();
358	    Serial.println(F("[WiFi] ✅ 设备配置已保存"));
359	    return true;
360	}
361
362	// ============================================================
363	// WiFiManager 保存参数回调
364	// ============================================================
365
366	// ============================================================
367	// 静态回调函数 (WiFiManager 要求 C 函数指针)
368	// ============================================================
369
370	void WiFiMgmt::_apCallback(WiFiManager* wm) {
371	    Serial.println(F("[WiFi] 🔧 进入配网模式"));
372	    Serial.printf("  AP WiFi名称: %s\n", WIFI_AP_SSID);
373	    Serial.printf("  AP 密码: %s\n", WIFI_AP_PASSWORD);
374	    Serial.println(F("  手机连接此 WiFi 即可打开配网页面 (自动弹出)"));
375	}
376
377	void WiFiMgmt::_saveParamsCallback() {
378	    if (!_instance) return;
379
380	    Serial.println(F("[WiFi] 💾 参数已保存"));
381
382	    // 从 WiFiManager 参数中读取值
383	    strncpy(_instance->_config.mqtt_server, _instance->_param_mqtt_server->getValue(),
384	            sizeof(_instance->_config.mqtt_server) - 1);
385	    strncpy(_instance->_config.mqtt_port,   _instance->_param_mqtt_port->getValue(),
386	            sizeof(_instance->_config.mqtt_port) - 1);
387	    strncpy(_instance->_config.mqtt_user,   _instance->_param_mqtt_user->getValue(),
388	            sizeof(_instance->_config.mqtt_user) - 1);
389	    strncpy(_instance->_config.mqtt_pass,   _instance->_param_mqtt_pass->getValue(),
390	            sizeof(_instance->_config.mqtt_pass) - 1);
391	    strncpy(_instance->_config.device_id,   _instance->_param_device_id->getValue(),
392	            sizeof(_instance->_config.device_id) - 1);
393	    strncpy(_instance->_config.env_note,    _instance->_param_env_note->getValue(),
394	            sizeof(_instance->_config.env_note) - 1);
395
396	    _instance->_saveConfig();
397
398	    Serial.println(F("[WiFi] 📋 新配置:"));
399	    Serial.printf("  MQTT: %s:%s\n",
400	                  _instance->_config.mqtt_server,
401	                  _instance->_config.mqtt_port);
402	    Serial.printf("  设备: %s\n", _instance->_config.device_id);
403	    Serial.printf("  环境: %s\n", _instance->_config.env_note);
404	}
405
406	// ============================================================
407	// Getter 方法
408	// ============================================================
409
410	bool WiFiMgmt::isConnected() const {
411	    return _connected && (WiFi.status() == WL_CONNECTED);
412	}
413
414	DeviceConfig WiFiMgmt::getConfig() const {
415	    return _config;
416	}
417
418	const char* WiFiMgmt::getDeviceId() const {
419	    return _config.device_id;
420	}
421
422	const char* WiFiMgmt::getEnvNote() const {
423	    return _config.env_note;
424	}
425
426	// ============================================================
427	// LED 模式控制
428	// ============================================================
429
430	void WiFiMgmt::_setLEDMode(uint8_t mode) {
431	    switch (mode) {
432	    case 0:
433	    case 1:
434	        break;
435	    case 2:
436	        digitalWrite(LED_WIFI_PIN, HIGH);
437	        _ledState = true;
438	        break;
439	    }
440	}
441
442	// ============================================================
443	// LED 闪烁任务 (配网阻塞期间的独立线程)
444	// ============================================================
445
446	void WiFiMgmt::_startLEDTask(uint32_t interval) {
447	    _stopLEDTask();
448	    _ledTaskInterval = interval;
449	    _ledTaskRunning = true;
450	    xTaskCreatePinnedToCore(
451	        _ledBlinkTask, "LedBlink",
452	        2048, nullptr, 1, nullptr, 0
453	    );
454	}
455
456	void WiFiMgmt::_stopLEDTask() {
457	    _ledTaskRunning = false;
458	}
459
460	void WiFiMgmt::_ledBlinkTask(void* param) {
461	    while (_ledTaskRunning) {
462	        digitalWrite(LED_WIFI_PIN, HIGH);
463	        vTaskDelay(pdMS_TO_TICKS(_ledTaskInterval / 2));
464	        digitalWrite(LED_WIFI_PIN, LOW);
465	        vTaskDelay(pdMS_TO_TICKS(_ledTaskInterval / 2));
466	    }
467	    vTaskDelete(nullptr);
468	}
469

/**
 * @file wifi_manager.cpp
 * @brief WiFi 配网管理器实现
 */

#include "wifi_manager.h"

// 单例指针 + LED 任务
WiFiMgmt* WiFiMgmt::_instance = nullptr;
bool WiFiMgmt::_ledTaskRunning = false;
uint32_t WiFiMgmt::_ledTaskInterval = 300;

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

WiFiMgmt::WiFiMgmt()
    : _connected(false)
    , _lastCheckTime(0)
    , _buttonPressTime(0)
    , _buttonPressed(false)
    , _ledLastToggle(0)
    , _ledState(false)
    , _param_mqtt_server(nullptr)
    , _param_mqtt_port(nullptr)
    , _param_mqtt_user(nullptr)
    , _param_mqtt_pass(nullptr)
    , _param_device_id(nullptr)
    , _param_env_note(nullptr)
    , _provisionStateCb(nullptr)
{
    memset(&_config, 0, sizeof(_config));
    _instance = this;  // 设置单例
}

WiFiMgmt::~WiFiMgmt() {
    _instance = nullptr;
}

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

bool WiFiMgmt::begin() {
    Serial.println(F("\n========================================"));
    Serial.println(F("  WiFi 配网管理器 初始化"));
    Serial.println(F("========================================"));

    // 初始化引脚
    pinMode(CONFIG_BUTTON_PIN, INPUT_PULLUP);
    pinMode(LED_WIFI_PIN, OUTPUT);
    digitalWrite(LED_WIFI_PIN, LOW);

    // 初始化 LittleFS
    if (!LittleFS.begin(true)) {
        Serial.println(F("[WiFi] ❌ LittleFS 挂载失败!"));
        return false;
    }
    Serial.println(F("[WiFi] ✅ LittleFS 已挂载"));

    // 加载配置
    if (!_loadConfig()) {
        Serial.println(F("[WiFi] 使用默认配置"));
        // 设置默认值
        strncpy(_config.mqtt_server, MQTT_DEFAULT_SERVER,   sizeof(_config.mqtt_server) - 1);
        strncpy(_config.mqtt_port,   MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port) - 1);
        strncpy(_config.mqtt_user,   MQTT_DEFAULT_USER,     sizeof(_config.mqtt_user) - 1);
        strncpy(_config.mqtt_pass,   MQTT_DEFAULT_PASS,     sizeof(_config.mqtt_pass) - 1);
        strncpy(_config.device_id,   MQTT_DEFAULT_DEVICE,   sizeof(_config.device_id) - 1);
        strncpy(_config.env_note,    MQTT_DEFAULT_ENV,      sizeof(_config.env_note) - 1);
    }

    // 配置 WiFiManager
    _wm.setDebugOutput(false);  // 关闭调试输出 (避免干扰)
    _wm.setConfigPortalTimeout(WIFI_CONFIG_TIMEOUT);
    _wm.setConnectTimeout(WIFI_CONNECT_TIMEOUT);
    _wm.setTitle("🏥 五劳无感康养监测 - 设备配网");  // 配网页面的中文标题
    _wm.setCustomHeadElement(
        "<style>"
        "body { font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif; }"
        ".msg .info { font-size: 14px; }"
        "</style>"
    );
    WiFi.setAutoReconnect(true);  // ESP32 自动重连

    // 注册保存回调 (必须使用静态函数, 不能使用 lambda)
    _wm.setSaveConfigCallback(_saveParamsCallback);

    // 创建自定义参数
    _param_mqtt_server = new WiFiManagerParameter(
        "mqtt_server", "MQTT 服务器地址",
        _config.mqtt_server, 40);
    _param_mqtt_port = new WiFiManagerParameter(
        "mqtt_port", "MQTT 端口",
        _config.mqtt_port, 6);
    _param_mqtt_user = new WiFiManagerParameter(
        "mqtt_user", "MQTT 用户名",
        _config.mqtt_user, 20);
    _param_mqtt_pass = new WiFiManagerParameter(
        "mqtt_pass", "MQTT 密码",
        _config.mqtt_pass, 20);
    _param_device_id = new WiFiManagerParameter(
        "device_id", "设备名称",
        _config.device_id, 20);
    _param_env_note = new WiFiManagerParameter(
        "env_note", "🏠 环境备注 (如: 主卧/客厅/老人房)",
        _config.env_note, 20);

    // 注册自定义参数
    _wm.addParameter(_param_mqtt_server);
    _wm.addParameter(_param_mqtt_port);
    _wm.addParameter(_param_mqtt_user);
    _wm.addParameter(_param_mqtt_pass);
    _wm.addParameter(_param_device_id);
    _wm.addParameter(_param_env_note);

    // 设置 AP 模式回调 (静态函数)
    _wm.setAPCallback(_apCallback);

    // 尝试连接 WiFi
    _connected = _connectWiFi();

    if (!_connected) {
        // 如果自动连接失败, 启动配网页面
        Serial.println(F("[WiFi] 自动连接失败, 启动配网模式..."));
        _emitProvisionState("ready");  // 协议 §4.4: 进入配网模式
        _setLEDMode(1);  // 快闪 = 配网模式
        // 配网期间 loop() 阻塞，LED 靠另一个小任务单独闪 (见 _startLEDTask)
        _startLEDTask(300);  // 300ms 快闪
        _connected = _connectWiFi();  // 内部会启动配网页面 (阻塞!)
        _stopLEDTask();
    }

    if (_connected) {
        _setLEDMode(2);  // 常亮 = 已连接
        _emitProvisionState("success");  // 协议 §4.4: 配网成功
    }

    _lastCheckTime = millis();
    return _connected;
}

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

bool WiFiMgmt::loop() {
    // 检测按钮长按
    if (_checkButtonLongPress()) {
        Serial.println(F("[WiFi] ⚠  检测到长按, 清除配置并重启..."));
        resetSettings();
        return false;
    }

    // 检查 WiFi 状态
    if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {
        _lastCheckTime = millis();

        if (WiFi.status() != WL_CONNECTED) {
            _connected = false;
            _setLEDMode(0);  // 慢闪 = 未连接
            Serial.println(F("[WiFi] ⚠  WiFi 断开, 尝试重连..."));
            _connectWiFi();
        } else {
            _connected = true;
            _setLEDMode(2);  // 常亮
        }
    }

    // LED 闪烁处理
    if (!_connected) {
        uint32_t interval = (_wm.getConfigPortalActive()) ? 300 : 1000;
        if (millis() - _ledLastToggle > interval) {
            _ledLastToggle = millis();
            _ledState = !_ledState;
            digitalWrite(LED_WIFI_PIN, _ledState ? HIGH : LOW);
        }
    }

    return _connected;
}

// ============================================================
// WiFi 连接
// ============================================================

bool WiFiMgmt::_connectWiFi() {
    // 检查是否有已保存的配置
    // autoConnect 会先尝试连接, 失败后启动 AP 配网页面
    _connected = _wm.autoConnect(WIFI_AP_SSID, WIFI_AP_PASSWORD);

    if (_connected) {
        Serial.println(F("[WiFi] ✅ 已连接!"));
        Serial.printf("  WiFi名称: %s\n", WiFi.SSID().c_str());
        Serial.printf("  IP地址:   %s\n", WiFi.localIP().toString().c_str());
        Serial.printf("  MAC地址:  %s\n", WiFi.macAddress().c_str());

        // 重新加载配置 (确保获取最新值)
        // autoConnect 过程中如果有保存, 回调已写入 LittleFS
        _loadConfig();

        // 同步自定义参数 (如果配置是从 LittleFS 加载的)
        _param_mqtt_server->setValue(_config.mqtt_server, sizeof(_config.mqtt_server));
        _param_mqtt_port->setValue(_config.mqtt_port, sizeof(_config.mqtt_port));
        _param_mqtt_user->setValue(_config.mqtt_user, sizeof(_config.mqtt_user));
        _param_mqtt_pass->setValue(_config.mqtt_pass, sizeof(_config.mqtt_pass));
        _param_device_id->setValue(_config.device_id, sizeof(_config.device_id));
        _param_env_note->setValue(_config.env_note, sizeof(_config.env_note));

        Serial.printf("  设备名: %s\n", _config.device_id);
        Serial.printf("  环境:   %s\n", _config.env_note);
        Serial.printf("  MQTT服务器: %s:%s\n", _config.mqtt_server, _config.mqtt_port);

        _emitProvisionState("success");  // 协议 §4.4: 配网成功
    } else {
        _emitProvisionState("wifi_failed");  // 协议 §4.4: WiFi 连接失败
    }

    return _connected;
}

// ============================================================
// 配网状态回调注册
// ============================================================

void WiFiMgmt::onProvisionState(ProvisionStateCallback cb) {
    _provisionStateCb = cb;
}

void WiFiMgmt::_emitProvisionState(const char* state) {
    Serial.printf("[WiFi] 📡 配网状态: %s\n", state);
    if (_provisionStateCb) {
        _provisionStateCb(state);
    }
}

// ============================================================
// 配网页面
// ============================================================

void WiFiMgmt::startConfigPortal() {
    Serial.println(F("[WiFi] 🔧 手动启动配网页面..."));
    _emitProvisionState("ready");  // 协议 §4.4: 进入配网模式
    _setLEDMode(1);
    _startLEDTask(300);  // 快闪
    _wm.startConfigPortal(WIFI_AP_SSID, WIFI_AP_PASSWORD);
    _stopLEDTask();
    _connected = (WiFi.status() == WL_CONNECTED);
    if (_connected) {
        _setLEDMode(2);
        _emitProvisionState("success");  // 协议 §4.4: 配网成功
    }
}

// ============================================================
// 重置设置
// ============================================================

void WiFiMgmt::resetSettings() {
    Serial.println(F("[WiFi] 清除所有配置..."));

    // 删除 LittleFS 中的配置文件
    if (LittleFS.exists(CONFIG_FILE_WIFI)) LittleFS.remove(CONFIG_FILE_WIFI);
    if (LittleFS.exists(CONFIG_FILE_MQTT)) LittleFS.remove(CONFIG_FILE_MQTT);
    if (LittleFS.exists(CONFIG_FILE_DEVICE)) LittleFS.remove(CONFIG_FILE_DEVICE);

    // 清除 WiFiManager 的配置
    _wm.resetSettings();

    // LED 快闪提示
    for (int i = 0; i < 10; i++) {
        digitalWrite(LED_WIFI_PIN, !digitalRead(LED_WIFI_PIN));
        delay(100);
    }

    Serial.println(F("[WiFi] 🔄 正在重启..."));
    delay(500);
    ESP.restart();
}

// ============================================================
// 按钮长按检测
// ============================================================

bool WiFiMgmt::_checkButtonLongPress() {
    bool pressed = (digitalRead(CONFIG_BUTTON_PIN) == LOW);

    if (pressed && !_buttonPressed) {
        // 按下
        _buttonPressed = true;
        _buttonPressTime = millis();
    } else if (!pressed && _buttonPressed) {
        // 松开
        _buttonPressed = false;
        if (millis() - _buttonPressTime >= BUTTON_LONG_PRESS_MS) {
            return true;  // 长按确认
        }
    }

    return false;
}

// ============================================================
// LittleFS 配置持久化
// ============================================================

bool WiFiMgmt::_loadConfig() {
    if (!LittleFS.exists(CONFIG_FILE_DEVICE)) {
        return false;
    }

    File file = LittleFS.open(CONFIG_FILE_DEVICE, "r");
    if (!file) return false;

    JsonDocument doc;
    DeserializationError err = deserializeJson(doc, file);
    file.close();

    if (err) {
        Serial.printf("[WiFi] JSON 解析失败: %s\n", err.c_str());
        return false;
    }

    strncpy(_config.mqtt_server, doc["mqtt_server"] | MQTT_DEFAULT_SERVER, sizeof(_config.mqtt_server) - 1);
    strncpy(_config.mqtt_port,   doc["mqtt_port"]   | MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port) - 1);
    strncpy(_config.mqtt_user,   doc["mqtt_user"]   | MQTT_DEFAULT_USER,     sizeof(_config.mqtt_user) - 1);
    strncpy(_config.mqtt_pass,   doc["mqtt_pass"]   | MQTT_DEFAULT_PASS,     sizeof(_config.mqtt_pass) - 1);
    strncpy(_config.device_id,   doc["device_id"]   | MQTT_DEFAULT_DEVICE,   sizeof(_config.device_id) - 1);
    strncpy(_config.env_note,    doc["env_note"]    | MQTT_DEFAULT_ENV,      sizeof(_config.env_note) - 1);

    Serial.println(F("[WiFi] ✅ 设备配置已加载"));
    Serial.printf("  设备名: %s, 环境: %s\n", _config.device_id, _config.env_note);
    return true;
}

bool WiFiMgmt::_saveConfig() {
    JsonDocument doc;
    doc["mqtt_server"] = _config.mqtt_server;
    doc["mqtt_port"]   = _config.mqtt_port;
    doc["mqtt_user"]   = _config.mqtt_user;
    doc["mqtt_pass"]   = _config.mqtt_pass;
    doc["device_id"]   = _config.device_id;
    doc["env_note"]    = _config.env_note;

    File file = LittleFS.open(CONFIG_FILE_DEVICE, "w");
    if (!file) {
        Serial.println(F("[WiFi] ❌ 无法创建配置文件"));
        return false;
    }

    if (serializeJson(doc, file) == 0) {
        Serial.println(F("[WiFi] ❌ 配置写入失败"));
        file.close();
        return false;
    }

    file.close();
    Serial.println(F("[WiFi] ✅ 设备配置已保存"));
    return true;
}

// ============================================================
// WiFiManager 保存参数回调
// ============================================================

// ============================================================
// 静态回调函数 (WiFiManager 要求 C 函数指针)
// ============================================================

void WiFiMgmt::_apCallback(WiFiManager* wm) {
    Serial.println(F("[WiFi] 🔧 进入配网模式"));
    Serial.printf("  AP WiFi名称: %s\n", WIFI_AP_SSID);
    Serial.printf("  AP 密码: %s\n", WIFI_AP_PASSWORD);
    Serial.println(F("  手机连接此 WiFi 即可打开配网页面 (自动弹出)"));
}

void WiFiMgmt::_saveParamsCallback() {
    if (!_instance) return;

    Serial.println(F("[WiFi] 💾 参数已保存"));

    // 从 WiFiManager 参数中读取值
    strncpy(_instance->_config.mqtt_server, _instance->_param_mqtt_server->getValue(),
            sizeof(_instance->_config.mqtt_server) - 1);
    strncpy(_instance->_config.mqtt_port,   _instance->_param_mqtt_port->getValue(),
            sizeof(_instance->_config.mqtt_port) - 1);
    strncpy(_instance->_config.mqtt_user,   _instance->_param_mqtt_user->getValue(),
            sizeof(_instance->_config.mqtt_user) - 1);
    strncpy(_instance->_config.mqtt_pass,   _instance->_param_mqtt_pass->getValue(),
            sizeof(_instance->_config.mqtt_pass) - 1);
    strncpy(_instance->_config.device_id,   _instance->_param_device_id->getValue(),
            sizeof(_instance->_config.device_id) - 1);
    strncpy(_instance->_config.env_note,    _instance->_param_env_note->getValue(),
            sizeof(_instance->_config.env_note) - 1);

    _instance->_saveConfig();

    Serial.println(F("[WiFi] 📋 新配置:"));
    Serial.printf("  MQTT: %s:%s\n",
                  _instance->_config.mqtt_server,
                  _instance->_config.mqtt_port);
    Serial.printf("  设备: %s\n", _instance->_config.device_id);
    Serial.printf("  环境: %s\n", _instance->_config.env_note);
}

// ============================================================
// Getter 方法
// ============================================================

bool WiFiMgmt::isConnected() const {
    return _connected && (WiFi.status() == WL_CONNECTED);
}

DeviceConfig WiFiMgmt::getConfig() const {
    return _config;
}

const char* WiFiMgmt::getDeviceId() const {
    return _config.device_id;
}

const char* WiFiMgmt::getEnvNote() const {
    return _config.env_note;
}

// ============================================================
// LED 模式控制
// ============================================================

void WiFiMgmt::_setLEDMode(uint8_t mode) {
    switch (mode) {
    case 0:
    case 1:
        break;
    case 2:
        digitalWrite(LED_WIFI_PIN, HIGH);
        _ledState = true;
        break;
    }
}

// ============================================================
// LED 闪烁任务 (配网阻塞期间的独立线程)
// ============================================================

void WiFiMgmt::_startLEDTask(uint32_t interval) {
    _stopLEDTask();
    _ledTaskInterval = interval;
    _ledTaskRunning = true;
    xTaskCreatePinnedToCore(
        _ledBlinkTask, "LedBlink",
        2048, nullptr, 1, nullptr, 0
    );
}

void WiFiMgmt::_stopLEDTask() {
    _ledTaskRunning = false;
}

void WiFiMgmt::_ledBlinkTask(void* param) {
    while (_ledTaskRunning) {
        digitalWrite(LED_WIFI_PIN, HIGH);
        vTaskDelay(pdMS_TO_TICKS(_ledTaskInterval / 2));
        digitalWrite(LED_WIFI_PIN, LOW);
        vTaskDelay(pdMS_TO_TICKS(_ledTaskInterval / 2));
    }
    vTaskDelete(nullptr);
}


每次配网成功，的提示要和online在一起发送


Now I understand the current flow. Let me look at the main.cpp to see how the provision state callback is connected and how the online message is sent.

The current flow:
1. `WiFiMgmt::begin()` calls `_connectWiFi()` which calls `_emitProvisionState("success")`
2. The `_emitProvisionState` callback goes to the provision handler in main.cpp
3. MQTT connects separately and calls `publishOnline()`

The user wants: when provisioning succeeds, the provision success message and the online message should be sent together (merged into one message, or at least sent at the same time).

Let me look at main.cpp to see how these are connected.


340	    const TickType_t freq = pdMS_TO_TICKS(10);
341
342	    while (true) {
343	        g_radar.loop();
344	        vTaskDelayUntil(&lastWake, freq);
345	    }
346	}
347
348	// ============================================================
349	// Setup
350	// ============================================================
351
352	void setup() {
353	    Serial.begin(115200);
354	    delay(500);
355
356	    Serial.println(F("\n"));
357	    Serial.println(F("╔══════════════════════════════════════════╗"));
358	    Serial.println(F("║  🏥 五劳无感康养监测系统 v1.0.0       ║"));
359	    Serial.println(F("║  ESP32-S3 + R60ABD1 + DHT11 + 水浸     ║"));
360	    Serial.println(F("║  MQTT 协议 V0.08                        ║"));
361	    Serial.println(F("╚══════════════════════════════════════════╝"));
362
363	    g_bootTime = millis();
364
365	    // 1. 报警模块 (最先, 引脚安全)
366	    g_alarm.begin();
367
368	    // 2. 雷达
369	    g_radar.begin();
370
371	    // 3. OLED 显示
372	    g_display.begin();
373
374	    // 4. 按钮
375	    g_buttons.begin();
376	    g_buttons.onPageSwitch([]() {
377	        g_display.nextPage();
378	    });
379	    g_buttons.onAlarmDismiss([]() {
380	        g_alarm.clearAllLocal();
381	        g_radar.clearFall();  // 数据也归零
382	    });
383	    g_buttons.onProvisionLongPress([]() {
384	        g_wifi.startConfigPortal();
385	    });
386
387	    // 5. WiFi (WiFiManager 配网)
388	    // 先注册配网状态回调, 再 begin(), 这样配网过程中的 ready/success 都不会丢失
389	    g_wifi.onProvisionState([](const char* state) {
390	        g_mqtt.publishProvisionStatus(state, "");
391	    });
392	    g_wifi.begin();
393
394	    // 6. 时间同步
395	    if (g_wifi.isConnected()) {
396	        syncTime();
397	    }
398
399	    // 7. MQTT
400	    DeviceConfig cfg = g_wifi.getConfig();
401	    g_mqtt.setServer(cfg.mqtt_server, atoi(cfg.mqtt_port),
402	                     cfg.mqtt_user, cfg.mqtt_pass);
403	    g_mqtt.begin(cfg.device_id);
404
405	    // 注册 MQTT 回调
406	    g_mqtt.onAlarmCommand(handleAlarmCommand);
407	    g_mqtt.onControlCommand(handleControlCommand);
408	    g_mqtt.onProvisionCommand(handleProvisionCommand);
409
410	    // 连接 MQTT
411	    if (g_wifi.isConnected()) {
412	        g_mqtt.connect();
413	    }
414
415	    // 8. 传感器
416	    g_sensor.begin();
417
418	    // 9. 雷达任务 (核心0)
419	    xTaskCreatePinnedToCore(

    const TickType_t freq = pdMS_TO_TICKS(10);

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

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

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

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

    g_bootTime = millis();

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

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

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

    // 4. 按钮
    g_buttons.begin();
    g_buttons.onPageSwitch([]() {
        g_display.nextPage();
    });
    g_buttons.onAlarmDismiss([]() {
        g_alarm.clearAllLocal();
        g_radar.clearFall();  // 数据也归零
    });
    g_buttons.onProvisionLongPress([]() {
        g_wifi.startConfigPortal();
    });

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

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

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

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

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

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

    // 9. 雷达任务 (核心0)
    xTaskCreatePinnedToCore(


1	/**
2	 * @file wifi_manager.h
3	 * @brief WiFi 配网管理器
4	 *
5	 * 功能:
6	 *  - 自动连接已保存的 WiFi
7	 *  - 配网失败/首次启动 → AP 模式 + 强制门户页面
8	 *  - 自定义参数: MQTT 服务器/端口/用户/密码、设备名称、环境备注
9	 *  - 长按按钮重置配置
10	 *  - LED 状态指示
11	 *  - LittleFS 持久化存储
12	 */
13
14	#ifndef WIFI_MANAGER_H
15	#define WIFI_MANAGER_H
16
17	#include <Arduino.h>
18	#include <WiFiManager.h>
19	#include <LittleFS.h>
20	#include <ArduinoJson.h>
21	#include "config.h"
22
23	// 设备配置结构体
24	struct DeviceConfig {
25	    char mqtt_server[40];
26	    char mqtt_port[6];
27	    char mqtt_user[20];
28	    char mqtt_pass[20];
29	    char device_id[20];
30	    char env_note[20];      // 环境备注 (如: "主卧", "客厅", "老人房")
31	};
32
33	// 配网状态回调 (state: ready / wifi_connecting / wifi_failed / success)
34	typedef void (*ProvisionStateCallback)(const char* state);
35
36	class WiFiMgmt {
37	public:
38	    WiFiMgmt();
39	    ~WiFiMgmt();
40
41	    // 单例指针 (用于静态回调)
42	    static WiFiMgmt* _instance;
43
44	    /**
45	     * @brief 注册配网状态回调
46	     * @param cb 回调函数，参数为 state 字符串
47	     * @note  触发时机: 进入配网页面/连接WiFi中/WiFi失败/配网成功
48	     *        用于实现协议 §4.4 配网状态通知
49	     */
50	    void onProvisionState(ProvisionStateCallback cb);
51
52	    /**
53	     * @brief 初始化 WiFi 管理器
54	     * @return true=成功
55	     */
56	    bool begin();
57
58	    /**
59	     * @brief 主循环, 处理配网流程和按钮检测
60	     * @return true=WiFi 已连接
61	     */
62	    bool loop();
63
64	    /**
65	     * @brief WiFi 是否已连接
66	     */
67	    bool isConnected() const;
68
69	    /**
70	     * @brief 获取设备配置
71	     */
72	    DeviceConfig getConfig() const;
73
74	    /**
75	     * @brief 获取设备 ID (用于 MQTT client_id)
76	     */
77	    const char* getDeviceId() const;
78
79	    /**
80	     * @brief 获取环境备注
81	     */
82	    const char* getEnvNote() const;
83
84	    /**
85	     * @brief 强制进入配网模式
86	     */
87	    void startConfigPortal();
88
89	    /**
90	     * @brief 清除所有配置并重启
91	     */
92	    void resetSettings();
93
94	    /** LED 闪烁任务 (配网阻塞期间用) */
95	    static void _ledBlinkTask(void* param);
96	    void _startLEDTask(uint32_t interval);
97	    void _stopLEDTask();
98
99	    static bool _ledTaskRunning;
100	    static uint32_t _ledTaskInterval;
101
102	private:
103	    /**
104	     * @brief 尝试连接 WiFi
105	     */
106	    bool _connectWiFi();
107
108	    /**
109	     * @brief 设置 LED 状态
110	     * @param mode 0=慢闪(未连接) 1=快闪(配网) 2=常亮(已连接)
111	     */
112	    void _setLEDMode(uint8_t mode);
113
114	    /**
115	     * @brief 检测按钮长按
116	     */
117	    bool _checkButtonLongPress();
118
119	    /**
120	     * @brief 加载/保存配置到 LittleFS
121	     */
122	    bool _loadConfig();
123	    bool _saveConfig();
124
125	    /**
126	     * @brief 保存 WiFi 参数回调 (WiFiManager 的回调)
127	     */
128	    static void _saveParamsCallback();
129	    static void _apCallback(WiFiManager* wm);
130
131	    WiFiManager     _wm;
132	    DeviceConfig    _config;
133	    bool            _connected;
134	    uint32_t        _lastCheckTime;
135	    uint32_t        _buttonPressTime;
136	    bool            _buttonPressed;
137
138	    // LED 闪烁状态
139	    uint32_t        _ledLastToggle;
140	    bool            _ledState;
141
142	    // WiFiManager 自定义参数
143	    WiFiManagerParameter* _param_mqtt_server;
144	    WiFiManagerParameter* _param_mqtt_port;
145	    WiFiManagerParameter* _param_mqtt_user;
146	    WiFiManagerParameter* _param_mqtt_pass;
147	    WiFiManagerParameter* _param_device_id;
148	    WiFiManagerParameter* _param_env_note;
149
150	    // 配网状态回调
151	    ProvisionStateCallback _provisionStateCb;
152
153	    /**
154	     * @brief 触发配网状态回调 (内部用)
155	     */
156	    void _emitProvisionState(const char* state);
157	};
158
159	#endif // WIFI_MANAGER_H
160

/**
 * @file wifi_manager.h
 * @brief WiFi 配网管理器
 *
 * 功能:
 *  - 自动连接已保存的 WiFi
 *  - 配网失败/首次启动 → AP 模式 + 强制门户页面
 *  - 自定义参数: MQTT 服务器/端口/用户/密码、设备名称、环境备注
 *  - 长按按钮重置配置
 *  - LED 状态指示
 *  - LittleFS 持久化存储
 */

#ifndef WIFI_MANAGER_H
#define WIFI_MANAGER_H

#include <Arduino.h>
#include <WiFiManager.h>
#include <LittleFS.h>
#include <ArduinoJson.h>
#include "config.h"

// 设备配置结构体
struct DeviceConfig {
    char mqtt_server[40];
    char mqtt_port[6];
    char mqtt_user[20];
    char mqtt_pass[20];
    char device_id[20];
    char env_note[20];      // 环境备注 (如: "主卧", "客厅", "老人房")
};

// 配网状态回调 (state: ready / wifi_connecting / wifi_failed / success)
typedef void (*ProvisionStateCallback)(const char* state);

class WiFiMgmt {
public:
    WiFiMgmt();
    ~WiFiMgmt();

    // 单例指针 (用于静态回调)
    static WiFiMgmt* _instance;

    /**
     * @brief 注册配网状态回调
     * @param cb 回调函数，参数为 state 字符串
     * @note  触发时机: 进入配网页面/连接WiFi中/WiFi失败/配网成功
     *        用于实现协议 §4.4 配网状态通知
     */
    void onProvisionState(ProvisionStateCallback cb);

    /**
     * @brief 初始化 WiFi 管理器
     * @return true=成功
     */
    bool begin();

    /**
     * @brief 主循环, 处理配网流程和按钮检测
     * @return true=WiFi 已连接
     */
    bool loop();

    /**
     * @brief WiFi 是否已连接
     */
    bool isConnected() const;

    /**
     * @brief 获取设备配置
     */
    DeviceConfig getConfig() const;

    /**
     * @brief 获取设备 ID (用于 MQTT client_id)
     */
    const char* getDeviceId() const;

    /**
     * @brief 获取环境备注
     */
    const char* getEnvNote() const;

    /**
     * @brief 强制进入配网模式
     */
    void startConfigPortal();

    /**
     * @brief 清除所有配置并重启
     */
    void resetSettings();

    /** LED 闪烁任务 (配网阻塞期间用) */
    static void _ledBlinkTask(void* param);
    void _startLEDTask(uint32_t interval);
    void _stopLEDTask();

    static bool _ledTaskRunning;
    static uint32_t _ledTaskInterval;

private:
    /**
     * @brief 尝试连接 WiFi
     */
    bool _connectWiFi();

    /**
     * @brief 设置 LED 状态
     * @param mode 0=慢闪(未连接) 1=快闪(配网) 2=常亮(已连接)
     */
    void _setLEDMode(uint8_t mode);

    /**
     * @brief 检测按钮长按
     */
    bool _checkButtonLongPress();

    /**
     * @brief 加载/保存配置到 LittleFS
     */
    bool _loadConfig();
    bool _saveConfig();

    /**
     * @brief 保存 WiFi 参数回调 (WiFiManager 的回调)
     */
    static void _saveParamsCallback();
    static void _apCallback(WiFiManager* wm);

    WiFiManager     _wm;
    DeviceConfig    _config;
    bool            _connected;
    uint32_t        _lastCheckTime;
    uint32_t        _buttonPressTime;
    bool            _buttonPressed;

    // LED 闪烁状态
    uint32_t        _ledLastToggle;
    bool            _ledState;

    // WiFiManager 自定义参数
    WiFiManagerParameter* _param_mqtt_server;
    WiFiManagerParameter* _param_mqtt_port;
    WiFiManagerParameter* _param_mqtt_user;
    WiFiManagerParameter* _param_mqtt_pass;
    WiFiManagerParameter* _param_device_id;
    WiFiManagerParameter* _param_env_note;

    // 配网状态回调
    ProvisionStateCallback _provisionStateCb;

    /**
     * @brief 触发配网状态回调 (内部用)
     */
    void _emitProvisionState(const char* state);
};

#endif // WIFI_MANAGER_H


Now I understand the full picture. Let me trace the current flow:

1. `setup()` in main.cpp (line 389-392):
   - Registers provision callback: `g_mqtt.publishProvisionStatus(state, "")`
   - Calls `g_wifi.begin()` which connects WiFi and emits "success" state

2. After WiFi connects, MQTT connects separately (line 411-413):
   - `g_mqtt.connect()` → which calls `publishOnline()` internally? No wait, let me check — `connect()` does subscribe but does NOT call `publishOnline()`.

   Actually looking at `mqtt_manager.cpp` line 72-149, the `connect()` method just connects and subscribes. It does NOT call `publishOnline()`.

   But in the `loop()` at line 178-179, when reconnecting: `connect()` then `publishOnline()`.

   And in main.cpp setup, after `g_mqtt.connect()` there's no explicit `publishOnline()` call. Let me look at the main loop to see where `publishOnline()` is called for the first time...

Actually wait, let me look at the main loop more carefully.

The user's request is: "每次配网成功，的提示要和online在一起发送" — "Every time provisioning succeeds, the provision notification and online should be sent together."

Currently:
1. WiFi connects → `_emitProvisionState("success")` → `g_mqtt.publishProvisionStatus("success", "")` is called immediately
2. But at this point MQTT isn't even connected yet! (MQTT connects later in setup)
3. `publishProvisionStatus` calls `isConnected()` check first, so if MQTT isn't connected, the message is dropped!

So the current flow has a problem: the provision success message is sent before MQTT is connected, so it never actually gets published.

The user wants: when provisioning is successful, send the provision status AND online status together in one message (or at least, after MQTT connects, send both together).

Let me think about the best approach:

**Option A**: Merge provision status into the online message. Add a `provisionState` field to the online JSON, or combine both into one topic.

**Option B**: After MQTT connects, send `publishOnline()` immediately followed by `publishProvisionStatus("success")` if it was a fresh provision.

**Option C**: Modify the flow so that when WiFi provisioning succeeds, instead of immediately publishing provision status (which fails because MQTT is not connected), we store the state and send it after MQTT connects, along with the online message.

The user says "要和online在一起发送" — send together with online. This sounds like they want both messages sent at the same time after MQTT connects.

I think the cleanest approach is:
1. In the provision state callback, if state is "success", don't send immediately — instead flag it
2. After MQTT connects in setup, send online + provision success together (or sequentially)

Actually, looking more carefully at the current code flow:

In `setup()`:
```
g_wifi.onProvisionState([](const char* state) {
    g_mqtt.publishProvisionStatus(state, "");  // This runs during g_wifi.begin() when MQTT isn't connected yet!
});
g_wifi.begin();  // WiFi connects here, emits "success" → publishProvisionStatus is called but MQTT not connected → message lost!
...
g_mqtt.connect();  // MQTT connects here, but no publishOnline() is called!
```

So there's actually a bug: the provision success message is lost, and online is never published on initial connect (only on reconnect in loop()).

Let me also check the main loop to see where publishOnline is called for initial connection.

I need to read more of main.cpp to see the main loop.


让我再看看主循环部分。


418	    // 9. 雷达任务 (核心0)
419	    xTaskCreatePinnedToCore(
420	        radarTask, "RadarTask",
421	        RADAR_TASK_STACK, nullptr,
422	        RADAR_TASK_PRIORITY, nullptr,
423	        RADAR_TASK_CORE
424	    );
425
426	    Serial.println(F("[系统] ✅ 系统就绪"));
427	    Serial.println(F("══════════════════════════════════════════\n"));
428	}
429
430	// ============================================================
431	// 主循环 (核心1)
432	// ============================================================
433
434	void loop() {
435	    uint32_t now = millis();
436
437	    // --- 0. 按钮轮询 ---
438	    g_buttons.loop();
439
440	    // --- 1. WiFi ---
441	    bool wifiOk = g_wifi.loop();
442
443	    // --- 2. MQTT (含自动重连) ---
444	    if (wifiOk) {
445	        g_mqtt.loop();
446
447	        // WiFi 刚连上但 MQTT 还没连
448	        static bool wasDisconnected = true;
449	        if (g_mqtt.isConnected()) {
450	            if (wasDisconnected) {
451	                // 刚恢复连接, 同步时间
452	                syncTime();
453	                wasDisconnected = false;
454	            }
455	        } else {
456	            wasDisconnected = true;
457	        }
458	    }
459
460	    // --- 3. 传感器 (独立，任何传感器故障不影响其他) ---
461	    g_sensor.read();
462
463	    // --- 4. 本地报警检查 (离线也工作) ---
464	    RadarRawData radar = g_radar.getData();
465	    checkLocalAlarms(radar);
466
467	    // --- 传感器故障检测 (每30秒检查一次，避免刷屏) ---
468	    static uint32_t lastFaultCheck = 0;
469	    if (now - lastFaultCheck > 30000) {
470	        lastFaultCheck = now;
471	        if (!g_sensor.isDhtValid() && millis() > 60000) {
472	            Serial.println(F("[系统] ⚠ DHT11 未检测到 (不影响运行)"));
473	        }
474	        if (g_radar.isTimeout()) {
475	            Serial.println(F("[系统] ⚠ 雷达超时无数据 (不影响运行)"));
476	        }
477	    }
478
479	    // --- 5. 报警模块更新 (定时器/闪烁) ---
480	    g_alarm.loop();
481
482	    // --- 6. 遥测上报 (1秒周期) ---
483	    if (now - g_lastReportTime >= REPORT_INTERVAL_MS) {
484	        g_lastReportTime = now;
485
486	        TelemetryData tele = buildTelemetry(radar);
487	        g_mqtt.publishAllPoints(tele, g_sequence);
488	        g_sequence++;
489
490	        // 调试输出 + OLED 显示
491	        printDebug(tele);
492	        g_display.update(tele, wifiOk, g_alarm.isBuzzerOn());
493	    }
494
495	    // --- 7. MQTT 配网状态 (WiFiManager 触发时) ---
496	    // WiFiManager 的 captive portal 启动/关闭时更新状态
497	    // 此逻辑已集成在 wifi_manager 中

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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