/**
* @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",
"
"
"配置完成⏳
"
"
配置已保存
设备正在连接 WiFi...
等待指示灯常亮
"
"
");
_portalStopFlag = true;
return;
}
_handleRoot();
}
void WiFiMgmt::_handleReset() {
_server->send(200, "text/html; charset=utf-8",
"已重置"
"");
delay(500);
_stopPortal();
resetSettings();
}
// ============================================================
// 配网 HTML 页面 (精简版)
// ============================================================
void WiFiMgmt::_updateWifiScan() {
_wifiScanCache = "";
int n = WiFi.scanNetworks(false, true);
for (int i = 0; i < n; i++) {
_wifiScanCache += "";
}
if (n == 0) _wifiScanCache = "";
WiFi.scanDelete();
_lastScanTime = millis();
}
String WiFiMgmt::_htmlPage() {
if (_wifiScanCache.length() == 0) _updateWifiScan();
String html;
html.reserve(4096);
html += "";
html += "设备配网";
// Header + 提示
html += "
📡
观照-五劳无感 设备配网
WiFi 联网与参数配置
";
html += "
💡 也可用 ESP-Touch App 一键配网
";
html += "
";
html += "
";
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;
}
}
}