Scan no longer blocks LVGL; always hw-reset GT911; larger buttons and click areas. Co-authored-by: Cursor <cursoragent@cursor.com>
459 lines
12 KiB
C++
459 lines
12 KiB
C++
#include "wifi_prov.h"
|
|
|
|
#include <DNSServer.h>
|
|
#include <Preferences.h>
|
|
#include <WebServer.h>
|
|
#include <cstring>
|
|
|
|
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("<!DOCTYPE html><html><head><meta charset=utf-8>"
|
|
"<meta name=viewport content=\"width=device-width,initial-scale=1\">"
|
|
"<title>CYD WiFi</title>"
|
|
"<style>"
|
|
"body{font-family:system-ui,sans-serif;margin:16px;background:#0f1419;color:#eaf0f6}"
|
|
"h1{font-size:1.25rem}label{display:block;margin:.6rem 0 .2rem;color:#9ab}"
|
|
"input,select,button{width:100%;box-sizing:border-box;padding:.7rem;border-radius:8px;"
|
|
"border:1px solid #334;background:#1a2330;color:#eaf0f6;font-size:1rem}"
|
|
"button{margin-top:1rem;background:#1a6aa8;border:none;font-weight:600}"
|
|
".hint{color:#7a8b9c;font-size:.85rem;margin-top:1rem}"
|
|
"</style></head><body>"
|
|
"<h1>WiFi Setup</h1>"
|
|
"<form method=POST action=/save>"
|
|
"<label>Network</label><select name=ssid>");
|
|
|
|
if (n <= 0) {
|
|
html += F("<option value=\"\">(no networks — type below)</option>");
|
|
} else {
|
|
for (int i = 0; i < n; ++i) {
|
|
html += F("<option value=\"");
|
|
html += html_escape(String(items[i].ssid));
|
|
html += F("\">");
|
|
html += html_escape(String(items[i].ssid));
|
|
html += F(" (");
|
|
html += String(items[i].rssi);
|
|
html += F(" dBm)</option>");
|
|
}
|
|
}
|
|
|
|
html += F("</select>"
|
|
"<label>Or SSID</label><input name=ssid_manual placeholder=\"manual SSID\">"
|
|
"<label>Password</label><input name=pass type=password placeholder=\"WiFi password\">"
|
|
"<button type=submit>Save & Connect</button>"
|
|
"</form>"
|
|
"<p class=hint>AP: ");
|
|
html += html_escape(String(g_ap_ssid));
|
|
html += F(" · http://192.168.4.1/</p></body></html>");
|
|
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("<!DOCTYPE html><html><body style=\"font-family:sans-serif;background:#0f1419;"
|
|
"color:#eaf0f6;padding:24px\"><h2>Connecting...</h2>"
|
|
"<p>You can close this page. Device will leave setup AP.</p></body></html>"));
|
|
|
|
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);
|
|
}
|
|
}
|