You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
AutoConnect/src/AutoConnect.cpp

1308 lines
45 KiB

/**
* AutoConnect class implementation.
* @file AutoConnect.cpp
* @author hieromon@gmail.com
* @version 1.2.0
* @date 2020-04-24
* @copyright MIT license.
*/
#include "AutoConnect.h"
#ifdef ARDUINO_ARCH_ESP32
#include <esp_wifi.h>
#endif
/**
* An actual reset function dependent on the architecture
*/
#if defined(ARDUINO_ARCH_ESP8266)
#define SOFT_RESET() ESP.reset()
#define SET_HOSTNAME(x) do { WiFi.hostname(x); } while(0)
#elif defined(ARDUINO_ARCH_ESP32)
#define SOFT_RESET() ESP.restart()
#define SET_HOSTNAME(x) do { WiFi.setHostname(x); } while(0)
#endif
/**
* AutoConnect default constructor. This entry activates WebServer
* internally and the web server is allocated internal.
*/
AutoConnect::AutoConnect()
: _scanCount( 0 )
, _connectTimeout( AUTOCONNECT_TIMEOUT )
, _menuTitle( _apConfig.title )
{
memset(&_credential, 0x00, sizeof(station_config_t));
}
/**
* Run the AutoConnect site using the externally ensured ESP 8266 WebServer.
* User's added URI handler response can be included in handleClient method.
* @param webServer A reference of ESP8266WebServer instance.
*/
AutoConnect::AutoConnect(WebServerClass& webServer)
: AutoConnect()
{
_webServer = WebserverUP(&webServer, [](WebServerClass*){});
}
/**
* A destructor. Free AutoConnect web pages and release Web server.
* When the server is hosted it will be purged.
*/
AutoConnect::~AutoConnect() {
end();
}
/**
* Starts establishing WiFi connection without SSID and password.
*/
bool AutoConnect::begin(void) {
return begin(nullptr, nullptr);
}
/**
* Starts establishing WiFi connection.
* Before establishing, start the Web server and DNS server for the captive
* portal. Then begins connection establishment in WIFI_STA mode. If
* connection can not established with the specified SSID and password,
* switch to WIFI_AP_STA mode and activate SoftAP.
* @param ssid SSID to be connected.
* @param passphrase Password for connection.
* @param timeout A time out value in milliseconds for waiting connection.
* @return true Connection established, AutoConnect service started with WIFI_STA mode.
* @return false Could not connected, Captive portal started with WIFI_AP_STA mode.
*/
bool AutoConnect::begin(const char* ssid, const char* passphrase, unsigned long timeout) {
bool cs;
// Overwrite for the current timeout value.
_connectTimeout = timeout;
// Start WiFi connection with station mode.
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_STA);
delay(100);
// Set host name
if (_apConfig.hostName.length())
SET_HOSTNAME(_apConfig.hostName.c_str());
// Start Ticker according to the WiFi condition with Ticker is available.
if (_apConfig.ticker) {
_ticker.reset(new AutoConnectTicker(_apConfig.tickerPort, _apConfig.tickerOn));
if (WiFi.status() != WL_CONNECTED)
_ticker->start(AUTOCONNECT_FLICKER_PERIODDC, (uint8_t)AUTOCONNECT_FLICKER_WIDTHDC);
}
// If the portal is requested promptly skip the first WiFi.begin and
// immediately start the portal.
if (_apConfig.immediateStart) {
cs = false;
_apConfig.autoReconnect = false;
AC_DBG("Start the portal immediately\n");
}
else {
cs = true;
// Prepare valid configuration according to the WiFi connection right order.
const char* c_ssid = ssid;
const char* c_password = passphrase;
station_config_t current;
if (_getConfigSTA(&current))
AC_DBG("Current:%.32s\n", current.ssid);
if (_apConfig.principle == AC_PRINCIPLE_RSSI && (ssid == nullptr && passphrase == nullptr)) {
// AC_PRINCIPLE_RSSI is available when SSID and password are not provided.
// Find the strongest signal from the broadcast among the saved credentials.
if ((cs = _loadCurrentCredential(reinterpret_cast<char*>(current.ssid), reinterpret_cast<char*>(current.password), AC_PRINCIPLE_RSSI, false))) {
c_ssid = reinterpret_cast<const char*>(current.ssid);
c_password = reinterpret_cast<const char*>(current.password);
AC_DBG("Adopted:%.32s\n", c_ssid);
}
}
if (cs) {
// Advance configuration for STA mode. Restore previous configuration of STA.
_loadAvailCredential(reinterpret_cast<const char*>(current.ssid));
if (!_configSTA(_apConfig.staip, _apConfig.staGateway, _apConfig.staNetmask, _apConfig.dns1, _apConfig.dns2))
return false;
// Try to connect by STA immediately.
if (c_ssid == nullptr && c_password == nullptr)
WiFi.begin();
else {
_disconnectWiFi(false);
WiFi.begin(c_ssid, c_password);
}
AC_DBG("WiFi.begin(%s%s%s)\n", c_ssid == nullptr ? "" : c_ssid, c_password == nullptr ? "" : ",", c_password == nullptr ? "" : c_password);
cs = _waitForConnect(_connectTimeout) == WL_CONNECTED;
}
}
// Reconnect with a valid credential as the autoReconnect option is enabled.
if (!cs && _apConfig.autoReconnect && (ssid == nullptr && passphrase == nullptr)) {
// Load a valid credential.
char ssid_c[sizeof(station_config_t::ssid) + sizeof('\0')];
char password_c[sizeof(station_config_t::password) + sizeof('\0')];
if (_loadCurrentCredential(ssid_c, password_c, _apConfig.principle, true)) {
// Try to reconnect with a stored credential.
AC_DBG("autoReconnect loaded:%s(%s)\n", ssid_c, _apConfig.principle == AC_PRINCIPLE_RECENT ? "RECENT" : "RSSI");
const char* psk = strlen(password_c) ? password_c : nullptr;
_configSTA(IPAddress(_credential.config.sta.ip), IPAddress(_credential.config.sta.gateway), IPAddress(_credential.config.sta.netmask), IPAddress(_credential.config.sta.dns1), IPAddress(_credential.config.sta.dns2));
WiFi.begin(ssid_c, psk);
AC_DBG("WiFi.begin(%s%s%s)\n", ssid_c, psk == nullptr ? "" : ",", psk == nullptr ? "" : psk);
cs = _waitForConnect(_connectTimeout) == WL_CONNECTED;
}
}
_currentHostIP = WiFi.localIP();
// End first begin process, the captive portal specific process starts here.
if (cs) {
// Activate AutoConnectUpdate if it is attached and incorporate it into the AutoConnect menu.
if (_update)
_update->enable();
}
// Rushing into the portal.
else {
// The captive portal is effective at the autoRise is valid only.
if (_apConfig.autoRise) {
// Change WiFi working mode, Enable AP with STA
WiFi.setAutoConnect(false);
_disconnectWiFi(false);
// Activate the AP mode with configured softAP and start the access point.
WiFi.mode(WIFI_AP_STA);
while (WiFi.getMode() != WIFI_AP_STA) {
delay(1);
yield();
}
// Connection unsuccessful, launch the captive portal.
#if defined(ARDUINO_ARCH_ESP8266)
_config();
#endif
WiFi.softAP(_apConfig.apid.c_str(), _apConfig.psk.c_str(), _apConfig.channel, _apConfig.hidden);
do {
delay(100);
yield();
} while (!WiFi.softAPIP());
#if defined(ARDUINO_ARCH_ESP32)
_config();
#endif
if (_apConfig.apip) {
do {
delay(100);
yield();
} while (WiFi.softAPIP() != _apConfig.apip);
}
_currentHostIP = WiFi.softAPIP();
AC_DBG("SoftAP %s/%s Ch(%d) IP:%s %s\n", _apConfig.apid.c_str(), _apConfig.psk.c_str(), _apConfig.channel, _currentHostIP.toString().c_str(), _apConfig.hidden ? "hidden" : "");
// Start ticker with AP_STA
if (_ticker)
_ticker->start(AUTOCONNECT_FLICKER_PERIODAP, (uint8_t)AUTOCONNECT_FLICKER_WIDTHAP);
// Fork to the exit routine that starts captive portal.
cs = _onDetectExit ? _onDetectExit(_currentHostIP) : true;
// Start Web server when TCP connection is enabled.
_startWebServer();
// Start captive portal without cancellation by DetectExit.
if (cs) {
// Prepare for redirecting captive portal detection.
// Pass all URL requests to _captivePortal to disguise the captive portal.
_startDNSServer();
// Start the captive portal to make a new connection
bool hasTimeout = false;
_portalAccessPeriod = millis();
while (WiFi.status() != WL_CONNECTED && !_rfReset) {
handleClient();
// Force execution of queued processes.
yield();
// Check timeout
if ((hasTimeout = _hasTimeout(_apConfig.portalTimeout))) {
AC_DBG("CP timeout exceeded:%ld\n", millis() - _portalAccessPeriod);
break;
}
}
cs = WiFi.status() == WL_CONNECTED;
// If WLAN successfully connected, release DNS server.
if (cs) {
_dnsServer->stop();
_dnsServer.reset();
AC_DBG("DNS server stopped\n");
}
// Captive portal staying time exceeds timeout,
// Close the portal if an option for keeping the portal is false.
else if (hasTimeout) {
if (_apConfig.retainPortal) {
_purgePages();
AC_DBG("Maintain portal\n");
}
else {
_stopPortal();
}
}
}
}
else {
AC_DBG("Suppress autoRise\n");
}
}
// It doesn't matter the connection status for launching the Web server.
if (!_responsePage)
_startWebServer();
return cs;
}
/**
* Configure AutoConnect portal access point.
* @param ap SSID for access point.
* @param psk Password for access point.
*/
bool AutoConnect::config(const char* ap, const char* password) {
_apConfig.apid = String(ap);
_apConfig.psk = String(password);
return _config();
}
/**
* Configure AutoConnect portal access point.
* @param Config AutoConnectConfig class instance.
*/
bool AutoConnect::config(AutoConnectConfig& Config) {
_apConfig = Config;
return _config();
}
/**
* Configure access point.
* Set up access point with internal AutoConnectConfig parameter corrected
* by Config method.
*/
bool AutoConnect::_config(void) {
if (static_cast<uint32_t>(_apConfig.apip) == 0U || static_cast<uint32_t>(_apConfig.gateway) == 0U || static_cast<uint32_t>(_apConfig.netmask) == 0U) {
AC_DBG("Warning: Contains invalid SoftAPIP address(es).\n");
}
bool rc = WiFi.softAPConfig(_apConfig.apip, _apConfig.gateway, _apConfig.netmask);
AC_DBG("SoftAP configure %s, %s, %s %s\n", _apConfig.apip.toString().c_str(), _apConfig.gateway.toString().c_str(), _apConfig.netmask.toString().c_str(), rc ? "" : "failed");
return rc;
}
/**
* Advance configuration for STA mode.
* @param ip IP address
* @param gateway Gateway address
* @param netmask Netmask
* @param dns1 Primary DNS address
* @param dns2 Secondary DNS address
* @return true Station successfully configured
* @return false WiFi.config failed
*/
bool AutoConnect::_configSTA(const IPAddress& ip, const IPAddress& gateway, const IPAddress& netmask, const IPAddress& dns1, const IPAddress& dns2) {
bool rc;
AC_DBG("WiFi.config(IP=%s, Gateway=%s, Subnetmask=%s, DNS1=%s, DNS2=%s)\n", ip.toString().c_str(), gateway.toString().c_str(), netmask.toString().c_str(), dns1.toString().c_str(), dns2.toString().c_str());
if (!(rc = WiFi.config(ip, gateway, netmask, dns1, dns2))) {
AC_DBG("failed\n");
}
#ifdef ARDUINO_ARCH_ESP8266
AC_DBG("DHCP client(%s)\n", wifi_station_dhcpc_status() == DHCP_STOPPED ? "STOPPED" : "STARTED");
#endif
return rc;
}
/**
* Get URI to redirect at boot. It uses the URI according to the
* AutoConnectConfig::bootUti setting with the AutoConnectConfig::homeUri
* as the boot path.
* @return the boot uri.
*/
String AutoConnect::_getBootUri(void) {
if (_apConfig.bootUri == AC_ONBOOTURI_ROOT)
return String(AUTOCONNECT_URI);
else if (_apConfig.bootUri == AC_ONBOOTURI_HOME)
return _apConfig.homeUri.length() > 0 ? _apConfig.homeUri : String("/");
else
return _emptyString;
}
/**
* Obtains the currently established AP connection to determine if the
* station configuration needs to run before the first WiFi.begin.
* Get the SSID of the currently connected AP stored in the ESP module
* by using the SDK API natively.
* AutoConnect::begin retrieves the IP configuration from the stored
* credentials of AutoConnectCredential based on that SSID and executes
* WiFi.config before WiFi.begin.
* @param config Station configuration stored in the ESP module.
* @return true The config parameter has obtained configuration.
* @return false Station configuration does not exist.
*/
bool AutoConnect::_getConfigSTA(station_config_t* config) {
bool rc;
uint8_t* ssid;
uint8_t* bssid;
#if defined(ARDUINO_ARCH_ESP8266)
struct station_config current;
ssid = current.ssid;
bssid = current.bssid;
rc = wifi_station_get_config(&current);
#elif defined(ARDUINO_ARCH_ESP32)
wifi_config_t current;
ssid = current.sta.ssid;
bssid = current.sta.bssid;
rc = (esp_wifi_get_config(WIFI_IF_STA, &current) == ESP_OK);
#endif
if (rc) {
memcpy(config->ssid, ssid, sizeof(station_config_t::ssid));
memcpy(config->bssid, bssid, sizeof(station_config_t::bssid));
}
return rc;
}
/**
* Put a user site's home URI.
* The URI specified by home is linked from "HOME" in the AutoConnect
* portal menu.
* @param uri A URI string of user site's home.
*/
void AutoConnect::home(const String& uri) {
_apConfig.homeUri = uri;
}
/**
* Stops AutoConnect captive portal service.
*/
void AutoConnect::end(void) {
_responsePage.reset();
_currentPageElement.reset();
_ticker.reset();
_update.reset();
_ota.reset();
_stopPortal();
_dnsServer.reset();
_webServer.reset();
}
/**
* Returns the current hosted ESP8266WebServer.
*/
WebServerClass& AutoConnect::host(void) {
return *_webServer;
}
/**
* Returns AutoConnectAux instance of specified.
* @param uri An uri string.
* @return A pointer of AutoConnectAux instance.
*/
AutoConnectAux* AutoConnect::aux(const String& uri) const {
AutoConnectAux* aux_p = _aux;
while (aux_p) {
if (!strcmp(aux_p->uri(), uri.c_str()))
break;
aux_p = aux_p->_next;
}
return aux_p;
}
/**
* Append auxiliary pages made up with AutoConnectAux.
* @param aux A reference to AutoConnectAux that made up
* the auxiliary page to be added.
*/
void AutoConnect::join(AutoConnectAux& aux) {
if (_aux)
_aux->_concat(aux);
else
_aux = &aux;
aux._join(*this);
AC_DBG("%s on hands\n", aux.uri());
}
/**
* Append auxiliary pages made up with AutoConnectAux.
* @param aux A vector of reference to AutoConnectAux that made up
* the auxiliary page to be added.
*/
void AutoConnect::join(AutoConnectAuxVT auxVector) {
for (std::reference_wrapper<AutoConnectAux> aux : auxVector)
join(aux.get());
}
/**
* Creates an AutoConnectAux dynamically with the specified URI and
* integrates it into the menu. Returns false if a menu item with
* the same URI already exists.
* @param uri An uri of a new item to add
* @param title Title of the menu item
* @return true Added
* @return false The same item has existed.
*/
bool AutoConnect::addMenuItem(const String& uri, const String& title) {
AutoConnectAux* reg = aux(uri);
if (!reg) {
reg = new AutoConnectAux(uri, title);
join(*reg);
return true;
}
return false;
}
/**
* Creates an AutoConnectAux dynamically with the specified URI and
* integrates it into the menu. It will register the request handler
* for the WebServer after the addMenuItem works. It has similar
* efficacy to calling addMenuItem and WebSever::on at once.
* @param uri An uri of a new item to add
* @param title Title of the menu item
* @param handler Function of the request handler for WebServer class
* @return true Added
* @return false The same item has existed.
*/
bool AutoConnect::addMenuItem(const String& uri, const String& title, WebServerClass::THandlerFunction handler) {
if (_webServer) {
if (addMenuItem(uri, title)) {
_webServer->on(uri, handler);
return true;
}
}
return false;
}
/**
* Release a AutoConnectAux from the portal.
* @param uri An uri of the AutoConnectAux should be released
* @return true Specified AUX has released
* @return false Specified AUX not registered
*/
AutoConnectAux* AutoConnect::release(const String &uri) {
AutoConnectAux** self = &_aux;
while (*self) {
if (!strcmp((*self)->uri(), uri.c_str())) {
AC_DBG("%s released\n", (*self)->uri());
AutoConnectAux* ref = *self;
*self = (*self)->_next;
return ref;
}
self = &((*self)->_next);
}
return nullptr;
}
/**
* Starts Web server for AutoConnect service.
*/
void AutoConnect::_startWebServer(void) {
// Boot Web server
if (!_webServer) {
// Only when hosting WebServer internally
_webServer = WebserverUP(new WebServerClass(AUTOCONNECT_HTTPPORT), std::default_delete<WebServerClass>() );
AC_DBG("WebServer allocated\n");
}
// Discard the original the not found handler to redirect captive portal detection.
// It is supposed to evacuate but ESP8266WebServer::_notFoundHandler is not accessible.
_webServer->onNotFound(std::bind(&AutoConnect::_handleNotFound, this));
// here, Prepare PageBuilders for captive portal
if (!_responsePage) {
_responsePage.reset( new PageBuilder() );
_responsePage->exitCanHandle(std::bind(&AutoConnect::_classifyHandle, this, std::placeholders::_1, std::placeholders::_2));
_responsePage->onUpload(std::bind(&AutoConnect::_handleUpload, this, std::placeholders::_1, std::placeholders::_2));
_responsePage->insert(*_webServer);
_webServer->begin();
AC_DBG("http server started\n");
}
else {
AC_DBG("http server readied\n");
}
}
/**
* Starts DNS server for Captive portal.
*/
void AutoConnect::_startDNSServer(void) {
// Boot DNS server, set up for captive portal redirection.
if (!_dnsServer) {
_dnsServer.reset(new DNSServer());
_dnsServer->setErrorReplyCode(DNSReplyCode::NoError);
_dnsServer->start(AUTOCONNECT_DNSPORT, "*", WiFi.softAPIP());
AC_DBG("DNS server started\n");
}
}
/**
* Handling for the AutoConnect web interface.
* Invoke the handleClient of parent web server to process client request of
* AutoConnect WEB interface.
* No effects when the web server is not available.
*/
void AutoConnect::handleClient(void) {
// Is there DNS Server process next request?
if (_dnsServer)
_dnsServer->processNextRequest();
// handleClient valid only at _webServer activated.
if (_webServer)
_webServer->handleClient();
handleRequest();
}
/**
* Handling for the AutoConnect menu request.
*/
void AutoConnect::handleRequest(void) {
// Handling processing requests to AutoConnect.
if (_rfConnect) {
// Leave from the AP currently.
if (WiFi.status() == WL_CONNECTED)
_disconnectWiFi(true);
// Leave current AP, reconfigure station
_configSTA(_apConfig.staip, _apConfig.staGateway, _apConfig.staNetmask, _apConfig.dns1, _apConfig.dns2);
// An attempt to establish a new AP.
int32_t ch = _connectCh == 0 ? _apConfig.channel : _connectCh;
char ssid_c[sizeof(station_config_t::ssid) + 1];
char password_c[sizeof(station_config_t::password) + 1];
*ssid_c = '\0';
strncat(ssid_c, reinterpret_cast<const char*>(_credential.ssid), sizeof(ssid_c) - 1);
*password_c = '\0';
strncat(password_c, reinterpret_cast<const char*>(_credential.password), sizeof(password_c) - 1);
AC_DBG("Attempt:%s Ch(%d)\n", ssid_c, (int)ch);
WiFi.begin(ssid_c, password_c, ch);
if ((_rsConnect = _waitForConnect(_connectTimeout)) == WL_CONNECTED) {
if (WiFi.BSSID() != NULL) {
memcpy(_credential.bssid, WiFi.BSSID(), sizeof(station_config_t::bssid));
_currentHostIP = WiFi.localIP();
_redirectURI = String(F(AUTOCONNECT_URI_SUCCESS));
// Save current credential
if (_apConfig.autoSave == AC_SAVECREDENTIAL_AUTO) {
AutoConnectCredential credit(_apConfig.boundaryOffset);
if (credit.save(&_credential)) {
AC_DBG("%.*s credential saved\n", sizeof(_credential.ssid), reinterpret_cast<const char*>(_credential.ssid));
}
else {
AC_DBG("credential %.*s save failed\n", sizeof(_credential.ssid), reinterpret_cast<const char*>(_credential.ssid));
}
}
// Ensures that keeps a connection with the current AP while the portal behaves.
_setReconnect(AC_RECONNECT_SET);
}
else {
AC_DBG("%.*s has no BSSID, saving is unavailable\n", sizeof(_credential.ssid), reinterpret_cast<const char*>(_credential.ssid));
}
// Activate AutoConnectUpdate if it is attached and incorporate
// it into the AutoConnect menu.
if (_update)
_update->enable();
}
else {
_currentHostIP = WiFi.softAPIP();
_redirectURI = String(F(AUTOCONNECT_URI_FAIL));
_disconnectWiFi(false);
while (WiFi.status() != WL_IDLE_STATUS && WiFi.status() != WL_DISCONNECTED) {
delay(1);
yield();
}
// Restore the ticker
if (_ticker && WiFi.getMode() == WIFI_AP_STA)
_ticker->start(AUTOCONNECT_FLICKER_PERIODAP, (uint8_t)AUTOCONNECT_FLICKER_WIDTHAP);
}
_rfConnect = false;
}
if (_rfReset) {
// Reset or disconnect by portal operation result
_stopPortal();
AC_DBG("Reset\n");
delay(1000);
SOFT_RESET();
delay(1000);
}
if (_rfDisconnect) {
// Disconnect from the current AP.
_stopPortal();
_disconnectWiFi(false);
while (WiFi.status() == WL_CONNECTED) {
delay(10);
yield();
}
AC_DBG("Disconnected\n");
// Reset disconnection request
_rfDisconnect = false;
if (_apConfig.autoReset) {
delay(1000);
SOFT_RESET();
delay(1000);
}
}
// Handle the update behaviors for attached AutoConnectUpdate.
if (_update)
_update->handleUpdate();
// Attach AutoConnectOTA if OTA is available.
if (_apConfig.ota == AC_OTA_BUILTIN) {
if (!_ota) {
_ota.reset(new AutoConnectOTA());
_ota->attach(*this);
_ota->authentication(_apConfig.auth);
_ota->setTicker(_apConfig.tickerPort, _apConfig.tickerOn);
}
}
// Post-process for AutoConnectOTA
if (_ota) {
if (_ota->status() == AutoConnectOTA::OTA_RIP) {
// Indicate the reboot at the next handleClient turn
// with on completion of the update via OTA.
_webServer->client().setNoDelay(true);
_rfReset = true;
}
// Reflect the menu display specifier from AutoConnectConfig to AutoConnectOTA page
_ota->menu(_apConfig.menuItems & AC_MENUITEM_UPDATE);
}
// Post-process for ticker
if (_ticker) {
if (WiFi.status() == WL_CONNECTED)
_ticker->stop();
}
}
/**
* Register the exit routine for AutoConnectAux.
* @param uri Specify the URI of the AutoConnectAux page that
* registers the exit routine.
* @param handler A handler function of the exit routine.
* @param order Specify an enumeration type of
* AutoConnectExitOrder_t for the call timing of the exit routine.
* @return true An exit routine registered.
* @return false AutoConnectAux page for the specified URI is not
* registered.
*/
bool AutoConnect::on(const String& uri, const AuxHandlerFunctionT handler, AutoConnectExitOrder_t order) {
AutoConnectAux* aux = _aux;
while (aux) {
if (!strcmp(uri.c_str(), aux->uri())) {
aux->on(handler, order);
return true;
}
aux = aux->_next;
}
return false;
}
/**
* Register the exit routine that is being called when WiFi connected.
* @param fn A function of the exit routine.
*/
void AutoConnect::onConnect(ConnectExit_ft fn) {
_onConnectExit = fn;
}
/**
* Register the exit routine for the starting captive portal.
* @param fn A function of the exit routine.
*/
void AutoConnect::onDetect(DetectExit_ft fn) {
_onDetectExit = fn;
}
/**
* Register the handler function for undefined url request detected.
* @param fn A function of the not found handler.
*/
void AutoConnect::onNotFound(WebServerClass::THandlerFunction fn) {
_notFoundHandler = fn;
}
/**
* Load current available credential
* @param ssid A pointer to the buffer that SSID should be stored.
* @param password A pointer to the buffer that password should be stored.
* @param principle WiFi connection principle.
* @param excludeCurrent Skip loading the current SSID.
* @return true Current SSID and password returned.
* @return false There is no available SSID.
*/
bool AutoConnect::_loadCurrentCredential(char* ssid, char* password, const AC_PRINCIPLE_t principle, const bool excludeCurrent) {
bool rc;
if ((rc = _loadAvailCredential(nullptr, principle, excludeCurrent))) {
strcpy(ssid, reinterpret_cast<const char*>(_credential.ssid));
strcpy(password, reinterpret_cast<const char*>(_credential.password));
}
return rc;
}
/**
* Load stored credentials that match nearby WLANs.
* @param ssid SSID which should be loaded. If nullptr is assigned, search SSID with WiFi.scan.
* @param principle WiFi connection principle.
* @param excludeCurrent Skip loading the current SSID.
* @return true A matched credential of BSSID was loaded.
*/
bool AutoConnect::_loadAvailCredential(const char* ssid, const AC_PRINCIPLE_t principle, const bool excludeCurrent) {
AutoConnectCredential credential(_apConfig.boundaryOffset);
if (credential.entries() > 0) {
// Scan the vicinity only when the saved credentials are existing.
if (!ssid) {
WiFi.scanDelete();
int8_t nn = WiFi.scanNetworks(false, true);
AC_DBG("%d network(s) found\n", (int)nn);
if (nn > 0) {
station_config_t validConfig; // Temporary to find the strongest RSSI.
int32_t minRSSI = -120; // Min value to find the strongest RSSI.
// Seek SSID
const char* currentSSID = WiFi.SSID().c_str();
const bool skipCurrent = excludeCurrent & (strlen(currentSSID) > 0);
for (uint8_t i = 0; i < credential.entries(); i++) {
credential.load(i, &_credential);
// Seek valid configuration according to the WiFi connection principle.
// Verify that an available SSIDs meet AC_PRINCIPLE_t requirements.
for (uint8_t n = 0; n < nn; n++) {
if (skipCurrent && !strcmp(currentSSID, WiFi.SSID(n).c_str()))
continue;
if (!memcmp(_credential.bssid, WiFi.BSSID(n), sizeof(station_config_t::bssid))) {
// Excepts SSID that has weak RSSI under the lower limit.
if (WiFi.RSSI(n) < _apConfig.minRSSI) {
AC_DBG("%s:%" PRId32 "dBm, rejected\n", reinterpret_cast<const char*>(_credential.ssid), WiFi.RSSI(n));
continue;
}
// Determine valid credential
switch (principle) {
case AC_PRINCIPLE_RECENT:
// By BSSID, exit to keep the credential just loaded.
return true;
case AC_PRINCIPLE_RSSI:
// Verify that most strong radio signal.
// Continue seeking to find the strongest WIFI signal SSID.
if (WiFi.RSSI(n) > minRSSI) {
minRSSI = WiFi.RSSI(n);
memcpy(&validConfig, &_credential, sizeof(station_config_t));
}
break;
}
break;
}
}
}
// Increasing the minSSI will indicate the successfully sought for AC_PRINCIPLE_RSSI.
// Restore the credential that has maximum RSSI.
if (minRSSI > -120) {
memcpy(&_credential, &validConfig, sizeof(station_config_t));
return true;
}
}
}
// The SSID to load was specified.
// Set the IP configuration globally from the saved credential.
else if (strlen(ssid))
if (credential.load(ssid, &_credential) >= 0) {
if (_credential.dhcp == STA_STATIC) {
_apConfig.staip = static_cast<IPAddress>(_credential.config.sta.ip);
_apConfig.staGateway = static_cast<IPAddress>(_credential.config.sta.gateway);
_apConfig.staNetmask = static_cast<IPAddress>(_credential.config.sta.netmask);
_apConfig.dns1 = static_cast<IPAddress>(_credential.config.sta.dns1);
_apConfig.dns2 = static_cast<IPAddress>(_credential.config.sta.dns2);
}
return true;
}
}
return false;
}
/**
* Get the total amount of memory required to hold the AutoConnect credentials
* and any custom configuration settings stored in EEPROM.
* This function is available only for ESP8266 use.
* @return Combined size of AutoConnect credentials and custom settings.
*/
uint16_t AutoConnect::getEEPROMUsedSize(void) {
#if defined(ARDUINO_ARCH_ESP8266)
AutoConnectCredential credentials(_apConfig.boundaryOffset);
return _apConfig.boundaryOffset + credentials.dataSize();
#elif defined(ARDUINO_ARCH_ESP32)
return 0;
#endif
}
/**
* Disconnect from the AP and stop the AutoConnect portal.
* Stops DNS server and flush tcp sending.
*/
void AutoConnect::_stopPortal(void) {
if (_dnsServer)
_dnsServer->stop();
if (_webServer) {
_webServer->client().stop();
delay(1000);
}
_setReconnect(AC_RECONNECT_RESET);
WiFi.softAPdisconnect(false);
AC_DBG("Portal stopped\n");
}
/**
* Redirect to captive portal if we got a request for another domain.
* Return true in that case so the page handler do not try to handle the request again.
*/
bool AutoConnect::_captivePortal(void) {
String hostHeader = _webServer->hostHeader();
if (!_isIP(hostHeader) && (hostHeader != WiFi.localIP().toString()) && (!hostHeader.endsWith(F(".local")))) {
AC_DBG("Detected application, %s, %s\n", hostHeader.c_str(), WiFi.localIP().toString().c_str());
String location = String(F("http://")) + _webServer->client().localIP().toString() + _getBootUri();
_webServer->sendHeader(String(F("Location")), location, true);
_webServer->send(302, String(F("text/plain")), _emptyString);
_webServer->client().flush();
_webServer->client().stop();
return true;
}
return false;
}
/**
* Check whether the stay-time in the captive portal has a timeout.
* If the station is connected, the time measurement will be reset.
* @param timeout The time limit for keeping the captive portal.
* @return true There is no connection from the station even the time limit exceeds.
* @return false Connectionless duration has not exceeded yet.
*/
bool AutoConnect::_hasTimeout(unsigned long timeout) {
uint8_t staNum;
if (!_apConfig.portalTimeout)
return false;
#if defined(ARDUINO_ARCH_ESP8266)
staNum = 0;
struct station_info* station = wifi_softap_get_station_info();
while (station) {
staNum++;
station = STAILQ_NEXT(station, next);
}
wifi_softap_free_station_info();
#elif defined(ARDUINO_ARCH_ESP32)
staNum = WiFi.softAPgetStationNum();
#endif
if (staNum)
_portalAccessPeriod = millis();
return (millis() - _portalAccessPeriod > timeout) ? true : false;
}
/**
* A handler that redirects access to the captive portal to the connection
* configuration page.
*/
void AutoConnect::_handleNotFound(void) {
if (!_captivePortal()) {
if (_notFoundHandler) {
_notFoundHandler();
}
else {
PageElement page404(_PAGE_404, { { String(F("HEAD")), std::bind(&AutoConnect::_token_HEAD, this, std::placeholders::_1) } });
String html = page404.build();
_webServer->sendHeader(String(F("Cache-Control")), String(F("no-cache, no-store, must-revalidate")), true);
_webServer->sendHeader(String(F("Pragma")), String(F("no-cache")));
_webServer->sendHeader(String(F("Expires")), String("-1"));
_webServer->sendHeader(String(F("Content-Length")), String(html.length()));
_webServer->send(404, String(F("text/html")), html);
}
}
}
/**
* Reset the ESP8266 module.
* It is called from the PageBuilder of the disconnect page and indicates
* the request for disconnection. It will be into progress after handleClient.
*/
String AutoConnect::_induceReset(PageArgument& args) {
AC_UNUSED(args);
_rfReset = true;
return String(F(AUTOCONNECT_BUTTONLABEL_RESET " in progress..."));
}
/**
* Disconnect from AP.
* It is called from the PageBuilder of the disconnect page and indicates
* the request for disconnection. It will be into progress after handleClient.
*/
String AutoConnect::_induceDisconnect(PageArgument& args) {
AC_UNUSED(args);
_rfDisconnect = true;
return _emptyString;
}
/**
* Indicates a connection establishment request and returns a redirect
* response to the waiting for connection page. This is called from
* handling of the current request by PageBuilder triggered by handleClient().
* If "Credential" exists in POST parameter, it reads from EEPROM.
* @param args http request arguments.
* @return A redirect response including "Location:" header.
*/
String AutoConnect::_induceConnect(PageArgument& args) {
// Retrieve credential from the post method content.
if (args.hasArg(String(F(AUTOCONNECT_PARAMID_CRED)))) {
// Read from EEPROM
AutoConnectCredential credential(_apConfig.boundaryOffset);
credential.load(args.arg(String(F(AUTOCONNECT_PARAMID_CRED))).c_str(), &_credential);
#ifdef AC_DEBUG
IPAddress staip = IPAddress(_credential.config.sta.ip);
AC_DBG("Credential loaded:%.*s(%s)\n", sizeof(station_config_t::ssid), reinterpret_cast<const char*>(_credential.ssid), _credential.dhcp == STA_DHCP ? "DHCP" : staip.toString().c_str());
#endif
}
else {
AC_DBG("Queried SSID:%s\n", args.arg(AUTOCONNECT_PARAMID_SSID).c_str());
// Credential had by the post parameter.
strncpy(reinterpret_cast<char*>(_credential.ssid), args.arg(String(F(AUTOCONNECT_PARAMID_SSID))).c_str(), sizeof(_credential.ssid));
strncpy(reinterpret_cast<char*>(_credential.password), args.arg(String(F(AUTOCONNECT_PARAMID_PASS))).c_str(), sizeof(_credential.password));
// Static IP detection
if (args.hasArg(String(F(AUTOCONNECT_PARAMID_DHCP)))) {
_credential.dhcp = STA_DHCP;
_credential.config.sta.ip = _credential.config.sta.gateway = _credential.config.sta.netmask = _credential.config.sta.dns1 = _credential.config.sta.dns2 = 0U;
}
else {
_credential.dhcp = STA_STATIC;
const String paramId[] = {
String(F(AUTOCONNECT_PARAMID_STAIP)),
String(F(AUTOCONNECT_PARAMID_GTWAY)),
String(F(AUTOCONNECT_PARAMID_NTMSK)),
String(F(AUTOCONNECT_PARAMID_DNS1)),
String(F(AUTOCONNECT_PARAMID_DNS2))
};
for (uint8_t i = 0; i < sizeof(station_config_t::_config::addr) / sizeof(uint32_t); i++) {
if (args.hasArg(paramId[i])) {
IPAddress ip;
if (ip.fromString(args.arg(paramId[i])))
_credential.config.addr[i] = static_cast<uint32_t>(ip);
}
}
}
}
// Restore the configured IPs to STA configuration
_apConfig.staip = static_cast<IPAddress>(_credential.config.sta.ip);
_apConfig.staGateway = static_cast<IPAddress>(_credential.config.sta.gateway);
_apConfig.staNetmask = static_cast<IPAddress>(_credential.config.sta.netmask);
_apConfig.dns1 = static_cast<IPAddress>(_credential.config.sta.dns1);
_apConfig.dns2 = static_cast<IPAddress>(_credential.config.sta.dns2);
// Determine the connection channel based on the scan result.
_connectCh = 0;
for (uint8_t nn = 0; nn < _scanCount; nn++) {
String ssid = WiFi.SSID(nn);
if (!strncmp(ssid.c_str(), reinterpret_cast<const char*>(_credential.ssid), sizeof(station_config_t::ssid))) {
_connectCh = WiFi.channel(nn);
break;
}
}
// Turn on the trigger to start WiFi.begin().
_rfConnect = true;
// Since v0.9.7, the redirect method changed from a 302 response to the
// meta tag with refresh attribute.
// This approach for ESP32 makes an inefficient choice. The waiting
// procedure for a connection attempt should be the server side. Also,
// the proper value of waiting time until refreshing is unknown. But
// AutoConnect cannot avoid the following error as affairs stand now
// that occurs at connection establishment.
// [WiFiClient.cpp:463] connected(): Disconnected: RES: 0, ERR: 128
// When connecting as a station, TCP reset caused by switching of the
// radio channel occurs. Although the Espressif view is true. However,
// the actual TCP reset occurs not at the time of switching the channel.
// It occurs at the connection from the ESP32 to the AP is established
// and it is possible that TCP reset is occurring in other situations.
// So, it may not be the real cause. Client-origin redirects with HTML
// refresh depend on the behavior of the arduino-esp32 library. Thus,
// the implementations for redirects with HTML will continue until
// the arduino-esp32 core supports reconnection.
// Redirect to waiting URI while executing connection request.
// String url = String(F("http://")) + _webServer->client().localIP().toString() + String(AUTOCONNECT_URI_RESULT);
// _webServer->sendHeader(F("Location"), url, true);
// _webServer->send(302, F("text/plain"), "");
// _webServer->client().flush();
// _webServer->client().stop();
// _responsePage->cancel();
return _emptyString;
}
/**
* Responds response as redirect to the connection result page.
* A destination as _redirectURI is indicated by loop to establish connection.
*/
String AutoConnect::_invokeResult(PageArgument& args) {
AC_UNUSED(args);
String redirect = String(F("http://"));
// The host address to which the connection result for ESP32 responds
// changed from v0.9.7. This change is a measure according to the
// implementation of the arduino-esp32 1.0.1.
#if defined(ARDUINO_ARCH_ESP32)
// In ESP32, the station IP address just established could not be reached.
redirect += _webServer->client().localIP().toString();
#elif defined(ARDUINO_ARCH_ESP8266)
// In ESP8266, the host address that responds for the connection
// successful is the IP address of ESP8266 as a station.
// This is the specification as before.
redirect += _currentHostIP.toString();
#endif
AC_DBG("Redirect to %s\n", redirect.c_str());
redirect += _redirectURI;
_webServer->sendHeader(String(F("Location")), redirect, true);
_webServer->send(302, String(F("text/plain")), _emptyString);
_webServer->client().flush();
_webServer->client().stop();
_waitForEndTransmission(); // Wait for response transmission complete
_responsePage->cancel();
return _emptyString;
}
/**
* Classify the requested URI to responsive page builder.
* There is always only one PageBuilder instance that can exist in
* AutoConnect for saving RAM. Invokes a subordinate function that
* dynamically generates a response page at handleRequest. This is
* a part of the handling of http request originated from handleClient.
*/
bool AutoConnect::_classifyHandle(HTTPMethod method, String uri) {
AC_UNUSED(method);
_portalAccessPeriod = millis();
AC_DBG("Host:%s,URI:%s", _webServer->hostHeader().c_str(), uri.c_str());
// Here, classify requested uri
if (uri == _uri) {
AC_DBG_DUMB(",already allocated\n");
return true; // The response page already exists.
}
// Dispose decrepit page
if (_uri.length())
_prevUri = _uri; // Save current uri for the upload request
_purgePages();
// Create the page dynamically
_currentPageElement.reset( _setupPage(uri) );
if (!_currentPageElement && _aux) {
// Requested URL is not a normal page, exploring AUX pages
_currentPageElement.reset(_aux->_setupPage(uri));
}
if (_currentPageElement) {
AC_DBG_DUMB(",generated:%s", uri.c_str());
_uri = uri;
_responsePage->addElement(*_currentPageElement);
_responsePage->setUri(_uri.c_str());
}
AC_DBG_DUMB(",%s\n", _currentPageElement != nullptr ? " allocated" : "ignored");
return _currentPageElement != nullptr ? true : false;
}
/**
* A wrapper of the upload function for the WebServerClass. Invokes the
* upload function of the AutoConnectAux which has a destination URI.
*/
void AutoConnect::_handleUpload(const String& requestUri, const HTTPUpload& upload) {
AutoConnectAux* aux = _aux;
while (aux) {
if (aux->_uriStr == requestUri) {
aux->upload(_prevUri, upload);
break;
}
aux = aux->_next;
}
}
/**
* Purge allocated pages.
*/
void AutoConnect::_purgePages(void) {
_responsePage->clearElement();
if (_currentPageElement) {
_currentPageElement.reset();
_uri = String("");
}
}
/**
* It checks whether the specified character string is a valid IP address.
* @param ipStr IP string for validation.
* @return true Valid.
*/
bool AutoConnect::_isIP(String ipStr) {
for (uint8_t i = 0; i < ipStr.length(); i++) {
char c = ipStr.charAt(i);
if (c != '.' && (c < '0' || c > '9'))
return false;
}
return true;
}
/**
* Convert MAC address in uint8_t array to Sting XX:XX:XX:XX:XX:XX format.
* @param mac Array of MAC address 6 bytes.
* @return MAC address string in XX:XX:XX:XX:XX:XX format.
*/
String AutoConnect::_toMACAddressString(const uint8_t mac[]) {
String macAddr = String("");
for (uint8_t i = 0; i < 6; i++) {
char buf[3];
sprintf(buf, "%02X", mac[i]);
macAddr += buf;
if (i < 5)
macAddr += ':';
}
return macAddr;
}
/**
* Convert dBm to the wifi signal quality.
* @param rssi dBm.
* @return A signal quality percentage.
*/
unsigned int AutoConnect::_toWiFiQuality(int32_t rssi) {
unsigned int qu;
if (rssi == 31) // WiFi signal is weak and RSSI value is unreliable.
qu = 0;
else if (rssi <= -100)
qu = 0;
else if (rssi >= -50)
qu = 100;
else
qu = 2 * (rssi + 100);
return qu;
}
/**
* Wait for establishment of the connection until the specified time expires.
* @param timeout Expiration time by millisecond unit.
* @return wl_status_t
*/
wl_status_t AutoConnect::_waitForConnect(unsigned long timeout) {
wl_status_t wifiStatus;
AC_DBG("Connecting");
unsigned long st = millis();
while ((wifiStatus = WiFi.status()) != WL_CONNECTED) {
if (timeout) {
if (millis() - st > timeout)
break;
}
AC_DBG_DUMB("%c", '.');
delay(300);
}
AC_DBG_DUMB("%s IP:%s\n", wifiStatus == WL_CONNECTED ? "established" : "time out", WiFi.localIP().toString().c_str());
if (WiFi.status() == WL_CONNECTED)
if (_onConnectExit) {
IPAddress localIP = WiFi.localIP();
_onConnectExit(localIP);
}
return wifiStatus;
}
/**
* Control the automatic reconnection behaves. Reconnection behavior
* to the AP connected during captive portal operation is activated
* by an order as the argument.
* @param order AC_RECONNECT_SET or AC_RECONNECT_RESET
*/
void AutoConnect::_setReconnect(const AC_STARECONNECT_t order) {
#if defined(ARDUINO_ARCH_ESP32)
if (order == AC_RECONNECT_SET) {
_disconnectEventId = WiFi.onEvent([](WiFiEvent_t e, WiFiEventInfo_t info) {
AC_DBG("STA lost connection:%d\n", info.disconnected.reason);
AC_DBG("STA connection %s\n", WiFi.reconnect() ? "restored" : "failed");
}, WiFiEvent_t::SYSTEM_EVENT_AP_STADISCONNECTED);
AC_DBG("Event<%d> handler registered\n", static_cast<int>(WiFiEvent_t::SYSTEM_EVENT_AP_STADISCONNECTED));
}
else if (order == AC_RECONNECT_RESET) {
if (_disconnectEventId) {
WiFi.removeEvent(_disconnectEventId);
AC_DBG("Event<%d> handler released\n", static_cast<int>(WiFiEvent_t::SYSTEM_EVENT_AP_STADISCONNECTED));
}
}
#elif defined(ARDUINO_ARCH_ESP8266)
bool strc = order == AC_RECONNECT_SET ? true : false;
WiFi.setAutoReconnect(strc);
AC_DBG("STA reconnection:%s\n", strc ? "EN" : "DIS");
#endif
}
/**
* Wait for the end of transmission of the http response by closed
* from the http client.
*/
void AutoConnect::_waitForEndTransmission(void) {
#ifdef AC_DEBUG
AC_DBG("Leaves:");
unsigned long lt = millis();
#endif
while (_webServer->client().connected()) {
delay(1);
yield();
}
#ifdef AC_DEBUG
// Notifies of the time taken to end the session. If the http client
// times out, AC_DEBUG must be enabled and it is necessary to confirm
// that the http response is being transmitted correctly.
// To trace the correctness the close sequence of TCP connection,
// enable the debug log of the Arduino core side. If the normal,
// a message of closed the TCP connection will be logged between
// "Leaves:" and "the time taken to end the session" of the log.
AC_DBG_DUMB("%d[ms]\n", static_cast<int>(millis() - lt));
#endif
}
/**
* Disconnects the station from an associated access point.
* @param wifiOff The station mode turning switch.
*/
void AutoConnect::_disconnectWiFi(bool wifiOff) {
#if defined(ARDUINO_ARCH_ESP8266)
WiFi.disconnect(wifiOff);
#elif defined(ARDUINO_ARCH_ESP32)
WiFi.disconnect(wifiOff, true);
#endif
while (WiFi.status() == WL_CONNECTED) {
delay(1);
yield();
}
// Restart the ticker to indicate that ESP module into the disconnected state.
if (_ticker) {
uint32_t tc = AUTOCONNECT_FLICKER_PERIODDC;
uint8_t tw = AUTOCONNECT_FLICKER_WIDTHDC;
if (WiFi.getMode() == WIFI_AP_STA) {
tc = AUTOCONNECT_FLICKER_PERIODAP;
tw = AUTOCONNECT_FLICKER_WIDTHAP;
}
_ticker->start(tc, tw);
}
}
/**
* Initialize an empty string to allow returning const String& with nothing.
*/
const String AutoConnect::_emptyString = String("");