From 275cb2d398b964edf61ed6a208d127f4270a6f84 Mon Sep 17 00:00:00 2001 From: Hieromon Ikasamo Date: Tue, 8 Oct 2019 15:33:40 +0900 Subject: [PATCH 01/25] Support the Config New with static IP --- src/AutoConnect.cpp | 186 ++++++++++++++++++++++++++-------- src/AutoConnect.h | 17 +++- src/AutoConnectCredential.cpp | 159 ++++++++++++++++++++++------- src/AutoConnectCredential.h | 61 +++++++---- src/AutoConnectPage.cpp | 100 ++++++++++++++---- src/AutoConnectPage.h | 10 +- 6 files changed, 400 insertions(+), 133 deletions(-) diff --git a/src/AutoConnect.cpp b/src/AutoConnect.cpp index 7bebde1..9de0f0b 100644 --- a/src/AutoConnect.cpp +++ b/src/AutoConnect.cpp @@ -2,8 +2,8 @@ * AutoConnect class implementation. * @file AutoConnect.cpp * @author hieromon@gmail.com - * @version 1.0.6 - * @date 2019-09-17 + * @version 1.1.0 + * @date 2019-10-08 * @copyright MIT license. */ @@ -53,7 +53,7 @@ void AutoConnect::_initialize(void) { _menuTitle = _apConfig.title; _connectTimeout = AUTOCONNECT_TIMEOUT; _scanCount = 0; - memset(&_credential, 0x00, sizeof(struct station_config)); + memset(&_credential, 0x00, sizeof(station_config_t)); #ifdef ARDUINO_ARCH_ESP32 _disconnectEventId = -1; // The member available for ESP32 only #endif @@ -111,22 +111,14 @@ bool AutoConnect::begin(const char* ssid, const char* passphrase, unsigned long _ticker->start(AUTOCONNECT_FLICKER_PERIODDC, (uint8_t)AUTOCONNECT_FLICKER_WIDTHDC); } - // Advance configuration for STA mode. -#ifdef AC_DEBUG - String staip_s = _apConfig.staip.toString(); - String staGateway_s = _apConfig.staGateway.toString(); - String staNetmask_s = _apConfig.staNetmask.toString(); - String dns1_s = _apConfig.dns1.toString(); - String dns2_s = _apConfig.dns2.toString(); - AC_DBG("WiFi.config(IP=%s, Gateway=%s, Subnetmask=%s, DNS1=%s, DNS2=%s)\n", staip_s.c_str(), staGateway_s.c_str(), staNetmask_s.c_str(), dns1_s.c_str(), dns2_s.c_str()); -#endif - if (!WiFi.config(_apConfig.staip, _apConfig.staGateway, _apConfig.staNetmask, _apConfig.dns1, _apConfig.dns2)) { - AC_DBG("failed\n"); - return false; + // Advance configuration for STA mode. Restore previous configuration of STA. + station_config_t current; + if (_getConfigSTA(¤t)) { + AC_DBG("Current:%.32s\n", current.ssid); + _loadAvailCredential(reinterpret_cast(current.ssid)); } -#ifdef ARDUINO_ARCH_ESP8266 - AC_DBG("DHCP client(%s)\n", wifi_station_dhcpc_status() == DHCP_STOPPED ? "STOPPED" : "STARTED"); -#endif + if (!_configSTA(_apConfig.staip, _apConfig.staGateway, _apConfig.staNetmask, _apConfig.dns1, _apConfig.dns2)) + return false; // If the portal is requested promptly skip the first WiFi.begin and // immediately start the portal. @@ -150,16 +142,17 @@ bool AutoConnect::begin(const char* ssid, const char* passphrase, unsigned long // Reconnect with a valid credential as the autoReconnect option is enabled. if (!cs && _apConfig.autoReconnect && (ssid == nullptr && passphrase == nullptr)) { // Load a valid credential. - if (_loadAvailCredential()) { + if (_loadAvailCredential(nullptr)) { // Try to reconnect with a stored credential. - char ssid_c[sizeof(station_config::ssid) + 1]; - char password_c[sizeof(station_config::password) + 1]; + char ssid_c[sizeof(station_config_t::ssid) + sizeof('\0')]; + char password_c[sizeof(station_config_t::password) + sizeof('\0')]; *ssid_c = '\0'; - strncat(ssid_c, reinterpret_cast(_credential.ssid), sizeof(ssid_c) - 1); + strncat(ssid_c, reinterpret_cast(_credential.ssid), sizeof(ssid_c) - sizeof('\0')); *password_c = '\0'; - strncat(password_c, reinterpret_cast(_credential.password), sizeof(password_c) - 1); + strncat(password_c, reinterpret_cast(_credential.password), sizeof(password_c) - sizeof('\0')); AC_DBG("autoReconnect loaded SSID:%s\n", ssid_c); 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; @@ -191,7 +184,7 @@ bool AutoConnect::begin(const char* ssid, const char* passphrase, unsigned long // Connection unsuccessful, launch the captive portal. #if defined(ARDUINO_ARCH_ESP8266) - if (!(_apConfig.apip == IPAddress(0, 0, 0, 0) || _apConfig.gateway == IPAddress(0, 0, 0, 0) || _apConfig.netmask == IPAddress(0, 0, 0, 0))) { + if (!_apConfig.apip && !_apConfig.gateway && !_apConfig.netmask) { _config(); } #endif @@ -199,13 +192,13 @@ bool AutoConnect::begin(const char* ssid, const char* passphrase, unsigned long do { delay(100); yield(); - } while (WiFi.softAPIP() == IPAddress(0, 0, 0, 0)); + } while (!WiFi.softAPIP()); #if defined(ARDUINO_ARCH_ESP32) - if (!(_apConfig.apip == IPAddress(0, 0, 0, 0) || _apConfig.gateway == IPAddress(0, 0, 0, 0) || _apConfig.netmask == IPAddress(0, 0, 0, 0))) { + if (!(static_cast(_apConfig.apip) == 0U || static_cast(_apConfig.gateway) == 0U || static_cast(_apConfig.netmask) == 0U)) { _config(); } #endif - if (_apConfig.apip != IPAddress(0, 0, 0, 0)) { + if (_apConfig.apip) { do { delay(100); yield(); @@ -312,6 +305,63 @@ bool AutoConnect::_config(void) { 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; +} + +/** + * 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(¤t); +#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, ¤t) == 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 @@ -469,10 +519,13 @@ void AutoConnect::handleRequest(void) { 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::ssid) + 1]; - char password_c[sizeof(station_config::password) + 1]; + 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(_credential.ssid), sizeof(ssid_c) - 1); *password_c = '\0'; @@ -481,7 +534,7 @@ void AutoConnect::handleRequest(void) { WiFi.begin(ssid_c, password_c, ch); if (_waitForConnect(_connectTimeout) == WL_CONNECTED) { if (WiFi.BSSID() != NULL) { - memcpy(_credential.bssid, WiFi.BSSID(), sizeof(station_config::bssid)); + memcpy(_credential.bssid, WiFi.BSSID(), sizeof(station_config_t::bssid)); _currentHostIP = WiFi.localIP(); _redirectURI = String(F(AUTOCONNECT_URI_SUCCESS)); @@ -592,26 +645,40 @@ void AutoConnect::onNotFound(WebServerClass::THandlerFunction fn) { /** * Load stored credentials that match nearby WLANs. + * @param ssid SSID which should be loaded. If nullptr is assigned, search SSID with WiFi.scan. * @return true A matched credential of BSSID was loaded. */ -bool AutoConnect::_loadAvailCredential(void) { +bool AutoConnect::_loadAvailCredential(const char* ssid) { AutoConnectCredential credential(_apConfig.boundaryOffset); if (credential.entries() > 0) { // Scan the vicinity only when the saved credentials are existing. - WiFi.scanDelete(); - int8_t nn = WiFi.scanNetworks(false, true); - AC_DBG("%d network(s) found\n", (int)nn); - if (nn > 0) { - // Determine valid credentials by BSSID. - for (uint8_t i = 0; i < credential.entries(); i++) { - credential.load(i, &_credential); - for (uint8_t n = 0; n < nn; n++) { - if (!memcmp(_credential.bssid, WiFi.BSSID(n), sizeof(station_config::bssid))) - return true; + if (!ssid) { + WiFi.scanDelete(); + int8_t nn = WiFi.scanNetworks(false, true); + AC_DBG("%d network(s) found\n", (int)nn); + if (nn > 0) { + // Determine valid credentials by BSSID. + for (uint8_t i = 0; i < credential.entries(); i++) { + credential.load(i, &_credential); + for (uint8_t n = 0; n < nn; n++) { + if (!memcmp(_credential.bssid, WiFi.BSSID(n), sizeof(station_config_t::bssid))) + return true; + } } } } + else if (strlen(ssid)) + if (credential.load(ssid, &_credential) >= 0) { + if (_credential.dhcp == STA_STATIC) { + _apConfig.staip = static_cast(_credential.config.sta.ip); + _apConfig.staGateway = static_cast(_credential.config.sta.gateway); + _apConfig.staNetmask = static_cast(_credential.config.sta.netmask); + _apConfig.dns1 = static_cast(_credential.config.sta.dns1); + _apConfig.dns2 = static_cast(_credential.config.sta.dns2); + } + return true; + } } return false; } @@ -738,11 +805,14 @@ String AutoConnect::_induceConnect(PageArgument& args) { if (args.hasArg(String(F(AUTOCONNECT_PARAMID_CRED)))) { // Read from EEPROM AutoConnectCredential credential(_apConfig.boundaryOffset); - struct station_config entry; + station_config_t entry; credential.load(args.arg(String(F(AUTOCONNECT_PARAMID_CRED))).c_str(), &entry); strncpy(reinterpret_cast(_credential.ssid), reinterpret_cast(entry.ssid), sizeof(_credential.ssid)); strncpy(reinterpret_cast(_credential.password), reinterpret_cast(entry.password), sizeof(_credential.password)); - AC_DBG("Credential loaded:%.*s\n", sizeof(station_config::ssid), _credential.ssid); +#ifdef AC_DEBUG + IPAddress staip = IPAddress(_credential.config.sta.ip); + AC_DBG("Credential loaded:%.*s(%s)\n", sizeof(station_config_t::ssid), reinterpret_cast(_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()); @@ -755,12 +825,39 @@ String AutoConnect::_induceConnect(PageArgument& args) { _connectCh = 0; for (uint8_t nn = 0; nn < _scanCount; nn++) { String ssid = WiFi.SSID(nn); - if (!strncmp(ssid.c_str(), reinterpret_cast(_credential.ssid), sizeof(station_config::ssid))) { + if (!strncmp(ssid.c_str(), reinterpret_cast(_credential.ssid), sizeof(station_config_t::ssid))) { _connectCh = WiFi.channel(nn); break; } } + // Static IP detection + _credential.config.sta.ip = _credential.config.sta.gateway = _credential.config.sta.netmask = _credential.config.sta.dns1 = _credential.config.sta.dns2 = 0U; + if (!args.hasArg(String(F(AUTOCONNECT_PARAMID_DHCP)))) { + _credential.dhcp = STA_STATIC; + if (args.hasArg(String(F(AUTOCONNECT_PARAMID_STAIP)))) { + _apConfig.staip.fromString(args.arg(String(F(AUTOCONNECT_PARAMID_STAIP)))); + _credential.config.sta.ip = (uint32_t)_apConfig.staip; + } + if (args.hasArg(String(F(AUTOCONNECT_PARAMID_GTWAY)))) { + _apConfig.staGateway.fromString(args.arg(String(F(AUTOCONNECT_PARAMID_GTWAY)))); + _credential.config.sta.gateway = (uint32_t)_apConfig.staGateway; + } + if (args.hasArg(String(F(AUTOCONNECT_PARAMID_NTMSK)))) { + _apConfig.staNetmask.fromString(args.arg(String(F(AUTOCONNECT_PARAMID_NTMSK)))); + _credential.config.sta.netmask = (uint32_t)_apConfig.staNetmask; + } + if (args.hasArg(String(F(AUTOCONNECT_PARAMID_DNS1)))) { + _apConfig.dns1.fromString(args.arg(String(F(AUTOCONNECT_PARAMID_DNS1)))); + _credential.config.sta.dns1 = (uint32_t)_apConfig.dns1; + } + if (args.hasArg(String(F(AUTOCONNECT_PARAMID_DNS2)))) + _apConfig.dns2.fromString(args.arg(String(F(AUTOCONNECT_PARAMID_DNS2)))); + _credential.config.sta.dns2 = (uint32_t)_apConfig.dns2; + } + else + _credential.dhcp = STA_DHCP; + // Turn on the trigger to start WiFi.begin(). _rfConnect = true; @@ -811,6 +908,7 @@ String AutoConnect::_invokeResult(PageArgument& args) { // 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); diff --git a/src/AutoConnect.h b/src/AutoConnect.h index 415d569..f1f385b 100644 --- a/src/AutoConnect.h +++ b/src/AutoConnect.h @@ -2,8 +2,8 @@ * Declaration of AutoConnect class and accompanying AutoConnectConfig class. * @file AutoConnect.h * @author hieromon@gmail.com - * @version 1.0.0 - * @date 2019-08-15 + * @version 1.1.0 + * @date 2019-10-07 * @copyright MIT license. */ @@ -229,10 +229,12 @@ class AutoConnect { } AC_STARECONNECT_t; void _initialize(void); bool _config(void); + bool _configSTA(const IPAddress& ip, const IPAddress& gateway, const IPAddress& netmask, const IPAddress& dns1, const IPAddress& dns2); + bool _getConfigSTA(station_config_t* config); void _startWebServer(void); void _startDNSServer(void); void _handleNotFound(void); - bool _loadAvailCredential(void); + bool _loadAvailCredential(const char* ssid); void _stopPortal(void); bool _classifyHandle(HTTPMethod mothod, String uri); void _handleUpload(const String& requestUri, const HTTPUpload& upload); @@ -289,8 +291,8 @@ class AutoConnect { std::unique_ptr _update; /** Saved configurations */ - AutoConnectConfig _apConfig; - struct station_config _credential; + AutoConnectConfig _apConfig; + station_config_t _credential; uint8_t _hiddenSSIDCount; int16_t _scanCount; uint8_t _connectCh; @@ -374,6 +376,11 @@ class AutoConnect { String _token_LIST_SSID(PageArgument& args); String _token_SSID_COUNT(PageArgument& args); String _token_HIDDEN_COUNT(PageArgument& args); + String _token_CONFIG_STAIP(PageArgument& args); + String _token_CONFIG_STAGATEWAY(PageArgument& args); + String _token_CONFIG_STANETMASK(PageArgument& args); + String _token_CONFIG_STADNS1(PageArgument& args); + String _token_CONFIG_STADNS2(PageArgument& args); String _token_OPEN_SSID(PageArgument& args); String _token_UPTIME(PageArgument& args); String _token_BOOTURI(PageArgument& args); diff --git a/src/AutoConnectCredential.cpp b/src/AutoConnectCredential.cpp index c4de8d2..116a6bd 100644 --- a/src/AutoConnectCredential.cpp +++ b/src/AutoConnectCredential.cpp @@ -2,8 +2,8 @@ * AutoConnectCredential class dispatcher. * @file AutoConnectCredential.cpp * @author hieromon@gmail.com - * @version 1.0.2 - * @date 2019-09-16 + * @version 1.1.0 + * @date 2019-10-07 * @copyright MIT license. */ @@ -16,16 +16,23 @@ * AutoConnectCredential constructor takes the available count of saved * entries. * A stored credential data structure in EEPROM. - * 0 7 8 9a b (t) - * +--------+-+--+-----------------+-----------------+--+ - * |AC_CREDT|e|ss|ssid\0pass\0bssid|ssid\0pass\0bssid|\0| - * +--------+-+--+-----------------+-----------------+--+ + * 0 7 8 9a b (u) (u+16) (t) + * +--------+-+--+-----------------+-+--+--+--+----+----+-----------------+--+ + * |AC_CREDT|e|ss|ssid\0pass\0bssid|d|ip|gw|nm|dns1|dns2|ssid\0pass\0bssid|\0| + * +--------+-+--+-----------------+-+--+--+--+----+----+-----------------+--+ * AC_CREDT : Identifier. 8 characters. * e : Number of contained entries(uint8_t). * ss : Container size, excluding ID and number of entries(uint16_t). * ssid: SSID string with null termination. * password : Password string with null termination. * bssid : BSSID 6 bytes. + * d : DHCP is in available. 0:DCHP 1:Static IP + * ip - dns2 : Optional fields for static IPs configuration, these fields are available when d=1. + * ip : Static IP (uint32_t) + * gw : Gateway address (uint32_t) + * nm : Netmask (uint32_t) + * dns1 : Primary DNS (uint32) + * dns2 : Secondary DNS (uint32_t) * t : The end of the container is a continuous '\0'. * The AC_CREDT identifier is at the beginning of the area. * SSID and PASSWORD are terminated by '\ 0'. @@ -81,7 +88,7 @@ AutoConnectCredential::~AutoConnectCredential() { * false Could not deleted. */ bool AutoConnectCredential::del(const char* ssid) { - struct station_config entry; + station_config_t entry; bool rc = false; if (load(ssid, &entry) >= 0) { @@ -100,9 +107,15 @@ bool AutoConnectCredential::del(const char* ssid) { // Erase BSSID _eeprom->write(_dp++, 0xff); - for (uint8_t i = 0; i < sizeof(station_config::bssid); i++) + for (uint8_t i = 0; i < sizeof(station_config_t::bssid); i++) _eeprom->write(_dp++, 0xff); + // Erase ip configuration extention + if (_eeprom->read(_dp) == STA_STATIC) { + for (uint8_t i = 0; i < sizeof(station_config_t::_config); i++) + _eeprom->write(_dp++, 0xff); + } + // End 0xff writing, update headers. _entries--; _eeprom->write(_offset + static_cast(sizeof(AC_IDENTIFIER)) - 1, _entries); @@ -124,15 +137,15 @@ bool AutoConnectCredential::del(const char* ssid) { * @retval The entry number of the SSID in EEPROM. If the number less than 0, * the specified SSID was not found. */ -int8_t AutoConnectCredential::load(const char* ssid, struct station_config* config) { +int8_t AutoConnectCredential::load(const char* ssid, station_config_t* config) { int8_t entry = -1; _dp = AC_HEADERSIZE; if (_entries) { _eeprom->begin(AC_HEADERSIZE + _containSize); for (uint8_t i = 0; i < _entries; i++) { - _retrieveEntry(reinterpret_cast(config->ssid), reinterpret_cast(config->password), config->bssid); - if (!strcmp(ssid, (const char*)config->ssid)) { + _retrieveEntry(config); + if (!strcmp(ssid, reinterpret_cast(config->ssid))) { entry = i; break; } @@ -151,12 +164,12 @@ int8_t AutoConnectCredential::load(const char* ssid, struct station_config* conf * @retval true The entry number of the SSID in EEPROM. * false The number is not available. */ -bool AutoConnectCredential::load(int8_t entry, struct station_config* config) { +bool AutoConnectCredential::load(int8_t entry, station_config_t* config) { _dp = AC_HEADERSIZE; if (_entries && entry < _entries) { _eeprom->begin(AC_HEADERSIZE + _containSize); while (entry-- >= 0) - _retrieveEntry(reinterpret_cast(config->ssid), reinterpret_cast(config->password), config->bssid); + _retrieveEntry(config); _eeprom->end(); return true; } @@ -174,18 +187,18 @@ bool AutoConnectCredential::load(int8_t entry, struct station_config* config) { * @retval true Successfully saved. * @retval false EEPROM commit failed. */ -bool AutoConnectCredential::save(const struct station_config* config) { +bool AutoConnectCredential::save(const station_config_t* config) { static const char _id[] = AC_IDENTIFIER; - struct station_config stage; + station_config_t stage; int8_t entry; bool rep = false; bool rc; // Detect same entry for replacement. - entry = load((const char*)(config->ssid), &stage); + entry = load(reinterpret_cast(config->ssid), &stage); // Saving start. - _eeprom->begin(AC_HEADERSIZE + _containSize + sizeof(struct station_config)); + _eeprom->begin(AC_HEADERSIZE + _containSize + sizeof(station_config_t)); // Determine insertion or replacement. if (entry >= 0) { @@ -196,9 +209,15 @@ bool AutoConnectCredential::save(const struct station_config* config) { dm--; _eeprom->write(_dp, 0xff); // Clear SSID, Passphrase } - for (uint8_t i = 0; i < sizeof(station_config::bssid); i++) { + for (uint8_t i = 0; i < sizeof(station_config_t::bssid); i++) { _eeprom->write(_dp++, 0xff); // Clear BSSID } + uint8_t ss = _eeprom->read(_dp); // Read dhcp assignment flag + _eeprom->write(_dp++, 0xff); // Clear dhcp + if (ss == (uint8_t)STA_STATIC) { + for (uint8_t i = 0 ; i < sizeof(station_config_t::_config); i++) + _eeprom->write(_dp++, 0xff); // Clear static IPs + } } else { // Same entry not found. increase the entry. @@ -213,7 +232,11 @@ bool AutoConnectCredential::save(const struct station_config* config) { delay(10); // Seek insertion point, evaluate capacity to insert the new entry. - uint16_t eSize = strlen((const char*)config->ssid) + strlen((const char*)config->password) + sizeof(station_config::bssid) + 2; + uint16_t eSize = strlen(reinterpret_cast(config->ssid)) + strlen(reinterpret_cast(config->password)) + sizeof(station_config_t::bssid) + sizeof(station_config_t::dhcp); + if (config->dhcp == (uint8_t)STA_STATIC) + eSize += sizeof(station_config_t::_config); + eSize += sizeof('\0') + sizeof('\0'); + for (_dp = AC_HEADERSIZE; _dp < _containSize + AC_HEADERSIZE; _dp++) { uint8_t c = _eeprom->read(_dp); if (c == 0xff) { @@ -241,9 +264,17 @@ bool AutoConnectCredential::save(const struct station_config* config) { c = *dt++; _eeprom->write(_dp++, c); } while (c != '\0'); - for (uint8_t i = 0; i < sizeof(station_config::bssid); i++) { + for (uint8_t i = 0; i < sizeof(station_config_t::bssid); i++) _eeprom->write(_dp++, config->bssid[i]); // write BSSID + _eeprom->write(_dp++, config->dhcp); // write dhcp flag + if (config->dhcp == (uint8_t)STA_STATIC) { + for (uint8_t e = 0; e < sizeof(station_config_t::_config::addr) / sizeof(uint32_t); e++) { + uint32_t ip = config->config.addr[e]; + for (uint8_t b = 1; b <= sizeof(ip); b++) + _eeprom->write(_dp++, ((uint8_t*)&ip)[sizeof(ip) - b]); + } } + // Terminate container, mark to the end of credential area. // When the entry is replaced, not mark a terminator. if (!rep) { @@ -268,27 +299,41 @@ bool AutoConnectCredential::save(const struct station_config* config) { * @param ssid A SSID storing address. * @param password A password storing address. */ -void AutoConnectCredential::_retrieveEntry(char* ssid, char* password, uint8_t* bssid) { +void AutoConnectCredential::_retrieveEntry(station_config_t* config) { uint8_t ec; // Skip unavailable entry. while ((ec = _eeprom->read(_dp++)) == 0xff) {} + _ep = _dp - 1; // Retrieve SSID - _ep = _dp - 1; - *ssid++ = ec; + uint8_t* bp = config->ssid; + *bp++ = ec; do { ec = _eeprom->read(_dp++); - *ssid++ = ec; + *bp++ = ec; } while (ec != '\0'); // Retrieve Password + bp = config->password; do { ec = _eeprom->read(_dp++); - *password++ = ec; + *bp++ = ec; } while (ec != '\0'); // Retrieve BSSID - for (uint8_t i = 0; i < sizeof(station_config::bssid); i++) - bssid[i] = _eeprom->read(_dp++); + for (uint8_t i = 0; i < sizeof(station_config_t::bssid); i++) + config->bssid[i] = _eeprom->read(_dp++); + // Extended readout for static IP + config->dhcp = _eeprom->read(_dp++); + if (config->dhcp == (uint8_t)STA_STATIC) { + for (uint8_t e = 0; e < sizeof(station_config_t::_config::addr) / sizeof(uint32_t); e++) { + uint32_t* ip = &config->config.addr[e]; + *ip = 0; + for (uint8_t b = 0; b < sizeof(uint32_t); b++) { + *ip <<= 8; + *ip += _eeprom->read(_dp++); + } + } + } } #else @@ -299,19 +344,24 @@ void AutoConnectCredential::_retrieveEntry(char* ssid, char* password, uint8_t* * The credential area in the flash used by AutoConnect was moved from * EEPROM to NVS with v.1.0.0. A stored credential data structure of * Preferences is as follows. It has no identifier as AC_CREDT. - * 0 12 3 (t) - * +-+--+-----------------+-----------------+--+ - * |e|ss|ssid\0pass\0bssid|ssid\0pass\0bssid|\0| - * +-+--+-----------------+-----------------+--+ + * 0 12 3 (u) (u+16) (t) + * +-+--+-----------------+-+--+--+--+----+----+-----------------+--+ + * |e|ss|ssid\0pass\0bssid|d|ip|gw|nm|dns1|dns2|ssid\0pass\0bssid|\0| + * +-+--+-----------------+-+--+--+--+----+----+-----------------+--+ * e : Number of contained entries(uint8_t). * ss : Container size, excluding ID and number of entries(uint16_t). * ssid: SSID string with null termination. * password : Password string with null termination. * bssid : BSSID 6 bytes. + * d : DHCP is in available. 0:DCHP 1:Static IP + * ip - dns2 : Optional fields for static IPs configuration, these fields are available when d=1. + * ip : Static IP (uint32_t) + * gw : Gateway address (uint32_t) + * nm : Netmask (uint32_t) + * dns1 : Primary DNS (uint32) + * dns2 : Secondary DNS (uint32_t) * t : The end of the container is a continuous '\0'. - * The AC_CREDT identifier is at the beginning of the area. * SSID and PASSWORD are terminated by '\ 0'. - * Free area are filled with FF, which is reused as an area for insertion. */ AutoConnectCredential::AutoConnectCredential() { _allocateEntry(); @@ -362,7 +412,7 @@ inline uint8_t AutoConnectCredential::entries(void) { * @retval The entry number of the SSID. If the number less than 0, * the specified SSID was not found. */ -int8_t AutoConnectCredential::load(const char* ssid, struct station_config* config) { +int8_t AutoConnectCredential::load(const char* ssid, station_config_t* config) { // Determine the number in entries int8_t en = 0; _entries = _import(); // Reload the saved credentials @@ -386,7 +436,7 @@ int8_t AutoConnectCredential::load(const char* ssid, struct station_config* conf * @retval true The entry number of the SSID. * false The number is not available. */ -bool AutoConnectCredential::load(int8_t entry, struct station_config* config) { +bool AutoConnectCredential::load(int8_t entry, station_config_t* config) { _entries = _import(); for (decltype(_credit)::iterator it = _credit.begin(), e = _credit.end(); it != e; ++it) { if (!entry--) { @@ -406,7 +456,7 @@ bool AutoConnectCredential::load(int8_t entry, struct station_config* config) { * @retval true Successfully saved. * @retval false Preferences commit failed. */ -bool AutoConnectCredential::save(const struct station_config* config) { +bool AutoConnectCredential::save(const station_config_t* config) { if (_add(config)) { return _commit() > 0 ? true : false; } @@ -432,6 +482,9 @@ bool AutoConnectCredential::_add(const station_config_t* config) { AC_CREDTBODY_t credtBody; credtBody.password = String(reinterpret_cast(config->password)); memcpy(credtBody.bssid, config->bssid, sizeof(AC_CREDTBODY_t::bssid)); + credtBody.dhcp = config->dhcp; + for (uint8_t e = 0; e < sizeof(AC_CREDTBODY_t::ip) / sizeof(uint32_t); e++) + credtBody.ip[e] = credtBody.dhcp == (uint8_t)STA_STATIC ? config->config.addr[e] : 0U; std::pair rc = _credit.insert(std::make_pair(ssid, credtBody)); _entries = _credit.size(); #ifdef AC_DBG @@ -455,7 +508,11 @@ size_t AutoConnectCredential::_commit(void) { for (const auto& credt : _credit) { ssid = credt.first; credtBody = credt.second; - sz += ssid.length() + sizeof('\0') + credtBody.password.length() + sizeof('\0') + sizeof(AC_CREDTBODY_t::bssid); + sz += ssid.length() + sizeof('\0') + credtBody.password.length() + sizeof('\0') + sizeof(AC_CREDTBODY_t::bssid) + sizeof(AC_CREDTBODY_t::dhcp); + if (credtBody.dhcp == (uint32_t)STA_STATIC) { + for (uint8_t e = 0; e < sizeof(AC_CREDTBODY_t::ip) / sizeof(uint32_t); e++) + sz += sizeof(uint32_t); + } } // When the entry is not empty, the size of container terminator as '\0' must be added. _containSize = sz + (_entries ? sizeof('\0') : 0); @@ -484,6 +541,16 @@ size_t AutoConnectCredential::_commit(void) { dp += itemLen; memcpy(&credtPool[dp], credtBody.bssid, sizeof(station_config_t::bssid)); dp += sizeof(station_config_t::bssid); + // DHCP/Static IP indicator + credtPool[dp++] = (uint8_t)credtBody.dhcp; + // Static IP configuration + if (credtBody.dhcp == STA_STATIC) { + for (uint8_t e = 0; e < sizeof(AC_CREDTBODY_t::ip) / sizeof(uint32_t); e++) { + // uint32_t ip = credtBody.ip[e]; + for (uint8_t b = 1; b <= sizeof(credtBody.ip[e]); b++) + credtPool[dp++] = ((uint8_t*)&credtBody.ip[e])[sizeof(credtBody.ip[e]) - b]; + } + } } if (_credit.size() > 0) credtPool[dp] = '\0'; // Terminates a container @@ -552,9 +619,22 @@ uint8_t AutoConnectCredential::_import(void) { // BSSID dp += credtBody.password.length() + sizeof('\0'); memcpy(credtBody.bssid, &credtPool[dp], sizeof(AC_CREDTBODY_t::bssid)); + dp += sizeof(AC_CREDTBODY_t::bssid); + // DHCP/Static IP indicator + credtBody.dhcp = credtPool[dp++]; + // Static IP configuration + for (uint8_t e = 0; e < sizeof(AC_CREDTBODY_t::ip) / sizeof(uint32_t); e++) { + uint32_t* ip = &credtBody.ip[e]; + *ip = 0U; + if (credtBody.dhcp == (uint8_t)STA_STATIC) { + for (uint8_t b = 0; b < sizeof(uint32_t); b++) { + *ip <<= 8; + *ip += credtPool[dp++]; + } + } + } // Make an entry _credit.insert(std::make_pair(ssid, credtBody)); - dp += sizeof(AC_CREDTBODY_t::bssid); } free(credtPool); } @@ -585,6 +665,9 @@ void AutoConnectCredential::_obtain(AC_CREDT_t::iterator const& it, station_conf ssid.toCharArray(reinterpret_cast(config->ssid), sizeof(station_config_t::ssid)); credtBody.password.toCharArray(reinterpret_cast(config->password), sizeof(station_config_t::password)); memcpy(config->bssid, credtBody.bssid, sizeof(station_config_t::bssid)); + config->dhcp = credtBody.dhcp; + for (uint8_t e = 0; e < sizeof(AC_CREDTBODY_t::ip) / sizeof(uint32_t); e++) + config->config.addr[e] = credtBody.dhcp == (uint8_t)STA_STATIC ? credtBody.ip[e] : 0U; } #endif diff --git a/src/AutoConnectCredential.h b/src/AutoConnectCredential.h index ec6b641..de75003 100644 --- a/src/AutoConnectCredential.h +++ b/src/AutoConnectCredential.h @@ -2,8 +2,8 @@ * Declaration of AutoConnectCredential class. * @file AutoConnectCredential.h * @author hieromon@gmail.com - * @version 1.0.2 - * @date 2019-09-16 + * @version 1.1.0 + * @date 2019-10-07 * @copyright MIT license. */ @@ -32,13 +32,6 @@ extern "C" { #define AC_CREDENTIAL_PREFERENCES 0 #endif #include -struct station_config { -uint8_t ssid[32]; -uint8_t password[64]; -uint8_t bssid_set; -uint8_t bssid[6]; -wifi_fast_scan_threshold_t threshold; -}; #endif #include "AutoConnectDefs.h" @@ -59,15 +52,37 @@ wifi_fast_scan_threshold_t threshold; #define AC_IDENTIFIER "AC_CREDT" #endif +typedef enum { + STA_DHCP, + STA_STATIC +} station_config_dhcp; + +typedef struct { + uint8_t ssid[32]; + uint8_t password[64]; + uint8_t bssid[6]; + uint8_t dhcp; /**< 1:DHCP, 2:Static IP */ + union _config { + uint32_t addr[5]; + struct _sta { + uint32_t ip; + uint32_t gateway; + uint32_t netmask; + uint32_t dns1; + uint32_t dns2; + } sta; + } config; +} station_config_t; + class AutoConnectCredentialBase { public: explicit AutoConnectCredentialBase() : _entries(0), _containSize(0) {} virtual ~AutoConnectCredentialBase() {} virtual uint8_t entries(void) { return _entries; } virtual bool del(const char* ssid) = 0; - virtual int8_t load(const char* ssid, struct station_config* config) = 0; - virtual bool load(int8_t entry, struct station_config* config) = 0; - virtual bool save(const struct station_config* config) = 0; + virtual int8_t load(const char* ssid, station_config_t* config) = 0; + virtual bool load(int8_t entry, station_config_t* config) = 0; + virtual bool save(const station_config_t* config) = 0; protected: virtual void _allocateEntry(void) = 0; /**< Initialize storage for credentials. */ @@ -88,15 +103,15 @@ class AutoConnectCredential : public AutoConnectCredentialBase { explicit AutoConnectCredential(uint16_t offset); ~AutoConnectCredential(); bool del(const char* ssid) override; - int8_t load(const char* ssid, struct station_config* config) override; - bool load(int8_t entry, struct station_config* config) override; - bool save(const struct station_config* config) override; + int8_t load(const char* ssid, station_config_t* config) override; + bool load(int8_t entry, station_config_t* config) override; + bool save(const station_config_t* config) override; protected: void _allocateEntry(void) override; /**< Initialize storage for credentials. */ private: - void _retrieveEntry(char* ssid, char* password, uint8_t* bssid); /**< Read an available entry. */ + void _retrieveEntry(station_config_t* config); /**< Read an available entry. */ int _dp; /**< The current address in EEPROM */ int _ep; /**< The current entry address in EEPROM */ @@ -127,20 +142,22 @@ class AutoConnectCredential : public AutoConnectCredentialBase { ~AutoConnectCredential(); bool del(const char* ssid) override; uint8_t entries(void) override; - int8_t load(const char* ssid, struct station_config* config) override; - bool load(int8_t entry, struct station_config* config) override; - bool save(const struct station_config* config) override; + int8_t load(const char* ssid, station_config_t* config) override; + bool load(int8_t entry, station_config_t* config) override; + bool save(const station_config_t* config) override; protected: void _allocateEntry(void) override; /**< Initialize storage for credentials. */ private: typedef struct { - String password; - uint8_t bssid[6]; + String password; + uint8_t bssid[6]; + uint8_t dhcp; /**< 1:DHCP, 2:Static IP */ + uint32_t ip[5]; } AC_CREDTBODY_t; /**< Credential entry */ typedef std::map AC_CREDT_t; - typedef station_config station_config_t; + // typedef station_config station_config_t; bool _add(const station_config_t* config); /**< Add an entry */ size_t _commit(void); /**< Write back to the nvs */ diff --git a/src/AutoConnectPage.cpp b/src/AutoConnectPage.cpp index 269fc95..24b0257 100644 --- a/src/AutoConnectPage.cpp +++ b/src/AutoConnectPage.cpp @@ -2,8 +2,8 @@ * AutoConnect portal site web page implementation. * @file AutoConnectPage.h * @author hieromon@gmail.com - * @version 1.0.2 - * @date 2019-09-17 + * @version 1.1.0 + * @date 2019-10-07 * @copyright MIT license. */ @@ -38,12 +38,12 @@ const char AutoConnect::_CSS_BASE[] PROGMEM = { ".base-panel{" "margin:0 22px 0 22px" "}" - ".base-panel>*>label{" + ".base-panel * label{" "display:inline-block;" "width:3.0em;" "text-align:right" "}" - ".base-panel>*>label.slist{" + ".base-panel * .slist{" "width:auto;" "font-size:0.9em;" "margin-left:10px;" @@ -100,34 +100,33 @@ const char AutoConnect::_CSS_BASE[] PROGMEM = { /**< non-marked list for UL */ const char AutoConnect::_CSS_UL[] PROGMEM = { - "ul.noorder{" + ".noorder,.exp{" "padding:0;" "list-style:none;" "display:table" "}" - "ul.noorder li{" - "display:table-row" + ".noorder li,.exp{" + "display:table-row-group" "}" - "ul.noorder>*>label{" + ".noorder li label, .exp li *{" "display:table-cell;" "width:auto;" - "margin-right:10px;" "text-align:right;" "padding:10px 0.5em" "}" - "ul.noorder input[type=\"checkbox\"]{" + ".noorder input[type=\"checkbox\"]{" "-moz-appearance:checkbox;" "-webkit-appearance:checkbox" "}" - "ul.noorder input[type=\"radio\"]{" + ".noorder input[type=\"radio\"]{" "margin-right:0.5em;" "-moz-appearance:radio;" "-webkit-appearance:radio" "}" - "ul.noorder input[type=\"text\"]{" + ".noorder input[type=\"text\"]{" "width:auto" "}" - "ul.noorder input[type=\"text\"]:invalid{" + ".noorder input[type=\"text\"]:invalid{" "background:#fce4d6" "}" }; @@ -223,7 +222,8 @@ const char AutoConnect::_CSS_INPUT_TEXT[] PROGMEM = { "color:#D9434E" "}" ".aux-page label{" - "padding:10px 0.5em" + "display:inline;" + "padding:10px 0.5em;" "}" }; @@ -683,18 +683,44 @@ const char AutoConnect::_PAGE_CONFIGNEW[] PROGMEM = { "" "" "" - "
  • " + "
  • " + "" + "" + "
  • " + "
  • " + "" + "" + "
  • " + "
  • " + "" + "" + "
  • " + "
  • " + "" + "" + "
  • " + "
  • " + "" + "" + "
  • " + "
  • " + "" + "" + "
  • " + "
  • " "" "" "" "" - "" "" + "" "" }; @@ -1189,10 +1215,35 @@ String AutoConnect::_token_HIDDEN_COUNT(PageArgument& args) { return String(_hiddenSSIDCount); } +String AutoConnect::_token_CONFIG_STAIP(PageArgument& args) { + AC_UNUSED(args); + return !_apConfig.staip ? String(F("0.0.0.0")) : _apConfig.staip.toString(); +} + +String AutoConnect::_token_CONFIG_STAGATEWAY(PageArgument& args) { + AC_UNUSED(args); + return !_apConfig.staGateway ? String(F("0.0.0.0")) : _apConfig.staGateway.toString(); +} + +String AutoConnect::_token_CONFIG_STANETMASK(PageArgument& args) { + AC_UNUSED(args); + return !_apConfig.staNetmask ? String(F("0.0.0.0")) : _apConfig.staNetmask.toString(); +} + +String AutoConnect::_token_CONFIG_STADNS1(PageArgument& args) { + AC_UNUSED(args); + return !_apConfig.dns1 ? String(F("0.0.0.0")) : _apConfig.dns1.toString(); +} + +String AutoConnect::_token_CONFIG_STADNS2(PageArgument& args) { + AC_UNUSED(args); + return !_apConfig.dns2 ? String(F("0.0.0.0")) : _apConfig.dns2.toString(); +} + String AutoConnect::_token_OPEN_SSID(PageArgument& args) { AC_UNUSED(args); AutoConnectCredential credit(_apConfig.boundaryOffset); - struct station_config entry; + station_config_t entry; String ssidList; String rssiSym; @@ -1210,7 +1261,7 @@ String AutoConnect::_token_OPEN_SSID(PageArgument& args) { ssidList += String(F("(entry.ssid)) + String(F("\">")); for (int8_t sc = 0; sc < (int8_t)_scanCount; sc++) { - if (!memcmp(entry.bssid, WiFi.BSSID(sc), sizeof(station_config::bssid))) { + if (!memcmp(entry.bssid, WiFi.BSSID(sc), sizeof(station_config_t::bssid))) { _connectCh = WiFi.channel(sc); rssiSym = String(AutoConnect::_toWiFiQuality(WiFi.RSSI(sc))) + String(F("% Ch.")) + String(_connectCh) + String(F("")); if (WiFi.encryptionType(sc) != ENC_TYPE_NONE) @@ -1240,7 +1291,7 @@ String AutoConnect::_token_BOOTURI(PageArgument& args) { String AutoConnect::_token_CURRENT_SSID(PageArgument& args) { AC_UNUSED(args); - char ssid_c[sizeof(station_config::ssid) + 1]; + char ssid_c[sizeof(station_config_t::ssid) + 1]; *ssid_c = '\0'; strncat(ssid_c, reinterpret_cast(_credential.ssid), sizeof(ssid_c) - 1); String ssid = String(ssid_c); @@ -1307,6 +1358,11 @@ PageElement* AutoConnect::_setupPage(String uri) { elm->addToken(String(FPSTR("LIST_SSID")), std::bind(&AutoConnect::_token_LIST_SSID, this, std::placeholders::_1)); elm->addToken(String(FPSTR("SSID_COUNT")), std::bind(&AutoConnect::_token_SSID_COUNT, this, std::placeholders::_1)); elm->addToken(String(FPSTR("HIDDEN_COUNT")), std::bind(&AutoConnect::_token_HIDDEN_COUNT, this, std::placeholders::_1)); + elm->addToken(String(FPSTR("CONFIG_IP")), std::bind(&AutoConnect::_token_CONFIG_STAIP, this, std::placeholders::_1)); + elm->addToken(String(FPSTR("CONFIG_GW")), std::bind(&AutoConnect::_token_CONFIG_STAGATEWAY, this, std::placeholders::_1)); + elm->addToken(String(FPSTR("CONFIG_NM")), std::bind(&AutoConnect::_token_CONFIG_STANETMASK, this, std::placeholders::_1)); + elm->addToken(String(FPSTR("CONFIG_DNS1")), std::bind(&AutoConnect::_token_CONFIG_STADNS1, this, std::placeholders::_1)); + elm->addToken(String(FPSTR("CONFIG_DNS2")), std::bind(&AutoConnect::_token_CONFIG_STADNS2, this, std::placeholders::_1)); } else if (uri == String(AUTOCONNECT_URI_CONNECT)) { diff --git a/src/AutoConnectPage.h b/src/AutoConnectPage.h index 3a53869..748b945 100644 --- a/src/AutoConnectPage.h +++ b/src/AutoConnectPage.h @@ -2,8 +2,8 @@ * AutoConnect portal site web page declaration. * @file AutoConnectPage.h * @author hieromon@gmail.com - * @version 1.0.0 - * @date 2019-08-15 + * @version 1.1.0 + * @date 2019-10-04 * @copyright MIT license. */ @@ -15,6 +15,12 @@ #define AUTOCONNECT_PARAMID_SSID "SSID" #define AUTOCONNECT_PARAMID_PASS "Passphrase" #define AUTOCONNECT_PARAMID_CRED "Credential" +#define AUTOCONNECT_PARAMID_DHCP "dhcp" +#define AUTOCONNECT_PARAMID_STAIP "staip" +#define AUTOCONNECT_PARAMID_GTWAY "gateway" +#define AUTOCONNECT_PARAMID_NTMSK "netmask" +#define AUTOCONNECT_PARAMID_DNS1 "dns1" +#define AUTOCONNECT_PARAMID_DNS2 "dns2" // AutoConnect menu hyper-link as image #define AUTOCONNECT_GLYPH_COG_16 "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAA" \ From ba3251f42b7411aca95d064bb863f419b71ed89e Mon Sep 17 00:00:00 2001 From: Hieromon Ikasamo Date: Tue, 8 Oct 2019 16:59:34 +0900 Subject: [PATCH 02/25] Support the Config New with static IP --- mkdocs/images/staticip.png | Bin 0 -> 24328 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 mkdocs/images/staticip.png diff --git a/mkdocs/images/staticip.png b/mkdocs/images/staticip.png new file mode 100644 index 0000000000000000000000000000000000000000..7a02cd22c7449c24897497c3c4abdcf693ab6637 GIT binary patch literal 24328 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 Date: Tue, 8 Oct 2019 18:28:19 +0900 Subject: [PATCH 03/25] Fixed static IP loss with Open SSIDs --- src/AutoConnect.cpp | 67 +++++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/src/AutoConnect.cpp b/src/AutoConnect.cpp index 9de0f0b..2d13fde 100644 --- a/src/AutoConnect.cpp +++ b/src/AutoConnect.cpp @@ -806,9 +806,7 @@ String AutoConnect::_induceConnect(PageArgument& args) { // Read from EEPROM AutoConnectCredential credential(_apConfig.boundaryOffset); station_config_t entry; - credential.load(args.arg(String(F(AUTOCONNECT_PARAMID_CRED))).c_str(), &entry); - strncpy(reinterpret_cast(_credential.ssid), reinterpret_cast(entry.ssid), sizeof(_credential.ssid)); - strncpy(reinterpret_cast(_credential.password), reinterpret_cast(entry.password), sizeof(_credential.password)); + 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(_credential.ssid), _credential.dhcp == STA_DHCP ? "DHCP" : staip.toString().c_str()); @@ -819,8 +817,44 @@ String AutoConnect::_induceConnect(PageArgument& args) { // Credential had by the post parameter. strncpy(reinterpret_cast(_credential.ssid), args.arg(String(F(AUTOCONNECT_PARAMID_SSID))).c_str(), sizeof(_credential.ssid)); strncpy(reinterpret_cast(_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; + IPAddress cast; + if (args.hasArg(String(F(AUTOCONNECT_PARAMID_STAIP)))) { + cast.fromString(args.arg(String(F(AUTOCONNECT_PARAMID_STAIP)))); + _credential.config.sta.ip = static_cast(cast); + } + if (args.hasArg(String(F(AUTOCONNECT_PARAMID_GTWAY)))) { + cast.fromString(args.arg(String(F(AUTOCONNECT_PARAMID_GTWAY)))); + _credential.config.sta.gateway = static_cast(cast); + } + if (args.hasArg(String(F(AUTOCONNECT_PARAMID_NTMSK)))) { + cast.fromString(args.arg(String(F(AUTOCONNECT_PARAMID_NTMSK)))); + _credential.config.sta.netmask = static_cast(cast); + } + if (args.hasArg(String(F(AUTOCONNECT_PARAMID_DNS1)))) { + cast.fromString(args.arg(String(F(AUTOCONNECT_PARAMID_DNS1)))); + _credential.config.sta.dns1 = static_cast(cast); + } + if (args.hasArg(String(F(AUTOCONNECT_PARAMID_DNS2)))) { + cast.fromString(args.arg(String(F(AUTOCONNECT_PARAMID_DNS2)))); + _credential.config.sta.dns2 = static_cast(cast); + } + } } + // Restore the configured IPs to STA configuration + _apConfig.staip = static_cast(_credential.config.sta.ip); + _apConfig.staGateway = static_cast(_credential.config.sta.gateway); + _apConfig.staNetmask = static_cast(_credential.config.sta.netmask); + _apConfig.dns1 = static_cast(_credential.config.sta.dns1); + _apConfig.dns2 = static_cast(_credential.config.sta.dns2); + // Determine the connection channel based on the scan result. _connectCh = 0; for (uint8_t nn = 0; nn < _scanCount; nn++) { @@ -831,33 +865,6 @@ String AutoConnect::_induceConnect(PageArgument& args) { } } - // Static IP detection - _credential.config.sta.ip = _credential.config.sta.gateway = _credential.config.sta.netmask = _credential.config.sta.dns1 = _credential.config.sta.dns2 = 0U; - if (!args.hasArg(String(F(AUTOCONNECT_PARAMID_DHCP)))) { - _credential.dhcp = STA_STATIC; - if (args.hasArg(String(F(AUTOCONNECT_PARAMID_STAIP)))) { - _apConfig.staip.fromString(args.arg(String(F(AUTOCONNECT_PARAMID_STAIP)))); - _credential.config.sta.ip = (uint32_t)_apConfig.staip; - } - if (args.hasArg(String(F(AUTOCONNECT_PARAMID_GTWAY)))) { - _apConfig.staGateway.fromString(args.arg(String(F(AUTOCONNECT_PARAMID_GTWAY)))); - _credential.config.sta.gateway = (uint32_t)_apConfig.staGateway; - } - if (args.hasArg(String(F(AUTOCONNECT_PARAMID_NTMSK)))) { - _apConfig.staNetmask.fromString(args.arg(String(F(AUTOCONNECT_PARAMID_NTMSK)))); - _credential.config.sta.netmask = (uint32_t)_apConfig.staNetmask; - } - if (args.hasArg(String(F(AUTOCONNECT_PARAMID_DNS1)))) { - _apConfig.dns1.fromString(args.arg(String(F(AUTOCONNECT_PARAMID_DNS1)))); - _credential.config.sta.dns1 = (uint32_t)_apConfig.dns1; - } - if (args.hasArg(String(F(AUTOCONNECT_PARAMID_DNS2)))) - _apConfig.dns2.fromString(args.arg(String(F(AUTOCONNECT_PARAMID_DNS2)))); - _credential.config.sta.dns2 = (uint32_t)_apConfig.dns2; - } - else - _credential.dhcp = STA_DHCP; - // Turn on the trigger to start WiFi.begin(). _rfConnect = true; From 8a4dabef12992997bec45b51a9f00af9f9279f98 Mon Sep 17 00:00:00 2001 From: Hieromon Ikasamo Date: Tue, 8 Oct 2019 18:29:31 +0900 Subject: [PATCH 04/25] Change for testing --- src/AutoConnectDefs.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AutoConnectDefs.h b/src/AutoConnectDefs.h index 2a5e171..97e8fd5 100644 --- a/src/AutoConnectDefs.h +++ b/src/AutoConnectDefs.h @@ -11,7 +11,7 @@ #define _AUTOCONNECTDEFS_H_ // Uncomment the following AC_DEBUG to enable debug output. -//#define AC_DEBUG +#define AC_DEBUG // Debug output destination can be defined externally with AC_DEBUG_PORT #ifndef AC_DEBUG_PORT From dde5306cbdae021c0e6ae3424cc83e2612d69f72 Mon Sep 17 00:00:00 2001 From: Hieromon Ikasamo Date: Wed, 9 Oct 2019 17:38:40 +0900 Subject: [PATCH 05/25] Support the Config New with static IP --- mkdocs/images/process_begin.svg | 250 ++++++++++++++++++++++++++++---- 1 file changed, 221 insertions(+), 29 deletions(-) diff --git a/mkdocs/images/process_begin.svg b/mkdocs/images/process_begin.svg index 9815b95..ab1e19a 100644 --- a/mkdocs/images/process_begin.svg +++ b/mkdocs/images/process_begin.svg @@ -9,9 +9,9 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="99.56517mm" - height="304.07242mm" - viewBox="0 0 99.56517 304.07241" + width="99.499031mm" + height="371.83472mm" + viewBox="0 0 99.499031 371.83471" version="1.1" id="svg8776" inkscape:version="0.92.2 (5c3e80d, 2017-08-06)" @@ -224,6 +224,21 @@ style="stroke-width:2" id="path2786" /> + + + + originx="-23.664512" + originy="15.544299" /> @@ -280,7 +295,17 @@ inkscape:label="レイヤー 1" inkscape:groupmode="layer" id="layer1" - transform="translate(-23.598368,-8.4718652)"> + transform="translate(-23.664512,59.290408)"> + + + transform="translate(-33.734378,-65.116439)"> STA - @@ -781,13 +801,13 @@ @@ -838,13 +858,13 @@ inkscape:connector-curvature="0" /> @@ -870,7 +890,7 @@ inkscape:connector-curvature="0" /> @@ -891,7 +911,7 @@ inkscape:connector-curvature="0" /> @@ -912,7 +932,7 @@ inkscape:connector-curvature="0" /> @@ -982,12 +1002,6 @@ x="60.844864" y="110.3716" style="stroke-width:0.26458332">YES - @@ -1201,5 +1215,183 @@ x="60.844868" y="50.651817" style="stroke-width:0.26458332">Specified + + + + Set hostname + + + + + WiFi.config (STA) + + + + Load current config + + + + + + Static IP + + + + + + + + Restore saved config + + + NO + YES From 042e9300a1bb45332e41086e8e1705b7dfccf211 Mon Sep 17 00:00:00 2001 From: Hieromon Ikasamo Date: Thu, 10 Oct 2019 16:46:57 +0900 Subject: [PATCH 06/25] Changed IP address regular expression --- examples/ConfigIP/ConfigIP.ino | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/ConfigIP/ConfigIP.ino b/examples/ConfigIP/ConfigIP.ino index c77d8fa..7364fa3 100644 --- a/examples/ConfigIP/ConfigIP.ino +++ b/examples/ConfigIP/ConfigIP.ino @@ -63,28 +63,28 @@ static const char AUX_CONFIGIP[] PROGMEM = R"( "name": "staip", "type": "ACInput", "label": "IP", - "pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$", + "pattern": "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", "global": true }, { "name": "gateway", "type": "ACInput", "label": "Gateway", - "pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$", + "pattern": "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", "global": true }, { "name": "netmask", "type": "ACInput", "label": "Netmask", - "pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$", + "pattern": "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", "global": true }, { "name": "dns1", "type": "ACInput", "label": "DNS", - "pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$", + "pattern": "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", "global": true }, { From 650885f0cf74b101ede100d9c81ce5fb185a6f3d Mon Sep 17 00:00:00 2001 From: Hieromon Ikasamo Date: Thu, 10 Oct 2019 16:48:58 +0900 Subject: [PATCH 07/25] Added a pattern to the static IP fields --- src/AutoConnectPage.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/AutoConnectPage.cpp b/src/AutoConnectPage.cpp index 24b0257..13ae8ba 100644 --- a/src/AutoConnectPage.cpp +++ b/src/AutoConnectPage.cpp @@ -713,6 +713,10 @@ const char AutoConnect::_PAGE_CONFIGNEW[] PROGMEM = { "" "" " )\" ; ACInput(Text1, \"Text1\" ); ACInput(Text2, \"Text2\" ); ACButton(Button, \"COPY\" , \"CopyText()\" ); ACElement(TextCopy, scCopyText); post \u00b6 Specifies a tag to add behind the HTML code generated from the element. The default values is AC_Tag_None . AutoConnectCheckbox \u00b6 AutoConnectCheckbox generates an HTML < input type = \"checkbox\" > tag and a < label > tag. It places horizontally on a custom Web page by default. Sample AutoConnectCheckbox checkbox(\"checkbox\", \"uniqueapid\", \"Use APID unique\", false); On the page: Constructor \u00b6 AutoConnectCheckbox( const char * name, const char * value, const char * label, const bool checked, const ACPosition_t labelPosition, const ACPosterior_t post) name \u00b6 It is the name of the AutoConnectCheckbox element and matches the name attribute of the input tag. It also becomes the parameter name of the query string when submitted. value \u00b6 It becomes a value of the value attribute of an HTML < input type = \"checkbox\" > tag. label \u00b6 A label is an optional string. A label is always arranged on the right side of the checkbox. Specification of a label will generate an HTML