#include "wifi_prov.h" #include #include #include #include static constexpr const char *NVS_NS = "wifi_prov"; static constexpr const char *NVS_SSID = "ssid"; static constexpr const char *NVS_PASS = "pass"; static Preferences g_prefs; static WifiProvState g_state = WIFI_PROV_IDLE; static WifiProvStateCb g_cb = nullptr; static void *g_cb_user = nullptr; static char g_ssid[WIFI_PROV_SSID_MAX + 1] = {}; static char g_pass[WIFI_PROV_PASS_MAX + 1] = {}; static bool g_has_creds = false; static uint32_t g_connect_started_ms = 0; static uint32_t g_last_reconnect_ms = 0; static bool g_want_connect = false; static DNSServer *g_dns = nullptr; static WebServer *g_http = nullptr; static bool g_portal = false; static char g_ap_ssid[33] = WIFI_PROV_AP_SSID; static bool g_scan_async = false; static void set_state(WifiProvState s) { if (g_state == s) { return; } g_state = s; if (g_cb) { g_cb(s, g_cb_user); } } static void load_creds() { g_has_creds = false; g_ssid[0] = '\0'; g_pass[0] = '\0'; if (!g_prefs.begin(NVS_NS, true)) { return; } String ssid = g_prefs.getString(NVS_SSID, ""); String pass = g_prefs.getString(NVS_PASS, ""); g_prefs.end(); if (ssid.length() == 0 || ssid.length() > WIFI_PROV_SSID_MAX) { return; } strncpy(g_ssid, ssid.c_str(), WIFI_PROV_SSID_MAX); g_ssid[WIFI_PROV_SSID_MAX] = '\0'; strncpy(g_pass, pass.c_str(), WIFI_PROV_PASS_MAX); g_pass[WIFI_PROV_PASS_MAX] = '\0'; g_has_creds = true; } static void begin_sta_connect(const char *ssid, const char *password) { WiFi.mode(WIFI_STA); WiFi.setAutoReconnect(true); WiFi.persistent(false); WiFi.disconnect(false, true); delay(50); WiFi.begin(ssid, password); g_want_connect = true; g_connect_started_ms = millis(); g_last_reconnect_ms = millis(); set_state(WIFI_PROV_CONNECTING); Serial.printf("[wifi] connecting to \"%s\"...\n", ssid); } static String html_escape(const String &s) { String o; o.reserve(s.length()); for (size_t i = 0; i < s.length(); ++i) { const char c = s[i]; if (c == '&') { o += "&"; } else if (c == '<') { o += "<"; } else if (c == '>') { o += ">"; } else if (c == '"') { o += """; } else { o += c; } } return o; } static String portal_page() { WifiScanItem items[WIFI_PROV_MAX_SCAN]; const int n = wifi_prov_scan(items, WIFI_PROV_MAX_SCAN); String html; html.reserve(2048); html += F("" "" "CYD WiFi" "" "

WiFi Setup

" "
" "" "" "" "" "
" "

AP: "); html += html_escape(String(g_ap_ssid)); html += F(" · http://192.168.4.1/

"); return html; } static void handle_root() { g_http->send(200, "text/html", portal_page()); } static void handle_save() { String ssid = g_http->arg("ssid_manual"); ssid.trim(); if (ssid.length() == 0) { ssid = g_http->arg("ssid"); ssid.trim(); } String pass = g_http->arg("pass"); g_http->send(200, "text/html", F("

Connecting...

" "

You can close this page. Device will leave setup AP.

")); if (ssid.length() > 0) { delay(200); wifi_prov_portal_stop(); wifi_prov_connect_to(ssid.c_str(), pass.c_str()); } } static void handle_captive() { g_http->sendHeader("Location", "http://192.168.4.1/", true); g_http->send(302, "text/plain", ""); } static void handle_not_found() { handle_captive(); } void wifi_prov_set_state_cb(WifiProvStateCb cb, void *user) { g_cb = cb; g_cb_user = user; } bool wifi_prov_begin() { load_creds(); WiFi.mode(WIFI_STA); WiFi.setAutoReconnect(true); WiFi.persistent(false); if (g_has_creds) { begin_sta_connect(g_ssid, g_pass); } else { set_state(WIFI_PROV_IDLE); Serial.println("[wifi] no credentials"); } return g_has_creds; } bool wifi_prov_has_credentials() { return g_has_creds; } bool wifi_prov_is_connected() { return WiFi.status() == WL_CONNECTED; } WifiProvState wifi_prov_state() { return g_state; } String wifi_prov_ssid() { if (WiFi.status() == WL_CONNECTED) { return WiFi.SSID(); } return String(g_ssid); } String wifi_prov_ip() { if (WiFi.status() != WL_CONNECTED) { return String(); } return WiFi.localIP().toString(); } int8_t wifi_prov_rssi() { if (WiFi.status() != WL_CONNECTED) { return 0; } return (int8_t)WiFi.RSSI(); } void wifi_prov_save(const char *ssid, const char *password) { if (!ssid || !ssid[0]) { return; } strncpy(g_ssid, ssid, WIFI_PROV_SSID_MAX); g_ssid[WIFI_PROV_SSID_MAX] = '\0'; if (password) { strncpy(g_pass, password, WIFI_PROV_PASS_MAX); } else { g_pass[0] = '\0'; } g_pass[WIFI_PROV_PASS_MAX] = '\0'; g_has_creds = true; if (g_prefs.begin(NVS_NS, false)) { g_prefs.putString(NVS_SSID, g_ssid); g_prefs.putString(NVS_PASS, g_pass); g_prefs.end(); } Serial.printf("[wifi] saved \"%s\"\n", g_ssid); } void wifi_prov_clear() { g_ssid[0] = '\0'; g_pass[0] = '\0'; g_has_creds = false; g_want_connect = false; if (g_prefs.begin(NVS_NS, false)) { g_prefs.clear(); g_prefs.end(); } WiFi.disconnect(true, true); set_state(WIFI_PROV_IDLE); Serial.println("[wifi] credentials cleared"); } void wifi_prov_connect_saved() { if (!g_has_creds) { set_state(WIFI_PROV_FAILED); return; } begin_sta_connect(g_ssid, g_pass); } void wifi_prov_connect_to(const char *ssid, const char *password) { wifi_prov_save(ssid, password ? password : ""); begin_sta_connect(g_ssid, g_pass); } static int fill_scan_results(WifiScanItem *out, int max_items, int found) { if (found <= 0) { WiFi.scanDelete(); return 0; } const int n = found < max_items ? found : max_items; for (int i = 0; i < n; ++i) { String s = WiFi.SSID(i); strncpy(out[i].ssid, s.c_str(), WIFI_PROV_SSID_MAX); out[i].ssid[WIFI_PROV_SSID_MAX] = '\0'; out[i].rssi = WiFi.RSSI(i); out[i].auth = WiFi.encryptionType(i); } WiFi.scanDelete(); return n; } int wifi_prov_scan(WifiScanItem *out, int max_items) { if (!out || max_items <= 0) { return 0; } if (max_items > WIFI_PROV_MAX_SCAN) { max_items = WIFI_PROV_MAX_SCAN; } if (g_scan_async) { WiFi.scanDelete(); g_scan_async = false; } if (g_portal) { WiFi.mode(WIFI_AP_STA); } else { WiFi.mode(WIFI_STA); } const int found = WiFi.scanNetworks(/*async=*/false, /*hidden=*/true); return fill_scan_results(out, max_items, found); } bool wifi_prov_scan_start() { if (g_scan_async) { return true; } if (g_portal) { WiFi.mode(WIFI_AP_STA); } else { WiFi.mode(WIFI_STA); } /* async — does not block LVGL loop */ const int16_t rc = WiFi.scanNetworks(/*async=*/true, /*hidden=*/true); if (rc == WIFI_SCAN_FAILED) { Serial.println("[wifi] scan start failed"); return false; } g_scan_async = true; Serial.println("[wifi] scan async started"); return true; } bool wifi_prov_scan_busy() { return g_scan_async; } int wifi_prov_scan_poll(WifiScanItem *out, int max_items) { if (!g_scan_async) { return 0; } if (!out || max_items <= 0) { return 0; } if (max_items > WIFI_PROV_MAX_SCAN) { max_items = WIFI_PROV_MAX_SCAN; } const int16_t found = WiFi.scanComplete(); if (found == WIFI_SCAN_RUNNING) { return -1; } g_scan_async = false; if (found == WIFI_SCAN_FAILED) { WiFi.scanDelete(); Serial.println("[wifi] scan failed"); return 0; } const int n = fill_scan_results(out, max_items, found); Serial.printf("[wifi] scan done n=%d\n", n); return n; } void wifi_prov_portal_start(const char *ap_ssid) { if (g_portal) { return; } if (ap_ssid && ap_ssid[0]) { strncpy(g_ap_ssid, ap_ssid, sizeof(g_ap_ssid) - 1); g_ap_ssid[sizeof(g_ap_ssid) - 1] = '\0'; } WiFi.mode(WIFI_AP_STA); WiFi.softAP(g_ap_ssid); delay(100); g_dns = new DNSServer(); g_http = new WebServer(80); g_dns->start(53, "*", WiFi.softAPIP()); g_http->on("/", HTTP_GET, handle_root); g_http->on("/save", HTTP_POST, handle_save); g_http->on("/generate_204", HTTP_GET, handle_captive); g_http->on("/gen_204", HTTP_GET, handle_captive); g_http->on("/hotspot-detect.html", HTTP_GET, handle_root); g_http->on("/connecttest.txt", HTTP_GET, handle_captive); g_http->on("/ncsi.txt", HTTP_GET, handle_captive); g_http->onNotFound(handle_not_found); g_http->begin(); g_portal = true; set_state(WIFI_PROV_PORTAL); Serial.printf("[wifi] portal AP \"%s\" @ %s\n", g_ap_ssid, WiFi.softAPIP().toString().c_str()); } void wifi_prov_portal_stop() { if (!g_portal) { return; } if (g_http) { g_http->stop(); delete g_http; g_http = nullptr; } if (g_dns) { g_dns->stop(); delete g_dns; g_dns = nullptr; } WiFi.softAPdisconnect(true); g_portal = false; Serial.println("[wifi] portal stopped"); } bool wifi_prov_portal_active() { return g_portal; } void wifi_prov_loop() { if (g_portal && g_dns && g_http) { g_dns->processNextRequest(); g_http->handleClient(); } const wl_status_t st = WiFi.status(); const uint32_t now = millis(); if (g_want_connect || g_state == WIFI_PROV_CONNECTING) { if (st == WL_CONNECTED) { g_want_connect = false; if (g_portal) { wifi_prov_portal_stop(); } set_state(WIFI_PROV_CONNECTED); Serial.printf("[wifi] connected %s ip=%s rssi=%d\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str(), WiFi.RSSI()); return; } if (now - g_connect_started_ms >= WIFI_PROV_CONNECT_TIMEOUT_MS || st == WL_CONNECT_FAILED || st == WL_NO_SSID_AVAIL) { g_want_connect = false; set_state(WIFI_PROV_FAILED); Serial.println("[wifi] connect failed"); return; } } // 已配置且非门户:断线后周期性重连 if (g_has_creds && !g_portal && g_state == WIFI_PROV_CONNECTED && st != WL_CONNECTED) { set_state(WIFI_PROV_CONNECTING); g_want_connect = true; g_connect_started_ms = now; } if (g_has_creds && !g_portal && st != WL_CONNECTED && g_state != WIFI_PROV_CONNECTING && now - g_last_reconnect_ms >= WIFI_PROV_RECONNECT_MS) { g_last_reconnect_ms = now; begin_sta_connect(g_ssid, g_pass); } }