1	/**
2	 * @file wifi_manager.cpp
3	 * @brief WiFi 配网管理器 — AP + WebServer
4	 */
5
6	#include "wifi_manager.h"
7
8	// BLE 配网回调: 小程序写入 JSON → 仅在 BLE 栈中暂存, 不直接处理
9	class BLEProvisionCB : public BLECharacteristicCallbacks {
10	    void onWrite(BLECharacteristic* ch) override {
11	        std::string val = ch->getValue();
12	        if (val.length() > 0 && WiFiMgmt::_instance) {
13	            Serial.printf("[BLE] 收到配网数据 (%d 字节)\n", val.length());
14	            // 仅暂存 → 在 loop() 里处理 (BTC_TASK 栈太小, 写 Flash 会栈溢出)
15	            WiFiMgmt::_instance->enqueueBLEJson(val.c_str());
16	        }
17	    }
18	};
19
20	WiFiMgmt* WiFiMgmt::_instance = nullptr;
21
22	WiFiMgmt::WiFiMgmt()
23	    : _server(nullptr), _dns(nullptr)
24	    , _connected(false)
25	    , _portalActive(false), _portalStopFlag(false)
26	    , _portalStartTime(0)
27	    , _lastCheckTime(0), _buttonPressTime(0), _buttonPressed(false)
28	    , _lastScanTime(0)
29	    , _provisionSuccess(false)
30	    , _ledTaskHandle(nullptr), _ledMode(0)
31	    , _bleServer(nullptr), _bleInfoChar(nullptr), _bleChar(nullptr), _bleAdv(nullptr)
32	    , _blePendingFlag(false)
33	{
34	    memset(&_config, 0, sizeof(_config));
35	    _instance = this;
36	}
37
38	WiFiMgmt::~WiFiMgmt() {
39	    _stopPortal();
40	    _instance = nullptr;
41	}
42
43	// ============================================================
44	// 初始化
45	// ============================================================
46
47	bool WiFiMgmt::begin() {
48	    Serial.println(("\n=== WiFi 配网管理器 (SmartConfig) ==="));
49
50	    pinMode(CONFIG_BUTTON_PIN, INPUT_PULLUP);
51	    pinMode(LED_WIFI_PIN, OUTPUT);
52	    digitalWrite(LED_WIFI_PIN, LOW);
53
54	    xTaskCreate(_ledTaskFunc, "WiFiLed", 1024, nullptr, 1, &_ledTaskHandle);
55
56	    // 冷启动延迟, 等 Flash 稳定
57	    delay(500);
58	    if (!LittleFS.begin(true)) {
59	        Serial.println(("[WiFi] LittleFS 挂载失败!"));
60	        return false;
61	    }
62	    Serial.println(("[WiFi] LittleFS 就绪"));
63
64	    if (!_loadConfig()) {
65	        Serial.println(("[WiFi] 使用默认配置"));
66	        strncpy(_config.mqtt_server, MQTT_DEFAULT_SERVER, sizeof(_config.mqtt_server)-1);
67	        strncpy(_config.mqtt_port,   MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port)-1);
68	        strncpy(_config.mqtt_user,   MQTT_DEFAULT_USER, sizeof(_config.mqtt_user)-1);
69	        strncpy(_config.mqtt_pass,   MQTT_DEFAULT_PASS, sizeof(_config.mqtt_pass)-1);
70	        strncpy(_config.device_id,   MQTT_DEFAULT_DEVICE, sizeof(_config.device_id)-1);
71	        strncpy(_config.env_note,    MQTT_DEFAULT_ENV, sizeof(_config.env_note)-1);
72	        strncpy(_config.env_type,    "bedroom",        sizeof(_config.env_type)-1);
73	        memset(_config.wifi_ssid, 0, sizeof(_config.wifi_ssid));
74	        memset(_config.wifi_pass, 0, sizeof(_config.wifi_pass));
75	    }
76
77	    WiFi.setAutoReconnect(true);
78
79	    // 有配置 → 直接连
80	    bool hasConfig = LittleFS.exists(CONFIG_FILE_DEVICE);
81	    Serial.printf("[WiFi] 配置文件: %s\n", hasConfig ? "存在" : "不存在");
82	    if (hasConfig) {
83	        _connected = _connectWiFi();
84	        Serial.printf("[WiFi] 自动连接: %s\n", _connected ? "成功" : "失败");
85	    } else {
86	        Serial.println(("[WiFi] 无配置, 跳过连接"));
87	        _connected = false;
88	    }
89
90	    if (!_connected) {
91	        _setLEDMode(2);  // 快闪 = 配网模式
92	        _startPortal();  // 只开 AP
93	    } else {
94	        _setLEDMode(3);  // 常亮
95	    }
96
97	    _lastCheckTime = millis();
98	    return _connected;
99	}
100
101	// ============================================================
102	// 主循环
103	// ============================================================
104
105	bool WiFiMgmt::loop() {
106	    // --- BLE 待处理数据 (从 loop 上下文安全处理, 不占 BTC_TASK 栈) ---
107	    _processBLEPending();
108
109	    // --- Portal 模式 ---
110	    if (_portalActive) {
111	        if (_portalStopFlag) {
112	            _stopPortal();
113	            _portalStopFlag = false;
114	            if (_pendingSSID.length() > 0) {
115	                Serial.printf("[WiFi] 尝试连接: %s\n", _pendingSSID.c_str());
116	                _connected = _tryConnect(_pendingSSID.c_str(), _pendingPass.c_str());
117	                _pendingSSID = ""; _pendingPass = "";
118	                if (_connected) {
119	                    _setLEDMode(3);
120	                    _provisionSuccess = true;  // 标记配网成功
121	                } else {
122	                    _setLEDMode(2);
123	                    _startPortal();
124	                }
125	            }
126	            return _connected;
127	        }
128
129	        _dns->processNextRequest();
130	        _server->handleClient();
131	        delay(10);
132	        return _connected;
133	    }
134
135	    // --- 正常运行 ---
136	    if (_checkButtonLongPress()) {
137	        Serial.println(("[WiFi] 长按, 进配网模式"));
138	        _setLEDMode(2);
139	        WiFi.disconnect();
140	        _startPortal();
141	        _connected = false;
142	        return false;
143	    }
144
145	    if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {
146	        _lastCheckTime = millis();
147	        wl_status_t s = WiFi.status();
148	        if (s != WL_CONNECTED) {
149	            if (_connected) {
150	                _connected = false;
151	                _setLEDMode(1);
152	                Serial.printf("[WiFi] 断开! status=%d RSSI=%d\n", s, WiFi.RSSI());
153	            }
154	            // 不手动 reconnect(), 让 WiFi.setAutoReconnect(true) 自动处理
155	        } else if (!_connected) {
156	            _connected = true;
157	            _setLEDMode(3);
158	            Serial.printf("[WiFi] 已连接 RSSI=%d\n", WiFi.RSSI());
159	        }
160	    }
161
162	    return _connected;
163	}
164
165	// ============================================================
166	// WiFi 连接
167	// ============================================================
168
169	bool WiFiMgmt::_connectWiFi() {
170	    if (_config.wifi_ssid[0] == '\0') {
171	        Serial.println(("[WiFi] 无已存WiFi密码"));
172	        return false;
173	    }
174	    Serial.printf("[WiFi] 连接: %s\n", _config.wifi_ssid);
175
176	    // 崩溃重启后 WiFi 模块可能残留旧状态, 彻底重置
177	    WiFi.disconnect(true);
178	    delay(200);
179	    WiFi.mode(WIFI_STA);
180	    delay(100);
181	    WiFi.begin(_config.wifi_ssid, _config.wifi_pass);
182	    int retry = 0;
183	    while (WiFi.status() != WL_CONNECTED && retry < 40) {  // 20s
184	        delay(500); Serial.print("."); yield(); retry++;
185	    }
186	    Serial.println();
187	    _connected = (WiFi.status() == WL_CONNECTED);
188	    Serial.printf("[WiFi] 连接结果: %s  status=%d\n", _connected ? "成功" : "失败", WiFi.status());
189	    if (_connected) Serial.printf("[WiFi] SSID=%s IP=%s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
190	    return _connected;
191	}
192
193	bool WiFiMgmt::_tryConnect(const char* ssid, const char* pass) {
194	    WiFi.mode(WIFI_STA);
195	    WiFi.persistent(true);   // 确保持久化 WiFi 密码到 NVS (断电不丢)
196	    WiFi.begin(ssid, pass);
197	    int retry = 0;
198	    while (WiFi.status() != WL_CONNECTED && retry < 40) {  // 20s
199	        delay(500); Serial.print("."); retry++;
200	    }
201	    bool ok = (WiFi.status() == WL_CONNECTED);
202	    Serial.printf("\n[WiFi] 连接结果: %s  status=%d\n", ok ? "成功!" : "失败", WiFi.status());
203	    return ok;
204	}
205
206	// ============================================================
207	// 配网 Portal (AP + WebServer)
208	// ============================================================
209
210	void WiFiMgmt::startConfigPortal() {
211	    _setLEDMode(2);
212	    _startPortal();
213	}
214
215	void WiFiMgmt::_startPortal() {
216	    if (_portalActive) return;
217
218	    WiFi.disconnect(true);
219	    delay(200);
220	    WiFi.mode(WIFI_AP);
221	    WiFi.softAPConfig(IPAddress(192,168,4,1), IPAddress(192,168,4,1), IPAddress(255,255,255,0));
222	    WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);
223	    Serial.printf("[WiFi] AP: %s  IP: 192.168.4.1\n", WIFI_AP_SSID);
224
225	    // DNS 劫持: 所有域名 → 192.168.4.1 → 触发手机强制门户弹窗
226	    _dns = new DNSServer();
227	    _dns->start(53, "*", WiFi.softAPIP());
228
229	    _server = new WebServer(80);
230	    _server->on("/",      HTTP_GET,  [this]() { _handleRoot(); });
231	    _server->on("/save",  HTTP_POST, [this]() { _handleSave(); });
232	    _server->on("/reset", HTTP_GET,  [this]() { _handleReset(); });
233	    _server->onNotFound(  [this]() { _handleRoot(); });
234	    _server->begin();
235
236	    _portalActive = true;
237	    _portalStartTime = millis();
238
239	    Serial.println(("[WiFi] 手机连 AP 后浏览器打开 http://192.168.4.1"));
240
241	    _startBLE();  // 同时启动 BLE 配网广播
242	}
243
244	void WiFiMgmt::_stopPortal() {
245	    if (!_portalActive) return;
246	    _stopBLE();
247	    if (_server) { _server->stop(); delete _server; _server = nullptr; }
248	    if (_dns)    { _dns->stop();    delete _dns;    _dns = nullptr; }
249	    WiFi.softAPdisconnect(true);
250	    _portalActive = false;
251	}
252
253	// ============================================================
254	// Web 处理器
255	// ============================================================
256
257	void WiFiMgmt::_handleRoot() {
258	    _server->send(200, "text/html; charset=utf-8", _htmlPage());
259	}
260
261	void WiFiMgmt::_handleSave() {
262	    if (_server->hasArg("s")) {
263	        _pendingSSID = _server->arg("s");
264	        _pendingPass = _server->arg("p");
265	        // 持久化 WiFi 密码到 LittleFS
266	        strncpy(_config.wifi_ssid, _pendingSSID.c_str(), sizeof(_config.wifi_ssid)-1);
267	        strncpy(_config.wifi_pass, _pendingPass.c_str(), sizeof(_config.wifi_pass)-1);
268	        if (_server->hasArg("device_id"))  strncpy(_config.device_id, _server->arg("device_id").c_str(), sizeof(_config.device_id)-1);
269	        if (_server->hasArg("env_type"))   strncpy(_config.env_type,  _server->arg("env_type").c_str(),  sizeof(_config.env_type)-1);
270	        if (_server->hasArg("env_note"))   strncpy(_config.env_note,  _server->arg("env_note").c_str(),  sizeof(_config.env_note)-1);
271	        if (_server->hasArg("mqtt_server")) strncpy(_config.mqtt_server, _server->arg("mqtt_server").c_str(), sizeof(_config.mqtt_server)-1);
272	        if (_server->hasArg("mqtt_port"))  strncpy(_config.mqtt_port,  _server->arg("mqtt_port").c_str(), sizeof(_config.mqtt_port)-1);
273	        if (_server->hasArg("mqtt_user"))  strncpy(_config.mqtt_user,  _server->arg("mqtt_user").c_str(), sizeof(_config.mqtt_user)-1);
274	        if (_server->hasArg("mqtt_pass"))  strncpy(_config.mqtt_pass,  _server->arg("mqtt_pass").c_str(), sizeof(_config.mqtt_pass)-1);
275	        _saveConfig();
276
277	        _server->send(200, "text/html; charset=utf-8",
278	            "<!DOCTYPE html><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1'>"
279	            "<title>配置完成</title>

⏳
"
284	            "
配置已保存

设备正在连接 WiFi...
等待指示灯常亮
"
285	            "
");
290
291	        _portalStopFlag = true;
292	        return;
293	    }
294	    _handleRoot();
295	}
296
297	void WiFiMgmt::_handleReset() {
298	    _server->send(200, "text/html; charset=utf-8",
299	        "<!DOCTYPE html><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>已重置</title>"
300	        "

配置已清除

设备正在重启...

");
303	    delay(500);
304	    _stopPortal();
305	    resetSettings();
306	}
307
308	// ============================================================
309	// 配网 HTML 页面 (精简版)
310	// ============================================================
311
312	void WiFiMgmt::_updateWifiScan() {
313	    _wifiScanCache = "";
314	    int n = WiFi.scanNetworks(false, true);
315	    for (int i = 0; i < n; i++) {
316	        _wifiScanCache += "<option value='" + WiFi.SSID(i) + "'>" + WiFi.SSID(i) + " (" + String(WiFi.RSSI(i)) + "dBm)</option>";
317	    }
318	    if (n == 0) _wifiScanCache = "<option value=''>未发现 WiFi</option>";
319	    WiFi.scanDelete();
320	    _lastScanTime = millis();
321	}
322
323	String WiFiMgmt::_htmlPage() {
324	    if (_wifiScanCache.length() == 0) _updateWifiScan();
325
326	    String html;
327	    html.reserve(4096);
328	    html += "<!DOCTYPE html><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'>";
329	    html += "<meta name='theme-color' content='#12a889'><title>设备配网</title>
";
350
351	    // Header + 提示
352	    html += "

📡

观照-五劳无感 设备配网

WiFi 联网与参数配置

";
353	    html += "
💡 也可用 ESP-Touch App 一键配网
";
354
355	    html += "<form method='POST' action='/save'>";
356	    html += "

📶

无线网络

选择 WiFi 或使用 SmartConfig

";
357	    html += "
WiFi 名称
📡<select name='s'>";
358	    html += _wifiScanCache;
359	    html += "</select>

";
360	    html += "
WiFi 密码
🔒<input type='password' name='p' placeholder='请输入 WiFi 密码'>

";
361
362	    html += "

⚙

设备基础信息

唯一标识与安装位置

";
363	    html += "
设备编号
#<input type='text' name='device_id' value='";
364	    html += _config.device_id;
365	    html += "'>

";
366	    html += "
环境类型
🏷<select name='env_type'>";
367	    const char* envTypes[] = {"bedroom","livingroom","bathroom","hallway","balcony","kitchen"};
368	    const char* envLabels[] = {"🛏 卧室","🛋 客厅","🛁 浴室/卫生间","🚶 走廊","🌿 阳台","🍳 厨房"};
369	    for (int i = 0; i < 6; i++) {
370	        html += "<option value='";
371	        html += envTypes[i];
372	        html += "'";
373	        if (strcmp(_config.env_type, envTypes[i]) == 0 || (strlen(_config.env_type) == 0 && i == 0))
374	            html += " selected";
375	        html += ">";
376	        html += envLabels[i];
377	        html += "</option>";
378	    }
379	    html += "</select>

";
380	    html += "
位置备注
🏠<input type='text' name='env_note' placeholder='如: 主卧 / 客厅 / 老人房' value='";
381	    html += _config.env_note;
382	    html += "'>

";
383
384	    html += "<input type='hidden' name='mqtt_server' value='";
385	    html += _config.mqtt_server;
386	    html += "'><input type='hidden' name='mqtt_port' value='";
387	    html += _config.mqtt_port;
388	    html += "'><input type='hidden' name='mqtt_user' value='";
389	    html += _config.mqtt_user;
390	    html += "'><input type='hidden' name='mqtt_pass' value='";
391	    html += _config.mqtt_pass;
392	    html += "'>";
393
394	    html += "
<button type='submit'>保存配置并重启连接</button>
</form>";
395	    html += "<button class='danger' onclick=\"if(confirm('确定恢复出厂?'))location.href='/reset'\">恢复出厂配置</button>";
396	    html += "
Health Monitor · Device Provisioning

";
397
398	    return html;
399	}
400
401	// ============================================================
402	// BLE 蓝牙配网
403	// ============================================================
404
405	void WiFiMgmt::_startBLE() {
406	    // 如果 BLE 栈已在运行, 仅重启广播 (避免 deinit→init 失败)
407	    if (_bleServer != nullptr) {
408	        if (_bleAdv) {
409	            // 重设广告数据 (stop 后 start 需要重新配置)
410	            BLEAdvertisementData advData;
411	            advData.setCompleteServices(BLEUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b"));
412	            advData.setShortName("HealthMon");
413	            _bleAdv->setAdvertisementData(advData);
414	            _bleAdv->setScanResponse(false);
415	            _bleAdv->start();
416	        }
417	        Serial.println("[BLE] ✅ 广播已重开");
418	        return;
419	    }
420
421	    BLEDevice::init("HealthMonitor");
422	    _bleServer = BLEDevice::createServer();
423
424	    BLEService* svc = _bleServer->createService("4fafc201-1fb5-459e-8fcc-c5c9c331914b");
425
426	    // 特征1: 设备信息 (READ) — 小程序连接后读取设备编号等
427	    _bleInfoChar = svc->createCharacteristic(
428	        "4fafc202-1fb5-459e-8fcc-c5c9c331914b",
429	        BLECharacteristic::PROPERTY_READ
430	    );
431	    // 设备信息 JSON (最多 512 字节)
432	    JsonDocument info;
433	    info["device_id"]   = _config.device_id;
434	    info["mqtt_server"] = _config.mqtt_server;
435	    info["mqtt_port"]   = _config.mqtt_port;
436	    info["fw_version"]  = FW_VERSION;
437	    String infoJson;
438	    serializeJson(info, infoJson);
439	    _bleInfoChar->setValue(infoJson.c_str());
440
441	    // 特征2: 配网写入 (WRITE + NOTIFY) — 小程序发配网数据, 设备回复
442	    _bleChar = svc->createCharacteristic(
443	        "4fafc203-1fb5-459e-8fcc-c5c9c331914b",
444	        BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY
445	    );
446	    _bleChar->setCallbacks(new BLEProvisionCB());
447	    _bleChar->addDescriptor(new BLE2902());  // 启用 notify
448	    _bleChar->setValue("");  // 初始为空
449
450	    svc->start();
451
452	    _bleAdv = _bleServer->getAdvertising();
453	    BLEAdvertisementData advData;
454	    advData.setCompleteServices(BLEUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b"));
455	    advData.setShortName("HealthMon");
456	    _bleAdv->setAdvertisementData(advData);
457	    _bleAdv->setScanResponse(false);  // 不需要 scan response, 全在主包
458	    _bleAdv->start();
459
460	    Serial.printf("[BLE] ✅ 配网广播: HealthMonitor  device_id=%s\n", _config.device_id);
461	}
462
463	void WiFiMgmt::_stopBLE() {
464	    if (!_bleServer) return;
465
466	    // ★ 核心修复: 先断开所有 BLE 客户端连接,
467	    //    否则手机端缓存连接状态, 下次扫描不到设备
468	    size_t connCount = _bleServer->getConnectedCount();
469	    if (connCount > 0) {
470	        Serial.printf("[BLE] 断开 %d 个已连接客户端\n", connCount);
471	        for (size_t i = 0; i < connCount; i++) {
472	            _bleServer->disconnect(i);
473	        }
474	        delay(300);  // 等待 BLE 协议栈完成断开流程
475	    }
476
477	    if (_bleAdv) { _bleAdv->stop(); }
478	    Serial.println("[BLE] 广播已暂停");
479	}
480
481	// BLE 线程安全入队 (仅暂存, 不在 BTC_TASK 回调里处理)
482	void WiFiMgmt::enqueueBLEJson(const char* json) {
483	    _blePendingJson  = json;
484	    _blePendingFlag  = true;
485	}
486
487	// 在 loop() 上下文中处理 BLE 配网数据 (安全 — 不在 BTC_TASK 栈中)
488	void WiFiMgmt::_processBLEPending() {
489	    if (!_blePendingFlag) return;
490	    _blePendingFlag = false;
491	    Serial.printf("[BLE] loop 处理配网: %s\n", _blePendingJson.c_str());
492	    handleBLEProvision(_blePendingJson.c_str());
493	    _blePendingJson = "";
494	}
495
496	void WiFiMgmt::handleBLEProvision(const char* json) {
497	    JsonDocument doc;
498	    DeserializationError err = deserializeJson(doc, json);
499	    if (err) {
500	        Serial.printf("[BLE] JSON 解析失败: %s\n", err.c_str());
501	        if (_bleChar) { _bleChar->setValue("ERR_JSON"); _bleChar->notify(); }
502	        return;
503	    }
504
505	    // 提取 WiFi 凭证
506	    const char* ssid = doc["s"] | "";
507	    const char* pass = doc["p"] | "";
508
509	    if (strlen(ssid) == 0) {
510	        Serial.println("[BLE] 未提供 WiFi SSID");
511	        if (_bleChar) { _bleChar->setValue("ERR_NO_SSID"); _bleChar->notify(); }
512	        return;
513	    }
514
515	    _pendingSSID = ssid;
516	    _pendingPass = pass;
517
518	    // 保存到 LittleFS (和 Web 配网完全相同的逻辑)
519	    strncpy(_config.wifi_ssid, ssid, sizeof(_config.wifi_ssid) - 1);
520	    strncpy(_config.wifi_pass, pass, sizeof(_config.wifi_pass) - 1);
521
522	    if (doc.containsKey("device_id"))   strncpy(_config.device_id,   doc["device_id"]   | "", sizeof(_config.device_id) - 1);
523	    if (doc.containsKey("env_note"))    strncpy(_config.env_note,    doc["env_note"]    | "", sizeof(_config.env_note) - 1);
524	    if (doc.containsKey("env_type"))    strncpy(_config.env_type,    doc["env_type"]    | "", sizeof(_config.env_type) - 1);
525	    if (doc.containsKey("mqtt_server")) strncpy(_config.mqtt_server, doc["mqtt_server"] | "", sizeof(_config.mqtt_server) - 1);
526	    if (doc.containsKey("mqtt_port"))   strncpy(_config.mqtt_port,   doc["mqtt_port"]   | "", sizeof(_config.mqtt_port) - 1);
527	    if (doc.containsKey("mqtt_user"))   strncpy(_config.mqtt_user,   doc["mqtt_user"]   | "", sizeof(_config.mqtt_user) - 1);
528	    if (doc.containsKey("mqtt_pass"))   strncpy(_config.mqtt_pass,   doc["mqtt_pass"]   | "", sizeof(_config.mqtt_pass) - 1);
529
530	    _saveConfig();
531	    _portalStopFlag = true;  // 触发延迟关闭 AP+BLE → 连接 WiFi
532
533	    Serial.printf("[BLE] ✅ 配网完成, SSID=%s\n", ssid);
534
535	    // 回复 OK
536	    if (_bleChar) { _bleChar->setValue("OK"); _bleChar->notify(); }
537	    delay(100);
538	}
539
540	// ============================================================
541	// 重置
542	// ============================================================
543
544	void WiFiMgmt::resetSettings() {
545	    Serial.println(("[WiFi] 清除所有配置..."));
546	    if (LittleFS.exists(CONFIG_FILE_WIFI))   LittleFS.remove(CONFIG_FILE_WIFI);
547	    if (LittleFS.exists(CONFIG_FILE_MQTT))   LittleFS.remove(CONFIG_FILE_MQTT);
548	    if (LittleFS.exists(CONFIG_FILE_DEVICE)) LittleFS.remove(CONFIG_FILE_DEVICE);
549	    WiFi.disconnect(true, true);
550	    for (int i = 0; i < 10; i++) { digitalWrite(LED_WIFI_PIN, !digitalRead(LED_WIFI_PIN)); delay(100); }
551	    Serial.println(("[WiFi] 正在重启..."));
552	    delay(500);
553	    ESP.restart();
554	}
555
556	// ============================================================
557	// 按钮
558	// ============================================================
559
560	bool WiFiMgmt::_checkButtonLongPress() {
561	    bool pressed = (digitalRead(CONFIG_BUTTON_PIN) == LOW);
562	    if (pressed && !_buttonPressed) {
563	        _buttonPressed = true; _buttonPressTime = millis();
564	    } else if (!pressed && _buttonPressed) {
565	        _buttonPressed = false;
566	        if (millis() - _buttonPressTime >= BUTTON_LONG_PRESS_MS) return true;
567	    }
568	    return false;
569	}
570
571	// ============================================================
572	// LittleFS
573	// ============================================================
574
575	bool WiFiMgmt::_loadConfig() {
576	    if (!LittleFS.exists(CONFIG_FILE_DEVICE)) return false;
577	    File file = LittleFS.open(CONFIG_FILE_DEVICE, "r");
578	    if (!file) return false;
579	    JsonDocument doc;
580	    DeserializationError err = deserializeJson(doc, file);
581	    file.close();
582	    if (err) return false;
583	    strncpy(_config.mqtt_server, doc["mqtt_server"] | MQTT_DEFAULT_SERVER, sizeof(_config.mqtt_server)-1);
584	    strncpy(_config.mqtt_port, doc["mqtt_port"] | MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port)-1);
585	    strncpy(_config.mqtt_user, doc["mqtt_user"] | MQTT_DEFAULT_USER, sizeof(_config.mqtt_user)-1);
586	    strncpy(_config.mqtt_pass, doc["mqtt_pass"] | MQTT_DEFAULT_PASS, sizeof(_config.mqtt_pass)-1);
587	    strncpy(_config.device_id, doc["device_id"] | MQTT_DEFAULT_DEVICE, sizeof(_config.device_id)-1);
588	    strncpy(_config.env_note, doc["env_note"] | MQTT_DEFAULT_ENV, sizeof(_config.env_note)-1);
589	    strncpy(_config.env_type, doc["env_type"] | "bedroom", sizeof(_config.env_type)-1);
590	    strncpy(_config.wifi_ssid, doc["wifi_ssid"] | "", sizeof(_config.wifi_ssid)-1);
591	    strncpy(_config.wifi_pass, doc["wifi_pass"] | "", sizeof(_config.wifi_pass)-1);
592	    Serial.printf("[WiFi] 配置加载, 已存WiFi: %s\n", _config.wifi_ssid[0] ? _config.wifi_ssid : "(空)");
593	    return true;
594	}
595
596	bool WiFiMgmt::_saveConfig() {
597	    JsonDocument doc;
598	    doc["mqtt_server"] = _config.mqtt_server;
599	    doc["mqtt_port"]   = _config.mqtt_port;
600	    doc["mqtt_user"]   = _config.mqtt_user;
601	    doc["mqtt_pass"]   = _config.mqtt_pass;
602	    doc["device_id"]   = _config.device_id;
603	    doc["env_note"]    = _config.env_note;
604	    doc["env_type"]    = _config.env_type;
605	    doc["wifi_ssid"]   = _config.wifi_ssid;
606	    doc["wifi_pass"]   = _config.wifi_pass;
607	    File file = LittleFS.open(CONFIG_FILE_DEVICE, "w");
608	    if (!file) return false;
609	    if (serializeJson(doc, file) == 0) { file.close(); return false; }
610	    file.close();
611	    return true;
612	}
613
614	// ============================================================
615	// Getter
616	// ============================================================
617
618	bool WiFiMgmt::isConnected() const { return _connected && (WiFi.status() == WL_CONNECTED); }
619	bool WiFiMgmt::justProvisioned() { bool v = _provisionSuccess; _provisionSuccess = false; return v; }
620	DeviceConfig WiFiMgmt::getConfig() const { return _config; }
621	const char* WiFiMgmt::getDeviceId() const { return _config.device_id; }
622	const char* WiFiMgmt::getEnvNote() const { return _config.env_note; }
623
624	// ============================================================
625	// LED
626	// ============================================================
627
628	void WiFiMgmt::_setLEDMode(uint8_t mode) { _ledMode = mode; }
629
630	void WiFiMgmt::_ledTaskFunc(void* param) {
631	    const TickType_t fastMs = pdMS_TO_TICKS(300);
632	    const TickType_t slowMs = pdMS_TO_TICKS(1000);
633	    while (true) {
634	        if (!_instance) { vTaskDelay(slowMs); continue; }
635	        uint8_t mode = _instance->_ledMode;
636	        switch (mode) {
637	        case 0: digitalWrite(LED_WIFI_PIN, LOW); vTaskDelay(slowMs); break;
638	        case 1: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(slowMs);
639	                digitalWrite(LED_WIFI_PIN, LOW);  vTaskDelay(slowMs); break;
640	        case 2: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(fastMs);
641	                digitalWrite(LED_WIFI_PIN, LOW);  vTaskDelay(fastMs); break;
642	        case 3: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(slowMs); break;
643	        }
644	    }
645	}
646

/**
 * @file wifi_manager.cpp
 * @brief WiFi 配网管理器 — AP + WebServer
 */

#include "wifi_manager.h"

// BLE 配网回调: 小程序写入 JSON → 仅在 BLE 栈中暂存, 不直接处理
class BLEProvisionCB : public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic* ch) override {
        std::string val = ch->getValue();
        if (val.length() > 0 && WiFiMgmt::_instance) {
            Serial.printf("[BLE] 收到配网数据 (%d 字节)\n", val.length());
            // 仅暂存 → 在 loop() 里处理 (BTC_TASK 栈太小, 写 Flash 会栈溢出)
            WiFiMgmt::_instance->enqueueBLEJson(val.c_str());
        }
    }
};

WiFiMgmt* WiFiMgmt::_instance = nullptr;

WiFiMgmt::WiFiMgmt()
    : _server(nullptr), _dns(nullptr)
    , _connected(false)
    , _portalActive(false), _portalStopFlag(false)
    , _portalStartTime(0)
    , _lastCheckTime(0), _buttonPressTime(0), _buttonPressed(false)
    , _lastScanTime(0)
    , _provisionSuccess(false)
    , _ledTaskHandle(nullptr), _ledMode(0)
    , _bleServer(nullptr), _bleInfoChar(nullptr), _bleChar(nullptr), _bleAdv(nullptr)
    , _blePendingFlag(false)
{
    memset(&_config, 0, sizeof(_config));
    _instance = this;
}

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

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

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

    pinMode(CONFIG_BUTTON_PIN, INPUT_PULLUP);
    pinMode(LED_WIFI_PIN, OUTPUT);
    digitalWrite(LED_WIFI_PIN, LOW);

    xTaskCreate(_ledTaskFunc, "WiFiLed", 1024, nullptr, 1, &_ledTaskHandle);

    // 冷启动延迟, 等 Flash 稳定
    delay(500);
    if (!LittleFS.begin(true)) {
        Serial.println(("[WiFi] LittleFS 挂载失败!"));
        return false;
    }
    Serial.println(("[WiFi] LittleFS 就绪"));

    if (!_loadConfig()) {
        Serial.println(("[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);
        strncpy(_config.env_type,    "bedroom",        sizeof(_config.env_type)-1);
        memset(_config.wifi_ssid, 0, sizeof(_config.wifi_ssid));
        memset(_config.wifi_pass, 0, sizeof(_config.wifi_pass));
    }

    WiFi.setAutoReconnect(true);

    // 有配置 → 直接连
    bool hasConfig = LittleFS.exists(CONFIG_FILE_DEVICE);
    Serial.printf("[WiFi] 配置文件: %s\n", hasConfig ? "存在" : "不存在");
    if (hasConfig) {
        _connected = _connectWiFi();
        Serial.printf("[WiFi] 自动连接: %s\n", _connected ? "成功" : "失败");
    } else {
        Serial.println(("[WiFi] 无配置, 跳过连接"));
        _connected = false;
    }

    if (!_connected) {
        _setLEDMode(2);  // 快闪 = 配网模式
        _startPortal();  // 只开 AP
    } else {
        _setLEDMode(3);  // 常亮
    }

    _lastCheckTime = millis();
    return _connected;
}

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

bool WiFiMgmt::loop() {
    // --- BLE 待处理数据 (从 loop 上下文安全处理, 不占 BTC_TASK 栈) ---
    _processBLEPending();

    // --- Portal 模式 ---
    if (_portalActive) {
        if (_portalStopFlag) {
            _stopPortal();
            _portalStopFlag = false;
            if (_pendingSSID.length() > 0) {
                Serial.printf("[WiFi] 尝试连接: %s\n", _pendingSSID.c_str());
                _connected = _tryConnect(_pendingSSID.c_str(), _pendingPass.c_str());
                _pendingSSID = ""; _pendingPass = "";
                if (_connected) {
                    _setLEDMode(3);
                    _provisionSuccess = true;  // 标记配网成功
                } else {
                    _setLEDMode(2);
                    _startPortal();
                }
            }
            return _connected;
        }

        _dns->processNextRequest();
        _server->handleClient();
        delay(10);
        return _connected;
    }

    // --- 正常运行 ---
    if (_checkButtonLongPress()) {
        Serial.println(("[WiFi] 长按, 进配网模式"));
        _setLEDMode(2);
        WiFi.disconnect();
        _startPortal();
        _connected = false;
        return false;
    }

    if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {
        _lastCheckTime = millis();
        wl_status_t s = WiFi.status();
        if (s != WL_CONNECTED) {
            if (_connected) {
                _connected = false;
                _setLEDMode(1);
                Serial.printf("[WiFi] 断开! status=%d RSSI=%d\n", s, WiFi.RSSI());
            }
            // 不手动 reconnect(), 让 WiFi.setAutoReconnect(true) 自动处理
        } else if (!_connected) {
            _connected = true;
            _setLEDMode(3);
            Serial.printf("[WiFi] 已连接 RSSI=%d\n", WiFi.RSSI());
        }
    }

    return _connected;
}

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

bool WiFiMgmt::_connectWiFi() {
    if (_config.wifi_ssid[0] == '\0') {
        Serial.println(("[WiFi] 无已存WiFi密码"));
        return false;
    }
    Serial.printf("[WiFi] 连接: %s\n", _config.wifi_ssid);

    // 崩溃重启后 WiFi 模块可能残留旧状态, 彻底重置
    WiFi.disconnect(true);
    delay(200);
    WiFi.mode(WIFI_STA);
    delay(100);
    WiFi.begin(_config.wifi_ssid, _config.wifi_pass);
    int retry = 0;
    while (WiFi.status() != WL_CONNECTED && retry < 40) {  // 20s
        delay(500); Serial.print("."); yield(); retry++;
    }
    Serial.println();
    _connected = (WiFi.status() == WL_CONNECTED);
    Serial.printf("[WiFi] 连接结果: %s  status=%d\n", _connected ? "成功" : "失败", WiFi.status());
    if (_connected) Serial.printf("[WiFi] SSID=%s IP=%s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
    return _connected;
}

bool WiFiMgmt::_tryConnect(const char* ssid, const char* pass) {
    WiFi.mode(WIFI_STA);
    WiFi.persistent(true);   // 确保持久化 WiFi 密码到 NVS (断电不丢)
    WiFi.begin(ssid, pass);
    int retry = 0;
    while (WiFi.status() != WL_CONNECTED && retry < 40) {  // 20s
        delay(500); Serial.print("."); retry++;
    }
    bool ok = (WiFi.status() == WL_CONNECTED);
    Serial.printf("\n[WiFi] 连接结果: %s  status=%d\n", ok ? "成功!" : "失败", WiFi.status());
    return ok;
}

// ============================================================
// 配网 Portal (AP + WebServer)
// ============================================================

void WiFiMgmt::startConfigPortal() {
    _setLEDMode(2);
    _startPortal();
}

void WiFiMgmt::_startPortal() {
    if (_portalActive) return;

    WiFi.disconnect(true);
    delay(200);
    WiFi.mode(WIFI_AP);
    WiFi.softAPConfig(IPAddress(192,168,4,1), IPAddress(192,168,4,1), IPAddress(255,255,255,0));
    WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);
    Serial.printf("[WiFi] AP: %s  IP: 192.168.4.1\n", WIFI_AP_SSID);

    // DNS 劫持: 所有域名 → 192.168.4.1 → 触发手机强制门户弹窗
    _dns = new DNSServer();
    _dns->start(53, "*", WiFi.softAPIP());

    _server = new WebServer(80);
    _server->on("/",      HTTP_GET,  [this]() { _handleRoot(); });
    _server->on("/save",  HTTP_POST, [this]() { _handleSave(); });
    _server->on("/reset", HTTP_GET,  [this]() { _handleReset(); });
    _server->onNotFound(  [this]() { _handleRoot(); });
    _server->begin();

    _portalActive = true;
    _portalStartTime = millis();

    Serial.println(("[WiFi] 手机连 AP 后浏览器打开 http://192.168.4.1"));

    _startBLE();  // 同时启动 BLE 配网广播
}

void WiFiMgmt::_stopPortal() {
    if (!_portalActive) return;
    _stopBLE();
    if (_server) { _server->stop(); delete _server; _server = nullptr; }
    if (_dns)    { _dns->stop();    delete _dns;    _dns = nullptr; }
    WiFi.softAPdisconnect(true);
    _portalActive = false;
}

// ============================================================
// Web 处理器
// ============================================================

void WiFiMgmt::_handleRoot() {
    _server->send(200, "text/html; charset=utf-8", _htmlPage());
}

void WiFiMgmt::_handleSave() {
    if (_server->hasArg("s")) {
        _pendingSSID = _server->arg("s");
        _pendingPass = _server->arg("p");
        // 持久化 WiFi 密码到 LittleFS
        strncpy(_config.wifi_ssid, _pendingSSID.c_str(), sizeof(_config.wifi_ssid)-1);
        strncpy(_config.wifi_pass, _pendingPass.c_str(), sizeof(_config.wifi_pass)-1);
        if (_server->hasArg("device_id"))  strncpy(_config.device_id, _server->arg("device_id").c_str(), sizeof(_config.device_id)-1);
        if (_server->hasArg("env_type"))   strncpy(_config.env_type,  _server->arg("env_type").c_str(),  sizeof(_config.env_type)-1);
        if (_server->hasArg("env_note"))   strncpy(_config.env_note,  _server->arg("env_note").c_str(),  sizeof(_config.env_note)-1);
        if (_server->hasArg("mqtt_server")) strncpy(_config.mqtt_server, _server->arg("mqtt_server").c_str(), sizeof(_config.mqtt_server)-1);
        if (_server->hasArg("mqtt_port"))  strncpy(_config.mqtt_port,  _server->arg("mqtt_port").c_str(), sizeof(_config.mqtt_port)-1);
        if (_server->hasArg("mqtt_user"))  strncpy(_config.mqtt_user,  _server->arg("mqtt_user").c_str(), sizeof(_config.mqtt_user)-1);
        if (_server->hasArg("mqtt_pass"))  strncpy(_config.mqtt_pass,  _server->arg("mqtt_pass").c_str(), sizeof(_config.mqtt_pass)-1);
        _saveConfig();

        _server->send(200, "text/html; charset=utf-8",
            "<!DOCTYPE html><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1'>"
            "<title>配置完成</title>

⏳
"
            "
配置已保存

设备正在连接 WiFi...
等待指示灯常亮
"
            "
");

        _portalStopFlag = true;
        return;
    }
    _handleRoot();
}

void WiFiMgmt::_handleReset() {
    _server->send(200, "text/html; charset=utf-8",
        "<!DOCTYPE html><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>已重置</title>"
        "

配置已清除

设备正在重启...

");
    delay(500);
    _stopPortal();
    resetSettings();
}

// ============================================================
// 配网 HTML 页面 (精简版)
// ============================================================

void WiFiMgmt::_updateWifiScan() {
    _wifiScanCache = "";
    int n = WiFi.scanNetworks(false, true);
    for (int i = 0; i < n; i++) {
        _wifiScanCache += "<option value='" + WiFi.SSID(i) + "'>" + WiFi.SSID(i) + " (" + String(WiFi.RSSI(i)) + "dBm)</option>";
    }
    if (n == 0) _wifiScanCache = "<option value=''>未发现 WiFi</option>";
    WiFi.scanDelete();
    _lastScanTime = millis();
}

String WiFiMgmt::_htmlPage() {
    if (_wifiScanCache.length() == 0) _updateWifiScan();

    String html;
    html.reserve(4096);
    html += "<!DOCTYPE html><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'>";
    html += "<meta name='theme-color' content='#12a889'><title>设备配网</title>
";

    // Header + 提示
    html += "

📡

观照-五劳无感 设备配网

WiFi 联网与参数配置

";
    html += "
💡 也可用 ESP-Touch App 一键配网
";

    html += "<form method='POST' action='/save'>";
    html += "

📶

无线网络

选择 WiFi 或使用 SmartConfig

";
    html += "
WiFi 名称
📡<select name='s'>";
    html += _wifiScanCache;
    html += "</select>

";
    html += "
WiFi 密码
🔒<input type='password' name='p' placeholder='请输入 WiFi 密码'>

";

    html += "

⚙

设备基础信息

唯一标识与安装位置

";
    html += "
设备编号
#<input type='text' name='device_id' value='";
    html += _config.device_id;
    html += "'>

";
    html += "
环境类型
🏷<select name='env_type'>";
    const char* envTypes[] = {"bedroom","livingroom","bathroom","hallway","balcony","kitchen"};
    const char* envLabels[] = {"🛏 卧室","🛋 客厅","🛁 浴室/卫生间","🚶 走廊","🌿 阳台","🍳 厨房"};
    for (int i = 0; i < 6; i++) {
        html += "<option value='";
        html += envTypes[i];
        html += "'";
        if (strcmp(_config.env_type, envTypes[i]) == 0 || (strlen(_config.env_type) == 0 && i == 0))
            html += " selected";
        html += ">";
        html += envLabels[i];
        html += "</option>";
    }
    html += "</select>

";
    html += "
位置备注
🏠<input type='text' name='env_note' placeholder='如: 主卧 / 客厅 / 老人房' value='";
    html += _config.env_note;
    html += "'>

";

    html += "<input type='hidden' name='mqtt_server' value='";
    html += _config.mqtt_server;
    html += "'><input type='hidden' name='mqtt_port' value='";
    html += _config.mqtt_port;
    html += "'><input type='hidden' name='mqtt_user' value='";
    html += _config.mqtt_user;
    html += "'><input type='hidden' name='mqtt_pass' value='";
    html += _config.mqtt_pass;
    html += "'>";

    html += "
<button type='submit'>保存配置并重启连接</button>
</form>";
    html += "<button class='danger' onclick=\"if(confirm('确定恢复出厂?'))location.href='/reset'\">恢复出厂配置</button>";
    html += "
Health Monitor · Device Provisioning

";

    return html;
}

// ============================================================
// BLE 蓝牙配网
// ============================================================

void WiFiMgmt::_startBLE() {
    // 如果 BLE 栈已在运行, 仅重启广播 (避免 deinit→init 失败)
    if (_bleServer != nullptr) {
        if (_bleAdv) {
            // 重设广告数据 (stop 后 start 需要重新配置)
            BLEAdvertisementData advData;
            advData.setCompleteServices(BLEUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b"));
            advData.setShortName("HealthMon");
            _bleAdv->setAdvertisementData(advData);
            _bleAdv->setScanResponse(false);
            _bleAdv->start();
        }
        Serial.println("[BLE] ✅ 广播已重开");
        return;
    }

    BLEDevice::init("HealthMonitor");
    _bleServer = BLEDevice::createServer();

    BLEService* svc = _bleServer->createService("4fafc201-1fb5-459e-8fcc-c5c9c331914b");

    // 特征1: 设备信息 (READ) — 小程序连接后读取设备编号等
    _bleInfoChar = svc->createCharacteristic(
        "4fafc202-1fb5-459e-8fcc-c5c9c331914b",
        BLECharacteristic::PROPERTY_READ
    );
    // 设备信息 JSON (最多 512 字节)
    JsonDocument info;
    info["device_id"]   = _config.device_id;
    info["mqtt_server"] = _config.mqtt_server;
    info["mqtt_port"]   = _config.mqtt_port;
    info["fw_version"]  = FW_VERSION;
    String infoJson;
    serializeJson(info, infoJson);
    _bleInfoChar->setValue(infoJson.c_str());

    // 特征2: 配网写入 (WRITE + NOTIFY) — 小程序发配网数据, 设备回复
    _bleChar = svc->createCharacteristic(
        "4fafc203-1fb5-459e-8fcc-c5c9c331914b",
        BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY
    );
    _bleChar->setCallbacks(new BLEProvisionCB());
    _bleChar->addDescriptor(new BLE2902());  // 启用 notify
    _bleChar->setValue("");  // 初始为空

    svc->start();

    _bleAdv = _bleServer->getAdvertising();
    BLEAdvertisementData advData;
    advData.setCompleteServices(BLEUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b"));
    advData.setShortName("HealthMon");
    _bleAdv->setAdvertisementData(advData);
    _bleAdv->setScanResponse(false);  // 不需要 scan response, 全在主包
    _bleAdv->start();

    Serial.printf("[BLE] ✅ 配网广播: HealthMonitor  device_id=%s\n", _config.device_id);
}

void WiFiMgmt::_stopBLE() {
    if (!_bleServer) return;

    // ★ 核心修复: 先断开所有 BLE 客户端连接,
    //    否则手机端缓存连接状态, 下次扫描不到设备
    size_t connCount = _bleServer->getConnectedCount();
    if (connCount > 0) {
        Serial.printf("[BLE] 断开 %d 个已连接客户端\n", connCount);
        for (size_t i = 0; i < connCount; i++) {
            _bleServer->disconnect(i);
        }
        delay(300);  // 等待 BLE 协议栈完成断开流程
    }

    if (_bleAdv) { _bleAdv->stop(); }
    Serial.println("[BLE] 广播已暂停");
}

// BLE 线程安全入队 (仅暂存, 不在 BTC_TASK 回调里处理)
void WiFiMgmt::enqueueBLEJson(const char* json) {
    _blePendingJson  = json;
    _blePendingFlag  = true;
}

// 在 loop() 上下文中处理 BLE 配网数据 (安全 — 不在 BTC_TASK 栈中)
void WiFiMgmt::_processBLEPending() {
    if (!_blePendingFlag) return;
    _blePendingFlag = false;
    Serial.printf("[BLE] loop 处理配网: %s\n", _blePendingJson.c_str());
    handleBLEProvision(_blePendingJson.c_str());
    _blePendingJson = "";
}

void WiFiMgmt::handleBLEProvision(const char* json) {
    JsonDocument doc;
    DeserializationError err = deserializeJson(doc, json);
    if (err) {
        Serial.printf("[BLE] JSON 解析失败: %s\n", err.c_str());
        if (_bleChar) { _bleChar->setValue("ERR_JSON"); _bleChar->notify(); }
        return;
    }

    // 提取 WiFi 凭证
    const char* ssid = doc["s"] | "";
    const char* pass = doc["p"] | "";

    if (strlen(ssid) == 0) {
        Serial.println("[BLE] 未提供 WiFi SSID");
        if (_bleChar) { _bleChar->setValue("ERR_NO_SSID"); _bleChar->notify(); }
        return;
    }

    _pendingSSID = ssid;
    _pendingPass = pass;

    // 保存到 LittleFS (和 Web 配网完全相同的逻辑)
    strncpy(_config.wifi_ssid, ssid, sizeof(_config.wifi_ssid) - 1);
    strncpy(_config.wifi_pass, pass, sizeof(_config.wifi_pass) - 1);

    if (doc.containsKey("device_id"))   strncpy(_config.device_id,   doc["device_id"]   | "", sizeof(_config.device_id) - 1);
    if (doc.containsKey("env_note"))    strncpy(_config.env_note,    doc["env_note"]    | "", sizeof(_config.env_note) - 1);
    if (doc.containsKey("env_type"))    strncpy(_config.env_type,    doc["env_type"]    | "", sizeof(_config.env_type) - 1);
    if (doc.containsKey("mqtt_server")) strncpy(_config.mqtt_server, doc["mqtt_server"] | "", sizeof(_config.mqtt_server) - 1);
    if (doc.containsKey("mqtt_port"))   strncpy(_config.mqtt_port,   doc["mqtt_port"]   | "", sizeof(_config.mqtt_port) - 1);
    if (doc.containsKey("mqtt_user"))   strncpy(_config.mqtt_user,   doc["mqtt_user"]   | "", sizeof(_config.mqtt_user) - 1);
    if (doc.containsKey("mqtt_pass"))   strncpy(_config.mqtt_pass,   doc["mqtt_pass"]   | "", sizeof(_config.mqtt_pass) - 1);

    _saveConfig();
    _portalStopFlag = true;  // 触发延迟关闭 AP+BLE → 连接 WiFi

    Serial.printf("[BLE] ✅ 配网完成, SSID=%s\n", ssid);

    // 回复 OK
    if (_bleChar) { _bleChar->setValue("OK"); _bleChar->notify(); }
    delay(100);
}

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

void WiFiMgmt::resetSettings() {
    Serial.println(("[WiFi] 清除所有配置..."));
    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);
    WiFi.disconnect(true, true);
    for (int i = 0; i < 10; i++) { digitalWrite(LED_WIFI_PIN, !digitalRead(LED_WIFI_PIN)); delay(100); }
    Serial.println(("[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) 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);
    strncpy(_config.env_type, doc["env_type"] | "bedroom", sizeof(_config.env_type)-1);
    strncpy(_config.wifi_ssid, doc["wifi_ssid"] | "", sizeof(_config.wifi_ssid)-1);
    strncpy(_config.wifi_pass, doc["wifi_pass"] | "", sizeof(_config.wifi_pass)-1);
    Serial.printf("[WiFi] 配置加载, 已存WiFi: %s\n", _config.wifi_ssid[0] ? _config.wifi_ssid : "(空)");
    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;
    doc["env_type"]    = _config.env_type;
    doc["wifi_ssid"]   = _config.wifi_ssid;
    doc["wifi_pass"]   = _config.wifi_pass;
    File file = LittleFS.open(CONFIG_FILE_DEVICE, "w");
    if (!file) return false;
    if (serializeJson(doc, file) == 0) { file.close(); return false; }
    file.close();
    return true;
}

// ============================================================
// Getter
// ============================================================

bool WiFiMgmt::isConnected() const { return _connected && (WiFi.status() == WL_CONNECTED); }
bool WiFiMgmt::justProvisioned() { bool v = _provisionSuccess; _provisionSuccess = false; return v; }
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) { _ledMode = mode; }

void WiFiMgmt::_ledTaskFunc(void* param) {
    const TickType_t fastMs = pdMS_TO_TICKS(300);
    const TickType_t slowMs = pdMS_TO_TICKS(1000);
    while (true) {
        if (!_instance) { vTaskDelay(slowMs); continue; }
        uint8_t mode = _instance->_ledMode;
        switch (mode) {
        case 0: digitalWrite(LED_WIFI_PIN, LOW); vTaskDelay(slowMs); break;
        case 1: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(slowMs);
                digitalWrite(LED_WIFI_PIN, LOW);  vTaskDelay(slowMs); break;
        case 2: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(fastMs);
                digitalWrite(LED_WIFI_PIN, LOW);  vTaskDelay(fastMs); break;
        case 3: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(slowMs); break;
        }
    }
}


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

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

#include "mqtt_manager.h"

MQTTManager* MQTTManager::_instance = nullptr;

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

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

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

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

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

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

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

    return true;
}

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

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

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

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

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

    // Client ID = gateid
    String clientId = _gateid;

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

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

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

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

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

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

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

    return _connected;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    String payload;
    serializeJson(doc, payload);

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

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

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

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

    String payload;
    serializeJson(doc, payload);

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

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

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

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

    String payload;
    serializeJson(doc, payload);

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

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

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

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

    String payload;
    serializeJson(doc, payload);

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

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

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

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

    String payload;
    serializeJson(doc, payload);

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

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

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

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

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

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

    String payload;
    serializeJson(doc, payload);

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

// ============================================================
// 上行: 无人极简报文 (省4G流量, 仅 Presence + RadarValid)
// ============================================================

void MQTTManager::publishPresenceOnly(bool presence, bool radarValid, 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: ~120 字节 vs 完整报文 ~500 字节
    JsonDocument doc;
    doc["gateid"]   = _gateid;
    doc["type"]     = "report";
    doc["time"]     = timeStr;
    doc["sequence"] = String(sequence);
    doc["source"]   = "da";

    String meterId = String(_gateid) + "001";

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

    JsonObject v = m["values"].to<JsonObject>();
    v["Presence"]   = presence ? "1" : "0";
    v["RadarValid"] = radarValid ? "1" : "0";

    String payload;
    serializeJson(doc, payload);

    _mqtt.publish("report/allpoints", payload.c_str());
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


蓝牙配网后，没有发报文，但是重启后就有


[Request interrupted by user]


Connection error.

The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the second argument to fetch()


还没好吗


No response requested.


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

继续


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

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