diff --git a/src/Wippersnapper.cpp b/src/Wippersnapper.cpp index d702b915..72896b28 100644 --- a/src/Wippersnapper.cpp +++ b/src/Wippersnapper.cpp @@ -2479,7 +2479,7 @@ void Wippersnapper::haltError(String error, ws_led_status_t ledStatusColor) { #else // Calls to delay() and yield() feed the ESP8266's // hardware and software watchdog timers, delayMicroseconds does not. - delayMicroseconds(1000); + delayMicroseconds(1000000); #endif } } diff --git a/src/Wippersnapper.h b/src/Wippersnapper.h index cd93d961..09fe26e9 100644 --- a/src/Wippersnapper.h +++ b/src/Wippersnapper.h @@ -162,7 +162,8 @@ typedef enum { FSM_NET_ESTABLISH_MQTT, } fsm_net_t; -#define WS_WDT_TIMEOUT 60000 ///< WDT timeout +#define WS_WDT_TIMEOUT 60000 ///< WDT timeout +#define WS_MAX_ALT_WIFI_NETWORKS 3 ///< Maximum number of alternative networks /* MQTT Configuration */ #define WS_KEEPALIVE_INTERVAL_MS \ 5000 ///< Session keepalive interval time, in milliseconds @@ -316,6 +317,8 @@ class Wippersnapper { Adafruit_MQTT *_mqtt; /*!< Reference to Adafruit_MQTT, _mqtt. */ secretsConfig _config; /*!< Wippersnapper secrets.json as a struct. */ + networkConfig _multiNetworks[3]; /*!< Wippersnapper networks as structs. */ + bool _isWiFiMulti = false; /*!< True if multiple networks are defined. */ // TODO: Does this need to be within this class? int32_t totalDigitalPins; /*!< Total number of digital-input capable pins */ diff --git a/src/network_interfaces/Wippersnapper_ESP32.h b/src/network_interfaces/Wippersnapper_ESP32.h index 39348873..388dc6f7 100644 --- a/src/network_interfaces/Wippersnapper_ESP32.h +++ b/src/network_interfaces/Wippersnapper_ESP32.h @@ -24,6 +24,7 @@ #include "Adafruit_MQTT_Client.h" #include "Arduino.h" #include "WiFi.h" +#include "WiFiMulti.h" #include extern Wippersnapper WS; @@ -109,8 +110,17 @@ class Wippersnapper_ESP32 : public Wippersnapper { // Was the network within secrets.json found? for (int i = 0; i < n; ++i) { - if (strcmp(_ssid, WiFi.SSID(i).c_str()) == 0) + if (strcmp(_ssid, WiFi.SSID(i).c_str()) == 0) { return true; + } + if (WS._isWiFiMulti) { + // multi network mode + for (int j = 0; j < WS_MAX_ALT_WIFI_NETWORKS; j++) { + if (strcmp(WS._multiNetworks[j].ssid, WiFi.SSID(i).c_str()) == 0) { + return true; + } + } + } } // User-set network not found, print scan results to serial console @@ -191,6 +201,7 @@ class Wippersnapper_ESP32 : public Wippersnapper { const char *_ssid; ///< WiFi SSID const char *_pass; ///< WiFi password WiFiClientSecure *_mqtt_client; ///< Pointer to a WiFi client object (TLS/SSL) + WiFiMulti _wifiMulti; ///< WiFiMulti object for multi-network mode const char *_aio_root_ca_staging = "-----BEGIN CERTIFICATE-----\n" @@ -262,11 +273,35 @@ class Wippersnapper_ESP32 : public Wippersnapper { if (strlen(_ssid) == 0) { _status = WS_SSID_INVALID; } else { + WiFi.setAutoReconnect(false); _disconnect(); delay(100); - WiFi.begin(_ssid, _pass); - _status = WS_NET_DISCONNECTED; - delay(5000); + if (WS._isWiFiMulti) { + // multi network mode + _wifiMulti.APlistClean(); + _wifiMulti.setAllowOpenAP(false); + // add default network + _wifiMulti.addAP(_ssid, _pass); + // add array of alternative networks + for (int i = 0; i < WS_MAX_ALT_WIFI_NETWORKS; i++) { + if (strlen(WS._multiNetworks[i].ssid) > 0) { + _wifiMulti.addAP(WS._multiNetworks[i].ssid, + WS._multiNetworks[i].pass); + } + } + if (_wifiMulti.run(20000) == WL_CONNECTED) { + _status = WS_NET_CONNECTED; + } else { + _status = WS_NET_DISCONNECTED; + } + } else { + // single network mode + WiFi.begin(_ssid, _pass); + _status = WS_NET_DISCONNECTED; + WS.feedWDT(); + delay(5000); + } + WS.feedWDT(); } } diff --git a/src/network_interfaces/Wippersnapper_ESP8266.h b/src/network_interfaces/Wippersnapper_ESP8266.h index 32d2cf26..35a95db0 100644 --- a/src/network_interfaces/Wippersnapper_ESP8266.h +++ b/src/network_interfaces/Wippersnapper_ESP8266.h @@ -21,6 +21,7 @@ #include "Adafruit_MQTT.h" #include "Adafruit_MQTT_Client.h" #include "ESP8266WiFi.h" +#include "ESP8266WiFiMulti.h" #include "Wippersnapper.h" /* NOTE - Projects that require "Secure MQTT" (TLS/SSL) also require a new @@ -64,6 +65,8 @@ class Wippersnapper_ESP8266 : public Wippersnapper { _ssid = 0; _pass = 0; _wifi_client = new WiFiClient; + WiFi.persistent(false); + WiFi.mode(WIFI_STA); } /**************************************************************************/ @@ -132,8 +135,16 @@ class Wippersnapper_ESP8266 : public Wippersnapper { // Was the network within secrets.json found? for (int i = 0; i < n; ++i) { - if (strcmp(_ssid, WiFi.SSID(i).c_str()) == 0) + if (strcmp(_ssid, WiFi.SSID(i).c_str()) == 0) { return true; + } + if (WS._isWiFiMulti) { + // multi network mode + for (int j = 0; j < WS_MAX_ALT_WIFI_NETWORKS; j++) { + if (strcmp(WS._multiNetworks[j].ssid, WiFi.SSID(i).c_str()) == 0) + return true; + } + } } // User-set network not found, print scan results to serial console @@ -211,6 +222,7 @@ class Wippersnapper_ESP8266 : public Wippersnapper { const char *_ssid = NULL; const char *_pass = NULL; WiFiClient *_wifi_client; + ESP8266WiFiMulti _wifiMulti; /**************************************************************************/ /*! @@ -222,21 +234,62 @@ class Wippersnapper_ESP8266 : public Wippersnapper { if (WiFi.status() == WL_CONNECTED) return; - // Attempt connection - _disconnect(); - delay(100); - // ESP8266 MUST be in STA mode to avoid device acting as client/server - WiFi.mode(WIFI_STA); - WiFi.begin(_ssid, _pass); - _status = WS_NET_DISCONNECTED; - delay(100); + if (strlen(_ssid) == 0) { + _status = WS_SSID_INVALID; + } else { + WiFi.setAutoReconnect(false); + // Attempt connection + _disconnect(); + delay(100); + // ESP8266 MUST be in STA mode to avoid device acting as client/server + WiFi.mode(WIFI_STA); + WiFi.begin(_ssid, _pass); + _status = WS_NET_DISCONNECTED; + delay(100); + + if (strlen(WS._multiNetworks[0].ssid) > 0) { + // multi network mode + for (int i = 0; i < WS_MAX_ALT_WIFI_NETWORKS; i++) { + if (strlen(WS._multiNetworks[i].ssid) > 0 && + (_wifiMulti.existsAP(WS._multiNetworks[i].ssid) == false)) { + // doesn't exist, add it + _wifiMulti.addAP(WS._multiNetworks[i].ssid, + WS._multiNetworks[i].pass); + } + } + // add default network + if (_wifiMulti.existsAP(_ssid) == false) { + _wifiMulti.addAP(_ssid, _pass); + } + long startRetry = millis(); + WS_DEBUG_PRINTLN("CONNECTING"); + while (_wifiMulti.run(5000) != WL_CONNECTED && + millis() - startRetry < 10000) { + // ESP8266 WDT requires yield() during a busy-loop so it doesn't bite + yield(); + } + if (WiFi.status() == WL_CONNECTED) { + _status = WS_NET_CONNECTED; + } else { + _status = WS_NET_DISCONNECTED; + } + } else { + // single network mode - // wait for a connection to be established - long startRetry = millis(); - WS_DEBUG_PRINTLN("CONNECTING"); - while (WiFi.status() != WL_CONNECTED && millis() - startRetry < 10000) { - // ESP8266 WDT requires yield() during a busy-loop so it doesn't bite - yield(); + // wait for a connection to be established + long startRetry = millis(); + WS_DEBUG_PRINTLN("CONNECTING"); + while (WiFi.status() != WL_CONNECTED && millis() - startRetry < 10000) { + // ESP8266 WDT requires yield() during a busy-loop so it doesn't bite + yield(); + } + if (WiFi.status() == WL_CONNECTED) { + _status = WS_NET_CONNECTED; + } else { + _status = WS_NET_DISCONNECTED; + } + } + WS.feedWDT(); } } diff --git a/src/network_interfaces/ws_networking_pico.h b/src/network_interfaces/ws_networking_pico.h index c2a8d556..55578fb7 100644 --- a/src/network_interfaces/ws_networking_pico.h +++ b/src/network_interfaces/ws_networking_pico.h @@ -108,8 +108,17 @@ class ws_networking_pico : public Wippersnapper { // Was the network within secrets.json found? for (int i = 0; i < n; ++i) { - if (strcmp(_ssid, WiFi.SSID(i)) == 0) + if (strcmp(_ssid, WiFi.SSID(i)) == 0) { return true; + } + if (WS._isWiFiMulti) { + // multi network mode + for (int j = 0; j < WS_MAX_ALT_WIFI_NETWORKS; j++) { + if (strcmp(WS._multiNetworks[j].ssid, WiFi.SSID(i)) == 0) { + return true; + } + } + } } // User-set network not found, print scan results to serial console @@ -191,6 +200,7 @@ class ws_networking_pico : public Wippersnapper { const char *_ssid; ///< WiFi SSID const char *_pass; ///< WiFi password WiFiClientSecure *_mqtt_client; ///< Pointer to a secure MQTT client object + WiFiMulti _wifiMulti; ///< WiFiMulti object for multi-network mode const char *_aio_root_ca_staging = "-----BEGIN CERTIFICATE-----\n" @@ -259,27 +269,47 @@ class ws_networking_pico : public Wippersnapper { if (WiFi.status() == WL_CONNECTED) return; + WiFi.mode(WIFI_STA); + WS.feedWDT(); + WiFi.setTimeout(20000); + WS.feedWDT(); + if (strlen(_ssid) == 0) { _status = WS_SSID_INVALID; } else { _disconnect(); delay(5000); WS.feedWDT(); - WiFi.mode(WIFI_STA); - WS.feedWDT(); - WiFi.setTimeout(20000); - WS.feedWDT(); - WiFi.begin(_ssid, _pass); - // Wait setTimeout duration for a connection and check if connected every - // 5 seconds - for (int i = 0; i < 4; i++) { - WS.feedWDT(); - delay(5000); + if (WS._isWiFiMulti) { + // multi network mode + _wifiMulti.clearAPList(); + // add default network + _wifiMulti.addAP(_ssid, _pass); + // add array of alternative networks + for (int i = 0; i < WS_MAX_ALT_WIFI_NETWORKS; i++) { + _wifiMulti.addAP(WS._multiNetworks[i].ssid, + WS._multiNetworks[i].pass); + } WS.feedWDT(); - if (WiFi.status() == WL_CONNECTED) { + if (_wifiMulti.run(10000) == WL_CONNECTED) { + WS.feedWDT(); _status = WS_NET_CONNECTED; return; } + WS.feedWDT(); + } else { + WiFi.begin(_ssid, _pass); + // Wait setTimeout duration for a connection and check if connected + // every 5 seconds + for (int i = 0; i < 4; i++) { + WS.feedWDT(); + delay(5000); + WS.feedWDT(); + if (WiFi.status() == WL_CONNECTED) { + _status = WS_NET_CONNECTED; + return; + } + } } _status = WS_NET_DISCONNECTED; } diff --git a/src/provisioning/ConfigJson.h b/src/provisioning/ConfigJson.h index b420364f..990aba9d 100644 --- a/src/provisioning/ConfigJson.h +++ b/src/provisioning/ConfigJson.h @@ -19,6 +19,9 @@ #include "Config.h" #include +// Converters for network configuration +void convertToJson(const networkConfig &src, JsonVariant dst); +void convertFromJson(JsonVariantConst src, networkConfig &dst); // Converters for secrets configuration void convertToJson(const secretsConfig &src, JsonVariant dst); void convertFromJson(JsonVariantConst src, secretsConfig &dst); diff --git a/src/provisioning/littlefs/WipperSnapper_LittleFS.cpp b/src/provisioning/littlefs/WipperSnapper_LittleFS.cpp index 662f63d6..57a534a8 100644 --- a/src/provisioning/littlefs/WipperSnapper_LittleFS.cpp +++ b/src/provisioning/littlefs/WipperSnapper_LittleFS.cpp @@ -67,6 +67,49 @@ void WipperSnapper_LittleFS::parseSecrets() { fsHalt(String("ERROR: deserializeJson() failed with code ") + error.c_str()); } + if (doc.containsKey("network_type_wifi")) { + // set default network config + convertFromJson(doc["network_type_wifi"], WS._config.network); + + if (!doc["network_type_wifi"].containsKey("alternative_networks")) { + // do nothing extra, we already have the only network + WS_DEBUG_PRINTLN("Found single wifi network in secrets.json"); + + } else if (doc["network_type_wifi"]["alternative_networks"] + .is()) { + + WS_DEBUG_PRINTLN("Found multiple wifi networks in secrets.json"); + // Parse network credentials from array in secrets + JsonArray altnetworks = doc["network_type_wifi"]["alternative_networks"]; + int8_t altNetworkCount = (int8_t)altnetworks.size(); + WS_DEBUG_PRINT("Network count: "); + WS_DEBUG_PRINTLN(altNetworkCount); + if (altNetworkCount == 0) { + fsHalt("ERROR: No alternative network entries found under " + "network_type_wifi.alternative_networks in secrets.json!"); + } + // check if over 3, warn user and take first three + for (int i = 0; i < altNetworkCount; i++) { + if (i >= 3) { + WS_DEBUG_PRINT("WARNING: More than 3 networks in secrets.json, " + "only the first 3 will be used. Not using "); + WS_DEBUG_PRINTLN(altnetworks[i]["network_ssid"].as()); + break; + } + convertFromJson(altnetworks[i], WS._multiNetworks[i]); + WS_DEBUG_PRINT("Added SSID: "); + WS_DEBUG_PRINTLN(WS._multiNetworks[i].ssid); + WS_DEBUG_PRINT("PASS: "); + WS_DEBUG_PRINTLN(WS._multiNetworks[i].pass); + } + WS._isWiFiMulti = true; + } else { + fsHalt("ERROR: Unrecognised value type for " + "network_type_wifi.alternative_networks in secrets.json!"); + } + } else { + fsHalt("ERROR: Could not find network_type_wifi in secrets.json!"); + } // Extract a config struct from the JSON document WS._config = doc.as(); diff --git a/src/provisioning/tinyusb/Wippersnapper_FS.cpp b/src/provisioning/tinyusb/Wippersnapper_FS.cpp index 1aefeea1..5d4db75f 100644 --- a/src/provisioning/tinyusb/Wippersnapper_FS.cpp +++ b/src/provisioning/tinyusb/Wippersnapper_FS.cpp @@ -13,7 +13,8 @@ * */ #if defined(ARDUINO_MAGTAG29_ESP32S2) || defined(ARDUINO_METRO_ESP32S2) || \ - defined(ARDUINO_FUNHOUSE_ESP32S2) || defined(ADAFRUIT_PYPORTAL_M4_TITANO) || \ + defined(ARDUINO_FUNHOUSE_ESP32S2) || \ + defined(ADAFRUIT_PYPORTAL_M4_TITANO) || \ defined(ADAFRUIT_METRO_M4_AIRLIFT_LITE) || defined(ADAFRUIT_PYPORTAL) || \ defined(ARDUINO_ADAFRUIT_FEATHER_ESP32S2) || \ defined(ARDUINO_ADAFRUIT_QTPY_ESP32S2) || \ @@ -295,7 +296,7 @@ bool Wippersnapper_FS::createBootFile() { void Wippersnapper_FS::createSecretsFile() { // Open file for writing File32 secretsFile = wipperFatFs.open("/secrets.json", FILE_WRITE); - + // Create a default secretsConfig structure secretsConfig secretsConfig; strcpy(secretsConfig.aio_user, "YOUR_IO_USERNAME_HERE"); @@ -317,7 +318,8 @@ void Wippersnapper_FS::createSecretsFile() { delay(2500); // Signal to user that action must be taken (edit secrets.json) - writeToBootOut("ERROR: Please edit the secrets.json file. Then, reset your board.\n"); + writeToBootOut( + "ERROR: Please edit the secrets.json file. Then, reset your board.\n"); #ifdef USE_DISPLAY WS._ui_helper->show_scr_error( "INVALID SETTINGS FILE", @@ -344,33 +346,91 @@ void Wippersnapper_FS::parseSecrets() { JsonDocument doc; DeserializationError error = deserializeJson(doc, secretsFile); if (error) { - fsHalt(String("ERROR: Unable to parse secrets.json file - deserializeJson() failed with code") + error.c_str()); + fsHalt(String("ERROR: Unable to parse secrets.json file - " + "deserializeJson() failed with code") + + error.c_str()); + } + + if (doc.containsKey("network_type_wifi")) { + // set default network config + convertFromJson(doc["network_type_wifi"], WS._config.network); + + if (!doc["network_type_wifi"].containsKey("alternative_networks")) { + // do nothing extra, we already have the only network + WS_DEBUG_PRINTLN("Found single wifi network in secrets.json"); + + } else if (doc["network_type_wifi"]["alternative_networks"] + .is()) { + + WS_DEBUG_PRINTLN("Found multiple wifi networks in secrets.json"); + // Parse network credentials from array in secrets + JsonArray altnetworks = doc["network_type_wifi"]["alternative_networks"]; + int8_t altNetworkCount = (int8_t)altnetworks.size(); + WS_DEBUG_PRINT("Network count: "); + WS_DEBUG_PRINTLN(altNetworkCount); + if (altNetworkCount == 0) { + fsHalt("ERROR: No alternative network entries found under " + "network_type_wifi.alternative_networks in secrets.json!"); + } + // check if over 3, warn user and take first three + for (int i = 0; i < altNetworkCount; i++) { + if (i >= 3) { + WS_DEBUG_PRINT("WARNING: More than 3 networks in secrets.json, " + "only the first 3 will be used. Not using "); + WS_DEBUG_PRINTLN(altnetworks[i]["network_ssid"].as()); + break; + } + convertFromJson(altnetworks[i], WS._multiNetworks[i]); + WS_DEBUG_PRINT("Added SSID: "); + WS_DEBUG_PRINTLN(WS._multiNetworks[i].ssid); + WS_DEBUG_PRINT("PASS: "); + WS_DEBUG_PRINTLN(WS._multiNetworks[i].pass); + } + WS._isWiFiMulti = true; + } else { + fsHalt("ERROR: Unrecognised value type for " + "network_type_wifi.alternative_networks in secrets.json!"); + } + } else { + fsHalt("ERROR: Could not find network_type_wifi in secrets.json!"); } // Extract a config struct from the JSON document - WS._config = doc.as(); + WS._config = doc.as(); // Validate the config struct is not filled with default values - if (strcmp(WS._config.aio_user, "YOUR_IO_USERNAME_HERE") == 0 || strcmp(WS._config.aio_key, "YOUR_IO_KEY_HERE") == 0) { - writeToBootOut("ERROR: Invalid IO credentials in secrets.json! TO FIX: Please change io_username and io_key to match your Adafruit IO credentials!\n"); + if (strcmp(WS._config.aio_user, "YOUR_IO_USERNAME_HERE") == 0 || + strcmp(WS._config.aio_key, "YOUR_IO_KEY_HERE") == 0) { + writeToBootOut( + "ERROR: Invalid IO credentials in secrets.json! TO FIX: Please change " + "io_username and io_key to match your Adafruit IO credentials!\n"); #ifdef USE_DISPLAY WS._ui_helper->show_scr_error( "INVALID IO CREDS", - "The \"io_username/io_key\" fields within secrets.json are invalid, please " + "The \"io_username/io_key\" fields within secrets.json are invalid, " + "please " "change it to match your Adafruit IO credentials. Then, press RESET."); #endif - fsHalt("ERROR: Invalid IO credentials in secrets.json! TO FIX: Please change io_username and io_key to match your Adafruit IO credentials!"); + fsHalt( + "ERROR: Invalid IO credentials in secrets.json! TO FIX: Please change " + "io_username and io_key to match your Adafruit IO credentials!"); } - if (strcmp(WS._config.network.ssid, "YOUR_WIFI_SSID_HERE") == 0 || strcmp(WS._config.network.pass, "YOUR_WIFI_PASS_HERE") == 0) { - writeToBootOut("ERROR: Invalid network credentials in secrets.json! TO FIX: Please change network_ssid and network_password to match your Adafruit IO credentials!\n"); + if (strcmp(WS._config.network.ssid, "YOUR_WIFI_SSID_HERE") == 0 || + strcmp(WS._config.network.pass, "YOUR_WIFI_PASS_HERE") == 0) { + writeToBootOut("ERROR: Invalid network credentials in secrets.json! TO " + "FIX: Please change network_ssid and network_password to " + "match your Adafruit IO credentials!\n"); #ifdef USE_DISPLAY WS._ui_helper->show_scr_error( "INVALID NETWORK", - "The \"network_ssid and network_password\" fields within secrets.json are invalid, please " - "change it to match your WiFi credentials. Then, press RESET."); + "The \"network_ssid and network_password\" fields within secrets.json " + "are invalid, please change it to match your WiFi credentials. Then, " + "press RESET."); #endif - fsHalt("ERROR: Invalid network credentials in secrets.json! TO FIX: Please change network_ssid and network_password to match your Adafruit IO credentials!"); + fsHalt("ERROR: Invalid network credentials in secrets.json! TO FIX: Please " + "change network_ssid and network_password to match your Adafruit IO " + "credentials!"); } // Close secrets.json file @@ -437,7 +497,8 @@ void Wippersnapper_FS::createDisplayConfig() { // Create and fill JSON document from displayConfig JsonDocument doc; if (!doc.set(displayConfig)) { - fsHalt("ERROR: Unable to set displayConfig, no space in arduinoJSON document!"); + fsHalt("ERROR: Unable to set displayConfig, no space in arduinoJSON " + "document!"); } // Write the file out to the filesystem serializeJsonPretty(doc, displayFile); @@ -451,7 +512,8 @@ void Wippersnapper_FS::parseDisplayConfig(displayConfig &dispCfg) { if (!wipperFatFs.exists("/display_config.json")) { WS_DEBUG_PRINTLN("Could not find display_config.json, generating..."); #ifdef ARDUINO_FUNHOUSE_ESP32S2 - createDisplayConfig(); // generate a default display_config.json for FunHouse + createDisplayConfig(); // generate a default display_config.json for + // FunHouse #endif } @@ -465,7 +527,9 @@ void Wippersnapper_FS::parseDisplayConfig(displayConfig &dispCfg) { JsonDocument doc; DeserializationError error = deserializeJson(doc, file); if (error) { - fsHalt(String("FATAL ERROR: Unable to parse display_config.json - deserializeJson() failed with code") + error.c_str()); + fsHalt(String("FATAL ERROR: Unable to parse display_config.json - " + "deserializeJson() failed with code") + + error.c_str()); } // Close the file, we're done with it file.close();