#include "WiFiConnect.h" #include #include "constants.h" // ---- CSS for the captive portal ---- static const char TPL_STYLE[] PROGMEM = ""; // ---- JavaScript for the WiFi configuration page (AJAX scan, password toggle, hidden network) ---- static const char TPL_WIFI_JS[] PROGMEM = "function scan(){" "var nl=document.getElementById('nl');" "nl.innerHTML='
';" "var x=new XMLHttpRequest;" "x.onload=function(){" "try{var d=JSON.parse(x.responseText);var h='';" "for(var i=0;i'" "+''" "+(n.e?'':'')+'';}" "nl.innerHTML=h||'
'+T.nn+'
';" "}catch(e){nl.innerHTML='
'+T.nn+'
';}};" "x.onerror=function(){nl.innerHTML='
'+T.nn+'
';};" "x.open('GET','/scan');x.send();}" "function esc(s){var d=document.createElement('div');" "d.appendChild(document.createTextNode(s));return d.innerHTML;}" "function sel(el){" "var s=el.querySelector('span').textContent;" "document.getElementById('s').value=s;" "document.getElementById('hn').checked=false;" "document.getElementById('p').focus();}" "function togglePw(){" "var p=document.getElementById('p');" "p.type=p.type==='password'?'text':'password';}" "function toggleHn(){" "var c=document.getElementById('hn').checked;" "var s=document.getElementById('s');" "if(c){s.value='';s.focus();}}" "window.addEventListener('load',scan);"; // ---- WiFiConnectParam implementation ---- WiFiConnectParam::WiFiConnectParam(const char *custom) { _id = NULL; _placeholder = NULL; _length = 0; _value = NULL; _customHTML = custom; } WiFiConnectParam::WiFiConnectParam(const char *id, const char *placeholder, const char *defaultValue, int length) { init(id, placeholder, defaultValue, length, ""); } void WiFiConnectParam::init(const char *id, const char *placeholder, const char *defaultValue, int length, const char *custom) { _id = id; _placeholder = placeholder; _length = length; _value = NULL; _customHTML = custom; setValue(defaultValue); } void WiFiConnectParam::setValue(const char *newValue) { if (_length > 0) { delete[] _value; _value = new char[_length + 1]; memset(_value, 0, _length + 1); if (newValue != NULL) strncpy(_value, newValue, _length); } } const char* WiFiConnectParam::getValue() { return _value; } const char* WiFiConnectParam::getID() { return _id; } const char* WiFiConnectParam::getPlaceholder() { return _placeholder; } int WiFiConnectParam::getValueLength() { return _length; } const char* WiFiConnectParam::getCustomHTML() { return _customHTML; } // ---- WiFiConnect implementation ---- WiFiConnect::WiFiConnect() { _apName[0] = '\0'; } void WiFiConnect::setAPName() { String ssid = "TEF_" + String((uint32_t)ESP.getEfuseMac()); strcpy(_apName, ssid.c_str()); } void WiFiConnect::addParameter(WiFiConnectParam *p) { if (_paramsCount < WiFiConnect_MAX_PARAMS) { _params[_paramsCount] = p; _paramsCount++; } } boolean WiFiConnect::autoConnect() { return autoConnect(NULL, NULL, WIFI_STA); } boolean WiFiConnect::autoConnect(char const *ssidName, char const *ssidPassword, WiFiMode_t acWiFiMode) { WiFi.mode(acWiFiMode); if (WiFi.status() == WL_CONNECTED) { return true; } int c = 0; while (c < RETRY_ATTEMPTS) { long ms = millis(); if (ssidName == NULL || strlen(ssidName) == 0) { wifi_config_t conf; esp_wifi_get_config(WIFI_IF_STA, &conf); String stored_ssid = String(reinterpret_cast(conf.sta.ssid)); if (stored_ssid == "") { return false; } WiFi.begin(); } else { WiFi.begin(ssidName, ssidPassword); } while (millis() - (unsigned long)ms < ((unsigned int)CONNECTION_TIMEOUT_SECS * 1000)) { int ws = WiFi.status(); if (ws == WL_CONNECTED) { delay(500); return true; } else if (ws == WL_CONNECT_FAILED) { delay(500); } else { delay(200); yield(); } } c++; } return false; } boolean WiFiConnect::startConfigurationPortal(int8_t cancelPin) { delay(50); if (WiFi.status() != WL_CONNECTED) { WiFi.mode(WIFI_AP); } else { WiFi.mode(WIFI_AP_STA); } dnsServer.reset(new DNSServer()); server.reset(new WebServer(80)); setAPName(); WiFi.softAP(_apName); delay(500); /* Setup the DNS server redirecting all domains to the AP IP */ dnsServer->setErrorReplyCode(DNSReplyCode::NoError); dnsServer->start(53, "*", WiFi.softAPIP()); /* Setup web pages: root, wifi config, scan API, logo, and not found */ server->on("/", std::bind(&WiFiConnect::handleRoot, this)); server->on("/wifi", std::bind(&WiFiConnect::handleWifi, this)); server->on("/scan", std::bind(&WiFiConnect::handleScan, this)); server->on("/wifisave", std::bind(&WiFiConnect::handleWifiSave, this)); server->on("/logo.png", std::bind(&WiFiConnect::handleLogo, this)); /* Captive portal detection endpoints — redirect to root to trigger popup */ server->on("/fwlink", std::bind(&WiFiConnect::handleRoot, this)); // Windows server->on("/redirect", std::bind(&WiFiConnect::handleRoot, this)); // Windows 10+ server->on("/hotspot-detect.html", std::bind(&WiFiConnect::handleRoot, this)); // Apple iOS/macOS server->on("/library/test/success.html", std::bind(&WiFiConnect::handleRoot, this)); // Apple legacy server->on("/generate_204", std::bind(&WiFiConnect::handleRoot, this)); // Android server->on("/gen_204", std::bind(&WiFiConnect::handleRoot, this)); // Android alt server->on("/connecttest.txt", std::bind(&WiFiConnect::handleRoot, this)); // Windows 11 server->on("/ncsi.txt", std::bind(&WiFiConnect::handleRoot, this)); // Windows NCSI server->onNotFound(std::bind(&WiFiConnect::handleNotFound, this)); server->begin(); if (cancelPin >= 0) { while (digitalRead(cancelPin) == LOW) delay(10); delay(200); } _readyToConnect = false; while (true) { dnsServer->processNextRequest(); server->handleClient(); if (_readyToConnect) { _readyToConnect = false; if (autoConnect(_ssid.c_str(), _password.c_str(), WIFI_AP_STA)) { WiFi.mode(WIFI_STA); delay(500); break; } } if (cancelPin >= 0 && digitalRead(cancelPin) == LOW) { delay(50); // debounce if (digitalRead(cancelPin) == LOW) break; } yield(); } server->close(); server.reset(); dnsServer.reset(); return (WiFi.status() == WL_CONNECTED); } // ---- Page handlers ---- void WiFiConnect::handleRoot() { if (captivePortal()) return; String page = F("" "" "" ""); page += textUI(311); page += F(""); page += FPSTR(TPL_STYLE); page += F("
" "

"); page += _apName; page += F("

"); page += textUI(297); page += F("
"); server->sendHeader("Content-Length", String(page.length())); server->send(200, "text/html", page); } void WiFiConnect::handleWifi() { String page = F("" "" "" ""); page += textUI(312); page += F(""); page += FPSTR(TPL_STYLE); page += F("
" "

"); page += _apName; page += F("

"); // ---- Networks card ---- page += F("

"); page += textUI(298); page += F("

" "
"); // ---- Connection form card ---- page += F("

"); page += textUI(304); page += F("

"); // Hidden network toggle page += F(""); // SSID field page += F(""); // Password field with SVG eye toggle page += F("
"); // Custom parameters for (int i = 0; i < _paramsCount; i++) { if (_params[i] == NULL) break; if (_params[i]->getID() != NULL) { char parLength[4]; snprintf(parLength, 4, "%d", _params[i]->getValueLength()); page += F("getCustomHTML(); page += F(">"); } else { page += F("

"); page += _params[i]->getCustomHTML(); page += F("

"); } } // Submit button page += F("
"); // JavaScript: inject translated text, then functions String nnText = textUI(313); nnText.replace("'", "\\'"); page += F("
"); server->sendHeader("Content-Length", String(page.length())); server->send(200, "text/html", page); } void WiFiConnect::handleScan() { int n = WiFi.scanNetworks(); String json = F("{\"n\":["); if (n > 0) { // Sort by RSSI int indices[n]; for (int i = 0; i < n; i++) indices[i] = i; for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { if (WiFi.RSSI(indices[j]) > WiFi.RSSI(indices[i])) { std::swap(indices[i], indices[j]); } } } // Remove duplicates (must be RSSI sorted) for (int i = 0; i < n; i++) { if (indices[i] == -1) continue; String cssid = WiFi.SSID(indices[i]); for (int j = i + 1; j < n; j++) { if (cssid == WiFi.SSID(indices[j])) indices[j] = -1; } } bool first = true; for (int i = 0; i < n; i++) { if (indices[i] == -1) continue; int quality = getRSSIasQuality(WiFi.RSSI(indices[i])); if (quality < MINIMUM_QUALITY) continue; if (!first) json += ','; first = false; // Escape SSID for JSON String ssid = WiFi.SSID(indices[i]); ssid.replace("\\", "\\\\"); ssid.replace("\"", "\\\""); json += F("{\"s\":\""); json += ssid; json += F("\",\"r\":"); json += String(quality); json += F(",\"e\":"); json += (WiFi.encryptionType(indices[i]) != WIFI_AUTH_OPEN) ? '1' : '0'; json += '}'; } } json += F("]}"); server->send(200, "application/json", json); } void WiFiConnect::handleWifiSave() { _ssid = server->arg("s").c_str(); _ssid.trim(); _password = server->arg("p").c_str(); _password.trim(); // Read custom parameters from form for (int i = 0; i < _paramsCount; i++) { if (_params[i] == NULL || _params[i]->getID() == NULL) continue; String value = server->arg(_params[i]->getID()).c_str(); value.toCharArray(_params[i]->_value, _params[i]->_length); } String page = F("" "" "" ""); page += textUI(314); page += F(""); page += FPSTR(TPL_STYLE); page += F("
" "

"); page += _apName; page += F("

"); // Connecting spinner page += F("
" "
" "

"); page += textUI(306); page += F(" "); page += _ssid; page += F(" ...

"); // Polling script: polls /foo every 3s; if AP responds → fail, if 20 timeouts → success page += F(""); // Success message page += F("
"); page += textUI(307); page += F(" "); page += _ssid; page += F("!
"); page += textUI(308); page += F("
"); // Failure message page += F("
"); page += textUI(309); page += F(" "); page += _ssid; page += F(".
"); page += textUI(310); page += F("
"); page += F("
"); server->sendHeader("Content-Length", String(page.length())); server->send(200, "text/html", page); _readyToConnect = true; } void WiFiConnect::handleLogo() { fs::File file = SPIFFS.open("/logo.png", "r"); if (!file) { server->send(404, "text/plain", "Logo not found"); return; } server->streamFile(file, "image/png"); file.close(); } void WiFiConnect::handleNotFound() { if (captivePortal()) { return; } String message = "File Not Found\n\n"; message += "URI: "; message += server->uri(); message += "\nMethod: "; message += (server->method() == HTTP_GET) ? "GET" : "POST"; message += "\nArguments: "; message += server->args(); message += "\n"; for (uint8_t i = 0; i < server->args(); i++) { message += " " + server->argName(i) + ": " + server->arg(i) + "\n"; } server->sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); server->sendHeader("Pragma", "no-cache"); server->sendHeader("Expires", "-1"); server->sendHeader("Content-Length", String(message.length())); server->send(404, "text/plain", message); } boolean WiFiConnect::captivePortal() { if (!isIp(server->hostHeader())) { String msg = "redirect\n"; server->sendHeader("Location", String("http://") + toStringIp(server->client().localIP()), true); server->sendHeader("Content-Length", String(msg.length())); server->send(302, "text/plain", msg); return true; } return false; } boolean WiFiConnect::isIp(String str) { for (unsigned int i = 0; i < str.length(); i++) { int c = str.charAt(i); if (c != '.' && (c < '0' || c > '9')) { return false; } } return true; } String WiFiConnect::toStringIp(IPAddress ip) { String res = ""; for (int i = 0; i < 3; i++) { res += String((ip >> (8 * i)) & 0xFF) + "."; } res += String(((ip >> 8 * 3)) & 0xFF); return res; } int WiFiConnect::getRSSIasQuality(int RSSI) { int quality = 0; if (RSSI <= -100) { quality = 0; } else if (RSSI >= -50) { quality = 100; } else { quality = 2 * (RSSI + 100); } return quality; }