/** * @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 += "
📶
无线网络
选择 WiFi 或使用 SmartConfig
"; html += "
📡
"; html += "
🔒
"; html += "
⚙️
设备基础信息
唯一标识与安装位置
"; html += "
#
"; html += "
🏷️