diff --git a/.github/workflows/compile-examples.yml b/.github/workflows/compile-examples.yml index 58c9272..704d2bd 100644 --- a/.github/workflows/compile-examples.yml +++ b/.github/workflows/compile-examples.yml @@ -124,6 +124,7 @@ jobs: sketches: >- examples/ESP32/ESP32 examples/ESP32/ESP32OTA + tests/nvs_config/sketch - name: ESP32 fqbn: esp32:esp32:esp32 core: esp32:esp32@3.3.12 @@ -133,6 +134,7 @@ jobs: examples/ESP32/ESP32OTA examples/ESP32/ESP32RTOS examples/ESP32/ESP32WebConfig + tests/nvs_config/sketch examples/Thinger/NBCore - name: ESP32-S3 fqbn: esp32:esp32:esp32s3 diff --git a/.github/workflows/test-nvs-config.yml b/.github/workflows/test-nvs-config.yml new file mode 100644 index 0000000..3b21b88 --- /dev/null +++ b/.github/workflows/test-nvs-config.yml @@ -0,0 +1,26 @@ +name: test-nvs-config +on: [push, pull_request, workflow_dispatch] +permissions: + contents: read +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + core: [esp32_core, esp32_core_v2] + flags: + - '' + - '-DTHINGER_ESP32_CONFIG_USE_LITTLEFS' + - '-DTHINGER_ESP32_CONFIG_SKIP_LEGACY_MIGRATION' + steps: + - uses: actions/checkout@v5 + - name: Compile real PSON, ThingerWifi, WebConfig and pinned WiFiMulti + run: >- + g++ -std=c++11 -Wall -Wextra -Wno-unused-parameter + ${{ matrix.flags }} + -DCONFIG_ESP_WIFI_ENTERPRISE_SUPPORT=${{ matrix.core == 'esp32_core' && '1' || '0' }} + -Itests/nvs_config/stubs -Itests/nvs_config/${{ matrix.core }} -Isrc + tests/nvs_config/test.cpp tests/nvs_config/${{ matrix.core }}/WiFiMulti.cpp + -o /tmp/test-nvs-config + - name: Run migration and storage failure tests + run: /tmp/test-nvs-config diff --git a/README.md b/README.md index 963fa13..db0ab4b 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,146 @@ Each board family uses its own header. Every board in this table is compiled on **Legacy** (discontinued hardware, kept for compatibility): Adafruit CC3000 (`ThingerCC3000.h`) and the Arduino WiFi shield (`ThingerWifi.h`), still compiled on CI; Texas Instruments CC3200 with Energia and LinkIt ONE (`ThingerLinkItOneWifi.h`, `ThingerLinkItOneGPRS.h`), whose cores are no longer available. +## ESP32 WebConfig storage and migration + +`ThingerESP32WebConfig` stores the entire configuration object in the ESP32's +default NVS partition, namespace `thinger`, key `config`. The versioned blob +contains the original PSON payload, including custom and unknown fields. Later +portal saves update the loaded object rather than dropping unregistered fields. +WiFiManager's native Wi-Fi credentials remain managed by the ESP32 Wi-Fi core +in NVS; they are not part of the upstream `/config.pson` schema. Any Wi-Fi fields +added to that file by an application are preserved with all other fields; the +application's configuration callback continues to interpret them. + +When the NVS record is absent, the first load imports `/config.pson` from SPIFFS. +For ESP32 firmware that previously used LittleFS, define +`THINGER_ESP32_CONFIG_USE_LITTLEFS` before including `ThingerESP32WebConfig.h`. +Choose the filesystem actually used by the deployed firmware. The importer +never formats flash. It validates the complete PSON object, commits it to NVS, +checks the saved bytes and reloads from NVS before removing the legacy file. +Once a record exists, loading never consults the filesystem again, including +when legacy cleanup fails. A reset during import leaves either the original +file for retry or an authoritative NVS record. + +NVS failures, invalid/unsupported records and an unreadable legacy volume +stop configuration loading rather than overwriting data through the portal. +The file remains untouched if import fails. The default maximum PSON payload is +16 KiB; override `THINGER_NVS_CONFIG_MAX_SIZE` before including the header for +larger deployments, subject to available RAM and NVS capacity. + +For **new devices without a legacy filesystem**, define +`THINGER_ESP32_CONFIG_SKIP_LEGACY_MIGRATION` before including the header. This +allows first-time portal setup without mounting a filesystem; do not enable it +on a deployed fleet that needs migration. Preserve the old filesystem partition +and NVS partition during the firmware update until migration has completed. + +`clean_credentials()` commits an empty-state NVS record instead of removing +the key. This prevents a leftover legacy file from restoring cleared settings. +It preserves the existing method's scope: it does not erase WiFiManager's +separate native Wi-Fi configuration. Explicit cleanup is also available to an +application that intentionally discards an unreadable configuration. + +On **all ESP32 Wi-Fi clients**, including `ThingerESP32` and WebConfig, +`add_wifi(ssid, password)` now commits the complete personal-network list to the +separate NVS key `thinger/wifi`. Calls for the same SSID update its password; +identical calls do not rewrite flash. Null and empty passwords both represent +an open network. The library owns copies of the credential strings. + +Initialization is lazy, so global constructors do not access NVS. `add_wifi()` +loads and merges the existing list before updating it. Connection attempts load +the list and rebuild `WiFiMulti` even if the sketch never calls `add_wifi()` +after reboot. WebConfig checks this restored list before opening its portal, +and successful portal submissions are added to the same persistent list. +An existing ESP32 native Wi-Fi configuration remains the fallback only while +the `thinger/wifi` record has never been created. An explicitly empty saved list +does not enable that fallback. + +The `add_wifi()` signature remains `void`. Check `wifi_storage_ok()` immediately +after a call to detect storage errors or rejected credentials. The accepted +limits match WiFiMulti in the tested cores: 1–31 bytes for SSID, 0–63 bytes for +password, at most 255 list entries, and the configured NVS payload limit. +`restore_wifi_networks()` is also available to initialize or retry loading +explicitly. A read error or invalid Wi-Fi record blocks connecting with that +record; it does not replace the saved list with an empty one. A failed update +does not register the proposed credentials in RAM. After a commit whose +verification failed, the next load may recover the complete new record. + +The ESP32 personal-network registry also provides: + +```cpp +std::vector ssids; +if (thing.list_wifi(ssids)) { + for (const auto& ssid : ssids) Serial.println(ssid.c_str()); +} + +if (!thing.remove_wifi("network-b") && !thing.wifi_storage_ok()) { + // Handle a storage/restore failure. An absent SSID also returns false, + // but leaves wifi_storage_ok() true after a successful load. +} +``` + +`list_wifi(ssids)` loads the saved list lazily and returns an independent +snapshot of SSIDs, in registration order. It never exposes passwords or writes +flash. A valid empty list returns true; on failure the output stays unchanged. +`remove_wifi(ssid)` matches the SSID exactly, including case. It commits the +remaining complete list and rebuilds WiFiMulti before reporting success. If the +removed SSID is the driver's current network, it requests a disconnect and +clearing that driver's saved AP. Removing an absent/invalid SSID does not write +flash. A failed update may have committed before verification failed, as with +`add_wifi()`; retry loading before deciding which list survived. + +Removing the last personal network leaves a valid empty NVS record. A reboot +cannot resurrect an old AP through the library's native-driver fallback. +WebConfig opens the portal when another network is required; other clients +report `NETWORK_NOT_CONFIGURED`. Listing/deletion only applies to networks +registered by `add_wifi()` or the portal, preserving RAM enterprise networks +and the separate PSON configuration. Future explicit `add_wifi()` calls for a +removed SSID register it again, including unconditional calls in `setup()`. + +`bool clear_wifi()` removes all personal networks registered by `add_wifi()` or +the portal, commits a valid empty list to NVS and rebuilds WiFiMulti. For example: + +```cpp +if (!thing.clear_wifi()) { + // Handle the storage failure; the clear could not be verified. +} +``` + +The empty record remains authoritative across resets, blocking native-driver +fallback even if old driver credentials survived separately. A current +non-enterprise connection is asked to disconnect and clear its saved AP. +PSON configuration, Thinger credentials, custom parameters, unrelated NVS keys, +and RAM-only enterprise credentials are preserved. An active enterprise +connection is retained. Explicit clearing can replace a malformed Wi-Fi record +without loading it first; it never formats or erases the NVS partition. +The return value is true only after commit, readback verification and rebuilding +the runtime list. As with other updates, a failed readback can follow a successful +commit; reload to determine the surviving state. New explicit `add_wifi()` calls +or a portal submission can register networks again after clearing. + +`add_wifi_enterprise()` retains its existing RAM-only behavior. Adding/updating +a personal network preserves enterprise entries already registered in RAM. +`clean_credentials()` clears only the PSON configuration, preserving the Wi-Fi +list, just as it previously preserved the native Wi-Fi credentials. Stopping +calls to `add_wifi()` does not delete networks that were already persisted. +Opaque application-defined Wi-Fi fields in the migrated PSON are preserved; +their existing application callback still defines how to interpret them. + +ESP8266 continues to use its existing LittleFS backend and RAM WiFiMulti list; +ESP32 NVS is not an ESP8266 storage API. Host tests in `tests/nvs_config` run in +CI against the real ThingerWifi, ESP32 WebConfig and PSON code and unmodified +WiFiMulti sources from ESP32 cores 2.0.17 and 3.3.12. Only radio, transport, NVS +and filesystem operations are simulated. The matrix covers SPIFFS, LittleFS +and new devices without a filesystem. Tests cover reboot without `add_wifi()`, +strongest-network selection, fallback, open/hidden networks, password updates, +duplicate prevention, portal persistence, failed writes/commits/reads, and full +PSON migration alongside multiple networks. They also verify independent SSID +snapshots, persistent deletion, active/last-network removal, blocking stale +native-driver fallback, deletion failure recovery, clearing all networks, +preservation of other NVS data, and clearing malformed records. Core fixture sources retain their +LGPL licenses. `tests/nvs_config/sketch` checks the public persistence API in the +ESP32 and Nano ESP32 board compilation matrix, alongside the existing examples. + ## OTA updates The boards with an OTA header can be updated remotely over the Internet. The [Thinger.io extension for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=thinger-io.thinger-io) builds the firmware with PlatformIO, sends it to the device (compressed when the board supports it) and reboots it. diff --git a/src/ThingerESP32NVS.h b/src/ThingerESP32NVS.h new file mode 100644 index 0000000..948f7ec --- /dev/null +++ b/src/ThingerESP32NVS.h @@ -0,0 +1,161 @@ +// The MIT License (MIT) +// +// Copyright (c) 2017 THINK BIG LABS SL +// Author: alvarolb@gmail.com (Alvaro Luis Bustamante) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#ifndef THINGER_ESP32_NVS_H +#define THINGER_ESP32_NVS_H + +#include +#include +#include +#include "thinger/pson.h" + +// Maximum PSON payload, excluding the record header. Override before including. +#ifndef THINGER_NVS_CONFIG_MAX_SIZE +#define THINGER_NVS_CONFIG_MAX_SIZE 16384 +#endif + +namespace thinger_config { + +class buffer_decoder : public protoson::pson_decoder { +public: + buffer_decoder(const uint8_t* data, size_t size) : data_(data), size_(size) {} +protected: + bool read(void* buffer, size_t size) override { + if(size > size_ - bytes_read()) return false; + memcpy(buffer, data_ + bytes_read(), size); + return protoson::pson_decoder::read(buffer, size); + } +private: + const uint8_t* data_; + size_t size_; +}; + +class buffer_encoder : public protoson::pson_encoder { +public: + buffer_encoder(uint8_t* data, size_t size) : data_(data), size_(size) {} + bool valid() const { return valid_; } +protected: + bool write(const void* buffer, size_t size) override { + if(size > size_ - bytes_written()) { + valid_ = false; + return false; + } + memcpy(data_ + bytes_written(), buffer, size); + return protoson::pson_encoder::write(buffer, size); + } +private: + uint8_t* data_; + size_t size_; + bool valid_ = true; +}; + +// One NVS blob is the commit unit: header and the complete PSON object are +// replaced together. A cleared record prevents reimporting stale legacy files. +class nvs_config { +public: + enum class result { missing, ready, cleared, error }; + + static bool decode(const uint8_t* bytes, size_t size, protoson::pson& config) { + if(!bytes || !size || size > THINGER_NVS_CONFIG_MAX_SIZE) return false; + protoson::pson decoded; + buffer_decoder decoder(bytes, size); + if(!decoder.decode(decoded) || decoder.bytes_read() != size || !decoded.is_object()) return false; + protoson::pson::swap(decoded, config); + return true; + } + + static result load(protoson::pson& config, const char* key = "config") { + handle storage; + esp_err_t err = nvs_open("thinger", NVS_READONLY, &storage.value); + if(err == ESP_ERR_NVS_NOT_FOUND) return result::missing; + if(err != ESP_OK) return result::error; + storage.open = true; + size_t size = 0; + err = nvs_get_blob(storage.value, key, nullptr, &size); + if(err == ESP_ERR_NVS_NOT_FOUND) return result::missing; + if(err != ESP_OK || size < header_size || size - header_size > THINGER_NVS_CONFIG_MAX_SIZE) return result::error; + std::unique_ptr record(new(std::nothrow) uint8_t[size]); + if(!record || nvs_get_blob(storage.value, key, record.get(), &size) != ESP_OK) return result::error; + if(memcmp(record.get(), "TCFG", 4) || record[4] != 1 || record[6] || record[7]) return result::error; + if(record[5] == 0) return size == header_size ? result::cleared : result::error; + if(record[5] != 1 || !decode(record.get() + header_size, size - header_size, config)) return result::error; + return result::ready; + } + + static bool save(protoson::pson& config, const char* key = "config") { + if(!config.is_object()) return false; + protoson::pson_encoder counter; + counter.encode(config); + const size_t size = counter.bytes_written(); + if(!size || size > THINGER_NVS_CONFIG_MAX_SIZE) return false; + std::unique_ptr bytes(new(std::nothrow) uint8_t[size]); + if(!bytes) return false; + buffer_encoder encoder(bytes.get(), size); + encoder.encode(config); + return encoder.valid() && encoder.bytes_written() == size && save_legacy(bytes.get(), size, key); + } + + // Keep the original PSON bytes, including fields unknown to this firmware. + static bool save_legacy(const uint8_t* bytes, size_t size, const char* key = "config") { + protoson::pson config; + return decode(bytes, size, config) && write_record(bytes, size, 1, key); + } + + static bool clear(const char* key = "config") { return write_record(nullptr, 0, 0, key); } + +private: + enum { header_size = 8 }; + struct handle { + nvs_handle_t value = 0; + bool open = false; + ~handle() { if(open) nvs_close(value); } + }; + + static bool write_record(const uint8_t* bytes, size_t size, uint8_t state, const char* key) { + if(size > THINGER_NVS_CONFIG_MAX_SIZE) return false; + const size_t total = header_size + size; + std::unique_ptr record(new(std::nothrow) uint8_t[total]); + std::unique_ptr check(new(std::nothrow) uint8_t[total]); + if(!record || !check) return false; + const uint8_t header[header_size] = {'T', 'C', 'F', 'G', 1, state, 0, 0}; + memcpy(record.get(), header, header_size); + if(size) memcpy(record.get() + header_size, bytes, size); + { + handle storage; + if(nvs_open("thinger", NVS_READWRITE, &storage.value) != ESP_OK) return false; + storage.open = true; + if(nvs_set_blob(storage.value, key, record.get(), total) != ESP_OK || + nvs_commit(storage.value) != ESP_OK) return false; + } + handle storage; + if(nvs_open("thinger", NVS_READONLY, &storage.value) != ESP_OK) return false; + storage.open = true; + size_t read = total; + return nvs_get_blob(storage.value, key, check.get(), &read) == ESP_OK && + read == total && memcmp(record.get(), check.get(), total) == 0; + } +}; + +} // namespace thinger_config + +#endif diff --git a/src/ThingerESP32WebConfig.h b/src/ThingerESP32WebConfig.h index 03b66d1..5c9f968 100644 --- a/src/ThingerESP32WebConfig.h +++ b/src/ThingerESP32WebConfig.h @@ -23,126 +23,116 @@ #ifndef THINGER_ESP32_WEBCONFIG_H #define THINGER_ESP32_WEBCONFIG_H -#define FORMAT_SPIFFS_IF_FAILED true #include +// Select the filesystem used by the deployed firmware, only for migration. +// SPIFFS is the upstream ESP32 default. Never format during migration. +#ifdef THINGER_ESP32_CONFIG_USE_LITTLEFS +#include +#else #include +#endif #include "ThingerESP32.h" #include "ThingerWebConfig.h" +#include "ThingerESP32NVS.h" - -class pson_spiffs_decoder : public protoson::pson_decoder{ +class ThingerESP32WebConfig : public ThingerWebConfig { public: - pson_spiffs_decoder(File& file) : file_(file) - { + ThingerESP32WebConfig(const char* user="", const char* device="", const char* credential="") : + ThingerWebConfig(user, device, credential) {} + + bool clean_credentials() override { + // A committed empty-state record also blocks stale file reimports. + // Preserve the existing API's scope: WiFiManager's credentials are separate. + storage_error_ = !thinger_config::nvs_config::clear(); + return !storage_error_; } protected: - virtual bool read(void* buffer, size_t size){ - size_t read = file_.readBytes((char*)buffer, size); - protoson::pson_decoder::read(buffer, read); - return read == size; - } + bool configuration_storage_error() const override { return storage_error_ || !this->wifi_storage_ok(); } -private: - File& file_; -}; + bool prepare_wifi_configuration() override { return this->restore_wifi_networks(); } -class pson_spiffs_encoder : public protoson::pson_encoder{ -public: - pson_spiffs_encoder(File& file) : file_(file) - { + bool has_wifi_configuration(WiFiManager& manager) override { + return this->has_wifi_networks() || (!this->wifi_registry_exists_ && manager.getWiFiIsSaved()); } -protected: - virtual bool write(const void* buffer, size_t size){ - size_t wrote = file_.write((const uint8_t*)buffer, size); - protoson::pson_encoder::write(buffer, wrote); - return wrote == size; + bool save_portal_wifi_configuration(WiFiManager& manager) override { + const String ssid = manager.getWiFiSSID(true); + const String password = manager.getWiFiPass(true); + this->add_wifi(ssid.c_str(), password.c_str()); + return this->wifi_storage_ok(); } -private: - File& file_; -}; - -class ThingerESP32WebConfig : public ThingerWebConfig{ - - public: - ThingerESP32WebConfig(const char* user="", const char* device="", const char* credential="") : - ThingerWebConfig(user, device, credential) { - + bool save_configuration(pson& config) override { + storage_error_ = !thinger_config::nvs_config::save(config); + if(storage_error_) THINGER_DEBUG("_CONFIG", "Cannot save NVS configuration!"); + return !storage_error_; } - bool clean_credentials() override{ - THINGER_DEBUG("_CONFIG", "Cleaning credentials..."); - // clean Thinger.io credentials from file system - if(SPIFFS.begin(FORMAT_SPIFFS_IF_FAILED)) { - THINGER_DEBUG("_CONFIG", "FS Mounted!"); - if (SPIFFS.exists(CONFIG_FILE)) { - THINGER_DEBUG("_CONFIG", "Removing Config File..."); - if(SPIFFS.remove(CONFIG_FILE)){ - THINGER_DEBUG("_CONFIG", "Config file removed!"); - return true; - }else{ - THINGER_DEBUG("_CONFIG", "Cannot delete config file!"); - } - }else{ - THINGER_DEBUG("_CONFIG", "No config file to delete!"); - } - SPIFFS.end(); + bool load_configuration(pson& config) override { + using store = thinger_config::nvs_config; + storage_error_ = false; + const store::result result = store::load(config); + if(result == store::result::ready) return true; + if(result == store::result::cleared) return false; + if(result == store::result::error) { + storage_error_ = true; + THINGER_DEBUG("_CONFIG", "Cannot read NVS configuration; legacy import blocked!"); + return false; } + // Only a missing NVS record may trigger the one-time legacy import. +#ifdef THINGER_ESP32_CONFIG_SKIP_LEGACY_MIGRATION + // New production devices may omit the legacy filesystem entirely. return false; +#else + return migrate_legacy(config); +#endif } - - protected: - - bool save_configuration(pson& config) override{ - THINGER_DEBUG("_CONFIG", "Updating Device Info..."); - if (SPIFFS.begin(FORMAT_SPIFFS_IF_FAILED)) { - File configFile = SPIFFS.open(CONFIG_FILE, "w"); - if (configFile) { - pson_spiffs_encoder encoder(configFile); - encoder.encode(config); - configFile.close(); - THINGER_DEBUG("_CONFIG", "Done!"); - return true; - } else { - THINGER_DEBUG("_CONFIG", "Failed to open config file for writing!"); - } - SPIFFS.end(); +private: + bool storage_error_ = false; + + bool migrate_legacy(pson& config) { +#ifdef THINGER_ESP32_CONFIG_USE_LITTLEFS + auto& filesystem = LittleFS; +#else + auto& filesystem = SPIFFS; +#endif + if(!filesystem.begin(false)) { + // Distinguish an unreadable legacy volume from an absent file. + // Explicit clean_credentials() can initialize an NVS-only device. + storage_error_ = true; + THINGER_DEBUG("_CONFIG", "Cannot mount legacy FS; configuration left intact!"); + return false; } - return false; - } - - bool load_configuration(pson& config) override{ - THINGER_DEBUG("_CONFIG", "Mounting FS..."); - if (SPIFFS.begin(FORMAT_SPIFFS_IF_FAILED)) { - THINGER_DEBUG("_CONFIG", "FS Mounted!"); - if (SPIFFS.exists(CONFIG_FILE)) { - //file exists, reading and loading - THINGER_DEBUG("_CONFIG", "Opening Config File..."); - File configFile = SPIFFS.open(CONFIG_FILE, "r"); - if(configFile){ - THINGER_DEBUG("_CONFIG", "Config File is Open!"); - pson_spiffs_decoder decoder(configFile); - bool result = decoder.decode(config); - configFile.close(); - return result; - }else{ - THINGER_DEBUG("_CONFIG", "Config File is Not Available!"); + if(!filesystem.exists(CONFIG_FILE)) { + filesystem.end(); + return false; + } + File file = filesystem.open(CONFIG_FILE, "r"); + bool migrated = false; + if(file) { + const size_t size = file.size(); + if(size && size <= THINGER_NVS_CONFIG_MAX_SIZE) { + std::unique_ptr bytes(new(std::nothrow) uint8_t[size]); + if(bytes && file.readBytes(reinterpret_cast(bytes.get()), size) == size) { + // save_legacy validates PSON and verifies the committed bytes. + // Reload from NVS before using the migrated configuration. + migrated = thinger_config::nvs_config::save_legacy(bytes.get(), size) && + thinger_config::nvs_config::load(config) == thinger_config::nvs_config::result::ready; } } - // close SPIFFS - SPIFFS.end(); - } else { - THINGER_DEBUG("_CONFIG", "Failed to Mount FS!"); + file.close(); } - return false; + if(migrated && !filesystem.remove(CONFIG_FILE)) { + // NVS is already authoritative, even if cleanup could not finish. + THINGER_DEBUG("_CONFIG", "NVS migration complete; legacy file cleanup failed!"); + } + filesystem.end(); + storage_error_ = !migrated; + return migrated; } - }; - - -#endif \ No newline at end of file +#endif diff --git a/src/ThingerESP32WifiStore.h b/src/ThingerESP32WifiStore.h new file mode 100644 index 0000000..789bba6 --- /dev/null +++ b/src/ThingerESP32WifiStore.h @@ -0,0 +1,90 @@ +// The MIT License (MIT) +// +// Copyright (c) 2017 THINK BIG LABS SL +// Author: alvarolb@gmail.com (Alvaro Luis Bustamante) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#ifndef THINGER_ESP32_WIFI_STORE_H +#define THINGER_ESP32_WIFI_STORE_H + +#include +#include +#include "ThingerESP32NVS.h" + +namespace thinger_config { + +struct wifi_network { + std::string ssid; + std::string password; +}; + +class wifi_store { +public: + using networks = std::vector; + + // Match the accepted credentials of the ESP32 WiFiMulti core (2.x/3.x). + static bool valid(const char* ssid, const char* password) { + return ssid && *ssid && strlen(ssid) <= 31 && (!password || strlen(password) <= 63); + } + + static bool load(networks& output, bool* record_exists = nullptr) { + protoson::pson data; + const auto result = nvs_config::load(data, "wifi"); + if(result == nvs_config::result::error) return false; + networks loaded; + if(result == nvs_config::result::ready) { + if(!data["version"].is_number() || (int)data["version"] != 1 || !data["networks"].is_array()) return false; + protoson::pson_array& entries = data["networks"]; + if(entries.size() > 255) return false; + for(auto it = entries.begin(); it.valid(); it.next()) { + auto& item = it.item(); + if(!item.is_object() || !item["ssid"].is_string() || + !(item["password"].is_string() || item["password"].get_type() == protoson::pson::empty_string)) return false; + const char* ssid = item["ssid"]; + const char* password = item["password"]; + if(!valid(ssid, password)) return false; + for(const auto& network : loaded) if(network.ssid == ssid) return false; + loaded.push_back({ssid, password}); + } + } + output.swap(loaded); + if(record_exists) *record_exists = result != nvs_config::result::missing; + return true; + } + + static bool save(const networks& input) { + protoson::pson data; + data["version"] = 1; + protoson::pson_array& entries = data["networks"]; + if(input.size() > 255) return false; + for(const auto& network : input) { + if(!valid(network.ssid.c_str(), network.password.c_str())) return false; + auto* item = entries.create_item(); + if(!item) return false; + (*item)["ssid"] = network.ssid.c_str(); + (*item)["password"] = network.password.c_str(); + } + return entries.size() == input.size() && nvs_config::save(data, "wifi"); + } +}; + +} // namespace thinger_config + +#endif diff --git a/src/ThingerWebConfig.h b/src/ThingerWebConfig.h index 10ed23b..b10e918 100644 --- a/src/ThingerWebConfig.h +++ b/src/ThingerWebConfig.h @@ -97,12 +97,20 @@ class ThingerWebConfig : public ThingerDevice { virtual bool load_configuration(pson& configuration) = 0; virtual bool save_configuration(pson& configuration) = 0; + virtual bool configuration_storage_error() const { return false; } + virtual bool prepare_wifi_configuration() { return true; } + virtual bool has_wifi_configuration(WiFiManager& manager) { return manager.getWiFiIsSaved(); } + virtual bool save_portal_wifi_configuration(WiFiManager&) { return true; } bool connect_network() override{ - // read current thinger.io credentials from file system + // Read the persisted configuration (NVS on ESP32). pson config; bool thinger_config = load_configuration(config); + if(!prepare_wifi_configuration() || configuration_storage_error()) { + this->set_error(ThingerClient::ERROR_UNKNOWN); + return false; + } if(thinger_config){ THINGER_DEBUG("_CONFIG", "Config File Decoded!"); @@ -125,7 +133,7 @@ class ThingerWebConfig : public ThingerDevice { #endif // if credentials and wifi already configured, try to connect to wifi - if(thinger_config && wifiManager.getWiFiIsSaved()){ + if(strlen(user_) && strlen(device_) && strlen(credential_) && has_wifi_configuration(wifiManager)){ THINGER_DEBUG("_CONFIG", "Connecting using previous configuration..."); return ThingerDevice::connect_network(); // don't have credentials or Wifi, initiate config portal @@ -149,13 +157,17 @@ class ThingerWebConfig : public ThingerDevice { if(captive_portal_callback_!=NULL) captive_portal_callback_(wifiManager); bool wifiConnected = wifiManager.startConfigPortal(THINGER_DEVICE_SSID, THINGER_DEVICE_SSID_PSWD); + if(wifiConnected && !save_portal_wifi_configuration(wifiManager)) { + this->set_error(ThingerClient::ERROR_UNKNOWN); + return false; + } // store new values (if it was empty) if(strlen(user_)==0) strcpy(user_, user_parameter.getValue()); if(strlen(device_)==0) strcpy(device_, device_parameter.getValue()); if(strlen(credential_)==0) strcpy(credential_, credential_parameter.getValue()); - pson config; + // Update the loaded object so unregistered custom fields survive. config["user"] = (const char *) user_; config["device"] = (const char *) device_; config["credential"] = (const char *) credential_; @@ -164,7 +176,10 @@ class ThingerWebConfig : public ThingerDevice { config[parameters_[i]->getID()] = parameters_[i]->getValue(); } - save_configuration(config); + if(!save_configuration(config)) { + this->set_error(ThingerClient::ERROR_UNKNOWN); + return false; + } if(config_callback_!=NULL) config_callback_(config); if(wifi_callback_!=NULL) wifi_callback_(wifiConnected); @@ -189,4 +204,4 @@ class ThingerWebConfig : public ThingerDevice { }; -#endif \ No newline at end of file +#endif diff --git a/src/ThingerWifi.h b/src/ThingerWifi.h index d5f8bce..677e196 100644 --- a/src/ThingerWifi.h +++ b/src/ThingerWifi.h @@ -25,6 +25,9 @@ #define THINGER_WIFI_H #include "ThingerClient.h" +#ifdef ESP32 +#include "ThingerESP32WifiStore.h" +#endif // ESP32, ESP8266 and the Arduino-Pico core can keep several networks and connect to the strongest one #if defined(ESP8266) @@ -50,10 +53,21 @@ class ThingerWifiClient : public ThingerClient { protected: virtual bool network_connected(){ +#ifdef ESP32 + if(!restore_wifi_networks()) return false; + if(wifi_registry_exists_ && !has_wifi_networks()) return false; +#endif return (WiFi.status() == WL_CONNECTED) && !(WiFi.localIP() == (IPAddress)INADDR_NONE); } virtual bool connect_network(){ +#ifdef ESP32 + if(!restore_wifi_networks()) return false; + if(wifi_registry_exists_ && !has_wifi_networks()) { + set_error(NETWORK_NOT_CONFIGURED); + return false; + } +#endif unsigned long wifi_timeout = millis(); #ifdef ESP32 @@ -174,6 +188,28 @@ class ThingerWifiClient : public ThingerClient { // ESP32, ESP8266 and Pico W accept several networks (call it once per network) void add_wifi(const char* ssid, const char* password=nullptr) { +#ifdef ESP32 + if(!thinger_config::wifi_store::valid(ssid, password) || !restore_wifi_networks()) { + wifi_storage_failure(); + return; + } + auto candidate = wifi_credentials_; + bool found = false; + for(auto& network : candidate) { + if(network.ssid != ssid) continue; + if(network.password == (password ? password : "")) return; + network.password = password ? password : ""; + found = true; + break; + } + if(!found) candidate.push_back({ssid, password ? password : ""}); + if(candidate.size() + runtime_enterprise_count() > 255 || + !thinger_config::wifi_store::save(candidate) || !install_wifi_networks(candidate)) { + wifi_storage_failure(); + } else { + wifi_registry_exists_ = true; + } +#else #ifdef THINGER_WIFI_MULTI if(wifi_multi_.addAP(ssid, password)) wifi_networks_++; // a single network connects directly, without scanning, so it can be a hidden network @@ -181,14 +217,103 @@ class ThingerWifiClient : public ThingerClient { #endif wifi_ssid_ = ssid; wifi_password_ = password; +#endif + } + +#ifdef ESP32 + // Snapshot the registered personal-network SSIDs without exposing passwords. + // On failure, the caller's output remains unchanged; an empty list is valid. + bool list_wifi(std::vector& ssids) { + if(!restore_wifi_networks()) return false; + std::vector snapshot; + for(const auto& network : wifi_credentials_) snapshot.push_back(network.ssid); + ssids.swap(snapshot); + return true; + } + + // Exact, case-sensitive SSID match. False means absent/invalid input or a + // failed update; wifi_storage_ok() distinguishes an NVS/restore failure. + bool remove_wifi(const char* ssid) { + if(!ssid || !*ssid) return false; + if(!restore_wifi_networks()) return false; + auto candidate = wifi_credentials_; + bool found = false; + for(auto it = candidate.begin(); it != candidate.end(); ++it) { + if(it->ssid != ssid) continue; + candidate.erase(it); + found = true; + break; + } + if(!found) return false; + // Keep the name alive even if the input referred to an owned string. + const std::string removed(ssid); + // Commit even an empty list: absence would enable the legacy driver's + // native saved-AP fallback and could resurrect the deleted last SSID. + if(!thinger_config::wifi_store::save(candidate) || !install_wifi_networks(candidate)) { + wifi_storage_failure(); + return false; + } + wifi_registry_exists_ = true; + if(WiFi.SSID() == removed.c_str()) WiFi.disconnect(false, true); + return true; + } + + // Clear only the persistent add_wifi()/portal registry. Keep an empty + // record so a reboot cannot fall back to native saved credentials. This + // explicit reset may also replace a corrupt/unreadable Wi-Fi record. + bool clear_wifi() { + thinger_config::wifi_store::networks empty; + if(!thinger_config::wifi_store::save(empty) || !install_wifi_networks(empty)) { + wifi_storage_failure(); + return false; + } + wifi_registry_exists_ = true; + const String current = WiFi.SSID(); + bool enterprise_connection = false; +#if CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT + if(enterprise_ssid_ && current == enterprise_ssid_) enterprise_connection = true; + for(const auto& network : runtime_enterprise_) { + if(current == network.ssid.c_str()) enterprise_connection = true; + } +#endif + if(current.length() && !enterprise_connection) WiFi.disconnect(false, true); + return true; + } + + // Lazy initialization: global device constructors run before NVS is ready. + // add_wifi(), WebConfig and connection attempts all call this method. + bool restore_wifi_networks() { + if(wifi_networks_loaded_) return !wifi_storage_error_; + thinger_config::wifi_store::networks loaded; + bool record_exists = false; + if(!thinger_config::wifi_store::load(loaded, &record_exists) || !install_wifi_networks(loaded)) { + wifi_storage_failure(); + return false; + } + wifi_registry_exists_ = record_exists; + return true; } + // add_wifi keeps its void signature. Check this immediately after calling + // it to detect invalid input, NVS errors or WiFiMulti allocation failure. + bool wifi_storage_ok() const { return !wifi_storage_error_; } + + bool has_wifi_networks() const { + return wifi_networks_ > 0 +#if CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT + || enterprise_ssid_ != nullptr +#endif + ; + } +#endif + #if defined(ESP32) && CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT // WPA2-Enterprise network with PEAP/EAP-MSCHAPv2, i.e., eduroam. The (outer) identity is the username // if empty. ca_pem is the CA certificate of the authentication server: without it, the device does not // validate the server and could send the credentials to a fake access point with the same SSID void add_wifi_enterprise(const char* ssid, const char* username, const char* password, const char* identity="", const char* ca_pem=nullptr) { + if(!restore_wifi_networks()) return; if(identity==nullptr || strlen(identity)==0) identity = username; // validating the server requires a direct connection (only one network can use it) if(ca_pem!=nullptr && enterprise_ssid_==nullptr){ @@ -199,7 +324,10 @@ class ThingerWifiClient : public ThingerClient { enterprise_ca_ = ca_pem; return; } - if(wifi_multi_.addAP(ssid, password, username, identity)){ + if(wifi_networks_ < 255 && wifi_multi_.addAP(ssid, password, username, identity)){ + // These credentials remain RAM-only, as before. Retain them when + // rebuilding WiFiMulti after a personal network is added/updated. + runtime_enterprise_.push_back({ssid, password ? password : "", username ? username : "", identity ? identity : ""}); wifi_networks_++; wifi_enterprise_ = true; } @@ -211,6 +339,58 @@ class ThingerWifiClient : public ThingerClient { const char* wifi_ssid_ = nullptr; const char* wifi_password_ = nullptr; #ifdef ESP32 + thinger_config::wifi_store::networks wifi_credentials_; + bool wifi_networks_loaded_ = false; + bool wifi_storage_error_ = false; + bool wifi_registry_exists_ = false; +#if CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT + struct enterprise_network { + std::string ssid, password, username, identity; + }; + std::vector runtime_enterprise_; +#endif + + size_t runtime_enterprise_count() const { +#if CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT + return runtime_enterprise_.size(); +#else + return 0; +#endif + } + + void wifi_storage_failure() { + wifi_storage_error_ = true; + wifi_networks_loaded_ = false; + set_error(ERROR_UNKNOWN); + THINGER_DEBUG("NETWORK", "Cannot persist/restore WiFi network list!"); + } + + bool install_wifi_networks(thinger_config::wifi_store::networks& networks) { + if(networks.size() + runtime_enterprise_count() > 255) return false; + // Core 2.x has no public APlistClean(). Reconstruct in place rather + // than copying WiFiMulti, which owns heap-allocated AP strings. + wifi_multi_.~THINGER_WIFI_MULTI(); + new (&wifi_multi_) THINGER_WIFI_MULTI(); + for(const auto& network : networks) { + if(!wifi_multi_.addAP(network.ssid.c_str(), network.password.c_str())) return false; + } +#if CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT + for(const auto& network : runtime_enterprise_) { + if(!wifi_multi_.addAP(network.ssid.c_str(), network.password.c_str(), + network.username.c_str(), network.identity.c_str())) return false; + } +#endif + wifi_credentials_.swap(networks); + // Own the strings: caller buffers and decoded PSON objects may expire. + wifi_ssid_ = wifi_credentials_.empty() ? nullptr : wifi_credentials_.front().ssid.c_str(); + wifi_password_ = wifi_credentials_.empty() ? nullptr : wifi_credentials_.front().password.c_str(); + wifi_networks_ = static_cast(wifi_credentials_.size() + runtime_enterprise_count()); + wifi_enterprise_ = runtime_enterprise_count() != 0; + wifi_networks_loaded_ = true; + wifi_storage_error_ = false; + return true; + } + // error of the last disconnection reported by the ESP32 WiFi events static THINGER_ERROR& wifi_disconnect_error(){ static THINGER_ERROR error = ERROR_UNKNOWN; @@ -249,4 +429,4 @@ class ThingerWifiClient : public ThingerClient { #define ThingerWifi ThingerWifiClient -#endif \ No newline at end of file +#endif diff --git a/tests/nvs_config/esp32_core/LICENSE.md b/tests/nvs_config/esp32_core/LICENSE.md new file mode 100644 index 0000000..b14d908 --- /dev/null +++ b/tests/nvs_config/esp32_core/LICENSE.md @@ -0,0 +1,503 @@ +### GNU LESSER GENERAL PUBLIC LICENSE + +Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + [This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + +### Preamble + +The licenses for most software are designed to take away your freedom +to share and change it. By contrast, the GNU General Public Licenses +are intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. + +This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations +below. + +When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + +To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + +For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + +We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + +To protect each distributor, we want to make it very clear that there +is no warranty for the free library. Also, if the library is modified +by someone else and passed on, the recipients should know that what +they have is not the original version, so that the original author's +reputation will not be affected by problems that might be introduced +by others. + +Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + +Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + +When a program is linked with a library, whether statically or using a +shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + +We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + +For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it +becomes a de-facto standard. To achieve this, non-free programs must +be allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + +In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + +Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + +The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + +### TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +**0.** This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). Each +licensee is addressed as "you". + +A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control +compilation and installation of the library. + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does and +what the program that uses the Library does. + +**1.** You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a +fee. + +**2.** You may modify your copy or copies of the Library or any +portion of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + +- **a)** The modified work must itself be a software library. +- **b)** You must cause the files modified to carry prominent + notices stating that you changed the files and the date of + any change. +- **c)** You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. +- **d)** If a facility in the modified Library refers to a function + or a table of data to be supplied by an application program that + uses the facility, other than as an argument passed when the + facility is invoked, then you must make a good faith effort to + ensure that, in the event an application does not supply such + function or table, the facility still operates, and performs + whatever part of its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of + the application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + +**3.** You may opt to apply the terms of the ordinary GNU General +Public License instead of this License to a given copy of the Library. +To do this, you must alter all the notices that refer to this License, +so that they refer to the ordinary GNU General Public License, version +2, instead of to this License. (If a newer version than version 2 of +the ordinary GNU General Public License has appeared, then you can +specify that version instead if you wish.) Do not make any other +change in these notices. + +Once this change is made in a given copy, it is irreversible for that +copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the +Library into a program that is not a library. + +**4.** You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from +a designated place, then offering equivalent access to copy the source +code from the same place satisfies the requirement to distribute the +source code, even though third parties are not compelled to copy the +source along with the object code. + +**5.** A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a work, +in isolation, is not a derivative work of the Library, and therefore +falls outside the scope of this License. + +However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. Section +6 states terms for distribution of such executables. + +When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + +If such an object file uses only numerical parameters, data structure +layouts and accessors, and small macros and small inline functions +(ten lines or less in length), then the use of the object file is +unrestricted, regardless of whether it is legally a derivative work. +(Executables containing this object code plus portions of the Library +will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + +**6.** As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a work +containing portions of the Library, and distribute that work under +terms of your choice, provided that the terms permit modification of +the work for the customer's own use and reverse engineering for +debugging such modifications. + +You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + +- **a)** Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood that + the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) +- **b)** Use a suitable shared library mechanism for linking with + the Library. A suitable mechanism is one that (1) uses at run time + a copy of the library already present on the user's computer + system, rather than copying library functions into the executable, + and (2) will operate properly with a modified version of the + library, if the user installs one, as long as the modified version + is interface-compatible with the version that the work was + made with. +- **c)** Accompany the work with a written offer, valid for at least + three years, to give the same user the materials specified in + Subsection 6a, above, for a charge no more than the cost of + performing this distribution. +- **d)** If distribution of the work is made by offering access to + copy from a designated place, offer equivalent access to copy the + above specified materials from the same place. +- **e)** Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + +It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + +**7.** You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + +- **a)** Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other + library facilities. This must be distributed under the terms of + the Sections above. +- **b)** Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + +**8.** You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + +**9.** You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + +**10.** Each time you redistribute the Library (or any work based on +the Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + +**11.** If, as a consequence of a court judgment or allegation of +patent infringement or for any other reason (not limited to patent +issues), conditions are imposed on you (whether by court order, +agreement or otherwise) that contradict the conditions of this +License, they do not excuse you from the conditions of this License. +If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, +then as a consequence you may not distribute the Library at all. For +example, if a patent license would not permit royalty-free +redistribution of the Library by all those who receive copies directly +or indirectly through you, then the only way you could satisfy both it +and this License would be to refrain entirely from distribution of the +Library. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply, and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + +**12.** If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + +**13.** The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. Such +new versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + +**14.** If you wish to incorporate parts of the Library into other +free programs whose distribution conditions are incompatible with +these, write to the author to ask for permission. For software which +is copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + +**NO WARRANTY** + +**15.** BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +**16.** IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + +### END OF TERMS AND CONDITIONS + +### How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms +of the ordinary General Public License). + +To apply these terms, attach the following notices to the library. It +is safest to attach them to the start of each source file to most +effectively convey the exclusion of warranty; and each file should +have at least the "copyright" line and a pointer to where the full +notice is found. + + one line to give the library's name and an idea of what it does. + Copyright (C) year name of author + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper +mail. + +You should also get your employer (if you work as a programmer) or +your school, if any, to sign a "copyright disclaimer" for the library, +if necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in + the library `Frob' (a library for tweaking knobs) written + by James Random Hacker. + + signature of Ty Coon, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/tests/nvs_config/esp32_core/SOURCE.txt b/tests/nvs_config/esp32_core/SOURCE.txt new file mode 100644 index 0000000..67122c0 --- /dev/null +++ b/tests/nvs_config/esp32_core/SOURCE.txt @@ -0,0 +1,6 @@ +Unmodified WiFiMulti test fixture from Espressif arduino-esp32. +Header: https://github.com/espressif/arduino-esp32/blob/3.3.12/libraries/WiFi/src/WiFiMulti.h +Git blob SHA: 997b636e62bba687d8782dfa7ac445906cf682f6 +Source: https://github.com/espressif/arduino-esp32/blob/3.3.12/libraries/WiFi/src/WiFiMulti.cpp +Git blob SHA: 958a603d3872e320c9d790816541b83688a259f7 +License: LGPL-2.1-or-later; see file headers and LICENSE.md. diff --git a/tests/nvs_config/esp32_core/WiFiMulti.cpp b/tests/nvs_config/esp32_core/WiFiMulti.cpp new file mode 100644 index 0000000..958a603 --- /dev/null +++ b/tests/nvs_config/esp32_core/WiFiMulti.cpp @@ -0,0 +1,436 @@ +/** + * + * @file WiFiMulti.cpp + * @date 16.05.2015 + * @author Markus Sattler + * + * Copyright (c) 2015 Markus Sattler. All rights reserved. + * This file is part of the esp8266 core for Arduino environment. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "WiFiMulti.h" +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_HOSTED_ENABLED +#include +#include +#include + +WiFiMulti::WiFiMulti() { + ipv6_support = false; +} + +void WiFiMulti::APlistClean(void) { + for (auto entry : APlist) { + if (entry.ssid) { + free(entry.ssid); + } + if (entry.passphrase) { + free(entry.passphrase); + } +#if CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT + if (entry.username) { + free(entry.username); + } + if (entry.identity) { + free(entry.identity); + } +#endif /* CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT */ + } + APlist.clear(); +} + +WiFiMulti::~WiFiMulti() { + APlistClean(); +} + +#if CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT +bool WiFiMulti::addAP(const char *ssid, const char *passphrase, const char *username, const char *identity) { +#else +bool WiFiMulti::addAP(const char *ssid, const char *passphrase) { +#endif /* CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT */ + WifiAPlist_t newAP; + + if (!ssid || *ssid == '\0' || strlen(ssid) > 31) { + // fail SSID too long or missing! + log_e("[WIFI][APlistAdd] no ssid or ssid too long"); + return false; + } + + if (passphrase && strlen(passphrase) > 63) { + // fail passphrase too long! + log_e("[WIFI][APlistAdd] passphrase too long"); + return false; + } + +#if CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT + if (username && strlen(username) > 63) { + // fail username too long! + log_e("[WIFI][APlistAdd] username too long"); + return false; + } + + if (identity && strlen(identity) > 63) { + // fail identity too long! + log_e("[WIFI][APlistAdd] identity too long"); + return false; + } +#endif /* CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT */ + + newAP.ssid = strdup(ssid); + + if (!newAP.ssid) { + log_e("[WIFI][APlistAdd] fail newAP.ssid == 0"); + return false; + } + + if (passphrase && *passphrase != '\0') { + newAP.passphrase = strdup(passphrase); + if (!newAP.passphrase) { + log_e("[WIFI][APlistAdd] fail newAP.passphrase == 0"); + free(newAP.ssid); + return false; + } + } else { + newAP.passphrase = NULL; + } + +#if CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT + if (username && *username != '\0') { + newAP.username = strdup(username); + if (!newAP.username) { + log_e("[WIFI][APlistAdd] fail newAP.username == 0"); + free(newAP.ssid); + if (newAP.passphrase) { + free(newAP.passphrase); + } + return false; + } + } else { + newAP.username = NULL; + } + + if (identity) { // Allow empty identity as not all WPA2-Enterprise networks require it + newAP.identity = strdup(identity); + if (!newAP.identity) { + log_e("[WIFI][APlistAdd] fail newAP.identity == 0"); + free(newAP.ssid); + if (newAP.passphrase) { + free(newAP.passphrase); + } + if (newAP.username) { + free(newAP.username); + } + return false; + } + } else { + newAP.identity = NULL; + } +#endif /* CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT */ + + newAP.hasFailed = false; + APlist.push_back(newAP); + log_i("[WIFI][APlistAdd] add SSID: %s", newAP.ssid); + return true; +} + +uint8_t WiFiMulti::run(uint32_t connectTimeout, bool scanHidden) { + int8_t scanResult; + unsigned long startTime; + uint8_t status = WiFi.status(); + if (status == WL_CONNECTED) { + if (!_bWFMInit && _connectionTestCBFunc != NULL) { + if (_connectionTestCBFunc() == true) { + _bWFMInit = true; + return status; + } + } else { + if (!_bStrict) { + return status; + } else { + for (auto ap : APlist) { + if (WiFi.SSID() == ap.ssid) { + return status; + } + } + } + } + WiFi.disconnect(false, false); + delay(10); + status = WiFi.status(); + } + + scanResult = WiFi.scanNetworks(false, scanHidden); + if (scanResult == WIFI_SCAN_RUNNING) { + // scan is running + return WL_NO_SSID_AVAIL; + } else if (scanResult >= 0) { + // scan done analyze + int32_t bestIndex = -1; +#if CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT + WifiAPlist_t bestNetwork{NULL, NULL, NULL, NULL, false}; +#else + WifiAPlist_t bestNetwork{NULL, NULL, false}; +#endif /* CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT */ + int bestNetworkDb = INT_MIN; + int bestNetworkSec = WIFI_AUTH_MAX; + uint8_t bestBSSID[6]; + int32_t bestChannel = 0; + + log_i("[WIFI] scan done"); + + if (scanResult == 0) { + log_e("[WIFI] no networks found"); + } else { + log_i("[WIFI] %d networks found", scanResult); + + int8_t failCount = 0; + int8_t foundCount = 0; + for (int8_t i = 0; i < scanResult; ++i) { + + String ssid_scan; + int32_t rssi_scan; + uint8_t sec_scan; + uint8_t *BSSID_scan; + int32_t chan_scan; + bool hidden_scan; + + WiFi.getNetworkInfo(i, ssid_scan, sec_scan, rssi_scan, BSSID_scan, chan_scan); + hidden_scan = (ssid_scan.length() == 0) && scanHidden; + // add any Open WiFi AP to the list, if allowed with setAllowOpenAP(true) + if (_bAllowOpenAP && sec_scan == WIFI_AUTH_OPEN) { + bool found = false; + for (auto check : APlist) { + if (ssid_scan == check.ssid) { + found = true; + break; + } + } + // If we didn't find it, add this Open WiFi AP to the list + if (!found) { + log_i("[WIFI][APlistAdd] adding Open WiFi SSID: %s", ssid_scan.c_str()); + addAP(ssid_scan.c_str()); + } + } + + if (hidden_scan) { + log_v("hidden ssid on channel %" PRIi32 " found, trying to connect with known credentials...", chan_scan); + } + + bool known = false; + for (uint32_t x = 0; x < APlist.size(); x++) { + WifiAPlist_t entry = APlist[x]; + + if (ssid_scan == entry.ssid || hidden_scan) { // SSID match or hidden network found + if (!hidden_scan) { + log_v("known ssid: %s, has failed: %s", entry.ssid, entry.hasFailed ? "yes" : "no"); + foundCount++; + } + if (!entry.hasFailed) { + if (hidden_scan) { + WiFi.begin(entry.ssid, entry.passphrase, chan_scan, BSSID_scan); + + // If the ssid returned from the scan is empty, it is a hidden SSID + // it appears that the WiFi.begin() function is asynchronous and takes + // additional time to connect to a hidden SSID. Therefore a delay of 1000ms + // is added for hidden SSIDs before calling WiFi.status() + delay(1000); + + status = WiFi.status(); + startTime = millis(); + + while (status != WL_CONNECTED && (millis() - startTime) <= connectTimeout) { + delay(10); + status = WiFi.status(); + } + + WiFi.disconnect(); + delay(10); + + if (status == WL_CONNECTED) { + log_v("hidden ssid %s found", entry.ssid); + ssid_scan = entry.ssid; + foundCount++; + } else { + continue; + } + } + known = true; + log_v("rssi_scan: %" PRIi32 ", bestNetworkDb: %d", rssi_scan, bestNetworkDb); + if (rssi_scan > bestNetworkDb) { // best network +#if CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT + if (sec_scan == WIFI_AUTH_OPEN || ((sec_scan != WIFI_AUTH_WPA2_ENTERPRISE) && entry.passphrase) + || ((sec_scan == WIFI_AUTH_WPA2_ENTERPRISE) && entry.passphrase && entry.username && entry.identity)) { +#else + if (sec_scan == WIFI_AUTH_OPEN || entry.passphrase) { // check for passphrase if not open wlan +#endif + log_v("best network is now: %s", ssid_scan); + bestIndex = x; + bestNetworkSec = sec_scan; + bestNetworkDb = rssi_scan; + bestChannel = chan_scan; + memcpy((void *)&bestNetwork, (void *)&entry, sizeof(bestNetwork)); + memcpy((void *)&bestBSSID, (void *)BSSID_scan, sizeof(bestBSSID)); + } else { + log_v("insufficient credentials for %s", ssid_scan); + } + } + break; + } else { + failCount++; + } + } + } + + if (known) { + log_d( + " ---> %d: [%" PRIi32 "][%02X:%02X:%02X:%02X:%02X:%02X] %s (%" PRIi32 ") (%c) (%s)", i, chan_scan, BSSID_scan[0], BSSID_scan[1], BSSID_scan[2], + BSSID_scan[3], BSSID_scan[4], BSSID_scan[5], ssid_scan.c_str(), rssi_scan, (sec_scan == WIFI_AUTH_OPEN) ? ' ' : '*', + (hidden_scan) ? "hidden" : "visible" + ); + } else { + log_d( + " %d: [%" PRIi32 "][%02X:%02X:%02X:%02X:%02X:%02X] %s (%" PRIi32 ") (%c) (%s)", i, chan_scan, BSSID_scan[0], BSSID_scan[1], BSSID_scan[2], + BSSID_scan[3], BSSID_scan[4], BSSID_scan[5], ssid_scan.c_str(), rssi_scan, (sec_scan == WIFI_AUTH_OPEN) ? ' ' : '*', + (hidden_scan) ? "hidden" : "visible" + ); + } + } + log_v("foundCount = %d, failCount = %d", foundCount, failCount); + // if all the APs in the list have failed, reset the failure flags + if (foundCount == failCount) { + resetFails(); // keeps trying the APs in the list + } + } + // clean up ram + WiFi.scanDelete(); + + if (bestIndex >= 0) { + log_i( + "[WIFI] Connecting BSSID: %02X:%02X:%02X:%02X:%02X:%02X SSID: %s Channel: %" PRIi32 " (%d)", bestBSSID[0], bestBSSID[1], bestBSSID[2], bestBSSID[3], + bestBSSID[4], bestBSSID[5], bestNetwork.ssid, bestChannel, bestNetworkDb + ); + +#if CONFIG_LWIP_IPV6 + if (ipv6_support == true) { + WiFi.enableIPv6(); + } +#endif + WiFi.disconnect(); + delay(10); +#if CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT + if (bestNetworkSec != WIFI_AUTH_WPA2_ENTERPRISE) { + WiFi.begin(bestNetwork.ssid, (bestNetworkSec == WIFI_AUTH_OPEN) ? NULL : bestNetwork.passphrase, bestChannel, bestBSSID); + } else { + WiFi.begin( + bestNetwork.ssid, WPA2_AUTH_PEAP, bestNetwork.identity, bestNetwork.username, bestNetwork.passphrase, NULL, NULL, NULL, -1, bestChannel, bestBSSID + ); + } +#else + WiFi.begin(bestNetwork.ssid, (bestNetworkSec == WIFI_AUTH_OPEN) ? NULL : bestNetwork.passphrase, bestChannel, bestBSSID); +#endif /* CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT */ + status = WiFi.status(); + _bWFMInit = true; + + startTime = millis(); + // wait for connection, fail, or timeout + while (status != WL_CONNECTED && (millis() - startTime) <= connectTimeout) { // && status != WL_NO_SSID_AVAIL && status != WL_CONNECT_FAILED + delay(10); + status = WiFi.status(); + } + + switch (status) { + case WL_CONNECTED: + log_i("[WIFI] Connecting done."); + log_d("[WIFI] SSID: %s", WiFi.SSID().c_str()); + log_d("[WIFI] IP: %s", WiFi.localIP().toString().c_str()); + log_d("[WIFI] MAC: %s", WiFi.BSSIDstr().c_str()); + log_d("[WIFI] Channel: %" PRIi32, WiFi.channel()); + + if (_connectionTestCBFunc != NULL) { + // We connected to an AP but if it's a captive portal we're not going anywhere. Test it. + if (_connectionTestCBFunc()) { + resetFails(); + } else { + markAsFailed(bestIndex); + WiFi.disconnect(); + delay(10); + status = WiFi.status(); + } + } else { + resetFails(); + } + break; + case WL_NO_SSID_AVAIL: + log_e("[WIFI] Connecting Failed AP not found."); + markAsFailed(bestIndex); + break; + case WL_CONNECT_FAILED: + log_e("[WIFI] Connecting Failed."); + markAsFailed(bestIndex); + break; + default: + log_e("[WIFI] Connecting Failed (%d).", status); + markAsFailed(bestIndex); + break; + } + } else { + log_e("[WIFI] no matching wifi found!"); + } + } else { + // start scan + log_d("[WIFI] delete old wifi config..."); + WiFi.disconnect(); + + log_d("[WIFI] start scan"); + // scan wifi async mode + WiFi.scanNetworks(true); + } + + return status; +} + +#if CONFIG_LWIP_IPV6 +void WiFiMulti::enableIPv6(bool state) { + ipv6_support = state; +} +#endif + +void WiFiMulti::markAsFailed(int32_t i) { + APlist[i].hasFailed = true; + log_d("[WIFI] Marked SSID %s as failed", APlist[i].ssid); +} + +void WiFiMulti::resetFails() { + for (uint32_t i = 0; i < APlist.size(); i++) { + APlist[i].hasFailed = false; + } + log_d("[WIFI] Resetting failure flags"); +} + +void WiFiMulti::setStrictMode(bool bStrict) { + _bStrict = bStrict; +} + +void WiFiMulti::setAllowOpenAP(bool bAllowOpenAP) { + _bAllowOpenAP = bAllowOpenAP; +} + +void WiFiMulti::setConnectionTestCallbackFunc(ConnectionTestCB_t cbFunc) { + _connectionTestCBFunc = cbFunc; +} + +#endif /* SOC_WIFI_SUPPORTED */ diff --git a/tests/nvs_config/esp32_core/WiFiMulti.h b/tests/nvs_config/esp32_core/WiFiMulti.h new file mode 100644 index 0000000..997b636 --- /dev/null +++ b/tests/nvs_config/esp32_core/WiFiMulti.h @@ -0,0 +1,97 @@ +/** + * + * @file ESP8266WiFiMulti.h + * @date 16.05.2015 + * @author Markus Sattler + * + * Copyright (c) 2015 Markus Sattler. All rights reserved. + * This file is part of the esp8266 core for Arduino environment. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#pragma once + +#include "soc/soc_caps.h" +#include "sdkconfig.h" +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_HOSTED_ENABLED + +#include "WiFi.h" +#include + +typedef struct { + char *ssid; + char *passphrase; +#if CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT + char *username; + char *identity; +#endif /* CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT */ + bool hasFailed; +} WifiAPlist_t; + +typedef std::function ConnectionTestCB_t; + +class WiFiMulti { +public: + WiFiMulti(); + ~WiFiMulti(); + +#if CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT + // For open networks an ssid must be provided. For networks secured with a passphrase, e.g. WPA2-PSK, both an ssid and + // passphrase must be provided. For networks secured with WPA2-Enterprise and supporting e.g. PEAPv0/EAP-MSCHAPv2, + // a username and anonymous identity must be provided in addition to an ssid and passphrase (password). The anonymous + // identity may be an empty string if it is not required by the network. Certificate-based WPA2-Enterprise is not yet + // supported, nor is providing a CA certificate for PEAP. + bool addAP(const char *ssid, const char *passphrase = NULL, const char *username = NULL, const char *identity = NULL); +#else + bool addAP(const char *ssid, const char *passphrase = NULL); +#endif /* CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT */ + + uint8_t run(uint32_t connectTimeout = 5000, bool scanHidden = false); +#if CONFIG_LWIP_IPV6 + void enableIPv6(bool state); +#endif + + // Force (default: true) to only keep connected or to connect to an AP from the provided WiFiMulti list. + // When bStrict is false, it will keep the last/current connected AP even if not in the WiFiMulti List. + void setStrictMode(bool bStrict = true); + + // allows (true) to connect to ANY open AP, even if not in the user list + // default false (do not connect to an open AP that has not been explicitaly added by the user to list) + void setAllowOpenAP(bool bAllowOpenAP = false); + + // clears the current list of Multi APs and frees the memory + void APlistClean(void); + + // allow the user to define a callback function that will validate the connection to the Internet. + // if the callback returns true, the connection is considered valid and the AP will added to the validated AP list. + // set the callback to NULL to disable the feature and validate any SSID that is in the list. + void setConnectionTestCallbackFunc(ConnectionTestCB_t cbFunc); + +private: + std::vector APlist; + bool ipv6_support; + + bool _bStrict = true; + bool _bAllowOpenAP = false; + ConnectionTestCB_t _connectionTestCBFunc = NULL; + bool _bWFMInit = false; + + void markAsFailed(int32_t i); + void resetFails(); +}; + +#endif /* SOC_WIFI_SUPPORTED */ diff --git a/tests/nvs_config/esp32_core_v2/LICENSE.md b/tests/nvs_config/esp32_core_v2/LICENSE.md new file mode 100644 index 0000000..b14d908 --- /dev/null +++ b/tests/nvs_config/esp32_core_v2/LICENSE.md @@ -0,0 +1,503 @@ +### GNU LESSER GENERAL PUBLIC LICENSE + +Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + [This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + +### Preamble + +The licenses for most software are designed to take away your freedom +to share and change it. By contrast, the GNU General Public Licenses +are intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. + +This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations +below. + +When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + +To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + +For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + +We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + +To protect each distributor, we want to make it very clear that there +is no warranty for the free library. Also, if the library is modified +by someone else and passed on, the recipients should know that what +they have is not the original version, so that the original author's +reputation will not be affected by problems that might be introduced +by others. + +Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + +Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + +When a program is linked with a library, whether statically or using a +shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + +We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + +For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it +becomes a de-facto standard. To achieve this, non-free programs must +be allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + +In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + +Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + +The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + +### TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +**0.** This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). Each +licensee is addressed as "you". + +A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control +compilation and installation of the library. + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does and +what the program that uses the Library does. + +**1.** You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a +fee. + +**2.** You may modify your copy or copies of the Library or any +portion of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + +- **a)** The modified work must itself be a software library. +- **b)** You must cause the files modified to carry prominent + notices stating that you changed the files and the date of + any change. +- **c)** You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. +- **d)** If a facility in the modified Library refers to a function + or a table of data to be supplied by an application program that + uses the facility, other than as an argument passed when the + facility is invoked, then you must make a good faith effort to + ensure that, in the event an application does not supply such + function or table, the facility still operates, and performs + whatever part of its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of + the application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + +**3.** You may opt to apply the terms of the ordinary GNU General +Public License instead of this License to a given copy of the Library. +To do this, you must alter all the notices that refer to this License, +so that they refer to the ordinary GNU General Public License, version +2, instead of to this License. (If a newer version than version 2 of +the ordinary GNU General Public License has appeared, then you can +specify that version instead if you wish.) Do not make any other +change in these notices. + +Once this change is made in a given copy, it is irreversible for that +copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the +Library into a program that is not a library. + +**4.** You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from +a designated place, then offering equivalent access to copy the source +code from the same place satisfies the requirement to distribute the +source code, even though third parties are not compelled to copy the +source along with the object code. + +**5.** A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a work, +in isolation, is not a derivative work of the Library, and therefore +falls outside the scope of this License. + +However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. Section +6 states terms for distribution of such executables. + +When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + +If such an object file uses only numerical parameters, data structure +layouts and accessors, and small macros and small inline functions +(ten lines or less in length), then the use of the object file is +unrestricted, regardless of whether it is legally a derivative work. +(Executables containing this object code plus portions of the Library +will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + +**6.** As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a work +containing portions of the Library, and distribute that work under +terms of your choice, provided that the terms permit modification of +the work for the customer's own use and reverse engineering for +debugging such modifications. + +You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + +- **a)** Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood that + the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) +- **b)** Use a suitable shared library mechanism for linking with + the Library. A suitable mechanism is one that (1) uses at run time + a copy of the library already present on the user's computer + system, rather than copying library functions into the executable, + and (2) will operate properly with a modified version of the + library, if the user installs one, as long as the modified version + is interface-compatible with the version that the work was + made with. +- **c)** Accompany the work with a written offer, valid for at least + three years, to give the same user the materials specified in + Subsection 6a, above, for a charge no more than the cost of + performing this distribution. +- **d)** If distribution of the work is made by offering access to + copy from a designated place, offer equivalent access to copy the + above specified materials from the same place. +- **e)** Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + +It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + +**7.** You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + +- **a)** Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other + library facilities. This must be distributed under the terms of + the Sections above. +- **b)** Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + +**8.** You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + +**9.** You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + +**10.** Each time you redistribute the Library (or any work based on +the Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + +**11.** If, as a consequence of a court judgment or allegation of +patent infringement or for any other reason (not limited to patent +issues), conditions are imposed on you (whether by court order, +agreement or otherwise) that contradict the conditions of this +License, they do not excuse you from the conditions of this License. +If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, +then as a consequence you may not distribute the Library at all. For +example, if a patent license would not permit royalty-free +redistribution of the Library by all those who receive copies directly +or indirectly through you, then the only way you could satisfy both it +and this License would be to refrain entirely from distribution of the +Library. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply, and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + +**12.** If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + +**13.** The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. Such +new versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + +**14.** If you wish to incorporate parts of the Library into other +free programs whose distribution conditions are incompatible with +these, write to the author to ask for permission. For software which +is copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + +**NO WARRANTY** + +**15.** BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +**16.** IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + +### END OF TERMS AND CONDITIONS + +### How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms +of the ordinary General Public License). + +To apply these terms, attach the following notices to the library. It +is safest to attach them to the start of each source file to most +effectively convey the exclusion of warranty; and each file should +have at least the "copyright" line and a pointer to where the full +notice is found. + + one line to give the library's name and an idea of what it does. + Copyright (C) year name of author + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper +mail. + +You should also get your employer (if you work as a programmer) or +your school, if any, to sign a "copyright disclaimer" for the library, +if necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in + the library `Frob' (a library for tweaking knobs) written + by James Random Hacker. + + signature of Ty Coon, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/tests/nvs_config/esp32_core_v2/SOURCE.txt b/tests/nvs_config/esp32_core_v2/SOURCE.txt new file mode 100644 index 0000000..35ce27f --- /dev/null +++ b/tests/nvs_config/esp32_core_v2/SOURCE.txt @@ -0,0 +1,6 @@ +Unmodified WiFiMulti test fixture from Espressif arduino-esp32. +Header: https://github.com/espressif/arduino-esp32/blob/2.0.17/libraries/WiFi/src/WiFiMulti.h +Git blob SHA: 38ddb5d9f958fe47d0bccf74f4d7e61396c69dd5 +Source: https://github.com/espressif/arduino-esp32/blob/2.0.17/libraries/WiFi/src/WiFiMulti.cpp +Git blob SHA: 3d69e481293c73a7b562d66d1bb44b63b62fb493 +License: LGPL-2.1-or-later; see file headers and LICENSE.md. diff --git a/tests/nvs_config/esp32_core_v2/WiFiMulti.cpp b/tests/nvs_config/esp32_core_v2/WiFiMulti.cpp new file mode 100644 index 0000000..3d69e48 --- /dev/null +++ b/tests/nvs_config/esp32_core_v2/WiFiMulti.cpp @@ -0,0 +1,204 @@ +/** + * + * @file WiFiMulti.cpp + * @date 16.05.2015 + * @author Markus Sattler + * + * Copyright (c) 2015 Markus Sattler. All rights reserved. + * This file is part of the esp8266 core for Arduino environment. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "WiFiMulti.h" +#include +#include +#include + +WiFiMulti::WiFiMulti() +{ +} + +WiFiMulti::~WiFiMulti() +{ + for(uint32_t i = 0; i < APlist.size(); i++) { + WifiAPlist_t entry = APlist[i]; + if(entry.ssid) { + free(entry.ssid); + } + if(entry.passphrase) { + free(entry.passphrase); + } + } + APlist.clear(); +} + +bool WiFiMulti::addAP(const char* ssid, const char *passphrase) +{ + WifiAPlist_t newAP; + + if(!ssid || *ssid == 0x00 || strlen(ssid) > 31) { + // fail SSID too long or missing! + log_e("[WIFI][APlistAdd] no ssid or ssid too long"); + return false; + } + + if(passphrase && strlen(passphrase) > 64) { + // fail passphrase too long! + log_e("[WIFI][APlistAdd] passphrase too long"); + return false; + } + + newAP.ssid = strdup(ssid); + + if(!newAP.ssid) { + log_e("[WIFI][APlistAdd] fail newAP.ssid == 0"); + return false; + } + + if(passphrase && *passphrase != 0x00) { + newAP.passphrase = strdup(passphrase); + if(!newAP.passphrase) { + log_e("[WIFI][APlistAdd] fail newAP.passphrase == 0"); + free(newAP.ssid); + return false; + } + } else { + newAP.passphrase = NULL; + } + + APlist.push_back(newAP); + log_i("[WIFI][APlistAdd] add SSID: %s", newAP.ssid); + return true; +} + +uint8_t WiFiMulti::run(uint32_t connectTimeout) +{ + int8_t scanResult; + uint8_t status = WiFi.status(); + if(status == WL_CONNECTED) { + for(uint32_t x = 0; x < APlist.size(); x++) { + if(WiFi.SSID()==APlist[x].ssid) { + return status; + } + } + WiFi.disconnect(false,false); + delay(10); + status = WiFi.status(); + } + + scanResult = WiFi.scanNetworks(); + if(scanResult == WIFI_SCAN_RUNNING) { + // scan is running + return WL_NO_SSID_AVAIL; + } else if(scanResult >= 0) { + // scan done analyze + WifiAPlist_t bestNetwork { NULL, NULL }; + int bestNetworkDb = INT_MIN; + uint8_t bestBSSID[6]; + int32_t bestChannel = 0; + + log_i("[WIFI] scan done"); + + if(scanResult == 0) { + log_e("[WIFI] no networks found"); + } else { + log_i("[WIFI] %d networks found", scanResult); + for(int8_t i = 0; i < scanResult; ++i) { + + String ssid_scan; + int32_t rssi_scan; + uint8_t sec_scan; + uint8_t* BSSID_scan; + int32_t chan_scan; + + WiFi.getNetworkInfo(i, ssid_scan, sec_scan, rssi_scan, BSSID_scan, chan_scan); + + bool known = false; + for(uint32_t x = APlist.size() ; x > 0; x--) { + WifiAPlist_t entry = APlist[x-1]; + + if(ssid_scan == entry.ssid) { // SSID match + known = true; + if(rssi_scan > bestNetworkDb) { // best network + if(sec_scan == WIFI_AUTH_OPEN || entry.passphrase) { // check for passphrase if not open wlan + bestNetworkDb = rssi_scan; + bestChannel = chan_scan; + memcpy((void*) &bestNetwork, (void*) &entry, sizeof(bestNetwork)); + memcpy((void*) &bestBSSID, (void*) BSSID_scan, sizeof(bestBSSID)); + } + } + break; + } + } + + if(known) { + log_d(" ---> %d: [%d][%02X:%02X:%02X:%02X:%02X:%02X] %s (%d) %c", i, chan_scan, BSSID_scan[0], BSSID_scan[1], BSSID_scan[2], BSSID_scan[3], BSSID_scan[4], BSSID_scan[5], ssid_scan.c_str(), rssi_scan, (sec_scan == WIFI_AUTH_OPEN) ? ' ' : '*'); + } else { + log_d(" %d: [%d][%02X:%02X:%02X:%02X:%02X:%02X] %s (%d) %c", i, chan_scan, BSSID_scan[0], BSSID_scan[1], BSSID_scan[2], BSSID_scan[3], BSSID_scan[4], BSSID_scan[5], ssid_scan.c_str(), rssi_scan, (sec_scan == WIFI_AUTH_OPEN) ? ' ' : '*'); + } + } + } + + // clean up ram + WiFi.scanDelete(); + + if(bestNetwork.ssid) { + log_i("[WIFI] Connecting BSSID: %02X:%02X:%02X:%02X:%02X:%02X SSID: %s Channel: %d (%d)", bestBSSID[0], bestBSSID[1], bestBSSID[2], bestBSSID[3], bestBSSID[4], bestBSSID[5], bestNetwork.ssid, bestChannel, bestNetworkDb); + + WiFi.begin(bestNetwork.ssid, bestNetwork.passphrase, bestChannel, bestBSSID); + status = WiFi.status(); + + auto startTime = millis(); + // wait for connection, fail, or timeout + while(status != WL_CONNECTED && status != WL_NO_SSID_AVAIL && status != WL_CONNECT_FAILED && (millis() - startTime) <= connectTimeout) { + delay(10); + status = WiFi.status(); + } + + switch(status) { + case WL_CONNECTED: + log_i("[WIFI] Connecting done."); + log_d("[WIFI] SSID: %s", WiFi.SSID().c_str()); + log_d("[WIFI] IP: %s", WiFi.localIP().toString().c_str()); + log_d("[WIFI] MAC: %s", WiFi.BSSIDstr().c_str()); + log_d("[WIFI] Channel: %d", WiFi.channel()); + break; + case WL_NO_SSID_AVAIL: + log_e("[WIFI] Connecting Failed AP not found."); + break; + case WL_CONNECT_FAILED: + log_e("[WIFI] Connecting Failed."); + break; + default: + log_e("[WIFI] Connecting Failed (%d).", status); + break; + } + } else { + log_e("[WIFI] no matching wifi found!"); + } + } else { + // start scan + log_d("[WIFI] delete old wifi config..."); + WiFi.disconnect(); + + log_d("[WIFI] start scan"); + // scan wifi async mode + WiFi.scanNetworks(true); + } + + return status; +} diff --git a/tests/nvs_config/esp32_core_v2/WiFiMulti.h b/tests/nvs_config/esp32_core_v2/WiFiMulti.h new file mode 100644 index 0000000..38ddb5d --- /dev/null +++ b/tests/nvs_config/esp32_core_v2/WiFiMulti.h @@ -0,0 +1,51 @@ +/** + * + * @file ESP8266WiFiMulti.h + * @date 16.05.2015 + * @author Markus Sattler + * + * Copyright (c) 2015 Markus Sattler. All rights reserved. + * This file is part of the esp8266 core for Arduino environment. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef WIFICLIENTMULTI_H_ +#define WIFICLIENTMULTI_H_ + +#include "WiFi.h" +#include + +typedef struct { + char * ssid; + char * passphrase; +} WifiAPlist_t; + +class WiFiMulti +{ +public: + WiFiMulti(); + ~WiFiMulti(); + + bool addAP(const char* ssid, const char *passphrase = NULL); + + uint8_t run(uint32_t connectTimeout=5000); + +private: + std::vector APlist; +}; + +#endif /* WIFICLIENTMULTI_H_ */ diff --git a/tests/nvs_config/sketch/sketch.ino b/tests/nvs_config/sketch/sketch.ino new file mode 100644 index 0000000..0958a50 --- /dev/null +++ b/tests/nvs_config/sketch/sketch.ino @@ -0,0 +1,21 @@ +// Compile-only API integration sketch, also suitable for a new test device. +// Remove SKIP_LEGACY_MIGRATION when updating a device with config.pson. +#define THINGER_ESP32_CONFIG_SKIP_LEGACY_MIGRATION +#include + +ThingerESP32WebConfig device("user", "device", "credential"); + +void setup() { + device.add_wifi("network-a", "password-a"); + device.add_wifi("network-b", "password-b"); + if(!device.wifi_storage_ok()) return; + // On subsequent boots, both add_wifi calls above can be omitted. + device.restore_wifi_networks(); + std::vector ssids; + if(!device.list_wifi(ssids)) return; + device.remove_wifi("network-b"); + // Call on an explicit reset action, rather than on each normal boot. + if(!device.clear_wifi()) return; +} + +void loop() { device.handle(); } diff --git a/tests/nvs_config/stubs/DNSServer.h b/tests/nvs_config/stubs/DNSServer.h new file mode 100644 index 0000000..6f70f09 --- /dev/null +++ b/tests/nvs_config/stubs/DNSServer.h @@ -0,0 +1 @@ +#pragma once diff --git a/tests/nvs_config/stubs/FS.h b/tests/nvs_config/stubs/FS.h new file mode 100644 index 0000000..1838582 --- /dev/null +++ b/tests/nvs_config/stubs/FS.h @@ -0,0 +1,31 @@ +#pragma once +#include +#include +#include +#include +class File { +public: + File(std::vector* bytes = nullptr) : bytes_(bytes) {} + explicit operator bool() const { return bytes_ != nullptr; } + size_t size() const { return bytes_->size(); } + size_t readBytes(char* out, size_t size) { + size = std::min(size, bytes_->size() - offset_); + memcpy(out, bytes_->data() + offset_, size); + offset_ += size; + return size; + } + void close() { bytes_ = nullptr; } +private: + std::vector* bytes_; + size_t offset_ = 0; +}; +struct FakeFS { + bool mount_ok = true, present = false, remove_ok = true; + int mounts = 0, unmounts = 0, removals = 0; + std::vector bytes; + bool begin(bool format) { if(format) throw "migration must never format"; ++mounts; return mount_ok; } + bool exists(const char*) { return present; } + File open(const char*, const char* mode) { if(strcmp(mode, "r")) throw "legacy FS must be read-only"; return File(present ? &bytes : nullptr); } + bool remove(const char*) { ++removals; if(!remove_ok) return false; present = false; return true; } + void end() { ++unmounts; } +}; diff --git a/tests/nvs_config/stubs/LittleFS.h b/tests/nvs_config/stubs/LittleFS.h new file mode 100644 index 0000000..a69ab55 --- /dev/null +++ b/tests/nvs_config/stubs/LittleFS.h @@ -0,0 +1,3 @@ +#pragma once +#include "FS.h" +extern FakeFS LittleFS; diff --git a/tests/nvs_config/stubs/SPIFFS.h b/tests/nvs_config/stubs/SPIFFS.h new file mode 100644 index 0000000..e16c283 --- /dev/null +++ b/tests/nvs_config/stubs/SPIFFS.h @@ -0,0 +1,3 @@ +#pragma once +#include "FS.h" +extern FakeFS SPIFFS; diff --git a/tests/nvs_config/stubs/WiFi.h b/tests/nvs_config/stubs/WiFi.h new file mode 100644 index 0000000..e3310e5 --- /dev/null +++ b/tests/nvs_config/stubs/WiFi.h @@ -0,0 +1,77 @@ +#pragma once +#include +#include +#include +#include +#include +#include "sdkconfig.h" +using String = std::string; +extern int wifi_status; +extern bool native_wifi_saved; +enum { WL_CONNECTED = 3, WL_NO_SSID_AVAIL = 1, WL_CONNECT_FAILED = 4, WL_DISCONNECTED = 6 }; +enum { WIFI_SCAN_RUNNING = -1, WIFI_AUTH_OPEN = 0, WIFI_AUTH_WPA2_PSK = 3, WIFI_AUTH_WPA2_ENTERPRISE = 5, WIFI_AUTH_MAX = 10 }; +enum { ARDUINO_EVENT_WIFI_STA_DISCONNECTED, WIFI_REASON_NO_AP_FOUND, WIFI_REASON_AUTH_FAIL, + WIFI_REASON_AUTH_EXPIRE, WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT, WIFI_REASON_HANDSHAKE_TIMEOUT, WPA2_AUTH_PEAP }; +using arduino_event_id_t = int; +struct arduino_event_info_t { struct { uint16_t reason; } wifi_sta_disconnected; }; +static const uint32_t INADDR_NONE = 0xffffffff; +struct IPAddress { + uint32_t value; + IPAddress(uint32_t v) : value(v) {} + bool operator==(const IPAddress& b) const { return value == b.value; } +}; +class WiFiClient {}; +struct AvailableAP { + std::string ssid, password; + int32_t rssi; + uint8_t auth; + bool hidden; + AvailableAP(const char* s, const char* p, int32_t r, uint8_t a, bool h = false) + : ssid(s), password(p), rssi(r), auth(a), hidden(h) {} +}; +extern unsigned long fake_millis; +inline unsigned long millis() { fake_millis += 100; return fake_millis; } +inline void delay(unsigned long ms) { fake_millis += ms; } +#define log_e(...) do {} while(0) +#define log_i(...) do {} while(0) +#define log_d(...) do {} while(0) +#define log_v(...) do {} while(0) +struct FakeWiFi { + std::vector available; + std::string selected, last_ssid, last_password, native_ssid, native_password; + int scans = 0, begins = 0, default_begins = 0, disconnects = 0, erases = 0; + uint8_t bssid[6] = {1, 2, 3, 4, 5, 6}; + int status() const { return wifi_status; } + IPAddress localIP() const { return IPAddress(0x01020304); } + template void onEvent(F, int) {} + std::string SSID() const { return selected; } + bool disconnect(bool = false, bool erase = false) { + ++disconnects; + if(erase) { ++erases; native_ssid.clear(); native_password.clear(); native_wifi_saved = false; } + wifi_status = WL_DISCONNECTED; selected.clear(); return true; + } + int begin() { ++default_begins; return begin(native_ssid.c_str(), native_password.c_str()); } + int begin(const char* ssid, const char* password = nullptr, int32_t = 0, const uint8_t* = nullptr, bool = true) { + ++begins; last_ssid = ssid ? ssid : ""; last_password = password ? password : ""; + wifi_status = WL_NO_SSID_AVAIL; + for(const auto& ap : available) { + if(ap.ssid != last_ssid) continue; + wifi_status = ap.password == last_password ? WL_CONNECTED : WL_CONNECT_FAILED; + if(wifi_status == WL_CONNECTED) selected = last_ssid; + break; + } + return wifi_status; + } + int begin(const char* ssid, int, const char*, const char*, const char* password, + const char* = nullptr, const char* = nullptr, const char* = nullptr, + int = -1, int32_t channel = 0, const uint8_t* address = nullptr) { + return begin(ssid, password, channel, address); + } + int8_t scanNetworks(bool = false, bool = false) { ++scans; return static_cast(available.size()); } + void getNetworkInfo(int i, String& ssid, uint8_t& auth, int32_t& rssi, uint8_t*& address, int32_t& channel) { + const auto& ap = available[i]; ssid = ap.hidden ? "" : ap.ssid; + auth = ap.auth; rssi = ap.rssi; address = bssid; channel = 1; + } + void scanDelete() {} +}; +extern FakeWiFi WiFi; diff --git a/tests/nvs_config/stubs/WiFiClientSecure.h b/tests/nvs_config/stubs/WiFiClientSecure.h new file mode 100644 index 0000000..76d3732 --- /dev/null +++ b/tests/nvs_config/stubs/WiFiClientSecure.h @@ -0,0 +1,2 @@ +#pragma once +#include "WiFi.h" diff --git a/tests/nvs_config/stubs/WiFiManager.h b/tests/nvs_config/stubs/WiFiManager.h new file mode 100644 index 0000000..98470b6 --- /dev/null +++ b/tests/nvs_config/stubs/WiFiManager.h @@ -0,0 +1,40 @@ +#pragma once +#include +#include +#include "WiFi.h" +extern int portal_calls; +extern int wifi_status; +extern const char* submitted_user; +extern const char* submitted_device; +extern const char* submitted_credential; +class WiFiManagerParameter { +public: + WiFiManagerParameter(const char* id, const char*, const char* value, int, const char* = "") : id_(id), value_(value) {} + const char* getID() const { return id_.c_str(); } + const char* getValue() const { return value_.c_str(); } + std::string id_, value_; +}; +class WiFiManager { +public: + void setDebugOutput(bool) {} + bool getWiFiIsSaved() { return native_wifi_saved; } + String getWiFiSSID(bool) { return WiFi.native_ssid; } + String getWiFiPass(bool) { return WiFi.native_password; } + void addParameter(WiFiManagerParameter* p) { parameters_.push_back(p); } + bool startConfigPortal(const char*, const char*) { + ++portal_calls; + for(auto p : parameters_) { + if(p->id_ == "user") p->value_ = submitted_user; + if(p->id_ == "device") p->value_ = submitted_device; + if(p->id_ == "credential") p->value_ = submitted_credential; + } + wifi_status = 3; + native_wifi_saved = true; + WiFi.native_ssid = "portal-wifi"; + WiFi.native_password = "portal-password"; + WiFi.selected = WiFi.native_ssid; + return true; + } +private: + std::vector parameters_; +}; diff --git a/tests/nvs_config/stubs/esp32-hal.h b/tests/nvs_config/stubs/esp32-hal.h new file mode 100644 index 0000000..76d3732 --- /dev/null +++ b/tests/nvs_config/stubs/esp32-hal.h @@ -0,0 +1,2 @@ +#pragma once +#include "WiFi.h" diff --git a/tests/nvs_config/stubs/nvs.h b/tests/nvs_config/stubs/nvs.h new file mode 100644 index 0000000..d379ac3 --- /dev/null +++ b/tests/nvs_config/stubs/nvs.h @@ -0,0 +1,12 @@ +#pragma once +#include +#include +typedef int esp_err_t; +typedef unsigned nvs_handle_t; +enum { ESP_OK = 0, ESP_FAIL = -1, ESP_ERR_NVS_NOT_FOUND = 1, ESP_ERR_NVS_INVALID_LENGTH = 2 }; +enum nvs_open_mode_t { NVS_READONLY, NVS_READWRITE }; +esp_err_t nvs_open(const char*, nvs_open_mode_t, nvs_handle_t*); +void nvs_close(nvs_handle_t); +esp_err_t nvs_get_blob(nvs_handle_t, const char*, void*, size_t*); +esp_err_t nvs_set_blob(nvs_handle_t, const char*, const void*, size_t); +esp_err_t nvs_commit(nvs_handle_t); diff --git a/tests/nvs_config/stubs/sdkconfig.h b/tests/nvs_config/stubs/sdkconfig.h new file mode 100644 index 0000000..b451d3a --- /dev/null +++ b/tests/nvs_config/stubs/sdkconfig.h @@ -0,0 +1,6 @@ +#pragma once +#ifndef CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT +#define CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT 1 +#endif +#define CONFIG_LWIP_IPV6 0 +#define CONFIG_ESP_HOSTED_ENABLED 0 diff --git a/tests/nvs_config/stubs/soc/soc_caps.h b/tests/nvs_config/stubs/soc/soc_caps.h new file mode 100644 index 0000000..25330a0 --- /dev/null +++ b/tests/nvs_config/stubs/soc/soc_caps.h @@ -0,0 +1,2 @@ +#pragma once +#define SOC_WIFI_SUPPORTED 1 diff --git a/tests/nvs_config/test.cpp b/tests/nvs_config/test.cpp new file mode 100644 index 0000000..0a5b377 --- /dev/null +++ b/tests/nvs_config/test.cpp @@ -0,0 +1,591 @@ +// Real ThingerWifi, ESP32, WebConfig, PSON and the pinned ESP32 WiFiMulti. +// Only the transport, radio, NVS and filesystem operations are simulated. +#include +#include +#include +#include +#include "FS.h" +#include "nvs.h" +#include "thinger/pson.h" +#include "WiFi.h" + +using namespace protoson; +protoson::dynamic_memory_allocator allocator; +protoson::memory_allocator& protoson::pool = allocator; +FakeFS SPIFFS, LittleFS; +int portal_calls = 0, wifi_status = 3; +bool native_wifi_saved = false; +unsigned long fake_millis = 0; +FakeWiFi WiFi; +const char* submitted_user = "new-user"; +const char* submitted_device = "new-device"; +const char* submitted_credential = "new-credential"; +#define THINGER_DEBUG(a, b) do {} while(0) +#define THINGER_DEBUG_VALUE(a, b, c) do {} while(0) +#define ESP32 +#define _DISABLE_TLS_ +#define THINGER_CLIENT_H +class ThingerClient { +public: + enum THINGER_ERROR { ERROR_UNKNOWN, NETWORK_NOT_CONFIGURED, NETWORK_NOT_FOUND, + NETWORK_AUTH_ERROR, NETWORK_NO_IP }; + template ThingerClient(Client&, const char*, const char*, const char*) {} + void set_error(THINGER_ERROR value) { error = value; } + THINGER_ERROR error = ERROR_UNKNOWN; +}; +#include "ThingerESP32WebConfig.h" + +struct FakeNVS { + bool present = false; + bool fail_open = false, fail_write = false, fail_commit = false, fail_read = false; + std::vector committed, pending; + std::map> other; + std::string pending_key, fail_key; + int handles = 0, writes = 0, commits = 0; +} nv; +esp_err_t nvs_open(const char* name, nvs_open_mode_t mode, nvs_handle_t* h) { + assert(std::string(name) == "thinger"); + if(nv.fail_open) return ESP_FAIL; + if(mode == NVS_READONLY && !nv.present && nv.other.empty()) return ESP_ERR_NVS_NOT_FOUND; + ++nv.handles; *h = 1; return ESP_OK; +} +void nvs_close(nvs_handle_t) { --nv.handles; } +esp_err_t nvs_get_blob(nvs_handle_t, const char* key, void* out, size_t* size) { + const std::vector* record; + if(std::string(key) == "config") { + if(!nv.present) return ESP_ERR_NVS_NOT_FOUND; + record = &nv.committed; + } else { + auto it = nv.other.find(key); + if(it == nv.other.end()) return ESP_ERR_NVS_NOT_FOUND; + record = &it->second; + } + if(out && nv.fail_read && (nv.fail_key.empty() || nv.fail_key == key)) return ESP_FAIL; + if(out && *size < record->size()) return ESP_ERR_NVS_INVALID_LENGTH; + if(out) memcpy(out, record->data(), record->size()); + *size = record->size(); return ESP_OK; +} +esp_err_t nvs_set_blob(nvs_handle_t, const char* key, const void* bytes, size_t size) { + ++nv.writes; + if(nv.fail_write && (nv.fail_key.empty() || nv.fail_key == key)) return ESP_FAIL; + nv.pending_key = key; + nv.pending.assign(static_cast(bytes), static_cast(bytes) + size); + return ESP_OK; +} +esp_err_t nvs_commit(nvs_handle_t) { + ++nv.commits; + if(nv.fail_commit && (nv.fail_key.empty() || nv.fail_key == nv.pending_key)) return ESP_FAIL; + if(nv.pending_key == "config") { nv.committed = nv.pending; nv.present = true; } + else nv.other[nv.pending_key] = nv.pending; + return ESP_OK; +} +class Device : public ThingerESP32WebConfig { +public: + using ThingerESP32WebConfig::load_configuration; + using ThingerESP32WebConfig::save_configuration; + using ThingerESP32WebConfig::configuration_storage_error; + using ThingerESP32WebConfig::connect_network; + using ThingerESP32WebConfig::ThingerESP32WebConfig; + size_t network_count() const { return wifi_networks_; } +}; +using Store = thinger_config::nvs_config; +FakeFS& legacy() { +#ifdef THINGER_ESP32_CONFIG_USE_LITTLEFS + return LittleFS; +#else + return SPIFFS; +#endif +} +void reset() { + assert(nv.handles == 0); + nv = FakeNVS(); SPIFFS = FakeFS(); LittleFS = FakeFS(); + portal_calls = 0; wifi_status = WL_CONNECTED; + native_wifi_saved = false; WiFi = FakeWiFi(); fake_millis = 0; +} +std::vector encode(pson& config) { + pson_encoder counter; counter.encode(config); + std::vector bytes(counter.bytes_written()); + thinger_config::buffer_encoder encoder(bytes.data(), bytes.size()); encoder.encode(config); + assert(encoder.valid()); return bytes; +} +void seed() { + pson original; + original["user"] = "legacy-user"; + original["device"] = "legacy-device"; + original["credential"] = "legacy-secret"; + original["ssid"] = "legacy-wifi"; + original["password"] = "legacy-password"; + original["custom"]["threshold"] = 42; + original["custom"]["enabled"] = true; + original["empty"] = ""; + uint8_t bytes[] = {0, 1, 2, 255}; original["binary"].set_bytes(bytes, sizeof(bytes)); + legacy().bytes = encode(original); legacy().present = true; +} +void test_migration() { + reset(); seed(); const auto expected = legacy().bytes; + Device d; pson config; assert(d.load_configuration(config)); + assert(!legacy().present && legacy().removals == 1); + assert(legacy().mounts == legacy().unmounts); + assert(std::vector(nv.committed.begin()+8, nv.committed.end()) == expected); + assert(encode(config) == expected); + assert(std::string((const char*)config["ssid"]) == "legacy-wifi"); + assert((int)config["custom"]["threshold"] == 42); + // Simulate reboot with a stale legacy file and a broken filesystem. + seed(); legacy().mount_ok = false; int mounts = legacy().mounts; + Device reboot; pson loaded; assert(reboot.load_configuration(loaded)); + assert(legacy().mounts == mounts && encode(loaded) == expected); + assert(nv.writes == 1); // Reads/reconnections do not rewrite flash. +} +void test_failures() { + for(int failure = 0; failure != 4; ++failure) { + reset(); seed(); + nv.fail_open = failure == 0; nv.fail_write = failure == 1; + nv.fail_commit = failure == 2; nv.fail_read = failure == 3; + Device d; assert(!d.connect_network()); + assert(d.configuration_storage_error() && legacy().present && portal_calls == 0); + assert(nv.handles == 0); + nv.fail_open = nv.fail_write = nv.fail_commit = nv.fail_read = false; + Device reboot; pson config; assert(reboot.load_configuration(config)); + assert(std::string((const char*)config["user"]) == "legacy-user"); + } +} +void test_cleanup_and_reset() { + reset(); seed(); legacy().remove_ok = false; + Device d; pson config; assert(d.load_configuration(config)); + assert(legacy().present); int mounts = legacy().mounts; + assert(d.clean_credentials()); + Device reboot; pson empty; assert(!reboot.load_configuration(empty)); + assert(!reboot.configuration_storage_error() && legacy().mounts == mounts); + assert(Store::load(empty) == Store::result::cleared); + // Failure to clear preserves the previous committed config. + assert(d.save_configuration(config)); auto committed = nv.committed; + nv.fail_commit = true; assert(!d.clean_credentials()); assert(nv.committed == committed); +} +void test_bad_data() { + reset(); seed(); legacy().bytes.pop_back(); + Device d; pson config; assert(!d.load_configuration(config)); + assert(d.configuration_storage_error() && legacy().present && !nv.present); + reset(); seed(); legacy().bytes.resize(THINGER_NVS_CONFIG_MAX_SIZE+1); + Device large; assert(!large.load_configuration(config)); assert(legacy().present); + reset(); seed(); nv.present = true; nv.committed = {1, 2, 3}; + Device corrupt; assert(!corrupt.load_configuration(config)); + assert(corrupt.configuration_storage_error() && legacy().mounts == 0); + reset(); seed(); legacy().mount_ok = false; + Device unreadable; assert(!unreadable.connect_network()); + assert(legacy().present && portal_calls == 0 && nv.writes == 0); +} +void test_portal_preserves_custom_fields() { + reset(); seed(); Device d; assert(d.connect_network()); + assert(portal_calls == 1); + pson loaded; assert(Store::load(loaded) == Store::result::ready); + assert((int)loaded["custom"]["threshold"] == 42); + assert(std::string((const char*)loaded["password"]) == "legacy-password"); + reset(); nv.fail_write = true; Device failed; + assert(!failed.connect_network() && failed.configuration_storage_error()); +} +void test_new_device() { + reset(); Device d; pson config; + assert(!d.load_configuration(config) && !d.configuration_storage_error()); + assert(d.connect_network()); assert(portal_calls == 1); + assert(Store::load(config) == Store::result::ready); + assert(std::string((const char*)config["user"]) == submitted_user); +} + +class WifiDevice : public ThingerESP32 { +public: + WifiDevice() : ThingerESP32("user", "device", "credential") {} + using ThingerESP32::connect_network; + size_t network_count() const { return wifi_networks_; } +}; + +void reboot_radio() { + WiFi = FakeWiFi(); native_wifi_saved = false; wifi_status = WL_DISCONNECTED; +} + +void test_wifi_reboot_and_selection() { + reset(); + { + WifiDevice first; + char ssid[] = "network-a", password[] = "password-a"; + first.add_wifi(ssid, password); assert(first.wifi_storage_ok()); + memset(ssid, 'x', strlen(ssid)); memset(password, 'x', strlen(password)); + first.add_wifi("network-b", "password-b"); assert(first.wifi_storage_ok()); + first.add_wifi("open-network"); assert(first.wifi_storage_ok()); + assert(first.network_count() == 3); + const int writes = nv.writes; + first.add_wifi("network-a", "password-a"); + first.add_wifi("open-network", ""); + assert(nv.writes == writes && first.network_count() == 3); + } + const int writes = nv.writes; + reboot_radio(); + WifiDevice reboot; + WiFi.available = {{"network-a", "password-a", -70, WIFI_AUTH_WPA2_PSK}, + {"network-b", "password-b", -40, WIFI_AUTH_WPA2_PSK}, + {"unregistered", "", -10, WIFI_AUTH_OPEN}}; + assert(reboot.connect_network()); + assert(reboot.network_count() == 3 && WiFi.selected == "network-b"); + assert(WiFi.scans > 0 && WiFi.default_begins == 0 && nv.writes == writes); + // The preferred AP disappears: the real WiFiMulti must select another. + WiFi.disconnect(); WiFi.available.erase(WiFi.available.begin()+1); + assert(reboot.connect_network() && WiFi.selected == "network-a"); + WiFi.disconnect(); WiFi.available = {{"open-network", "", -50, WIFI_AUTH_OPEN}}; + assert(reboot.connect_network() && WiFi.selected == "open-network"); + assert(WiFi.last_password.empty()); +} + +void test_wifi_password_update_and_single_network() { + reset(); + { + WifiDevice first; + first.add_wifi("hidden-network", "old-password"); + first.add_wifi("hidden-network", "new-password"); + assert(first.wifi_storage_ok() && first.network_count() == 1); + const int writes = nv.writes; + first.add_wifi("hidden-network", "new-password"); assert(nv.writes == writes); + } + reboot_radio(); WifiDevice reboot; + WiFi.available = {{"hidden-network", "new-password", -50, WIFI_AUTH_WPA2_PSK, true}}; + assert(reboot.connect_network() && reboot.network_count() == 1); + assert(WiFi.scans == 0 && WiFi.last_password == "new-password" && WiFi.default_begins == 0); + const auto saved = nv.other["wifi"]; + reboot.add_wifi("", "invalid"); assert(!reboot.wifi_storage_ok()); + assert(nv.other["wifi"] == saved); + reboot.add_wifi(nullptr); assert(!reboot.wifi_storage_ok()); + assert(reboot.restore_wifi_networks()); + // Repeating setup after reboot does not duplicate or erase other networks. + reboot.add_wifi("hidden-network", "new-password"); + reboot.add_wifi("another-network", "another-password"); + assert(reboot.wifi_storage_ok() && reboot.network_count() == 2); +} + +void test_wifi_write_failures() { + for(int failure = 0; failure != 3; ++failure) { + reset(); WifiDevice first; + first.add_wifi("old-network", "old-password"); assert(first.wifi_storage_ok()); + const auto saved = nv.other["wifi"]; + nv.fail_key = "wifi"; + nv.fail_write = failure == 0; nv.fail_commit = failure == 1; nv.fail_read = failure == 2; + first.add_wifi("new-network", "new-password"); + assert(!first.wifi_storage_ok() && first.network_count() == 1); + if(failure != 2) assert(nv.other["wifi"] == saved); + nv.fail_write = nv.fail_commit = nv.fail_read = false; + reboot_radio(); WifiDevice reboot; + assert(reboot.restore_wifi_networks()); + // A failed verification can follow a successful commit; reboot trusts + // that complete new record, never a partial list. + assert(reboot.network_count() == (failure == 2 ? 2u : 1u)); + assert(nv.handles == 0); + } +} + +void test_wifi_cold_read_failure_and_retry() { + reset(); { WifiDevice first; first.add_wifi("network-a", "password-a"); } + reboot_radio(); WifiDevice reboot; + nv.fail_key = "wifi"; nv.fail_read = true; + WiFi.available = {{"network-a", "password-a", -40, WIFI_AUTH_WPA2_PSK}}; + assert(!reboot.connect_network() && !reboot.wifi_storage_ok() && WiFi.begins == 0); + nv.fail_read = false; + assert(reboot.connect_network() && reboot.wifi_storage_ok()); + pson incompatible; incompatible["version"] = 2; + (void)static_cast(incompatible["networks"]); + assert(Store::save(incompatible, "wifi")); + reboot_radio(); WifiDevice invalid; + assert(!invalid.connect_network() && !invalid.wifi_storage_ok() && WiFi.begins == 0); +} + +void test_explicit_credentials_without_pson() { + reset(); Device first("user", "device", "credential"); + first.add_wifi("network-a", "password-a"); + first.add_wifi("network-b", "password-b"); + WiFi.available = {{"network-b", "password-b", -40, WIFI_AUTH_WPA2_PSK}}; + assert(first.connect_network() && portal_calls == 0 && !nv.present); + reboot_radio(); Device reboot("user", "device", "credential"); + WiFi.available = {{"network-a", "password-a", -40, WIFI_AUTH_WPA2_PSK}}; + assert(reboot.connect_network() && portal_calls == 0 && reboot.network_count() == 2); +} + +void test_portal_wifi_reboot() { + reset(); + { Device first; assert(first.connect_network()); assert(portal_calls == 1); } + thinger_config::wifi_store::networks networks; + assert(thinger_config::wifi_store::load(networks)); + assert(networks.size() == 1 && networks[0].ssid == "portal-wifi"); + reboot_radio(); Device reboot; + WiFi.available = {{"portal-wifi", "portal-password", -40, WIFI_AUTH_WPA2_PSK}}; + assert(reboot.connect_network() && portal_calls == 1 && WiFi.selected == "portal-wifi"); +} + +void test_complete_migration_with_multiple_wifi() { + reset(); seed(); const auto expected = legacy().bytes; + { + Device first; + // Registering Wi-Fi first must not suppress the separate PSON import. + first.add_wifi("network-a", "password-a"); + first.add_wifi("network-b", "password-b"); + WiFi.available = {{"network-b", "password-b", -40, WIFI_AUTH_WPA2_PSK}}; + assert(first.connect_network() && portal_calls == 0); + assert(!legacy().present && first.network_count() == 2); + } + assert(std::vector(nv.committed.begin()+8, nv.committed.end()) == expected); + const auto wifi_record = nv.other["wifi"]; + seed(); legacy().mount_ok = false; const int mounts = legacy().mounts; + reboot_radio(); Device reboot; + WiFi.available = {{"network-a", "password-a", -40, WIFI_AUTH_WPA2_PSK}}; + assert(reboot.connect_network() && portal_calls == 0); + assert(reboot.network_count() == 2 && WiFi.selected == "network-a"); + assert(legacy().mounts == mounts && nv.other["wifi"] == wifi_record); + pson config; assert(Store::load(config) == Store::result::ready); + assert(encode(config) == expected); + // Invalid Wi-Fi records must not reopen the portal or access legacy FS. + nv.other["wifi"] = {0, 1, 2}; reboot_radio(); Device corrupt; + assert(!corrupt.connect_network() && portal_calls == 0 && legacy().mounts == mounts); + assert(nv.committed.size() == expected.size()+8); +} + +void test_enterprise_coexistence() { +#if CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT + reset(); WifiDevice first; + first.add_wifi_enterprise("enterprise", "username", "enterprise-password"); + first.add_wifi("personal", "personal-password"); + first.add_wifi("personal", "updated-password"); + assert(first.network_count() == 2 && first.wifi_storage_ok()); + WiFi.available = {{"enterprise", "enterprise-password", -40, WIFI_AUTH_WPA2_ENTERPRISE}}; + assert(first.connect_network() && WiFi.selected == "enterprise"); + thinger_config::wifi_store::networks networks; + assert(thinger_config::wifi_store::load(networks) && networks.size() == 1); + reboot_radio(); WifiDevice reboot; assert(reboot.restore_wifi_networks()); + assert(reboot.network_count() == 1); // add_wifi_enterprise remains RAM-only. +#endif +} + +void test_list_and_remove_wifi() { + reset(); WifiDevice first; + std::vector ssids = {"sentinel"}; + assert(first.list_wifi(ssids) && ssids.empty() && nv.writes == 0); + first.add_wifi("network-a", "password-a"); + first.add_wifi("network-b", "password-b"); + first.add_wifi("network-c"); + const int writes = nv.writes; + assert(first.list_wifi(ssids)); + assert((ssids == std::vector{"network-a", "network-b", "network-c"})); + assert(nv.writes == writes); + ssids[0] = "modified-snapshot"; + assert(first.list_wifi(ssids) && ssids[0] == "network-a"); + assert(!first.remove_wifi("Network-b") && !first.remove_wifi(nullptr) && !first.remove_wifi("")); + assert(first.wifi_storage_ok() && nv.writes == writes); + WiFi.selected = "network-a"; + assert(first.remove_wifi("network-b") && first.network_count() == 2); + assert(WiFi.selected == "network-a" && WiFi.disconnects == 0); + assert(!first.remove_wifi("network-b") && first.wifi_storage_ok() && nv.writes == writes+1); + reboot_radio(); WifiDevice reboot; + assert(reboot.list_wifi(ssids)); + assert((ssids == std::vector{"network-a", "network-c"})); + WiFi.available = {{"network-b", "password-b", -10, WIFI_AUTH_WPA2_PSK}, + {"network-a", "password-a", -70, WIFI_AUTH_WPA2_PSK}, + {"network-c", "", -40, WIFI_AUTH_OPEN}}; + assert(reboot.connect_network() && WiFi.selected == "network-c"); +} + +void test_remove_active_and_last_wifi() { + reset(); WifiDevice first; + pson config; config["custom"]["threshold"] = 42; assert(Store::save(config)); + const auto saved_config = nv.committed; + first.add_wifi("network-a", "password-a"); + first.add_wifi("network-b", "password-b"); + WiFi.available = {{"network-a", "password-a", -40, WIFI_AUTH_WPA2_PSK}, + {"network-b", "password-b", -70, WIFI_AUTH_WPA2_PSK}}; + assert(first.connect_network() && WiFi.selected == "network-a"); + const int disconnects = WiFi.disconnects; + assert(first.remove_wifi("network-a") && first.network_count() == 1); + assert(WiFi.disconnects == disconnects+1 && WiFi.erases == 1); + const int scans = WiFi.scans; + assert(first.connect_network() && WiFi.selected == "network-b"); + assert(WiFi.scans == scans); // The direct-connect pointer now refers to B. + assert(first.remove_wifi("network-b") && first.network_count() == 0); + assert(WiFi.erases == 2 && nv.other.count("wifi") == 1); + assert(nv.committed == saved_config); // PSON data is never rewritten. + reboot_radio(); WifiDevice reboot; + // Even if a driver credential survived independently, do not resurrect it. + native_wifi_saved = true; WiFi.native_ssid = "network-b"; WiFi.native_password = "password-b"; + WiFi.available = {{"network-b", "password-b", -40, WIFI_AUTH_WPA2_PSK}}; + std::vector ssids; + assert(reboot.list_wifi(ssids) && ssids.empty()); + assert(!reboot.connect_network() && WiFi.default_begins == 0 && WiFi.begins == 0); + assert(reboot.error == ThingerClient::NETWORK_NOT_CONFIGURED); + // Explicitly registering another network still works after an empty list. + reboot.add_wifi("network-c"); + assert(reboot.wifi_storage_ok() && reboot.list_wifi(ssids) && ssids.size() == 1); +} + +void test_remove_failure_and_list_error() { + for(int failure = 0; failure != 3; ++failure) { + reset(); WifiDevice first; + first.add_wifi("network-a", "password-a"); first.add_wifi("network-b", "password-b"); + const auto saved = nv.other["wifi"]; + WiFi.selected = "network-a"; + nv.fail_key = "wifi"; + nv.fail_write = failure == 0; nv.fail_commit = failure == 1; nv.fail_read = failure == 2; + assert(!first.remove_wifi("network-a") && !first.wifi_storage_ok()); + assert(first.network_count() == 2 && WiFi.disconnects == 0); + if(failure != 2) assert(nv.other["wifi"] == saved); + nv.fail_write = nv.fail_commit = nv.fail_read = false; + reboot_radio(); WifiDevice reboot; + std::vector ssids; + assert(reboot.list_wifi(ssids) && ssids.size() == (failure == 2 ? 1u : 2u)); + assert(ssids.back() == "network-b"); + } + reset(); { WifiDevice first; first.add_wifi("network-a", "password-a"); } + nv.fail_key = "wifi"; nv.fail_read = true; WifiDevice reboot; + std::vector ssids = {"preserved-output"}; + const int writes = nv.writes; + assert(!reboot.list_wifi(ssids) && !reboot.wifi_storage_ok()); + assert(ssids.size() == 1 && ssids[0] == "preserved-output" && nv.writes == writes); + nv.fail_read = false; + assert(reboot.list_wifi(ssids) && ssids[0] == "network-a"); +} + +void test_webconfig_after_last_removal() { + reset(); + { WifiDevice first; first.add_wifi("deleted-network", "deleted-password"); assert(first.remove_wifi("deleted-network")); } + reboot_radio(); native_wifi_saved = true; + WiFi.native_ssid = "deleted-network"; WiFi.native_password = "deleted-password"; + WiFi.available = {{"deleted-network", "deleted-password", -40, WIFI_AUTH_WPA2_PSK}}; + Device reboot("user", "device", "credential"); + assert(reboot.connect_network() && portal_calls == 1 && WiFi.default_begins == 0); + std::vector ssids; + assert(reboot.list_wifi(ssids) && ssids.size() == 1 && ssids[0] == "portal-wifi"); +} + +void test_legacy_native_fallback_without_registry() { + reset(); reboot_radio(); + WiFi.native_ssid = "legacy-network"; WiFi.native_password = "legacy-password"; + WiFi.available = {{"legacy-network", "legacy-password", -40, WIFI_AUTH_WPA2_PSK}}; + WifiDevice first; + assert(first.connect_network() && WiFi.default_begins == 1 && nv.other.empty()); +} + +void test_remove_preserves_enterprise() { +#if CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT + reset(); WifiDevice first; + first.add_wifi_enterprise("enterprise", "username", "enterprise-password"); + first.add_wifi("personal", "personal-password"); + assert(first.remove_wifi("personal") && first.network_count() == 1); + std::vector ssids; + assert(first.list_wifi(ssids) && ssids.empty()); + assert(!first.remove_wifi("enterprise") && first.wifi_storage_ok()); + WiFi.available = {{"enterprise", "enterprise-password", -40, WIFI_AUTH_WPA2_ENTERPRISE}}; + assert(first.connect_network() && WiFi.selected == "enterprise"); +#endif +} + +void test_clear_wifi_preserves_other_settings() { + reset(); seed(); + assert(Store::save_legacy(legacy().bytes.data(), legacy().bytes.size())); + const auto config = nv.committed; + nv.other["unrelated"] = {9, 8, 7}; + WifiDevice first; + first.add_wifi("network-a", "password-a"); + first.add_wifi("network-b", "password-b"); first.add_wifi("open-network"); + WiFi.available = {{"network-b", "password-b", -40, WIFI_AUTH_WPA2_PSK}}; + assert(first.connect_network() && WiFi.selected == "network-b"); + const int erases = WiFi.erases; + assert(first.clear_wifi() && first.wifi_storage_ok() && first.network_count() == 0); + assert(WiFi.erases == erases+1 && wifi_status == WL_DISCONNECTED); + std::vector ssids = {"sentinel"}; + assert(first.list_wifi(ssids) && ssids.empty()); + assert(nv.committed == config && (nv.other["unrelated"] == std::vector{9, 8, 7})); + assert(nv.other.count("wifi") == 1 && !nv.other["wifi"].empty()); + reboot_radio(); WifiDevice reboot; + native_wifi_saved = true; WiFi.native_ssid = "network-b"; WiFi.native_password = "password-b"; + WiFi.available = {{"network-b", "password-b", -40, WIFI_AUTH_WPA2_PSK}}; + assert(reboot.list_wifi(ssids) && ssids.empty()); + assert(!reboot.connect_network() && WiFi.begins == 0 && WiFi.default_begins == 0); + assert(reboot.clear_wifi() && reboot.list_wifi(ssids) && ssids.empty()); + assert(nv.committed == config && legacy().present); // Never reads/deletes the old PSON file. + reboot.add_wifi("new-network", "new-password"); + assert(reboot.wifi_storage_ok() && reboot.list_wifi(ssids) && ssids.size() == 1 && ssids[0] == "new-network"); +} + +void test_clear_wifi_without_a_registry() { + reset(); WifiDevice first; + native_wifi_saved = true; WiFi.selected = WiFi.native_ssid = "legacy-network"; + WiFi.native_password = "legacy-password"; + assert(first.clear_wifi() && WiFi.erases == 1 && nv.other.count("wifi") == 1); + reboot_radio(); WifiDevice reboot; + assert(!reboot.connect_network() && WiFi.default_begins == 0); + assert(reboot.clear_wifi()); // Clearing an already empty registry succeeds. +} + +void test_clear_wifi_failures_and_corrupt_record() { + for(int failure = 0; failure != 4; ++failure) { + reset(); WifiDevice first; + first.add_wifi("network-a", "password-a"); first.add_wifi("network-b", "password-b"); + const auto old = nv.other["wifi"]; + WiFi.selected = "network-a"; + nv.fail_key = "wifi"; + nv.fail_open = failure == 0; nv.fail_write = failure == 1; + nv.fail_commit = failure == 2; nv.fail_read = failure == 3; + assert(!first.clear_wifi() && !first.wifi_storage_ok()); + assert(first.network_count() == 2 && WiFi.disconnects == 0); + if(failure != 3) assert(nv.other["wifi"] == old); + nv.fail_open = nv.fail_write = nv.fail_commit = nv.fail_read = false; + reboot_radio(); WifiDevice reboot; std::vector ssids; + assert(reboot.list_wifi(ssids) && ssids.size() == (failure == 3 ? 0u : 2u)); + assert(nv.handles == 0); + } + reset(); seed(); assert(Store::save_legacy(legacy().bytes.data(), legacy().bytes.size())); + const auto config = nv.committed; nv.other["wifi"] = {0, 1, 2}; + WifiDevice corrupt; std::vector ssids = {"sentinel"}; + assert(!corrupt.list_wifi(ssids) && ssids[0] == "sentinel"); + assert(corrupt.clear_wifi() && corrupt.wifi_storage_ok()); + assert(corrupt.list_wifi(ssids) && ssids.empty() && nv.committed == config); +} + +void test_clear_wifi_preserves_enterprise() { +#if CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT + reset(); WifiDevice first; + first.add_wifi_enterprise("enterprise", "username", "enterprise-password"); + first.add_wifi("personal", "personal-password"); + WiFi.available = {{"enterprise", "enterprise-password", -40, WIFI_AUTH_WPA2_ENTERPRISE}}; + assert(first.connect_network()); + const int disconnects = WiFi.disconnects; + assert(first.clear_wifi() && first.network_count() == 1 && WiFi.disconnects == disconnects); + std::vector ssids; + assert(first.list_wifi(ssids) && ssids.empty() && first.connect_network()); + assert(WiFi.selected == "enterprise"); + reset(); WifiDevice with_ca; + with_ca.add_wifi_enterprise("enterprise-ca", "username", "enterprise-password", "", "test-ca"); + with_ca.add_wifi("personal", "personal-password"); + WiFi.available = {{"enterprise-ca", "enterprise-password", -40, WIFI_AUTH_WPA2_ENTERPRISE}}; + assert(with_ca.connect_network()); + const int ca_disconnects = WiFi.disconnects; + assert(with_ca.clear_wifi() && WiFi.disconnects == ca_disconnects); + assert(with_ca.has_wifi_networks() && with_ca.connect_network() && WiFi.selected == "enterprise-ca"); +#endif +} + +int main() { + test_wifi_reboot_and_selection(); test_wifi_password_update_and_single_network(); + test_wifi_write_failures(); test_enterprise_coexistence(); test_portal_wifi_reboot(); + test_wifi_cold_read_failure_and_retry(); test_explicit_credentials_without_pson(); + test_list_and_remove_wifi(); test_remove_active_and_last_wifi(); + test_remove_failure_and_list_error(); test_webconfig_after_last_removal(); test_remove_preserves_enterprise(); + test_legacy_native_fallback_without_registry(); + test_clear_wifi_preserves_other_settings(); test_clear_wifi_without_a_registry(); + test_clear_wifi_failures_and_corrupt_record(); test_clear_wifi_preserves_enterprise(); + std::cout << "PASS: clear_wifi persistent empty registry, other settings/enterprise preserved, failures and recovery\n"; + std::cout << "PASS: SSID snapshots, persistent deletion, active/last removal, no native resurrection, failure recovery\n"; + std::cout << "PASS: real WiFiMulti restore/selection/fallback/open/hidden/update, NVS failures, portal integration\n"; +#ifdef THINGER_ESP32_CONFIG_SKIP_LEGACY_MIGRATION + reset(); legacy().mount_ok = false; Device d; + assert(d.connect_network() && legacy().mounts == 0); + std::cout << "PASS: fresh NVS-only device\n"; +#else + test_migration(); test_failures(); test_cleanup_and_reset(); + test_bad_data(); test_portal_preserves_custom_fields(); test_new_device(); + test_complete_migration_with_multiple_wifi(); + std::cout << "PASS: migration, reboot, write/commit/read errors, cleanup, reset, invalid data, custom fields, provisioning\n"; +#endif + assert(nv.handles == 0); +}