Files
health-wellness-device-2/src/wifi_manager.cpp
2026-08-12 09:22:28 +08:00

655 lines
29 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @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();
// --- 配网完成 → WiFi 连接 (Portal 内/外 统一处理) ---
if (_portalStopFlag) {
if (_portalActive) {
_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; // 标记配网成功 → main 里触发 MQTT 重连
} else {
_setLEDMode(2);
if (!_portalActive) {
_startPortal(); // 连接失败 → 开 AP 重试
}
}
}
return _connected;
}
// --- Portal 模式 ---
if (_portalActive) {
_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) {
// ★ 先从 AP 模式彻底断开再切 STA和 _connectWiFi() 保持一致
WiFi.disconnect(true);
delay(200);
WiFi.mode(WIFI_STA);
delay(100);
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><html lang='zh-CN'><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1'>"
"<title>配置完成</title><style>body{font-family:sans-serif;background:#f3faf8;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}"
".box{text-align:center;background:#fff;padding:32px;border-radius:16px;box-shadow:0 2px 12px rgba(0,0,0,0.08);max-width:340px}"
"h1{color:#12a889;font-size:20px}.ok{font-size:48px;margin:16px}"
"p{color:#8aa09a;font-size:14px;line-height:1.8}"
"</style></head><body><div class='box'><div class='ok' id='icon'>⏳</div>"
"<h1 id='title'>配置已保存</h1><p id='msg'>设备正在连接 WiFi...<br>等待指示灯<b>常亮</b></p>"
"</div><script>"
"var n=0;setInterval(function(){n++;"
"if(n>8){document.getElementById('icon').textContent='✅';"
"document.getElementById('msg').innerHTML='请切换回您的 WiFi 网络<br><b>指示灯常亮</b>即连接成功';clearInterval()}"
"},2000);</script></body></html>");
_portalStopFlag = true;
return;
}
_handleRoot();
}
void WiFiMgmt::_handleReset() {
_server->send(200, "text/html; charset=utf-8",
"<!DOCTYPE html><html lang='zh-CN'><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>已重置</title>"
"<style>body{font-family:sans-serif;background:#f3faf8;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}"
".box{text-align:center;background:#fff;padding:32px;border-radius:16px;box-shadow:0 2px 12px rgba(0,0,0,0.08)}"
"h1{color:#f53f3f;font-size:18px}p{color:#8aa09a;font-size:14px}</style></head><body><div class='box'><h1>配置已清除</h1><p>设备正在重启...</p></div></body></html>");
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><html lang='zh-CN'><head><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><style>";
html += ":root{--p:#12a889;--bg:#f3faf8;--cb:#fff;--ch:#eef8f5;--cg:#e9fff8;--t:#15201d;--t2:#4e6660;--t3:#8aa09a}";
html += "*{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',sans-serif;background:var(--bg);min-height:100vh;padding:12px 16px 24px;color:var(--t);font-size:14px;line-height:1.5}";
html += ".wrap{max-width:420px;margin:0 auto}.hdr{display:flex;align-items:center;justify-content:center;position:relative;padding:20px 0 16px}";
html += ".hdricon{width:56px;height:56px;border-radius:50%;background:var(--cg);display:flex;align-items:center;justify-content:center;font-size:28px;margin-right:14px;box-shadow:0 4px 12px rgba(18,168,137,.12)}";
html += "h1{font-size:20px;font-weight:700;color:var(--t)}h3{font-size:12px;font-weight:400;color:var(--t2);margin-top:4px}";
html += ".card{background:var(--cb);border-radius:16px;padding:20px;margin-bottom:16px;box-shadow:0 2px 12px rgba(0,0,0,.06)}";
html += ".chdr{display:flex;align-items:center;gap:12px;margin-bottom:18px}.chicon{width:36px;height:36px;border-radius:10px;background:var(--cg);display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0}";
html += ".ctitle{font-size:16px;font-weight:600;color:var(--t)}.csub{font-size:12px;color:var(--t3);margin-top:2px}";
html += ".field{margin-top:14px}.field:first-child{margin-top:0}label{display:block;font-size:14px;font-weight:500;color:var(--t2);margin-bottom:8px}";
html += ".iwrap{display:flex;align-items:center;background:var(--ch);border-radius:10px;border:1px solid transparent;transition:border-color .2s}";
html += ".iicon{padding:0 10px;font-size:16px;color:var(--t3)}input,select{flex:1;width:100%;padding:12px 14px;font-size:14px;border:none;background:transparent;color:var(--t);outline:none}";
html += ".iwrap:focus-within{border-color:var(--p);background:#fff}select{-webkit-appearance:none;padding-right:30px}";
html += ".gate{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-radius:12px;background:var(--cg);margin-bottom:14px;font-size:13px}";
html += ".glabel{color:var(--t2)}.gid{color:var(--p);font-weight:600}";
html += ".btns{display:flex;flex-direction:column;gap:12px;margin-top:8px}";
html += "button{width:100%;height:48px;border:none;border-radius:24px;font-size:15px;font-weight:600;color:#fff;background:var(--p);box-shadow:0 2px 6px rgba(18,168,137,.2);cursor:pointer}";
html += "button.danger{background:#fff;color:#f53f3f;border:1px solid #ffd6d6;box-shadow:none}";
html += ".ft{text-align:center;font-size:11px;color:var(--t3);padding:20px 0 6px}";
html += "@media(prefers-color-scheme:dark){body{background:#0f1f1c;color:#e6f0ee}.card{background:#182b27}.iwrap{background:#1f3530}.iwrap:focus-within{background:#243d38}}";
html += "</style></head><body><div class='wrap'>";
// Header + 提示
html += "<div class='hdr'><div class='hdricon'>📡</div><div><h1>观照-五劳无感 设备配网</h1><h3>WiFi 联网与参数配置</h3></div></div>";
html += "<div class='gate' style='justify-content:center'><span style='color:var(--p)'>💡 也可用 <b>ESP-Touch</b> App 一键配网</span></div>";
html += "<form method='POST' action='/save'>";
html += "<div class='card'><div class='chdr'><div class='chicon'>📶</div><div><div class='ctitle'>无线网络</div><div class='csub'>选择 WiFi 或使用 SmartConfig</div></div></div>";
html += "<div class='field'><label>WiFi 名称</label><div class='iwrap'><span class='iicon'>📡</span><select name='s'>";
html += _wifiScanCache;
html += "</select></div></div>";
html += "<div class='field'><label>WiFi 密码</label><div class='iwrap'><span class='iicon'>🔒</span><input type='password' name='p' placeholder='请输入 WiFi 密码'></div></div></div>";
html += "<div class='card'><div class='chdr'><div class='chicon'>⚙️</div><div><div class='ctitle'>设备基础信息</div><div class='csub'>唯一标识与安装位置</div></div></div>";
html += "<div class='field'><label>设备编号</label><div class='iwrap'><span class='iicon'>#</span><input type='text' name='device_id' value='";
html += _config.device_id;
html += "'></div></div>";
html += "<div class='field'><label>环境类型</label><div class='iwrap'><span class='iicon'>🏷️</span><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></div></div>";
html += "<div class='field'><label>位置备注</label><div class='iwrap'><span class='iicon'>🏠</span><input type='text' name='env_note' placeholder='如: 主卧 / 客厅 / 老人房' value='";
html += _config.env_note;
html += "'></div></div></div>";
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 += "<div class='btns'><button type='submit'>保存配置并重启连接</button></div></form>";
html += "<button class='danger' onclick=\"if(confirm('确定恢复出厂?'))location.href='/reset'\">恢复出厂配置</button>";
html += "<div class='ft'>Health Monitor · Device Provisioning</div></div></body></html>";
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;
}
}
}